解密BERT中的位置编码:为什么随机初始化效果不输三角函数?
在自然语言处理领域,Transformer架构及其衍生模型(如BERT)彻底改变了我们对序列建模的认知。其中,一个看似微小却至关重要的设计选择——位置编码(Positional Encoding)——常常成为技术讨论的焦点。经典的Transformer论文采用了由正弦和余弦函数精心构造的确定性位置编码,而到了BERT时代,开发者却选择了看似“简单粗暴”的随机初始化位置向量,并让模型在训练中自行学习。这不禁让人好奇:为什么这种看似“不优雅”的随机初始化方法,在BERT这类大规模预训练模型中,其表现能与精心设计的三角函数编码分庭抗礼,甚至在某些场景下更胜一筹?
对于从事NLP研发的工程师和研究者而言,理解这个选择背后的逻辑,不仅仅是掌握一个技术细节,更是洞察现代深度学习模型设计哲学的一扇窗口。它触及了模型容量、数据规模、优化动态以及归纳偏置等核心议题。本文将深入剖析位置编码的本质,结合数学原理与工程实践,为你揭示随机初始化位置编码在BERT中表现卓越的深层原因,并提供直观的可视化对比实验,助你彻底掌握这一关键设计。
1. 位置编码:从绝对定位到相对关系
在深入对比两种编码方式之前,我们首先要理解位置编码究竟要解决什么问题。循环神经网络(RNN)和长短期记忆网络(LSTM)等模型天然具备处理序列顺序的能力,因为它们的计算是逐步进行的。然而,Transformer架构的核心——自注意力机制(Self-Attention)——本身是置换不变的。这意味着,如果将输入序列中任意两个词的位置互换,自注意力层输出的词向量集合在忽略位置信息的情况下是完全相同的。模型无法区分“猫追老鼠”和“老鼠追猫”在语序上的根本差异。
因此,我们必须向模型注入位置信息。位置编码的本质,是为序列中的每个位置分配一个独特的、可学习的(或预设的)向量表示,并将其与词嵌入向量相加,从而使最终的输入表征同时包含词汇语义和位置信息。
1.1 三角函数位置编码:优雅的数学设计
原始Transformer论文提出的位置编码(PE)是一个充满数学美感的确定性函数。对于位置 pos 和维度 i,编码计算如下:
import numpy as np
def get_positional_encoding(pos, d_model):
"""
计算位置pos在d_model维空间中的三角函数编码。
"""
PE = np.zeros(d_model)
for i in range(0, d_model, 2):
PE[i] = np.sin(pos / (10000 ** (2 * i / d_model)))
if i + 1 < d_model:
PE[i + 1] = np.cos(pos / (10000 ** (2 * i / d_model)))
return PE
这种设计有几个精妙之处:
- 唯一性:每个位置都有独一无二的编码。
- 相对位置关系:对于固定的偏移量
k,位置pos + k的编码可以表示为位置pos编码的线性函数。这使得模型能够轻松学习到相对位置关系。 - 值域有界:正弦余弦函数的值域在[-1, 1]之间,与经过归一化处理的词嵌入向量尺度匹配。
从数学归纳偏置的角度看,三角函数编码为模型预先注入了“位置相近的token应具有更相似的编码”以及“相对位置可计算”的先验知识。这在数据量有限时尤为重要,能帮助模型更快地收敛。
1.2 随机初始化位置编码:将问题交给数据
与Transformer的预设方案不同,BERT及其许多后续变体(如RoBERTa、ALBERT)采用了一种更简单直接的方法:随机初始化一个位置嵌入矩阵。
import torch
import torch.nn as nn
class LearnedPositionalEncoding(nn.Module):
def __init__(self, max_seq_len, d_model):
super().__init__()
# 创建一个可学习的位置嵌入矩阵,形状为 (max_seq_len, d_model)
self.position_embeddings = nn.Embedding(max_seq_len, d_model)
def forward(self, token_embeddings):
# token_embeddings形状: (batch_size, seq_len, d_model)
batch_size, seq_len, d_model = token_embeddings.size()
# 生成位置ID [0, 1, 2, ..., seq_len-1]
position_ids = torch.arange(seq_len, dtype=torch.long, device=token_embeddings.device)
position_ids = position_ids.unsqueeze(0).expand(batch_size, -1) # 扩展为batch维度
# 获取位置嵌入并相加
pos_embeddings = self.position_embeddings(position_ids)
return token_embeddings + pos_embeddings
这种方法没有任何关于位置关系的硬编码假设。初始时,位置5和位置6的嵌入向量之间可能没有任何系统性关联。模型需要从海量的训练数据中,自行发现并学习位置之间的规律和关系。
提示:这里的“随机初始化”通常指从某个分布(如均值为0、标准差较小的正态分布)中采样,并非完全无序。其初始状态虽然不包含预设的位置规律,但为优化过程提供了一个起点。
2. 随机编码何以成功?数据规模与模型容量的双重胜利
随机初始化位置编码的成功,并非偶然。它建立在两个现代深度学习的关键基石之上:超大规模的训练数据和超高容量的模型参数。
2.1 数据规模:从“需要引导”到“自行发现”
在深度学习领域,一个核心观察是:当数据量足够大时,数据本身所蕴含的规律足以指导模型学习到有效的表示,有时甚至优于人为设计的强归纳偏置。
- 小数据场景:在数据有限时,一个强先验(如三角函数编码)至关重要。它能防止模型过拟合到噪声,并引导其快速学习到位置的基本规律(如邻近性、顺序性)。没有这种引导,模型可能难以从少量样本中概括出有效的位置表示。
- 大数据场景:BERT的预训练使用了数十亿甚至上万亿的词汇(如BookCorpus和英文维基百科)。在如此庞大的语料中,“位置”与“语言结构”之间的关联模式反复出现。例如:
- 句首的单词更可能是主语或状语。
- 介词后面通常跟着名词或名词短语。
- 动词与其宾语通常处于特定的相对位置关系。
模型通过海量的“[MASK]”预测和“下一句预测”任务,被迫去建模词语之间的依赖关系,而这种依赖关系天然就包含了位置信息。模型中的自注意力机制和全连接层有足够的机会和样本,去调整那些随机初始化的位置向量,让它们编码出对当前任务最有用的位置特征——这些特征可能不仅仅是简单的相对距离,还包括更复杂的、与句法语义交织的位置模式。
下表对比了两种编码方式在不同数据规模下的理论表现:
| 特性维度 | 三角函数位置编码 | 随机初始化位置编码 |
|---|---|---|
| 设计哲学 | 强归纳偏置,预设相对位置关系 | 弱归纳偏置,数据驱动学习 |
| 小数据适应性 | 优。能快速收敛,避免过拟合。 | 差。容易学习到噪声或无法形成有效表示。 |
| 大数据适应性 | 良。表现稳定,但可能限制模型学习更复杂位置模式的能力。 | 优。有潜力学习到数据中更丰富、更任务相关的位序特征。 |
| 外推性 | 理论上强。可计算任意长度的位置编码。 | 受限。通常只能处理预定义最大长度内的位置,对更长序列需要特殊处理。 |
| 计算与存储 | 无需存储,动态计算,无额外参数。 | 需要存储一个位置嵌入矩阵,增加模型参数量。 |
2.2 模型容量:参数化学习的强大威力
BERT-base拥有1.1亿参数,BERT-large更是达到3.4亿参数。其中,位置嵌入矩阵的参数量相对较小(例如,最大长度512,隐藏维度768,则位置参数约为512*768 ≈ 40万),但这部分参数并非孤立存在。它们与词嵌入、注意力权重、前馈网络权重共同构成了一个极其强大的函数逼近器。
- 联合优化:在训练过程中,位置嵌入、词嵌入以及所有Transformer层的参数是同时被优化的。梯度回传会调整位置向量,使其与词义、上下文信息协同工作,共同解决预训练任务。这种端到端的联合学习能力,使得模型可以找到一种“内洽”的位置表示,这种表示可能比通用的三角函数形式更适配于语言建模任务。
- 超越绝对与相对:学习到的位置嵌入,可能编码了比“第几个词”更复杂的信息。它可能学会了区分“句子开头”、“句子中间”、“句子结尾”,或者区分“从句内部位置”和“主句位置”。这些是预设的三角函数编码难以直接表达的。
注意:随机初始化位置编码的一个潜在缺点是长度外推能力弱。如果模型在预训练时只见过最长512个token的序列,那么它学到的位置嵌入也只覆盖了前512个位置。当推理时遇到更长的序列,如何处理超出部分的位置信息就成了一个挑战。而三角函数编码理论上可以为任意长度的位置生成编码。
3. 可视化实验:对比两种编码的实际表现
理论分析需要实验佐证。我们可以设计一个简单的可视化实验,来观察在模拟的预训练任务下,两种位置编码学习到的位置向量有何不同。
3.1 实验设置
我们构建一个微型BERT模型,仅包含一层Transformer编码器,隐藏维度为64。在一个人工构造的小型文本数据集上,进行掩码语言模型(MLM)的预训练。我们训练两个模型,唯一区别是位置编码方式:一个使用固定的三角函数编码(sin/cos),另一个使用随机初始化并学习的位置嵌入(learned)。
训练完成后,我们提取两个模型中第一个位置(pos=0)和第十个位置(pos=9)的位置向量(对于学习式编码,就是嵌入矩阵的第一行和第十行;对于三角函数编码,就是计算出的对应向量)。
3.2 分析与可视化
我们可以通过降维(如PCA或t-SNE)将这些高维向量投影到二维平面进行观察,或者直接计算位置向量之间的余弦相似度。
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE
import numpy as np
# 假设我们已经训练好两个模型,并提取了所有位置的位置向量
# pos_vectors_sin_cos: 形状 (seq_len, d_model),三角函数编码
# pos_vectors_learned: 形状 (seq_len, d_model),学习到的编码
# 计算位置向量间的余弦相似度矩阵
def cosine_similarity_matrix(vectors):
norm = np.linalg.norm(vectors, axis=1, keepdims=True)
normalized = vectors / norm
sim_matrix = np.dot(normalized, normalized.T)
return sim_matrix
sim_sin = cosine_similarity_matrix(pos_vectors_sin_cos)
sim_learned = cosine_similarity_matrix(pos_vectors_learned)
# 绘制热力图
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
im0 = axes[0].imshow(sim_sin[:20, :20], cmap='RdBu_r', vmin=-1, vmax=1)
axes[0].set_title('Sin/Cos Position Encoding Similarity')
axes[0].set_xlabel('Position j')
axes[0].set_ylabel('Position i')
plt.colorbar(im0, ax=axes[0])
im1 = axes[1].imshow(sim_learned[:20, :20], cmap='RdBu_r', vmin=-1, vmax=1)
axes[1].set_title('Learned Position Embedding Similarity')
axes[1].set_xlabel('Position j')
plt.colorbar(im1, ax=axes[1])
plt.tight_layout()
plt.show()
预期结果分析:
- 三角函数编码:其相似度热力图会呈现出清晰的带状结构。位置i和位置j的相似度主要取决于它们的绝对距离
|i-j|,并且由于正弦函数的周期性,可能还会呈现出一定的周期模式。这是一种非常规则、平滑的相似度分布。 - 学习到的编码:其相似度热力图可能看起来更“杂乱”,但仔细看会发现一些块状或局部聚集的模式。例如,前几个位置(可能是句首特殊标记如
[CLS]和开头词语)彼此之间可能更相似;中间某些位置(可能对应常见的短语结构)也可能形成聚类。它反映的不再是简单的几何距离,而是模型从数据中学到的、与语言功能相关的“位置类别”。
这个实验直观地表明,学习到的位置嵌入捕获的是一种任务导向的、与语义结合的位置表示,而非纯粹的、数学意义上的序列索引。
4. 工程实践启示与选择策略
理解了原理,我们在实际项目中应如何选择或设计位置编码呢?以下是一些实用的考量点。
4.1 何时选择随机初始化学习式编码?
- 大规模预训练:这是学习式编码的主场。当你拥有海量数据(如互联网级文本)和足够大的模型时,放心使用它,让数据说话。
- 领域特定任务:如果你在一个特定领域(如医学文献、法律条文)进行从头预训练,且该领域的文本具有独特的位置结构(如法律条款的编号体系、论文的章节结构),学习式编码可能比通用的三角函数编码更能适应这些特性。
- 计算资源允许:学习式编码引入了额外的参数。虽然参数量不大,但在边缘设备部署时仍需考虑。对于参数量极度敏感的场景,无参数的三角函数编码更有优势。
4.2 何时考虑使用或改进三角函数编码?
- 资源受限或数据量小:在小规模数据集上微调或训练模型时,使用预设的三角函数编码作为强先验,通常能带来更稳定、更快的收敛。
- 需要处理长序列或可变长度序列:如果你的任务经常需要处理远超训练时所见长度的序列(如长文档摘要、基因序列分析),三角函数编码因其外推性而成为更安全的选择。近年来,也出现了许多改进的相对位置编码(如T5的Relative Position Bias、RoPE等),它们在长序列任务上表现优异。
- 模型可解释性要求高:如果你需要分析模型如何利用位置信息,三角函数编码的确定性使其更容易被分析和理解。
4.3 一种混合策略:从“预设”到“学习”
在实践中,还有一种两全其美的思路:使用三角函数编码进行初始化,但让其参与训练,变为可学习的参数。这相当于给了模型一个良好的起点(具备基本的相对位置感知),同时又保留了根据具体任务和数据调整位置表示的灵活性。许多现代模型变体采用了这种策略。
class SinCosLearnablePositionalEncoding(nn.Module):
"""使用三角函数初始化,但可学习的位置编码"""
def __init__(self, max_seq_len, d_model):
super().__init__()
# 先计算三角函数编码作为初始值
position = torch.arange(max_seq_len).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model))
pe = torch.zeros(max_seq_len, d_model)
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
# 将其注册为可学习参数
self.position_embeddings = nn.Parameter(pe, requires_grad=True)
def forward(self, x):
return x + self.position_embeddings[:x.size(1), :]
这种方法的优点是兼具了两种世界的优势:拥有三角函数编码的稳定性和外推性起点,又具备学习式编码的适应能力。在不少实际项目中,我发现在中等规模数据上,这种初始化方式比纯随机初始化收敛得更快,最终效果也略有提升。
位置编码虽只是Transformer-BERT庞大体系中的一个组件,但其设计选择深刻反映了深度学习从“精心设计特征”到“用数据和算力学习特征”的范式转变。随机初始化位置编码在BERT中的成功,不是一个取巧的偶然,而是大数据与大模型时代背景下,对“数据中蕴含的知识远超过人类先验设计”这一事实的生动印证。作为实践者,我们的任务不再是寻找那个“唯一正确”的编码方式,而是根据任务、数据、资源的约束,做出最合适的技术选型,并理解其背后的权衡。下次当你配置模型参数时,面对那个 position_embedding_type 选项,希望你能更加自信地做出选择。
1595




被折叠的 条评论
为什么被折叠?



