ComplEx 模型数学原理:复数空间嵌入

好,咱们直接进入正题。上一章我们聊了知识图谱补全的基本任务,也对比了几种经典模型的得分函数。这一章我重点讲讲 ComplEx 是怎么通过复数空间来解决对称和反对称关系问题的。说白了,这是一个非常优雅的技巧——把实体和关系嵌入到复数域里,借用欧拉公式天然处理方向性。

为什么要用复数?从 DistMult 的缺陷说起

先回忆一下 DistMult 的得分函数:f(e_s, r, e_o) = e_s · r · e_o,点积运算在实数空间里是对称的,所以模型只能学到对称关系。但我当时做一个药物副作用预测项目时发现,绝大多数药物‑副作用关系都不是对称的——比如“阿司匹林导致胃出血”,反过来“胃出血”不会导致阿司匹林。用 DistMult 跑完之后,对称关系得分居然跟非对称关系一样高,显然不合理。

后来读到 Trouillon 那篇 ComplEx 论文,思路很直接:把嵌入从实数推广到复数。复数有幅值和相位,相位天然可以编码方向。你想想看,一个复数 a + bi 的共轭 a - bi 旋转了虚部的符号,这不就区分出“头→尾”和“尾→头”了吗?

核心洞察: 引入复共轭运算,让得分函数变成f(s,r,o) = Re(⟨e_s, r, e_o⟩),其中⟨·,·,·⟩是在复数域上的三线性乘积,再取实部。因为共轭的存在,f(s,r,o) ≠ f(o,r,s),除非关系是对称的。

复数嵌入的数学定义

我们需要为每个实体和关系定义两组参数:实部向量和虚部向量。假设嵌入维度为 k,那么每个实体 e 对应两个 k 维向量:Re(e) ∈ ℝᵏ 和 Im(e) ∈ ℝᵏ。关系同理,记作 Re(r), Im(r)。

在复数域中,实体嵌入可以写作 zₛ = Re(e_s) + i·Im(e_s),关系嵌入 wᵣ = Re(r) + i·Im(r),目标实体嵌入 zₒ = Re(e_o) + i·Im(e_o)。得分函数定义为:

f(s, r, o) = Re( ⟨zₛ, wᵣ, z̅ₒ⟩ )    // z̅ₒ 是 zₒ 的复共轭

展开实部:
f(s, r, o) =   ⟨Re(e_s), Re(r), Re(e_o)⟩ 
             + ⟨Re(e_s), Im(r), Im(e_o)⟩ 
             + ⟨Im(e_s), Re(r), Im(e_o)⟩ 
             - ⟨Im(e_s), Im(r), Re(e_o)⟩

注意最后一项是负号,这就是不对称性的来源。如果关系是对称的,模型会学到 Im(r)=0,那最后两项抵消,变成对称得分。我在调优时发现,大部分药物‑靶标关系确实需要非零的虚部才能拟合正确方向。

参数规模与初始化

比起 DistMult,ComplEx 的参数翻了一倍,但维度 k 可以适当减半达到类似效果。我习惯把实体和关系的维度统一设为 k=200,这样每个实体有 400 个参数,关系也是 400 个参数,整体规模跟 Rescal 的 k=100 差不多。

模型 实体参数 关系参数 总参数量(N实体,M关系)
DistMult N·k M·k (N+M)·k
ComplEx 2·N·k 2·M·k 2·(N+M)·k
Rescal N·k M·k² N·k + M·k²

初始化陷阱: 我曾直接使用均匀分布初始化实部和虚部,结果训练初期 loss 剧烈震荡。原因是复数域中模长过大导致梯度爆炸。后来我改用 nn.init.xavier_uniform_ 对实部和虚部分别初始化,再用 0.01 的小缩放,收敛就稳定了。

训练细节与损失函数

我推荐使用二元交叉熵损失 + 负采样。对每个正三元组,随机替换头或尾生成 1:1 或 1:10 的负样本。PyTorch 实现中,我们可以把实体嵌入和关系嵌入合并成一个复数向量,但运算时仍用实部和虚部分开计算更直接。

下面是我常用的得分函数实现片段:

import torch

def complEx_score(head_emb, rel_emb, tail_emb):
    # head_emb, rel_emb, tail_emb: (batch, k) 每个包含实部和虚部
    # 假设输入形状: (batch, 2*k) 前k维实部,后k维虚部
    h_re, h_im = head_emb.chunk(2, dim=-1)
    r_re, r_im = rel_emb.chunk(2, dim=-1)
    t_re, t_im = tail_emb.chunk(2, dim=-1)

    # 计算实部 = h_re·r_re·t_re + h_re·r_im·t_im + h_im·r_re·t_im - h_im·r_im·t_re
    score = (torch.sum(h_re * r_re * t_re, dim=-1) +
             torch.sum(h_re * r_im * t_im, dim=-1) +
             torch.sum(h_im * r_re * t_im, dim=-1) -
             torch.sum(h_im * r_im * t_re, dim=-1))
    return score  # (batch,)

训练时我会把 score 传给 sigmoid,然后计算 BCE。你也可以用 margin ranking loss,不过我觉得 BCE 在药物关系预测这种不平衡数据上更稳。

经验之谈: 负采样策略直接影响效果。我试过均匀随机采样和基于伯努利分布的“类型受限采样”(只采样同类型实体的负例),后者在药物关系数据集中 Hit@10 提升了 3 个点。如果你在医学知识图谱上做,建议用类型受限采样。

SVG:ComplEx 模型复数空间嵌入流程

下面这张图展示了 ComplEx 处理一个三元组的完整流程。实体和关系的实部/虚部分别经过共轭操作与点积,最后聚合出实部得分。

ComplEx 模型复数嵌入计算流程 头实体 zₛ Re(e_s), Im(e_s) 关系 wᵣ Re(r), Im(r) 尾实体 zₒ Re(e_o), Im(e_o) 复共轭 Re, -Im 三线乘积 ⟨zₛ, wᵣ, z̅ₒ⟩ 在复数域上 取实部 Re(·) 得分 f(s,r,o) 说明:每个实体/关系需要学习实部和虚部两组向量;共轭操作改变虚部符号,从而引入方向性 最终得分是复数三线型乘积的实部,可展开为四项标量累加

与常见模型的对比

我把复数嵌入跟另外两种方法放在一起做了个对比表,方便你理解 ComplEx 的独特位置:

模型 嵌入空间 对称关系 反对称关系 参数效率
DistMult ℝᵏ ✅ 自然支持 ❌ 无法区分 高
ComplEx ℂᵏ ✅ (Im=0) ✅ (Im≠0) 中(2k做实参)
RotatE ℂᵏ (旋转) ✅ (θ=0) ✅ (θ≠0) 中(仅关系有相位)

个人偏好: 如果数据集里对称和反对称关系都很多,我首选 ComplEx;如果主要关心链式推理(比如药物‑靶标‑疾病路径),RotatE 的旋转组合特性可能更强。但 ComplEx 的梯度更方便计算,调参成本更低。

避坑指南:复共轭的梯度

我刚才提到展开式里有负号项,这导致实部梯度和虚部梯度方向可能不同。有一次我手动写反向传播时漏掉了虚部对实部的耦合项,导致 Loss 下降一半就卡住了。现代框架自动微分会处理好,但你如果用自定义算子,务必检查梯度形状。

另外,我建议在训练过程中监控实体嵌入的复数模长分布。如果大部分模长接近0,说明模型在走捷径(欠拟合);如果个别模长 >10,很可能是过拟合了某些高频率实体。一个简单的办法:每 5 个 epoch 输出一次 torch.mean(torch.abs(z)),正常应该在 0.5~2 之间。