本專案參考:
https://aistudio.baidu.com/aistudio/projectdetail/5012408?contributionType=1
其它相關專案:
關於圖計算&圖學習的基礎知識概覽:前置知識點學習(PGL)[系列一] https://aistudio.baidu.com/aistudio/projectdetail/4982973?contributionType=1
圖機器學習(GML)&圖神經網路(GNN)原理和程式碼實現(前置學習系列二):https://aistudio.baidu.com/aistudio/projectdetail/4990947?contributionType=1
【圖來源:網路,筆記由筆者添上】
演演算法流程:
【其中使用skip-gram模型是為了利用梯度的方法對引數進行更新訓練】
【圖來源:網路,筆記由筆者添上】
演演算法流程:
【其中使用skip-gram模型是為了利用梯度的方法對引數進行更新訓練】
此外,關於以上兩個演演算法的定義和概念就不一一展示說明了,後邊等有空了補上。下邊說一下我學習的一些參考資料,希望對大家有幫助。
1.【論文筆記】DeepWalk——陌上疏影涼
1.【網路圖模型綜述
1.【異構圖神經網路簡介--機器之心
1.【Graph Embedding之metapath2vec--圈圈_Master
1.【從Random Walk談到Bacterial foraging optimization algorithm(BFOA),再談到Ramdom Walk Graph Segmentation圖分割演演算法
【註解展示,是為了方便自己理解,同時也希望能幫到和自己一樣在學習這塊知識的小夥伴】
實現Graph類的random_walk函數--可參考
思路
from pgl.graph import Graph
import numpy as np
class UserDefGraph(Graph):
def random_walk(self, nodes, walk_len):
"""
輸入:nodes - 當前節點id list (batch_size,)
walk_len - 最大路徑長度 int
輸出:以當前節點為起點得到的路徑 list (batch_size, walk_len)
用到的函數
1. self.successor(nodes)
描述:獲取當前節點的下一個相鄰節點id列表
輸入:nodes - list (batch_size,)
輸出:succ_nodes - list of list ((num_successors_i,) for i in range(batch_size))
2. self.outdegree(nodes)
描述:獲取當前節點的出度
輸入:nodes - list (batch_size,)
輸出:out_degrees - list (batch_size,)
"""
walks = [[node] for node in nodes] # 首先獲得當前節點列表對應的一個向量
walks_ids = np.arange(0, len(nodes)) # 遊走路徑中節點對應id號
cur_nodes = np.array(nodes) # 當前節點情況
for l in range(walk_len): # 根據遊走長度進行遍歷--破出條件:1. range結束;2. outdegree==0【出度為零,沒有可繼續的節點】
"""選取有下一個節點的路徑繼續取樣,否則結束"""
outdegree = self.outdegree(cur_nodes) # 計算當前節點的出度--也就是對應有哪些位置的鄰近節點
walk_mask = (outdegree != 0) # 根據出度來確定掩碼--True, False--將出度為0的部分複製為False,反之True
if not np.any(walk_mask): # 判斷是否沒有可繼續的節點情況--出度為0
break
cur_nodes = cur_nodes[walk_mask] # 根據掩碼獲取可繼續前進的節點,作為後邊討論的當前可前行節點
walks_ids = walks_ids[walk_mask] # 獲取掩碼下,原節點id,組成新的work_ids用於後邊討論,但本身還是作為一個節點的標記,對應這是第幾個節點
outdegree = outdegree[walk_mask] # 根據掩碼獲取相應的不為0的出度--用於後邊計算前行的路徑
######################################
# 請在此補充程式碼取樣出下一個節點
'''
[註解有點多,所以放外邊了]
PS:
1. successor 可獲取當前節點的下一個相鄰節點id列表,
那麼successor 計算出下一節點的集合後,我們需要從中隨機取出一個節點--所以我們要建立隨機取樣的index_list(索引序列集)
2. 建立index_list=>為了才到合適的index資訊,採用np.floor與np.random,rand()實現:
eg: np.floor(np.random.rand(outdegree.shape[0]) * outdegree).astype('int64')
np.random.rand(outdegree.shape[0]): 根據出度集的形狀來取得相應形狀的亂數--這裡體現遊走的隨機性
np.random.rand(outdegree.shape[0]) * outdegree:利用生成的亂數與出度集對應元素相乘——這裡得到一些列的亂數,亂數範圍在0~最大出度值--保證路徑有效
np.floor(np.random.rand(outdegree.shape[0]) * outdegree)——實現向下取整,這樣就得到了相應遊走路徑中接下來那個點的索引
具體範例:
np.floor(np.random.rand(20) * 3).astype('int64')
result: array([0, 1, 2, 1, 0, 0, 0, 0, 1, 1, 1, 2, 0, 2, 2, 2, 2, 1, 2, 0])
3. 既然知道了隨機取樣的序列集了,那麼接下就是分配新的遊走路徑了
next_nodes = [] # 用於後邊存放—— 裝配有下一個節點的新路徑
# 引數說明:
succ_nodes:相鄰節點id列表
sample_index:對應出度生成的隨即索引集
walks_ids:遊走路徑中節點對應id號
# 接下來的迴圈指的是,將節點列表、隨機取樣序列、遊走路徑中節點對應id號一一對應進行填充--得到一個遊走情況
for s, ind, walk_id in zip(succ_nodes, sample_index, walks_ids):
walks[walk_id].append(s[ind]) # 注意: 從開始已經知道walks=>[[], [], []]是這種形式的,這樣這裡的append,就很容易理解成為相應節點新增可以繼續前行的節點,形成一條路徑
next_nodes.append(s[ind]) # 同時獲取接下來要重新討論遊走時所需的新節點--即:如:從1走到了2,從3走到了7: [[1], [3]]=>[[1, 2], [3, 7]]
# 接下來自然就該考慮把新的2, 7 作為下一次遊走時討論出度的節點啦
'''
succ_nodes = self.successor(cur_nodes) # 返回可繼續的節點集合
# next_nodes = ...
sample_index = np.floor(np.random.rand(outdegree.shape[0]) * outdegree).astype('int64')
next_nodes = []
for s, ind, walk_id in zip(succ_nodes, sample_index, walks_ids):
walks[walk_id].append(s[ind])
next_nodes.append(s[ind])
######################################
cur_nodes = np.array(next_nodes) # 將節點轉換為np型別,方便一些操作運算--同時保證前後資料型別
# 遍歷完遊走長度的次數,就可以返回得到的隨機遊走路徑啦
return walks
# 可增加輪次提高精度--epoch
# 當前引數精度大概在95%左右
!python my_deepwalk.py --use_my_random_walk --epoch 35 # 35 用自己實現的random walk訓練DeepWalk模型,可在 ./tmp/deepwalk/walks/ 中檢視構造的節點路徑
[INFO] 2022-11-11 14:28:28,099 [my_deepwalk.py: 250]: Step 1200 DeepWalk Loss: 0.198106 0.242671 s/step.
[INFO] 2022-11-11 14:28:30,539 [my_deepwalk.py: 250]: Step 1210 DeepWalk Loss: 0.187183 0.309996 s/step.
[INFO] 2022-11-11 14:28:33,171 [my_deepwalk.py: 250]: Step 1220 DeepWalk Loss: 0.189533 0.244672 s/step.
[INFO] 2022-11-11 14:28:35,537 [my_deepwalk.py: 250]: Step 1230 DeepWalk Loss: 0.202293 0.232859 s/step.
[INFO] 2022-11-11 14:28:37,920 [my_deepwalk.py: 250]: Step 1240 DeepWalk Loss: 0.189366 0.244727 s/step.
[INFO] 2022-11-11 14:28:40,450 [my_deepwalk.py: 250]: Step 1250 DeepWalk Loss: 0.188601 0.254400 s/step.
[INFO] 2022-11-11 14:28:42,875 [my_deepwalk.py: 250]: Step 1260 DeepWalk Loss: 0.191343 0.247985 s/step.
[INFO] 2022-11-11 14:28:45,286 [my_deepwalk.py: 250]: Step 1270 DeepWalk Loss: 0.186549 0.255688 s/step.
[INFO] 2022-11-11 14:28:47,653 [my_deepwalk.py: 250]: Step 1280 DeepWalk Loss: 0.188638 0.240493 s/step.
[INFO] 2022-11-11 14:29:45,898 [link_predict.py: 199]: Step 180 Train Loss: 0.398023 Train AUC: 0.960870
[INFO] 2022-11-11 14:29:46,023 [link_predict.py: 223]: Step 180 Test Loss: 0.399052 Test AUC: 0.960234
[INFO] 2022-11-11 14:29:48,816 [link_predict.py: 199]: Step 190 Train Loss: 0.396805 Train AUC: 0.960916
[INFO] 2022-11-11 14:29:48,951 [link_predict.py: 223]: Step 190 Test Loss: 0.397910 Test AUC: 0.960275
[INFO] 2022-11-11 14:29:51,783 [link_predict.py: 199]: Step 200 Train Loss: 0.396290 Train AUC: 0.960936
[INFO] 2022-11-11 14:29:51,913 [link_predict.py: 223]: Step 200 Test Loss: 0.397469 Test AUC: 0.960292
NOTE:在得到節點路徑後,node2vec會使用SkipGram模型學習節點表示,給定中心節點,預測區域性路徑中還有哪些節點。模型中用了negative sampling來降低計算量。
參考 PGL/examples/node2vec/node2vec.py 中的 node2vec_model 函數
import paddle.fluid.layers as l
def userdef_loss(embed_src, weight_pos, weight_negs):
"""
輸入:embed_src - 中心節點向量 list (batch_size, 1, embed_size)
weight_pos - 標籤節點向量 list (batch_size, 1, embed_size)
weight_negs - 負樣本節點向量 list (batch_size, neg_num, embed_size)
輸出:loss - 正負樣本的交叉熵 float
"""
##################################
# 請在這裡實現SkipGram的loss計算過程
### 負取樣計算部分——Multi Sigmoids
# 分別計算正樣本和負樣本的 logits(概率)
pos_logits = l.matmul(
embed_src, weight_pos, transpose_y=True) # [batch_size, 1, 1] -- matmul:矩陣相乘
neg_logits = l.matmul(
embed_src, weight_negs, transpose_y=True) # [batch_size, 1, neg_num]
# 設定正樣本標籤,並計算正樣本loss
ones_label = pos_logits * 0. + 1.
ones_label.stop_gradient = True # 關閉梯度記錄
pos_loss = l.sigmoid_cross_entropy_with_logits(pos_logits, ones_label) # 交叉熵計算==對應公式2
# 設定負樣本標籤,並計算負樣本loss
zeros_label = neg_logits * 0.
zeros_label.stop_gradient = True
neg_loss = l.sigmoid_cross_entropy_with_logits(neg_logits, zeros_label) # 交叉熵計算==對應公式3
# 總的Loss計算為正樣本與負樣本loss之和
loss = (l.reduce_mean(pos_loss) + l.reduce_mean(neg_loss)) / 2 # 得到總的loss
##################################
return loss
NOTE:Node2Vec會根據與上個節點的距離按不同概率取樣得到當前節點的下一個節點。
<img src="https://ai-studio-static-online.cdn.bcebos.com/09001163a1064101a8dd2892eb559cf2006aa93d7fe84c70b2ad47b810f4c86a" width="85%" height="85%" /><br>
參考; PGL/pgl/graph_kernel.pyx 中用Cython語言實現了節點取樣函數node2vec_sample
import numpy as np
# 隨機節點的獲取
def node2vec_sample(succ, prev_succ, prev_node, p, q):
"""
輸入:succ - 當前節點的下一個相鄰節點id列表 list (num_neighbors,)
prev_succ - 前一個節點的下一個相鄰節點id列表 list (num_neighbors,)
prev_node - 前一個節點id int
p - 控制回到上一節點的概率 float
q - 控制偏向DFS還是BFS float
輸出:下一個節點id int
"""
##################################
# 請在此實現node2vec的節點取樣函數
# 節點引數資訊
succ_len = len(succ) # 獲取相鄰節點id列表節點長度(相對當前)
prev_succ_len = len(prev_succ) # 獲取相鄰節點id列表節點長度(相對前一個節點)
prev_succ_set = np.asarray([]) # 前一節點的相鄰節點id列表
for i in range(prev_succ_len): # 遍歷得到前一節點的相鄰節點id列表的新list——prev_succ_set,用於後邊概率的討論
# 將前一節點list,依次押入新的list中
prev_succ_set = np.append(prev_succ_set,prev_succ[i]) # ? prev_succ_set.insert(prev_succ[i])
# 概率引數資訊
probs = [] # 儲存每一個待前往的概率
prob = 0 # 記錄當前討論的節點概率
prob_sum = 0. # 所有待前往的節點的概率之和
# 遍歷當前節點的相鄰節點
for i in range(succ_len): # 遍歷每一個當前節點前往的概率
if succ[i] == prev_node: # case 1 : 取樣節點與前一節點一致,那麼概率為--1/q(原地)
prob = 1. / p
# case 2 完整的應該是: np.where(prev_succ_set==succ[i]) and np.where(succ==succ[i])
# 但是因為succ本身就是取樣集,所以np.where(succ==succ[i])總成立,故而忽略,不考慮
elif np.where(prev_succ_set==succ[i]): # case 2 : 取樣節點在前一節點list內,那麼概率為--1 ?cpython中的程式碼: prev_succ_set.find(succ[i]) != prev_succ_set.end()
prob = 1.
elif np.where(prev_succ_set!=succ[i]): # case 3 : 取樣節點不在前一節點list內,那麼概率為--1/q
prob = 1. / q
else:
prob = 0. # case 4 : other
probs.append(prob) # 將待前往的每一個節點的概率押入儲存
prob_sum += prob # 計算所有節點的概率之和
RAND_MAX = 65535 # 這是一個亂數的最值,用於計算隨機值的--根據C/C++標準,最小在30000+,這裡取2^16次方
rand_num = float(np.random.randint(0, RAND_MAX+1)) / RAND_MAX * prob_sum # 計算一個隨機概率:0~prob_sum. ?cpython中的程式碼: float(rand())/RAND_MAX * prob_sum
sampled_succ = 0. # 當前節點的相鄰節點中確定的取樣點
# rand_num => 是0~prob_num的一個值,表示我們的擷取概率閾值--即當遍歷了n個節點時,若已遍歷的節點的概率之和已經超過了rand_num
# 我們取剛好滿足已遍歷的節點的概率之和已經超過了rand_num的最近一個節點作為我們的取樣節點
# 比如: 遍歷到第5個節點時,權重概率和大於等於rand_num,此時第5個節點就是對應的取樣的節點了
# 為了方便實現:這裡利用迴圈遞減--判斷條件就變成了————當rand_num減到<=0時,開始取樣節點
for i in range(succ_len): # 遍歷當前節點的所有相鄰節點
rand_num -= probs[i] # 利用rand_num這個隨機獲得的概率值作為依據,進行一個迴圈概率檢驗
if rand_num <= 0: # 當遇到第一次使得rand_num減到<=0後,說明到這個節點為止, 遍歷應該終止了,此時的節點即未所求的節點,【停止檢驗條件】
sampled_succ = succ[i] # 並把當前節點作為確定的節點
return sampled_succ # 返回待取樣的節點--節點一定在succ中
[INFO] 2022-11-11 14:38:49,133 [link_predict.py: 199]: Step 170 Train Loss: 0.454199 Train AUC: 0.954936
[INFO] 2022-11-11 14:38:49,260 [link_predict.py: 223]: Step 170 Test Loss: 0.454974 Test AUC: 0.954118
[INFO] 2022-11-11 14:38:51,997 [link_predict.py: 199]: Step 180 Train Loss: 0.452219 Train AUC: 0.955133
[INFO] 2022-11-11 14:38:52,122 [link_predict.py: 223]: Step 180 Test Loss: 0.453069 Test AUC: 0.954312
[INFO] 2022-11-11 14:38:54,851 [link_predict.py: 199]: Step 190 Train Loss: 0.450969 Train AUC: 0.955254
[INFO] 2022-11-11 14:38:54,978 [link_predict.py: 223]: Step 190 Test Loss: 0.451892 Test AUC: 0.954428
[INFO] 2022-11-11 14:38:57,714 [link_predict.py: 199]: Step 200 Train Loss: 0.450440 Train AUC: 0.955305
[INFO] 2022-11-11 14:38:57,842 [link_predict.py: 223]: Step 200 Test Loss: 0.451436 Test AUC: 0.954473
1. 回顧並總結了圖的基本概念。<br>
2. 學習思考演演算法實現的程式碼思路--Node2Vec的實現以及RandomWalk的實現。<br>
3. 對原始碼閱讀能力的提升。<br>
其它相關筆記:<br>
關於圖計算&圖學習的基礎知識概覽:前置知識點學習(PGL)[系列一] https://aistudio.baidu.com/aistudio/projectdetail/4982973?contributionType=1
圖機器學習(GML)&圖神經網路(GNN)原理和程式碼實現(前置學習系列二):https://aistudio.baidu.com/aistudio/projectdetail/4990947?contributionType=1
<br>
* 如果我的專案對你有幫助不如點一個心,fork一下,以備可以常複習哦!