4、能量函数与边际损失函数归纳总结
好,咱们今天聊聊 TransE 里最核心的两个东西:能量函数和边际损失函数。
说白了,能量函数就是衡量一个三元组(头实体、关系、尾实体)到底有多“合理”。而边际损失函数,就是告诉模型怎么去学、学到什么程度才算好。这两个东西一配合,TransE 才能跑起来。
我个人习惯把能量函数看作一把尺子,把损失函数看作一个教练。尺子量好坏,教练定标准。嗯,咱们一个一个掰开来看。
4.1 能量函数:怎么判断一个三元组合不合理?
TransE 的核心假设特别简单:头实体向量 + 关系向量 ≈ 尾实体向量。
那能量函数呢,就是来衡量这个“≈”到底差了多少。公式长这样:
f(h, r, t) = || h + r - t ||_p
这里 || · ||_p 可以是 L1 范数或者 L2 范数。我个人在项目中常用 L1 范数,为什么?因为 L1 对异常值没那么敏感,训练过程中更稳定。你想想看,L2 会把大误差放大得更大,有时候一个离谱的负样本就能把梯度带偏。
能量值越小,说明这个三元组越合理。比如(阿司匹林,治疗,头痛),能量值应该很低。而 (阿司匹林,治疗,汽车),能量值就得高。
| 范数类型 | 公式 | 特点 | 我的建议 |
|---|---|---|---|
| L1 | |h + r - t| | 梯度恒定,对异常值不敏感 | 推荐,训练更平滑 |
| L2 | (h + r - t)² | 梯度随误差增大,对异常敏感 | 调试时可尝试,收敛可能更快 |
注意,这里的实体和关系都是嵌入向量,维度要一致。比如都是 100 维或 200 维。维度怎么选?
💡 我的经验:维度选择
5 万实体、1 万关系的药物图谱,我用 128 维就够。但如果你数据量更大(比如几十万实体),我建议试试 200 维。低了欠拟合,高了容易过拟合。
来看一段 Python 实现,其实特别简单:
def energy_function(h, r, t, norm=1):
"""
计算三元组能量值
:param h: 头实体向量
:param r: 关系向量
:param t: 尾实体向量
:param norm: 1 为 L1,2 为 L2
:return: 能量值(标量)
"""
if norm == 1:
return torch.sum(torch.abs(h + r - t))
elif norm == 2:
return torch.sum((h + r - t) ** 2)
4.2 边际损失函数:让正例和负例拉开距离
光能算能量还不够,你得告诉模型:正样本(正确的三元组)能量要低,负样本(错误的三元组)能量要高,而且两者之间要留出足够宽的缓冲区。
这个缓冲区,就是边际(margin)。边际损失函数的公式长这样:
L = Σ max(0, γ + f(h, r, t) - f(h', r, t'))
其中:
γ —— 边际值,超参数
f(h, r, t) —— 正样本能量
f(h', r, t') —— 负样本能量
为什么叫“边际”?说白了,正样本的能量必须比负样本的能量至少低 γ 这么多。如果差值没达到 γ,就有损失;如果已经超过了,那就没有损失,模型就不需要更新了。
我在项目中遇到过一个问题:γ 设得太小,比如 0.1,正负样本几乎分不开;设得太大,比如 10,模型会一直尝试拉开距离,训练根本稳不下来。所以 γ 的选择很关键。
⚠️ 我曾经踩过的坑
有一回我把 γ 设成了 50,想着“越大越安全”,结果训练了三天都没收敛。后来改成 1.0,两小时就好了。记住:边际不是越大越好,而是刚好够用。
再来看损失函数的实现:
def margin_loss(pos_energy, neg_energy, margin=1.0):
"""
计算边际损失
:param pos_energy: 正样本能量
:param neg_energy: 负样本能量
:param margin: 边际值
:return: 损失值(标量)
"""
# 核心:正样本 - 负样本 + 边际,取正的部分
losses = torch.relu(margin + pos_energy - neg_energy)
return torch.mean(losses)
4.3 负样本怎么产生?
我刚才提到了负样本,但没有细说。实际训练时,你用的一般是正样本三元组。那负样本从哪来?替换法。
- 替换头实体:把正确的头实体换成随机的其他实体
- 替换尾实体:把正确的尾实体换成随机的其他实体
比如正样本(阿司匹林,治疗,头痛),可以变成:
- (对乙酰氨基酚,治疗,头痛)—— 替换头实体
- (阿司匹林,治疗,胃痛)—— 替换尾实体
这两者都是负样本,能量应该高。但要注意:替换的时候,有可能运气不好,替换成了另一个正确的三元组。比如(对乙酰氨基酚,治疗,头痛)其实也是正确的!这就叫“假阴性”。
💡 避坑指南
我曾经用统一的训练集去采样,结果负样本里有 5% 是假阴性。后来我改用“伯努利采样”:以 50% 的概率替换头实体,50% 替换尾实体,并且过滤掉训练集中已知的正确三元组。这能大幅度提高模型质量。
4.4 训练流程:把能量函数和损失函数串起来
现在我们来梳理一下整个训练步骤:
- 采样一个正样本:从训练集随机取一个三元组
- 生成对应的负样本:替换头或尾实体
- 计算正样本能量:f(h, r, t) = ||h + r - t||
- 计算负样本能量:f(h', r, t') = ||h' + r - t'||
- 计算损失:max(0, γ + f(h, r, t) - f(h', r, t'))
- 反向传播:更新实体和关系的嵌入向量
- 重复:直到损失收敛
来看一下完整的训练循环:
for epoch in range(num_epochs):
total_loss = 0
for batch in dataloader:
# batch 包含正样本的 (h, r, t)
pos_h, pos_r, pos_t = batch
# 生成负样本
neg_h, neg_r, neg_t = generate_negative(pos_h, pos_r, pos_t)
# 计算能量
pos_energy = energy_function(pos_h, pos_r, pos_t)
neg_energy = energy_function(neg_h, neg_r, neg_t)
# 计算损失
loss = margin_loss(pos_energy, neg_energy, margin=1.0)
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f"Epoch {epoch}: Loss = {total_loss / len(dataloader):.4f}")
4.5 关键超参数速查表
我这里整理了一个表格,方便你以后调参时快速参考:
| 参数 | 推荐范围 | 过大 | 过小 |
|---|---|---|---|
| 嵌入维度 d | 50~200 | 过拟合 | 欠拟合 |
| 边际值 γ | 0.5~5.0 | 不收敛 | 正负样本无法区分 |
| 学习率 lr | 0.001~0.01 | 振荡 | 收敛太慢 |
| 范数类型 p | L1 或 L2 | —— | —— |
我个人常用的组合是:d=128, γ=1.0, lr=0.005, L1。这组参数在药物图谱上表现相当稳健。
4.6 核心逻辑归纳:一张图看懂
好,接下来我用一张 SVG 图把能量函数和边际损失函数的关系理清楚:
图里可以看到:正样本能量往下走,负样本能量往上走,两者差距够大时损失为 0。这就是边际损失的精髓。
4.7 归纳总结:记住这四点
最后,我把能量函数和边际损失函数的核心要点提炼成几句话,帮你快速记住:
- 能量函数:f = ||h + r - t||,越小越合理。
- 边际损失:确保正样本能量 ≤ 负样本能量 - γ。
- γ 是安全边际:推荐 1.0 起步,根据验证集调整。
- L1 比 L2 更稳健:我推荐 L1,尤其是在数据有噪音的药物图谱中。
其实这两个函数加在一起,干的事特别简单:拉开好三元组和坏三元组之间的距离。你先把这个逻辑理解透,后面再怎么调参、怎么扩展,都离不开这个基础。