本文目录导读:
这是一个很核心的问题,位置编码的加入方式取决于模型架构,但最主流的方法(尤其是Transformer中)是 “直接加到输入 embedding 上”。
下面为你详细拆解几种常见的加入方式及其原理。
核心思路:为什么是“加”而不是“拼接”?
在Transformer中,位置编码通常直接加到词向量(Word Embedding)上,而不是拼接。
- 加法:
输入 = word_embedding + position_encoding - 拼接:
输入 = concat(word_embedding, position_encoding)
为什么用加法? 假设词向量的维度是512维。
- 维度不变:加法不改变维度,而拼接会变成1024维,导致模型参数剧增,计算量翻倍。
- 信息融合:模型本质上是在做向量空间中的旋转、缩放等线性变换,通过加法,模型可以很容易地学习到“哪个维度携带着位置信息,哪个维度携带着语义信息”,相当于每个向量既包含了“是什么词”又包含了“在哪里”这两种信息。
主要加入方式
根据位置编码的类型不同,加入方式略有区别,主要有以下几种:
绝对位置编码(Absolute Position Encoding)
这是最经典、最广泛使用的方式,来自《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是位置索引(0, 1, 2, ...)i是维度索引d_model是向量维度
-
代码实现(PyTorch风格):
import torch import math def create_sinusoidal_encoding(seq_len, d_model): position = torch.arange(seq_len).unsqueeze(1) # (seq_len, 1) div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model)) pe = torch.zeros(seq_len, d_model) pe[:, 0::2] = torch.sin(position * div_term) # 偶数维度用sin pe[:, 1::2] = torch.cos(position * div_term) # 奇数维度用cos return pe # (seq_len, d_model) # 假设词向量 shape: (batch, seq_len, d_model) word_embeddings = torch.randn(2, 10, 512) # batch=2, seq_len=10, dim=512 pos_encoding = create_sinusoidal_encoding(10, 512).unsqueeze(0) # (1, 10, 512) # 关键步骤:直接相加 final_input = word_embeddings + pos_encoding -
优点:无需训练、有界、可外推(理论上可以处理比训练时更长的序列)。
-
缺点:模型难以学到“相对位置”关系(比如词A和词B之间的距离)。
可学习位置编码(Learnable Position Encoding)
这是BERT、GPT等主流预训练模型使用的方式。
-
方式:定义一个大小(
max_seq_len, d_model)的可训练矩阵(nn.Embedding),模型在训练过程中,会自动调整每个位置对应的向量值,加入方式依然是直接加到词向量上。 -
代码实现:
import torch.nn as nn class LearnablePositionalEncoding(nn.Module): def __init__(self, max_len, d_model): super().__init__() # nn.Embedding 是一个可训练的查找表 self.pos_embedding = nn.Embedding(max_len, d_model) def forward(self, x): # x shape: (batch, seq_len, d_model) seq_len = x.size(1) # 生成位置索引 [0, 1, 2, ..., seq_len-1] positions = torch.arange(seq_len, device=x.device) # 获取对应位置的可学习向量 pos_vectors = self.pos_embedding(positions) # (seq_len, d_model) # 关键步骤:直接相加 return x + pos_vectors.unsqueeze(0) # 广播到batch维度 -
优点:模型可以自适应地学习最佳的位置表示,非常灵活,对于固定长度的任务(如BERT的512长度),效果很好。
-
缺点:不能外推(extrapolate)到比训练时更长的序列(训练时最大长度是512,就不能处理513个token)。
相对位置编码(Relative Position Encoding)
这种方式不直接加到输入层,而是修改注意力分数的计算方式。
-
核心思想:在计算 Query 和 Key 的注意力分数时,不去管它们在绝对位置0和1,而是关心它们之间的相对距离(相隔3个词”)。
-
加入方式:
- Attention Score 修正:在原始的
Q * K^T结果上,加上一个基于相对位置(i-j)的偏置项b_{i,j}。 - 公式:
Attention(Q, K, V) = softmax( (Q * K^T) / sqrt(d_k) + R ) * V,R是相对位置矩阵。
- Attention Score 修正:在原始的
-
代表模型:Transformer-XL、T5、DeBERTa,T5甚至更进一步,直接使用偏置,不生成位置向量。
-
优点:处理长序列(外推能力)极强;更符合逻辑(句子中“词与词的关系”往往取决于它们的相对距离而非绝对位置)。
-
缺点:实现相对复杂,计算量稍大。
旋转位置编码(Rotary Position Encoding, RoPE)
这是目前大模型(如LLaMA, Mistral, Qwen)最流行的方法。
- 方式:既不直接加到输入,也不直接改注意力分数,而是在计算
Q和K向量时,通过旋转矩阵对它们进行旋转变换,从而隐式地编码位置信息。 - 优点:同时具备绝对位置编码的易用性和相对位置编码的外推能力(理论上可以无限长),能天然地引入相对距离信息。
- 加入方式(简化理解):
Q' = rotate(Q, position),K' = rotate(K, position),Attention = softmax( Q' * K'^T / sqrt(d) ),旋转操作就是在高维空间里对向量进行角度旋转。
总结与对比
| 编码类型 | 加入方式 | 是否可学习 | 外推能力 | 代表模型 | 核心公式/操作 |
|---|---|---|---|---|---|
| 绝对位置 (Sinusoidal) | 直接加到输入 | 否 | 较好 | 原始Transformer | input += PE(pos) |
| 绝对位置 (可学习) | 直接加到输入 | 是 | 差 (固定长度) | BERT, GPT-2 | input += Embedding(pos) |
| 相对位置 | 修改注意力分数 | 是 | 强 | T5, Transformer-XL | score = QK^T + b_{i-j} |
| 旋转位置 (RoPE) | 旋转Q/K向量 | 否 | 最强 | LLaMA, Mistral, Qwen | Q = rotate(Q, pos) |
一句话回答你的问题:
最常见的做法是:在Transformer模型的Embedding层之后、多头注意力层之前,直接将位置编码矩阵相加到词向量矩阵上。
- 如果你在用BERT/GPT:用 可学习的Embedding 相加。
- 如果你在用LLaMA/Qwen:用 RoPE 方法,它不在Embedding层加,而是在计算Attention时对Q/K进行旋转操作。