推薦系統(三)Graph Embedding之LINE

上一篇博客推薦系統(二)Graph Embedding之DeepWalk中講到Graph Embedding的開山之作DeepWalk,該博客講述了在圖結構上進行RandomWalk獲取訓練樣本,並通過Word2Vec模型來訓練得到圖中每個節點的Embedding向量。

但DeepWalk有兩個比較大的問題:

  1. DeepWalk將問題抽象成無權圖,這意味着高頻次user behavior路徑和低頻次user behavior路徑在模型看來是等價的。
  2. 所處環境相似但不直連的兩個節點沒有進行特殊的處理,這類節點的Embedding向量本應比較相似,而這類信息是沒有辦法通過深度優先遍歷的方式來獲取的,只能通過寬度優先遍歷的方式來獲取,使得這類節點的Embedding向量相去甚遠。

上述兩點缺陷正是本篇博客提到的LINE算法所關注的。本篇博客着重講述LINE算法的模型構建和模型優化兩個過程。在模型構建階段,在圖結構中隨機提取指定條數的邊作爲訓練樣本集合,之後對於訓練集合中的每條邊,採用First/Second-order Proximity作爲模型的兩種構建手段,並利用相對熵作爲損失函數。在模型優化階段,採用負採樣變更損失函數+alias邊採樣的方式進行加速運算。

關鍵字: First-order Proximity,Second-order Proximity,相對熵,Edge Sampling

如下是本篇博客的主要內容:

  • First/Second-order Proximity
  • Model Optimization
  • 代碼實現
  • 總結

1. First/Second-order Proximity

LINE算法將user behavior 抽象爲有權圖,即每個邊都帶有權重,如下圖所示。這樣就能區分出高頻次user behavior路徑和低頻次user behavior路徑,相當於把原先DeepWalk中缺失的信息補充上。
在這裏插入圖片描述
而只將無權圖變爲有權圖還遠遠不夠,如何利用這部分信息纔是關鍵。一般情況下如果圖中兩個節點有直接的邊向量,且邊的權值較大,則有理由相信這兩個節點的Embedding向量的距離需要足夠近,這就是論文中提到的First-order Proximity。而對於兩個環境較爲相似但不直連的節點,即他們共享很多相同的鄰居,他們的Embedding向量也應該比較相似纔對,就如下圖中的節點5和節點6一樣。對於這類節點的建模,即論文中提到的Second-order Proximity。如下會對這兩種建模思想進行介紹。
在這裏插入圖片描述
設user behavior圖結構爲(V,E)(V,E),其節點用viv_i來表示,節點自身Embedding向量用ui\vec{u_i}來表示,而當viv_i被作爲上下文節點時,則其Embedding向量用ui\vec{u_i}'表示,例如上圖中2節點本身的Embedding向量爲u2\vec{u_2},而如果其作爲5或者6的相鄰節點時,其上下文Embedding向量爲u2\vec{u_2}'。對於圖的邊(vi,vj)(v_i,v_j),設邊的權值爲wi,jw_{i, j}

1.1 First-order Proximity

對於圖中直連邊(vi,vj)(v_i,v_j),模型預測概率爲:
p1(vi,vj)=11+exp(uiTuj)p_1(v_i, v_j)=\frac{1}{1+exp(-\vec{u_i}^T\cdot\vec{u_j})}

可以看出這個預測概率其實就是sigmoid函數,而這條邊出現的真實概率爲
p^1(vi,vj)=wi,jW,W=(i,j)Ewi,j\hat{p}_1(v_i, v_j)=\frac{w_{i,j}}{W}, W=\sum_{(i,j)\in E}w_{i,j}

模型的目標是使預測概率和真實概率的分佈儘量相近,即使得p1(vi,vj)p_1(v_i, v_j)p^1(vi,vj)\hat{p}_1(v_i, v_j)的KL散度儘量小,不熟悉KL散度的小夥伴可以參考這裏,這個大神講的真的非常的清楚。而這裏省略了WW這個常數項,模型的目標如下所示:
O1=(i,j)Ewi,jlogp1(vi,vj)O_1=-\sum_{(i,j)\in E}w_{i,j}logp_1(v_i,v_j)

1.2 Second-order Proximity

對於直連的邊(vi,vj)(v_i,v_j),直接給出模型預測的已知viv_i時的條件概率爲:
p2(vjvi)=exp(ujTui)k=1Vexp(ukTui)p_2(v_j|v_i)=\frac{exp(-\vec{u_j}'^T\cdot\vec{u_i})}{\sum_{k=1}^{|V|}exp(-\vec{u_k}'^T\cdot\vec{u_i})}

可以看出這個預測概率也是類似sigmoid函數,這條邊的真實條件概率變爲如下公式,其中did_i表示節點viv_i的出度,
p^2(vjvi)=wi,jdi\hat{p}_2(v_j|v_i)=\frac{w_{i,j}}{d_i}

這時模型的目標和First-order Proximity類似,即使得p2(vjvi)p_2(v_j|v_i)儘量接近於p^2(vjvi)\hat{p}_2(v_j|v_i),經過簡化,模型的目標爲:
O2=(i,j)Ewi,jlogp2(vjvi)O_2=-\sum_{(i,j)\in E}w_{i,j}logp_2(v_j|v_i)

可能很多人在這裏都會有所疑惑,爲什麼Second-order Proximity能夠使環境相似但不直連節點的Embedding向量距離相近,而且原始論文中也沒有提到。這裏我的理解是這樣的:假設viv_ivmv_m是符合上述條件的兩個節點,他們都連接着vjv_j,則p2(vjvi)p_2(v_j|v_i)p2(vjvm)p_2(v_j|v_m)兩個式子只有ui\vec{u_i}um\vec{u_m}是不同的,且p^2(vjvi)\hat{p}_2(v_j|v_i)p^2(vjvm)\hat{p}_2(v_j|v_m)都爲1,則這樣模型學習出來的結果必然會讓ui\vec{u_i}um\vec{u_m}比較相近。

2. Model Optimization

2.1 負採樣更改損失函數

計算Second-order Proximity的損失函數時,可以看出模型有一個比較大的問題,就是每次求p2(vjvi)p_2(v_j|v_i)時都要遍歷圖中的所有節點,這樣是非常耗時的,論文中引入負採樣的方式,即在計算p2(vjvi)p_2(v_j|v_i)表示公式的分母的時候,並不需要遍歷所有的節點,而是選取K個負邊進行計算,公式如下所示:
logσ(ujTui)+i=1KEvnPn(v)[logσ(unTui)]log\sigma(\vec{u_j}'^T\cdot\vec{u_i})+\sum_{i=1}^{K}E_{v_n \sim P_n(v)}[log\sigma(\vec{u_n}'^T\cdot\vec{u_i})]

2.2 alias採樣

2.1節中損失函數的計算效率問題已經解決,但是現在又有一個問題,即隨機梯度下降過程中梯度不穩定的問題,因爲p1(vi,vj)p_1(v_i, v_j)p2(vjvi)p_2(v_j|v_i)的計算公式中都有wi,jw_{i,j},梯度計算公式中都含有wi,jw_{i,j},拿p2(vjvi)p_2(v_j|v_i)的梯度計算舉例,有如下公式,wi,jw_{i,j}過大則會導致梯度爆炸,wi,jw_{i,j}過小則會導致梯度消失。
O2ui=wi,jlogp2(vjvi)ui\frac{\partial O_2}{\partial \vec{u_i}}=w_{i,j} \cdot \frac{\partial logp_2(v_j|v_i)}{\partial \vec{u_i}}

針對上述情況,論文提出一種解決方案,使得有權圖變爲無權圖,即使所有的wi,jw_{i,j}都變爲1,但是在採樣的時候要根據原先每條邊的權值大小調整採樣概率,例如一個權重爲5的邊要比一個權重爲1的邊被採到的概率大,這時就需要選擇一種合適的採樣策略,使得采樣後的數據和原先數據的分佈儘量相似,這裏就用到了大名鼎鼎的alias採樣,不熟悉的小夥伴可以參考這裏

3. 代碼實現

和DeepWalk類似,這裏依然援引知乎淺夢大神的github代碼,這裏分享下我對其LINE代碼實現的兩點見解。

  1. 實現負採樣的代碼,思路比較新穎,在line.py的函數batch_iter中,對於一個batch的正樣本集合,與之搭配negative_ratio個batch的負樣本集合來進行學習,整個過程只是通過mod這一個變量進行控制的,思路真的很棒。

  2. 針對負採樣,我這邊有一點個人的見解,原先負採樣的思路是針對一個節點viv_i,先隨機採一個batch的與viv_i直連的節點,與viv_i拼接在一起構成正樣本集合,之後隨機採若干個batch的節點,與viv_i拼接在一起構成負樣本集合。這個做法的問題在於如果這個負樣本集合中有與viv_i直連的節點vjv_j構成的邊(vi,vj)(v_i,v_j),則會使模型的學習變得艱難,因爲在採集正樣本的時候已經採集過(vi,vj)(v_i,v_j)了,但是這裏又將其作爲負樣本,這樣會讓模型變得困惑。在這裏我自己新添加了幾行代碼來回避上述問題,如下所示,

    # 若干代碼...
    if mod == 0:
        h = []
        t = []
        for i in range(start_index, end_index):
            if random.random() >= self.edge_accept[shuffle_indices[i]]:
                shuffle_indices[i] = self.edge_alias[shuffle_indices[i]]
            cur_h = edges[shuffle_indices[i]][0]
            cur_t = edges[shuffle_indices[i]][1]
            h.append(cur_h)
            t.append(cur_t)
        sign = np.ones(len(h))
    else:
        sign = np.ones(len(h))*-1
        t = []
        for i in range(len(h)):
            negative_sampled_index = alias_sample(self.node_accept, self.node_alias)
            # 新添加代碼,迴避採樣困惑問題
            constructed_edge = (self.idx2node[h[i]], self.idx2node[negative_sampled_index])
            if constructed_edge in self.graph.edges:
                sign[i] = 1
            # -----------------------
    
            t.append(negative_sampled_index)
    # 若干代碼...
    

    經過上述添加代碼的改善後,經過50個epoch的訓練後,loss由原先的0.0473變爲0.0203,可以看出,新添加的代碼確實有效果,如果大家有異議的話,可以儘管提~

4. 總結

本篇博客介紹了LINE算法整體思路、加速訓練的手段以及代碼實現的一些細節,希望能夠給大家帶來幫助。但LINE算法依然有其自己的缺點,即算法過分關注鄰接特徵,即只去關注鄰接節點或相似節點,沒有像DeepWalk一樣考慮一條路徑上的特徵,而後面我們要講述的Node2Vec算法能夠很好地兼顧這兩個方面。

參考

  1. 【Graph Embedding】LINE:算法原理,實現和應用
  2. LINE- Large-scale Information Network Embedding
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章