2018 ESWC | Modeling Relational Data with Graph Convolutional Networks
Paper: https://arxiv.org/pdf/1703.06103.pdf
Code: https://github.com/JinheonBaek/RGCN
RGCN :GCN 在多边类型的应用
本文中作者提出的R-GCN模型应用于链路预测和实体分类两项任务上,对于链路预测任务,通过在关系图中的多个推理步骤中使用编码器模型来积累信息,可以显著改进链路预测的模型;对于实体分类任务,则是类似于GCN论文中,即对每个节点使用一个softmax分类器,通过R-GCN来提取每个节点表示用于节点类别的预测。
本篇论文最主要的贡献有三:
- 将GCN框架应用于关系数据建模,特别是链路预测和实体分类任务。
- 在具有大量关系的多图中应用了参数共享以及实现稀疏约束的技术
- 使用一个在关系图中执行多步信息传播的模型来加强因子分解模型。
模型
GCN模型
R-GCN和GCN都是利用图卷积模型来模拟信息在网络结构中的传递,因此那这个部分的一个框架可以如下所示:
其中:
g
m
g_m
gm指的就是将传入的消息进行聚合并通过激活函数传递
M
i
M_i
Mi指的是节点
v
i
v_i
vi 的传入消息集,通常选择为传入的边集
h
i
(
l
)
h_i^{(l)}
hi(l)指的是节点
i
i
i的第
l
l
l层节点表示,
h
j
(
l
)
h_j^{(l)}
hj(l)指的是节点
i
i
i的所有邻居节点的第
l
l
l层节点表示,
RGCN模型
定义了在一个关系多图的传播模型,图中节点
v
i
v_i
vi的更新方式如下:
N
i
r
N_i^{r}
Nir表示节点
i
i
i的关系为
r
r
r的邻居节点集合
c
i
,
r
c_{i,r}
ci,r是一个正则化常量,其中
c
i
,
r
c_{i,r}
ci,r的取值为
∣
N
i
∣
|N_i^|
∣Ni∣
W
r
(
l
)
W_r^{(l)}
Wr(l)是线性转化函数,将同类型边的邻居节点,使用用一个参数矩阵
W
r
(
l
)
W_r^{(l)}
Wr(l)进行转化。
通过这幅图可以看清,对于中心红色的节点进行一次卷积,通过聚合邻居节点的信息来更新自身节点的表示。其中邻居节点的聚合是按照边的类型进行分类,根据边类型的不同进行相应的转换,收集的信息经过一个正则化的加和(绿色方块),最后通过激活函数(relu)。其中每个顶点的信息更新共享参数,并行计算,同时也包括自连接,也就是说包括了节点自身表示。
正则化
如果网络中存在大量不同类型的edge,则我们最终需要为每一种edge的类型都生成一个linear层,这样做一方面会导致模型参数量的线性增长,一方面对于某些类型的edge其出现的次数可能很少,这就会导致在每个epoch里,这种类型的edge对应的linear layer最终只在很少的节点上不断更新,从而产生过拟合少量节点的问题。文中对此提出了两种独立的方法对R-GCN层进行规则化:基函数分解和块对角分解。其实本质上就是做局部参数的共享,类似于多任务学习中的share button结构。
基数分解法
基数分解法对于 的定义如下:
从公式可以看出,对于不同类型的关系
r
r
r,其参数矩阵
W
r
(
l
)
W_r^{(l)}
Wr(l)是来自
V
b
(
l
)
∈
R
d
(
l
+
1
)
×
d
(
l
)
V_b^{(l)} \in R^{d^{(l+1)}\times d^{(l)}}
Vb(l)∈Rd(l+1)×d(l)和系数
a
r
b
(
l
)
a_{rb}^{(l)}
arb(l)的线性组合.因此只有系数
a
r
b
(
l
)
a_{rb}^{(l)}
arb(l)和关系类型
r
r
r相关。同时对于所有的
V
b
(
l
)
V_b^{(l)}
Vb(l)也就实现了不同关系类型之间的有效权重共享。还有一点好处是可以减轻稀有关系(rare relations)数据的过拟合问题。是因为稀疏关系矩阵是由
V
b
(
l
)
V_b^{(l)}
Vb(l)共享参数组成 。(
V
b
(
l
)
V_b^{(l)}
Vb(l)可以由频繁关系的数据进行很好的训练)
块对角分解
块对角分解的公式如下:
从公式可以看出每一个
W
r
(
l
)
W_r^{(l)}
Wr(l)定义为低维矩阵的直接求和
d
i
a
g
(
Q
1
r
(
l
)
.
.
.
Q
B
r
(
l
)
)
diag(Q^{(l)}_{1r}...Q^{(l)}_{Br})
diag(Q1r(l)...QBr(l))其中
W
r
(
l
)
W_r^{(l)}
Wr(l)为块对角矩阵。块分解可以看作是每个关系类型的权重矩阵上的稀疏性约束。两种分解都减少了拟合多关系数据所需的参数数量。同时,期望可以减轻对稀有关系的过度拟合,因为参数的更新在稀有关系和频繁关系之间是共享的。
实验
对于节点分类任务:
实验使用的数据集如下,其中包括边,边的类型,标签,分类等信息。
实体分类实验结果:R-GCN 中选择每层有 16 个隐藏单元,并且使用基函数分解的正则化方法的两层模型。
对于链路预测任务:
链路预测实验结果:对于 FB15K 和 Wn18,使用具有两个基函数的基分解和具有 200 维嵌入的单个编码层来报告结果。对于 FB15K-237,发现块对角分解表现最好,使用两层块尺寸为 5×5 和 500 维嵌入。
模型部分
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.nn.conv import MessagePassing
from utils import uniform
class RGCN(torch.nn.Module):
def __init__(self, num_entities, num_relations, num_bases, dropout):
super(RGCN, self).__init__()
self.entity_embedding = nn.Embedding(num_entities, 100) # 初始化entity
self.relation_embedding = nn.Parameter(torch.Tensor(num_relations, 100))
nn.init.xavier_uniform_(self.relation_embedding, gain=nn.init.calculate_gain('relu'))
self.conv1 = RGCNConv(
100, 100, num_relations * 2, num_bases=num_bases)
self.conv2 = RGCNConv(
100, 100, num_relations * 2, num_bases=num_bases)
self.dropout_ratio = dropout
def forward(self, entity, edge_index, edge_type, edge_norm):
x = self.entity_embedding(entity)
x = F.relu(self.conv1(x, edge_index, edge_type, edge_norm))
x = F.dropout(x, p = self.dropout_ratio, training = self.training)
x = self.conv2(x, edge_index, edge_type, edge_norm)
return x
def distmult(self, embedding, triplets):
s = embedding[triplets[:,0]]
r = self.relation_embedding[triplets[:,1]]
o = embedding[triplets[:,2]]
score = torch.sum(s * r * o, dim=1)
return score
def score_loss(self, embedding, triplets, target):
score = self.distmult(embedding, triplets)
return F.binary_cross_entropy_with_logits(score, target)
def reg_loss(self, embedding):
return torch.mean(embedding.pow(2)) + torch.mean(self.relation_embedding.pow(2))
class RGCNConv(MessagePassing):
r"""The relational graph convolutional operator from the `"Modeling
Relational Data with Graph Convolutional Networks"
<https://arxiv.org/abs/1703.06103>`_ paper
.. math::
\mathbf{x}^{\prime}_i = \mathbf{\Theta}_{\textrm{root}} \cdot
\mathbf{x}_i + \sum_{r \in \mathcal{R}} \sum_{j \in \mathcal{N}_r(i)}
\frac{1}{|\mathcal{N}_r(i)|} \mathbf{\Theta}_r \cdot \mathbf{x}_j,
where :math:`\mathcal{R}` denotes the set of relations, *i.e.* edge types.
Edge type needs to be a one-dimensional :obj:`torch.long` tensor which
stores a relation identifier
:math:`\in \{ 0, \ldots, |\mathcal{R}| - 1\}` for each edge.
Args:
in_channels (int): Size of each input sample.
out_channels (int): Size of each output sample.
num_relations (int): Number of relations.
num_bases (int): Number of bases used for basis-decomposition.
root_weight (bool, optional): If set to :obj:`False`, the layer will
not add transformed root node features to the output.
(default: :obj:`True`)
bias (bool, optional): If set to :obj:`False`, the layer will not learn
an additive bias. (default: :obj:`True`)
**kwargs (optional): Additional arguments of
:class:`torch_geometric.nn.conv.MessagePassing`.
"""
def __init__(self, in_channels, out_channels, num_relations, num_bases,
root_weight=True, bias=True, **kwargs):
super(RGCNConv, self).__init__(aggr='mean', **kwargs)
self.in_channels = in_channels
self.out_channels = out_channels
self.num_relations = num_relations
self.num_bases = num_bases
self.basis = nn.Parameter(torch.Tensor(num_bases, in_channels, out_channels))
self.att = nn.Parameter(torch.Tensor(num_relations, num_bases))
if root_weight:
self.root = nn.Parameter(torch.Tensor(in_channels, out_channels))# 自身节点训练参数
else:
self.register_parameter('root', None)
if bias:
self.bias = nn.Parameter(torch.Tensor(out_channels))
else:
self.register_parameter('bias', None)
self.reset_parameters()
def reset_parameters(self):
size = self.num_bases * self.in_channels
uniform(size, self.basis)
uniform(size, self.att)
uniform(size, self.root)
uniform(size, self.bias)
def forward(self, x, edge_index, edge_type, edge_norm=None, size=None):
""""""
return self.propagate(edge_index, size=size, x=x, edge_type=edge_type,
edge_norm=edge_norm)
def message(self, x_j, edge_index_j, edge_type, edge_norm):
w = torch.matmul(self.att, self.basis.view(self.num_bases, -1))
# If no node features are given, we implement a simple embedding
# loopkup based on the target node index and its edge type.
if x_j is None:
w = w.view(-1, self.out_channels)
index = edge_type * self.in_channels + edge_index_j
out = torch.index_select(w, 0, index)
else:
w = w.view(self.num_relations, self.in_channels, self.out_channels)
w = torch.index_select(w, 0, edge_type) # 不同的边有不同的权重
out = torch.bmm(x_j.unsqueeze(1), w).squeeze(-2)
return out if edge_norm is None else out * edge_norm.view(-1, 1)
def update(self, aggr_out, x):
if self.root is not None:
if x is None:
out = aggr_out + self.root
else:
out = aggr_out + torch.matmul(x, self.root)
if self.bias is not None:
out = out + self.bias
return out
def __repr__(self):
return '{}({}, {}, num_relations={})'.format(
self.__class__.__name__, self.in_channels, self.out_channels,
self.num_relations)