上一篇博客推薦系統(二)Graph Embedding之DeepWalk中講到Graph Embedding的開山之作DeepWalk,該博客講述了在圖結構上進行RandomWalk獲取訓練樣本,並通過Word2Vec模型來訓練得到圖中每個節點的Embedding向量。
但DeepWalk有兩個比較大的問題:
- DeepWalk將問題抽象成無權圖,這意味着高頻次user behavior路徑和低頻次user behavior路徑在模型看來是等價的。
- 所處環境相似但不直連的兩個節點沒有進行特殊的處理,這類節點的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圖結構爲,其節點用來表示,節點自身Embedding向量用來表示,而當被作爲上下文節點時,則其Embedding向量用表示,例如上圖中2節點本身的Embedding向量爲,而如果其作爲5或者6的相鄰節點時,其上下文Embedding向量爲。對於圖的邊,設邊的權值爲。
1.1 First-order Proximity
對於圖中直連邊,模型預測概率爲:
可以看出這個預測概率其實就是sigmoid
函數,而這條邊出現的真實概率爲
模型的目標是使預測概率和真實概率的分佈儘量相近,即使得和的KL散度儘量小,不熟悉KL散度的小夥伴可以參考這裏,這個大神講的真的非常的清楚。而這裏省略了這個常數項,模型的目標如下所示:
1.2 Second-order Proximity
對於直連的邊,直接給出模型預測的已知時的條件概率爲:
可以看出這個預測概率也是類似sigmoid
函數,這條邊的真實條件概率變爲如下公式,其中表示節點的出度,
這時模型的目標和First-order Proximity類似,即使得儘量接近於,經過簡化,模型的目標爲:
可能很多人在這裏都會有所疑惑,爲什麼Second-order Proximity能夠使環境相似但不直連節點的Embedding向量距離相近,而且原始論文中也沒有提到。這裏我的理解是這樣的:假設和是符合上述條件的兩個節點,他們都連接着,則和兩個式子只有和是不同的,且和都爲1,則這樣模型學習出來的結果必然會讓和比較相近。
2. Model Optimization
2.1 負採樣更改損失函數
計算Second-order Proximity的損失函數時,可以看出模型有一個比較大的問題,就是每次求時都要遍歷圖中的所有節點,這樣是非常耗時的,論文中引入負採樣的方式,即在計算表示公式的分母的時候,並不需要遍歷所有的節點,而是選取K
個負邊進行計算,公式如下所示:
2.2 alias採樣
2.1節中損失函數的計算效率問題已經解決,但是現在又有一個問題,即隨機梯度下降過程中梯度不穩定的問題,因爲和的計算公式中都有,梯度計算公式中都含有,拿的梯度計算舉例,有如下公式,過大則會導致梯度爆炸,過小則會導致梯度消失。
針對上述情況,論文提出一種解決方案,使得有權圖變爲無權圖,即使所有的都變爲1,但是在採樣的時候要根據原先每條邊的權值大小調整採樣概率,例如一個權重爲5的邊要比一個權重爲1的邊被採到的概率大,這時就需要選擇一種合適的採樣策略,使得采樣後的數據和原先數據的分佈儘量相似,這裏就用到了大名鼎鼎的alias採樣,不熟悉的小夥伴可以參考這裏。
3. 代碼實現
和DeepWalk類似,這裏依然援引知乎淺夢大神的github代碼,這裏分享下我對其LINE代碼實現的兩點見解。
-
實現負採樣的代碼,思路比較新穎,在
line.py
的函數batch_iter
中,對於一個batch的正樣本集合,與之搭配negative_ratio
個batch的負樣本集合來進行學習,整個過程只是通過mod
這一個變量進行控制的,思路真的很棒。 -
針對負採樣,我這邊有一點個人的見解,原先負採樣的思路是針對一個節點,先隨機採一個batch的與直連的節點,與拼接在一起構成正樣本集合,之後隨機採若干個batch的節點,與拼接在一起構成負樣本集合。這個做法的問題在於如果這個負樣本集合中有與直連的節點構成的邊,則會使模型的學習變得艱難,因爲在採集正樣本的時候已經採集過了,但是這裏又將其作爲負樣本,這樣會讓模型變得困惑。在這裏我自己新添加了幾行代碼來回避上述問題,如下所示,
# 若干代碼... 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算法能夠很好地兼顧這兩個方面。