3、相对位置编码:从绝对到相对,T5等模型中的相对位置偏置
为什么我们需要“相对”这个概念?
上一节我们聊了绝对位置编码。说白了,就是给每个词发一个固定的“座位号”。但有个问题——你想想看,模型真的关心“第5个词”这个绝对编号吗?
其实它更关心的是“这个词离我有多远”。比如在句子“我今天吃了苹果”里,“苹果”和“吃”的关系,跟它们之间的距离密切相关,跟它们是不是第3个、第5个词关系不大。
这就是相对位置编码的出发点。我个人习惯把绝对位置编码比作“门牌号”,而相对位置编码是“距离标尺”。
核心思想: 模型在计算注意力时,只关心两个token之间的相对距离,而不是它们在序列中的绝对位置。
从绝对到相对:一个简单的推导
我们先回顾一下绝对位置编码下的注意力分数计算。假设位置 i 的query向量是 qi,位置 j 的key向量是 kj,那么注意力分数就是:
score(i, j) = q_i^T * k_j
如果加上绝对位置编码,我们会把位置信息加到输入embedding里。但相对位置编码的做法不同——它直接在注意力分数计算中引入一个可学习的偏置项 bi-j:
score(i, j) = q_i^T * k_j + b_{i-j}
这里的 bi-j 只依赖于 i 和 j 的差值。也就是说,无论这两个词在句子开头还是结尾,只要它们相隔3个位置,就用同一个偏置值。
我的经验: 我在做文本分类项目时,发现绝对位置编码在长文本上表现不稳定。换成相对位置编码后,模型对句子中不同位置的语义关系捕捉得更准了。尤其是那些“头尾呼应”的句式,效果提升很明显。
T5模型中的相对位置偏置
Google的T5模型用了一种非常简洁的实现方式。它没有搞复杂的数学变换,而是直接维护一个可查找的偏置表。
具体来说,T5定义了一个最大相对距离 k(比如128)。对于任意两个位置 i 和 j,它们的相对距离 d = i - j 会被裁剪到 [-k, k] 范围内。然后模型从一个可训练的矩阵中查找对应的偏置值。
我画了一张图来展示这个过程:
你看,整个过程非常直观。T5的做法让我觉得特别清爽——没有复杂的三角函数,没有矩阵分解,就是一个简单的查表操作。
代码实现:手写一个T5风格的相对位置偏置
下面我给出一个简化版的实现。注意,这里我跳过了batch和head维度,只展示核心逻辑。
import torch
import torch.nn as nn
class T5RelativePositionBias(nn.Module):
def __init__(self, num_heads, max_distance=128):
super().__init__()
# 可训练的偏置表
# 距离范围是 [-max_distance, max_distance],共 2*max_distance+1 个值
self.bias = nn.Embedding(2 * max_distance + 1, num_heads)
self.max_distance = max_distance
def forward(self, seq_len):
# 生成所有位置对
positions = torch.arange(seq_len, device=self.bias.weight.device)
# 计算相对距离矩阵
# rel_dist[i][j] = i - j
rel_dist = positions.unsqueeze(1) - positions.unsqueeze(0)
# 裁剪到 [-max_distance, max_distance]
rel_dist = torch.clamp(rel_dist, -self.max_distance, self.max_distance)
# 偏移到非负索引
# 原始范围 [-max_distance, max_distance] -> [0, 2*max_distance]
rel_dist = rel_dist + self.max_distance
# 查表得到偏置
# 输出形状: [seq_len, seq_len, num_heads]
bias = self.bias(rel_dist)
return bias.permute(2, 0, 1) # [num_heads, seq_len, seq_len]
# 使用示例
bias_layer = T5RelativePositionBias(num_heads=8, max_distance=128)
seq_len = 10
bias = bias_layer(seq_len)
print(f"偏置形状: {bias.shape}") # [8, 10, 10]
我曾经踩过的坑: 一开始我把max_distance设得太小,比如32。结果序列长度超过32时,远处的token之间就分不清距离了。模型表现直接崩了。后来我改成128,效果才稳定下来。建议max_distance至少设为序列最大长度的1/2。
相对位置编码的优缺点
| 优点 | 缺点 |
|---|---|
| 对序列长度变化更鲁棒 | 实现比绝对位置编码复杂一点 |
| 能更好地捕捉局部依赖关系 | 需要额外存储偏置表参数 |
| 在长文本任务中通常表现更好 | 某些任务上可能不如学习到的绝对位置 |
| 可解释性更强(偏置值直接反映距离影响) | 对极端长序列仍需裁剪策略 |
实际项目中的选择建议
我个人在项目中是这样选择的:
- 短文本分类(< 128 tokens):绝对位置编码就够用了,简单直接
- 长文本理解(> 256 tokens):优先考虑相对位置编码,尤其是T5这种轻量实现
- 生成任务(如翻译、摘要):相对位置编码几乎是标配,因为生成序列长度不确定
一个小技巧: 如果你不确定选哪种,可以两个都试试。在验证集上对比一下loss曲线。我见过不少项目,换用相对位置编码后,收敛速度明显加快。
嗯,相对位置编码就聊到这里。它没有绝对位置编码那么“数学美”,但胜在实用。尤其是T5那种查表方式,简单粗暴但有效。下一节我们会聊另一个热门话题——旋转位置编码,那才是真正让我眼前一亮的东西。
公众号:蓝海资料掘金营,微信deep3321