一句话总结:词嵌入(Embedding)提供 Token 的内容特征,位置编码(Positional Encoding)提供顺序线索。原始 Transformer 把它们相加,是为了在不增加
d_model、不新增投影参数的情况下,把两种信号放进同一条 residual stream;这是一种有效的紧凑设计,不是数学上的无损打包。
14.1 回顾:输入处理的两个步骤
在第 4 章和第 5 章,我们分别学习了:
| 章节 | 组件 | 作用 |
|---|---|---|
| 第 4 章 | Embedding | 把 Token ID 变成向量(语义信息) |
| 第 5 章 | Positional Encoding | 给向量添加位置信息 |
这一章,我们来深入理解:为什么这两个信息要通过"相加"来结合?
14.2 两种信息的本质
14.2.1 词嵌入:语义信息
回顾第 4 章的 Embedding 查找表:
更准确地说,每个 Token 在查找表里有一行可学习参数。训练会让这些向量承载词法和统计特征,我们常把它概括成“语义信息”。下面仍用原来的例子帮助直觉,但相邻关系不是每个模型原始 embedding table 都必须满足的定律:
- "国王"和"王后"的向量接近(都是皇室)
- "国王"和"苹果"的向量很远(没有关系)
Embedding 回答的问题是:这个词是什么意思?
14.2.2 位置编码:位置信息
回顾第 5 章的位置编码:
原始 Transformer 的位置编码是一组正弦/余弦函数,每个位置会算出一个依赖位置下标的向量:
- 位置 0 对应一组数值
- 位置 1 对应另一组数值
- ...
位置编码回答的问题是:这个词在句子的什么位置?
14.2.3 两种信息都很重要
考虑这两个句子:
- "我爱你"
- "你爱我"
它们包含完全相同的词,但意思完全不同。位置改变了含义。
所以 Transformer 需要同时知道:
- 每个词是什么(Embedding)
- 每个词在哪里(Positional Encoding)
14.3 结合方式:相加 vs 拼接
14.3.1 两种选择
有两种直观的方式来结合 Embedding 和 Positional Encoding:
方式一:拼接(Concatenation)
输入向量 = [Embedding; Positional Encoding]
维度:d_model + d_model = 2 × d_model
方式二:相加(Addition)
输入向量 = Embedding + Positional Encoding
维度:d_model(不变)
Transformer 选择了相加。为什么?
14.3.2 相加的优势
1. 维度不增加
如果 d_model = 512,拼接会产生 1024 维向量。接下来有两条路:
- 让后续网络也保持 1024 维:activation 宽一倍;对于输入和输出都随模型宽度增加的方阵投影,参数量与矩阵乘法开销约变为四倍。
- 先把 1024 维投影回 512 维:网络仍可正常工作,但会多出一个
1024 × 512的可学习投影。
所以,拼接不是“错误方法”,也不是所有参数都会统一翻倍。只是与直接相加相比,它要么扩大后续网络,要么增加一次投影。相加则不新增参数,输出仍是 512 维。
2. 两种信号都会影响结果
两个向量相加后,会叠加成同一个输入:
embedding = [0.5, 0.3, -0.2, 0.8, ...] # 语义
position = [0.1, 0.0, 0.1, -0.1, ...] # 位置
combined = [0.6, 0.3, -0.1, 0.7, ...] # 两种信号共同影响它
要注意:只看 combined,一般不能唯一还原原来的两个向量。因为许多不同的 (embedding, position) 都会得到同一个和。这里的目标也不是把两者无损压缩后再拆开,而是让模型学会利用这组混合信号。
3. 训练会围绕这种接口共同适配
Token embedding、Attention 的投影矩阵,以及可学习的位置向量(如果采用这种方式)会一起训练。模型不需要恢复两张原始表,只需要让混合后的表示对预测任务有用。
14.3.3 一个直观的类比
想象你在一个派对上:
- 你的外表(Embedding):告诉别人"你是谁"
- 你站的位置(Positional Encoding):告诉别人"你在哪"
别人看到你时,会同时利用“你是谁”和“你站在哪里”。这个类比只说明两类线索可以共同参与判断;它不表示把两者相加以后还能逐项无损还原。
14.4 计算方式示例
14.4.1 具体计算
看这张图,我们来追踪一个维度上的玩具计算。为了把算术讲清楚,这里用最简单的 SGD;真实训练通常还会使用 AdamW 等优化器,并同时更新大量参数。
假设:
old_embedding_value = 0.9 # 某个词的 embedding 中的一个值
positional_encoding = 0.1 # 对应位置的位置编码值
输入阶段(相加):
combined = embedding + positional_encoding
= 0.9 + 0.1
= 1.0
训练阶段(假设 SGD 更新这个 embedding 值):
new_embedding_value = old_embedding_value - lr * error_gradient
= 0.9 - 0.1 * (-0.4)
= 0.9 + 0.04
= 0.94
下一次前向传播:
new_combined_value = new_embedding_value + positional_encoding
= 0.94 + 0.1
= 1.04
14.4.2 关键观察
注意这个过程中:
- Embedding 是可学习的:在训练过程中不断更新
- 这里的 Positional Encoding 是固定的:原始 Transformer 的正弦/余弦值不参与训练;如果换成可学习位置 embedding,它也会收到梯度并更新
- 相加发生在每次前向传播:不是一次性的
14.5 深层逻辑:为什么这种设计有效?
14.5.1 先看一次线性投影
令 Token embedding 为 E,位置向量为 P,输入就是:
Z = E + P
Q = Z @ Wq = E @ Wq + P @ Wq
K = Z @ Wk = E @ Wk + P @ Wk
这不是“正交以后自动分家”,而是线性变换的分配律。Wq 和 Wk 会在训练中适配,让内容与位置的混合对 Attention 有用。
14.5.2 Attention 看到的是四类交互
把 QKᵀ 展开,会得到四类项:
(E Wq)(E Wk)ᵀ # 内容-内容
(E Wq)(P Wk)ᵀ # 内容-位置
(P Wq)(E Wk)ᵀ # 位置-内容
(P Wq)(P Wk)ᵀ # 位置-位置
这比“语义占一些维度、位置占另一些正交维度”更准确。模型既能利用“小沈阳”和“赵本山”的内容关系,也能利用两者在句子中的方向和距离;内容与位置的交叉项本身也可能有用。
14.5.3 不要求无损分离
加法映射 (E, P) → E + P 是多对一的,因此 Attention 一般不能从和向量中唯一还原 E 与 P。但语言模型的任务不是复原输入前的两张表,而是预测下一个 Token。只要这套共同训练的表示保留了对预测有用的线索,设计就成立。
14.6 变体:不同的位置编码方法
14.6.1 原始方法:相加固定编码
input = Embedding(token_ids) + PositionalEncoding(positions)
这是原始 Transformer(2017)使用的方法,采用正弦余弦固定编码。
注意:GPT-2 虽然也是"相加",但使用的是可学习的位置编码,而非固定的正弦编码。
14.6.2 可学习位置编码
GPT-1、GPT-2 和 GPT-3 采用的是可学习的绝对位置 embedding。这不代表所有后来带有 GPT 名称或 decoder-only 结构的模型都使用同一种方法。
import torch
from torch import nn
class LearnablePositionalEncoding(nn.Module):
def __init__(self, max_len, d_model):
super().__init__()
# 位置编码也是一个可学习的 Embedding
self.pe = nn.Embedding(max_len, d_model)
def forward(self, x):
seq_len = x.size(1)
if seq_len > self.pe.num_embeddings:
raise ValueError("sequence length exceeds max_len")
positions = torch.arange(seq_len, device=x.device)
return x + self.pe(positions)
它不需要手工规定正弦频率,可以从训练数据中学习位置模式;代价是 lookup table 有一个原生范围,超出范围时需要扩表并继续训练或采用其他扩展办法。
14.6.3 RoPE:旋转位置编码
更现代的方法是 RoPE(Rotary Position Embedding),它不是简单地相加,而是通过旋转来编码位置:
Q_rotated = rotate(Q, position)
K_rotated = rotate(K, position)
RoPE 的优势:
- 用绝对位置决定旋转角度,但旋转后的
Q·K会显式依赖两个位置的相对位移 - 不给 residual stream 额外相加一个位置向量
LLaMA、GPT-NeoX、Mistral 等模型使用 RoPE。不过,RoPE 不等于模型可以直接无损外推到任意长度。超过训练上下文时,往往还需要位置插值、频率缩放或继续训练。这个话题我们在第 25 章详细讨论。
14.7 实际代码:输入处理完整流程
14.7.1 PyTorch 实现
import torch
from torch import nn
class TransformerInput(nn.Module):
def __init__(self, vocab_size, d_model, max_len, dropout=0.1):
super().__init__()
# 词嵌入
self.token_embedding = nn.Embedding(vocab_size, d_model)
# 位置编码(可学习)
self.position_embedding = nn.Embedding(max_len, d_model)
# Dropout
self.dropout = nn.Dropout(dropout)
def forward(self, x):
# x: [batch_size, seq_len] 的 token IDs
if x.size(1) > self.position_embedding.num_embeddings:
raise ValueError("sequence length exceeds max_len")
# 1. 词嵌入
token_emb = self.token_embedding(x) # [batch, seq, d_model]
# 2. 可学习的绝对位置 embedding
positions = torch.arange(x.size(1), device=x.device)
pos_emb = self.position_embedding(positions) # [seq, d_model]
# 3. 相加
combined = token_emb + pos_emb # [batch, seq, d_model]
# 4. Dropout
return self.dropout(combined)
这段代码演示的是 GPT-2 风格的可学习绝对位置,所以没有混入原始 Transformer 对 Token embedding 乘以 √d_model 的另一套约定。
14.7.2 维度追踪
输入 token_ids: [4, 16] # 4个句子,每句16个token
Token Embedding:
查表: token_embedding([4, 16])
输出: [4, 16, 512] # 每个token变成512维向量
Position Embedding:
位置: [0, 1, 2, ..., 15]
查表: position_embedding([16])
输出: [16, 512] # 每个位置有512维向量
广播: [4, 16, 512] # 广播到batch维度
相加:
[4, 16, 512] + [4, 16, 512] = [4, 16, 512]
输出: [4, 16, 512] # 包含语义+位置信息的向量
14.8 常见问题
14.8.1 相加会不会让信息混淆?
从数学上说,会丢掉唯一可逆性。 单看 E + P,不能保证还原出唯一的 E 和 P。
但模型不需要做这件事。它只需要利用混合表示完成预测,而 Token embedding、位置表示和后续投影都是围绕这个接口设计或共同训练的。原始 Transformer 的实验也说明,这种紧凑做法在实际任务上可行。
14.8.2 为什么要注意两种信号的尺度?
如果位置编码太大,会"淹没"词嵌入的信息:
embedding = [0.5, 0.3, -0.2] # 语义信息
position = [10, 20, -15] # 位置信息(太大了!)
combined = [10.5, 20.3, -15.2] # 主要是位置信息
如果两者的尺度极端失衡,较大的那一项会主导初始表示。但不存在“位置编码必须很小”的统一定律。正弦/余弦值落在 [-1, 1];原始 Transformer 同时把 Token embedding 乘以 √d_model。可学习位置 embedding 的尺度则由初始化和训练共同决定。
14.8.3 可学习 vs 固定位置编码哪个好?
| 类型 | 优点 | 缺点 |
|---|---|---|
| 固定(正弦/余弦) | 公式可计算到训练长度以外,不新增位置参数 | 算得出更远位置,不等于模型一定能在更长序列上泛化 |
| 可学习绝对位置 | 能学习训练任务中的位置模式 | lookup table 有固定范围,扩展通常需要改表并继续训练 |
| RoPE | Q·K 显式包含相对位移 | 长上下文扩展仍可能需要缩放、插值或训练 |
没有一种方法对所有模型都最好。原始 Transformer 论文报告过:在当时的翻译实验中,可学习位置 embedding 与固定正弦方式结果几乎相同。选择取决于模型结构、训练长度和长上下文目标。
14.9 本章总结
14.9.1 核心概念
| 信息类型 | 来源 | 表示什么 | 是否可学习 |
|---|---|---|---|
| 词嵌入 | Embedding 查表 | 词的语义 | 是 |
| 位置编码 | 位置 Embedding/正弦函数 | 词的位置 | 取决于实现 |
| 输入向量 | 两者相加 | 语义 + 位置 | - |
14.9.2 为什么相加而不是拼接
- 维度不变:不增加计算量
- 没有新增投影参数:两组同维向量可以直接进入 residual stream
- 交互可学习:Attention 能利用内容、位置以及它们的交叉项
14.9.3 核心认知
词嵌入提供“是什么”的线索,位置编码提供“在哪里”的线索。原始 Transformer 用相加把它们放进同一个
d_model:这个和不是可逆压缩,也不依赖未经证明的正交假设;它是一种参数少、接口简单,并且能让 Attention 学习多类交互的设计。
本章交付物
学完这一章,你应该能够:
- 解释词嵌入和位置编码分别表示什么
- 说出为什么选择相加而不是拼接
- 理解相加后信息如何被 Attention 利用
- 知道可学习位置编码和固定位置编码的区别
下一章预告
现在我们理解了输入是如何准备的:词嵌入 + 位置编码 → 相加 → 输入向量。
下一章,我们将把所有组件串起来,追踪数据从输入到输出的完整前向传播过程。这是理解 Transformer 如何工作的最后一块拼图!