首先,假如我们定义一个极简的传递规则

A是邻接矩阵,X是特征矩阵, 其物理意义就是 通过矩阵乘法操作,批量把图中的相邻节点汇聚到当前节点。
但是由于A的对角线都是 0.因此自身的节点特征会被过滤掉。
图神经网络的核心是 吸周围之精华,再叠加自身,因而需要改进来保留自身特征。如何做?
方法是给每个节点添加一个自环,即将邻接矩阵对角线值各加1,此时用
表示,
做到了聚合邻居节点并保留自身信息。
但是当图过于复杂时,聚合邻居信息会不断执行矩阵乘法或加法,可能导致特征值太大而溢出。如何做?
方法是邻接矩阵归一化。那么如何归一化呢?我们由A可以得到图的度D,由于A变成了
,我们认为
的度为
。常用的归一化方式就是用度数矩阵的倒数
。
则

但是
仅仅对矩阵A进行了列上的缩放,操作后的元素值是不对称的,某种程度破坏了图结构的对称性。(这是为什么?)那么如何修复这种对称性呢?
方法是在行的方向上也进行对等缩放,具体 做法是,让邻接矩阵
右乘一个缩放因子
,这样就使得缩放版本的邻接矩阵重新恢复对称性。于是信息聚合的方式为

能够很好地缩放邻接矩阵,既然-1次幂可以完成,为什么不尝试一下(-1/2)次幂呢?
事实上,对每个矩阵元素都实施
这种操作可以对邻接矩阵地每一行每一列”无偏差“地进行一次归一化,以防相邻节点间度数不匹配对归一化地影响。(why)?
于是就出现了被众多学术论文广泛采纳地邻接矩阵地缩放形式

上述仅仅考虑到邻接矩阵对获取邻居节点信息的影响,即只考虑拓扑结构施加的影响。事实上,对于特定节点而言,不同维度的特征值对给定任务的影响程度是不同的,如果第对各个特征值进行时 打分就,就要涉及到权值矩阵W了,也就是要构造更为完整的图神经网络模型 AWX。权值矩阵W通常是通过学习得到的。

如果我们想压缩节点输出的维度,也可以缩减权值矩阵的输出维度。
在以上的分析中,没有考虑激活函数的影响,无法给予神经网络的非线性变换能力,因此通常我们需要使用sigmoid、tanh、Relu等作为激活函数,最后再用argmax函数模拟一个分类的输出。
reference:
code:
- import networkx as nx
- import matplotlib.pyplot as plt
- import numpy as np
-
- #定义节点
- N = [(f"v{i}", 0) for i in range (1,3)] + [(f"v{i}",1) for i in range (3,5)] + [(f"v{i}",2) for i in range (5,6)] #定义节点
-
- #定义边
- E = [("v1","v2"),("v1","v3"),
- ("v2","v1"),("v2","v3"),("v2","v4"),
- ("v3","v1"),("v3","v2"),("v3","v4"),
- ("v4","v2"),("v4","v3"),("v4","v5"),
- ("v5","v4")
- ] #定义边
-
- G = nx.Graph() #构造图
-
- G.add_nodes_from(list(map(lambda x: x[0],N))) #给图添加节点
- G.add_edges_from(E) #给图添加边
-
- ncolor =['r']*2 + ['b']*2 +['g']*1 #设置节点颜色
- nsize = [700]*2 + [700]*2 + [700]*1 #设置节点的大小
-
- #显示图
- nx.draw(G, with_labels= True, font_weight ='bold', font_color = 'w', node_color =ncolor, node_size =nsize)
- plt.show()
-
- #借用nx构造邻接矩阵
- A = np.array(nx.adjacency_matrix(G).todense())
- print(A)
-
- #构造特征矩阵X
- X = np.array([[i,-i, i+2] for i in range (A.shape[0])])
- print(X)
-
- #为了不丢失自己的属性,需要修改本身的邻接矩阵,因为最初邻接矩阵的斜对角线为0
- I = np.eye(A.shape[0])
- A_hat = A + I
- print('A_hat')
- print(A_hat)
-
- #计算自环邻接矩阵的度
- D_hat = np.diag(np.sum(A_hat,axis= 0 ))
- print(D_hat)
-
- #获取D——hat的逆矩阵,即一个缩放因子
- D_1 = np.diag(D_hat) ** (-1) *np.eye(A_hat.shape[0])
- print('D_1')
- print(D_1)
-
- #缩放版的邻接矩阵
- A_scale = D_1 @ A_hat #对矩阵A仅仅进行了列方向上的缩放
- print('A_scale')
- print(A_scale)
-
- #用A_scale来聚合邻居节点的信息
- X_new = A_scale @ X
- print('X_new')
- print(X_new)
-
- #修复原本的缩放的不对称性
- scale_factor = D_1 @ A_hat @ D_1 #scale_factor 是对称的,而 A_scale是不对称 的
- print('scale_factor')
- print(scale_factor)
-
-
- #用scale_factor来聚合邻居节点的信息
- X_new1 = scale_factor @ X
- print('X_new1')
- print(X_new1)
-
-
- D_sq_half = np.diag(D_hat) ** (-0.5) *np.eye(A_hat.shape[0])
- print('D_sq_half')
- print(D_sq_half)
-
- #修复原本的缩放的不对称性
- scale_factor2 = D_sq_half @ A_hat @ D_sq_half #scale_factor 是对称的,而 A_scale是不对称 的
- print('scale_factor2')
- print(scale_factor2)
-
- #用scale_factor2来聚合邻居节点的信息
- X_new2 = scale_factor2 @ X
- print('X_new2')
- print(X_new2)
-
- #给出的权值矩阵
- W = np.array([[0.13,0.24],
- [0.37,-0.32],
- [0.14,-0.15]])
-
- X_new3 = X_new2 @ W
- print(X_new3)
-
- #也可以缩减W的尺寸压缩节点的输出维度
- W1 = np.array([[0.13],
- [0.37],
- [0.14]])
- #计算logits
- logits = X_new2 @ W1
- print(logits)
-
- #以上都没有考虑到激活函数,无法模拟神经网络的非线性变换能力,可以使用激活函数
- y = logits * (logits >0) #使用Relu函数
- print(y)
-
- #为了实现分类等功能,还需要添加一层Softmax
- def softmax(x):
- return np.exp(x) /np.sum(np.exp(x), axis = 0)
-
- prob = softmax(y)
- print('y')
- print(y)
-
- #模拟一个分类输出
- pred = np.argmax(prob)
- print(pred)
-
-
-