在自然语言处理领域,从序列到序列的建模曾长期被循环神经网络(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 。这种串行性意味着:

  1. 无法并行 :必须等前一个时间步计算完成,才能计算下一个。
  2. 长程依赖衰减 :信息在多个时间步中传递,容易丢失或失真。

注意力机制(Attention Mechanism) 的引入是关键的突破。其核心思想是:当模型处理序列中的某个元素(如一个词)时,它可以直接“关注”序列中所有其他元素,并根据相关性动态地为这些元素分配不同的权重,从而直接捕获全局上下文信息,而无需经历漫长的顺序传递。

Transformer将这一思想发挥到极致,提出了 “自注意力”(Self-Attention) ,让序列中的每个元素都与其他所有元素进行交互,计算出一组新的、富含全局信息的表示。

1.2 Transformer 架构总览

Transformer是一个典型的编码器-解码器(Encoder-Decoder)架构,最初用于机器翻译等序列生成任务。

输入序列 -> [编码器] -> 中间表示 -> [解码器] -> 输出序列

其整体架构如下图所示(此处以文字描述替代图表):

  • 编码器(Encoder) :由N个(原论文N=6)完全相同的层堆叠而成。每一层包含两个子层:
    1. 多头自注意力机制(Multi-Head Self-Attention)
    2. 前馈神经网络(Position-wise Feed-Forward Network) 每个子层周围都应用了 残差连接(Residual Connection) 层归一化(Layer Normalization) 。即: 输出 = LayerNorm(x + Sublayer(x))
  • 解码器(Decoder) :同样由N个相同的层堆叠。每层包含 三个 子层:
    1. 掩码多头自注意力机制(Masked Multi-Head Self-Attention) :确保解码时当前位置只能关注到之前已生成的输出,这是自回归生成的关键。
    2. 编码器-解码器注意力机制(Encoder-Decoder Attention) :让解码器关注编码器的输出。
    3. 前馈神经网络(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实现中的关键技巧,主要有两种:

  1. Padding Mask :用于掩盖序列中无意义的填充符号( <pad> )。
  2. 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时,以下几点需要特别注意:

  1. 维度对齐检查 :这是最常见的错误来源。务必确保 d_model num_heads d_k d_v d_ff 等维度满足整除关系,并且各层输入输出形状匹配。在模型初始化后,用一个小批量数据跑一遍前向传播来验证形状。
  2. 掩码的正确应用
    • 训练阶段 :解码器必须使用因果掩码,防止信息泄露。
    • 推断阶段 :通常使用自回归的方式,每次生成一个词,因此也需要掩码。
    • Padding Mask :必须应用于所有涉及序列长度的注意力计算中。
  3. 初始化策略 :使用Xavier或Kaiming初始化。Transformer原论文使用了特定的初始化方式,但现代框架的默认初始化通常也能工作。对于深层模型,初始化尤为重要。
  4. 预热学习率 :务必使用带预热的学习率调度。跳过预热可能导致训练初期不稳定甚至发散。
  5. 梯度检查 :在训练初期,监控梯度的范数。如果出现NaN或无限大,检查学习率、初始化、数据(是否有异常值)和损失函数。
  6. 混合精度训练 :使用 torch.cuda.amp 进行自动混合精度训练,可以显著减少显存占用并加快训练速度,尤其对于大模型。
  7. 使用现有库 :对于生产环境,除非有极特殊的需求,否则优先使用高度优化和测试过的库,如Hugging Face的 transformers 库。它提供了数百个预训练模型和易用的接口。
  8. 理解计算复杂度 :自注意力的计算复杂度是序列长度的平方级(O(L²))。对于超长序列(如长文档、高分辨率图像),需要考虑使用稀疏注意力、线性注意力或类似Swin Transformer的窗口化方法。

Transformer以其优雅的设计和强大的性能,为深度学习模型架构树立了新的标杆。从理解其最基础的自注意力公式开始,到亲手实现每一个组件,再到掌握训练技巧和了解其变体,这个过程能让你真正把握这一核心技术的精髓。建议在理解本文代码的基础上,尝试在小型数据集(如IWSLT翻译数据集)上完成训练和评估,或使用Hugging Face库加载预训练模型进行微调,以积累实战经验。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐