如何利用圖卷積網絡對圖進行深度學習
圖數據的機器學習是一項非常困難的任務,因為圖數據結構非常復雜,但同時也提供了大量的信息。這篇文章是關於如何利用圖卷積網絡(GCNs)對圖進行深度學習的系列文章中的第一篇,GCNs是一種功能強大的神經網絡,旨在直接處理圖並利用其結構信息。系列目錄:
- 圖卷積網絡的高級介紹
- 第二部分:基於譜域圖卷積的半監督學習
在這篇文章中,我將介紹GCNs,並使用編碼示例說明信息是如何通過GCN的隱藏層傳播的。我們將看到GCN如何聚合來自前一層的信息,以及這種機制如何在圖中生成節點的有用特性表示。
什么是GCN
GCN 是一類非常強大的用於圖數據的神經網絡架構。事實上,它非常強大,即使是隨機初始化的兩層 GCN 也可以生成圖網絡中節點的有用特征表征。下圖展示了這種兩層 GCN 生成的每個節點的二維表征。請注意,即使沒有經過任何訓練,這些二維表征也能夠保存圖中節點的相對鄰近性。
更形式化地說,圖卷積網絡(GCN)是一個對圖數據進行操作的神經網絡。給定圖 G = (V, E),GCN 的輸入為:
- 一個輸入維度為 N × F⁰ 的特征矩陣 X,其中 N 是圖網絡中的節點數而 F⁰ 是每個節點的輸入特征數。
- 一個圖結構的維度為 N × N 的矩陣表征,例如圖 G 的鄰接矩陣 A。
因此,GCN 中的隱藏層可以寫作 Hⁱ = f(Hⁱ⁻¹, A))。其中,H⁰ = X,f 是一種傳播規則。每一個隱藏層 Hⁱ 都對應一個維度為 N × Fⁱ 的特征矩陣,該矩陣中的每一行都是某個節點的特征表征。在每一層中,GCN 會使用傳播規則 f 將這些信息聚合起來,從而形成下一層的特征。這樣一來,在每個連續的層中特征就會變得越來越抽象。在該框架下,GCN 的各種變體只不過是在傳播規則 f 的選擇上有所不同 。
一個簡單的傳播規則示例
下面,本文將給出一個最簡單的傳播規則示例 :
f(Hⁱ, A) = σ(AHⁱWⁱ)
其中,Wⁱ 是第 i 層的權重矩陣,σ 是非線性激活函數(如 ReLU 函數)。權重矩陣的維度為 Fⁱ × Fⁱ⁺¹,即權重矩陣第二個維度的大小決定了下一層的特征數。如果你對卷積神經網絡很熟悉,那么你會發現由於這些權重在圖中的節點間共享,該操作與卷積核濾波操作類似。
簡化
接下來我們在最簡單的層次上研究傳播規則。令:
- i = 1,(約束條件 f 是作用於輸入特征矩陣的函數)
- σ 為恆等函數
- 選擇權重(約束條件: AH⁰W⁰ =AXW⁰ = AX)
換言之,f(X, A) = AX。該傳播規則可能過於簡單,本文后面會補充缺失的部分。此外,AX 等價於多層感知機的輸入層。
簡單的圖示例
我們將使用下面的圖作為簡單的示例:
使用 numpy 編寫的上述有向圖的鄰接矩陣表征如下:
A = np.matrix([ [0, 1, 0, 0], [0, 0, 1, 1], [0, 1, 0, 0], [1, 0, 1, 0]], dtype=float )
接下來,需要抽取出特征!我們基於每個節點的索引為其生成兩個整數特征,這簡化了本文后面手動驗證矩陣運算的過程。
In [3]: X = np.matrix([ [i, -i] for i in range(A.shape[0]) ], dtype=float) X Out[3]: matrix([ [ 0., 0.], [ 1., -1.], [ 2., -2.], [ 3., -3.] ])
應用傳播規則
現在已經建立了一個圖,其鄰接矩陣為 A,輸入特征的集合為 X。下面讓我們來看看,當我們對其應用傳播規則后會發生什么:
我們現在已經建立了一個圖,其鄰接矩陣為 A,輸入特征的集合為 X。下面讓我們來看看,當我們對其應用傳播規則后會發生什么:
In [6]: A * X Out[6]: matrix([ [ 1., -1.], [ 5., -5.], [ 1., -1.], [ 2., -2.]]
每個節點的表征(每一行)現在是其相鄰節點特征的和!換句話說,圖卷積層將每個節點表示為其相鄰節點的聚合。大家可以自己動手驗證這個計算過程。請注意,在這種情況下,如果存在從 v 到 n 的邊,則節點 n 是節點 v 的鄰居。
問題所在
你可能已經發現了其中的問題:
- 節點的聚合表征不包含它自己的特征!該表征是相鄰節點的特征聚合,因此只有具有自環(self-loop)的節點才會在該聚合中包含自己的特征 。
- 度大的節點在其特征表征中將具有較大的值,度小的節點將具有較小的值。這可能會導致梯度消失或梯度爆炸 [1, 2],也會影響隨機梯度下降算法(隨機梯度下降算法通常被用於訓練這類網絡,且對每個輸入特征的規模(或值的范圍)都很敏感)。
接下來,本文將分別對這些問題展開討論。
增加自環
為了解決第一個問題,我們可以直接為每個節點添加一個自環 [1, 2]。具體而言,這可以通過在應用傳播規則之前將鄰接矩陣 A 與單位矩陣 I 相加來實現。
In [4]: I = np.matrix(np.eye(A.shape[0])) I Out[4]: matrix([ [1., 0., 0., 0.], [0., 1., 0., 0.], [0., 0., 1., 0.], [0., 0., 0., 1.] ]) In [8]: A_hat = A + I A_hat * X Out[8]: matrix([ [ 1., -1.], [ 6., -6.], [ 3., -3.], [ 5., -5.]])
現在,由於每個節點都是自己的鄰居,每個節點在對相鄰節點的特征求和過程中也會囊括自己的特征!
對特征表征進行歸一化處理
通過將鄰接矩陣 A 與度矩陣 D 的逆相乘,對其進行變換,從而通過節點的度對特征表征進行歸一化。因此,我們簡化后的傳播規則如下:
f(X, A) = D⁻¹AX
讓我們看看發生了什么。我們首先計算出節點的度矩陣。注意:此處計算節點的度是用節點的入度,也可以根據自身的任務特點用出度,在本文中,這個選擇是任意的。一般來說,您應該考慮節點之間的關系是如何與您的具體任務相關。例如,您可以使用in-degree來進行度矩陣計算,前提是只有關於節點的in-neighbors的信息與預測其具體任務中的標簽相關。相反,如果只有關於外部鄰居的信息是相關的,則可以使用out-degree。最后,如果節點的out-和in-鄰居都與您的預測相關,那么您可以基於in-和out-度的組合來計算度矩陣。
正如我將在下一篇文章中討論的那樣,您還可以通過其他方法對表示進行歸一化,而不是使用逆矩陣乘法。
計算度矩陣:
In [9]: D = np.array(np.sum(A, axis=0))[0] D = np.matrix(np.diag(D)) D Out[9]: matrix([ [1., 0., 0., 0.], [0., 2., 0., 0.], [0., 0., 2., 0.], [0., 0., 0., 1.] ])
在應用傳播規則之前,不妨看看我們對鄰接矩陣進行變換后發生了什么。
變換之前:
A = np.matrix([ [0, 1, 0, 0], [0, 0, 1, 1], [0, 1, 0, 0], [1, 0, 1, 0]], dtype=float )
變換之后:
In [10]: D**-1 * A Out[10]: matrix([ [0. , 1. , 0. , 0. ], [0. , 0. , 0.5, 0.5], [0. , 0.5, 0. , 0. ], [1. , 0. , 1. , 0. ] ])
可以觀察到,鄰接矩陣中每一行的權重(值)都除以該行對應節點的度。我們接下來對變換后的鄰接矩陣應用傳播規則:
得到與相鄰節點的特征均值對應的節點表征。這是因為(變換后)鄰接矩陣的權重對應於相鄰節點特征加權和的權重。大家可以自己動手驗證這個結果。
整合
現在,我們將把自環和歸一化技巧結合起來。此外,我們還將重新介紹之前為了簡化討論而省略的有關權重和激活函數的操作。
添加權重
首先要做的是應用權重。請注意,這里的 D_hat 是 A_hat = A + I 對應的度矩陣,即具有強制自環的矩陣 A 的度矩陣。
In [45]: W = np.matrix([ [1, -1], [-1, 1] ]) D_hat**-1 * A_hat * X * W Out[45]: matrix([ [ 1., -1.], [ 4., -4.], [ 2., -2.], [ 5., -5.] ])
如果我們想要減小輸出特征表征的維度,我們可以減小權重矩陣 W 的規模:
In [46]: W = np.matrix([ [1], [-1] ]) D_hat**-1 * A_hat * X * W Out[46]: matrix([[1.], [4.], [2.], [5.]] )
添加激活函數:
本文選擇保持特征表征的維度,並應用 ReLU 激活函數。Relu函數的公式是,代碼為:
def relu(x): return (abs(x) + x) / 2
一個帶有鄰接矩陣、輸入特征、權重和激活函數的完整隱藏層如下:
In [51]: W = np.matrix([ [1, -1], [-1, 1] ]) relu(D_hat**-1 * A_hat * X * W) Out[51]: matrix([[1., 0.], [4., 0.], [2., 0.], [5., 0.]])
在真實場景下的應用
Zachary 空手道俱樂部
Zachary 空手道俱樂部是一個被廣泛使用的社交網絡,其中的節點代表空手道俱樂部的成員,邊代表成員之間的相互關系。當年,Zachary 在研究空手道俱樂部的時候,管理員和教員發生了沖突,導致俱樂部一分為二。下圖顯示了該網絡的圖表征,其中的節點標注是根據節點屬於俱樂部的哪個部分而得到的,「0」表示屬於Mr. Hi部分的中心節點,[32」表示屬於Officer陣營的中心節點,參考https://networkx.github.io/documentation/stable/_modules/networkx/generators/social.html#karate_club_graph。
def plot_graph(G): ''' G: a networkx G ''' %matplotlib notebook import matplotlib.pyplot as plt plt.figure() pos = nx.spring_layout(G) edges = G.edges() nodelist1 = [] nodelist2 = [] for i in range (34): if zkc.nodes[i]['club'] == 'Mr. Hi': nodelist1.append(i) else: nodelist2.append(i) nx.draw_networkx(G, pos, edges=edges); nx.draw_networkx_nodes(G, pos, nodelist=nodelist1, node_size=300, node_color='r',alpha = 0.8) nx.draw_networkx_nodes(G, pos, nodelist=nodelist2, node_size=300, node_color='b',alpha = 0.8) # nx.draw_networkx_edges(G, pos, edgelist=edges,alpha =0.4) plot_graph(zkc)
構建 GCN:
接下來,我們將構建一個圖卷積網絡。我們並不會真正訓練該網絡,但是會對其進行簡單的隨機初始化,從而生成我們在本文開頭看到的特征表征。我們將使用 networkx,它有一個可以很容易實現的 Zachary 空手道俱樂部的圖表征。然后,我們將計算 A_hat 和 D_hat 矩陣。
import networx as nx from networkx import to_numpy_matrix zkc = nx.karate_club_graph() order = sorted(list(zkc.nodes())) A = to_numpy_matrix(zkc, nodelist=order) I = np.eye(zkc.number_of_nodes()) A_hat = A + I D_hat = np.array(np.sum(A_hat, axis=0))[0] D_hat = np.matrix(np.diag(D_hat))
接下來,我們將隨機初始化權重。
W_1 = np.random.normal( loc=0, scale=1, size=(zkc.number_of_nodes(), 4)) W_1
W_2 = np.random.normal( loc=0, size=(W_1.shape[1], 2)) W_2
接着,我們會堆疊 GCN 層。這里,我們只使用單位矩陣作為特征表征,即每個節點被表示為一個 one-hot 編碼的類別變量。
def gcn_layer(A_hat, D_hat, X, W): return relu(D_hat**-1 * A_hat * X * W)
H_1 = gcn_layer(A_hat, D_hat, I, W_1) H_2 = gcn_layer(A_hat, D_hat, H_1, W_2) output = H_2 output
matrix([[0.17394349, 0.12613323], [0.21514969, 0.1248979 ], [0.2200705 , 0.07164891], [0.20434201, 0.16982548], [0.17972395, 0.26591497], [0.13314916, 0.17843399], [0.08887072, 0.11894365], [0.24585268, 0.25294519], [0.17443997, 0.14387933], [0.27246705, 0.07764017], [0.18066635, 0.25648615], [0.2912415 , 0.46735709], [0.23301799, 0.20337298], [0.20719444, 0.21210805], [0.10766761, 0.02108594], [0.1048165 , 0.02436026], [0.01142164, 0.0006152 ], [0.23678846, 0.06878243], [0.14423298, 0. ], [0.16002963, 0.10097575], [0.13788295, 0.11606215], [0.34037411, 0.27910508], [0.14169454, 0.09565058], [0.16444515, 0. ], [0.13790554, 0. ], [0.13951285, 0. ], [0.08406175, 0.04714129], [0.18639611, 0.04326001], [0.20328647, 0.26678944], [0.11548443, 0.04857866], [0.11449148, 0. ], [0.17931359, 0.1911088 ], [0.14008594, 0. ], [0.14183489, 0. ]])
經過多次隨機生成W_1和W_2權重矩陣,得到上圖H_2,但是我發現經過激活函數relu之后,x軸與y軸有很多零值,導致可視化效果很差,可視化效果如下圖,初步分析,可能的原因是權重矩陣是隨機生成的,沒有用后面的具體任務去更新權重矩陣,,畫圖代碼及圖片如下:
import matplotlib.pyplot as plt %matplotlib notebook for i in range (34): if zkc.nodes[i]['club'] == 'Mr. Hi': plt.scatter(np.array(output)[i,0],np.array(output)[i,1] ,label=str(i),color = 'b',alpha=0.5,s = 250) plt.text(np.array(output)[i,0],np.array(output)[i,1] ,i, horizontalalignment='center',verticalalignment='center', fontdict={'color':'black'}) # 為每個點添加標簽,一些形如(x軸,y軸,標簽)的元組,水平及垂直位置,背景顏色 else: plt.scatter(np.array(output)[i,0],np.array(output)[i,1] ,label = 'i',color = 'r',alpha=0.5,s = 250) plt.text(np.array(output)[i,0],np.array(output)[i,1] ,i, horizontalalignment='center',verticalalignment='center', fontdict={'color':'black'}) # plt.scatter(np.array(output)[:,0],np.array(output)[:,1],label = 0:33)
我嘗試去掉激活函數relu,重新運行一遍,發現效果反而更好
def gcn_layer(A_hat, D_hat, X, W): return D_hat**-1 * A_hat * X * W H_1 = gcn_layer(A_hat, D_hat, I, W_1) H_2 = gcn_layer(A_hat, D_hat, H_1, W_2) output = H_2 output
matrix([[ 0.98839777, 1.13734122], [ 0.51031539, 0.47345377], [ 0.77387213, 0.49289261], [ 0.61797444, 1.11746982], [ 2.12295151, 2.43676755], [ 1.56395358, 1.84093717], [ 1.57723571, 1.94485012], [ 0.62368931, 1.0618257 ], [ 0.75132512, 0.99456766], [ 0.55593252, 0.29953574], [ 1.93535421, 2.47420126], [ 0.70997674, 1.38977267], [ 0.68421621, 1.82005162], [ 0.53495206, 1.01818959], [ 0.72965886, -0.50122342], [ 0.48213244, 0.19867772], [ 1.41885305, 1.59985406], [ 0.27414389, 0.76435937], [ 0.60733412, -0.38820873], [ 0.27476357, 0.48294864], [ 0.51044012, 0.21351918], [ 0.45876093, 0.63466151], [ 0.64684601, 0.00845191], [-0.20156276, -1.09758021], [-0.17949636, -1.40743413], [-0.41499531, -1.76745965], [-0.48696632, -0.69577392], [ 0.06628659, -0.50773281], [ 0.91267734, 0.53837441], [-0.30924486, -0.83232449], [ 0.59774103, 0.78497008], [ 0.29614438, -0.24454598], [ 0.72875561, -0.55889401], [ 0.60941032, -0.69855984]])
feature_representations = { node: np.array(output)[node] for node in zkc.nodes()} feature_representations
{0: array([0.98839777, 1.13734122]), 1: array([0.51031539, 0.47345377]), 2: array([0.77387213, 0.49289261]), 3: array([0.61797444, 1.11746982]), 4: array([2.12295151, 2.43676755]), 5: array([1.56395358, 1.84093717]), 6: array([1.57723571, 1.94485012]), 7: array([0.62368931, 1.0618257 ]), 8: array([0.75132512, 0.99456766]), 9: array([0.55593252, 0.29953574]), 10: array([1.93535421, 2.47420126]), 11: array([0.70997674, 1.38977267]), 12: array([0.68421621, 1.82005162]), 13: array([0.53495206, 1.01818959]), 14: array([ 0.72965886, -0.50122342]), 15: array([0.48213244, 0.19867772]), 16: array([1.41885305, 1.59985406]), 17: array([0.27414389, 0.76435937]), 18: array([ 0.60733412, -0.38820873]), 19: array([0.27476357, 0.48294864]), 20: array([0.51044012, 0.21351918]), 21: array([0.45876093, 0.63466151]), 22: array([0.64684601, 0.00845191]), 23: array([-0.20156276, -1.09758021]), 24: array([-0.17949636, -1.40743413]), 25: array([-0.41499531, -1.76745965]), 26: array([-0.48696632, -0.69577392]), 27: array([ 0.06628659, -0.50773281]), 28: array([0.91267734, 0.53837441]), 29: array([-0.30924486, -0.83232449]), 30: array([0.59774103, 0.78497008]), 31: array([ 0.29614438, -0.24454598]), 32: array([ 0.72875561, -0.55889401]), 33: array([ 0.60941032, -0.69855984])}
import matplotlib.pyplot as plt %matplotlib notebook for i in range (34): if zkc.nodes[i]['club'] == 'Mr. Hi': plt.scatter(np.array(output)[i,0],np.array(output)[i,1] ,label=str(i),color = 'b',alpha=0.5,s = 250) plt.text(np.array(output)[i,0],np.array(output)[i,1] ,i, horizontalalignment='center',verticalalignment='center', fontdict={'color':'black'}) # 為每個點添加標簽,一些形如(x軸,y軸,標簽)的元組,水平及垂直位置,背景顏色 else: plt.scatter(np.array(output)[i,0],np.array(output)[i,1] ,label = 'i',color = 'r',alpha=0.5,s = 250) plt.text(np.array(output)[i,0],np.array(output)[i,1] ,i, horizontalalignment='center',verticalalignment='center', fontdict={'color':'black'}) # plt.scatter(np.array(output)[:,0],np.array(output)[:,1],label = 0:33)
你看,這樣的特征表征可以很好地將 Zachary 空手道俱樂部的兩個社區划分開來。至此,我們甚至都沒有開始訓練模型!我們應該注意到,在該示例中由於 ReLU 函數的作用,在 x 軸或 y 軸上隨機初始化的權重很可能為 0。
結語
本文中對圖卷積網絡進行了高水平的的介紹,並說明了 GCN 中每一層節點的特征表征是如何基於其相鄰節點的聚合構建的。讀者可以從中了解到如何使用 numpy 構建這些網絡,以及它們的強大:即使是隨機初始化的 GCN 也可以將 Zachary 空手道俱樂部網絡中的社區分離開來。
在下一篇文章中,我將更詳細地介紹技術細節,並展示如何使用半監督學習實現和訓練最近發布的GCN。。
參考文獻
[1] Blog post on graph convolutional networks by Thomas Kipf.
[2] Paper called Semi-Supervised Classification with Graph Convolutional Networks by Thomas Kipf and Max Welling.
[3] https://blog.csdn.net/weixin_42052081/article/details/89108966