
1. 为什么多关系图卷积需要一次“组合式”重构如果你接触过知识图谱、推荐系统或者生物分子相互作用网络大概率绕不开一个核心问题边上带标签的图怎么卷积。普通GCN把邻居特征一视同仁地平均R-GCN给每种关系配一套权重矩阵看起来已经解决了多关系的问题。但真正上手跑过大规模知识图谱的人会发现R-GCN的参数数量随关系种类线性膨胀一个几万种关系的图谱光权重矩阵就能把显存吃干净而且长尾关系根本训不动。CompGCN这篇工作的切入点非常直接与其给每种关系单独学一个变换矩阵不如把关系也当成一种“节点特征”用组合操作把节点和关系揉在一起。这个思路在直觉上很像我们处理文本时的做法——不是给每个词单独建一个分类器而是把词映射到向量空间通过运算捕捉词与词的关系。CompGCN把关系向量和节点向量做减法、乘法或相关性运算用一套共享的参数完成所有关系的消息传递。我第一次读到这个设计时第一反应是“这不就是把R-GCN的权重矩阵分解了吗”。但仔细推下来它解决的不只是参数量问题。R-GCN的每种关系独立变换导致关系之间完全隔离学到的表示无法迁移而CompGCN通过组合操作强制节点和关系在同一空间交互关系之间的语义相似性可以自然地被捕捉到。这一点在关系数量多但每个关系样本少的场景下尤其关键。这篇文章适合谁看如果你正在做知识图谱补全、多关系链路预测或者任何需要处理带类型边的图神经网络任务CompGCN的这套组合机制值得你花时间吃透。下面我会从设计动机、核心公式、实操细节到踩坑经验完整拆一遍。2. 从R-GCN到CompGCN组合操作到底改了什么2.1 R-GCN的参数量困境与关系隔离问题先回顾一下R-GCN的消息传递公式。对于节点v在第l1层的表示R-GCN的做法是h_v^(l1) σ( Σ_{r∈R} Σ_{u∈N_r(v)} W_r^(l) h_u^(l) W_0^(l) h_v^(l) )其中W_r是每种关系r对应的权重矩阵。假设隐藏维度是d关系种类是|R|那光关系权重就有|R| × d × d个参数。在WN18RR这样的数据集上关系数不到20种问题不大但到了Freebase这类有几千上万种关系的图谱参数量直接爆炸。更麻烦的是很多关系只有寥寥几条边W_r根本学不好最后这些关系的表示质量极差。还有一个容易被忽略的问题关系之间没有交互。W_r1和W_r2是两个完全独立的矩阵模型没有任何机制去利用“关系r1和r2语义相近”这个先验。比如“出生于”和“籍贯是”在语义上高度相关但R-GCN学出来的两个变换矩阵可能毫无关联。这在数据充足时不算大问题但在长尾关系上就是致命的。2.2 CompGCN的组合算子减法、乘法与相关性CompGCN的核心改动是把R-GCN中的W_r h_u替换成一个组合函数φ(h_u, h_r)其中h_r是关系r的向量表示。原论文尝试了三种组合方式减法Subtractionφ(h_u, h_r) h_u - h_r。这个操作在翻译模型TransE里被证明有效直觉是“头实体向量减去关系向量应该接近尾实体向量”。乘法Multiplicationφ(h_u, h_r) h_u * h_r。逐元素相乘类似DistMult的做法适合捕捉对称关系。相关性Correlationφ(h_u, h_r) h_u ★ h_r即循环相关操作类似HolE。实测下来减法在多数知识图谱数据集上表现最稳乘法在关系对称性强的场景有优势相关性计算开销最大但提升有限。论文最终推荐减法作为默认选择这也符合大多数从业者的使用习惯。关键在于关系向量h_r是全局共享的所有边共享同一套关系嵌入参数量从|R| × d × d直接降到|R| × d。在关系数上千的数据集上这个压缩比是数量级的。2.3 组合操作背后的参数共享逻辑你可能会问把W_r换成φ(h_u, h_r)表达能力会不会下降答案是不会反而可能更强。原因在于R-GCN的W_r是独立学习的而CompGCN的关系向量h_r在每一层都会和节点表示交互相当于关系表示也在随层数演化。更重要的是组合操作强制节点和关系在同一语义空间中对齐这带来两个好处第一关系之间的相似性可以被自然捕捉。如果两个关系向量接近它们对节点消息传递的影响也接近模型不需要为每个关系单独学一套参数就能泛化到新关系。第二参数效率极高。在关系数很少的数据集上CompGCN和R-GCN差距不大但在关系数上千的数据集上CompGCN的优势非常明显。我实测过一个有约3000种关系的图谱R-GCN直接OOMCompGCN用同样的显存跑得很流畅。注意组合操作的选择不是绝对的。如果你的关系种类少于50种且每种关系都有充足样本R-GCN可能仍然有竞争力。CompGCN的真正优势场景是关系多、长尾严重、参数量敏感的任务。3. 把公式拆开看CompGCN每一层到底在算什么3.1 节点更新的完整计算流程CompGCN的单层更新可以拆成四步第一步关系向量初始化。为每种关系r初始化一个d维向量h_r通常用均匀分布或正态分布初始化。这些向量是可学习的参数在训练中不断更新。第二步邻居消息组合。对于节点v的每个邻居u通过关系r连接计算组合消息m_{u,r} φ(h_u^(l), h_r^(l))如果关系有方向反向关系用另一个关系向量h_{r^{-1}}或者用同一个向量加一个方向标记。第三步消息聚合。把所有邻居的组合消息求和或取平均m_v^(l) Σ_{(u,r)∈N(v)} m_{u,r}第四步节点更新。把聚合消息和自环消息拼接或相加过一层线性变换和激活函数h_v^(l1) σ( W_self^(l) h_v^(l) W_msg^(l) m_v^(l) )注意这里的W_self和W_msg是所有关系共享的不再随关系变化。这就是参数量压缩的关键。3.2 关系向量在层间的传播方式CompGCN还有一个容易被忽略的细节关系向量本身也在层间更新。原论文的做法是关系向量在每一层通过一个单独的线性变换更新h_r^(l1) W_rel^(l) h_r^(l)这个设计让关系表示也能随层数演化而不是固定不变。消融实验表明如果去掉关系向量的层间更新模型在深层的表现会明显下降。原因在于浅层的关系表示可能只捕捉了粗粒度的语义深层需要更精细的区分。实操中W_rel通常和W_self、W_msg共享或者用独立的矩阵。我自己的经验是独立矩阵效果略好但参数量增加不多因为关系数远小于节点数W_rel的参数量是|R| × d × d在关系数可控时完全可以接受。3.3 基分解与低秩近似进一步压缩参数如果关系数实在太多CompGCN还支持基分解basis decomposition。思路和R-GCN的基分解类似不直接学每种关系的向量而是学一组基向量每种关系的向量是基向量的线性组合h_r Σ_{b1}^{B} α_{rb} v_b其中B是基的数量远小于关系数|R|。这样参数量从|R| × d降到B × d |R| × B。在关系数上万的数据集上这个压缩非常可观。不过基分解会带来一定的表达能力损失。我的建议是关系数在5000以下时直接用独立关系向量超过5000再考虑基分解。这个阈值不是绝对的取决于你的显存和任务精度要求。4. 动手实现从零搭建一个CompGCN层4.1 环境准备与依赖选择实现CompGCN不需要特殊的库PyTorch加上PyGPyTorch Geometric就够了。PyG从2.x版本开始内置了CompGCNConv但如果你想深入理解细节建议自己手写一遍。依赖清单Python 3.8PyTorch 1.10PyG 2.0可选用于消息传递框架NumPy、SciPy数据处理如果你用PyG的MessagePassing基类代码会简洁很多如果从零写需要自己处理邻接表的构建和消息聚合。我建议先用PyG跑通再手写一遍加深理解。4.2 组合函数的代码实现与选择依据先定义三种组合函数import torch import torch.nn as nn import torch.nn.functional as F def compose_sub(h_u, h_r): return h_u - h_r def compose_mult(h_u, h_r): return h_u * h_r def compose_corr(h_u, h_r): # 循环相关用FFT实现 return torch.fft.irfft(torch.fft.rfft(h_u, dim-1) * torch.conj(torch.fft.rfft(h_r, dim-1)), dim-1)选择依据减法适合大多数场景乘法适合对称关系多的图谱相关性计算开销大且提升不稳定。我实测过WN18RR和FB15k-237减法在两者上都表现最好乘法在WN18RR上略差但在对称关系子集上有优势。4.3 消息聚合与节点更新的代码细节一个完整的CompGCN层class CompGCNLayer(nn.Module): def __init__(self, in_dim, out_dim, num_rels, composesub): super().__init__() self.in_dim in_dim self.out_dim out_dim self.num_rels num_rels self.compose compose # 关系向量 self.rel_emb nn.Embedding(num_rels, in_dim) nn.init.xavier_uniform_(self.rel_emb.weight) # 共享的线性变换 self.w_self nn.Linear(in_dim, out_dim, biasFalse) self.w_msg nn.Linear(in_dim, out_dim, biasFalse) self.w_rel nn.Linear(in_dim, out_dim, biasFalse) self.act nn.ReLU() def forward(self, node_emb, edge_index, edge_type): # node_emb: [N, in_dim] # edge_index: [2, E] # edge_type: [E] src, dst edge_index rel self.rel_emb(edge_type) # [E, in_dim] # 组合 if self.compose sub: msg node_emb[src] - rel elif self.compose mult: msg node_emb[src] * rel else: msg compose_corr(node_emb[src], rel) # 聚合scatter_add out torch.zeros_like(node_emb) out.index_add_(0, dst, msg) # 更新 h self.w_self(node_emb) self.w_msg(out) h self.act(h) # 更新关系向量 rel_out self.w_rel(self.rel_emb.weight) return h, rel_out几个实操要点index_add_比scatter_add更稳定尤其在边数多的时候。关系向量的更新不要用激活函数原论文用的是线性变换加了ReLU反而掉点。自环消息必须保留否则节点会丢失自身信息。可以在edge_index里手动加自环边或者在更新时单独加W_self项。4.4 训练配置与超参数经验值以下是我在FB15k-237上跑通的一组配置供参考参数取值说明隐藏维度200再大提升有限显存翻倍层数23层以上容易过平滑组合方式sub减法最稳学习率0.001Adam配合梯度裁剪批大小1024边采样不是节点采样Dropout0.1加在每层输出基分解不用关系数5000时不需要训练时用边采样而不是全图训练每个batch随机采样一批边构建子图做消息传递。这样显存可控收敛也稳定。注意采样时要保证每个节点至少有一条边否则孤立节点的表示会退化。5. 实测中那些文档不会告诉你的坑5.1 关系向量初始化不当导致训练震荡我一开始用全零初始化关系向量结果训练loss剧烈震荡前几个epoch几乎不收敛。后来换成Xavier均匀初始化问题消失。原因在于如果关系向量初始为零组合操作尤其是减法会让所有邻居消息退化成节点自身表示模型在初期完全学不到关系信息。建议关系向量用Xavier或正态初始化标准差控制在0.1左右。不要用全零或全一也不要用太大的方差。5.2 反向关系处理与边方向的一致性知识图谱中的边通常是有向的但消息传递时需要考虑反向关系。CompGCN的处理方式是为每个反向关系分配一个独立的关系向量或者用同一个向量加一个方向标记。原论文用的是前者即关系总数翻倍。实操中如果你用PyG的edge_index需要手动把反向边加进去并给它们分配对应的反向关系ID。这一步很容易出错尤其是关系ID的映射。我的做法是先构建正向边的关系映射再统一加一个偏移量生成反向关系ID确保不重叠。注意如果忘记加反向边模型在链路预测任务上会掉5-10个点因为很多测试边依赖反向信息。5.3 层数加深后的过平滑与残差连接CompGCN和其他GCN一样层数多了会过平滑。我试过4层节点表示几乎趋同链路预测精度反而下降。2层是最佳选择3层需要加残差连接。残差连接的实现很简单每层输出加上输入h h node_emb # 需要维度一致如果输入输出维度不同用线性投影对齐。加了残差之后3层甚至4层也能训但提升不明显计算开销却翻倍。我的建议是优先用2层除非你的图谱直径特别大。5.4 评估指标的选择与负采样陷阱链路预测常用的指标是MRR、Hits1、Hits10。CompGCN原论文用的是filtered setting即过滤掉训练集中已有的正样本。如果你自己实现评估一定要做filtered否则指标会虚低。负采样时不要用均匀采样。知识图谱的实体度分布是幂律的均匀采样会让高频实体被过度采样为负样本导致模型学到“高频实体就是负样本”的偏见。建议用度相关的采样概率或者直接用1:1的正负比每个正样本采一个负样本。6. 组合操作之外CompGCN还能怎么改6.1 引入注意力机制做加权聚合CompGCN的聚合是简单求和所有邻居权重相同。一个自然的改进是引入注意力让模型自己学邻居的重要性。做法和GAT类似计算节点和邻居组合消息的注意力分数softmax后加权求和。我试过在CompGCN上加一层注意力在FB15k-237上MRR提升了约1.5个点但训练时间增加了30%。如果你的任务对精度要求极高且算力充足值得加否则简单求和性价比更高。6.2 多跳关系的组合传播CompGCN只处理一跳邻居。对于多跳关系比如“A的父亲的妻子”需要多跳推理。一个思路是在关系向量层面做组合把多跳路径上的关系向量依次做组合操作得到一个虚拟关系向量再用它去更新节点。这个思路在路径推理任务上有潜力但实现复杂度高且容易引入噪声。我目前还在实验阶段初步结果不如直接堆层数稳定。6.3 异构图上的适配与扩展CompGCN假设所有边都是同一类型只是关系标签不同。如果你的图是异构图比如同时有用户-商品、用户-用户、商品-商品需要为每种节点类型和边类型分别处理。一个可行的方案是为每种节点类型分配独立的线性变换关系向量全局共享。这样既保留了组合操作的参数效率又适配了异构性。我在一个模拟的推荐场景中试过这个方案效果比直接用R-GCN好但比专门设计的异构图模型略差。如果你的图异构性很强建议还是用专门的异构图神经网络。7. 一些个人体会CompGCN最打动我的地方不是某个具体指标而是它把“关系”从变换矩阵的索引变成了可学习的向量。这个视角转换让很多之前棘手的问题变得自然新关系冷启动直接初始化一个关系向量和已有关系向量做相似度匹配就能快速适配。关系语义漂移关系向量随层更新自动捕捉上下文变化。当然它也不是万能的。如果你的关系种类很少比如少于20种且每种关系样本充足R-GCN或者简单的RGCN变体可能更直接。CompGCN的优势在关系多、长尾重、参数敏感的场景下才真正体现。最后分享一个调试技巧先在小规模子图上验证组合操作的正确性。取100个节点、500条边手动算一遍消息传递和代码输出对比。这一步能帮你排除80%的实现bug比直接上全图调试高效得多。