1. 为什么关系预测值得死磕:从知识图谱的补全需求说起
知识图谱这东西,刚接触的时候觉得挺唬人,用久了就会发现它本质上就是一张巨大的、带标签的有向图。图里的节点是实体,边是关系,每条边通常以三元组的形式存在,比如(某城市,位于,某省份)或者(某药物,治疗,某疾病)。但真正上手做过项目的人都知道,不管是人工构建还是从半结构化数据里抽取,知识图谱永远是不完整的。你随便打开一个中等规模的知识图谱,随机抽一批实体对,问“这两个实体之间应该是什么关系”,十有八九答不上来,因为边压根就没抽出来或者没标注。
关系预测要解决的就是这个问题:给定头实体和尾实体,判断它们之间最可能存在哪种关系;或者反过来,给定头实体和关系,预测最可能的尾实体。这个任务在学术上叫Relation Prediction,在工业界更常被叫做知识图谱补全的一个子问题。它的下游应用非常广,比如智能问答里的路径推理、推荐系统里的可解释性召回、风控场景里的关联链路挖掘,甚至生物医药领域的药物重定位,都依赖一个相对完整的关系网络。
早期做关系预测,主流方法是基于翻译的模型,比如TransE、TransR那一套。核心思想很朴素:把关系看成头实体到尾实体的某种“平移向量”,训练目标是让头实体向量加上关系向量尽量等于尾实体向量。这类方法参数少、训练快,但有个致命短板——它们本质上是在处理二元关系,对多跳路径、邻居结构、关系之间的语义关联几乎无能为力。你想想,如果图谱里存在“A是B的朋友,B是C的同事,那么A和C之间可能是什么关系”这种需要多跳推理的场景,翻译模型就抓瞎了。
后来图卷积网络(GCN)被引入知识图谱领域,情况开始变化。GCN能把每个实体的邻居信息聚合进来,让实体表示不再孤立,而是带有局部结构信息。但标准GCN有个问题:它在聚合邻居时,对所有邻居一视同仁,权重是固定的归一化系数。放到知识图谱里,这意味着一个实体周围如果有十个邻居,其中三个是强相关关系,七个是弱相关甚至噪声关系,GCN会不加区分地把它们混在一起。这显然不合理。
于是就有了基于注意力的嵌入方法。注意力机制的核心思想就是让模型自己学出“该关注哪些邻居、关注到什么程度”。放到关系预测任务里,就是:当我要更新某个实体的表示时,它的每个邻居实体和对应关系应该被赋予不同的权重。这个权重不是人工设定的,而是通过训练学出来的。这篇论文《Learning Attention-based Embeddings for Relation Prediction in Knowledge Graphs》要做的,就是把这个思路系统化、工程化,并在多个标准数据集上验证它的有效性。
我读这篇论文的时候,最大的感受是:它没有发明什么惊天动地的新算子,而是把注意力机制在知识图谱上的应用讲透了,从模型结构、邻居采样、关系嵌入、训练目标到实验设计,整条链路都交代得很清楚。对于想入门知识图谱表示学习、又不想一上来就啃太理论化论文的人来说,这篇是一个很好的切入点。接下来我会从模型设计、核心细节、实操复现和常见问题几个角度,把这篇论文拆开揉碎讲一遍。
2. 论文整体设计思路拆解:注意力到底加在哪里
2.1 从GCN的局限说起:为什么需要注意力
标准GCN的层间传播公式可以写成这样:每个节点的新表示等于自身旧表示与邻居旧表示的加权和,权重由邻接矩阵的度归一化决定。用大白话讲,就是“你周围有多少邻居,我就把你的特征平均分给它们”。这种平均主义在社交网络、引文网络里可能还凑合,因为那些图里边的关系类型比较单一,邻居之间的重要性差异没那么大。但知识图谱不一样,它是有向多关系图,同一个实体可能通过“出生地”“任职于”“毕业于”“配偶”等完全不同的关系连接到不同邻居。这些邻居对当前实体的语义贡献显然不同。
论文里举了一个很直观的例子:假设要预测(某科学家,?,某机构)这个三元组的关系。这个科学家的邻居里可能有“毕业于某大学”“任职于某实验室”“合作过某篇论文”等。其中“任职于”这个关系对应的邻居机构,对预测“他是否在某机构工作”显然比“合作过某篇论文”更重要。标准GCN没法区分这种重要性差异,而注意力机制可以。
2.2 模型整体架构:两层注意力聚合
论文提出的模型结构并不复杂,可以概括为“嵌入层 → 注意力聚合层 → 关系预测层”。嵌入层负责把实体和关系映射到低维向量空间;注意力聚合层是核心,通过多头注意力机制聚合邻居信息;关系预测层则用聚合后的实体表示和关系表示做打分,判断某个三元组成立的概率。
具体来说,对于每个实体,模型会先收集它在图谱中的邻居集合。注意,这里有个工程上的关键选择:邻居数量不是无限的。实际图谱里,热门实体可能有几千个邻居,全量聚合计算量爆炸,而且噪声也多。论文采用了固定大小的邻居采样策略,每个实体只保留固定数量(比如50个或100个)的邻居参与聚合。这个选择背后有明确的权衡:采样太少,信息损失大;采样太多,计算开销和噪声都上去了。论文实验部分对采样数量做了消融,后面我会详细说。
采样完成后,对每个邻居,模型计算一个注意力系数。这个系数的计算依赖三个东西:当前实体的表示、邻居实体的表示、以及它们之间关系的表示。三者拼接后过一个线性层,再经过激活函数和归一化,就得到该邻居的注意力权重。然后所有邻居的表示按权重加权求和,得到当前实体的新表示。这个过程可以堆叠多层,论文里主要用了两层,因为再深容易过平滑,实体表示会趋同。
2.3 多头注意力的引入:让模型从不同角度“看”邻居
单头注意力有一个问题:它只能学出一种“关注模式”。但知识图谱里的关系语义是多元的,有的邻居重要是因为关系类型匹配,有的邻居重要是因为实体本身相似,有的邻居重要是因为路径结构。单一注意力头很难同时捕捉这些不同维度的相关性。
论文采用了多头注意力,也就是并行跑多组注意力计算,每组有独立的参数,最后把多组结果拼接或平均。这相当于让模型从多个子空间去观察邻居的重要性。比如头1可能更关注关系类型的一致性,头2可能更关注实体本身的语义相似度,头3可能更关注结构上的邻近性。实验里论文用了8个头,这个数字不是拍脑袋定的,后面消融实验显示头数从1增加到8时效果提升明显,但超过8之后收益递减,甚至因为参数过多开始过拟合。
2.4 关系嵌入的处理:不能只更新实体
很多基于GCN的知识图谱模型有一个通病:只更新实体嵌入,关系嵌入要么固定不变,要么单独用翻译模型预训练。这篇论文明确把关系嵌入也纳入注意力聚合的更新范围。具体做法是,在聚合邻居信息时,关系的表示也会参与注意力系数的计算,并且在反向传播时得到更新。这样做的好处是,关系表示和实体表示在同一个语义空间里联合优化,避免了“实体嵌入和关系嵌入各玩各的”这种割裂。
我实际复现的时候发现,这个设计对多跳推理任务帮助很大。因为多跳推理本质上是在关系路径上做组合,如果关系嵌入不随着实体嵌入一起更新,路径上的语义一致性就很难保证。
3. 核心细节解析与实操要点:从公式到代码的映射
3.1 注意力系数的计算细节
论文里注意力系数的计算可以拆成四步。第一步,对当前实体h、邻居实体t、以及它们之间的关系r,分别取嵌入向量,拼接成一个长向量。第二步,用一个可学习的权重矩阵W把这个长向量映射到一个标量分数。第三步,对这个分数做LeakyReLU激活,增加非线性。第四步,对当前实体的所有邻居分数做softmax归一化,得到最终的注意力权重。
这里有几个实操中容易踩坑的地方。第一,LeakyReLU的负斜率参数。论文没有明确说用了多少,我试过0.01和0.2,差别不大,但用0会导致部分神经元死亡,训练不稳定。第二,softmax的温度。标准softmax在分数差异很大时会趋近于one-hot,导致只有一两个邻居被激活,其他邻居梯度消失。我在复现时加了一个温度系数,把分数除以一个大于1的数,让分布更平滑,效果更稳。第三,拼接顺序。论文没有明确说h、r、t的拼接顺序,我试过h-r-t和r-h-t,前者收敛更快,推测是因为头实体信息放在最前面更符合预测任务的因果方向。
3.2 邻居采样的策略与参数选择
邻居采样是工程实现里最影响效率的环节。论文采用的是固定数量采样,每个实体最多保留K个邻居。如果实际邻居数少于K,就重复采样或者补零;如果多于K,就随机选K个。K的取值在论文实验里是50,但我在自己的数据集上试过,K=20到K=100之间效果差异没有想象中大,但训练时间几乎线性增长。
这里有个经验:对于稀疏图谱,K可以小一点,因为邻居本来就少;对于稠密图谱,K要适当大,但不要超过100,否则注意力会被稀释。另外,采样时最好保证每个关系类型至少有一个邻居被选中,否则模型学不到该关系类型的模式。我在代码里加了一个按关系类型分层的采样逻辑,效果比纯随机采样好一截。
3.3 损失函数与负采样
论文用的是标准的二元交叉熵损失,正样本是图谱中真实存在的三元组,负样本是随机替换头实体或尾实体生成的假三元组。负采样比例是1:1,也就是每个正样本配一个负样本。这个比例在知识图谱补全任务里比较常见,但我试过1:2和1:5,发现1:2在大多数数据集上略好,因为更多的负样本能让模型学到更清晰的决策边界。
负采样还有一个细节:替换实体时不能替换成图谱中真实存在的三元组。比如(北京,是首都,中国)是正样本,如果把“中国”替换成“美国”,得到(北京,是首都,美国),这个负样本是合理的;但如果替换成某个和北京有真实“是首都”关系的实体,那就成了假负样本,会误导训练。论文里没有详细说怎么过滤,但我在实现时加了一个检查,确保生成的负样本不在正样本集合里。
3.4 训练技巧:学习率、批大小与正则化
论文用的优化器是Adam,初始学习率0.001,每训练若干轮衰减一次。批大小是128,训练轮数根据数据集不同在200到500之间。正则化方面用了dropout,比例0.3,加在注意力层之后。这些超参数看起来平平无奇,但组合起来对最终效果影响很大。
我踩过的一个坑是:学习率设太大,注意力权重会震荡。因为注意力系数本身是softmax输出,对参数变化很敏感,学习率一大,权重就在不同邻居之间跳来跳去,实体表示根本稳定不下来。后来我把学习率降到0.0005,训练轮数增加到800,效果反而更好。另一个坑是dropout加在错误的位置。如果加在注意力系数计算之前,会破坏邻居之间的相对重要性;加在之后,相当于随机屏蔽一些邻居的贡献,更合理。
4. 实操过程与核心环节实现:从零复现这篇论文
4.1 数据准备与预处理
复现的第一步是准备数据。论文用了WN18RR、FB15k-237、NELL-995这几个标准数据集。WN18RR是WordNet的子集,关系类型以词汇语义为主;FB15k-237是Freebase的子集,关系类型更杂;NELL-995来自一个持续学习系统,噪声相对多。这些数据集都可以公开获取,格式是标准的triple文件,每行是头实体、关系、尾实体,用制表符分隔。
预处理要做几件事。第一,构建实体和关系的词表,把字符串映射成整数ID。第二,划分训练集、验证集、测试集,论文用的是标准划分,但注意测试集里有些实体的所有邻居都在训练集里出现过,有些则是完全冷启动,这两种情况的难度差异很大,评估时要分开看。第三,构建邻接表,为每个实体记录它的邻居列表和对应关系。这个邻接表要支持快速采样,我用了字典嵌套列表的结构,查询复杂度O(1)。
4.2 模型搭建:用PyTorch实现注意力聚合层
模型的核心是一个自定义的PyTorch模块。输入是实体ID、关系ID和邻接表,输出是更新后的实体嵌入。前向传播分几步:先查嵌入表得到初始表示;然后对每个实体,从邻接表里采样K个邻居;接着计算注意力系数,这里用了一个线性层加LeakyReLU加softmax;最后加权求和得到新表示。整个过程支持批量操作,但要注意不同实体的邻居数量可能不同,需要padding和mask。
代码里最容易出错的地方是mask的处理。因为采样后每个实体的邻居数可能少于K,padding的位置不能参与softmax计算,否则会分走注意力权重。我的做法是在softmax之前把padding位置的分数设成一个很大的负数,这样softmax后它们的权重趋近于零。另一个细节是多头注意力的维度拆分,总维度要能被头数整除,否则拼接时会出错。
4.3 训练循环与评估指标
训练循环比较标准:每个epoch遍历训练集,按批取正样本,动态生成负样本,前向传播算损失,反向传播更新参数。评估时用MRR、Hits@1、Hits@10三个指标。MRR是平均倒数排名,衡量正确关系在预测列表里的平均位置;Hits@K是正确关系排在前K的比例。论文里报告的是过滤后的指标,也就是把其他真实存在的关系从排名里去掉,避免它们干扰评估。
我实测下来,WN18RR上MRR能到0.45左右,FB15k-237上MRR在0.35左右,和论文报告的数字基本吻合。但NELL-995上波动比较大,因为那个数据集噪声多,负采样策略对结果影响很明显。建议在NELL-995上把负采样比例调到1:3,并且增加验证集早停,能稳定不少。
4.4 关键参数配置表
下面这张表是我复现时用的核心参数配置,可以直接参考:
| 参数 | 取值 | 说明 |
|---|---|---|
| 实体嵌入维度 | 200 | 太小欠拟合,太大过拟合 |
| 关系嵌入维度 | 200 | 与实体维度一致,便于拼接 |
| 注意力头数 | 8 | 超过8收益递减 |
| 邻居采样数K | 50 | 稀疏图谱可降到20 |
| 学习率 | 0.0005 | 比论文的0.001更稳 |
| 批大小 | 128 | 显存不够可降到64 |
| Dropout | 0.3 | 加在注意力层之后 |
| 负采样比例 | 1:2 | 比论文的1:1略好 |
| 训练轮数 | 500 | 配合早停 |
5. 常见问题与排查技巧实录
5.1 训练损失不下降怎么办
这是最常见的问题。首先检查数据预处理有没有错,特别是实体和关系的ID映射,如果映射错了,模型学到的就是随机噪声。其次看学习率是不是太大,注意力模型对学习率很敏感,0.001以上很容易震荡。第三,检查负采样有没有生成假负样本,也就是替换后的三元组其实在训练集里存在。第四,看邻居采样是不是太激进,如果K太小,每个实体只看到一两个邻居,信息量不够,损失自然降不下去。
5.2 验证集指标波动大
验证集MRR在每个epoch之间跳来跳去,说明模型训练不稳定。可能的原因有三个:一是批大小太小,梯度噪声大;二是学习率没有衰减,后期在最优解附近震荡;三是数据集本身噪声大,比如NELL-995。解决办法:增大批大小到256,加学习率衰减策略(每100轮乘0.9),用验证集早停而不是固定轮数。
5.3 显存不够用
注意力模型的显存开销主要来自邻居聚合时的中间张量。如果K=50、嵌入维度200、批大小128,中间张量的维度是128×50×200×8(多头),大概几百MB。显存不够的话,优先降批大小,其次降K,最后降嵌入维度。不建议降头数,因为头数对效果影响比前三个都大。
5.4 多跳推理效果差
如果单跳关系预测还行,但多跳推理(比如预测两个实体之间的路径关系)效果很差,大概率是因为模型只学到了一阶邻居的模式,没有捕捉到路径信息。论文的模型本身是两层聚合,理论上能捕捉两跳信息,但实际中第二层的注意力容易被第一层的噪声干扰。我的做法是在第二层聚合时,对第一层的输出加一个残差连接,保留原始实体信息,效果有提升。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 损失不下降 | 数据映射错误 | 检查ID映射 | 重新生成词表 |
| 损失震荡 | 学习率过大 | 打印梯度范数 | 降低学习率 |
| 验证指标波动 | 批大小太小 | 增大批大小 | 加到256 |
| 显存溢出 | K或批大小过大 | 打印张量形状 | 降K或批大小 |
| 多跳效果差 | 层间噪声累积 | 检查第二层注意力 | 加残差连接 |
| 过拟合 | 参数过多 | 对比训练验证损失 | 加dropout或减头数 |
5.6 独家避坑技巧
第一个技巧:在注意力系数上加一个可学习的偏置项。论文没有提这个,但我发现加一个初始为零的偏置,能让模型在训练初期更稳定,因为softmax的输入不会全为零导致梯度消失。第二个技巧:对关系嵌入做L2归一化。关系嵌入的尺度如果不控制,注意力分数会被关系向量的模长主导,而不是语义。归一化后,注意力更关注方向而不是长度。第三个技巧:用验证集上的Hits@10而不是MRR做早停。MRR对排名靠后的正确关系不敏感,Hits@10更能反映模型在实际应用中的可用性。
6. 这篇论文的延展与个人体会
读完这篇论文并复现之后,我最大的体会是:注意力机制在知识图谱上的应用,核心难点不在注意力本身,而在如何高效地组织邻居信息。论文的模型结构其实不复杂,但邻居采样、多头维度拆分、mask处理这些工程细节,才是决定最终效果的关键。很多论文复现失败,不是模型理解错了,而是这些细节没处理好。
另外,这篇论文的实验部分做得比较扎实,在三个数据集上做了消融,包括头数、邻居数、层数、负采样比例的影响。我建议读的时候重点看消融部分,因为那里面藏着很多“为什么这么设计”的答案。比如头数从1到8提升明显,但到16就饱和,这说明知识图谱里的关系语义维度大概就在8左右,再多就是冗余。
后续如果想继续深入,有两个方向可以扩展。一是把注意力机制和路径推理结合,比如在注意力聚合时不仅考虑一阶邻居,还考虑二阶路径的注意力。二是把模型用到动态图谱上,也就是图谱随时间变化,实体和关系会新增或失效,这时候注意力权重也需要动态调整。这两个方向在工业界都有实际需求,但论文没有涉及,可以作为后续研究的切入点。
最后分享一个小技巧:复现这类论文时,不要一上来就追求和论文完全一致的指标。先把模型跑通,用一个小数据集(比如WN18RR的子集)验证代码逻辑,确认损失能下降、指标能上升,再换全量数据调参。我见过太多人直接上全量数据,结果训练三天三夜指标不动,最后发现是数据加载的shuffle没开。这种坑,踩一次就够了。