Transformer模型深度游历:NLP领域的革新应用探索
本文将带你深入了解 Transformer 模型在自然语言处理(NLP)中的原理与应用,从最核心的自注意力机制到完整的编码器—解码器架构,并配以详尽的数学推导、代码示例与图解,帮助你快速掌握 Transformer 及其在机器翻译、文本分类等任务中的应用。
目录
- 引言
- 背景与发展历程
Transformer 模型概览
自注意力机制深度剖析
完整 Transformer 架构解析
代码示例:从零实现简化版 Transformer
图解:Transformer 各模块示意
Transformer 在 NLP 中的经典应用
优化与进阶:Transformers 家族演化
- 总结与最佳实践
引言
在传统 RNN、LSTM 基础上,Transformer 模型以其“全注意力(All-Attention)”的架构彻底颠覆了序列建模的思路。自 Vaswani 等人在 2017 年提出《Attention Is All You Need》 以来,Transformer 不仅在机器翻译、文本分类、文本生成等众多 NLP 任务中取得了突破性成果,也逐渐催生了如 BERT、GPT、T5 等一系列预训练大模型,成为当下最热门的研究方向之一。
本文将从 Transformer 的核心构件——自注意力机制开始,逐步深入其编码器(Encoder)与解码器(Decoder)结构,并通过 PyTorch 代码示例带你手把手实现一个简化版 Transformer,最后介绍其在实际 NLP 任务中的典型应用及后续发展。
背景与发展历程
在 Transformer 出现之前,主流的序列建模方法主要依赖循环神经网络(RNN)及其变体 LSTM、GRU 等。尽管 LSTM 能通过门控机制在一定程度上缓解长程依赖消失(vanishing gradient)的问题,但在并行化计算、长距离依赖捕捉等方面依旧存在瓶颈:
计算瓶颈
- RNN 需要按时间步(time-step)序贯计算,训练与推理难以并行化。
长程依赖与梯度消失
- 随着序列长度增大,若信息需要跨越多个时间步传播,LSTM 依旧会出现注意力衰减,要么依赖于注意力机制(如 Seq2Seq+Attention 架构),要么被限制在较短上下文窗口内。
注意力架构的初步尝试
- Luong Attention、Bahdanau Attention 等 Seq2Seq+Attention 结构,虽然缓解了部分长程依赖问题,但注意力仅在编码器—解码器之间进行,并没有完全“摆脱” RNN 的序列瓶颈。
Transformer 的核心思想是:完全用注意力机制替代 RNN/卷积,使序列中任意两处都能直接交互,从而实现并行化、高效地捕捉长程依赖。它一经提出,便在机器翻译上瞬间刷新了多项基准,随后被广泛迁移到各类 NLP 任务中。
Transformer 模型概览
3.1 为何需要 Transformer?
并行化计算
- RNN 需要按时间顺序一步步地“读入”上一个词的隐藏状态,导致 GPU/TPU 并行能力无法充分利用。
- Transformer 利用“自注意力”在同一层就能把序列内的所有位置同时进行计算,大幅提升训练速度。
全局依赖捕捉
- 传统 RNN 的信息传递依赖于“逐步传递”,即使有注意力层,编码仍受前几层的限制。
- Transformer 中的注意力可以直接在任何两个位置之间建立关联,不受序列距离影响。
建模灵活性
- 不同层之间可以采用不同数量的注意力头(Multi-Head Attention),更细腻地捕捉子空间信息。
- 编码器—解码器之间可以灵活地进行交互注意力(encoder-decoder attention)。
3.2 核心创新:自注意力机制(Self-Attention)
“自注意力”是 Transformer 最核心的模块,其基本思想是:对于序列中任意一个位置的隐藏表示,将它与序列中所有位置的隐藏表示进行“打分”计算权重,然后根据这些权重对所有位置的信息做加权求和,得到该位置的新的表示。这样,每个位置都能动态地“看看”整个句子,更好地捕获长程依赖。
下文我们将从数学公式与代码层面深入剖析自注意力的工作原理。
自注意力机制深度剖析
4.1 打破序列顺序的限制
在 RNN 中,序列信息是通过隐藏状态 $h\_t = f(h\_{t-1}, x\_t)$ 逐步传递的,第 $t$ 步的输出依赖于第 $t-1$ 步。这样会导致:
- 序列越长,早期信息越难保留;
- 难以并行,因为第 $t$ 步要等第 $t-1$ 步完成。
自注意力(Self-Attention) 的关键在于:一次性把整个序列 $X = [x\_1, x\_2, \dots, x\_n]$ 同时“看一遍”,并基于所有位置的交互计算每个位置的表示。
具体地,给定输入序列的隐藏表示矩阵 $X \in \mathbb{R}^{n \times d}$,在自注意力中,我们首先将 $X$ 线性映射为三组向量:Query(查询)、Key(键)、Value(值),分别记为:
$$
Q = XW^Q,\quad K = XW^K,\quad V = XW^V,
$$
其中权重矩阵 $W^Q, W^K, W^V \in \mathbb{R}^{d \times d\_k}$。随后,对于序列中的每个位置 $i$,(即 $Q\_i$)与所有位置的 Key 向量 ${K\_j}{j=1}^n$ 做点积打分,再通过 Softmax 得到注意力权重 $\alpha{ij}$,最后用这些权重加权 Value 矩阵:
$$
\text{Attention}(Q, K, V)_i
= \sum_{j=1}^n \alpha_{ij}\, V_j,\quad
\alpha_{ij} = \frac{\exp(Q_i \cdot K_j / \sqrt{d_k})}{\sum_{l=1}^n \exp(Q_i \cdot K_l / \sqrt{d_k})}.
$$
这样,位置 $i$ 的新表示 $\text{Attention}(Q,K,V)\_i$ 包含了序列上所有位置按相关度加权的信息。
4.2 Scaled Dot-Product Attention 数学推导
Query-Key 点积打分
对于序列中位置 $i$ 的 Query 向量 $Q\_i \in \mathbb{R}^{d\_k}$,和位置 $j$ 的 Key 向量 $K\_j \in \mathbb{R}^{d\_k}$,它们的点积:
$$
e_{ij} = Q_i \cdot K_j = \sum_{m=1}^{d_k} Q_i^{(m)}\, K_j^{(m)}.
$$
$e\_{ij}$ 表征了位置 $i$ 与位置 $j$ 的相似度。
缩放因子
由于当 $d\_k$ 较大时,点积值的方差会随着 $d\_k$ 增大而增大,使得 Softmax 的梯度在极端值区可能变得非常小,进而导致梯度消失或训练不稳定。因此,引入缩放因子 $\sqrt{d\_k}$,将打分结果缩放到合适范围:
$$
\tilde{e}_{ij} = \frac{Q_i \cdot K_j}{\sqrt{d_k}}.
$$
Softmax 正则化
将缩放后的分数映射为权重:
$$
\alpha_{ij} = \frac{\exp(\tilde{e}_{ij})}{\sum_{l=1}^{n} \exp(\tilde{e}_{il})},\quad \sum_{j=1}^{n} \alpha_{ij} = 1.
$$
加权输出
最终位置 $i$ 的输出为:
$$
\text{Attention}(Q, K, V)_i = \sum_{j=1}^{n} \alpha_{ij}\, V_j,\quad V_j \in \mathbb{R}^{d_v}.
$$
整个过程可以用矩阵形式表示为:
$$
\text{Attention}(Q,K,V)
= \text{softmax}\Bigl(\frac{QK^\top}{\sqrt{d_k}}\Bigr)\, V,
$$
其中 $QK^\top \in \mathbb{R}^{n \times n}$,Softmax 是对行进行归一化。
4.3 Multi-Head Attention 详解
单一的自注意力有时只能关注序列中的某种相关性模式,但自然语言中往往存在多种“子空间”关系,比如语义相似度、词性匹配、命名实体关系等。Multi-Head Attention(多头注意力) 就是将多个“自注意力头”并行计算,再将它们的输出拼接在一起,以捕捉多种不同的表达子空间:
多头并行计算
令模型设定头数为 $h$。对于第 $i$ 个头:
$$
Q_i = X\, W_i^Q,\quad K_i = X\, W_i^K,\quad V_i = X\, W_i^V,
$$
其中 $W\_i^Q, W\_i^K, W\_i^V \in \mathbb{R}^{d \times d\_k}$,通常令 $d\_k = d / h$。
然后第 $i$ 个头的注意力输出为:
$$
\text{head}_i = \text{Attention}(Q_i, K_i, V_i) \in \mathbb{R}^{n \times d_k}.
$$
拼接与线性映射
将所有头的输出在最后一个维度拼接:
$$
\text{Head} = \bigl[\text{head}_1; \text{head}_2; \dots; \text{head}_h\bigr] \in \mathbb{R}^{n \times (h\,d_k)}.
$$
再通过一个线性映射矩阵 $W^O \in \mathbb{R}^{(h,d\_k) \times d}$ 变换回原始维度:
$$
\text{MultiHead}(Q,K,V) = \text{Head}\, W^O \in \mathbb{R}^{n \times d}.
$$
- 注意力图示(简化)
输入 X (n × d)
│
┌──────▼──────┐ ┌──────▼──────┐ ... ┌──────▼──────┐
│ Linear Q₁ │ │ Linear Q₂ │ │ Linear Q_h │
│ (d → d_k) │ │ (d → d_k) │ │ (d → d_k) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ Linear K₁ │ │ Linear K₂ │ │ Linear K_h │
│ (d → d_k) │ │ (d → d_k) │ │ (d → d_k) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ Linear V₁ │ │ Linear V₂ │ │ Linear V_h │
│ (d → d_k) │ │ (d → d_k) │ │ (d → d_k) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│Attention₁(Q₁,K₁,V₁)│Attention₂(Q₂,K₂,V₂) ... Attention_h(Q_h,K_h,V_h)
│ (n×d_k → n×d_k) │ (n×d_k → n×d_k) (n×d_k → n×d_k)
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
┌───────────────────────────────────────────────────────┐
│ Concat(head₁, head₂, …, head_h) │ (n × (h d_k))
└───────────────────────────────────────────────────────┘
│
┌─────────▼─────────┐
│ Linear W^O │ ( (h d_k) → d )
└─────────┬─────────┘
│
输出 (n × d)
- 每个 Attention 头在不同子空间上进行投影与打分;
- 拼接后通过线性层整合各头的信息,得到最终的多头注意力输出。
4.4 位置编码(Positional Encoding)
自注意力是对序列中任意位置都能“直接注意”到,但它本身不具备捕获单词顺序(时序)信息的能力。为了解决这一点,Transformer 为输入添加了 位置编码,使模型在做注意力计算时能感知单词的相对/绝对位置。
正弦/余弦位置编码(原论文做法)
对于输入序列中第 $pos$ 个位置、第 $i$ 维维度,定义:
$$
\begin{aligned}
PE_{pos,\,2i} &= \sin\Bigl(\frac{pos}{10000^{2i/d_{\text{model}}}}\Bigr), \\
PE_{pos,\,2i+1} &= \cos\Bigl(\frac{pos}{10000^{2i/d_{\text{model}}}}\Bigr).
\end{aligned}
$$
可学习的位置编码
- 有些改进版本直接将位置编码当作可学习参数 $\mathrm{PE} \in \mathbb{R}^{n \times d\_{\text{model}}}$,在训练中共同优化。
- 其表达能力更强,但占用更多参数,对低资源场景可能不适。
- 位置编码可视化
import numpy as np
import matplotlib.pyplot as plt
def get_sinusoid_encoding_table(n_position, d_model):
"""生成 n_position×d_model 的正弦/余弦位置编码矩阵。"""
def get_angle(pos, i):
return pos / np.power(10000, 2 * (i//2) / d_model)
PE = np.zeros((n_position, d_model))
for pos in range(n_position):
for i in range(d_model):
angle = get_angle(pos, i)
if i % 2 == 0:
PE[pos, i] = np.sin(angle)
else:
PE[pos, i] = np.cos(angle)
return PE
# 可视化前 50 个位置、64 维位置编码的热力图
n_pos, d_model = 50, 64
PE = get_sinusoid_encoding_table(n_pos, d_model)
plt.figure(figsize=(10, 6))
plt.imshow(PE, cmap='viridis', aspect='auto')
plt.colorbar()
plt.title("Sinusoidal Positional Encoding (first 50 positions)")
plt.xlabel("Dimension")
plt.ylabel("Position")
plt.show()
- 上图横轴为编码维度 $i \in [0,63]$,纵轴为位置 $pos \in [0,49]$。可以看到正弦/余弦曲线在不同维度上呈现不同频率,从而让模型区分不同位置。
完整 Transformer 架构解析
5.1 Encoder(编码器)结构
一个标准的 Transformer Encoder 一般包含 $N$ 层相同的子层堆叠,每个子层由两个主要模块组成:
- Multi-Head Self-Attention
- Position-wise Feed-Forward Network(前馈网络)
同时,每个模块之后均有残差连接(Residual Connection)与层归一化(LayerNorm)。
Single Encoder Layer 结构图示:
输入 X (n × d)
│
┌────▼────┐
│ Multi- │
│ HeadAtt │
└────┬────┘
│
┌────▼────┐
│ Add & │
│ LayerNorm │
└────┬────┘
│
┌────▼────┐
│ Position- │
│ Feed-Forw │
└────┬────┘
│
┌────▼────┐
│ Add & │
│ LayerNorm │
└────┬────┘
│
输出 (n × d)
输入嵌入 + 位置编码
- 对原始单词序列进行嵌入(Embedding)操作得到 $X\_{\text{embed}} \in \mathbb{R}^{n \times d}$;
- 与对应位置的 $PE \in \mathbb{R}^{n \times d}$ 相加,得到最终输入 $X \in \mathbb{R}^{n \times d}$.
Multi-Head Self-Attention
- 将 $X$ 分别映射为 $Q, K, V$;
- 并行计算 $h$ 个头的注意力输出,拼接后线性映射回 $d$ 维;
- 输出记为 $\mathrm{MHA}(X) \in \mathbb{R}^{n \times d}$.
残差连接 + LayerNorm
- 残差连接:$\mathrm{Z}\_1 = \mathrm{LayerNorm}\bigl(X + \mathrm{MHA}(X)\bigr)$.
前馈全连接网络
对 $\mathrm{Z}1$ 做两层线性变换,通常中间维度为 $d{\mathrm{ff}} = 4d$:
$$
\mathrm{FFN}(\mathrm{Z}_1) = \max\Bigl(0,\, \mathrm{Z}_1 W_1 + b_1\Bigr)\, W_2 + b_2,
$$
其中 $W\_1 \in \mathbb{R}^{d \times d\_{\mathrm{ff}}}$,$W\_2 \in \mathbb{R}^{d\_{\mathrm{ff}} \times d}$;
- 输出 $\mathrm{FFN}(\mathrm{Z}\_1) \in \mathbb{R}^{n \times d}$.
残差连接 + LayerNorm
- 最终输出:$\mathrm{Z}\_2 = \mathrm{LayerNorm}\bigl(\mathrm{Z}\_1 + \mathrm{FFN}(\mathrm{Z}\_1)\bigr)$.
整个 Encoder 向后堆叠 $N$ 层后,将得到完整的编码表示 $\mathrm{EncOutput} \in \mathbb{R}^{n \times d}$.
5.2 Decoder(解码器)结构
Decoder 与 Encoder 类似,也包含 $N$ 个相同的子层,每个子层由三个模块组成:
- Masked Multi-Head Self-Attention
- Encoder-Decoder Multi-Head Attention
- Position-wise Feed-Forward Network
每个模块后同样有残差连接与层归一化。
Single Decoder Layer 结构图示:
输入 Y (m × d)
│
┌────▼─────┐
│ Masked │ ← Prev tokens 的 Masked Self-Attn
│ Multi-Head│
│ Attention │
└────┬─────┘
│
┌────▼─────┐
│ Add & │
│ LayerNorm│
└────┬─────┘
│
┌────▼──────────┐
│ Encoder-Decoder│ ← Query 来自上一步,Key&Value 来自 Encoder Output
│ Multi-Head │
│ Attention │
└────┬──────────┘
│
┌────▼─────┐
│ Add & │
│ LayerNorm│
└────┬─────┘
│
┌────▼──────────┐
│ Position-wise │
│ Feed-Forward │
└────┬──────────┘
│
┌────▼─────┐
│ Add & │
│ LayerNorm│
└────┬─────┘
│
输出 (m × d)
Masked Multi-Head Self-Attention
- 为保证解码时只能看到当前位置及之前的位置,使用掩码机制(Masking)将当前位置之后的注意力分数置为 $-\infty$,再做 Softmax。
- 这样,在生成时每个位置只能关注到当前位置及其之前,避免“作弊”。
Encoder-Decoder Multi-Head Attention
- Query 来自上一步的 Masked Self-Attn 输出;
- Key 和 Value 来自 Encoder 最后一层的输出 $\mathrm{EncOutput} \in \mathbb{R}^{n \times d}$;
- 作用是让 Decoder 在生成时能“查看”整个源序列的表示。
前馈网络(Feed-Forward)
- 与 Encoder 相同,先线性映射升维、ReLU 激活,再线性映射回原始维度;
- 残差连接与归一化后得到该层输出。
5.3 残差连接与层归一化(LayerNorm)
Transformer 在每个子层后使用 残差连接(Residual Connection),结合 Layer Normalization 保持梯度稳定,并加速收敛。
5.4 前馈全连接网络(Feed-Forward Network)
在每个 Encoder/Decoder 子层中,注意力模块之后都会紧跟一个两层前馈全连接网络(Position-wise FFN),其作用是对每个序列位置的表示进行更高维的非线性变换:
$$
\mathrm{FFN}(x) = \mathrm{ReLU}(x\, W_1 + b_1)\, W_2 + b_2,
$$
- 第一层将维度由 $d$ 提升到 $d\_{\mathrm{ff}}$(常取 $4d$);
- ReLU 激活后再线性映射回 $d$ 维;
- 每个位置独立计算,故称为“Position-wise”。
代码示例:从零实现简化版 Transformer
下面我们用 PyTorch 手把手实现一个简化版 Transformer,帮助你理解各模块的实现细节。
6.1 环境与依赖
# 建议 Python 版本 >= 3.7
pip install torch torchvision numpy matplotlib
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
6.2 Scaled Dot-Product Attention 实现
class ScaledDotProductAttention(nn.Module):
def __init__(self, d_k):
super(ScaledDotProductAttention, self).__init__()
self.scale = math.sqrt(d_k)
def forward(self, Q, K, V, mask=None):
"""
Q, K, V: (batch_size, num_heads, seq_len, d_k)
mask: (batch_size, 1, seq_len, seq_len) 或 None
"""
# Q @ K^T → (batch, heads, seq_q, seq_k)
scores = torch.matmul(Q, K.transpose(-2, -1)) / self.scale
# 如果有 mask,则将被 mask 的位置设为 -inf
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
# Softmax 获得 attention 权重 (batch, heads, seq_q, seq_k)
attn = F.softmax(scores, dim=-1)
# 加权 V 得到输出 (batch, heads, seq_q, d_k)
output = torch.matmul(attn, V)
return output, attn
d_k
是每个头的维度。mask
可用于解码器中的自注意力屏蔽未来位置,也可用于 padding mask。
6.3 Multi-Head Attention 实现
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, num_heads):
"""
d_model: 模型隐藏尺寸
num_heads: 注意力头数
"""
super(MultiHeadAttention, self).__init__()
assert d_model % num_heads == 0
self.d_model = d_model
self.num_heads = num_heads
self.d_k = d_model // num_heads
# Q, K, V 的线性层:将输入映射到 num_heads × d_k
self.W_Q = nn.Linear(d_model, d_model)
self.W_K = nn.Linear(d_model, d_model)
self.W_V = nn.Linear(d_model, d_model)
# 最后输出的线性映射
self.W_O = nn.Linear(d_model, d_model)
self.attention = ScaledDotProductAttention(self.d_k)
def split_heads(self, x):
"""
将 x 从 (batch, seq_len, d_model) → (batch, num_heads, seq_len, d_k)
"""
batch_size, seq_len, _ = x.size()
# 先 reshape,再 transpose
x = x.view(batch_size, seq_len, self.num_heads, self.d_k)
x = x.transpose(1, 2) # (batch, num_heads, seq_len, d_k)
return x
def combine_heads(self, x):
"""
将 x 从 (batch, num_heads, seq_len, d_k) → (batch, seq_len, d_model)
"""
batch_size, num_heads, seq_len, d_k = x.size()
x = x.transpose(1, 2).contiguous() # (batch, seq_len, num_heads, d_k)
x = x.view(batch_size, seq_len, num_heads * d_k) # (batch, seq_len, d_model)
return x
def forward(self, Q, K, V, mask=None):
"""
Q, K, V: (batch, seq_len, d_model)
mask: (batch, 1, seq_len, seq_len) 或 None
"""
# 1. 线性映射
q = self.W_Q(Q) # (batch, seq_len, d_model)
k = self.W_K(K)
v = self.W_V(V)
# 2. 划分 heads
q = self.split_heads(q) # (batch, heads, seq_len, d_k)
k = self.split_heads(k)
v = self.split_heads(v)
# 3. Scaled Dot-Product Attention
scaled_attention, attn_weights = self.attention(q, k, v, mask)
# scaled_attention: (batch, heads, seq_len, d_k)
# 4. 拼接 heads
concat_attention = self.combine_heads(scaled_attention) # (batch, seq_len, d_model)
# 5. 最后输出映射
output = self.W_O(concat_attention) # (batch, seq_len, d_model)
return output, attn_weights
split_heads
:将映射后的张量切分为多个头;combine_heads
:将多个头的输出拼接回原始维度;mask
可用于自注意力中屏蔽未来位置或填充区域。
6.4 位置编码实现
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
"""
d_model: 模型隐藏尺寸,max_len: 序列最大长度
"""
super(PositionalEncoding, self).__init__()
# 创建位置编码矩阵 PE (max_len, d_model)
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) # (max_len, 1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
# pos * 1/(10000^{2i/d_model})
pe[:, 0::2] = torch.sin(position * div_term) # 偶数维度
pe[:, 1::2] = torch.cos(position * div_term) # 奇数维度
pe = pe.unsqueeze(0) # (1, max_len, d_model)
# 将 pe 注册为 buffer,不参与反向传播
self.register_buffer('pe', pe)
def forward(self, x):
"""
x: (batch, seq_len, d_model)
"""
seq_len = x.size(1)
# 将位置编码加到输入嵌入上
x = x + self.pe[:, :seq_len, :]
return x
pe
在初始化时根据正弦/余弦函数预先计算好,并注册为 buffer,不参与梯度更新;- 在
forward
中,将前 seq_len
行位置编码与输入相加。
6.5 简化版 Encoder Layer
class EncoderLayer(nn.Module):
def __init__(self, d_model, num_heads, d_ff, dropout=0.1):
super(EncoderLayer, self).__init__()
self.mha = MultiHeadAttention(d_model, num_heads)
self.ffn = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.ReLU(),
nn.Linear(d_ff, d_model)
)
self.layernorm1 = nn.LayerNorm(d_model, eps=1e-6)
self.layernorm2 = nn.LayerNorm(d_model, eps=1e-6)
self.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
def forward(self, x, mask=None):
# Multi-Head Self-Attention
attn_output, _ = self.mha(x, x, x, mask) # (batch, seq_len, d_model)
attn_output = self.dropout1(attn_output)
out1 = self.layernorm1(x + attn_output) # 残差 + LayerNorm
# 前馈网络
ffn_output = self.ffn(out1) # (batch, seq_len, d_model)
ffn_output = self.dropout2(ffn_output)
out2 = self.layernorm2(out1 + ffn_output) # 残差 + LayerNorm
return out2
d_ff
通常取 $4 \times d\_{\text{model}}$;- Dropout 用于正则化;
- 两次 LayerNorm 分别位于 Attention 和 FFN 之后。
6.6 简化版 Decoder Layer
class DecoderLayer(nn.Module):
def __init__(self, d_model, num_heads, d_ff, dropout=0.1):
super(DecoderLayer, self).__init__()
self.mha1 = MultiHeadAttention(d_model, num_heads) # Masked Self-Attn
self.mha2 = MultiHeadAttention(d_model, num_heads) # Enc-Dec Attn
self.ffn = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.ReLU(),
nn.Linear(d_ff, d_model)
)
self.layernorm1 = nn.LayerNorm(d_model, eps=1e-6)
self.layernorm2 = nn.LayerNorm(d_model, eps=1e-6)
self.layernorm3 = nn.LayerNorm(d_model, eps=1e-6)
self.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
self.dropout3 = nn.Dropout(dropout)
def forward(self, x, enc_output, look_ahead_mask=None, padding_mask=None):
"""
x: (batch, target_seq_len, d_model)
enc_output: (batch, input_seq_len, d_model)
look_ahead_mask: 用于 Masked Self-Attn
padding_mask: 用于 Encoder-Decoder Attn 针对输入序列的填充
"""
# 1. Masked Multi-Head Self-Attention
attn1, attn_weights1 = self.mha1(x, x, x, look_ahead_mask)
attn1 = self.dropout1(attn1)
out1 = self.layernorm1(x + attn1)
# 2. Encoder-Decoder Multi-Head Attention
attn2, attn_weights2 = self.mha2(out1, enc_output, enc_output, padding_mask)
attn2 = self.dropout2(attn2)
out2 = self.layernorm2(out1 + attn2)
# 3. 前馈网络
ffn_output = self.ffn(out2)
ffn_output = self.dropout3(ffn_output)
out3 = self.layernorm3(out2 + ffn_output)
return out3, attn_weights1, attn_weights2
look_ahead_mask
用于遮蔽未来位置;padding_mask
用于遮蔽输入序列中的 padding 部分(在 Encoder-Decoder Attention 中);- Decoder Layer 有三个 LayerNorm 分别对应三个子层的残差连接。
6.7 完整 Transformer 模型组装
class SimpleTransformer(nn.Module):
def __init__(self,
input_vocab_size,
target_vocab_size,
d_model=512,
num_heads=8,
d_ff=2048,
num_encoder_layers=6,
num_decoder_layers=6,
max_len=5000,
dropout=0.1):
super(SimpleTransformer, self).__init__()
self.d_model = d_model
# 输入与输出的嵌入层
self.encoder_embedding = nn.Embedding(input_vocab_size, d_model)
self.decoder_embedding = nn.Embedding(target_vocab_size, d_model)
# 位置编码
self.pos_encoding = PositionalEncoding(d_model, max_len)
# Encoder 堆叠
self.encoder_layers = nn.ModuleList([
EncoderLayer(d_model, num_heads, d_ff, dropout)
for _ in range(num_encoder_layers)
])
# Decoder 堆叠
self.decoder_layers = nn.ModuleList([
DecoderLayer(d_model, num_heads, d_ff, dropout)
for _ in range(num_decoder_layers)
])
# 最后线性层映射到词表大小,用于计算预测分布
self.final_linear = nn.Linear(d_model, target_vocab_size)
def make_padding_mask(self, seq):
"""
seq: (batch, seq_len)
return mask: (batch, 1, 1, seq_len)
"""
mask = (seq == 0).unsqueeze(1).unsqueeze(2) # 假设 PAD token 索引为 0
# mask 的位置为 True 则表示要遮蔽
return mask # bool tensor
def make_look_ahead_mask(self, size):
"""
生成 (1, 1, size, size) 的上三角 mask,用于遮蔽未来时刻
"""
mask = torch.triu(torch.ones((size, size)), diagonal=1).bool()
return mask.unsqueeze(0).unsqueeze(0) # (1,1, size, size)
def forward(self, enc_input, dec_input):
"""
enc_input: (batch, enc_seq_len)
dec_input: (batch, dec_seq_len)
"""
batch_size, enc_len = enc_input.size()
_, dec_len = dec_input.size()
# 1. Encoder embedding + positional encoding
enc_embed = self.encoder_embedding(enc_input) * math.sqrt(self.d_model)
enc_embed = self.pos_encoding(enc_embed)
# 2. 生成 Encoder padding mask
enc_padding_mask = self.make_padding_mask(enc_input)
# 3. 通过所有 Encoder 层
enc_output = enc_embed
for layer in self.encoder_layers:
enc_output = layer(enc_output, enc_padding_mask)
# 4. Decoder embedding + positional encoding
dec_embed = self.decoder_embedding(dec_input) * math.sqrt(self.d_model)
dec_embed = self.pos_encoding(dec_embed)
# 5. 生成 Decoder masks
look_ahead_mask = self.make_look_ahead_mask(dec_len).to(enc_input.device)
dec_padding_mask = self.make_padding_mask(enc_input)
# 6. 通过所有 Decoder 层
dec_output = dec_embed
for layer in self.decoder_layers:
dec_output, attn1, attn2 = layer(dec_output, enc_output, look_ahead_mask, dec_padding_mask)
# 7. 最终线性映射
logits = self.final_linear(dec_output) # (batch, dec_seq_len, target_vocab_size)
return logits, attn1, attn2
- 输入与输出都先经过 Embedding + Positional Encoding;
- Encoder-Decoder 层中使用前文定义的
EncoderLayer
与 DecoderLayer
; - Mask 分为两部分:Decoder 的 look-ahead mask 和 Encoder-Decoder 的 padding mask;
- 最后输出词向量维度大小的 logits,用于交叉熵损失计算。
6.8 训练示例:机器翻译任务
下面以一个简单的“英法翻译”示例演示如何训练该简化 Transformer。由于数据集加载与预处理相对繁琐,以下示例仅演示关键训练逻辑,具体数据加载可使用类似 torchtext
或自定义方式。
import torch.optim as optim
# 超参数示例
INPUT_VOCAB_SIZE = 10000 # 英语词表大小
TARGET_VOCAB_SIZE = 12000 # 法语词表大小
D_MODEL = 512
NUM_HEADS = 8
D_FF = 2048
NUM_LAYERS = 4
MAX_LEN = 100
DROPOUT = 0.1
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 初始化模型
model = SimpleTransformer(
INPUT_VOCAB_SIZE,
TARGET_VOCAB_SIZE,
D_MODEL,
NUM_HEADS,
D_FF,
num_encoder_layers=NUM_LAYERS,
num_decoder_layers=NUM_LAYERS,
max_len=MAX_LEN,
dropout=DROPOUT
).to(device)
# 损失与优化器
criterion = nn.CrossEntropyLoss(ignore_index=0) # 假设 PAD token 索引为 0
optimizer = optim.Adam(model.parameters(), lr=1e-4)
def train_step(enc_batch, dec_batch, dec_target):
"""
enc_batch: (batch, enc_seq_len)
dec_batch: (batch, dec_seq_len) 输入给 Decoder,包括 <sos> 开头
dec_target: (batch, dec_seq_len) 真实目标,包括 <eos> 结尾
"""
model.train()
optimizer.zero_grad()
logits, _, _ = model(enc_batch, dec_batch) # (batch, dec_seq_len, target_vocab_size)
# 将 logits 与目标调整形状
loss = criterion(
logits.reshape(-1, logits.size(-1)),
dec_target.reshape(-1)
)
loss.backward()
optimizer.step()
return loss.item()
# 伪代码示例:训练循环
for epoch in range(1, 11):
total_loss = 0
for batch in train_loader: # 假设 train_loader 迭代器返回 (enc_batch, dec_batch, dec_target)
enc_batch, dec_batch, dec_target = [x.to(device) for x in batch]
loss = train_step(enc_batch, dec_batch, dec_target)
total_loss += loss
print(f"Epoch {epoch}, Loss: {total_loss/len(train_loader):.4f}")
train_loader
应返回三个张量:enc_batch
(源语言输入)、dec_batch
(目标语言输入,含 <sos>
)、dec_target
(目标语言标签,含 <eos>
);- 每轮迭代根据模型输出计算交叉熵损失并更新参数;
- 实际应用中,还需要学习率衰减、梯度裁剪等技巧以稳定训练。
图解:Transformer 各模块示意
7.1 自注意力机制示意图
输入序列(长度=4): Embedding+Positional Encoding
["I", "love", "NLP", "."] ↓ (4×d)
┌─────────────────────────────────────────────────────────────────┐
│ 输入矩阵 X (4×d) │
└─────────────────────────────────────────────────────────────────┘
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ Linear │ │ Linear │ │ Linear │
│ Q = XW^Q │ │ K = XW^K │ │ V = XW^V │
│ (4×d → 4×d_k) │ │ (4×d → 4×d_k) │ │ (4×d → 4×d_k) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ Split │ │ Split │ │ Split │
│ Heads: │ │ Heads: │ │ Heads: │
│ (4×d_k → num_heads × (4×d/h)) │ num_heads × (4×d/h) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
┌─────────────────────────────────────────────────────────────────┐
│ Scaled Dot-Product Attention for each head │
│ Attention(Q_i, K_i, V_i): │
│ scores = Q_i × K_i^T / √d_k; Softmax; output = scores×V_i │
└─────────────────────────────────────────────────────────────────┘
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ head₁: (4×d/h) │ head₂: (4×d/h) │ … head_h: (4×d/h) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
┌────────────────────────────────────────────────────┐
│ Concat(head₁, …, head_h) → (4×d_k × h = 4×d) │
└────────────────────────────────────────────────────┘
│
┌──────▼──────┐
│ Linear W^O │ (4×d → 4×d)
└──────┬──────┘
│
输出矩阵 (4×d)
- 上图以序列长度 4 为例,将 d 维表示映射到 $d\_k = d/h$ 后并行计算多头注意力,最后拼接再线性映射回 $d$ 维。
7.2 编码器—解码器整体流程图
源序列(英语): "I love NLP ."
↓ Tokenize + Embedding
↓ Positional Encoding
┌───────────────────────────────────────┐
│ Encoder Layer × N │
│ (Self-Attn → Add+Norm → FFN → Add+Norm) │
└───────────────────────────────────────┘
↓
Encoder 输出 (EncOutput) (n × d)
目标序列(法语): "J'aime le NLP ."
↓ Tokenize + Embedding
↓ Positional Encoding
┌───────────────────────────────────────┐
│ Decoder Layer × N (每层三步) │
│ 1. Masked Self-Attn → Add+Norm │
│ 2. Enc-Dec Attn → Add+Norm │
│ 3. FFN → Add+Norm │
└───────────────────────────────────────┘
↓
Decoder 输出 (DecOutput) (m × d)
↓ 线性层 + Softmax (target_vocab_size)
预测下一个单词概率分布
- 源序列进入 Encoder,多层自注意力捕获句内关系;
- Decoder 第一层做 Masked Self-Attention,只能关注目标序列已生成部分;
- 第二步做 Encoder-Decoder Attention,让 Decoder 查看 Encoder 提供的上下文;
- 最终经过前馈网络输出下一个词的概率。
7.3 位置编码可视化
在 4.4 节中,我们已经用代码示例展示了正弦/余弦位置编码的热力图。为了直观理解,回顾一下:
Sinusoidal Positional Encoding Heatmap- 纵轴:序列中的每个位置(从 0 开始);
- 横轴:隐藏表示的维度 $i$;
- 不同维度采用不同频率的正弦/余弦函数,确保位置信息在各个维度上交错分布。
Transformer 在 NLP 中的经典应用
8.1 机器翻译(Machine Translation)
Transformer 最初即为机器翻译设计,实验主要在 WMT 2014 英德、英法翻译数据集上进行:
- 性能:在 2017 年,该模型在 BLEU 分数上均超越当时最先进的 RNN+Attention 模型。
特点:
- 并行训练速度极快;
- 由于长程依赖捕捉能力突出,翻译长句表现尤为优异;
- 支持大规模预训练模型(如 mBART、mT5 等多语种翻译模型)。
示例:Hugging Face Transformers 应用机器翻译
from transformers import MarianMTModel, MarianTokenizer
# 以“英语→德语”为例,加载预训练翻译模型
model_name = 'Helsinki-NLP/opus-mt-en-de'
tokenizer = MarianTokenizer.from_pretrained(model_name)
model = MarianMTModel.from_pretrained(model_name)
def translate_en_to_de(sentence):
# 1. Tokenize
inputs = tokenizer.prepare_seq2seq_batch([sentence], return_tensors='pt')
# 2. 生成
translated = model.generate(**inputs, max_length=40)
# 3. 解码
tgt = [tokenizer.decode(t, skip_special_tokens=True) for t in translated]
return tgt[0]
src_sent = "Transformer models have revolutionized machine translation."
print("EN:", src_sent)
print("DE:", translate_en_to_de(src_sent))
- 上述示例展示了如何用预训练 Marian 翻译模型进行英语到德语翻译,感受 Transformer 在实际任务上的便捷应用。
8.2 文本分类与情感分析(Text Classification & Sentiment Analysis)
通过在 Transformer 编码器后接一个简单的线性分类头,可实现情感分类、主题分类等任务:
加载预训练 BERT(其实是 Transformer 编码器)
from transformers import BertTokenizer, BertForSequenceClassification
model_name = "bert-base-uncased"
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertForSequenceClassification.from_pretrained(model_name, num_labels=2)
微调示例
import torch
from torch.optim import AdamW
from torch.utils.data import DataLoader, Dataset
class TextDataset(Dataset):
def __init__(self, texts, labels, tokenizer, max_len):
self.texts = texts
self.labels = labels
self.tokenizer = tokenizer
self.max_len = max_len
def __len__(self):
return len(self.texts)
def __getitem__(self, idx):
text = self.texts[idx]
label = self.labels[idx]
encoding = self.tokenizer.encode_plus(
text,
add_special_tokens=True,
max_length=self.max_len,
padding='max_length',
truncation=True,
return_tensors='pt'
)
return {
'input_ids': encoding['input_ids'].squeeze(0),
'attention_mask': encoding['attention_mask'].squeeze(0),
'labels': torch.tensor(label, dtype=torch.long)
}
# 假设 texts_train、labels_train 已准备好
train_dataset = TextDataset(texts_train, labels_train, tokenizer, max_len=128)
train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True)
optimizer = AdamW(model.parameters(), lr=2e-5)
model.train()
for epoch in range(3):
total_loss = 0
for batch in train_loader:
input_ids = batch['input_ids'].to(model.device)
attention_mask = batch['attention_mask'].to(model.device)
labels = batch['labels'].to(model.device)
outputs = model(input_ids=input_ids, attention_mask=attention_mask, labels=labels)
loss = outputs.loss
loss.backward()
optimizer.step()
optimizer.zero_grad()
total_loss += loss.item()
print(f"Epoch {epoch+1}, Loss: {total_loss/len(train_loader):.4f}")
- 以上示例展示了如何在情感分类(IMDb 数据集等)上微调 BERT,BERT 本质上是 Transformer 的编码器部分,通过在顶端加分类头即可完成分类任务。
8.3 文本生成与摘要(Text Generation & Summarization)
Decoder 个性化的 Transformer(如 GPT、T5、BART)在文本生成、摘要任务中表现尤为突出:
示例:使用 Hugging Face 预训练 BART 做摘要任务
from transformers import BartTokenizer, BartForConditionalGeneration
# 加载预训练 BART 模型与分词器
model_name = "facebook/bart-large-cnn"
tokenizer = BartTokenizer.from_pretrained(model_name)
model = BartForConditionalGeneration.from_pretrained(model_name)
article = """
The COVID-19 pandemic has fundamentally altered the landscape of remote work,
with many companies adopting flexible work-from-home policies.
As organizations continue to navigate the challenges of maintaining productivity
and employee engagement, new technologies and management strategies are emerging
to support this transition.
"""
# 1. Encode 输入文章
inputs = tokenizer(article, max_length=512, return_tensors="pt", truncation=True)
# 2. 生成摘要(可调节 beam search 大小和摘要最大长度)
summary_ids = model.generate(
inputs["input_ids"],
num_beams=4,
max_length=80,
early_stopping=True
)
# 3. 解码输出
summary = tokenizer.decode(summary_ids[0], skip_special_tokens=True)
print("摘要:", summary)
- 运行后,BART 会输出一段简洁的文章摘要,展示 Transformer 在文本摘要领域的强大能力。
8.4 问答系统与对话生成(QA & Dialogue)
基于 Transformer 的预训练模型(如 BERT、RoBERTa、ALBERT、T5、GPT)已在问答与对话任务中被广泛应用:
检索式问答(Retrieval-based QA)
- 利用 BERT 对查询与一段文本进行编码,计算相似度以定位答案所在位置;
- 例如 SQuAD 数据集上,BERT Large 模型达到超过 90% 的 F1 分数。
生成式对话(Generative Dialogue)
- GPT 类模型通过自回归方式逐 token 生成回复;
- 使用对话上下文作为输入,模型自动学习上下文关联与回复策略;
- OpenAI ChatGPT、Google LaMDA 等都是这一范式的典型代表。
多任务联合训练
- 如 T5 可以将 QA、对话、翻译等任务都转化为文本—文本格式,通过一个统一框架处理多种任务。
优化与进阶:Transformers 家族演化
9.1 改进结构与高效注意力(Efficient Attention)
Transformer 原始自注意力计算为 $O(n^2)$,当序列长度 $n$ 非常大时会出现内存与算力瓶颈。为了解决这一问题,出现了多种高效注意力机制:
Sparse Attention
- 通过限制注意力矩阵为稀疏结构,只计算与相邻位置或特定模式有关的注意力分数;
- 例如 Longformer 的滑动窗口注意力(sliding-window attention)、BigBird 的随机+局部+全局混合稀疏模式。
Linformer
- 假设注意力矩阵存在低秩结构,将 Key、Value 做投影降维,使注意力计算复杂度从 $O(n^2)$ 降到 $O(n)$.
Performer
- 基于随机特征映射(Random Feature Mapping),将 Softmax Attention 近似为线性运算,时间复杂度降为 $O(n)$.
Reformer
- 通过局部敏感哈希(LSH)构建近似注意力,实现 $O(n \log n)$ 时间复杂度。
这些方法极大地拓宽了 Transformer 在超长序列(如文档级理解、多模态序列)上的应用场景。
9.2 预训练模型与微调范式(BERT、GPT、T5 等)
BERT(Bidirectional Encoder Representations from Transformers)
- 只采用编码器结构,利用Masked Language Modeling(MLM) 和 Next Sentence Prediction(NSP) 进行预训练;
- 其双向(Bidirectional)编码使得上下文理解更全面;
- 在 GLUE、SQuAD 等多项基准任务上刷新记录;
- 微调步骤:在下游任务(分类、问答、NER)上插入一个简单的线性层,联合训练整个模型。
GPT(Generative Pre-trained Transformer)
- 采用 Decoder-only 架构,进行自回归语言建模预训练;
- GPT-2、GPT-3 扩展到数十亿乃至数千亿参数,展现了强大的零/少样本学习能力;
- 在对话生成、文本续写、开放领域 QA、程序生成等任务中表现出众。
T5(Text-to-Text Transfer Transformer)
- 采用 Encoder-Decoder 架构,将所有下游任务都转化为文本—文本映射;
- 预训练任务为填空式(text infilling)和随机下采样(sentence permutation)、前向/后向预测等;
- 在多种任务上(如翻译、摘要、QA、分类)实现统一框架与端到端微调。
BART(Bidirectional and Auto-Regressive Transformers)
- 结合编码器—解码器与掩码生成,预训练目标包括文本破坏(text infilling)、删除随机句子、token 重排;
- 在文本摘要、生成式问答等任务中性能出色。
这些预训练范式为各类 NLP 任务提供了强大的“通用语言理解与生成”能力,使得构造少样本学习、跨领域迁移成为可能。
9.3 多模态 Transformer(Vision Transformer、Speech Transformer)
Vision Transformer(ViT)
- 将图像划分为若干固定大小的补丁(patch),将每个补丁视作一个“token”,然后用 Transformer 编码器对补丁序列建模;
- 预训练后在图像分类、目标检测、分割等任务上表现与卷积网络(CNN)相当,甚至更优。
Speech Transformer
- 用于语音识别(ASR)与语音合成(TTS)任务,直接对声谱图(spectrogram)等时频特征序列做自注意力建模;
- 相比传统的 RNN+Seq2Seq 结构,Transformer 在并行化与长程依赖捕捉方面具有显著优势;
Multimodal Transformer
- 将文本、图像、音频、视频等不同模态的信息联合建模,常见架构包括 CLIP(文本—图像对齐)、Flamingo(少样本多模态生成)、VideoBERT(视频+字幕联合模型)等;
- 在视觉问答(VQA)、图文检索、多模态对话系统等场景中取得突破性效果。
总结与最佳实践
掌握核心模块
- 理解并能实现 Scaled Dot-Product Attention 和 Multi-Head Attention;
- 熟练构造 Encoder Layer 和 Decoder Layer,掌握残差连接与 LayerNorm 细节;
- 了解位置编码的原理及其对捕捉序列顺序信息的重要性。
代码实现与调试技巧
- 在实现自注意力时,注意
mask
的维度与布尔值含义,避免注意力泄露; - 训练过程中常需要进行梯度裁剪(
torch.nn.utils.clip_grad_norm_
)、学习率预热与衰减、混合精度训练(torch.cuda.amp
)等操作; - 对于较大模型可使用分布式训练(
torch.nn.parallel.DistributedDataParallel
)或深度学习框架自带的高效实现,如 torch.nn.Transformer
、transformers
库等。
预训练与微调技巧
- 明确下游任务需求后,选择合适的预训练模型体系(Encoder-only、Decoder-only 或 Encoder-Decoder);
- 对任务数据进行合理预处理与增广;
- 微调时可冻结部分层,只训练顶层或新增层,尽量避免过拟合;
- 监控训练曲线,及时进行早停(Early Stopping)或调整学习率。
未来探索方向
- 高效注意力:研究如何在处理长文本、长音频、长视频时降低计算复杂度;
- 多模态融合:将 Transformer 从单一文本扩展到联合图像、音频、视频、多源文本等多模态场景;
- 边缘端与移动端部署:在资源受限环境中优化 Transformer 模型,如量化、剪枝、蒸馏等技术;
- 自监督与少样本学习:探索更高效的预训练目标与少样本学习范式,以降低对大规模标注数据的依赖。