3、可学习位置编码(Learned):BERT等模型使用的可学习位置编码原理、训练方式与局限性

聊完绝对位置编码,咱们来看看另一种更“偷懒”的做法——可学习位置编码。

说白了,就是我不给你设计什么正弦余弦公式了。我直接让模型自己去学一套位置向量。你想想看,这多省事?

BERT 用的就是这套方案。我记得当年第一次看到 BERT 论文时,心里还嘀咕:这玩意儿真能学出位置信息来?后来自己动手跑了一遍,嗯,还真行。

3.1 核心原理:把位置当成参数

可学习位置编码的思路非常直接:

  • 假设最大序列长度是 max_len,比如 512
  • 每个位置对应一个 d 维的向量
  • 那我们就搞一个形状为 [max_len, d] 的矩阵
  • 这个矩阵里的每个值,都是可训练的参数

用代码表示就是:

import torch.nn as nn

max_len = 512
d_model = 768

# 这就是可学习位置编码
self.position_embeddings = nn.Embedding(max_len, d_model)

# 使用时,生成位置索引
position_ids = torch.arange(max_len).unsqueeze(0)  # [1, 512]
position_embeds = self.position_embeddings(position_ids)  # [1, 512, 768]

然后呢?直接把词嵌入和位置嵌入加起来,送进 Transformer 里。就这么简单。

关键点:可学习位置编码本质上就是一个 Embedding 层。它和词嵌入的区别在于——词嵌入的索引是词在词表中的 ID,而位置嵌入的索引是词在句子中的位置序号。

3.2 训练方式:端到端学习

训练过程没什么特别的。就是正常的反向传播。

位置编码矩阵的梯度会随着训练不断更新。每个位置最终学到什么样子,完全取决于数据。

我做过一个实验:把训练好的 BERT 位置编码可视化出来。你会发现:

  • 相邻位置的向量比较相似
  • 距离越远,向量差异越大
  • 有些维度对位置敏感,有些维度不敏感

说白了,模型自己学会了“距离越近越相关”这个直觉。

我的经验:如果你自己训练一个模型,建议把位置编码的初始化范围设小一点,比如 N(0, 0.02)。我一开始用大了,结果训练初期位置编码的噪声把词嵌入的信息都淹没了,收敛特别慢。

3.3 局限性:三个绕不开的坑

可学习位置编码虽然简单好用,但坑也不少。我一个个说。

3.3.1 无法处理超长序列

这是最要命的限制。

你训练时设了 max_len=512,那模型就只能处理 512 个 token。来了个 600 的序列怎么办?

有人会说:那我设大一点不就行了?比如设成 2048。

问题是:

  • 参数数量暴增:2048 × 768 = 157 万个参数
  • 大部分位置在训练时根本没见过几次
  • 没见过的位置,学出来就是随机噪声

我曾经试过把 BERT 的 max_len 从 512 扩到 1024,然后做微调。结果 512 之后的位置编码基本没怎么更新,效果很差。

避坑指南:如果你需要处理长文本,别用可学习位置编码。要么用 ALBERT 那种参数共享的方案,要么直接上 RoPE。

3.3.2 缺乏外推能力

外推能力,说白了就是:训练时没见过 600 的位置,推理时能不能处理好?

可学习位置编码的回答是:不能。

为什么?因为每个位置的参数是独立的。位置 512 和位置 600 之间没有任何约束关系。模型没见过位置 600,那它的嵌入就是随机初始化的值。

你想想看,这多尴尬?

相比之下,正弦编码和 RoPE 都有明确的数学公式,可以计算出任意位置的编码。这就是外推能力。

3.3.3 参数冗余

这个点可能很多人没注意到。

可学习位置编码的参数量是 max_len × d。对于 BERT-base 来说,就是 512 × 768 ≈ 40 万参数。

但你真的需要这么多参数吗?

我做过一个消融实验:把位置编码的维度从 768 压缩到 128,然后投影回 768。效果几乎没有下降。这说明什么?说明位置编码的信息冗余度很高。

说白了,你用 40 万个参数去学一个本来可以用几十个参数表达的东西。浪费。

3.4 知识体系总览

下面这张图帮你理清可学习位置编码的核心逻辑:

可学习位置编码知识体系 可学习位置编码 原理:Embedding 层 训练:端到端反向传播 局限性:三大问题 形状:[max_len, d] 与词嵌入相加 无法处理超长序列 缺乏外推能力 参数冗余 代表模型:BERT、GPT-2、ALBERT

3.5 什么时候该用它?

说了这么多缺点,那可学习位置编码是不是一无是处?

也不是。它有两个明显的优点:

  • 实现简单:一行代码搞定,不用设计复杂的公式
  • 灵活性强:模型可以根据数据自适应地学习位置关系

我个人建议的使用场景:

  • 任务的最大序列长度固定且较短(比如 128 或 256)
  • 你不想折腾复杂的编码方案
  • 做 baseline 实验,快速验证想法

但如果你要做长文本、大模型、或者需要外推能力,那还是老老实实用 RoPE 吧。

一句话总结:可学习位置编码是“用参数换简单”,适合短序列、快速实验。但它的天花板很低,长序列场景下基本没法用。


公众号:蓝海资料掘金营,微信deep3321