论文地址:https://arxiv.org/pdf/1905.07854.pdf
论文代码:https://github.com/xiangwang1223/knowledge_graph_attention_network
传统的有监督学习方法中,例如因子分解机,在抽取出样本的属性特征后,把每个样本视作一个独立的事件来预测,而忽视了样本之间内在的关系。知识图谱可以把样本之间通过属性关联起来,使得样本之间不再独立预测。KGAT基于三个基准数据实验,优于Neural FM和RippleNet等。
KGAT把用户-物品的交互二分图和知识图谱融合在一起——协同知识图CKG,即将图谱关系、用户user和item的二部交互图融合到一个图空间,以融合CF信息和KG信息,KKG也能发现更高阶的关系信息。在协同知识图 G \mathcal{G} G中:
获得邻居的节点embedding和关系embedding,使用get_neighbors
函数,通过物品id
得到其邻居及连接他们的关系的id
,然后通过邻居和关系id
得到邻居实体与关系的embedding的方法,而中间的adj_entity
和adj_Relation
分别是图采样后得到的指定度数的邻接列表。
# 得到邻居的节点embedding和关系embedding
def get_neighbors( self, items ):
e_ids = [self.adj_entity[ item ] for item in items ]
r_ids = [ self.adj_relation[ item ] for item in items ]
e_ids = torch.LongTensor( e_ids )
r_ids = torch.LongTensor( r_ids )
neighbor_entities_embs = self.entity_embs( e_ids )
neighbor_relations_embs = self.relation_embs( r_ids )
return neighbor_entities_embs, neighbor_relations_embs
adj_entity
和adj_Relation
即代码中的dataloader4KGNN.construct_adj
函数返回值(通过kg邻接列表得到实体邻接列表和关系邻接列表),后面在消息传递时需要通过该邻接矩阵,获得当前节点的邻居节点embedding和关系embedding。
# 根据kg邻接列表,得到实体邻接列表和关系邻接列表
def construct_adj( neighbor_sample_size, kg_indexes, entity_num ):
print('生成实体邻接列表和关系邻接列表')
adj_entity = np.zeros([ entity_num, neighbor_sample_size ], dtype = np.int64 )
adj_relation = np.zeros([ entity_num, neighbor_sample_size ], dtype = np.int64 )
for entity in range( entity_num ):
neighbors = kg_indexes[ str( entity ) ]
n_neighbors = len( neighbors )
if n_neighbors >= neighbor_sample_size:
sampled_indices = np.random.choice( list( range( n_neighbors ) ),
size = neighbor_sample_size, replace = False )
else:
sampled_indices = np.random.choice( list( range( n_neighbors ) ),
size = neighbor_sample_size, replace = True )
adj_entity[ entity ] = np.array( [ neighbors[i][0] for i in sampled_indices ] )
adj_relation[ entity] = np.array( [ neighbors[i][1] for i in sampled_indices ] )
return adj_entity, adj_relation
这里的GAT我们为了简单起见使用dgl里面的dglnn.GATConv
层,这里我们做的节点分类的任务。图注意神经网络(GAT)来源于论文 Graph Attention Networks。其数学定义为,
x i ′ = α i , i Θ x i + ∑ j ∈ N ( i ) α i , j Θ x j , \mathbf{x}^{\prime}_i = \alpha_{i,i}\mathbf{\Theta}\mathbf{x}_{i} + \sum_{j \in \mathcal{N}(i)} \alpha_{i,j}\mathbf{\Theta}\mathbf{x}_{j}, xi′=αi,iΘxi+j∈N(i)∑αi,jΘxj,
GAT和所有的attention mechanism一样,GAT的计算也分为两步走:
(1)计算注意力系数(attention coefficient):(下图来自《GRAPH ATTENTION NETWORKS》)。其中注意力系数 α i , j \alpha_{i,j} αi,j的计算方法为,
α i , j = exp ( L e a k y R e L U ( a ⊤ [ Θ x i ∥ Θ x j ] ) ) ∑ k ∈ N ( i ) ∪ { i } exp ( L e a k y R e L U ( a ⊤ [ Θ x i ∥ Θ x k ] ) ) . \alpha_{i,j} = \frac{ \exp\left(\mathrm{LeakyReLU}\left(\mathbf{a}^{\top} [\mathbf{\Theta}\mathbf{x}_i \, \Vert \, \mathbf{\Theta}\mathbf{x}_j] \right)\right)} {\sum_{k \in \mathcal{N}(i) \cup \{ i \}} \exp\left(\mathrm{LeakyReLU}\left(\mathbf{a}^{\top} [\mathbf{\Theta}\mathbf{x}_i \, \Vert \, \mathbf{\Theta}\mathbf{x}_k] \right)\right)}. αi,j=∑k∈N(i)∪{i}exp(LeakyReLU(a⊤[Θxi∥Θxk]))exp(LeakyReLU(a⊤[Θxi∥Θxj])).
(2)加权求和(aggregate):根据(1)的系数,把特征加权求和(aggregate)
class GAT(nn.Module):
def __init__(self,in_size, hid_size, out_size, heads):
super().__init__()
self.gat_layers = nn.ModuleList()
# two-layer GAT(attention)
self.gat_layers.append(dglnn.GATConv(in_size, hid_size, heads[0], feat_drop=0.6, attn_drop=0.6, activation=F.elu))
# GATConv: in_feat, out_feat, num_head(multi-head)
self.gat_layers.append(dglnn.GATConv(hid_size*heads[0], out_size, heads[1], feat_drop=0.6, attn_drop=0.6, activation=None))
def forward(self, g, inputs):
h = inputs
for i, layer in enumerate(self.gat_layers):
h = layer(g, h)
if i == 1: # last layer
h = h.mean(1)
else: # other layer(s)
h = h.flatten(1)
return h
使用交叉熵损失函数,在dgl中我们使用train_mask
表示出在训练集的节点,如果对应位置的train_mask
为false则表示不会该节点不会出现在训练集。
该层为了得到知识图结构的实体和关系的嵌入表示,使用TransR模型,TransR模型使得三元组(h, r, t)在关系r
的投影平面上有平移关系: g ( h , r 1 , t ) = ∥ W r e h + e r − W r e t ∥ 2 2 g\left(h, r_1, t\right)=\left\|\boldsymbol{W}_r \boldsymbol{e}_h+\boldsymbol{e}_{\mathrm{r}}-\boldsymbol{W}_r \boldsymbol{e}_t\right\|_2^2 g(h,r1,t)=∥Wreh+er−Wret∥22其中:
使用pairwise ranking loss作为训练损失函数:
L K G = ∑ ⟨ h , r , t , t ′ ⟩ ∈ T − log σ ( g ( h , r , t ′ ) − g ( h , r , t ) ) \mathcal{L}_{\mathrm{KG}}=\sum_{\left\langle h, r, t, t^{\prime}\right\rangle \in \mathcal{T}}-\log \sigma\left(g\left(h, r, t^{\prime}\right)-g(h, r, t)\right) LKG=⟨h,r,t,t′⟩∈T∑−logσ(g(h,r,t′)−g(h,r,t))
其中:
通过层迭代的形式吸收图上高阶的邻域信息,同时通过GAT把重要信息保存,忽略噪声信息。
先考虑一层传播的操作过程:给定一个头节点 h h h, 令 N h = \mathcal{N}_h= Nh= { ( h , r , t ) ∣ ( h , r , t ) ∈ G } \{(h, r, t) \mid(h, r, t) \in \mathcal{G}\} {(h,r,t)∣(h,r,t)∈G} ,表示以它起始的所有三元组的集合,hrt分别为头实体向量、关系向量、尾实体向量。那么节点 h h h 在图上的一阶邻域向量表示:
e N h = ∑ ( h , r , t ) ∈ N h π ( h , r , t ) e t \boldsymbol{e}_{\mathcal{N}_h}=\sum_{(h, r, t) \in N_h} \pi(h, r, t) \boldsymbol{e}_t eNh=(h,r,t)∈Nh∑π(h,r,t)et
其中:
对应的一层传播代码:
# GAT消息传递
def GATMessagePass( self, h_embs, r_embs, t_embs ):
'''
:param h_embs: 头实体向量[ batch_size, e_dim ]
:param r_embs: 关系向量[ batch_size, n_neibours, r_dim ]
:param t_embs: 尾实体向量[ batch_size, n_neibours, e_dim ]
'''
# # 将h张量广播,维度扩散为 [ batch_size, n_neibours, e_dim ]
h_broadcast_embs = torch.cat( [ torch.unsqueeze( h_embs, 1 ) for _ in range( t_embs.shape[ 1 ] ) ], dim = 1 )
# [ batch_size, n_neibours, r_dim ]
tr_embs = self.Wr( t_embs )
# [ batch_size, n_neibours, r_dim ]
hr_embs = self.Wr( h_broadcast_embs )
# [ batch_size, n_neibours, r_dim ]
hr_embs= torch.tanh( hr_embs + r_embs)
# [ batch_size, n_neibours, 1 ]
atten = torch.sum( hr_embs * tr_embs,dim = -1 ,keepdim=True)
atten = torch.softmax( atten, dim = -1 )
# [ batch_size, n_neibours, e_dim ]
t_embs = t_embs * atten
# [ batch_size, e_dim ]
return torch.sum( t_embs, dim = 1 )
最后:需要把实体节点 h h h自身的嵌入表示 e h e_h eh和它基于邻域的嵌入表示 e N h \boldsymbol{e}_{\mathcal{N}_h} eNh融合起来,得到节点 h h h的新表示 e h ( 1 ) \boldsymbol{e}_h^{(1)} eh(1)。融合的方式有三种选择:
以上是注意力感知的表示传播层操作;若要考虑更高阶的信息,可以重复堆叠多次: e h ( l ) = f ( e l t { l − 1 ) , e N k ( l − 1 ) ) e_h^{(l)}=f\left(e_{l_t}^{\{l-1)}, e_{N_k}^{(l-1)}\right) eh(l)=f(elt{l−1),eNk(l−1))
上面的三种融合(聚合)方法对应代码:
# 消息聚合
def aggregate( self, h_embs, Nh_embs, agg_method = 'Bi-Interaction' ):
'''
:param h_embs: 原始的头实体向量 [ batch_size, e_dim ]
:param Nh_embs: 消息传递后头实体位置的向量 [ batch_size, e_dim ]
:param agg_method: 聚合方式,总共有三种,分别是'Bi-Interaction','concat','sum'
'''
if agg_method == 'Bi-Interaction':
return self.leakyRelu( self.W1( h_embs + Nh_embs ) )\
+ self.leakyRelu( self.W2( h_embs * Nh_embs ) )
elif agg_method == 'concat':
return self.leakyRelu( self.W_concat( torch.cat([ h_embs,Nh_embs ], dim = -1 ) ) )
else: #sum
return self.leakyRelu( self.W1( h_embs + Nh_embs ) )
该层需要把用户和物品在各层得到的向量拼接起来得到最终的表示: e u ∗ = e u ( 0 ) ∥ ⋯ ∥ e u ( L ) , e i ∗ = e i ( 0 ) ∥ ⋯ ∥ e ( L ) e_u^*=e_u^{(0)}\|\cdots\| e_u^{(L)}, e_i^*=e_i^{(0)}\|\cdots\| e^{(L)} eu∗=eu(0)∥⋯∥eu(L),ei∗=ei(0)∥⋯∥e(L)
用户对物品的偏好程度预测为两个向量的点积: y ^ u i = e u ∗ ⊤ e i ∗ \hat{y}_{u i}=\boldsymbol{e}_u^{* \top} \boldsymbol{e}_i^* y^ui=eu∗⊤ei∗
推荐预测的损失函数也是成对优化误差: L C F = ∑ ( u , i j ) ∈ O − log σ ( y ^ u i − y ^ u j ) \mathcal{L}_{\mathrm{CF}}=\sum_{(u, i j) \in \mathcal{O}}-\log \sigma\left(\hat{y}_{u i}-\hat{y}_{u j}\right) LCF=(u,ij)∈O∑−logσ(y^ui−y^uj)
其中:
class KGAT( nn.Module ):
def __init__( self, n_users, n_entitys, n_relations, e_dim, r_dim,
adj_entity, adj_relation ,agg_method = 'Bi-Interaction'):
super( KGAT, self ).__init__( )
self.user_embs = nn.Embedding( n_users, e_dim, max_norm = 1 )
self.entity_embs = nn.Embedding( n_entitys, e_dim, max_norm = 1 )
self.relation_embs = nn.Embedding( n_relations, r_dim, max_norm = 1 )
self.adj_entity = adj_entity # 节点的邻接列表
self.adj_relation = adj_relation # 关系的邻接列表
self.agg_method = agg_method # 聚合方法
# 初始化计算注意力时的关系变换线性层
self.Wr = nn.Linear( e_dim, r_dim )
# 初始化最终聚合时所用的激活函数
self.leakyRelu = nn.LeakyReLU( negative_slope = 0.2 )
# 初始化各种聚合时所用的线性层
if agg_method == 'concat':
self.W_concat = nn.Linear( e_dim * 2, e_dim )
else:
self.W1 = nn.Linear( e_dim, e_dim )
if agg_method == 'Bi-Interaction':
self.W2 = nn.Linear( e_dim, e_dim )
# 得到邻居的节点embedding和关系embedding
def get_neighbors( self, items ):
e_ids = [self.adj_entity[ item ] for item in items ]
r_ids = [ self.adj_relation[ item ] for item in items ]
e_ids = torch.LongTensor( e_ids )
r_ids = torch.LongTensor( r_ids )
neighbor_entities_embs = self.entity_embs( e_ids )
neighbor_relations_embs = self.relation_embs( r_ids )
return neighbor_entities_embs, neighbor_relations_embs
# GAT消息传递
def GATMessagePass( self, h_embs, r_embs, t_embs ):
'''
:param h_embs: 头实体向量[ batch_size, e_dim ]
:param r_embs: 关系向量[ batch_size, n_neibours, r_dim ]
:param t_embs: 为实体向量[ batch_size, n_neibours, e_dim ]
'''
# # 将h张量广播,维度扩散为 [ batch_size, n_neibours, e_dim ]
h_broadcast_embs = torch.cat( [ torch.unsqueeze( h_embs, 1 ) for _ in range( t_embs.shape[ 1 ] ) ], dim = 1 )
# [ batch_size, n_neibours, r_dim ]
tr_embs = self.Wr( t_embs )
# [ batch_size, n_neibours, r_dim ]
hr_embs = self.Wr( h_broadcast_embs )
# [ batch_size, n_neibours, r_dim ]
hr_embs= torch.tanh( hr_embs + r_embs)
# [ batch_size, n_neibours, 1 ]
atten = torch.sum( hr_embs * tr_embs,dim = -1 ,keepdim=True)
atten = torch.softmax( atten, dim = -1 )
# [ batch_size, n_neibours, e_dim ]
t_embs = t_embs * atten
# [ batch_size, e_dim ]
return torch.sum( t_embs, dim = 1 )
# 消息聚合
def aggregate( self, h_embs, Nh_embs, agg_method = 'Bi-Interaction' ):
'''
:param h_embs: 原始的头实体向量 [ batch_size, e_dim ]
:param Nh_embs: 消息传递后头实体位置的向量 [ batch_size, e_dim ]
:param agg_method: 聚合方式,总共有三种,分别是'Bi-Interaction','concat','sum'
'''
if agg_method == 'Bi-Interaction':
return self.leakyRelu( self.W1( h_embs + Nh_embs ) )\
+ self.leakyRelu( self.W2( h_embs * Nh_embs ) )
elif agg_method == 'concat':
return self.leakyRelu( self.W_concat( torch.cat([ h_embs,Nh_embs ], dim = -1 ) ) )
else: #sum
return self.leakyRelu( self.W1( h_embs + Nh_embs ) )
def forward( self, u, i ):
# # [ batch_size, n_neibours, e_dim ] and # [ batch_size, n_neibours, r_dim ]
# 得到邻居的节点embedding和关系embedding
t_embs, r_embs = self.get_neighbors( i )
# # [ batch_size, e_dim ]
h_embs = self.entity_embs( i )
# # [ batch_size, e_dim ]
Nh_embs = self.GATMessagePass( h_embs, r_embs, t_embs )
# # [ batch_size, e_dim ]
item_embs = self.aggregate( h_embs, Nh_embs, self.agg_method )
# # [ batch_size, e_dim ]
user_embs = self.user_embs( u )
# # [ batch_size ]
logits = torch.sigmoid( torch.sum( user_embs * item_embs, dim = 1 ) )
return logits
注意:上面其实是做了一个简化(在CKG嵌入层使用nn.embedding
),没有使用联合训练,如果要看联合训练的完整代码,请参考 https://github.com/LunaBlack/KGAT-pytorch。
在三个基准数据集:Amazon-book、Last-FM和Yelp2018数据集上实验,KGAT与SL (FM和NFM)、基于正则化(CFKG和CKE)、基于路径(MCRec和RippleNet)和基于图形神经网络(GC-MC)的方法进行了比较:
为了探索聚合器的影响,作者考虑了使用不同设置的KGAT-1变体,即之前提到的GCN、GraphSage和Bi-Interaction,实验结果如下表所示。可以看出,Bi-Interaction的性能最好:
[1] 推荐系统前沿与实践. 李东胜等
[2] 自然语言处理cs224n-2021–Lecture15: 知识图谱
[3] 东南大学《知识图谱》研究生课程课件
[4] 2022年中国知识图谱行业研究报告
[5] 浙江大学慕课:知识图谱导论.陈华钧老师
[6] https://conceptnet.io/
[7] KG paper:https://github.com/km1994/nlp_paper_study_kg
[8] 北大gStore - a graph based RDF triple store
[9] Natural Language Processing Demystified
[10] https://github.com/datawhalechina/team-learning-nlp/tree/master/KnowledgeGraph_Basic
[11] 新一代知识图谱关键技术综述. 东南大学 王萌
[12] cs224w(图机器学习)2021冬季课程学习笔记12 Knowledge Graph Embeddings
[13] 关系抽取和事件抽取代码案例:https://github.com/taishan1994/taishan1994 (西西嘛呦)
[14] 年末巨制:知识图谱嵌入方法研究总结
[15] “知识图谱+”系列:知识图谱+图神经网络
[16] 【知识图谱】斯坦福 CS520公开课(双语字幕)
[17] https://github.com/LIANGKE23/Awesome-Knowledge-Graph-Reasoning
[18] 再谈图谱表示:图网络表示GE与知识图谱表示KGE的原理对比与实操效果分析
[19] WSDM’23 | 工业界搜推广nlp论文整理
[20] https://github.com/LunaBlack/KGAT-pytorch
[21] 推荐系统之深度召回模型综述(PART III)
[22] 深度融合 | 当推荐系统遇上知识图谱(二)
[23] KGAT_基于知识图谱+图注意力网络的推荐系统(KG+GAT)
[24] KGAT的pytorch完整代码
[25] 美团基于知识图谱的个性化新闻推荐系统