从零实现Transformer:自注意力机制、多头注意力与位置编码详解
在自然语言处理领域,从序列到序列的建模曾长期被循环神经网络(RNN)及其变体LSTM、GRU所主导。然而,RNN固有的顺序计算特性,使其难以并行化,严重制约了模型训练的效率。当面对长序列时,梯度消失或爆炸问题也使得模型难以捕捉远距离的依赖关系。2017年,谷歌团队在论文《Attention Is All You Need》中提出的Transformer架构,彻底改变了这一局面。它完全摒弃了循环和卷积结构,仅依赖自注意力机制(Self-Attention)来建立序列中任意两个位置之间的全局依赖,实现了前所未有的并行计算能力和卓越的建模性能。
Transformer不仅是BERT、GPT等预训练大模型的基石,更在计算机视觉(Vision Transformer, Swin Transformer)、语音、多模态等领域大放异彩。无论你是希望深入理解当前大模型核心原理的开发者,还是准备在CV、NLP项目中应用Transformer的研究者,掌握其底层机制都至关重要。本文将系统性地拆解Transformer的每一个核心组件,从数学原理到代码实现,手把手带你构建一个可运行的简化版Transformer,并探讨其工程实践中的关键要点。
1. Transformer 核心思想与整体架构
在深入细节之前,我们首先要理解Transformer解决的核心问题及其设计哲学。
1.1 从RNN的瓶颈到注意力机制的崛起
RNN及其变体在处理序列时,需要按时间步依次计算。第t步的隐藏状态
h_t
依赖于
h_{t-1}
和当前输入
x_t
。这种串行性意味着:
- 无法并行 :必须等前一个时间步计算完成,才能计算下一个。
- 长程依赖衰减 :信息在多个时间步中传递,容易丢失或失真。
注意力机制(Attention Mechanism) 的引入是关键的突破。其核心思想是:当模型处理序列中的某个元素(如一个词)时,它可以直接“关注”序列中所有其他元素,并根据相关性动态地为这些元素分配不同的权重,从而直接捕获全局上下文信息,而无需经历漫长的顺序传递。
Transformer将这一思想发挥到极致,提出了 “自注意力”(Self-Attention) ,让序列中的每个元素都与其他所有元素进行交互,计算出一组新的、富含全局信息的表示。
1.2 Transformer 架构总览
Transformer是一个典型的编码器-解码器(Encoder-Decoder)架构,最初用于机器翻译等序列生成任务。
输入序列 -> [编码器] -> 中间表示 -> [解码器] -> 输出序列
其整体架构如下图所示(此处以文字描述替代图表):
-
编码器(Encoder)
:由N个(原论文N=6)完全相同的层堆叠而成。每一层包含两个子层:
- 多头自注意力机制(Multi-Head Self-Attention)
-
前馈神经网络(Position-wise Feed-Forward Network)
每个子层周围都应用了
残差连接(Residual Connection)
和
层归一化(Layer Normalization)
。即:
输出 = LayerNorm(x + Sublayer(x))。
-
解码器(Decoder)
:同样由N个相同的层堆叠。每层包含
三个
子层:
- 掩码多头自注意力机制(Masked Multi-Head Self-Attention) :确保解码时当前位置只能关注到之前已生成的输出,这是自回归生成的关键。
- 编码器-解码器注意力机制(Encoder-Decoder Attention) :让解码器关注编码器的输出。
- 前馈神经网络(Position-wise Feed-Forward Network) 每个子层同样有残差连接和层归一化。
- 输入/输出嵌入(Embedding) :将输入的词索引映射为稠密向量。
- 位置编码(Positional Encoding) :由于模型没有循环和卷积,无法感知序列顺序,因此必须显式地注入位置信息。
这个架构的核心在于,它通过堆叠多层自注意力层和前馈层,让信息在序列内部和编码器-解码器之间进行充分、高效的交互。
2. 环境准备与核心概念数学描述
在动手实现之前,我们需要明确一些数学符号和概念,并准备好开发环境。
2.1 关键符号与维度说明
假设我们有一个批次(batch)的输入序列:
-
批量大小(Batch Size)
:
B -
序列长度(Sequence Length)
:
L(对于编码器是L_src,解码器是L_tgt) -
模型维度(Model Dimension / Hidden Size)
:
d_model(原论文为512) -
词表大小(Vocabulary Size)
:
V
那么,输入经过词嵌入层后,形状为
(B, L, d_model)
。这是贯穿Transformer大部分计算的核心张量形状。
2.2 开发环境与工具
我们将使用PyTorch来实现一个简化版的Transformer,以便于理解。请确保你的环境已安装:
# 推荐使用Python 3.8+ 和 PyTorch 1.9+
pip install torch torchvision torchaudio
本文的代码示例将基于PyTorch框架。我们不会直接使用
torch.nn.Transformer
这个高度封装好的模块,而是从最基础的组件开始构建,以透彻理解其原理。
3. 核心组件原理与代码实现
现在,我们来逐一拆解并实现Transformer的每一个核心组件。
3.1 缩放点积注意力(Scaled Dot-Product Attention)
这是自注意力机制的核心计算公式。给定查询(Query)、键(Key)、值(Value)矩阵:
[ \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right) V ]
为什么需要缩放(除以 $\sqrt{d_k}$)? 假设Q和K的各个分量是独立随机变量,均值为0,方差为1。那么 $Q \cdot K$ 的均值为0,方差为 $d_k$。当 $d_k$ 很大时,点积的绝对值可能会变得非常大,将softmax函数推入梯度极小的饱和区,导致训练困难。缩放操作使得点积的方差重新回到1左右,稳定训练。
代码实现:
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
class ScaledDotProductAttention(nn.Module):
"""缩放点积注意力"""
def __init__(self, dropout=0.1):
super().__init__()
self.dropout = nn.Dropout(dropout)
def forward(self, query, key, value, mask=None):
# query, key, value 形状: (batch_size, num_heads, seq_len, d_k)
# mask 形状: (batch_size, 1, 1, seq_len) 或 (batch_size, 1, seq_len, seq_len)
d_k = query.size(-1) # 获取键向量的维度 d_k
# 计算 QK^T / sqrt(d_k)
scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
# 应用掩码(如果提供)
if mask is not None:
# 将mask中为True的位置(需要被掩盖)替换为一个非常大的负数,使得softmax后概率接近0
scores = scores.masked_fill(mask == 0, -1e9)
# 计算注意力权重
attn_weights = F.softmax(scores, dim=-1)
attn_weights = self.dropout(attn_weights)
# 加权求和得到输出
output = torch.matmul(attn_weights, value) # (batch_size, num_heads, seq_len, d_v)
return output, attn_weights # 返回输出和注意力权重(可用于可视化)
3.2 多头注意力机制(Multi-Head Attention)
单一的自注意力机制可能只关注到一种模式的依赖关系。多头注意力将
d_model
维的Q、K、V投影到
h
(头数)个不同的、维度更低的子空间(
d_k
,
d_v
,通常
d_k = d_v = d_model / h
),在每个子空间并行执行注意力函数,最后将结果拼接并投影回
d_model
维。
这样做的好处是模型可以同时关注来自不同表示子空间的信息。
[ \text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, ..., \text{head}_h) W^O ] [ \text{where head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V) ]
代码实现:
class MultiHeadAttention(nn.Module):
"""多头注意力机制"""
def __init__(self, d_model=512, num_heads=8, dropout=0.1):
super().__init__()
assert d_model % num_heads == 0, "d_model must be divisible by num_heads"
self.d_model = d_model
self.num_heads = num_heads
self.d_k = d_model // num_heads # 每个头的维度
# 定义线性投影层
self.W_q = nn.Linear(d_model, d_model) # 用于Query的投影
self.W_k = nn.Linear(d_model, d_model) # 用于Key的投影
self.W_v = nn.Linear(d_model, d_model) # 用于Value的投影
self.W_o = nn.Linear(d_model, d_model) # 输出投影
self.attention = ScaledDotProductAttention(dropout)
self.dropout = nn.Dropout(dropout)
self.layer_norm = nn.LayerNorm(d_model)
def forward(self, query, key, value, mask=None):
# query, key, value 形状: (batch_size, seq_len, d_model)
batch_size = query.size(0)
# 1. 线性投影并分头
Q = self.W_q(query).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
K = self.W_k(key).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
V = self.W_v(value).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
# 此时形状: (batch_size, num_heads, seq_len, d_k)
# 2. 应用缩放点积注意力
if mask is not None:
# 如果需要,将mask扩展到头维度 (batch_size, 1, seq_len) -> (batch_size, 1, 1, seq_len)
mask = mask.unsqueeze(1) # 适用于编码器的padding mask
# 对于解码器的双向mask,形状应为 (batch_size, 1, seq_len, seq_len)
attn_output, attn_weights = self.attention(Q, K, V, mask=mask)
# 3. 合并多头
attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
# 形状恢复为: (batch_size, seq_len, d_model)
# 4. 输出投影
output = self.W_o(attn_output)
output = self.dropout(output)
# 5. 残差连接与层归一化 (通常在EncoderLayer/DecoderLayer中完成,这里先返回未加残差的结果)
# output = self.layer_norm(query + output)
return output, attn_weights
3.3 位置编码(Positional Encoding)
由于自注意力机制是置换不变的(打乱输入顺序,输出仅顺序改变,但元素间关系不变),必须显式注入序列的顺序信息。原论文使用正弦和余弦函数来生成位置编码:
[ PE_{(pos, 2i)} = \sin(pos / 10000^{2i/d_{model}}) ] [ PE_{(pos, 2i+1)} = \cos(pos / 10000^{2i/d_{model}}) ]
其中
pos
是位置,
i
是维度。这种编码方式使得模型能够轻松学习到相对位置关系。
代码实现:
class PositionalEncoding(nn.Module):
"""位置编码"""
def __init__(self, d_model, max_len=5000, dropout=0.1):
super().__init__()
self.dropout = nn.Dropout(p=dropout)
# 计算位置编码矩阵
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))
pe[:, 0::2] = torch.sin(position * div_term) # 偶数维度用sin
pe[:, 1::2] = torch.cos(position * div_term) # 奇数维度用cos
pe = pe.unsqueeze(0) # (1, max_len, d_model) 便于广播
self.register_buffer('pe', pe) # 注册为缓冲区,不参与训练
def forward(self, x):
# x 形状: (batch_size, seq_len, d_model)
x = x + self.pe[:, :x.size(1), :] # 只取前seq_len个位置编码
return self.dropout(x)
3.4 前馈神经网络(Position-wise Feed-Forward Network)
这是一个应用于每个位置上的独立、相同的全连接前馈网络。它由两个线性变换和一个ReLU激活函数组成。
[ FFN(x) = \max(0, xW_1 + b_1)W_2 + b_2 ]
原论文中,中间层的维度
d_ff = 2048
,是
d_model=512
的4倍。这为模型提供了非线性变换能力。
代码实现:
class PositionwiseFeedForward(nn.Module):
"""位置前馈网络"""
def __init__(self, d_model=512, d_ff=2048, dropout=0.1):
super().__init__()
self.linear1 = nn.Linear(d_model, d_ff)
self.linear2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(dropout)
self.activation = nn.ReLU()
def forward(self, x):
# x 形状: (batch_size, seq_len, d_model)
return self.linear2(self.dropout(self.activation(self.linear1(x))))
3.5 编码器层(Encoder Layer)与解码器层(Decoder Layer)
现在我们将上述组件组合起来,构建编码器和解码器的单层结构。
编码器层实现:
class EncoderLayer(nn.Module):
"""Transformer编码器单层"""
def __init__(self, d_model=512, num_heads=8, d_ff=2048, dropout=0.1):
super().__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads, dropout)
self.feed_forward = PositionwiseFeedForward(d_model, d_ff, dropout)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
def forward(self, src, src_mask=None):
# src 形状: (batch_size, src_seq_len, d_model)
# src_mask 形状: (batch_size, 1, src_seq_len) 用于掩盖padding部分
# 子层1: 多头自注意力 + 残差 & 层归一化
attn_output, _ = self.self_attn(src, src, src, mask=src_mask)
src = src + self.dropout1(attn_output)
src = self.norm1(src)
# 子层2: 前馈网络 + 残差 & 层归一化
ff_output = self.feed_forward(src)
src = src + self.dropout2(ff_output)
src = self.norm2(src)
return src
解码器层实现:
class DecoderLayer(nn.Module):
"""Transformer解码器单层"""
def __init__(self, d_model=512, num_heads=8, d_ff=2048, dropout=0.1):
super().__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads, dropout)
self.cross_attn = MultiHeadAttention(d_model, num_heads, dropout) # 编码器-解码器注意力
self.feed_forward = PositionwiseFeedForward(d_model, d_ff, dropout)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.norm3 = nn.LayerNorm(d_model)
self.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
self.dropout3 = nn.Dropout(dropout)
def forward(self, tgt, memory, tgt_mask=None, memory_mask=None):
# tgt: 解码器输入 (batch_size, tgt_seq_len, d_model)
# memory: 编码器输出 (batch_size, src_seq_len, d_model)
# tgt_mask: 解码器自注意力掩码 (防止看到未来信息)
# memory_mask: 编码器-解码器注意力掩码 (通常与编码器输入的padding mask相同)
# 子层1: 掩码多头自注意力
attn_output1, _ = self.self_attn(tgt, tgt, tgt, mask=tgt_mask)
tgt = tgt + self.dropout1(attn_output1)
tgt = self.norm1(tgt)
# 子层2: 编码器-解码器注意力
attn_output2, _ = self.cross_attn(tgt, memory, memory, mask=memory_mask)
tgt = tgt + self.dropout2(attn_output2)
tgt = self.norm2(tgt)
# 子层3: 前馈网络
ff_output = self.feed_forward(tgt)
tgt = tgt + self.dropout3(ff_output)
tgt = self.norm3(tgt)
return tgt
4. 完整Transformer模型组装与实战示例
我们将编码器层、解码器层堆叠起来,并加上嵌入层、位置编码和最后的线性输出层,构建完整的Transformer模型。
4.1 构建完整Transformer类
class Transformer(nn.Module):
"""完整的Transformer模型(用于序列到序列任务)"""
def __init__(self, src_vocab_size, tgt_vocab_size, d_model=512, num_heads=8,
num_encoder_layers=6, num_decoder_layers=6, d_ff=2048, max_seq_len=5000, dropout=0.1):
super().__init__()
self.d_model = d_model
# 嵌入层
self.src_embedding = nn.Embedding(src_vocab_size, d_model)
self.tgt_embedding = nn.Embedding(tgt_vocab_size, d_model)
self.positional_encoding = PositionalEncoding(d_model, max_seq_len, dropout)
# 编码器堆叠
self.encoder_layers = nn.ModuleList([
EncoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_encoder_layers)
])
# 解码器堆叠
self.decoder_layers = nn.ModuleList([
DecoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_decoder_layers)
])
# 输出层
self.output_linear = nn.Linear(d_model, tgt_vocab_size)
# 初始化参数
self._init_parameters()
def _init_parameters(self):
"""使用Xavier初始化参数"""
for p in self.parameters():
if p.dim() > 1:
nn.init.xavier_uniform_(p)
def encode(self, src, src_mask):
# 源语言嵌入与位置编码
src_embedded = self.src_embedding(src) * math.sqrt(self.d_model) # 缩放嵌入
src_embedded = self.positional_encoding(src_embedded)
# 通过所有编码器层
enc_output = src_embedded
for layer in self.encoder_layers:
enc_output = layer(enc_output, src_mask)
return enc_output
def decode(self, tgt, memory, tgt_mask, memory_mask):
# 目标语言嵌入与位置编码
tgt_embedded = self.tgt_embedding(tgt) * math.sqrt(self.d_model)
tgt_embedded = self.positional_encoding(tgt_embedded)
# 通过所有解码器层
dec_output = tgt_embedded
for layer in self.decoder_layers:
dec_output = layer(dec_output, memory, tgt_mask, memory_mask)
return dec_output
def forward(self, src, tgt, src_mask=None, tgt_mask=None, memory_mask=None):
# src: 源序列索引 (batch_size, src_len)
# tgt: 目标序列索引 (batch_size, tgt_len)
# src_mask: 源序列padding掩码
# tgt_mask: 目标序列的因果掩码(防止看到未来)和padding掩码的组合
# memory_mask: 通常与src_mask相同
# 编码
memory = self.encode(src, src_mask)
# 解码
dec_output = self.decode(tgt, memory, tgt_mask, memory_mask)
# 线性投影到词表空间
output = self.output_linear(dec_output) # (batch_size, tgt_len, tgt_vocab_size)
return output
def generate_square_subsequent_mask(self, sz):
"""生成因果掩码(下三角为True,上三角为False),用于训练时掩盖未来信息"""
mask = (torch.triu(torch.ones(sz, sz)) == 1).transpose(0, 1)
mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
return mask # (sz, sz)
4.2 创建掩码(Mask)工具函数
掩码是Transformer实现中的关键技巧,主要有两种:
-
Padding Mask
:用于掩盖序列中无意义的填充符号(
<pad>)。 - Sequence Mask (Causal Mask) :用于解码器的自注意力,防止当前位置关注到未来的信息。
def create_padding_mask(seq, pad_idx=0):
"""创建padding掩码。seq中等于pad_idx的位置为0(需要被掩盖),否则为1。"""
# seq形状: (batch_size, seq_len)
mask = (seq != pad_idx).unsqueeze(1).unsqueeze(2) # (batch_size, 1, 1, seq_len)
return mask # 为1的位置是有效位置,为0的位置是padding位置
def create_combined_mask(tgt, pad_idx=0):
"""为解码器创建组合掩码(padding mask + 因果mask)"""
# tgt形状: (batch_size, tgt_len)
batch_size, tgt_len = tgt.shape
# 1. 创建padding mask
padding_mask = create_padding_mask(tgt, pad_idx) # (batch_size, 1, 1, tgt_len)
# 2. 创建因果掩码(下三角矩阵)
causal_mask = torch.tril(torch.ones(tgt_len, tgt_len)).bool() # (tgt_len, tgt_len)
causal_mask = causal_mask.unsqueeze(0).unsqueeze(0) # (1, 1, tgt_len, tgt_len)
# 3. 组合:只有padding mask和causal mask都为True的位置才需要关注
combined_mask = padding_mask & causal_mask # (batch_size, 1, tgt_len, tgt_len)
return combined_mask
4.3 运行一个简单的翻译示例
让我们用一个极简的模拟数据来验证模型的前向传播过程。
# 模拟参数
batch_size = 2
src_len = 5
tgt_len = 7
src_vocab_size = 100
tgt_vocab_size = 100
d_model = 512
# 创建模拟数据(假设pad_idx=0)
src_seq = torch.randint(1, src_vocab_size, (batch_size, src_len)) # 假设没有0(padding)
tgt_seq = torch.randint(1, tgt_vocab_size, (batch_size, tgt_len))
# 创建模型
model = Transformer(src_vocab_size, tgt_vocab_size, d_model=d_model)
# 创建掩码
src_mask = create_padding_mask(src_seq, pad_idx=0) # 这里src_seq没有0,所以mask全为True
# 对于解码器输入,我们通常用`tgt_seq`的前tgt_len-1个词作为输入,预测后tgt_len-1个词
# 这里简化,直接用完整序列
tgt_mask = create_combined_mask(tgt_seq, pad_idx=0)
# 前向传播
output = model(src_seq, tgt_seq, src_mask=src_mask, tgt_mask=tgt_mask)
print(f"模型输出形状: {output.shape}") # 应为 (batch_size, tgt_len, tgt_vocab_size)
print(f"输出示例(第一个序列的第一个词的概率分布): {output[0, 0, :10]}") # 查看前10个logits
运行上述代码,你应该能看到模型成功输出了正确形状的张量。这证明我们的模型组件连接正确,可以进行后续的训练。
5. 训练Transformer的常见问题与实战技巧
构建出模型只是第一步,成功训练一个Transformer需要关注许多细节。
5.1 学习率调度器(Warmup & Decay)
Transformer论文使用了带预热(Warmup)的学习率调度策略,这对于稳定训练至关重要。
[ \text{learning_rate} = d_{\text{model}}^{-0.5} \cdot \min(step_num^{-0.5}, step_num \cdot warmup_steps^{-1.5}) ]
实现示例:
class TransformerLRScheduler:
"""Transformer论文中的学习率调度器"""
def __init__(self, optimizer, d_model, warmup_steps=4000):
self.optimizer = optimizer
self.d_model = d_model
self.warmup_steps = warmup_steps
self.current_step = 0
def step(self):
self.current_step += 1
lr = self.d_model ** -0.5 * min(self.current_step ** -0.5, self.current_step * self.warmup_steps ** -1.5)
for param_group in self.optimizer.optimizer.param_groups:
param_group['lr'] = lr
return lr
# 使用示例
# optimizer = torch.optim.Adam(model.parameters(), lr=0, betas=(0.9, 0.98), eps=1e-9)
# scheduler = TransformerLRScheduler(optimizer, d_model=512, warmup_steps=4000)
# 在每个batch后调用 scheduler.step()
5.2 标签平滑(Label Smoothing)
在分类任务中,使用硬标签(0或1)容易导致模型过于自信和过拟合。标签平滑将真实标签的概率从1调整为
1 - ε
,并将
ε
均匀分给其他类别。
class LabelSmoothingLoss(nn.Module):
"""标签平滑交叉熵损失"""
def __init__(self, classes, padding_idx, smoothing=0.1, dim=-1):
super().__init__()
self.confidence = 1.0 - smoothing
self.smoothing = smoothing
self.classes = classes
self.dim = dim
self.padding_idx = padding_idx
def forward(self, pred, target):
pred = pred.log_softmax(dim=self.dim)
with torch.no_grad():
true_dist = torch.zeros_like(pred)
true_dist.fill_(self.smoothing / (self.classes - 2)) # 平滑到其他类
true_dist.scatter_(1, target.data.unsqueeze(1), self.confidence)
# 将padding位置的概率置为0
true_dist[:, self.padding_idx] = 0
mask = torch.nonzero(target.data == self.padding_idx)
if mask.dim() > 0:
true_dist.index_fill_(0, mask.squeeze(), 0.0)
return torch.mean(torch.sum(-true_dist * pred, dim=self.dim))
5.3 梯度裁剪(Gradient Clipping)
Transformer模型较深,梯度可能爆炸。在反向传播后、优化器更新前,对梯度范数进行裁剪是标准操作。
# 在训练循环中
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # 常用max_norm=1.0或5.0
optimizer.step()
5.4 批处理与填充(Batching & Padding)
序列长度不一致是NLP任务的常态。我们需要将一批序列填充到相同长度,并生成对应的
padding_mask
。可以使用PyTorch的
pad_sequence
和
pack_padded_sequence
(对于RNN)或自定义的mask机制(对于Transformer)。
from torch.nn.utils.rnn import pad_sequence
def collate_fn(batch):
# batch是一个列表,每个元素是(src_tensor, tgt_tensor)
src_batch, tgt_batch = zip(*batch)
# 填充源序列和目标序列
src_padded = pad_sequence(src_batch, batch_first=True, padding_value=0)
tgt_padded = pad_sequence(tgt_batch, batch_first=True, padding_value=0)
return src_padded, tgt_padded
# 在DataLoader中使用
# train_loader = DataLoader(dataset, batch_size=32, collate_fn=collate_fn, shuffle=True)
6. Transformer的变体与演进
原始的Transformer架构启发了无数后续工作,理解其变体有助于把握技术脉络。
6.1 仅编码器模型(Encoder-Only)
代表模型: BERT 。它只使用Transformer的编码器部分,通过掩码语言模型(MLM)和下一句预测(NSP)进行预训练,擅长理解任务(如文本分类、问答)。
6.2 仅解码器模型(Decoder-Only)
代表模型: GPT系列 。它只使用Transformer的解码器部分,并去掉了编码器-解码器注意力层。通过因果语言模型(自回归)进行预训练,擅长生成任务。
6.3 编码器-解码器模型(Encoder-Decoder)
代表模型: 原始Transformer、T5、BART 。保留完整结构,适用于需要同时理解和生成的任务,如翻译、摘要。
6.4 视觉Transformer(Vision Transformer, ViT)
将图像分割成固定大小的图块(patches),线性投影后加上位置编码,作为序列输入Transformer编码器。完全抛弃了卷积,在大规模数据上表现优异。
6.5 Swin Transformer
引入 滑动窗口(Shifted Windows) 和 层次化设计 ,在窗口内计算自注意力,大幅降低了计算复杂度,同时建立了像CNN一样的层次化特征图,非常适合作为视觉任务的通用骨干网络。
7. 工程最佳实践与避坑指南
在实际项目中应用Transformer时,以下几点需要特别注意:
-
维度对齐检查
:这是最常见的错误来源。务必确保
d_model、num_heads、d_k、d_v、d_ff等维度满足整除关系,并且各层输入输出形状匹配。在模型初始化后,用一个小批量数据跑一遍前向传播来验证形状。 -
掩码的正确应用
:
- 训练阶段 :解码器必须使用因果掩码,防止信息泄露。
- 推断阶段 :通常使用自回归的方式,每次生成一个词,因此也需要掩码。
- Padding Mask :必须应用于所有涉及序列长度的注意力计算中。
- 初始化策略 :使用Xavier或Kaiming初始化。Transformer原论文使用了特定的初始化方式,但现代框架的默认初始化通常也能工作。对于深层模型,初始化尤为重要。
- 预热学习率 :务必使用带预热的学习率调度。跳过预热可能导致训练初期不稳定甚至发散。
- 梯度检查 :在训练初期,监控梯度的范数。如果出现NaN或无限大,检查学习率、初始化、数据(是否有异常值)和损失函数。
-
混合精度训练
:使用
torch.cuda.amp进行自动混合精度训练,可以显著减少显存占用并加快训练速度,尤其对于大模型。 -
使用现有库
:对于生产环境,除非有极特殊的需求,否则优先使用高度优化和测试过的库,如Hugging Face的
transformers库。它提供了数百个预训练模型和易用的接口。 - 理解计算复杂度 :自注意力的计算复杂度是序列长度的平方级(O(L²))。对于超长序列(如长文档、高分辨率图像),需要考虑使用稀疏注意力、线性注意力或类似Swin Transformer的窗口化方法。
Transformer以其优雅的设计和强大的性能,为深度学习模型架构树立了新的标杆。从理解其最基础的自注意力公式开始,到亲手实现每一个组件,再到掌握训练技巧和了解其变体,这个过程能让你真正把握这一核心技术的精髓。建议在理解本文代码的基础上,尝试在小型数据集(如IWSLT翻译数据集)上完成训练和评估,或使用Hugging Face库加载预训练模型进行微调,以积累实战经验。
更多推荐
所有评论(0)