Node2vec原理剖析,代码实现
DeepWalk原理介紹
與詞嵌入類似,圖嵌入基本理念是基于相鄰頂點的關系,將目的頂點映射為稠密向量,以數值化的方式表達圖中的信息,以便在下游任務中運用。
Word2Vec根據詞與詞的共現關系學習向量的表示,DeepWalk受其啟發。它通過隨機游走的方式提取頂點序列,再用Word2Vec模型根據頂點和頂點的共現關系,學習頂點的向量表示??梢岳斫鉃橛梦淖职褕D的內容表達出來,如下圖所示。
DeepWalk訓練圖表示的整個過程大致可以分為2步:
- 隨機游走提取頂點序列
- 使用skip-gram學習頂點嵌入
訓練時采用層次Softmax(Hierarchical Softmax)優化算法,避免計算所有詞的softmax。
Node2vec原理
DeepWalk不適用于有權圖,它無法學習邊上的權重信息。Node2Vec可以看作DeepWalk的擴展,它學習嵌入的過程也可以分兩步:
- 二階隨機游走(2ndorderrandomwalk)
- 使用skip-gram學習頂點嵌入
可以看到與DeepWalk的區別就在于游走的方式,在二階隨機游走中,轉移概率 πvxπ_{vx}πvx? 受權值 wvxw_{vx}wvx? 影響(無權圖中wvxw_{vx}wvx? 為1):
πvx=αpq(t,x)?wvx\pi_{vx}=\alpha_{pq}(t,x) \cdot w_{vx}πvx?=αpq?(t,x)?wvx?
αpq(t,x)={1p,ifdtx=01,ifdtx=11q,ifdtx=2\alpha_{pq}(t,x)=\left\{ \begin{aligned} \frac{1}{p}, \quad & if\quad{d_{tx}=0} \\ 1, \quad & if\quad{d_{tx}=1} \\ \frac{1}{q}, \quad & if\quad{d_{tx}=2} \end{aligned} \right.αpq?(t,x)=??????????????p1?,1,q1?,?ifdtx?=0ifdtx?=1ifdtx?=2?
t 代表上一個節點,v 代表當前節點,x 代表下一個準備訪問的節點。 dtxd_{tx}dtx?代表上一個節點與待訪問節點的距離。 dtx=0d_{tx} = 0dtx?=0 代表從當前節點返回上一個訪問節點,即“t->v->t”。
算法通過p、q兩個超參數來控制游走到不同頂點的概率。
- q:被稱作進出參數, 控制“向內”還是“向外”游走。若q>1,傾向于訪問與 t 接近的頂點,若 q<1 則傾向于訪問遠離 t 的頂點。
- p:被稱為返回參數,控制重復訪問剛剛訪問過的頂點的概率。若設置的值較大,就不大會剛問剛剛訪問過的頂點。若設置的值較小,那就可能回路返回一步。
p,q的大小可以控制算法是偏向于DFS還是BFS。p越小,隨機游走回節點t的可能性越大,算法傾向于表達結構性;q越小,隨機游走到遠方的可能性越大,網絡傾向于表達同質性。
也即,BFS偏向于表達結構性,DFS傾向于表達同質性。
這里解釋一下,網絡的“同質性”指的是距離相近節點的embedding應該盡量近似,如圖,節點u與其相連的節點s1、s2、s3、s4的embedding表達應該是接近的,這就是“同質性“的體現。
“結構性”指的是結構上相似的節點的embedding應該盡量接近,圖中節點u和節點s6都是各自局域網絡的中心節點,結構上相似,其embedding的表達也應該近似,這是“結構性”的體現。
總的來說,node2vec是一種綜合考慮DFS鄰域和BFS鄰域的graph embedding方法。簡單來說,可以看作是deepwalk的一種擴展,是結合了DFS和BFS隨機游走的deepwalk。
Node2vec實現代碼詳解
Node2vec的算法分為如下幾步:
偽代碼如下:
維度d,每個節點生成r個長度為l的語料序列。上下文context長度為k。
start node : u
初始節點u和它的鄰域表示:{u,s4,s5,s6,s8,s9}
u的鄰域:Ns(u)=(s4,s5,s6,s8,s9)N_s(u)=(s_4,s_5,s_6,s_8,s_9)Ns?(u)=(s4?,s5?,s6?,s8?,s9?)
walk是節點u生成的隨機游走的樣本結果集。為了便于說明,上下兩個偽代碼框分別從0開始編號。
第5行到第8行對每個頂點進行r輪游走,生成長度為l的頂點序列,保存在集合walks中。
第9行是做梯度下降優化。推薦使用負采樣
node2vecWalk部分的第1步是初始化序列walk,只需注意此時walk已經包含了兩個元素[pre, curr],然后進行l步n2v游走。第3行獲取walk中上一步的節點作為當前節點curr,…剩下就沒有難度了。
注意,在GetNeighbors中,對于當前節點curr,可以對所有鄰居采樣,再計算。
代碼寫得很清楚,只需要注意構建圖的時候,對每個節點先走一步,產生(prevNodeId ,currentNodeId) 這個itempair,在調用node2vecWalk,對currentNodeId走l輪,產生(item1,item2,item3…)
具體源碼如下:
這部分為Alias Sample采樣算法
import numpy as np def alias_setup(probs):'''Compute utility lists for non-uniform sampling from discrete distributions.Refer to https://hips.seas.harvard.edu/blog/2013/03/03/the-alias-method-efficient-sampling-with-many-discrete-outcomes/for details'''K = len(probs)q = np.zeros(K) #保存樣本概率J = np.zeros(K, dtype=np.int) #保存補1的事件smaller = []larger = []for kk, prob in enumerate(probs):q[kk] = K*probif q[kk] < 1.0:smaller.append(kk)else:larger.append(kk)while len(smaller) > 0 and len(larger) > 0:small = smaller.pop()large = larger.pop()J[small] = largeq[large] = q[large] + q[small] - 1.0 #q[large]-(1-q[small])if q[large] < 1.0:smaller.append(large)else:larger.append(large)return J, q #(alias,prab)def alias_draw(J, q):'''Draw sample from a non-uniform discrete distribution using alias sampling.'''K = len(J)kk = int(np.floor(np.random.rand()*K))if np.random.rand() < q[kk]:return kkelse:return J[kk]這部分為真正的源碼。
import numpy as np import networkx as nx import random from gensim.models import word2vecclass Graph():def __init__(self, nx_G, is_directed, p, q):self.G = nx_Gself.is_directed = is_directedself.p = pself.q = qdef node2vec_walk(self, walk_length, start_node):'''Simulate a random walk starting from start node.'''G = self.Galias_nodes = self.alias_nodesalias_edges = self.alias_edgeswalk = [start_node]while len(walk) < walk_length:cur = walk[-1]cur_nbrs = sorted(G.neighbors(cur))if len(cur_nbrs) > 0:# 如果序列中僅有一個結點,即第一次游走# alias_nodes中保存了alias_setup的[alias, accept],通過alias_draw返回采樣的下一個索引號if len(walk) == 1:walk.append(cur_nbrs[alias_draw(alias_nodes[cur][0], alias_nodes[cur][1])])else:# 當前游走結點的前一個結點和下一個節點prev = walk[-2]# 使用alias_edges中記錄的[alias, accept],來采樣鄰居中的下一個節點next = cur_nbrs[alias_draw(alias_edges[(prev, cur)][0], alias_edges[(prev, cur)][1])]walk.append(next)else:breakreturn walkdef simulate_walks(self, num_walks, walk_length):'''Repeatedly simulate random walks from each node.'''G = self.Gwalks = []nodes = list(G.nodes())# nodes采樣一次為一個epoch,此處就是num_walks個epochprint('Walk iteration:')for walk_iter in range(num_walks):print(str(walk_iter+1), '/', str(num_walks))random.shuffle(nodes)for node in nodes:walks.append(self.node2vec_walk(walk_length=walk_length, start_node=node))return walksdef get_alias_edge(self, src, dst):'''Get the alias edge setup lists for a given edge.:return alias_setup(): 在上一次訪問頂點 t ,當前訪問頂點為 v 時到下一個頂點 x 的未歸一化轉移概率。:param src: 隨機游走序列種的上一個結點:param dst: 當前結點參數p控制重復訪問剛剛訪問過的頂點的概率。若p較大,則訪問剛剛訪問過的頂點的概率會變低。參數q控制著游走是向外還是向內:若q>1,隨機游走傾向于訪問和上一次的t接近的頂點(偏向BFS);若q<1,傾向于訪問遠離t的頂點(偏向DFS)'''G = self.Gp = self.pq = self.qunnormalized_probs = []for dst_nbr in sorted(G.neighbors(dst)):if dst_nbr == src: # 如果是要返回上一個節點unnormalized_probs.append(G[dst][dst_nbr]['weight']/p)elif G.has_edge(dst_nbr, src): # 如果接下來訪問的節點與src的距離與當前節點相等unnormalized_probs.append(G[dst][dst_nbr]['weight'])else:unnormalized_probs.append(G[dst][dst_nbr]['weight']/q)norm_const = sum(unnormalized_probs)normalized_probs = [float(u_prob)/norm_const for u_prob in unnormalized_probs]return alias_setup(normalized_probs)def preprocess_transition_probs(self):'''Preprocessing of transition probabilities for guiding the random walks.用于引導隨機游走的預處理,得到馬爾可夫轉移概率矩陣。'''G = self.Gis_directed = self.is_directedalias_nodes = {}# G.neighbors(node) 與頂點相鄰的所有頂點,更方便更快的訪問adjacency字典用: G[cur]for node in G.nodes():# 根據鄰居節點的權重,計算轉移概率unnormalized_probs = [G[node][nbr]['weight'] for nbr in sorted(G.neighbors(node))]norm_const = sum(unnormalized_probs)# 計算當前節點到鄰居節點的轉移概率,其實就是權重歸一化normalized_probs = [float(u_prob)/norm_const for u_prob in unnormalized_probs]# 設置alias table,保存每個節點的accept[i]和alias[i],為后面alias采樣做準備。alias_nodes[node] = alias_setup(normalized_probs)alias_edges = {}triads = {}# 保存每條邊的accept[i]和alias[i]if is_directed:for edge in G.edges():alias_edges[edge] = self.get_alias_edge(edge[0], edge[1])else:for edge in G.edges():alias_edges[edge] = self.get_alias_edge(edge[0], edge[1]) # 隨機游走序列種的上一個結點 當前節點alias_edges[(edge[1], edge[0])] = self.get_alias_edge(edge[1], edge[0])self.alias_nodes = alias_nodesself.alias_edges = alias_edgesprint(self.alias_nodes)print(self.alias_edges)returndef alias_setup(probs):'''Compute utility lists for non-uniform sampling from discrete distributions.Refer to https://hips.seas.harvard.edu/blog/2013/03/03/the-alias-method-efficient-sampling-with-many-discrete-outcomes/for details:param probs: 指定的采樣結果概率分布列表。期望按這個概率列表來采樣每個隨機變量X。:return J: alias[i]表示第i列中不是事件i的另一個事件的編號。:return p: accept[i]表示事件i占第i列矩形的面積的比例。'''K = len(probs)# q表示:accept數組q = np.zeros(K)# J表示:alias數組J = np.zeros(K, dtype=np.int)# Alias方法將整個概率分布壓成一個 1*N 的矩形,每個事件轉換為矩形中的面積。# 將面積大于1的事件多出的面積補充到面積小于1對應的事件中,以確保每一個小方格的面積為1,# 同時,保證每一方格至多存儲兩個事件。smaller = [] # 面積小于1的事件larger = [] # 面積大于1的事件for kk, prob in enumerate(probs):q[kk] = K*probif q[kk] < 1.0:smaller.append(kk)else:larger.append(kk)while len(smaller) > 0 and len(larger) > 0:small = smaller.pop()large = larger.pop()J[small] = large# 其實是 q[large] - (1.0 - q[small]),把大的削去(1.0 - q[small])填充到小的上q[large] = q[large] + q[small] - 1.0# 大的剩下的面積,放到下一輪繼續倒騰if q[large] < 1.0:smaller.append(large)else:larger.append(large)return J, qdef alias_draw(J, q):'''Draw sample from a non-uniform discrete distribution using alias sampling.參考:https://zhuanlan.zhihu.com/p/54867139:param q: accept數組,表示事件i占第i列矩形的面積的比例;:param J: alias數組,表示alias矩形的第i列中不是事件i的另一個事件的編號,也就是填充的那一列的序號;生成一個隨機數 kk in [0, K],另一個隨機數 x in [0,1],如果 x < accept[kk],表示接受事件kk,返回kk,否則拒絕事件kk,返回alias[kk]'''K = len(J)kk = int(np.floor(np.random.rand()*K))if np.random.rand() < q[kk]:return kkelse:return J[kk]def read_graph(input_file, directed):'''Reads the input network in networkx.'''if directed:G = nx.read_edgelist(input_file, delimiter=",", nodetype=int, data=(('weight',float),), create_using=nx.DiGraph())else:G = nx.read_edgelist(input_file, delimiter=",", nodetype=int, create_using=nx.DiGraph())for edge in G.edges():G[edge[0]][edge[1]]['weight'] = 1if not directed:G = G.to_undirected()return Gdef learn_embeddings(walks):'''Learn embeddings by optimizing the Skipgram objective using SGD.'''walks = [list(map(str, walk)) for walk in walks]print(walks) # model = word2vec.Word2Vec(walks, vector_size=64, window=3, min_count=0, sg=1, workers=1, epochs=5)# model.save_word2vec_format(args.output)#model.wv.save_word2vec_format(args.output, binary=False)returndef main(directed):'''Pipeline for representational learning for all nodes in a graph.'''nx_G = read_graph(r"C:\Users\Administrator\TensorFlow\game.csv", directed)print(list(nx_G.edges(data=True)), list(nx_G))for node in nx_G.neighbors(2):print(node)G = Graph(nx_G, False, 1, 2)G.preprocess_transition_probs()walks = G.simulate_walks(5, 3)learn_embeddings(walks)if __name__ == "__main__":main(directed = False)Alias Sample Method 別名采樣算法
更詳細的可以觀看如下blog:
Darts, Dice, and Coins: Sampling from a Discrete Distribution
在隨機游走的過程中,假設已經游走到當前節點,則下一步的游走要參考下面的公式。
πvx=αpq(t,x)?wvx\pi_{vx}=\alpha_{pq}(t,x) \cdot w_{vx}πvx?=αpq?(t,x)?wvx?
則對于不同的節點,被采樣到的概率是不一樣的,因此需要采用采樣算法。Alias Sample算法是一種時間復雜度為O(1)的算法,因此特別適合大規模的網絡游走情況。
構建Alias Table
每個概率乘以四(事件數)。
然后拼湊使每列值為 1,并保證每列最多只包含兩個事件。
至此整個方法大功告成。
Alias Method具體算法如下:
2.產生兩個隨機數,第一個產生1~N 之間的整數i,決定落在哪一列。扔第二次骰子,0~1之間的任意數,判斷其與Prab[i]大小,如果小于Prab[i],則采樣i,如果大于Prab[i],則采樣Alias[i]
讀者可以采樣自己運行一下Alias算法,體會一下,代碼在上面。
總結
以上是生活随笔為你收集整理的Node2vec原理剖析,代码实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux C 下的socket网络编程
- 下一篇: 服务器器ip的A段B段C段是什么意思有什