图卷积神经网络(GCN)理解与tensorflow2.0代码实现
圖(Graph),一般用 G=(V,E)G=(V,E)G=(V,E) 表示,這里的VVV是圖中節點的集合,EEE 為邊的集合,節點的個數用NNN表示。在一個圖中,有三個比較重要的矩陣:
鄰接矩陣與度矩陣例子如下圖所示:
對于圖像(Image)數據,我們可以用卷積核來提取特征,無論卷積核覆蓋在圖像的哪個部分,其內部結構都是一樣的,這是因為圖片結構具有平移不變性,如下圖左半部分所示:
但是對于圖(Graph)數據而言,其形狀是不規則的,不具有平移不變性。于是 GCN,也就是圖卷積神經網絡,其目標就是設計一種特征提取器,進而完成節點分類、變預測等任務,還順便可以得到每個節點的 embedding 表示。
上面展示了一個簡單的 3x3 的卷積核,每次自左向右,自上而下掃描圖片(Image)時,都是將 3x3 的像素進行加權求和,即:∑i=19wixi\sum_{i=1}^9 w_i x_i∑i=19?wi?xi?,然后將求和的結果作為該 3x3 區域的特征。
那么在圖(graph)中要怎么提取特征?這里給出兩種思路。
圖卷積
思路一
CNN加權求和的思想也可以應用到圖(Graph)的特征提取上,如下圖所示:
對于節點 iii,我們可以用其鄰接節點加權求和的結果來表示當前節點,這個操作我們稱為“聚合(aggregate)”:
agg(Xi)=∑j∈neighbor(i)AijXjagg(X_i) = \sum_{j \in neighbor(i)} A_{ij} X_j agg(Xi?)=j∈neighbor(i)∑?Aij?Xj?
考慮到與節點 iii 沒有邊連接的節點 jjj ,對應的權重 AijA_{ij}Aij? 為 0 ,因此上面的公式又可以改寫為:
agg(Xi)=∑j∈NAijXjagg(X_i) = \sum_{j \in N} A_{ij} X_j agg(Xi?)=j∈N∑?Aij?Xj?
那么,對于所有的節點而言,其聚合的結果可以用下面的公式表示:
agg(X)=AXagg(X) = AX agg(X)=AX
上面的公式只考慮了鄰居加權求和的結果,很多情況下,節點自身的信息是不可忽略的,因此一般情況下會把自身的特征也加回來:
agg(Xi)=∑j∈NAijXj+Xiagg(X_i) = \sum_{j \in N} A_{ij} X_j + X_i agg(Xi?)=j∈N∑?Aij?Xj?+Xi?
于是有:
agg(X)=AX+X=(A+I)Xagg(X) = AX + X = (A+I)X agg(X)=AX+X=(A+I)X
其中,III 是單位矩陣,令:
A~=A+I\tilde A = A+I A~=A+I
則有:
agg(X)=A~Xagg(X) = \tilde AX agg(X)=A~X
也就是說把單位矩陣 III 加到鄰接矩陣 AAA 上,即可在聚合操作中加入自身特征了。
現在有個問題,只能用自身節點以及鄰居節點加權求和的結果來表示某個節點的特征嗎?其實還有另一種思路。
思路二
在某些情況下,我們更關注節點之間的差值,因此可以對差值進行加權求和:
agg(Xi)=∑j∈NAij(Xi?Xj)=DiiXi?∑j∈NAijXj\begin{aligned} agg(X_i) & = \sum_{j \in N} A_{ij} (X_i - X_j) \\ &= D_{ii}X_i- \sum_{j \in N} A_{ij}X_j \\ \end{aligned} agg(Xi?)?=j∈N∑?Aij?(Xi??Xj?)=Dii?Xi??j∈N∑?Aij?Xj??
其中,D 表示度矩陣,表示節點與其他節點相連的邊的個數,對于無權圖而言,Dii=∑jAijD_{ii}=\sum_j A_{ij}Dii?=∑j?Aij? 。
對于整個圖的節點而言,上面的公式可以轉換為矩陣化的表示:
agg(X)=DX?AX=(D?A)X\begin{aligned} agg(X) &= DX - AX \\ &= (D-A)X \end{aligned} agg(X)?=DX?AX=(D?A)X?
實際上,上面公式中的 D?AD-AD?A 是拉普拉斯矩陣(用 LLL 表示):
L=D?AL = D - A L=D?A
拉普拉斯矩陣如下圖所示:
如果想更多地了解拉普拉斯矩陣在GCN中的作用,可以參考:如何理解 Graph Convolutional Network(GCN)?
歸一化
無論是思路一的 A~\tilde AA~ 還是思路二的 LLL,與CNN的卷積相似之處都是局部數據的聚合操作,只不過CNN 中卷積的局部連接數是固定的。但是在Graph中每個節點的鄰居個數都可能不同,進行聚合操作后,對于度較大的節點,得到的特征比較大,度較少的節點得到的特征就比較小,因此還需要進行歸一化的處理。
歸一化的思路有兩種:
(1)算數平均
Lrw=D?1LL^{rw}=D^{-1}L Lrw=D?1L
(2)幾何平均
Lsym=D?0.5LD?0.5L^{sym}=D^{-0.5}LD^{-0.5} Lsym=D?0.5LD?0.5
幾何平均受極端值影響較小,因此是GCN中比較常用的歸一化方法,于是有:
agg(X)=LsymX=D?0.5LD?0.5X=D?0.5(D?A)D?0.5X\begin{aligned} agg(X) &= L^{sym} X \\ &= D^{-0.5}LD^{-0.5}X \\ &= D^{-0.5}(D-A)D^{-0.5} X \end{aligned} agg(X)?=LsymX=D?0.5LD?0.5X=D?0.5(D?A)D?0.5X?
當然也可以是:
agg(X)=D?0.5A~D?0.5X=D?0.5(A+I)D?0.5X\begin{aligned} agg(X) & = D^{-0.5}\tilde A D^{-0.5} X\\ & = D^{-0.5}(A+I)D^{-0.5} X \end{aligned} agg(X)?=D?0.5A~D?0.5X=D?0.5(A+I)D?0.5X?
在實際的GCN代碼實現中,會對聚合結果進行一些變換,第 lll 層到第 l+1l+1l+1 層的傳播方式為:
H(l+1)=σ(D~?12A~D~?12H(l)W(l))H^{(l+1)}=\sigma\left(\tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}} H^{(l)} W^{(l)}\right) H(l+1)=σ(D~?21?A~D~?21?H(l)W(l))
其中:
- A~=A+I\tilde A=A+IA~=A+I ,也可以是 A~=D?A\tilde A = D - AA~=D?A
- D~\tilde DD~ 是 A~\tilde AA~ 的度矩陣,每個元素為:D~ii=∑jA~ij\tilde D_{ii}=\sum_j \tilde A_{ij}D~ii?=∑j?A~ij?
- HHH 是每一層的特征,對于輸入層而言,HHH 就是 XXX
- σ 是 sigmoid 函數
由于 D 是在矩陣 A 的基礎上得到的,因此在給定矩陣 A 之后,D~?12A~D~?12\tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}}D~?21?A~D~?21? 就可以事先計算好。
代碼實現
相關代碼可以在文末獲取。
Cora 數據集介紹
Cora數據集由機器學習論文組成,是近年來圖深度學習很喜歡使用的數據集。整個數據集有2708篇論文,所有樣本點被分為8個類別,類別分別是1)基于案例;2)遺傳算法;3)神經網絡;4)概率方法;5)強化學習;6)規則學習;7)理論。每篇論文都由一個1433維的詞向量表示,所以,每個樣本點具有1433個特征。詞向量的每個元素都對應一個詞,且該元素只有0或1兩個取值。取0表示該元素對應的詞不在論文中,取1表示在論文中。
定義圖卷積層
import tensorflow as tf from tensorflow.keras import activations, regularizers, constraints, initializersclass GCNConv(tf.keras.layers.Layer):def __init__(self,units,activation=lambda x: x,use_bias=True,kernel_initializer='glorot_uniform',bias_initializer='zeros',**kwargs):super(GCNConv, self).__init__()self.units = unitsself.activation = activations.get(activation)self.use_bias = use_biasself.kernel_initializer = initializers.get(kernel_initializer)self.bias_initializer = initializers.get(bias_initializer)def build(self, input_shape):""" GCN has two inputs : [shape(An), shape(X)]"""fdim = input_shape[1][1] # feature dim# 初始化權重矩陣self.weight = self.add_weight(name="weight",shape=(fdim, self.units),initializer=self.kernel_initializer,trainable=True)if self.use_bias:# 初始化偏置項self.bias = self.add_weight(name="bias",shape=(self.units, ),initializer=self.bias_initializer,trainable=True)def call(self, inputs):""" GCN has two inputs : [An, X]"""self.An = inputs[0]self.X = inputs[1]# 計算 XWif isinstance(self.X, tf.SparseTensor):h = tf.sparse.sparse_dense_matmul(self.X, self.weight)else:h = tf.matmul(self.X, self.weight)# 計算 AXWoutput = tf.sparse.sparse_dense_matmul(self.An, h)if self.use_bias:output = tf.nn.bias_add(output, self.bias)if self.activation:output = self.activation(output)return output定義 GCN 模型
class GCN():def __init__(self, An, X, sizes, **kwargs):self.with_relu = Trueself.with_bias = Trueself.lr = FLAGS.learning_rateself.dropout = FLAGS.dropoutself.verbose = FLAGS.verboseself.An = Anself.X = Xself.layer_sizes = sizesself.shape = An.shapeself.An_tf = sp_matrix_to_sp_tensor(self.An)self.X_tf = sp_matrix_to_sp_tensor(self.X)self.layer1 = GCNConv(self.layer_sizes[0], activation='relu')self.layer2 = GCNConv(self.layer_sizes[1])self.opt = tf.optimizers.Adam(learning_rate=self.lr)def train(self, idx_train, labels_train, idx_val, labels_val):K = labels_train.max() + 1train_losses = []val_losses = []# use adam to optimizefor it in range(FLAGS.epochs):tic = time()with tf.GradientTape() as tape:_loss = self.loss_fn(idx_train, np.eye(K)[labels_train])# optimize over weightsgrad_list = tape.gradient(_loss, self.var_list)grads_and_vars = zip(grad_list, self.var_list)self.opt.apply_gradients(grads_and_vars)# evaluate on the trainingtrain_loss, train_acc = self.evaluate(idx_train, labels_train, training=True)train_losses.append(train_loss)val_loss, val_acc = self.evaluate(idx_val, labels_val, training=False)val_losses.append(val_loss)toc = time()if self.verbose:print("iter:{:03d}".format(it),"train_loss:{:.4f}".format(train_loss),"train_acc:{:.4f}".format(train_acc),"val_loss:{:.4f}".format(val_loss),"val_acc:{:.4f}".format(val_acc),"time:{:.4f}".format(toc - tic))return train_lossesdef loss_fn(self, idx, labels, training=True):if training:# .nnz 是獲得X中元素的個數_X = sparse_dropout(self.X_tf, self.dropout, [self.X.nnz])else:_X = self.X_tfself.h1 = self.layer1([self.An_tf, _X])if training:_h1 = tf.nn.dropout(self.h1, self.dropout)else:_h1 = self.h1self.h2 = self.layer2([self.An_tf, _h1])self.var_list = self.layer1.weights + self.layer2.weights# calculate the loss base on idx and labels_logits = tf.gather(self.h2, idx)_loss_per_node = tf.nn.softmax_cross_entropy_with_logits(labels=labels,logits=_logits)_loss = tf.reduce_mean(_loss_per_node)# 加上 l2 正則化項_loss += FLAGS.weight_decay * sum(map(tf.nn.l2_loss, self.layer1.weights))return _lossdef evaluate(self, idx, true_labels, training):K = true_labels.max() + 1_loss = self.loss_fn(idx, np.eye(K)[true_labels], training=training).numpy()_pred_logits = tf.gather(self.h2, idx)_pred_labels = tf.argmax(_pred_logits, axis=1).numpy()_acc = accuracy_score(_pred_labels, true_labels)return _loss, _acc訓練模型
# 計算標準化的鄰接矩陣:根號D * A * 根號D def preprocess_graph(adj):# _A = A + I_adj = adj + sp.eye(adj.shape[0])# _dseq:各個節點的度構成的列表_dseq = _adj.sum(1).A1# 構造開根號的度矩陣_D_half = sp.diags(np.power(_dseq, -0.5))# 計算標準化的鄰接矩陣, @ 表示矩陣乘法adj_normalized = _D_half @ _adj @ _D_halfreturn adj_normalized.tocsr()if __name__ == "__main__":# 讀取數據# A_mat:鄰接矩陣,以scipy的csr形式存儲# X_mat:特征矩陣,以scipy的csr形式存儲# z_vec:label# train_idx,val_idx,test_idx: 要使用的節點序號A_mat, X_mat, z_vec, train_idx, val_idx, test_idx = load_data_planetoid(FLAGS.dataset)# 鄰居矩陣標準化An_mat = preprocess_graph(A_mat)# 節點的類別個數K = z_vec.max() + 1# 構造GCN模型gcn = GCN(An_mat, X_mat, [FLAGS.hidden1, K])# 訓練gcn.train(train_idx, z_vec[train_idx], val_idx, z_vec[val_idx])# 測試test_res = gcn.evaluate(test_idx, z_vec[test_idx], training=False)print("Dataset {}".format(FLAGS.dataset),"Test loss {:.4f}".format(test_res[0]),"test acc {:.4f}".format(test_res[1]))GCN 小結
本文使用到的代碼與數據集地址:https://github.com/zxxwin/tf2_gcn
GCN的優點: 可以捕捉graph的全局信息,從而很好地表示node的特征。
GCN的缺點:
參考文章:
如何理解 Graph Convolutional Network(GCN)?
2020年,我終于決定入門GCN
GCN(Graph Convolutional Network)的理解
總結
以上是生活随笔為你收集整理的图卷积神经网络(GCN)理解与tensorflow2.0代码实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 杭州公积金最高基数
- 下一篇: Mac中如何查看电脑的IP地址