Heterogeneous Information Network
传统的同构图(Homogeneous Graph)中只存在一种类型的节点和边,当图中的节点和边存在多种类型和各种复杂的关系时,再采用Homo的处理方式就不太可行了。这个时候不同类型的节点具有不同的特征,其特征可能落在不同的特征空间中,如果仍然共享网络参数、同样维度的特征空间,又或者尝试将异构图映射到同构图中,根本无法学习到“异构”的关键,即无法探索到不同节点之间,监督标签之间的联系,而这又是十分重要的。
如上图著名的异构例子,学术网络图,它包含“论文”paper、“作者”author、“会议”venue和“机构”org等节点类型。由于每个作者会隶属于某些机构,发表某些论文,而每篇论文又会被发表在某个会议上,也会引用其它的论文。每个节点同样可以有自己的特征——如论文可以用摘要当作特征等。
特别是异质图在实际中比同质图要更为常见,除了节点本身的类型会有不同外,或者说很多看起来同质的图,只要将边信息显式出来,节点和节点之间的关系就开始复杂多样了。不同关系的不同属性也会导致节点间的差别。处理同质图目前主要有的方案有:
- 将异构图映射到同构图
- 不同类型的节点不同编码
- metapath元路径得到先验知识指导随机游走
- 注意力机制
Message Passing
消息传递(Message Passing)来处理不同类型的边,即每条边上的消息通过每类边独有的线性变换得到,出自Relational Graph Convolutional Network (RGCN):
H
i
(
l
+
1
)
=
σ
(
∑
R
∑
i
∈
N
i
,
r
1
c
i
,
r
W
r
(
l
)
h
j
(
l
)
+
W
0
(
l
)
h
i
(
l
)
)
H_i^{(l+1)}=\sigma(\sum^R\sum_{i \in N_{i,r}}\frac{1}{c_{i,r}}W_r^{(l)}h_j^{(l)}+W_0^{(l)}h_i^{(l)})
Hi(l+1)=σ(∑Ri∈Ni,r∑ci,r1Wr(l)hj(l)+W0(l)hi(l))
Metapath-based Method
metapath+random walk算是目前解决异质的最好方法了吧。其根本还是用随机游走解决图问题的那一套方法,对于异质图的结构,那就得到专用于异质的游走“路线”就好,也就是metapath。元路径是连接两个对象的复合关系,是一种广泛应用的语义捕获结构,表示从节点到另一个结点所经过一系列Node的路径,数学定义为:
A
1
→
R
1
A
2
→
R
2
.
.
.
→
R
l
A
l
+
1
A_1 \rightarrow^{R_1} A_2 \rightarrow^{R_2} ... \rightarrow^{R_l} A_{l+1}
A1→R1A2→R2...→RlAl+1它描述了对象
A
1
A_1
A1和
A
l
+
1
A_{l+1}
Al+1之间的复合关系。如两部作者可以通过多个元路径连接,例如不同的作者引用同一篇文章:作者-引用论文-作者(APA)和不同的论文工作属于同一个作者:论文-作者-论文(PAP)。不同的元路径总是揭示不同的语义,在给定元路径的情况下,每个节点存在一组基于元路径的邻居,可以在异构图中显示不同的结构信息和丰富的语义。
比如不同元路径下的作者相似性就会不同,在APA这条路径中,一起合作过论文的作者更为相似,但是在APVPA路径中,经常在同一会议上发表论文的作者更相似。又比如我们考虑不同元路径下的对象排名,APA路径会给经常发表合著类论文的作者更高的排名,APVPA路径则会给在高产会议上发表更多论文的作者更高的排名。
接下来总结几篇Metapath-based的paper:
Heterogeneous Graph Attention Network(HAN,www’19)
如上图,HAN也遵循经典的异质图神经网络架构(节点级别聚合与语义级别聚合).,第一次引入注意力去解决这两个问题,即利用语义级别注意力和节点级别注意力来同时学习元路径与节点邻居的重要性。
-
节点级别:如何区分邻居之间的细微差别,并选择一些信息丰富的邻居是必需的,特别是在不同节点嵌入学习中扮演着不同的角色,显示出不同的重要性。由于节点的异质性,不同类型的节点具有不同的特征空间,故先将节点投影: h i ′ = M ϕ i ⋅ h i h_i'=M_{\phi_i}\cdot h_i hi′=Mϕi⋅hi再用自注意力学习节点间的重要性: e i j Φ = a t t n o d e ( h i ′ , h j ′ ; Φ ) e_{ij}^{\Phi}=att_{node}(h_i',h_j';\Phi) eijΦ=attnode(hi′,hj′;Φ) α i j Φ = s o f t m a x j ( e i j Φ ) \alpha_{ij}^{\Phi}=softmax_j(e_{ij}^{\Phi}) αijΦ=softmaxj(eijΦ)其中 Φ \Phi Φ是给定的元路径,即基于该元路径的节点对(i,j)的注意力取决于它们的特征。 z i Φ = σ ( ∑ α i j Φ ⋅ h j ′ ) z_i^{\Phi}=\sigma(\sum \alpha_{ij}^{\Phi}\cdot h_j') ziΦ=σ(∑αijΦ⋅hj′)再把注意力升级成多头: z i Φ = ∣ ∣ K σ ( ∑ α i j Φ ⋅ h j ′ ) z_i^{\Phi}=||^K \sigma(\sum \alpha_{ij}^{\Phi}\cdot h_j') ziΦ=∣∣Kσ(∑αijΦ⋅hj′)
-
语义级别:异构图形中包含着不同的有意义和复杂的语义信息,这些信息通常通过元路径来反映,而元路径的重要性是不等的。语义级别的注意力用MLP来学习: w Φ i = 1 ∣ V ∣ ∑ i ∈ V q T ⋅ t a n h ( W ⋅ z i Φ + b ) w_{\Phi_i}=\frac{1}{|V|}\sum_{i \in V} q^T\cdot tanh(W\cdot z_i^{\Phi}+b) wΦi=∣V∣1i∈V∑qT⋅tanh(W⋅ziΦ+b)
然后可以将最终的嵌入应用到特定的任务中,并设计不同的损失函数。对于半监督节点分类,最小化有类标签节点的预测类标签分布与真实类标签的交叉熵: L = ∑ Y l l n ( C ⋅ Z l ) L=\sum Y^l ln(C\cdot Z^l) L=∑Ylln(C⋅Zl)其中 C 是分类器的参数,Y和 Z 是有标签节点的标签和嵌入。
- 作者开源code:https://github.com/Jhy1993/HAN.
Graph Transformer Networks(GTN,NIPS’19)
如何选取合适元路径?元路径需要很强的先验知识进行构建,元路径选的好不好会极大的影响模型的效果。GTN的目标就是自动的逐步生成对任务有用的元路径,在原始图上识别未连接节点之间的有用连接。
GTN通过候选邻接矩阵来定义有效的meta-paths,具体的方式是利用权重选择,即对邻接矩阵的多通道卷积(每个通道即是一条元路径)。然后对于节点级别的聚合:
Z
i
=
σ
(
D
i
−
1
A
l
X
W
)
Z_i=\sigma(D_i^{-1}A^lXW)
Zi=σ(Di−1AlXW)语义级别的聚合,其中C是元路径,直接将多条元路径下的节点表示拼接起来:
Z
=
∣
∣
i
=
1
C
σ
(
D
i
−
1
A
l
X
W
)
Z=||^C_{i=1}\sigma(D_i^{-1}A^lXW)
Z=∣∣i=1Cσ(Di−1AlXW)
class GTLayer(nn.Module):
def __init__(self, in_channels, out_channels, first=True):
super(GTLayer, self).__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.first = first
if self.first == True:
self.conv1 = GTConv(in_channels, out_channels)
self.conv2 = GTConv(in_channels, out_channels)
else:
self.conv1 = GTConv(in_channels, out_channels)
def forward(self, A, H_=None):
if self.first == True:
a = self.conv1(A)
b = self.conv2(A)
H = torch.bmm(a,b)
W = [(F.softmax(self.conv1.weight, dim=1)).detach(),(F.softmax(self.conv2.weight, dim=1)).detach()]
else:
a = self.conv1(A)
H = torch.bmm(H_,a)
W = [(F.softmax(self.conv1.weight, dim=1)).detach()]
return H,W
作者开源code:https://github.com/seongjunyun/Graph_Transformer_Networks
Heterogeneous Graph Neural Network(HetGNN,KDD’19)
这篇论文没有使用metapath,对于不同类型的节点,采用了两种消息聚合的方式:同类型聚合,不同类型聚合。
- Sampling(random walk with restart strategy)。从固定点出发采样邻居节点,同时有一定概率回到固定点重新开始采用(保证所有类型都会被采样到),直到一个固定的数量。然后将这些采样的节点按照类型分类,对每一类根据节点出现频率topk作为邻居。
- 同类型聚合。对应着节点级别的聚合,使用把领域变成序列再用LSTM聚合器的思路: f 2 t = ∑ v ′ ∈ N t ( v ) b i l s t m ( f 1 ( v ′ ) ) ∣ N t ( v ) ∣ f^t_2=\frac{\sum_{v' \in N_t(v)} bilstm(f_1(v'))}{|N_t(v)|} f2t=∣Nt(v)∣∑v′∈Nt(v)bilstm(f1(v′))N是邻域,f是邻居节点v’的初始表示(这个特征也是把节点可能有的不同属性的feature到bilstm进行encoding)
- 不同类型聚合。对应着语义级别的聚合,既然类型不同,那就Attention之后,再融合: ϵ v = α v , v f 1 ( v ) + ∑ α v , t f 2 ( v ) \epsilon_v=\alpha^{v,v}f_1(v)+\sum \alpha^{v,t}f_2(v) ϵv=αv,vf1(v)+∑αv,tf2(v)
#先分别聚合自己的邻居,再聚合异构的邻居,最后针对不同类型的重要性不一样给个注意力。
def node_neigh_agg(self, id_batch, node_type): #用bilstm按类型聚合自己的邻居
embed_d = self.embed_d
if node_type == 1 or node_type == 2:
batch_s = int(len(id_batch[0]) / 10)
else:
#print (len(id_batch[0]))
batch_s = int(len(id_batch[0]) / 3)
if node_type == 1:#a类型,作者
neigh_agg = self.a_content_agg(id_batch).view(batch_s, 10, embed_d)
neigh_agg = torch.transpose(neigh_agg, 0, 1)
all_state, last_state = self.a_neigh_rnn(neigh_agg)
elif node_type == 2:#p类型,论文
neigh_agg = self.p_content_agg(id_batch).view(batch_s, 10, embed_d)
neigh_agg = torch.transpose(neigh_agg, 0, 1)
all_state, last_state = self.p_neigh_rnn(neigh_agg)
else:#v类型,地点
neigh_agg = self.v_content_agg(id_batch).view(batch_s, 3, embed_d)
neigh_agg = torch.transpose(neigh_agg, 0, 1)
all_state, last_state = self.v_neigh_rnn(neigh_agg)
neigh_agg = torch.mean(all_state, 0).view(batch_s, embed_d)
return neigh_agg
def node_het_agg(self, id_batch, node_type): #异构的邻居
a_neigh_batch = [[0] * 10] * len(id_batch)
p_neigh_batch = [[0] * 10] * len(id_batch)
v_neigh_batch = [[0] * 3] * len(id_batch)
for i in range(len(id_batch)):
if node_type == 1:#a类型,找到a的邻居列表中的3种类型的列表
a_neigh_batch[i] = self.a_neigh_list_train[0][id_batch[i]]
p_neigh_batch[i] = self.a_neigh_list_train[1][id_batch[i]]
v_neigh_batch[i] = self.a_neigh_list_train[2][id_batch[i]]
elif node_type == 2:#p类型,找到p的邻居列表中的3种类型的列表
a_neigh_batch[i] = self.p_neigh_list_train[0][id_batch[i]]
p_neigh_batch[i] = self.p_neigh_list_train[1][id_batch[i]]
v_neigh_batch[i] = self.p_neigh_list_train[2][id_batch[i]]
else:#v类型,找到v的邻居列表中的3种类型的列表
a_neigh_batch[i] = self.v_neigh_list_train[0][id_batch[i]]
p_neigh_batch[i] = self.v_neigh_list_train[1][id_batch[i]]
v_neigh_batch[i] = self.v_neigh_list_train[2][id_batch[i]]
a_neigh_batch = np.reshape(a_neigh_batch, (1, -1))
a_agg_batch = self.node_neigh_agg(a_neigh_batch, 1)#异构邻居聚合
p_neigh_batch = np.reshape(p_neigh_batch, (1, -1))
p_agg_batch = self.node_neigh_agg(p_neigh_batch, 2)
v_neigh_batch = np.reshape(v_neigh_batch, (1, -1))
v_agg_batch = self.node_neigh_agg(v_neigh_batch, 3)
#注意力模块
id_batch = np.reshape(id_batch, (1, -1))
if node_type == 1:
c_agg_batch = self.a_content_agg(id_batch)
elif node_type == 2:
c_agg_batch = self.p_content_agg(id_batch)
else:
c_agg_batch = self.v_content_agg(id_batch)
c_agg_batch_2 = torch.cat((c_agg_batch, c_agg_batch), 1).view(len(c_agg_batch), self.embed_d * 2)
a_agg_batch_2 = torch.cat((c_agg_batch, a_agg_batch), 1).view(len(c_agg_batch), self.embed_d * 2)
p_agg_batch_2 = torch.cat((c_agg_batch, p_agg_batch), 1).view(len(c_agg_batch), self.embed_d * 2)
v_agg_batch_2 = torch.cat((c_agg_batch, v_agg_batch), 1).view(len(c_agg_batch), self.embed_d * 2)
#计算注意力。a(v,i)表示第i类对节点v的重要度,所以要计算3种节点的3种权重。
concate_embed = torch.cat((c_agg_batch_2, a_agg_batch_2, p_agg_batch_2,\
v_agg_batch_2), 1).view(len(c_agg_batch), 4, self.embed_d * 2)
if node_type == 1:
atten_w = self.act(torch.bmm(concate_embed, self.a_neigh_att.unsqueeze(0).expand(len(c_agg_batch),\
*self.a_neigh_att.size())))
elif node_type == 2:
atten_w = self.act(torch.bmm(concate_embed, self.p_neigh_att.unsqueeze(0).expand(len(c_agg_batch),\
*self.p_neigh_att.size())))
else:
atten_w = self.act(torch.bmm(concate_embed, self.v_neigh_att.unsqueeze(0).expand(len(c_agg_batch),\
*self.v_neigh_att.size())))
atten_w = self.softmax(atten_w).view(len(c_agg_batch), 1, 4)
#最后用权重把不同的节点类型聚合起来
concate_embed = torch.cat((c_agg_batch, a_agg_batch, p_agg_batch,\
v_agg_batch), 1).view(len(c_agg_batch), 4, self.embed_d)
weight_agg_batch = torch.bmm(atten_w, concate_embed).view(len(c_agg_batch), self.embed_d)
return weight_agg_batch
完整的细节源代码逐行中文注释:https://github.com/nakaizura/Source-Code-Notebook/tree/master/HetGNN
Future Sight
- 如何保持异构图的结构和性质。这里的性质是指仍未被考虑过的有用的性质,比如动态性和不确定性。
- 异质图自监督学习or 预训练技术。博主已经整理过了,可见系列文章:传送门
- 异质图的公平性,鲁棒性,可解释性。
- 在实际应用汇总的可伸缩性和高效率。