从零实现Transformer:深入理解编码器-解码器架构与注意力机制
2026/8/21 13:23:33 网站建设 项目流程

在深入大模型和自然语言处理领域时,我们常常听到“注意力机制”是Transformer的核心。然而,很多学习者在理解了自注意力、多头注意力的原理后,面对一个完整的Transformer模型时,依然感到困惑:这些“零件”究竟是如何协同工作,最终构建出这个强大的架构的?本文将从一个工程化、系统化的视角,为你彻底拆解Transformer的搭建过程。我们将从最基础的编码器-解码器结构开始,一步步组装嵌入层、位置编码、多头注意力、前馈网络、残差连接与层归一化,最终形成一个可运行的模型。无论你是希望深入理解Transformer原理,还是计划从零实现一个模型用于自己的任务,这篇文章都将提供清晰的路径和可复现的代码。

1. Transformer架构全景:超越“注意力”的宏观视图

在深入细节之前,我们必须建立对Transformer的整体认知。Transformer并非一个单一的“注意力模块”,而是一个精心设计的、由多个子层堆叠而成的深度神经网络架构。

1.1 核心设计思想:编码器-解码器范式

Transformer沿用了机器翻译中经典的编码器-解码器(Encoder-Decoder)范式,但其内部完全基于自注意力机制,摒弃了循环神经网络(RNN)和卷积神经网络(CNN)。

  • 编码器(Encoder):负责处理输入序列(如源语言句子),并将其转换为一系列富含上下文信息的“中间表示”(Context Representation)。原始论文《Attention Is All You Need》中使用了N=6个完全相同的编码器层进行堆叠。
  • 解码器(Decoder):负责根据编码器的输出和已生成的部分输出序列(如目标语言已翻译的词),自回归地(Auto-regressively)预测下一个输出 token。解码器同样堆叠了N=6层。

这种设计使得模型能够并行处理整个输入序列,并有效捕捉长距离依赖关系,这是其相比RNN的巨大优势。

1.2 架构总览图与数据流

理解数据在模型中的流动路径至关重要。一个简化的工作流程如下:

  1. 输入嵌入:将输入的单词索引序列转换为稠密的向量表示。
  2. 位置编码:为序列中的每个位置生成一个独特的向量,并与词向量相加,以注入序列的顺序信息。
  3. 编码器堆叠:嵌入向量进入编码器层。每一层包含:
    • 多头自注意力子层:让序列中的每个词与其他所有词进行交互,计算全局依赖。
    • 前馈神经网络子层:对每个位置的表示进行独立的非线性变换。
    • 残差连接与层归一化:包围上述两个子层,用于稳定深度网络的训练。
  4. 解码器堆叠:编码器的输出与目标序列的嵌入(同样经过位置编码)进入解码器层。每一层包含:
    • 掩码多头自注意力子层:防止解码时“看到”未来的信息,确保自回归属性。
    • 多头交叉注意力子层:让解码器关注编码器的最终输出。
    • 前馈神经网络子层:与编码器中的相同。
    • 残差连接与层归一化:同样包围每个子层。
  5. 输出层:最后一个解码器层的输出经过一个线性层(通常维度映射到词表大小)和一个Softmax层,得到下一个词的概率分布。

2. 环境准备与核心工具

在动手搭建之前,我们需要准备好开发环境。本文将使用PyTorch作为主要的深度学习框架,因为它动态图的特点非常适合教学和原型设计。

2.1 环境配置

建议使用 Python 3.8+ 版本。首先创建一个虚拟环境并安装必要的包。

# 创建并激活虚拟环境 (可选) python -m venv transformer_env source transformer_env/bin/activate # Linux/Mac # transformer_env\Scripts\activate # Windows # 安装PyTorch (请根据你的CUDA版本前往官网获取对应命令) # 例如,对于CUDA 11.8 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 # 安装其他辅助库 pip install numpy matplotlib tqdm

2.2 项目结构规划

一个清晰的项目结构有助于管理复杂的模型代码。建议如下:

transformer_from_scratch/ ├── model.py # Transformer模型核心架构定义 ├── layers.py # 子层定义:注意力、前馈网络等 ├── embeddings.py # 词嵌入和位置编码 ├── train.py # 训练脚本 ├── config.py # 超参数配置 ├── utils.py # 工具函数(数据加载、掩码生成等) └── data/ # 存放示例数据

3. 基础“零件”的制造:从嵌入到注意力

让我们从最基础的组件开始实现。

3.1 词嵌入与位置编码

Transformer本身没有序列概念,需要通过位置编码(Positional Encoding)来注入顺序信息。

# embeddings.py import torch import torch.nn as nn import math class TokenEmbedding(nn.Module): """标准的词嵌入层""" def __init__(self, vocab_size, d_model): super().__init__() self.embedding = nn.Embedding(vocab_size, d_model) self.d_model = d_model def forward(self, x): # x: (batch_size, seq_len) # 乘以 sqrt(d_model) 是原始论文中的缩放操作 return self.embedding(x) * math.sqrt(self.d_model) class PositionalEncoding(nn.Module): """正弦余弦位置编码""" def __init__(self, d_model, max_len=5000, dropout=0.1): super().__init__() self.dropout = nn.Dropout(p=dropout) # 创建位置编码矩阵 (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)) 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) # 将其注册为buffer,不参与训练 def forward(self, x): # x: (batch_size, seq_len, d_model) x = x + self.pe[:, :x.size(1), :] # 只取前seq_len个位置 return self.dropout(x)

关键点register_buffer将位置编码矩阵注册为模型的“状态”,它会随模型移动(如从CPU到GPU),但不参与梯度更新。dropout在原始论文中被应用于嵌入和位置编码之和之后。

3.2 缩放点积注意力机制

这是多头注意力的核心计算单元。

# layers.py import torch import torch.nn as nn import math def scaled_dot_product_attention(query, key, value, mask=None, dropout=None): """ 计算缩放点积注意力。 Args: query, key, value: 形状均为 (batch_size, ..., seq_len, d_k) mask: 可选的掩码张量,形状需能广播到 (..., seq_len, seq_len) dropout: nn.Dropout层 Returns: 注意力加权后的输出,以及注意力权重 """ d_k = query.size(-1) # 计算 QK^T / sqrt(d_k) scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k) if mask is not None: # 将mask中为True/1的位置替换为一个极小的负数,使得softmax后概率接近0 scores = scores.masked_fill(mask == 0, -1e9) p_attn = scores.softmax(dim=-1) # (..., seq_len, seq_len) if dropout is not None: p_attn = dropout(p_attn) return torch.matmul(p_attn, value), p_attn

3.3 多头注意力层

将注意力机制并行化,允许模型同时关注来自不同表示子空间的信息。

# layers.py class MultiHeadAttention(nn.Module): """实现多头注意力机制""" def __init__(self, d_model, num_heads, 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) # W^Q self.w_k = nn.Linear(d_model, d_model) # W^K self.w_v = nn.Linear(d_model, d_model) # W^V self.w_o = nn.Linear(d_model, d_model) # 输出投影层 W^O self.dropout = nn.Dropout(dropout) self.attention = None # 可选:保存注意力权重用于可视化 def forward(self, query, key, value, mask=None): batch_size = query.size(0) # 1. 线性投影并分头 # (batch_size, seq_len, d_model) -> (batch_size, seq_len, num_heads, d_k) # 然后转置为 (batch_size, num_heads, seq_len, d_k) 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) # 2. 应用缩放点积注意力 # x: (batch_size, num_heads, seq_len, d_k) x, self.attention = scaled_dot_product_attention(Q, K, V, mask, self.dropout) # 3. 合并多头 # (batch_size, num_heads, seq_len, d_k) -> (batch_size, seq_len, d_model) x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model) # 4. 输出投影 return self.w_o(x)

为什么需要多头?单一注意力头可能只关注一种类型的模式(如语法、指代)。多个头允许模型在不同的子空间中并行学习不同的依赖关系,例如一个头关注局部语法,另一个头关注长距离的语义关联,从而增强模型的表达能力。

3.4 前馈网络与归一化

每个编码器和解码器层中都包含一个完全相同的前馈网络。

# layers.py class PositionwiseFeedForward(nn.Module): """位置逐点前馈网络,对每个位置独立应用相同的两层线性变换""" def __init__(self, d_model, d_ff, dropout=0.1): super().__init__() self.w_1 = nn.Linear(d_model, d_ff) # 第一层,通常扩大维度 self.w_2 = nn.Linear(d_ff, d_model) # 第二层,投影回d_model self.dropout = nn.Dropout(dropout) self.activation = nn.ReLU() # 原始论文使用ReLU def forward(self, x): # x: (batch_size, seq_len, d_model) return self.w_2(self.dropout(self.activation(self.w_1(x))))

残差连接与层归一化是稳定深度Transformer训练的关键。我们将它们实现为一个通用的子层连接模块。

# layers.py class SublayerConnection(nn.Module): """ 残差连接后接层归一化。 注意:原始论文是先进行层归一化,再进入子层(Pre-Norm)。 另一种常见变体是后归一化(Post-Norm)。 """ def __init__(self, size, dropout): super().__init__() self.norm = nn.LayerNorm(size) self.dropout = nn.Dropout(dropout) def forward(self, x, sublayer): """应用残差连接到任何与x相同形状的子层。""" # Pre-Norm: 先归一化,再经过子层和dropout,最后加残差 return x + self.dropout(sublayer(self.norm(x)))

4. 组装编码器与解码器层

有了基础零件,现在可以搭建编码器和解码器的单层结构。

4.1 编码器层

一个编码器层包含两个子层:多头自注意力和前馈网络,每个子层都被残差连接和层归一化包围。

# model.py import torch.nn as nn from layers import MultiHeadAttention, PositionwiseFeedForward, SublayerConnection class EncoderLayer(nn.Module): def __init__(self, d_model, num_heads, d_ff, dropout=0.1): super().__init__() self.self_attn = MultiHeadAttention(d_model, num_heads, dropout) self.feed_forward = PositionwiseFeedForward(d_model, d_ff, dropout) self.sublayer = nn.ModuleList([SublayerConnection(d_model, dropout) for _ in range(2)]) self.size = d_model def forward(self, x, mask): """ Args: x: (batch_size, src_seq_len, d_model) mask: (batch_size, 1, src_seq_len) 源序列的填充掩码 """ # 第一子层:多头自注意力 x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask)) # 第二子层:前馈网络 x = self.sublayer[1](x, self.feed_forward) return x

4.2 解码器层

解码器层更复杂,包含三个子层:掩码多头自注意力、多头交叉注意力和前馈网络。

# model.py class DecoderLayer(nn.Module): def __init__(self, d_model, num_heads, d_ff, 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.sublayer = nn.ModuleList([SublayerConnection(d_model, dropout) for _ in range(3)]) self.size = d_model def forward(self, x, memory, src_mask, tgt_mask): """ Args: x: (batch_size, tgt_seq_len, d_model) 解码器输入(已嵌入和位置编码的目标序列) memory: (batch_size, src_seq_len, d_model) 编码器的输出 src_mask: (batch_size, 1, src_seq_len) 源序列掩码 tgt_mask: (batch_size, tgt_seq_len, tgt_seq_len) 目标序列的因果掩码(防止看到未来) """ m = memory # 第一子层:掩码多头自注意力(关注已生成的目标序列) x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask)) # 第二子层:多头交叉注意力(关注编码器输出) x = self.sublayer[1](x, lambda x: self.cross_attn(x, m, m, src_mask)) # 第三子层:前馈网络 x = self.sublayer[2](x, self.feed_forward) return x

关键区别:解码器的第一个自注意力子层使用了tgt_mask(因果掩码),确保在预测位置i时,只能看到位置1i-1的信息。而交叉注意力子层则使用src_mask,确保解码器只关注源序列中的有效部分。

5. 构建完整的Transformer模型

现在,我们将编码器层、解码器层、嵌入层和输出层组合起来。

5.1 编码器与解码器堆叠

# model.py class Encoder(nn.Module): """N个编码器层的堆叠""" def __init__(self, layer, N): super().__init__() self.layers = nn.ModuleList([layer for _ in range(N)]) self.norm = nn.LayerNorm(layer.size) # 最后再加一个层归一化(Pre-Norm架构中有时可省略) def forward(self, x, mask): for layer in self.layers: x = layer(x, mask) return self.norm(x) class Decoder(nn.Module): """N个解码器层的堆叠""" def __init__(self, layer, N): super().__init__() self.layers = nn.ModuleList([layer for _ in range(N)]) self.norm = nn.LayerNorm(layer.size) def forward(self, x, memory, src_mask, tgt_mask): for layer in self.layers: x = layer(x, memory, src_mask, tgt_mask) return self.norm(x)

5.2 最终模型集成

# model.py from embeddings import TokenEmbedding, PositionalEncoding class Transformer(nn.Module): def __init__(self, src_vocab_size, tgt_vocab_size, N=6, d_model=512, d_ff=2048, num_heads=8, dropout=0.1, max_len=5000): super().__init__() # 1. 嵌入层 self.src_embed = nn.Sequential( TokenEmbedding(src_vocab_size, d_model), PositionalEncoding(d_model, max_len, dropout) ) self.tgt_embed = nn.Sequential( TokenEmbedding(tgt_vocab_size, d_model), PositionalEncoding(d_model, max_len, dropout) ) # 2. 编码器和解码器 encoder_layer = EncoderLayer(d_model, num_heads, d_ff, dropout) decoder_layer = DecoderLayer(d_model, num_heads, d_ff, dropout) self.encoder = Encoder(encoder_layer, N) self.decoder = Decoder(decoder_layer, N) # 3. 输出投影层 self.generator = nn.Linear(d_model, tgt_vocab_size) # 4. 参数初始化(重要!) self._init_parameters() def _init_parameters(self): """使用Xavier均匀初始化参数""" for p in self.parameters(): if p.dim() > 1: nn.init.xavier_uniform_(p) def forward(self, src, tgt, src_mask, tgt_mask): """ Args: src: (batch_size, src_len) 源语言序列索引 tgt: (batch_size, tgt_len) 目标语言序列索引(训练时输入,推理时逐步生成) src_mask: (batch_size, 1, src_len) 源序列填充掩码 tgt_mask: (batch_size, tgt_len, tgt_len) 目标序列因果掩码 Returns: logits: (batch_size, tgt_len, tgt_vocab_size) """ # 编码 memory = self.encode(src, src_mask) # 解码 output = self.decode(tgt, memory, src_mask, tgt_mask) # 生成logits logits = self.generator(output) return logits def encode(self, src, src_mask): src_embedded = self.src_embed(src) return self.encoder(src_embedded, src_mask) def decode(self, tgt, memory, src_mask, tgt_mask): tgt_embedded = self.tgt_embed(tgt) return self.decoder(tgt_embedded, memory, src_mask, tgt_mask)

5.3 掩码生成工具

Transformer需要两种掩码:填充掩码(Padding Mask)和因果掩码(Causal Mask)。

# utils.py import torch def create_padding_mask(seq, pad_idx=0): """为填充符创建掩码。seq中等于pad_idx的位置为0(需要被掩盖),否则为1。""" # seq: (batch_size, seq_len) mask = (seq != pad_idx).unsqueeze(1) # (batch_size, 1, seq_len) return mask def create_causal_mask(seq_len): """创建因果掩码(下三角矩阵),防止解码时看到未来信息。""" # 返回一个上三角为1,下三角(含对角线)为0的矩阵 mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1).bool() # 我们需要的是需要被掩盖的位置为True/1,所以取反?不,在scaled_dot_product_attention中,我们用mask==0来填充。 # 更常见的做法是生成一个下三角为False,上三角为True的矩阵。 mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1).bool() # 将其转换为需要被掩盖的位置为1 return mask.unsqueeze(0) # (1, seq_len, seq_len) 便于广播

6. 实战:一个简单的复制任务

为了验证我们的Transformer实现是否正确,最好的方法是运行一个简单的任务。我们设计一个“复制任务”:模型学习将输入序列原样输出。

6.1 生成模拟数据

# train.py (部分) import torch from torch.utils.data import Dataset, DataLoader class CopyDataset(Dataset): def __init__(self, num_samples=10000, seq_len=10, vocab_size=100): self.data = torch.randint(1, vocab_size, (num_samples, seq_len)) # 1是起始符,0是填充符 # 输入和输出相同 self.src = self.data self.tgt_input = self.data[:, :-1] # 解码器输入,去掉最后一个token self.tgt_output = self.data[:, 1:] # 解码器目标,去掉第一个token(起始符假设) def __len__(self): return len(self.data) def __getitem__(self, idx): return self.src[idx], self.tgt_input[idx], self.tgt_output[idx] def collate_fn(batch, pad_idx=0): src_batch, tgt_in_batch, tgt_out_batch = zip(*batch) src = torch.nn.utils.rnn.pad_sequence(src_batch, padding_value=pad_idx, batch_first=True) tgt_in = torch.nn.utils.rnn.pad_sequence(tgt_in_batch, padding_value=pad_idx, batch_first=True) tgt_out = torch.nn.utils.rnn.pad_sequence(tgt_out_batch, padding_value=pad_idx, batch_first=True) return src, tgt_in, tgt_out

6.2 训练循环

# train.py (续) from model import Transformer from utils import create_padding_mask, create_causal_mask import torch.nn as nn import torch.optim as optim from tqdm import tqdm def train_one_epoch(model, dataloader, optimizer, criterion, device, pad_idx): model.train() total_loss = 0 for src, tgt_in, tgt_out in tqdm(dataloader, desc='Training'): src, tgt_in, tgt_out = src.to(device), tgt_in.to(device), tgt_out.to(device) # 创建掩码 src_mask = create_padding_mask(src, pad_idx) tgt_len = tgt_in.size(1) tgt_mask = create_causal_mask(tgt_len).to(device) # 也需要为tgt_in创建填充掩码(如果序列长度不一) tgt_padding_mask = create_padding_mask(tgt_in, pad_idx) # 合并因果掩码和填充掩码 tgt_mask = tgt_mask & tgt_padding_mask # 逻辑与,两个掩码中任一为True的位置都需要被掩盖 # 前向传播 optimizer.zero_grad() logits = model(src, tgt_in, src_mask, tgt_mask) # (batch, tgt_len, vocab) # 计算损失 loss = criterion(logits.view(-1, logits.size(-1)), tgt_out.view(-1)) loss.backward() optimizer.step() total_loss += loss.item() return total_loss / len(dataloader) def main(): # 超参数 VOCAB_SIZE = 100 PAD_IDX = 0 BATCH_SIZE = 32 EPOCHS = 20 D_MODEL = 128 # 为了快速演示,使用较小维度 N_LAYERS = 2 N_HEADS = 4 D_FF = 512 DROPOUT = 0.1 LR = 0.0001 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') print(f'Using device: {device}') # 数据 dataset = CopyDataset(vocab_size=VOCAB_SIZE) dataloader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True, collate_fn=lambda b: collate_fn(b, PAD_IDX)) # 模型、损失、优化器 model = Transformer(src_vocab_size=VOCAB_SIZE, tgt_vocab_size=VOCAB_SIZE, N=N_LAYERS, d_model=D_MODEL, d_ff=D_FF, num_heads=N_HEADS, dropout=DROPOUT).to(device) criterion = nn.CrossEntropyLoss(ignore_index=PAD_IDX) # 忽略填充位置的损失 optimizer = optim.Adam(model.parameters(), lr=LR, betas=(0.9, 0.98), eps=1e-9) # 训练 for epoch in range(EPOCHS): avg_loss = train_one_epoch(model, dataloader, optimizer, criterion, device, PAD_IDX) print(f'Epoch {epoch+1:02d} | Loss: {avg_loss:.4f}') # 简单推理测试 model.eval() with torch.no_grad(): test_src = torch.tensor([[1, 2, 3, 4, 5]], device=device) # 假设1是起始符 # 贪心解码 max_len = 10 memory = model.encode(test_src, create_padding_mask(test_src, PAD_IDX).to(device)) ys = torch.ones(1, 1).fill_(1).long().to(device) # 起始符 for i in range(max_len-1): tgt_mask = create_causal_mask(ys.size(1)).to(device) out = model.decode(ys, memory, create_padding_mask(test_src, PAD_IDX).to(device), tgt_mask) prob = model.generator(out[:, -1:]) _, next_word = torch.max(prob, dim=-1) ys = torch.cat([ys, next_word], dim=1) if next_word.item() == PAD_IDX: # 遇到结束符则停止 break print(f"Input: {test_src.tolist()}") print(f"Output: {ys.tolist()}") if __name__ == '__main__': main()

运行此脚本,如果模型实现正确,经过若干轮训练后,损失应该会显著下降,并且在简单的复制任务上能取得不错的效果。这证明了我们的Transformer“骨架”是正确且可工作的。

7. 常见问题与调试技巧

在实现和训练Transformer时,你可能会遇到以下典型问题:

7.1 训练不收敛或损失为NaN

  • 原因1:梯度爆炸。这是深度网络常见问题。
    • 解决:使用梯度裁剪(torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0))。检查参数初始化是否正确(如使用Xavier初始化)。尝试降低学习率。
  • 原因2:学习率过高
    • 解决:使用更小的学习率(如1e-4或1e-5),并使用学习率预热(Warmup)策略,这是Transformer训练的标准技巧。
  • 原因3:掩码错误。错误的掩码可能导致注意力权重计算出错。
    • 解决:可视化你的src_masktgt_mask,确保需要被忽略的位置(如填充位、未来位)被正确设置为一个很大的负数(在softmax前)。

7.2 模型过拟合或欠拟合

  • 过拟合:训练损失下降,验证损失上升。
    • 解决:增加Dropout率,使用更多的数据增强,添加权重衰减(L2正则化),或简化模型(减少层数、头数、维度)。
  • 欠拟合:训练和验证损失都很高。
    • 解决:增加模型容量(更多层、更大d_model),训练更长时间,检查数据是否有问题,或降低正则化强度。

7.3 推理时输出无意义或重复

  • 原因:在自回归解码时,使用贪心解码容易陷入局部最优或重复循环。
    • 解决:使用束搜索(Beam Search)代替贪心解码。或者使用采样方法(如Top-k采样、核采样)增加多样性。同时,确保推理时tgt_mask是正确的因果掩码。

7.4 内存不足(OOM)

  • 原因:注意力矩阵的大小是序列长度的平方(seq_len * seq_len),对于长序列会消耗巨大内存。
    • 解决:对于极长序列,考虑使用Transformer-XL、Longformer、Reformer等变体,它们引入了稀疏注意力、分块注意力等机制。在训练时,可以减小批次大小(batch size)或序列长度。

8. 工程最佳实践与扩展方向

当你掌握了基础Transformer的搭建后,以下实践和方向能帮助你更好地应用于实际项目。

8.1 训练技巧

  1. 学习率预热与衰减:使用线性预热到某个学习率,然后按步数或轮数进行逆平方根衰减。这是训练稳定性的关键。
  2. 标签平滑(Label Smoothing):在计算交叉熵损失时,对真实标签进行平滑,可以防止模型对训练数据过度自信,提升泛化能力。
  3. 检查点(Checkpointing):定期保存模型状态和优化器状态,以便从中断处恢复训练。
  4. 混合精度训练:使用torch.cuda.amp进行自动混合精度训练,可以大幅减少GPU内存占用并加快训练速度。

8.2 架构变体与优化

  1. Pre-Norm vs Post-Norm:我们实现的是Pre-Norm(层归一化在子层前)。Post-Norm(层归一化在残差连接后)是原始论文的做法,但Pre-Norm通常训练更稳定。可以根据任务选择。
  2. 激活函数:尝试将ReLU替换为GELU,这在许多现代Transformer中表现更好。
  3. 注意力优化:了解Flash Attention等高效注意力实现,以加速长序列训练。
  4. 参数共享:可以在编码器和解码器的嵌入层与最终的输出投影层之间共享权重,以减少参数量。

8.3 应用于不同任务

Transformer是通用的序列到序列模型,通过调整输入输出,可应用于多种任务:

  • 机器翻译:标准的Seq2Seq应用,输入源语言,输出目标语言。
  • 文本摘要:输入长文档,输出摘要。
  • 代码生成:输入自然语言描述或部分代码,输出完整代码。
  • 图像分类(Vision Transformer):将图像分割成块,视为序列进行处理。
  • 语音识别:输入音频频谱图序列,输出文本序列。

搭建一个完整的Transformer模型,就像组装一台精密的仪器。注意力机制虽然是其最闪耀的“发动机”,但只有与嵌入层、位置编码、前馈网络、残差连接和层归一化这些“底盘”、“传动系统”和“控制系统”完美结合,才能发挥出真正的威力。希望这篇从零开始的构建指南,能帮助你不仅理解Transformer的每个零件,更掌握将它们组装成一个强大整体的方法和技能。下一步,你可以尝试在更复杂的数据集(如WMT翻译数据集)上训练它,或者将其作为基础模块,去探索BERT、GPT等更复杂的预训练模型架构。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询