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.Transformerbatch_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 对比

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

Transformer 的五大核心创新

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

参考链接


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

Logo

脑启社区是一个专注类脑智能领域的开发者社区。欢迎加入社区,共建类脑智能生态。社区为开发者提供了丰富的开源类脑工具软件、类脑算法模型及数据集、类脑知识库、类脑技术培训课程以及类脑应用案例等资源。

更多推荐