4. 位置编码:绝对位置编码与相对位置编码,视觉中的位置信息处理

大家好,欢迎来到第四章。

前面我们聊了Transformer的核心架构,注意力机制怎么把全局信息一网打尽。但你有没有想过一个问题:自注意力本身是“无序”的。它把输入当成一个集合,而不是序列。你想想看,如果我把一张图片的像素打乱顺序,注意力算出来的结果居然是一样的——这显然不合理。

所以,位置编码就登场了。它的任务很简单:告诉模型“谁在哪儿”

4.1 为什么视觉任务需要位置编码?

在NLP里,词序决定语义。“我爱你”和“你爱我”完全是两码事。在视觉里,位置信息更是命根子。一张猫脸,眼睛在左上,鼻子在中间,嘴巴在右下——这个空间结构一旦乱了,猫就不是猫了。

我早期做目标检测的时候,试过把图像切块后直接丢进Transformer。结果模型训练半天,loss死活降不下去。后来一查,发现是位置编码没加。模型根本不知道每个patch原来在图像的哪个位置,它以为所有patch都是平等的。嗯,这教训挺深刻的。

核心结论:没有位置编码,Transformer就是一个“词袋模型”。在视觉任务中,位置编码是必须的,不是可选项。

4.2 绝对位置编码

绝对位置编码,说白了就是给每个位置分配一个独一无二的“身份证号”。

4.2.1 原理与公式

最经典的绝对位置编码来自《Attention Is All You Need》。它用正弦和余弦函数生成位置向量:

PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

其中pos是位置索引,i是维度索引。这个设计有个巧妙之处:不同频率的正余弦波组合在一起,每个位置都能得到唯一的编码。而且,模型可以通过线性变换学到相对位置关系。

在视觉任务中,我们通常把图像分成N×N个patch。每个patch有一个绝对位置索引。比如patch在(行=3, 列=5),那就对应一个位置编码向量。

4.2.2 视觉中的实现方式

ViT(Vision Transformer)的做法很简单:直接学一个可训练的位置编码矩阵。形状是(num_patches, d_model)。训练过程中,这个矩阵会自己调整,找到最适合当前任务的位置表示。

# 伪代码示例
import torch.nn as nn

num_patches = 196  # 14x14
d_model = 768

# 可学习的绝对位置编码
pos_embed = nn.Parameter(torch.randn(1, num_patches, d_model))

# 加到patch embedding上
x = patch_embed + pos_embed

我个人习惯用可学习的位置编码。为什么?因为视觉任务里,图像的空间结构比文本更复杂。正弦编码虽然理论优美,但可学习编码在实践中的表现往往更好。我在ViT的复现项目中对比过,可学习编码在ImageNet上高了0.3-0.5个点。

小技巧:初始化位置编码时,可以用截断正态分布,均值0,标准差0.02。别用太大,否则训练初期梯度容易爆炸。

4.3 相对位置编码

绝对位置编码有个问题:它只关心“我在哪”,不关心“你离我多远”。但在视觉里,两个patch之间的相对距离往往更重要。比如,一个像素和它相邻的像素关系更紧密,和远处的像素关系更弱。

相对位置编码就是来解决这个问题的。

4.3.1 核心思想

相对位置编码不直接给每个位置一个向量,而是在注意力计算时,根据两个位置的偏移量,动态调整注意力分数

公式上,标准的注意力是:

Attention(Q, K, V) = softmax(QK^T / sqrt(d)) V

加入相对位置编码后变成:

Attention(Q, K, V) = softmax((QK^T + R) / sqrt(d)) V

其中R是一个偏置矩阵,R[i][j]表示位置i和位置j之间的相对位置关系。这个偏置可以是可学习的,也可以是根据距离计算的。

4.3.2 视觉中的常见做法

在Swin Transformer中,用了相对位置偏置(Relative Position Bias)。具体做法是:

  • 定义一个相对位置索引表,范围是[-window_size+1, window_size-1]
  • 把这个索引映射到一个可学习的偏置向量上
  • 在注意力计算时,直接加上这个偏置
# Swin Transformer中的相对位置偏置
# 假设window_size=7,相对位置范围是[-6, 6]
# 偏置表形状: (13, 13, num_heads)

# 计算相对位置索引
coords_h = torch.arange(window_size)
coords_w = torch.arange(window_size)
coords = torch.stack(torch.meshgrid([coords_h, coords_w]))
coords_flatten = torch.flatten(coords, 1)
relative_coords = coords_flatten[:, :, None] - coords_flatten[:, None, :]
relative_coords = relative_coords.permute(1, 2, 0).contiguous()
relative_coords[:, :, 0] += window_size - 1
relative_coords[:, :, 1] += window_size - 1
relative_coords[:, :, 0] *= 2 * window_size - 1

# 查表得到偏置
relative_position_bias_table = nn.Parameter(
    torch.zeros((2 * window_size - 1) * (2 * window_size - 1), num_heads))
relative_position_index = relative_coords.sum(-1)
relative_position_bias = relative_position_bias_table[relative_position_index]

我曾经在Swin Transformer的复现中踩过一个坑:相对位置索引的偏移量算错了。本来范围是[-6,6],我直接用了0到12的索引,结果注意力全乱套了。后来debug了一整天才发现,原来需要先偏移到非负范围,再映射到偏置表。这个细节,大家一定要注意。

避坑指南:相对位置索引的计算一定要小心。偏移量的顺序、乘法的系数,一个不对,模型就学不动。建议先在小数据集上验证一下注意力分布是否合理。

4.4 绝对 vs 相对:怎么选?

这个问题没有标准答案。我根据自己的项目经验,给一些参考:

场景 推荐方案 原因
图像分类(ViT) 绝对位置编码(可学习) 简单有效,分类任务对位置精度要求不高
目标检测(DETR) 绝对+相对混合 检测需要精确的空间关系,混合编码效果更好
语义分割 相对位置编码 分割任务对局部一致性要求高,相对编码更合适
视频理解 3D相对位置编码 时空维度都需要考虑相对关系

我个人建议:先从绝对位置编码开始。它实现简单,调试方便。如果发现模型对空间关系不够敏感,再换成相对位置编码或者混合方案。别一上来就搞复杂的,容易把自己绕进去。

4.5 视觉中的位置信息处理进阶

除了上面两种主流方法,还有一些有意思的变体:

4.5.1 2D位置编码

既然图像是2D的,那位置编码也应该考虑两个维度。一种做法是:分别对行和列做位置编码,然后拼接或相加

# 2D位置编码示例
row_embed = nn.Parameter(torch.randn(1, num_rows, d_model//2))
col_embed = nn.Parameter(torch.randn(1, num_cols, d_model//2))

# 扩展并拼接
row_embed = row_embed.unsqueeze(2).expand(-1, -1, num_cols, -1)
col_embed = col_embed.unsqueeze(1).expand(-1, num_rows, -1, -1)
pos_embed = torch.cat([row_embed, col_embed], dim=-1)
pos_embed = pos_embed.view(1, num_patches, d_model)

这种做法的好处是:模型能分别学到行和列的空间结构。我在做遥感图像分类时用过这个方案,效果比一维位置编码好不少。因为遥感图像里,行和列的方向性很重要(比如建筑物朝向)。

4.5.2 条件位置编码

还有一种思路:位置编码不是固定的,而是根据输入内容动态生成。比如CPE(Conditional Position Encoding)用卷积网络根据图像内容生成位置编码。

这样做的好处是:位置编码能适应不同尺度的物体。大物体需要更大的感受野,小物体需要更精细的位置信息。条件位置编码可以自动调整。

不过,这种方法的计算量比较大。我一般只在资源充足、对精度要求极高的项目里用。

4.6 总结与思考

位置编码是Transformer在视觉领域落地的关键一环。没有它,模型就是个“睁眼瞎”。

  • 绝对位置编码:简单直接,适合大多数任务。可学习版本是首选。
  • 相对位置编码:更符合视觉的局部性原理,在检测和分割任务中优势明显。
  • 2D编码:充分利用图像的空间结构,适合有方向性的任务。
  • 条件编码:灵活但计算量大,适合对精度要求极高的场景。

最后,我想说一句:别把位置编码当成一个“加进去就完事”的组件。它和你的任务、数据、模型结构都密切相关。多试试不同的方案,看看注意力图的变化,你会有很多新发现。

下一章,我们会聊聊Transformer在视觉中的经典架构——ViT和它的变体。到时候,位置编码还会再次出现,我们会看到它在不同架构中的具体用法。