圖卷積神經網路(GCN)理解與tensorflow2.0程式碼實現
圖(Graph),一般用 G = ( V , E ) G=(V,E) G=(V,E) 表示,這裡的 V V V是圖中節點的集合, E E E 為邊的集合,節點的個數用 N N N表示。在一個圖中,有三個比較重要的矩陣:
- 特徵矩陣 X X X:維度為 N × D N\times D N×D ,表示圖中有N個節點,每個節點的特徵個數是D。
- 鄰居矩陣 A A A:維度為 N × N N\times N N×N ,表示圖中N個節點之間的連線關係。
- 度矩陣 D D D:維度為 N × N N\times N N×N ,是一個對角矩陣,即只有對角線上不為零,其他位置元素都是 0 ,表示圖中N個節點與其他節點相連的邊的個數。對於無權圖而言, D i i = ∑ j A i j D_{ii}=\sum_j A_{ij} Dii=∑jAij 。
鄰接矩陣與度矩陣例子如下圖所示:
對於影像(Image)資料,我們可以用卷積核來提取特徵,無論卷積核覆蓋在影像的哪個部分,其內部結構都是一樣的,這是因為圖片結構具有平移不變性,如下圖左半部分所示:
但是對於圖(Graph)資料而言,其形狀是不規則的,不具有平移不變性。於是 GCN,也就是圖卷積神經網路,其目標就是設計一種特徵提取器,進而完成節點分類、變預測等任務,還順便可以得到每個節點的 embedding 表示。
上面展示了一個簡單的 3x3 的卷積核,每次自左向右,自上而下掃描圖片(Image)時,都是將 3x3 的畫素進行加權求和,即: ∑ i = 1 9 w i x i \sum_{i=1}^9 w_i x_i ∑i=19wixi,然後將求和的結果作為該 3x3 區域的特徵。
那麼在圖(graph)中要怎麼提取特徵?這裡給出兩種思路。
圖卷積
思路一
CNN加權求和的思想也可以應用到圖(Graph)的特徵提取上,如下圖所示:
對於節點
i
i
i,我們可以用其鄰接節點加權求和的結果來表示當前節點,這個操作我們稱為“聚合(aggregate)”:
a
g
g
(
X
i
)
=
∑
j
∈
n
e
i
g
h
b
o
r
(
i
)
A
i
j
X
j
agg(X_i) = \sum_{j \in neighbor(i)} A_{ij} X_j
agg(Xi)=j∈neighbor(i)∑AijXj
考慮到與節點
i
i
i 沒有邊連線的節點
j
j
j ,對應的權重
A
i
j
A_{ij}
Aij 為 0 ,因此上面的公式又可以改寫為:
a
g
g
(
X
i
)
=
∑
j
∈
N
A
i
j
X
j
agg(X_i) = \sum_{j \in N} A_{ij} X_j
agg(Xi)=j∈N∑AijXj
那麼,對於所有的節點而言,其聚合的結果可以用下面的公式表示:
a
g
g
(
X
)
=
A
X
agg(X) = AX
agg(X)=AX
上面的公式只考慮了鄰居加權求和的結果,很多情況下,節點自身的資訊是不可忽略的,因此一般情況下會把自身的特徵也加回來:
a
g
g
(
X
i
)
=
∑
j
∈
N
A
i
j
X
j
+
X
i
agg(X_i) = \sum_{j \in N} A_{ij} X_j + X_i
agg(Xi)=j∈N∑AijXj+Xi
於是有:
a
g
g
(
X
)
=
A
X
+
X
=
(
A
+
I
)
X
agg(X) = AX + X = (A+I)X
agg(X)=AX+X=(A+I)X
其中,
I
I
I 是單位矩陣,令:
A
~
=
A
+
I
\tilde A = A+I
A~=A+I
則有:
a
g
g
(
X
)
=
A
~
X
agg(X) = \tilde AX
agg(X)=A~X
也就是說把單位矩陣
I
I
I 加到鄰接矩陣
A
A
A 上,即可在聚合操作中加入自身特徵了。
現在有個問題,只能用自身節點以及鄰居節點加權求和的結果來表示某個節點的特徵嗎?其實還有另一種思路。
思路二
在某些情況下,我們更關注節點之間的差值,因此可以對差值進行加權求和:
a
g
g
(
X
i
)
=
∑
j
∈
N
A
i
j
(
X
i
−
X
j
)
=
D
i
i
X
i
−
∑
j
∈
N
A
i
j
X
j
\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)=DiiXi−j∈N∑AijXj
其中,D 表示度矩陣,表示節點與其他節點相連的邊的個數,對於無權圖而言,
D
i
i
=
∑
j
A
i
j
D_{ii}=\sum_j A_{ij}
Dii=∑jAij 。
對於整個圖的節點而言,上面的公式可以轉換為矩陣化的表示:
a
g
g
(
X
)
=
D
X
−
A
X
=
(
D
−
A
)
X
\begin{aligned} agg(X) &= DX - AX \\ &= (D-A)X \end{aligned}
agg(X)=DX−AX=(D−A)X
實際上,上面公式中的 D − A D-A D−A 是拉普拉斯矩陣(用 L L L 表示):
L = D − A L = D - A L=D−A
拉普拉斯矩陣如下圖所示:
如果想更多地瞭解拉普拉斯矩陣在GCN中的作用,可以參考:如何理解 Graph Convolutional Network(GCN)?
歸一化
無論是思路一的 A ~ \tilde A A~ 還是思路二的 L L L,與CNN的卷積相似之處都是區域性資料的聚合操作,只不過CNN 中卷積的區域性連線數是固定的。但是在Graph中每個節點的鄰居個數都可能不同,進行聚合操作後,對於度較大的節點,得到的特徵比較大,度較少的節點得到的特徵就比較小,因此還需要進行歸一化的處理。
歸一化的思路有兩種:
(1)算數平均
L
r
w
=
D
−
1
L
L^{rw}=D^{-1}L
Lrw=D−1L
(2)幾何平均
L
s
y
m
=
D
−
0.5
L
D
−
0.5
L^{sym}=D^{-0.5}LD^{-0.5}
Lsym=D−0.5LD−0.5
幾何平均受極端值影響較小,因此是GCN中比較常用的歸一化方法,於是有:
a
g
g
(
X
)
=
L
s
y
m
X
=
D
−
0.5
L
D
−
0.5
X
=
D
−
0.5
(
D
−
A
)
D
−
0.5
X
\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
當然也可以是:
a
g
g
(
X
)
=
D
−
0.5
A
~
D
−
0.5
X
=
D
−
0.5
(
A
+
I
)
D
−
0.5
X
\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程式碼實現中,會對聚合結果進行一些變換,第
l
l
l 層到第
l
+
1
l+1
l+1 層的傳播方式為:
H
(
l
+
1
)
=
σ
(
D
~
−
1
2
A
~
D
~
−
1
2
H
(
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~−21A~D~−21H(l)W(l))
其中:
- A ~ = A + I \tilde A=A+I A~=A+I ,也可以是 A ~ = D − A \tilde A = D - A A~=D−A
- D ~ \tilde D D~ 是 A ~ \tilde A A~ 的度矩陣,每個元素為: D ~ i i = ∑ j A ~ i j \tilde D_{ii}=\sum_j \tilde A_{ij} D~ii=∑jA~ij
- H H H 是每一層的特徵,對於輸入層而言, H H H 就是 X X X
- σ 是 sigmoid 函式
由於 D 是在矩陣 A 的基礎上得到的,因此在給定矩陣 A 之後, D ~ − 1 2 A ~ D ~ − 1 2 \tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}} D~−21A~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, initializers
class 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 = units
self.activation = activations.get(activation)
self.use_bias = use_bias
self.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]
# 計算 XW
if isinstance(self.X, tf.SparseTensor):
h = tf.sparse.sparse_dense_matmul(self.X, self.weight)
else:
h = tf.matmul(self.X, self.weight)
# 計算 AXW
output = 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 = True
self.with_bias = True
self.lr = FLAGS.learning_rate
self.dropout = FLAGS.dropout
self.verbose = FLAGS.verbose
self.An = An
self.X = X
self.layer_sizes = sizes
self.shape = An.shape
self.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() + 1
train_losses = []
val_losses = []
# use adam to optimize
for 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 weights
grad_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 training
train_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_losses
def 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_tf
self.h1 = self.layer1([self.An_tf, _X])
if training:
_h1 = tf.nn.dropout(self.h1, self.dropout)
else:
_h1 = self.h1
self.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 _loss
def 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_half
return 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的缺點:屬於直推式(transductive)的學習方式,需要把所有節點都參與訓練才能得到node embedding,無法快速得到新node的embedding。如果要得到新節點的表示,很可能就需要重新訓練所有節點的表示。
參考文章:
相關文章
- 深入淺出圖神經網路 GCN程式碼實戰神經網路GC
- 圖卷積神經網路分類的pytorch實現卷積神經網路PyTorch
- TensorFlow上實現卷積神經網路CNN卷積神經網路CNN
- Keras上實現卷積神經網路CNNKeras卷積神經網路CNN
- 卷積神經網路卷積神經網路
- 使用卷積神經網路實現圖片去摩爾紋卷積神經網路
- 文科生如何理解卷積神經網路?卷積神經網路
- 卷積神經網路數學原理解析卷積神經網路
- 卷積神經網路(CNN)介紹與實踐卷積神經網路CNN
- 卷積神經網路的原理及Python實現卷積神經網路Python
- 《卷積神經網路的Python實現》筆記卷積神經網路Python筆記
- 卷積神經網路概述卷積神經網路
- 解密卷積神經網路!解密卷積神經網路
- 5.2.1 卷積神經網路卷積神經網路
- 卷積神經網路CNN卷積神經網路CNN
- 卷積神經網路-AlexNet卷積神經網路
- 卷積神經網路-1卷積神經網路
- 卷積神經網路-2卷積神經網路
- 卷積神經網路-3卷積神經網路
- [譯] 使用 Python 和 Keras 實現卷積神經網路PythonKeras卷積神經網路
- 【python實現卷積神經網路】開始訓練Python卷積神經網路
- 卷積神經網路四種卷積型別卷積神經網路型別
- 全卷積神經網路FCN卷積神經網路
- 深度剖析卷積神經網路卷積神經網路
- Matlab程式設計之——卷積神經網路CNN程式碼解析Matlab程式設計卷積神經網路CNN
- TensorFlow實戰卷積神經網路之LeNet卷積神經網路
- 利用Python實現卷積神經網路的視覺化Python卷積神經網路視覺化
- 卷積神經網路鼻祖LeNet網路分析卷積神經網路
- CNN神經網路之卷積操作CNN神經網路卷積
- 卷積神經網路 part2卷積神經網路
- 14 卷積神經網路(進階)卷積神經網路
- 卷積神經網路(CNN)詳解卷積神經網路CNN
- 何為神經網路卷積層?神經網路卷積
- Tensorflow-卷積神經網路CNN卷積神經網路CNN
- 卷積神經網路中感受野的理解和計算卷積神經網路
- 【Tensorflow_DL_Note6】Tensorflow實現卷積神經網路(1)卷積神經網路
- 【Tensorflow_DL_Note7】Tensorflow實現卷積神經網路(2)卷積神經網路
- TensorFlow 一步一步實現卷積神經網路卷積神經網路