从零手写Transformer_使用PyTorch实现简化版英汉机器翻译
在上一篇文章中,我们手动实现了 Transformer 的核心组件,并详细解析了其结构原理。本文将在此基础上,演示一个简化版的英汉机器翻译系统,完整呈现其训练与推理流程。我们将使用 PyTorch 逐步搭建整个流程,重点突出核心逻辑,代码简洁易懂,便于理解。
需要说明的是,本文的目标并非构建高精度的翻译系统,而是通过最小样本和最简配置,演示 Transformer 从数据准备到模型构建,再到生成翻译结果的完整闭环。
1. 准备数据:构建中英文平行语料
在训练 Transformer 模型之前,我们需要准备一组中英文对照的数据,并实现基本的编码器(Tokenizer),将文本转换为模型可读的数字序列。
1.1 构建中英文句子对
我们先手动构建一个小型中英文平行语料,数据仅用于结构验证,并非用于泛化或真实训练。
raw_data = [
("I love machine learning", "我 喜欢 机器 学习"),
("Deep learning is powerful", "深度 学习 很 强大"),
("Transformer changed everything", "Transformer 改变 了 一切"),
]
这里每个元组表示一组输入输出对,左边是英文句子,右边是中文翻译(已用空格分词,便于处理)。
1.2 构建词表
接下来我们为每种语言分别构建词表(Vocabulary),包括一些特殊标记:
def build_vocab(sentences, min_freq=1):
from collections import Counter
counter = Counter()
for s in sentences:
counter.update(s.split())
vocab = {"<pad>": 0, "<bos>": 1, "<eos>": 2, "<unk>": 3}
for word, freq in counter.items():
if freq >= min_freq and word not in vocab:
vocab[word] = len(vocab)
return vocab
# 拆分源语言和目标语言句子
src_sentences = [s[0].lower() for s in raw_data] # 英文转小写
tgt_sentences = [s[1] for s in raw_data] # 中文已分词
# 分别构建词表
src_vocab = build_vocab(src_sentences)
tgt_vocab = build_vocab(tgt_sentences)
每个词表中包含了 <pad>(填充)、<bos>(句首)、<eos>(句尾)、<unk>(未知词)等特殊 token,以便后续处理。
1.3 编码器(Tokenizer)
我们实现一个简单的编码器,将句子转为固定长度的 token ID 序列,并自动添加句首、句尾标记。
def encode(sentence, vocab, max_len=10):
tokens = sentence.lower().split()
ids = [vocab.get(tok, vocab["<unk>"]) for tok in tokens]
ids = [vocab["<bos>"]] + ids + [vocab["<eos>"]]
# 补齐或截断
if len(ids) < max_len:
ids += [vocab["<pad>"]] * (max_len - len(ids))
else:
ids = ids[:max_len]
return ids
1.4 编码中英文数据
将原始句子编码为固定长度的整数序列,作为模型的输入和目标输出:
input_seqs = [encode(s[0], src_vocab, max_len=10) for s in raw_data]
target_seqs = [encode(s[1], tgt_vocab, max_len=10) for s in raw_data]
至此,我们已经得到了:
- input_seqs:模型的输入序列(英文,编码后)
- target_seqs:模型的目标输出(中文,编码后)
它们都是长度为 10 的 token ID 列表,可以直接用于 Transformer 模型的训练。
2. 定义 Transformer 模型结构
我们将使用 PyTorch 搭建完整的 Transformer 模型,包括嵌入层、位置编码、注意力机制、前馈网络、编码器和解码器。
2.1 位置编码
在 Transformer 中,由于没有像 RNN 或 CNN 那样的顺序结构,模型本身无法感知序列中各个单词的先后顺序。因此,我们需要一种机制来显式地注入位置信息,使模型能够理解词与词之间的相对或绝对位置——这正是位置编码的作用。
下面是我们用 PyTorch 实现的位置编码模块:
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
super().__init__()
# 创建一个形状为 (max_len, d_model) 的全 0 矩阵
pe = torch.zeros(max_len, d_model)
# 生成每个位置的索引:0, 1, 2, ..., max_len-1
pos = torch.arange(0, max_len).unsqueeze(1)
# 计算每个维度对应的位置编码频率
div = torch.exp(torch.arange(0, d_model, 2) * -(torch.log(torch.tensor(10000.0)) / d_model))
# 将正弦函数应用于偶数维度
pe[:, 0::2] = torch.sin(pos * div)
# 将余弦函数应用于奇数维度
pe[:, 1::2] = torch.cos(pos * div)
# 增加一个 batch 维度,方便与输入相加
self.pe = pe.unsqueeze(0)
def forward(self, x):
# 将前 seq_len 个位置编码加到输入上
return x + self.pe[:, :x.size(1)].to(x.device)
位置编码不会直接告诉模型 「第几个词是主语」 或 「谁在前谁在后」,而是通过数学函数为每个位置生成一组独特的数值向量,这些向量中包含了能反映位置差异的数学规律,模型通过学习这些规律,逐渐建立起对词序的理解。
2.2 多头注意力机制
多头注意力机制是 Transformer 的核心模块之一。它的主要作用是:
- 并行从多个子空间(head)学习不同的注意力表示;
- 提高模型对不同语义关系的理解能力;
- 在序列的不同位置捕捉更丰富、更细腻的上下文依赖。
在这个模块中,输入的每个词向量会被映射为**查询(Q)、键(K)、值(V)**三组向量,通过它们计算注意力权重,再结合值向量得到最终的注意力输出。
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, num_heads):
super().__init__()
self.num_heads = num_heads
self.head_dim = d_model // num_heads
assert d_model % num_heads == 0, "d_model must be divisible by num_heads"
# 为 Q(查询)、K(键)、V(值)分别定义线性变换层
self.q_proj = nn.Linear(d_model, d_model)
self.k_proj = nn.Linear(d_model, d_model)
self.v_proj = nn.Linear(d_model, d_model)
# 最后的线性层,用于整合所有头的输出
self.out_proj = nn.Linear(d_model, d_model)
def forward(self, q, k, v, mask=None):
# 输入维度:q, k, v 均为 [batch_size, seq_len, d_model]
B, T_q, _ = q.shape
T_k = k.shape[1]
# 对 Q、K、V 做线性映射(注意:映射后维度仍是 d_model)
q = self.q_proj(q)
k = self.k_proj(k)
v = self.v_proj(v)
# 拆分多头:[B, T, D] → [B, num_heads, T, head_dim]
def split_heads(x):
return x.view(B, -1, self.num_heads, self.head_dim).transpose(1, 2)
q = split_heads(q)
k = split_heads(k)
v = split_heads(v)
# 缩放点积注意力
scores = torch.matmul(q, k.transpose(-2, -1)) / (self.head_dim ** 0.5)
# 如果提供了 mask,则屏蔽无效位置(如 <pad> 或未来信息)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
# 计算注意力权重(概率分布)
attn = torch.softmax(scores, dim=-1)
# 使用注意力权重加权值向量,获得每个位置的上下文表示
out = torch.matmul(attn, v)
# 合并多头:[B, num_heads, T, head_dim] → [B, T, D]
out = out.transpose(1, 2).contiguous().view(B, T_q, -1)
# 通过输出线性层整合信息,维度仍为 d_model
return self.out_proj(out)
2.3 前馈神经网络与 Transformer Block
在多头注意力之后,Transformer 还会对每个位置的表示单独进行一次非线性变换,这就是所谓的前馈神经网络(Feed-Forward Network,简称 FFN)。
这个模块其实非常简单:对每个位置上的向量先通过一个线性层映射到更高维度,激活之后再映射回来,增强模型的表达能力。
class FeedForward(nn.Module):
def __init__(self, d_model, d_ff):
super().__init__()
self.net = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.ReLU(),
nn.Linear(d_ff, d_model),
)
def forward(self, x):
return self.net(x)
class TransformerBlock(nn.Module):
def __init__(self, d_model, num_heads, d_ff):
super().__init__()
# 多头注意力
self.attn = MultiHeadAttention(d_model, num_heads)
# 前馈网络
self.ff = FeedForward(d_model, d_ff)
# 第一次 LayerNorm
self.norm1 = nn.LayerNorm(d_model)
# 第二次 LayerNorm
self.norm2 = nn.LayerNorm(d_model)
def forward(self, x, mask=None):
# 残差连接 + LayerNorm(注意力)
x = self.norm1(x + self.attn(x, x, x, mask))
# 残差连接 + LayerNorm(前馈)
x = self.norm2(x + self.ff(x))
return x
2.4 构建编码器和解码器
Transformer 模型由编码器(Encoder)和解码器(Decoder)组成。编码器处理源语言输入,提取上下文特征;解码器则根据编码器输出和之前生成的目标词语,逐步生成翻译结果。
2.4.1 编码器模块
编码器由多个 TransformerBlock 组成,每个 block 使用共享的参数结构。
class Encoder(nn.Module):
def __init__(self, vocab_size, d_model, num_heads, d_ff, num_layers, max_len=100):
super().__init__()
# 词嵌入层:将输入的词ID转为向量表示
self.embedding = nn.Embedding(vocab_size, d_model)
# 位置编码:添加序列中单词的位置信息
self.pos_enc = PositionalEncoding(d_model, max_len)
# 堆叠多个 TransformerBlock(多层编码器)
self.layers = nn.ModuleList([
TransformerBlock(d_model, num_heads, d_ff) for _ in range(num_layers)
])
def forward(self, x, mask=None):
# 输入 x: [batch_size, seq_len] -> 词ID序列
x = self.embedding(x)
x = self.pos_enc(x)
# 依次通过每一层 TransformerBlock
for layer in self.layers:
x = layer(x, mask)
return x
2.4.2 解码器模块(支持掩码)
解码器与编码器结构类似,但每个 TransformerBlock 包括两个注意力子层:
- Masked Self-Attention:防止模型 「看到」 未来词语;
- Encoder-Decoder Attention:使解码器能关注输入序列的编码结果。
class DecoderBlock(nn.Module):
def __init__(self, d_model, num_heads, d_ff):
super().__init__()
# 解码器中的三个子层:Masked Self-Attention、Encoder-Decoder Attention、前馈网络
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.enc_dec_attn = MultiHeadAttention(d_model, num_heads)
self.ff = FeedForward(d_model, d_ff)
# 每个子层后都跟 LayerNorm 和残差连接
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.norm3 = nn.LayerNorm(d_model)
def forward(self, x, enc_out, tgt_mask=None, src_mask=None):
# Masked Self-Attention:仅关注当前位置及之前的 token
x = self.norm1(x + self.self_attn(x, x, x, mask=tgt_mask))
# Encoder-Decoder Attention:让 decoder 看 encoder 的输出
x = self.norm2(x + self.enc_dec_attn(x, enc_out, enc_out, mask=src_mask))
# 前馈网络 + 残差连接 + LayerNorm
x = self.norm3(x + self.ff(x))
return x
class Decoder(nn.Module):
def __init__(self, vocab_size, d_model, num_heads, d_ff, num_layers, max_len=100):
super().__init__()
# 解码器的词嵌入层
self.embedding = nn.Embedding(vocab_size, d_model)
# 位置编码,表示目标序列的位置信息
self.pos_enc = PositionalEncoding(d_model, max_len)
# 堆叠多个 DecoderBlock(多层解码器)
self.layers = nn.ModuleList([
DecoderBlock(d_model, num_heads, d_ff) for _ in range(num_layers)
])
# 输出层,将 decoder 输出映射为词表大小,用于 softmax 预测词
self.fc_out = nn.Linear(d_model, vocab_size)
def forward(self, x, enc_out, tgt_mask=None, src_mask=None):
x = self.embedding(x)
x = self.pos_enc(x)
# 依次通过每一层 DecoderBlock
for layer in self.layers:
x = layer(x, enc_out, tgt_mask, src_mask)
# 最后输出分类得分,用于预测词
return self.fc_out(x)
2.5 整合完整的 Transformer 模型
我们将编码器和解码器组合为一个完整的 Transformer 模型:
class Transformer(nn.Module):
def __init__(self, src_vocab_size, tgt_vocab_size, d_model=128, num_heads=4, d_ff=512, num_layers=2, max_len=100):
super().__init__()
self.encoder = Encoder(src_vocab_size, d_model, num_heads, d_ff, num_layers, max_len)
self.decoder = Decoder(tgt_vocab_size, d_model, num_heads, d_ff, num_layers, max_len)
def forward(self, src, tgt, src_mask=None, tgt_mask=None):
enc_out = self.encoder(src, src_mask)
out = self.decoder(tgt, enc_out, tgt_mask, src_mask)
return out
3. 模型训练与掩码机制
在训练 Transformer 模型时,为了让模型正确学习序列依赖关系,我们需要构建合适的掩码(mask),分别用于:
- 源语言掩码:屏蔽掉
<pad>填充位,防止编码器在注意力中处理无效内容; - 目标语言掩码:构造下三角掩码,确保模型在预测每个词时只能访问该词之前的内容,防止信息泄露。
3.1 构建掩码函数
# 构造源语言的掩码:True 表示有效位置,False 表示 <pad>
def create_src_mask(src_tensor, pad_idx):
mask = (src_tensor != pad_idx).unsqueeze(1).unsqueeze(2)
return mask
# 构造目标语言的下三角掩码:防止看到未来词
def generate_subsequent_mask(size):
return torch.tril(torch.ones(size, size)).unsqueeze(0).unsqueeze(0)
下三角掩码本质上是一个下三角矩阵(lower triangular matrix),对角线以下为 1,其余为 0。
举例:若目标序列长度为 5,则掩码为:
[[1, 0, 0, 0, 0],
[1, 1, 0, 0, 0],
[1, 1, 1, 0, 0],
[1, 1, 1, 1, 0],
[1, 1, 1, 1, 1]]
这个矩阵的作用是:
- 第 1 个词只能看到自己;
- 第 2 个词可以看到前 2 个;
- 第 3 个词可以看到前 3 个;
- ……以此类推。
3.2 执行训练循环
我们使用**交叉熵损失函数(CrossEntropyLoss)**来训练翻译模型,其中 <pad> 会被忽略,以防止模型学习无效的填充部分:
import torch
import torch.nn as nn
import torch.optim as optim
# 使用 GPU(如果可用)
device = torch.device("cuda「if torch.cuda.is_available() else」cpu")
# 实例化模型:指定源语言和目标语言的词表大小
model = Transformer(
src_vocab_size=len(src_vocab),
tgt_vocab_size=len(tgt_vocab),
d_model=128,
num_heads=4,
d_ff=512,
num_layers=2
).to(device)
# 优化器:Adam 通常在 Transformer 中表现良好
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# 损失函数:忽略目标序列中的 <pad> 位置
loss_fn = nn.CrossEntropyLoss(ignore_index=tgt_vocab["<pad>"])
num_epochs = 100
for epoch in range(num_epochs):
model.train()
total_loss = 0
# 遍历每个样本(因数据量小,这里不使用批量训练)
for src_seq, tgt_seq in zip(input_seqs, target_seqs):
# 转换为张量并移动到设备
src_tensor = torch.tensor([src_seq], dtype=torch.long).to(device)
tgt_tensor = torch.tensor([tgt_seq], dtype=torch.long).to(device)
# 构造 decoder 的输入和目标
# 输入:去掉句末 <eos>
# 输出:去掉句首 <bos>
tgt_input = tgt_tensor[:, :-1]
tgt_output = tgt_tensor[:, 1:]
# 生成源语言的注意力掩码(mask <pad>),防止编码器注意力聚焦到无效位置
src_mask = create_src_mask(src_tensor, src_vocab["<pad>"]).to(device)
# 生成 mask:解码器自注意力中的下三角 mask(防止看到未来词)
tgt_mask = generate_subsequent_mask(tgt_input.size(1)).to(device)
# 前向传播
logits = model(src_tensor, tgt_input, src_mask=src_mask, tgt_mask=tgt_mask)
# 将输出 reshape 为 [batch*seq_len, vocab_size],与标签对齐
logits = logits.reshape(-1, logits.size(-1))
tgt_output = tgt_output.reshape(-1)
# 计算损失
loss = loss_fn(logits, tgt_output)
# 反向传播与优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 累计损失
total_loss += loss.item()
if (epoch + 1) % 10 == 0 or epoch == 0:
print(f"Epoch {epoch+1:>3}: Loss = {total_loss:.4f}")
4. 推理解码:贪婪搜索
模型训练完成后,我们希望能够输入一句英文,得到对应的中文翻译。这就需要构建一个推理过程。
我们采用最简单的**贪婪解码(Greedy Decoding)**策略:每一步选择当前概率最高的词,逐步生成完整句子,直到遇到 <eos> 或达到最大长度。
def greedy_decode(model, input_sentence, max_len=10):
model.eval()
# 将输入英文句子编码为 token ID 序列,并转为张量,形状:[1, seq_len]
input_ids = torch.tensor([encode(input_sentence, src_vocab)], dtype=torch.long).to(device)
# 在推理阶段关闭梯度计算,加快速度、节省显存
with torch.no_grad():
# 生成源语言的注意力掩码(mask <pad>),防止编码器注意力聚焦到无效位置
src_mask = create_src_mask(input_ids, src_vocab["<pad>"]).to(device)
# 1. 编码器部分:将输入序列传入编码器,得到上下文表示 enc_out
enc_out = model.encoder(input_ids, mask=src_mask)
# 初始化生成序列,起始 token 为 <bos>(句子开头)
decoded = [tgt_vocab["<bos>"]]
# 2. 解码器部分:逐步生成每一个目标词(最多 max_len 个)
for _ in range(max_len):
# 当前已生成的 token ID 序列作为 decoder 的输入
tgt_input = torch.tensor([decoded], dtype=torch.long).to(device)
# 为 decoder 自注意力生成下三角 mask(防止看到未来词)
tgt_mask = generate_subsequent_mask(tgt_input.size(1)).to(device)
# 将当前 decoder 输入和 encoder 输出传入 decoder,得到 logits
logits = model.decoder(tgt_input, enc_out, tgt_mask=tgt_mask)
# 获取当前时间步(最后一个 token)对应的预测概率最大值
next_token = logits[0, -1].argmax().item()
# 将预测 token 加入到已生成的序列中
decoded.append(next_token)
# 如果预测为 <eos>,表示句子结束,停止解码
if next_token == tgt_vocab["<eos>"]:
break
# 构建目标词表的反向映射:ID → 词
inv_vocab = {v: k for k, v in tgt_vocab.items()}
# 将生成的 token ID(去掉 <bos> 和 <eos>)转为词字符串并拼接
return " ".join([inv_vocab.get(tok, "<unk>") for tok in decoded[1:-1]])
5. 示例:英译中翻译效果
我们用训练过的模型尝试翻译一两个句子看看效果(即使是极简模型,也能感受到它的结构能力):
test_sentence = "I love machine learning"
output = greedy_decode(model, test_sentence)
print("Input:", test_sentence)
print("Output:", output)
输出结果:
Epoch 1: Loss = 12.8265
Epoch 10: Loss = 0.2459
Epoch 20: Loss = 0.0639
Epoch 30: Loss = 0.0415
Epoch 40: Loss = 0.0304
Epoch 50: Loss = 0.0237
Epoch 60: Loss = 0.0191
Epoch 70: Loss = 0.0158
Epoch 80: Loss = 0.0134
Epoch 90: Loss = 0.0114
Epoch 100: Loss = 0.0099
Input: I love machine learning
Output: 我 喜欢 机器 学习
当然,由于数据量小、模型简化,我们的目标不是达到实际应用水平,而是完整跑通 Transformer 推理链路,理解其生成机制。
6. 总结与未来方向
本文从零构建了一个简化版的 Transformer 英汉翻译系统,完整展示了从数据准备、模型搭建到训练与推理的关键流程。我们手动构造中英文句对,生成词表并实现编码器,将句子转换为固定长度的 token 序列;随后基于 PyTorch 实现了位置编码、多头注意力、前馈网络、编码器与解码器模块,最终组合为完整的 Transformer 模型。
尽管数据量和模型规模有限,本项目成功演示了 Transformer 在机器翻译任务中的基本原理和端到端流程。通过训练与贪婪解码,我们验证了模型的翻译能力,为进一步理解和实践更复杂的 Transformer 应用打下了基础。
写这篇文章花了我很多时间与心力,如果您觉得内容对您有所启发,欢迎点赞并关注「凝思 AI」——您的支持,是我持续创作优质内容的不竭动力!
专注于人工智能与语言、视觉、数据交汇处的前沿探索,涵盖大语言模型(LLMs)、多模态模型、智能体框架、机器学习方法,以及基于数据的潜在空间建模等。