第2章:核心架构:Encoder-Decoder结构总览,输入输出流程
好,咱们直接进入正题。
Transformer 最核心的设计,就是 Encoder-Decoder 结构。说白了,它就是一个「编码-解码」的管道。你扔进去一句话,它先编码成机器能懂的「中间表示」,再解码成你想要的另一句话。
我刚开始接触这个结构时,总觉得它像个黑盒子。后来亲手搭了一遍,才明白里面的门道。今天我就带你把这个盒子拆开看看。
2.1 整体架构长什么样?
先看一张抽象图(你可以在脑子里想象一下):
- 左边是 Encoder(编码器):负责把输入序列「读进去」,提取特征。
- 右边是 Decoder(解码器):负责根据编码器的输出,一步步「生成」目标序列。
- 中间有个连接层:Decoder 的每一层都会「偷看」Encoder 的输出,这就是 Cross-Attention。
嗯,这里要注意:Encoder 和 Decoder 都不是单层,而是由 N 个相同的层堆叠起来的。原论文里 N=6,我习惯叫它「6层堆叠」。你想想看,层数越多,模型容量越大,但训练也越难。
核心记忆点:
- Encoder 是双向的(可以看整个句子)
- Decoder 是单向的(只能看已经生成的部分)
- 两者通过 Cross-Attention 交互
2.2 输入流程:从文本到向量
输入流程,说白了就是「把文字变成数字」的过程。我把它拆成三步:
- Tokenization(分词):把句子切成一个个 token。比如 "I love NLP" 切成 ["I", "love", "NLP"]。
- Embedding(嵌入):每个 token 映射成一个向量。维度一般是 512 或 768。
- Positional Encoding(位置编码):给每个向量加上位置信息。因为 Transformer 没有循环结构,得靠这个告诉模型「词与词的顺序」。
我在项目中遇到过一个问题:如果输入句子长度超过模型预设的最大长度(比如 512),就得截断或做滑窗。这个坑我踩过,后面会细说。
小技巧: 位置编码我建议用正弦余弦函数(原论文方案),不要用可学习的。原因?正弦余弦能外推到更长的序列,可学习的就不行。我试过,效果差不少。
2.3 输出流程:从向量到文本
输出流程是输入流程的逆过程,但有个关键区别:它是自回归的。
什么叫自回归?就是一次只生成一个 token,然后把生成的 token 拼回去,再生成下一个。就像你写文章,写一个字,看一遍,再写下一个字。
具体流程:
- Decoder 接收「已生成的部分」作为输入
- 经过 Masked Self-Attention(防止看到未来的词)
- 再经过 Cross-Attention(参考 Encoder 的输出)
- 最后通过一个线性层 + Softmax,输出下一个 token 的概率分布
- 选择概率最高的 token(或者用采样策略)作为输出
我曾经犯过一个低级错误:在推理时忘了加 <start> 和 <end> 标记。结果模型永远不知道什么时候开始,什么时候结束。嗯,这个教训挺深刻的。
2.4 输入输出的维度变化
咱们用一张表来理清维度变化。假设 batch_size=2,序列长度=10,embedding_dim=512:
| 阶段 | 输入形状 | 输出形状 | 说明 |
|---|---|---|---|
| Embedding | (2, 10) | (2, 10, 512) | token ID 转向量 |
| Positional Encoding | (2, 10, 512) | (2, 10, 512) | 加上位置信息 |
| Encoder 输出 | (2, 10, 512) | (2, 10, 512) | 经过 N 层编码 |
| Decoder 输入 | (2, 1, 512) | (2, 1, 512) | 初始只有 <start> |
| Decoder 输出 | (2, 1, 512) | (2, vocab_size) | 预测下一个 token |
注意: 训练时 Decoder 的输入是整个目标序列(右移一位),推理时是逐步生成的。这两者的实现方式完全不同,千万别搞混。我见过有人把训练代码直接拿来推理,结果内存爆炸。
2.5 一个简单的代码骨架
为了让你更直观地理解,我写个伪代码级别的骨架。别纠结细节,看流程就好:
class Transformer(nn.Module):
def __init__(self):
self.encoder = Encoder(num_layers=6)
self.decoder = Decoder(num_layers=6)
self.linear = nn.Linear(512, vocab_size)
def forward(self, src, tgt):
# src: 源语言句子 (batch, src_len)
# tgt: 目标语言句子 (batch, tgt_len)
memory = self.encoder(src) # 编码器输出
output = self.decoder(tgt, memory) # 解码器输出
logits = self.linear(output) # 映射到词表大小
return logits
你看,核心就三行:编码、解码、映射。但每一行背后都有复杂的注意力机制在支撑。
2.6 我的一点经验总结
最后,分享几个我踩过的坑:
- 输入输出长度不一致很正常:Encoder 输入 10 个词,Decoder 可能输出 15 个词。别慌,这是正常的。
- Padding 要小心:batch 内句子长度不同,需要 padding。但 padding 的位置不能参与注意力计算,记得用 mask。
- 推理速度慢是通病:自回归生成一次只出一个词,长句子会慢。我后来用 KV Cache 优化,速度提升了好几倍。
好了,这一章就到这里。Encoder-Decoder 结构是整个 Transformer 的骨架,后面的章节会往上面加肌肉和血管。你先把这张图刻在脑子里,后面学起来会轻松很多。