图机器学习(GNN Model )
1. 前言
1.回顾
首先是传统机器学习难以应用在图结构上。回忆一下节点嵌入任务,具体可以参考第三章。其目的在于将节点映射到d维向量,使得在图中相似的节点在向量域中也相似。我们已经学习了 Shallow” Encoding 的方法来进行映射过程,也就是使用一个大矩阵直接储存每个节点的表示向量,通过矩阵与向量乘法来实现嵌入过程。
目前这样的浅层映射主要会出现以下几个问题:
- 需要 o ( ∣ V ∣ ) o(|V|) o(∣V∣)复杂度(矩阵的元素数,即表示向量维度d×节点数|V| )的参数,太多节点间参数不共享,每个节点的表示向量都是完全独特的。
- transductive【1】:无法获取在训练时没出现过的节点的表示向量
- 无法应用节点自身特征信息
【1】归纳式(Inductive)vs 直推式(Transduction)
主要区别是直推式学习时,在训练模型时已经同学用到了训练和测试数据,而归纳式学习训练时只使用了训练数据,在测试时从未见过测试数据。
直推式学习不会建立一个预测模型,如果一个新数据节点加入测试数据集,那么我们需要重新训练模型,然后再预测标签。然而,归纳式学习会建立一个预测模型,遇到新数据节点不需要重新运行算法训练。
简而言之,归纳式学习企图基于已观测训练数据,建立一个通用模型,用来预测任何新数据节点。你可以预测任何数据节点,不局限在无标记数据上。相比之下,直推式学习是基于所有已经观测到的训练和测试数据来建立模型的,这种方法是通过已经有标记的节点信息来预测无标记数据节点。
直推式学习在遇到新的输入数据时会变得比较费事,每当新数据来了,你需要重新运行。然而,归纳式学习一开始就建立了预测模型,因而在新数据来的时候可以用非常少的代价就打上标记。
2. DNN
看下直接用邻居矩阵拼接节点特征,然后直接丢DNN里面是什么情况如
缺点:
1.参数量大,和节点数量+特征维度成正比
2.DNN结构训练好后如果图结构变化,邻接矩阵大小也会变化,此时无法适配原DNN
3.DNN对输入顺序是敏感的,而图是无序的,相同图不同的顺序图的邻接矩阵不一样,DNN无法处理无序的结构
因此,借用CNN的思想,将邻居节点信息汇聚到当前节点,但是在CNN中,卷积核大小是固定的,而图的邻居无法用固定大小的卷积核来处理,因此用的是聚合(aggregation) 思想。
3. deep graph encoders
deep graph encoders也就是用图神经网络GNN来进行节点嵌入。映射函数,即之前在第三章出现的node embedding中的encoder: ENC(v)=基于图结构的多层非线性转换
4. 一个GNN网络的结构如图
5. 通过网络可以解决的任务有
- 节点分类:预测节点的标签
- 链接预测:预测两点是否相连
- 社区发现:识别密集链接的节点簇
- 网络相似性:度量图/子图间的相似性
2. Describe aggregation strategies
1. 核心思想是生成的节点向量是基于其周边邻居
2. 分解后每个节点都有自己的计算图
3. 当然可以有深度的形式:节点在每一层都有不同embedding
- 第0层的embedding就是节点的输入节点的特征信息
- 第k层的embedding可以汇聚第k跳的邻居信息
4. 聚合的基本方式:
上面的聚合(Aggregation)是用的平均的方式(我感觉是求和后归一化,因为分母可以放到求和号前面),其中
B
l
,
W
l
B_l,W_l
Bl,Wl是用于线性变换的参数,也是模型需要训练的参数。虽然每层参数不一样,但是每层的参数是交互的。同时上面的公式也可以写成矩阵的形式:
1.
让
H
(
l
)
=
[
h
1
[
l
]
…
…
h
∣
V
∣
(
l
)
]
T
,
节点
e
m
b
e
d
d
i
n
g
的矩阵形式
2.
邻居消息聚合:
∑
u
∈
N
v
h
u
(
l
)
=
A
v
H
(
l
)
邻接矩阵里面
v
节点这行
3.
D
是度矩阵,是一个对角矩阵:
D
v
,
v
=
D
e
g
(
v
)
=
∣
N
(
v
)
∣
4.
则归一化可以表示为:
D
−
1
=
1
∣
N
(
v
)
∣
(
其实也很好理解,毕竟在邻接矩阵中一行中所有的值加起来才是度,度矩阵这样就天然的满足做归一化的分母
)
5.
因此上图中邻居节点的汇聚的矩阵形式可以写成
:
∑
u
∈
N
v
h
u
(
l
)
N
(
v
)
→
H
(
l
+
1
)
=
D
−
1
A
H
l
6.
因此整个式子可以写成
:
H
l
+
1
=
σ
(
A
~
H
(
l
)
W
l
T
+
H
(
l
)
B
l
T
)
,
其中
A
~
=
D
−
1
A
1.让H^{(l)}=[h_1^{[l]}……h_{|V|}^{(l)}]^T,节点embedding的矩阵形式\\ 2.邻居消息聚合:\sum_{u\in N_v}h_u^{(l)}=A_vH^{(l)}邻接矩阵里面v节点这行\\ 3.D是度矩阵,是一个对角矩阵:D_{v,v}=Deg(v)=|N(v)|\\ 4.则归一化可以表示为:D^{-1}=\frac{1}{|N_{(v)}|}\\ (其实也很好理解,毕竟在邻接矩阵中一行中所有的值加起来才是度,度矩阵这样就天然的满足做归一化的分母)\\ 5.因此上图中邻居节点的汇聚的矩阵形式可以写成: \sum_{u\in N_v}\frac{h_u^{(l)}}{N(v)}\rightarrow H^{(l+1)}=D^{-1}AH^{l}\\ 6.因此整个式子可以写成:H^{l+1}=\sigma(\textcolor{red}{\widetilde{A}H^{(l)}W_l^T}+\textcolor{blue}{H^{(l)}B_l^T}),其中\widetilde{A}=D^{-1}A
1.让H(l)=[h1[l]……h∣V∣(l)]T,节点embedding的矩阵形式2.邻居消息聚合:u∈Nv∑hu(l)=AvH(l)邻接矩阵里面v节点这行3.D是度矩阵,是一个对角矩阵:Dv,v=Deg(v)=∣N(v)∣4.则归一化可以表示为:D−1=∣N(v)∣1(其实也很好理解,毕竟在邻接矩阵中一行中所有的值加起来才是度,度矩阵这样就天然的满足做归一化的分母)5.因此上图中邻居节点的汇聚的矩阵形式可以写成:u∈Nv∑N(v)hu(l)→H(l+1)=D−1AHl6.因此整个式子可以写成:Hl+1=σ(A
H(l)WlT+H(l)BlT),其中A
=D−1A
红色:邻居聚合
蓝色:自身转换
#有向图的时候换成D^-1A,无向图则是D^0.5AD^0.5
def normalize_adj(mx):
"""Row-normalize sparse matrix"""
rowsum = np.array(mx.sum(1)) # 矩阵行求和
r_inv = np.power(rowsum, -1).flatten() # 求和的-1次方
r_inv[np.isinf(r_inv)] = 0. # 如果是inf,转换成0
r_mat_inv = sp.diags(r_inv) # 构造对角戏矩阵
mx = r_mat_inv.dot(mx) # 构造D-1*A,非对称方式,简化方式
return mx
def normalize_features(mx):
"""Row-normalize sparse matrix"""
rowsum = np.array(mx.sum(1))
r_inv = np.power(rowsum, -1).flatten()
r_inv[np.isinf(r_inv)] = 0.
r_mat_inv = sp.diags(r_inv)
mx = r_mat_inv.dot(mx)
return mx
3. GNN Model 设计
1. 如何训练GNN
节点嵌入 z u z_u zu
- 有监督学习:直接进行训练,优化目标 m i n θ L ( Y , f ( z v ) ) min_\theta L(Y,f(z_v)) minθL(Y,f(zv))
1. 如回归问题可以用L2 loss,分类问题可以用交叉熵
2. 比如二分类交叉熵:
L
=
−
∑
v
∈
V
(
y
v
l
o
g
(
σ
(
z
v
T
θ
)
)
+
(
1
−
y
v
)
l
o
g
(
1
−
σ
(
z
v
T
θ
)
)
)
L=-\sum_{v\in V}(y_vlog(\sigma(z_v^T\theta))+(1-y_v)log(1-\sigma(z_v^T\theta)))
L=−∑v∈V(yvlog(σ(zvTθ))+(1−yv)log(1−σ(zvTθ)))
1.
y
v
就是节点分类的标签
2.
z
v
T
就是编码器的输入,节点的嵌入向量
3.
θ
就是分类中的权重,也就是训练的参数
1.y_v就是节点分类的标签\\ 2.z_v^T就是编码器的输入,节点的嵌入向量\\ 3.\theta 就是分类中的权重,也就是训练的参数
1.yv就是节点分类的标签2.zvT就是编码器的输入,节点的嵌入向量3.θ就是分类中的权重,也就是训练的参数
- 无监督学习:用图结构作为学习目标
1. 比如节点相似性(随机游走、矩阵分解、图中节点相似性……等)
2. L = ∑ z u , z v C E ( y u , v , D E C ( z u , z v ) ) L=\sum_{z_u,z_v}CE(y_{u,v},DEC(z_u,z_v)) L=∑zu,zvCE(yu,v,DEC(zu,zv))
1. 如果u和v相似,则 y u , v = 1 y_{u,v}=1 yu,v=1
2. CE是交叉熵
3. DEC是节点嵌入的decoder(如内积)
2. 模型设计
1. 单层GNN
GNN Layer = Message + Aggregation
注意事项:
通常是用 h v l − 1 来计算 h v l h_v^{l-1}来计算h_v^{l} hvl−1来计算hvl,生成消息要生成节点本身的消息,而且本身的消息和邻居的消息使用不同的参数,例如:
邻居的消息计算完毕后进行aggregation,然后再把邻居汇聚的结果(黄色虚线框)和节点本身的消息(红色虚线框)进行concat或者sum,例如:
几种经典的模型(GCN,GrahphSAGE,GAT),都有着自己独特的消息传递和聚合的方式
2. 多层GNN
- 连接GNN网络层部分
- 连接GNN网络层的标准方式:按序堆叠。输入原始节点特征,输出L层后计算得到的节点嵌入向量
4. GNN模型设计中出现的问题
1. 多层GNN中出现over-smoothing 过渡平滑的问题
当GNN层数过深,所有的节点的embedding都趋向一致。(如果我们想用节点嵌入做节点分类任务,这就凉了)这里要提到Receptive field概念,类似CNN中的感受野,例如:
由于节点的embedding基本是由Receptive field覆盖的节点决定的,Receptive field越大,重合的节点越多,两个节点的embedding就越相似,这个现象就是over-smoothing。
总之:堆叠很多GNN网络层→节点具有高度重合的感受野→节点嵌入高度相似→过平滑问题
2. 解决过渡平滑的方案
- 不要用太深的GNN,而是加强每层GNN的表达能力,例如将aggregation中简单的线性变换,换成DNN。
- 加入不传递消息的GNN层,也就是非GNN层,如对每个节点应用MLP(在GNN层之前或之后均可,分别叫 pre-process layers 和 post-process layers)
pre-processing layers:如果节点特征必须经过编码就很重要(如节点表示图像/文字时)
post-processing layers:如果在节点嵌入的基础上需要进行推理和转换就很重要(如图分类、知识图谱等任务中)
5.如何让模型变得更深
1.增加skip connections【2】
如果实际任务还是需要很多层GNN网络,那么可以在GNN模型中增加skip connections(残差连接的方式)通过对过平滑问题进行观察,我们可以发现,靠前的GNN层可能能更好地区分节点。(就很明显嘛这事)因此我们可以在最终节点嵌入中增加靠前GNN层的影响力,实现方法是在GNN中直接添加捷径,保存上一层节点的嵌入向量其作用类似变相制造了多个路径,相当于做了多个模型的混合,就好比你找一个人来做评估,得到结果比较单一,找一帮人来评估,得到的结果就比较具有鲁棒性。
【2】相当于制造了多个模型(如图所示),N个skip connections就相当于创造了 2 N 2^N 2N 条路径,每一条路径最多有N个模块。这些路径都会对最终的节点嵌入产生影响,相当于自动获得了一个浅GNN和深GNN的融合模型。
2.skip connections示例
- 标准的GCN层是橘色的部分F(x),带有skip connections的GCN层则是添加了蓝色的部分X
- skip connections也可以跨多层,直接跨到最后一层,在最后一层聚合之前各层的嵌入(通过concat / pooling / LSTM)