欢迎光临
我们一直在努力

Transformer,AI时代的基石,伟大的架构革命【NLP系列第四篇】

Transformer,AI时代的基石,伟大的架构革命【NLP系列第四篇】

1. 从"RNN+注意力"到"只有注意力"

前三篇我们走了一条很清晰的路线:词向量 → RNN/LSTM/GRU → Seq2Seq + 注意力机制。上一篇讲到注意力机制时,它还是"寄生"在 RNN 上的——用 RNN 算 Q 和 KV,注意力只是给 Decoder 加了个"探照灯"。

但 2017 年 Google 的一篇论文直接掀了桌子:Attention Is All You Need。

意思是:不需要 RNN 了,注意力自己就能干所有事。

这就是 Transformer。它的核心思想就一句话:

彻底抛弃循环结构,完全基于注意力机制,实现全并行计算。

这篇博客会带着你把 Transformer 的架构从头拆到尾:位置编码、多头注意力、残差连接、层归一化、掩码机制……每个零件都讲清楚为什么需要、怎么工作,最后用 PyTorch 把它跑起来。


2. 核心概念回顾:自注意力与多头注意力

在上一篇中我们已经详细讲过自注意力,这里快速回顾几个关键点。

2.1 缩放点积注意力

自注意力的计算公式:

A

t

t

e

n

t

i

o

n

(

Q

,

K

,

V

)

=

softmax

(

Q

K

T

d

k

)

V

Attention(Q, K, V) = \\text{softmax}\\left(\\frac{QK^T}{\\sqrt{d_k}}\\right)V

Attention(Q,K,V)=softmax(dk

QKT)V

几个要点:

  • Q(Query):当前位置的"需求",想找什么
  • K(Key):各个位置的"索引",有什么信息可以提供
  • V(Value):各个位置的"内容",具体提供什么信息
  • 除以

    d

    k

    \\sqrt{d_k}

    dk

    :维度越高,点积值越大,softmax 后梯度越容易消失,缩放着保证数值稳定

矩阵形式一次算出所有位置两两之间的注意力,这是 Transformer 能全并行的根本原因。

2.2 多头注意力

单头自注意力有一个局限:所有注意力共享同一组 QKV 投影。但一句话里往往同时包含多种语义关系——句法、词义、指代等。

多头注意力就是把 QKV 投影到多个子空间(多个"头"),每个头独立计算注意力,最后拼接起来:

  • 某些头关注句法依赖(主谓宾关系)
  • 某些头关注共指关系(“it"指向"animal”)
  • 某些头关注长距离语义关联

多头注意力的参数量和单头一样(总维度不变),但表达能力更强。


3. Transformer 架构逐层解剖

3.1 总体结构

Transformer 延续了 Seq2Seq 的 Encoder-Decoder 架构:

编码器(Encoder)负责理解输入序列,解码器(Decoder)负责生成目标序列。标准配置是 6 层 Encoder + 6 层 Decoder,每层结构相同但参数独立。层数越多,模型能提取的语义越深。

3.2 Encoder 层拆解

每个 Encoder 层由四个核心组件构成:

位置编码

自注意力本身是无序的——"我爱你"和"你爱我"对它来说是一样的。所以 Transformer 需要额外注入位置信息。

原始 Transformer 使用正弦余弦位置编码:

P

E

(

p

o

s

,

2

i

)

=

sin

(

p

o

s

10000

2

i

/

d

m

o

d

e

l

)

PE(pos, 2i) = \\sin\\left(\\frac{pos}{10000^{2i/d_{model}}}\\right)

PE(pos,2i)=sin(100002i/dmodelpos)

P

E

(

p

o

s

,

2

i

+

1

)

=

cos

(

p

o

s

10000

2

i

/

d

m

o

d

e

l

)

PE(pos, 2i+1) = \\cos\\left(\\frac{pos}{10000^{2i/d_{model}}}\\right)

PE(pos,2i+1)=cos(100002i/dmodelpos)

其中

p

o

s

pos

pos 是位置,

i

i

i 是维度索引。每个位置得到一个唯一的编码向量,加到词 Embedding 上。

多头自注意力

每个 token 关注序列中所有其他 token(包括自己),通过注意力权重聚合上下文信息。这是 Encoder 理解序列语义的核心。

残差连接 + LayerNorm

残差连接:子层的输入和输出相加,形成一条"高速公路":

y

=

x

+

SubLayer

(

x

)

y = x + \\text{SubLayer}(x)

y=x+SubLayer(x)

反向传播时梯度可以直接跳过子层回传,解决深层网络的梯度消失问题。

层归一化(LayerNorm):对每个 token 的特征做标准化(均值为 0,方差为 1),加速收敛。和 BatchNorm 的区别在于——BatchNorm 跨样本归一化,LayerNorm 跨特征维度归一化,更适合变长序列。

Post-LN vs Pre-LN:原始 Transformer 用的是 Post-LN(先计算再归一化),但实践中 Pre-LN(先归一化再计算)训练更稳定,GPT 系列用的就是 Pre-LN。

前馈神经网络(FFN)

对每个位置的表示做逐位置、非线性变换,提升表达能力:

FFN

(

x

)

=

Linear

(

ReLU

(

Linear

(

x

)

)

)

\\text{FFN}(x) = \\text{Linear}( \\text{ReLU}( \\text{Linear}(x) ) )

FFN(x)=Linear(ReLU(Linear(x)))

两层线性变换中间夹一个 ReLU 激活,输入输出维度相同(

d

m

o

d

e

l

=

512

d_{model}=512

dmodel=512),中间隐藏层维度更大(

d

f

f

=

2048

d_{ff}=2048

dff=2048)。

Encoder 层的完整流程:

输入

x

x

x → 位置编码 → 多头自注意力 → 残差连接 + LayerNorm → FFN → 残差连接 + LayerNorm → 输出

3.3 Decoder 层拆解

Decoder 比 Encoder 多了一个交叉注意力层,而且要加掩码:

Masked 自注意力

Decoder 生成时,当前位置不能看到未来的词——否则就是作弊。实现方式是在自注意力分数矩阵上用一个下三角掩码,把未来位置设为

-\\infty

,softmax 后权重就是 0。

举个例子,生成第 3 个词时,只能看到第 1、2 个词和自己,不能看第 4、5 个:

# 下三角掩码(seq_len=5)(这里1代表能看,也可以用上三角,也就是1代表遮挡,不能看)
[ 1 0 0 0 0 ← 位置 1 只能看自己
1 1 0 0 0 ← 位置 2 能看 1、2
1 1 1 0 0 ← 位置 3 能看 1、2、3
1 1 1 1 0 ← 位置 4 能看 1~4
1 1 1 1 1 ] ← 位置 5 能看全部

交叉注意力(Encoder-Decoder Attention)

这就是上一篇讲的注意力机制——Q 来自 Decoder 当前步的隐状态,KV 来自 Encoder 的输出。作用是从源句中找出当前位置最该关注的信息。

前馈神经网络

和 Encoder 中的 FFN 完全一样。

Decoder 层的完整流程:

输入

y

y

y → 位置编码 → Masked 自注意力(只能看过去)→ 残差 + LayerNorm → 交叉注意力(Q 来自 Decoder,KV 来自 Encoder)→ 残差 + LayerNorm → FFN → 残差 + LayerNorm → 输出

3.4 训练 vs 推理

这是面试高频考点,也是新手最容易搞混的地方。

训练阶段:使用 Teacher Forcing——把完整目标序列一次性喂给 Decoder,通过掩码保证因果顺序。所有位置同时计算,充分利用 GPU 并行能力。

# 训练时:一次性输入,并行计算
output = transformer(src_emb, tgt_emb, tgt_mask=tgt_mask)

推理阶段:自回归生成,必须逐个词生成。每一步输入已生成的全部词,取最后一个位置的输出作为当前步的预测,拼到输入中,重复直到生成 <eos>。

# 推理时:循环生成
for i in range(max_len):
output = transformer(src_emb, generated_seq)
next_token = output[:, 1, :] # 取最后一个位置
generated_seq = concat(generated_seq, next_token)

训练能并行,推理不能并行——这是自回归生成模型的天然限制。


4. 代码实战:用 PyTorch 实现 Transformer

PyTorch 提供了完整的 nn.Transformer 模块,下面演示从 token ids 到前向输出的完整流程。

4.1 使用 nn.Transformer 官方 API

import torch
import torch.nn as nn

# ========== 超参数 ==========
d_model = 512 # 模型特征维度
nhead = 8 # 多头注意力头数
num_encoder_layers = 6 # Encoder 层数
num_decoder_layers = 6 # Decoder 层数
dim_feedforward = 2048 # FFN 隐藏层维度
dropout = 0.1 # Dropout 概率
max_len = 100 # 最大序列长度
src_vocab_size = 10000 # 源语言词表大小
tgt_vocab_size = 10000 # 目标语言词表大小
batch_size = 2
src_len = 10 # 源句长度
tgt_len = 8 # 目标句长度

# ========== 1. 构建 Transformer ==========
transformer = nn.Transformer(
d_model=d_model,
nhead=nhead,
num_encoder_layers=num_encoder_layers,
num_decoder_layers=num_decoder_layers,
dim_feedforward=dim_feedforward,
dropout=dropout,
activation='relu',
batch_first=True, # ⭐ 输入形状: (batch, seq_len, d_model)
norm_first=False, # False=Post-LN, True=Pre-LN
)

# ========== 2. 词嵌入层 ==========
src_embed = nn.Embedding(src_vocab_size, d_model)
tgt_embed = nn.Embedding(tgt_vocab_size, d_model)

# ========== 3. 模拟数据:随机生成 token ids ==========
src = torch.randint(0, src_vocab_size, (batch_size, src_len)) # (2, 10)
tgt = torch.randint(0, tgt_vocab_size, (batch_size, tgt_len)) # (2, 8)

# ========== 4. Embedding(实验中忽略位置编码,实际需要加上)==========
src_emb = src_embed(src) # (2, 10, 512)
tgt_emb = tgt_embed(tgt) # (2, 8, 512)

# ========== 5. ⭐ 生成因果掩码(Decoder 用,防止看到未来词)==========
# generate_square_subsequent_mask 生成下三角掩码矩阵
tgt_mask = nn.Transformer.generate_square_subsequent_mask(tgt_len)
# tgt_mask shape: (8, 8) — 下三角为 0,上三角为 -inf

# ========== 6. 前向传播 ==========
output = transformer(
src_emb, # 源序列 embedding
tgt_emb, # 目标序列 embedding
tgt_mask=tgt_mask, # 因果掩码
)

print(f"源序列: {src.shape}") # torch.Size([2, 10])
print(f"目标序列: {tgt.shape}") # torch.Size([2, 8])
print(f"输出: {output.shape}") # torch.Size([2, 8, 512])
# output 的最后一维通过线性层 + softmax 映射到词表大小,得到每个位置的预测词

4.2 手写简化版自注意力

如果想理解底层原理,这是自注意力最简实现:

import torch
import torch.nn as nn
import torch.nn.functional as F

class SelfAttention(nn.Module):
def __init__(self, embed_dim):
super().__init__()
## Q、K、V 三个投影矩阵
self.W_q = nn.Linear(embed_dim, embed_dim)
self.W_k = nn.Linear(embed_dim, embed_dim)
self.W_v = nn.Linear(embed_dim, embed_dim)

def forward(self, x):
batch_size, seq_len, embed_dim = x.shape

# 1. 投影得到 Q、K、V
q = self.W_q(x) # (batch, seq_len, embed_dim)
k = self.W_k(x) # (batch, seq_len, embed_dim)
v = self.W_v(x) # (batch, seq_len, embed_dim)

# 2. ⭐ 计算缩放点积分数
# (batch, seq_len, seq_len)
scores = torch.bmm(q, k.transpose(1, 2)) / torch.sqrt(
torch.tensor(embed_dim, dtype=torch.float32)
)

# 3. softmax 得到注意力权重
attn_weights = F.softmax(scores, dim=1) # (batch, seq_len, seq_len)

# 4. 加权求和得到输出
output = torch.bmm(attn_weights, v) # (batch, seq_len, embed_dim)

return output, attn_weights

## 使用示例
self_attn = SelfAttention(embed_dim=128)
x = torch.randn(2, 5, 128)
output, attn = self_attn(x)
print(f"输入: {x.shape} → 输出: {output.shape}, 权重: {attn.shape}")
# 输入: torch.Size([2, 5, 128]) → 输出: torch.Size([2, 5, 128]), 权重: torch.Size([2, 5, 5])


5. 避坑指南

5.1 batch_first 默认是 False

这是新手踩得最狠的坑。nn.Transformer 的 batch_first 默认是 False,要求的输入形状是 (seq_len, batch, d_model),而大部分人的习惯是 (batch, seq_len, d_model)。

建议始终显式设置 batch_first=True,否则 shape 全乱。

5.2 训练时别忘了 tgt_mask

# ❌ 不加 mask — 模型能看到未来词,等价于作弊
output = transformer(src_emb, tgt_emb)

# ✅ 加因果掩码
tgt_mask = nn.Transformer.generate_square_subsequent_mask(tgt_len)
output = transformer(src_emb, tgt_emb, tgt_mask=tgt_mask)

不加 tgt_mask,训练时的 loss 会异常低,但推理时效果极差——因为训练时模型"偷看"了答案。

5.3 训练和推理的代码结构完全不同

阶段输入方式计算方式掩码
训练 完整目标序列一次输入 并行计算 因果掩码
推理 逐个词拼接到已生成序列 顺序循环 下三角掩码(自动保证)

训练是一步到位,推理是循环生成——不要用推理的代码写训练逻辑,也别用训练的逻辑做推理。

5.4 Pre-LN vs Post-LN

原始 Transformer 用 Post-LN(先计算再归一化),但深层网络训练不稳定,需要 warmup。实践中 Pre-LN(先归一化再计算)更稳定,GPT、BERT 现代实现大多用 Pre-LN。

# Post-LN(原始版):子层 → 残差 → LayerNorm
# Pre-LN(稳定版):LayerNorm → 子层 → 残差
encoder_layer = nn.TransformerEncoderLayer(
d_model=512, nhead=8,
norm_first=True, # True = Pre-LN
)

5.5 位置编码不要漏

nn.Transformer 内部不包含位置编码,需要手动叠加。漏掉位置编码相当于让模型在"无序"的状态下处理序列,长序列效果会严重下降。

# 注意:nn.Transformer 不内置位置编码,需要自己加
src_emb = src_embed(src) + positional_encoding[:src_len]
tgt_emb = tgt_embed(tgt) + positional_encoding[:tgt_len]


6. 总结

Encoder vs Decoder 对比

对比维度EncoderDecoder
注意力类型 自注意力(双向) 掩码自注意力 + 交叉注意力
掩码 有(因果掩码,下三角)
输入 源序列 目标序列(已生成部分)
可见范围 序列中所有位置 只看当前位置之前(含自己)
Q 来源 自身 自注意力:自身 / 交叉注意力:Decoder
KV 来源 自身 自注意力:自身 / 交叉注意力:Encoder
输出 源序列的上下文表示 目标序列的预测结果

Transformer 的五大核心创新

  • 完全基于注意力:摆脱 RNN,实现全并行训练
  • 多头注意力:多个子空间并行学习不同类型的依赖关系
  • 位置编码:给无序的自注意力注入位置信息
  • 残差连接 + LayerNorm:支撑深层网络稳定训练
  • 自回归生成 + 因果掩码:训练时并行,推理时逐词生成
  • 参考链接

    • 原始论文:Attention Is All You Need
    • PyTorch nn.Transformer 官方文档
    • PyTorch TransformerEncoderLayer 官方文档
    • The Annotated Transformer

    下篇预告:Transformer 讲完了,下一篇终于轮到真正的大模型了——BERT 和 GPT 系列。它们各自在 Transformer 的基础上做了什么改进?为什么 BERT 适合理解任务、GPT 适合生成任务?敬请期待。

    赞(0)
    未经允许不得转载:171主机测评 » Transformer,AI时代的基石,伟大的架构革命【NLP系列第四篇】
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址