Transformer位置编码原理与实现:从正弦编码到RoPE
1. 项目概述:位置编码——让模型“看见”序列的秩序
在深度学习的序列建模领域,无论是处理自然语言、音频还是时间序列数据,模型本身通常是“无序”的。一个经典的Transformer模型,其自注意力机制(Self-Attention)在处理一个句子时,对于“我爱北京”和“北京爱我”这两个词序完全不同的输入,如果不做任何处理,它会计算出几乎相同的注意力权重,因为它只关心词与词之间的语义关联,而忽略了它们在序列中的绝对位置和相对顺序。这显然不符合我们的认知。位置编码(Positional Encoding, PE)就是为了解决这个问题而诞生的核心组件,它像给序列中的每个元素贴上一个“坐标标签”,告诉模型:“这个词在第几个位置”。
这个项目标题“07-位置编码 📍”暗示了这是一个系列教程或笔记中的第七部分,聚焦于位置编码这一关键技术。📍这个表情符号直观地表达了“定位”的概念。从相关热词来看,它紧密关联着Transformer架构、PyTorch实现、正弦编码、可学习编码以及RoPE、ViT位置编码等前沿变体。理解位置编码,不仅是理解Transformer的基石,也是掌握当下众多基于Transformer的视觉(ViT)、语音乃至多模态大模型的关键。
本文将从一个实践者的角度,深入拆解位置编码的为什么、是什么和怎么做。我会结合PyTorch代码,带你从零实现经典的正弦位置编码,探讨可学习位置编码的优劣,并分析像RoPE(旋转位置编码)这样的现代方案为何能成为大语言模型的宠儿。无论你是刚接触Transformer的新手,还是希望深化对模型细节理解的中级开发者,这篇文章都将提供可直接复现的代码和背后深刻的原理剖析。
2. 位置编码的核心原理与设计思路
2.1 自注意力机制的“位置盲”问题
要理解位置编码的必要性,必须回到自注意力机制本身。自注意力通过计算查询(Query)、键(Key)、值(Value)向量之间的相似度,来聚合全局信息。其计算过程本质上是置换等变(Permutation Equivariant)的。简单来说,如果你把输入序列的顺序打乱,输出的序列顺序也会相应打乱,但每个输出位置所聚合的信息内容(不考虑位置)是相似的。
用一个简单的例子说明:假设我们有一个包含词嵌入的序列X = [x1, x2, x3]。自注意力层计算输出Z = Attention(Q, K, V),其中Q=K=V=XW(W是可学习的权重矩阵)。由于点积注意力softmax((QK^T)/√d_k)V的计算只依赖于向量间的点积,而点积运算与向量的绝对位置无关。因此,对于输入X‘ = [x2, x1, x3](交换了x1和x2),其输出Z‘将会是Z的相应行被交换后的结果。模型无法区分“猫追老鼠”和“老鼠追猫”。
2.2 位置编码的注入方式
为了解决这个问题,我们需要将位置信息显式地注入到模型中。主流的方法是将位置编码向量与词嵌入向量进行相加。
设输入序列长度为L,词嵌入维度为d_model。词嵌入矩阵为E ∈ R^(L×d_model),位置编码矩阵为P ∈ R^(L×d_model)。那么Transformer的输入就是:X = E + P
这个简单的加法操作是经过精心设计的。它假设位置信息和语义信息存在于同一个向量空间的不同“子空间”或通道中,模型可以通过后续的线性变换和注意力机制学习到如何同时利用这两种信息。
注意:为什么是相加而不是拼接?相加保持了输入维度不变(仍是
d_model),避免了参数量的显著增加,同时实践表明模型能够有效学习到这种混合表示。拼接虽然信息分离更彻底,但会改变输入维度,需要调整后续所有层的权重维度,不够优雅且效率未必更高。
2.3 绝对位置编码 vs. 相对位置编码
根据编码方式所蕴含的信息,位置编码可以分为两大类:
绝对位置编码(Absolute Positional Encoding):为序列中的每个绝对位置(如第1个词、第2个词)分配一个独特的编码向量。经典的正弦/余弦编码和可学习位置编码都属于此类。它直接告诉模型“这是第几个位置”。
相对位置编码(Relative Positional Encoding):不关心绝对位置,而是编码序列中任意两个元素之间的相对距离或相对位置关系。例如,它编码“当前词”和“前一个词”、“后两个词”之间的关系。RoPE(旋转位置编码)和Transformer-XL中使用的编码是这类方法的杰出代表。它更符合语言的内在规律,因为我们理解一个词的意义,往往更依赖于它与其他词的相对关系,而非它在句子中的绝对序号。
近年来,相对位置编码因其更好的长度外推性(处理比训练时更长的序列)和理论上的优越性,在大型语言模型中逐渐成为主流。
3. 经典位置编码方案详解与PyTorch实现
3.1 正弦/余弦位置编码(Sinusoidal Positional Encoding)
这是原版Transformer论文《Attention Is All You Need》提出的方法,也是最具标志性的位置编码。它并非可学习参数,而是一个基于正弦和余弦函数的确定性公式。
公式解析:
对于位置pos(从0开始计数)和维度索引i(i=0,1,...,d_model-1),位置编码向量P(pos, 2i)和P(pos, 2i+1)的计算公式如下:P(pos, 2i) = sin(pos / 10000^(2i / d_model))P(pos, 2i+1) = cos(pos / 10000^(2i / d_model))
为什么设计成这样?
- 唯一性与连续性:每个位置都有唯一的编码。同时,由于正弦函数的性质,相邻位置的编码是平滑变化的,模型可以更容易地学习到位置之间的邻近关系。
- 可扩展性(长度外推):对于训练时未见过的更长序列(
pos很大),由于公式是定义好的,我们可以直接计算其位置编码,而不需要重新训练。尽管外推效果可能下降,但至少是可行的。 - 相对位置的可表达性:一个关键的性质是,对于一个固定的偏移量
k,P(pos+k)可以表示为P(pos)的线性函数。这意味着模型可能仅通过注意力机制中的线性变换,就能学会关注相对位置信息。这是其设计精妙之处。
PyTorch实现:
import torch import torch.nn as nn import math class SinusoidalPositionalEncoding(nn.Module): def __init__(self, d_model: int, max_len: int = 5000): super().__init__() # 创建一个形状为 (max_len, d_model) 的零矩阵来存储位置编码 pe = torch.zeros(max_len, d_model) # 生成位置索引 (max_len, 1) position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) # 计算分母项:10000^(2i/d_model),使用对数空间计算避免数值过大 div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) # 对偶数维度应用正弦函数 pe[:, 0::2] = torch.sin(position * div_term) # 对奇数维度应用余弦函数 pe[:, 1::2] = torch.cos(position * div_term) # 增加一个批次维度,最终形状为 (1, max_len, d_model),便于广播相加 pe = pe.unsqueeze(0) # 将其注册为缓冲区(buffer),而不是可训练参数(parameter) # 这意味着它会被保存和加载,但不会被优化器更新 self.register_buffer('pe', pe) def forward(self, x: torch.Tensor) -> torch.Tensor: """ Args: x: Tensor, shape [batch_size, seq_len, embedding_dim] Returns: Tensor: 添加了位置编码的输入,形状不变 """ # 将位置编码加到输入张量上。pe[:, :x.size(1)] 是为了适配可变序列长度 x = x + self.pe[:, :x.size(1)] return x # 使用示例 d_model = 512 seq_len = 100 batch_size = 4 embedding = torch.randn(batch_size, seq_len, d_model) # 模拟词嵌入 pos_encoder = SinusoidalPositionalEncoding(d_model) output = pos_encoder(embedding) print(f"输入形状: {embedding.shape}") print(f"输出形状: {output.shape}")实操心得:
register_buffer是关键。这确保了位置编码矩阵pe会随着模型一起被保存(state_dict)和加载,但不会被梯度更新。如果你错误地将其定义为nn.Parameter,优化器会尝试更新它,这违背了正弦编码“固定不变”的设计初衷。- 在实际的Transformer模型中,位置编码通常加在嵌入层之后,进入编码器堆叠之前。
- 对于非常长的序列(接近或超过
max_len),虽然可以计算,但高频维度(i较大的维度)的波长会非常长,可能导致位置信息区分度下降。这是所有绝对位置编码面临的共同挑战。
3.2 可学习位置编码(Learnable Positional Encoding)
这是一种更简单直观的方法:将位置编码直接视为可训练的模型参数。
实现方式:创建一个形状为(max_len, d_model)的nn.Embedding层或nn.Parameter。在 forward 过程中,根据输入序列的长度,取出对应位置的可学习向量,加到词嵌入上。
PyTorch实现:
class LearnablePositionalEncoding(nn.Module): def __init__(self, d_model: int, max_len: int = 5000): super().__init__() # 定义一个可学习的位置嵌入层 self.pe = nn.Parameter(torch.zeros(1, max_len, d_model)) # 通常使用较小的标准差进行初始化,如0.02或0.01 nn.init.normal_(self.pe, mean=0.0, std=0.02) def forward(self, x: torch.Tensor) -> torch.Tensor: x = x + self.pe[:, :x.size(1)] return x优点与缺点分析:
| 特性 | 可学习位置编码 | 正弦位置编码 |
|---|---|---|
| 灵活性 | 高。模型可以从数据中学习最适合任务的位置表示。 | 低。形式固定,无法根据数据调整。 |
| 长度外推 | 差。只能处理训练时见过的位置(≤max_len)。对于更长的序列,没有对应的学习过的向量。 | 较好。可通过公式计算任意位置,但外推性能会衰减。 |
| 训练稳定性 | 可能需要更仔细的初始化和调优。 | 非常稳定,无需担心初始化。 |
| 常见应用场景 | 在训练和推理序列长度固定或变化不大的任务中表现良好,如早期的BERT、一些机器翻译模型。 | 需要处理可变长度或希望有理论保障外推能力的场景,原版Transformer。 |
注意事项:
- 初始化很重要:可学习位置编码的初始化会影响训练收敛。通常使用较小的随机初始化(如正态分布
N(0, 0.02)),避免初始值过大淹没词嵌入信号。 - 过拟合风险:在数据量较小的任务上,可学习参数可能无法充分学习到有效的位置模式,反而容易过拟合。
max_len的选择:这是一个超参数。设置过小会限制模型处理长序列的能力;设置过大会增加不必要的参数,并可能使模型难以学习到远处位置的有效表示(因为那些位置在训练数据中很少出现)。
4. 进阶位置编码方案:RoPE与相对位置编码
4.1 旋转位置编码(RoPE)原理浅析
RoPE是近年来在大型语言模型(如LLaMA、GPT-NeoX)中广泛使用的相对位置编码方法。它的核心思想非常巧妙:通过旋转矩阵将绝对位置信息注入到注意力分数的计算中,从而间接地实现相对位置编码的效果。
直观理解:想象一下,我们把词嵌入向量看作高维空间中的点。RoPE为每个位置分配一个特定的“旋转角度”。在计算注意力得分(Query和Key的点积)时,先将Query向量和Key向量根据它们各自的位置进行旋转,然后再做点积。神奇的是,旋转后的点积结果只依赖于两个向量的原始内容以及它们之间的相对位置差,而与它们的绝对位置无关。
数学表达(简化):对于位置m的Query向量q_m和位置n的Key向量k_n,RoPE通过一个复数旋转操作(在代码中通常用实数矩阵实现)将它们转换为q_m’和k_n’,使得注意力分数满足:<q_m‘, k_n’> = g(q_m, k_n, m-n)这里<,>表示点积,g是一个只依赖于原始向量和相对位置m-n的函数。这就实现了将相对位置信息编码到注意力机制中。
为什么RoPE如此受欢迎?
- 相对性:直接建模了相对位置关系,更符合语言建模的直觉。
- 长度外推性优秀:由于其数学形式,RoPE在处理远长于训练序列的文本时,性能下降相对平滑,外推能力显著优于绝对位置编码。
- 兼容自注意力:实现上非常优雅,只需在计算Q和K之后、计算注意力分数之前,对Q和K应用旋转变换即可,不改变模型的其他部分。
4.2 RoPE的PyTorch核心实现
RoPE的实现涉及一些线性代数操作。以下是其核心部分的一个简化示例,帮助理解其流程:
import torch import torch.nn as nn import torch.nn.functional as F import math def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0): """ 预计算复数旋转因子(cis = cos + i*sin)。 Args: dim: 词嵌入维度,必须是偶数。 end: 最大序列长度。 theta: 用于控制波长的基础值。 Returns: freqs_cis: 复数张量,形状 (end, dim//2) """ # 计算频率:theta^(-2i/dim) for i in [0, 1, ..., dim//2 -1] freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim)) # 生成位置序列 t = [0, 1, ..., end-1] t = torch.arange(end, device=freqs.device) # 计算外积:freqs * t,形状 (end, dim//2) freqs = torch.outer(t, freqs).float() # 将其转换为复数形式:cis(freqs) = cos(freqs) + i*sin(freqs) freqs_cis = torch.polar(torch.ones_like(freqs), freqs) # 幅度为1,相位为freqs return freqs_cis def apply_rotary_emb( xq: torch.Tensor, xk: torch.Tensor, freqs_cis: torch.Tensor, ): """ 将旋转位置编码应用到Query和Key上。 Args: xq, xk: Query和Key张量,形状均为 (batch_size, seq_len, num_heads, head_dim) freqs_cis: 预计算的旋转因子,形状 (seq_len, head_dim//2) Returns: 旋转后的xq, xk,形状不变 """ # 将xq和xk的最后一维(head_dim)视为复数对 (x0, x1, x2, x3, ...) -> (x0+ix1, x2+ix3, ...) xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2)) xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2)) # 调整freqs_cis形状以进行广播 (seq_len, head_dim//2) -> (1, seq_len, 1, head_dim//2) freqs_cis = freqs_cis.unsqueeze(0).unsqueeze(2) # 复数乘法实现旋转: (a+bi) * (cosθ + i*sinθ) = (a cosθ - b sinθ) + i(a sinθ + b cosθ) xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3) xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(3) return xq_out.type_as(xq), xk_out.type_as(xk) # 在Transformer注意力模块中的使用示例(伪代码) class AttentionWithRoPE(nn.Module): def __init__(self, args): super().__init__() self.n_heads = args.n_heads self.head_dim = args.dim // args.n_heads # ... 其他初始化(Wq, Wk, Wv投影层等) # 预计算旋转因子,假设最大序列长度为args.max_seq_len self.freqs_cis = precompute_freqs_cis(self.head_dim, args.max_seq_len) def forward(self, x: torch.Tensor): batch_size, seq_len, _ = x.shape # 1. 计算Q, K, V q = self.wq(x) # (B, L, dim) k = self.wk(x) v = self.wv(x) # 2. 重塑为多头形式 (B, L, n_heads, head_dim) q = q.view(batch_size, seq_len, self.n_heads, self.head_dim) k = k.view(batch_size, seq_len, self.n_heads, self.head_dim) v = v.view(batch_size, seq_len, self.n_heads, self.head_dim) # 3. 应用旋转位置编码(仅对Q和K) # 取出当前序列长度对应的旋转因子 freqs_cis = self.freqs_cis[:seq_len] q, k = apply_rotary_emb(q, k, freqs_cis) # 4. 转置以进行批量矩阵乘法 (B, n_heads, L, head_dim) q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2) # 5. 计算缩放点积注意力分数 (B, n_heads, L, L) # 此时Q和K已包含相对位置信息 scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim) # ... 后续mask, softmax, 与V相乘等操作实操心得与避坑指南:
- 数值稳定性:在计算
freqs时,theta ** (2i/dim)可能导致数值上溢(当theta很大时)或下溢(当i/dim很大时)。使用对数空间计算exp(-log(theta) * 2i / dim)是更稳定的做法,如上文SinusoidalPositionalEncoding所示。RoPE的实现中也常采用此技巧。 - 精度问题:旋转操作涉及三角函数,对数值精度敏感。在混合精度训练(如AMP)中,确保关键计算(如
apply_rotary_emb)在足够的精度(如float32)下进行,或者使用经过数值稳定性优化的库(如xformers库中的apply_rotary_emb函数)。 - 因果注意力:在自回归语言模型中,需要结合因果掩码(Causal Mask)使用,确保当前位置只能看到之前的位置。RoPE本身不提供掩码,它只改变了Q和K的计算方式。
5. 位置编码在视觉Transformer(ViT)等领域的应用与变体
5.1 ViT中的位置编码:从1D到2D
视觉Transformer将图像切分为一系列图像块(Patches),然后将这些块视为一个序列进行处理。因此,它也需要位置编码来区分不同空间位置的图像块。
1D位置编码(原版ViT):最简单直接的方式,将二维空间位置(行,列)展平为一维序列索引。例如,一个14x14的网格,按行优先展开成0, 1, 2, ..., 195的序列,然后使用标准的可学习1D位置编码。这种方法忽略了二维空间的邻近性(例如,第13行的最后一个块和第14行的第一个块在1D序列中相邻,但在2D空间中却相隔甚远)。
2D位置编码:为了更好保留空间结构,可以为行和列分别分配位置编码,然后合并。
- 可学习2D编码:定义两个可学习的嵌入表
row_embed和col_embed,形状分别为(num_rows, d/2)和(num_cols, d/2)。对于一个位于(i, j)的块,其位置编码为concat(row_embed[i], col_embed[j])或row_embed[i] + col_embed[j]。 - 2D正弦编码:将正弦公式扩展到二维。为行坐标
pos_x和列坐标pos_y分别计算正弦编码,然后拼接或相加。这能更好地建模二维空间中的相对位置关系。
PyTorch实现2D可学习位置编码示例:
class Learnable2DPositionalEncoding(nn.Module): def __init__(self, d_model: int, grid_size: tuple): """ grid_size: (height, width) 图像块网格的高度和宽度 """ super().__init__() self.height, self.width = grid_size # 为行和列分别创建可学习嵌入 self.row_embed = nn.Parameter(torch.randn(self.height, d_model // 2)) self.col_embed = nn.Parameter(torch.randn(self.width, d_model // 2)) nn.init.normal_(self.row_embed, std=0.02) nn.init.normal_(self.col_embed, std=0.02) def forward(self, x: torch.Tensor) -> torch.Tensor: """ x: (B, L, d_model), L应该等于 height * width 假设x中块的顺序是行优先展开的。 """ batch_size, seq_len, d_model = x.shape assert seq_len == self.height * self.width, "序列长度必须等于网格大小" # 生成所有位置索引 rows = torch.arange(self.height).repeat_interleave(self.width) # [0,0,...,1,1,..., H-1] cols = torch.arange(self.width).repeat(self.height) # [0,1,...,W-1,0,1,...] # 获取对应的行、列嵌入并拼接 row_emb = self.row_embed[rows] # (L, d_model//2) col_emb = self.col_embed[cols] # (L, d_model//2) pos_emb = torch.cat([row_emb, col_emb], dim=-1) # (L, d_model) # 广播并相加 pos_emb = pos_emb.unsqueeze(0) # (1, L, d_model) x = x + pos_emb return x5.2 无需位置编码?探索位置感知的替代方案
近年来,也有一些研究尝试完全摒弃显式的位置编码,让模型从数据中隐式地学习位置信息。
- 相对注意力偏置(Relative Attention Bias):不向输入添加位置向量,而是在计算注意力分数时,直接加上一个基于查询键相对位置的偏置项
b(i-j)。这个偏置矩阵B是可学习的。Swin Transformer中就使用了这种相对位置偏置。它参数更少,且天然是平移不变的(对于图像分类等任务有益)。 - 卷积或池化预处理:在将图像块输入Transformer之前,先使用轻量的卷积层或池化层进行处理。卷积操作本身具有平移等变性,并能捕获局部空间关系,可以在一定程度上提供位置信息。
- 条件位置编码(Conditional Positional Encoding, CPE):CPE不是固定的或可学习的查找表,而是根据输入内容动态生成的。例如,使用一个深度可分离卷积(Depthwise Convolution)作用于输入序列(图像块),其输出作为位置编码。这样,位置编码能适应输入内容,更具灵活性。
选择建议:
- 对于自然语言处理,RoPE因其优秀的外推性和理论性质,已成为大语言模型的事实标准。
- 对于计算机视觉(ViT),可学习的1D或2D位置编码仍是主流且有效的选择,简单可靠。Swin Transformer的相对偏置方法在层次化设计中表现优异。
- 对于音频或时间序列,正弦位置编码或可学习编码都是常见选择,需根据序列长度是否固定、是否需要外推来决定。
- 当追求极致的平移不变性(如图像分类)或处理非网格数据(如图、点云)时,相对注意力偏置或动态位置编码(如CPE)值得尝试。
6. 位置编码的常见问题、调试技巧与实战经验
6.1 长度外推(Length Extrapolation)难题与应对
长度外推是指模型在推理时处理比训练时更长的序列的能力。这是位置编码面临的一大挑战。
- 问题表现:模型在长序列上性能急剧下降,生成无意义的文本或预测准确率暴跌。
- 根本原因:
- 绝对位置编码(正弦/可学习):对于正弦编码,虽然能计算,但高频维度在长序列下波长过长,区分度下降。对于可学习编码,模型根本没见过长位置对应的向量。
- 注意力模式变化:随着序列变长,注意力权重的分布可能发生变化,模型未学习过这种模式。
应对策略:
- 训练时使用更长序列:最直接有效的方法。在资源允许的情况下,尽量用更长的序列训练模型。
- 位置插值(Position Interpolation):对于已经用短序列训练好的模型(特别是使用RoPE的模型),可以将位置索引进行缩放。例如,训练时最大位置为2048,推理时需要4096。我们可以将推理时的位置索引
pos除以一个缩放因子s(如s=2),即使用pos/s来查询位置编码。这相当于将位置编码的“频率”降低,使其能覆盖更长的范围。LLaMA等模型的外推就采用了此类技术。 - NTK-aware Scaled RoPE:这是一种更聪明的RoPE外推方法,它不是在推理时简单缩放,而是在训练时就不均匀地缩放不同维度的频率。高频维度(对应
i大的维度)缩放得多一些,低频维度缩放得少一些。这样能更好地保持模型在训练长度内的性能,同时提升外推能力。 - 使用外推性更好的编码:从一开始就选择RoPE这类相对位置编码,其天然的外推性优于绝对位置编码。
6.2 位置编码的初始化与融合策略
- 初始化:
- 可学习位置编码:务必使用小标准差初始化(如0.02)。过大的初始化会干扰词嵌入的语义信息,导致训练初期不稳定。
- 与词嵌入的尺度协调:位置编码的幅度应与词嵌入的幅度相匹配。通常,在相加之前会对词嵌入乘以一个缩放因子
sqrt(d_model),以控制其方差。确保位置编码的初始化幅度与之协调。
- 融合策略:除了简单的加法,也有研究尝试其他融合方式,如拼接后通过一个线性层投影(增加参数),或使用门控机制动态调整位置信息的权重。但在大多数实践中,加法已被证明是简单且有效的,应作为首选。
6.3 调试与验证技巧
- 可视化位置编码:绘制位置编码矩阵的热力图(
plt.imshow(pe.squeeze().T)),观察其模式。正弦编码应呈现清晰的条纹状周期模式。可学习编码在训练初期可能是杂乱的,训练后应呈现出一定的结构(如平滑变化)。 - 检查梯度:在训练初期,监控位置编码参数的梯度。如果梯度始终为零或异常大,可能意味着它与模型其他部分的交互有问题。
- 设计简单测试:构建一个极简任务,如“输出序列中每个元素的位置索引”。用一个只有位置编码作为输入(词嵌入设为零)的小Transformer来学习这个任务。如果模型无法快速学会,说明位置编码的信息注入可能有问题。
- 对比消融实验:在你自己任务的验证集上,尝试去掉位置编码、使用不同种类的位置编码,观察性能变化。这是最直接的验证方式。
一个常见的坑:序列长度不一致的批处理在训练时,我们常使用动态填充(padding)来组成批次。位置编码应该只加到真实的 token 上,而不是 padding 部分。通常,在注意力机制中会使用注意力掩码(Attention Mask)来屏蔽 padding 位置。位置编码的加法操作本身不需要特殊处理,因为后续的注意力掩码会阻止模型关注这些加了位置编码的 padding 位置。但是,如果你使用了像RNN这样的递归网络,则需要小心处理。
位置编码虽是一个“小”组件,却是Transformer系列模型不可或缺的“灵魂”之一。理解其背后的原理,根据任务需求选择合适的方案,并能在实践中调试和优化,是构建高效Transformer模型的关键一步。从确定性的正弦波到可学习的参数表,再到精巧的旋转操作,位置编码的发展也体现了深度学习从手工设计特征到数据驱动学习,再到寻求更优归纳偏置的演进路径。
