4、相对位置编码(Relative):Shaw et al. 提出的相对位置编码思想
好,咱们继续聊位置编码。上一节我们聊了绝对位置编码,说白了就是给每个词一个固定的“座位号”。但我在实际做长文本任务时发现,这种硬编码方式有个很头疼的问题——模型很难学到“词与词之间的相对关系”。你想想看,句子“我喜欢猫”和“猫喜欢我”,如果只用绝对位置,模型得重新学习“喜欢”和“猫”在不同位置时的关系,这多累啊。
所以,Shaw 等人就在 2018 年提出了相对位置编码。这个思路很巧妙:我们不需要知道词在句子里的绝对坐标,只需要知道它们之间的距离。比如“我”和“喜欢”距离是 1,“我”和“猫”距离是 2,这就够了。
为什么需要相对位置?
我个人习惯把这个问题拆成两个痛点来讲:
- 长距离依赖:句子一长,绝对位置编码的向量之间差异会变得很小,模型很难区分“距离 50”和“距离 51”的区别。但相对位置编码只关心相对距离,距离远了就是远了,不会因为句子变长而模糊。
- 平移不变性:这个术语听着高大上,其实很简单——你把句子里的所有词都往后挪两个位置,绝对位置编码就全变了,模型得重新学。但相对位置编码不受影响,因为词与词之间的相对距离没变。我在做文本分类时遇到过这种情况:训练集里句子开头都是“你好”,测试集里变成了“请问你好”,结果模型就懵了。用相对位置编码就能解决这个问题。
核心思想:在计算注意力分数时,不再使用绝对位置向量,而是引入一个可学习的相对位置偏置。这个偏置只取决于两个 token 之间的相对距离。
Shaw et al. 的数学原理
咱们直接看公式。标准的自注意力计算是这样的:
Attention(Q, K, V) = softmax(QK^T / √d) V
Shaw 的改进版本在计算 Q 和 K 的相似度时,加了一个相对位置项:
e_ij = (x_i W_Q) (x_j W_K + a_{ij}^K)^T / √d
这里 a_{ij}^K 就是可学习的相对位置向量。它只依赖于 i 和 j 之间的差值 j - i。说白了,就是给每个可能的距离都配一个专属的向量。
嗯,这里要注意:a_{ij}^K 的维度跟 K 的维度是一样的。而且我们通常会对距离做截断,比如只考虑 [-k, k] 范围内的距离,超出部分用边界值代替。为什么?因为太远的词之间其实没什么直接关系,截断还能减少参数量。
我的经验:截断窗口 k 一般取 64 或 128 就够了。我曾经试过取 512,结果模型不仅没变好,训练还慢了不少。后来想想也对,两个词隔了 100 多个位置,它们之间早就被其他词隔断了,直接学它们的关系意义不大。
相对位置编码的两种实现方式
在实际工程中,我见过两种主流实现:
| 方式 | 描述 | 优缺点 |
|---|---|---|
| 加法式 | 在注意力分数计算时直接加上相对位置偏置 | 实现简单,但参数量随窗口大小线性增长 |
| 乘法式 | 将相对位置编码作为权重矩阵乘到 K 或 V 上 | 更灵活,但计算量稍大 |
我个人更推荐加法式,因为它在大多数任务上效果已经足够好,而且实现起来不容易出错。你想想看,乘法式要处理矩阵乘法,维度稍微没对齐就报错,调试起来很烦人。
代码示例:实现一个简单的相对位置偏置
下面我写一个 PyTorch 风格的伪代码,展示如何实现相对位置偏置:
import torch
import torch.nn as nn
class RelativePositionBias(nn.Module):
def __init__(self, max_distance=64, num_heads=8):
super().__init__()
self.max_distance = max_distance
# 为每个可能的距离和每个头学习一个偏置
self.relative_bias = nn.Embedding(2 * max_distance + 1, num_heads)
def forward(self, seq_len):
# 生成相对距离矩阵
# 比如 seq_len=4,距离矩阵是:
# [[0, 1, 2, 3],
# [-1, 0, 1, 2],
# [-2, -1, 0, 1],
# [-3, -2, -1, 0]]
distances = torch.arange(seq_len).unsqueeze(0) - torch.arange(seq_len).unsqueeze(1)
# 截断到 [-max_distance, max_distance]
distances = distances.clamp(-self.max_distance, self.max_distance)
# 偏移到非负索引
indices = distances + self.max_distance
# 查表得到偏置,形状 [seq_len, seq_len, num_heads]
bias = self.relative_bias(indices)
return bias.permute(2, 0, 1) # [num_heads, seq_len, seq_len]
这段代码的核心就是:先算距离矩阵,然后截断,最后查表。我在项目里经常把这个模块加到 Transformer 的注意力层里,效果立竿见影。
避坑指南:我曾经犯过一个错误——忘记对距离做截断。结果模型在训练时只见过 [-64, 64] 范围内的距离,但推理时遇到了距离 100 的情况,查表直接越界。所以一定要在训练和推理时都用相同的截断策略。
相对位置编码的优缺点
咱们客观地总结一下:
- 优点:
- 天然支持变长序列,不会因为句子长度变化而失效
- 对句子平移不敏感,泛化能力更强
- 长距离依赖建模效果更好
- 缺点:
- 实现比绝对位置编码复杂一些
- 需要额外存储相对位置偏置表
- 截断窗口的选择需要调参
说白了,相对位置编码就是用一点实现复杂度,换来了更好的泛化能力。在大多数 NLP 任务中,这个 trade-off 是值得的。
知识体系图
下面我用一张 SVG 图来总结相对位置编码的核心逻辑:
这张图展示了相对位置编码的完整流程:从输入序列开始,先计算两两之间的相对距离,然后截断到合理范围,再查表得到偏置,最后融入到注意力分数的计算中。整个过程清晰明了,没有弯弯绕绕。
好了,相对位置编码的核心思想就讲到这里。说白了,它就是让模型学会“词与词之间的距离关系”,而不是死记硬背每个词的绝对位置。下一节我们会聊另一个经典的位置编码方案——T5 的相对位置偏置,它在这个基础上做了进一步的简化。
一句话总结:相对位置编码通过引入可学习的距离偏置,解决了绝对位置编码在长距离依赖和平移不变性上的不足,是 Transformer 位置编码发展史上的重要里程碑。