简介:这份资源是一套基于Python与Keras-Transformer实现的中英文机器翻译项目,面向做毕业设计、课程设计或希望入门神经机器翻译的学生与开发者。项目完整可跑,源码经过测试,适合在理解Transformer注意力机制的基础上直接复现并延伸改造。压缩包共17个文件,约7.42MB,包含3个py脚本与2个ipynb笔记本用于数据处理和训练翻译,1个h5权重文件保存训练好的模型,6个pkl文件存储源/目标词表及编解码中间数据,另有md说明文档与txt语料,结构清晰便于按模块阅读。目前已有363人学习。读者可拿到从语料预处理、词表构建到模型训练与推理的完整流程,并可与作者另一份基于LSTM的机器翻译项目对比,直观体会两种架构在相同原始数据上的差异,是快速上手Transformer翻译任务的实用参考。
1. 从一份能跑通的 Keras-Transformer 中英翻译工程说起
很多人做毕业设计卡在同一个地方:模型结构看懂了,论文里的注意力公式也能推,但真要把一份中英机器翻译的代码从零跑到能出结果,中间隔着一堆环境、数据、维度对齐的坑。这份基于 Python 和 Keras-Transformer 的中英翻译工程,解决的正是这个断层——它把分词、词表构建、位置编码、编码器解码器堆叠、训练循环、推理解码串成一条能直接跑的链路,附带源码和使用文档,适合课程设计、毕业设计这类需要在有限时间内交付一个完整可演示系统的场景。你不需要从 Attention is all you need 的公式开始手推,但需要理解每个模块的输入输出形状,否则调参和排错时会完全抓瞎。下面按「先立住原理、再动手复现、最后讲坑」的顺序拆开讲。
2. Keras-Transformer 翻译模型的结构与选型理由
2.1 为什么中英翻译用 Transformer 而不是 RNN
中英翻译是典型的序列到序列任务,输入中文、输出英文,两边长度不固定,且存在长距离依赖——比如中文里的主语可能隔了很长一段才对应到英文的谓语。早期做法用 LSTM 做编码器解码器,问题是串行计算,长句子里靠前的信息传到后面会被稀释,梯度也容易断。Transformer 用自注意力把任意两个位置直接连起来,路径长度是常数级,长句依赖不再靠一步步传递。
选 Keras 而不是 PyTorch,主要是毕业设计场景的现实考虑:Keras 的fit、compile、回调机制封装得比较完整,训练循环不用自己写,代码量小,答辩时讲清楚每一层在干什么比讲清楚训练循环的每个细节更容易。代价是自定义训练逻辑时不如 PyTorch 灵活,但对中英翻译这种标准任务,Keras 的Model子类化已经够用。
Transformer 的核心是缩放点积注意力,公式是 softmax(QK^T/√d_k)V。这里的 √d_k 是缩放因子,d_k 是每个头的维度。不缩放的话,点积结果随维度增大而变大,softmax 会推到饱和区,梯度接近零。多头注意力则是把 d_model 拆成 h 份,每份独立做注意力再拼接,让模型在不同子空间里关注不同模式——有的头关注语法结构,有的头关注词义对应。
2.2 编码器解码器的层数与维度怎么定
这份工程里编码器和解码器各堆叠若干层,每层包含多头自注意力、前馈网络、残差连接和层归一化。参数选择上,中英翻译这种中等规模任务,常见配置是 d_model=512、h=8、d_ff=2048、层数 4 到 6。d_model 决定词向量和隐藏状态的维度,h 是头数,d_ff 是前馈网络中间层维度,通常是 d_model 的 4 倍。
为什么是 4 倍?前馈网络先升维再降维,升维到 4 倍给模型足够的非线性表达能力,降回原维度保证残差相加时形状一致。层数不是越多越好,4 层在几万到几十万句对的数据量上已经能收敛,再深容易过拟合且训练慢。如果你的数据集只有一两万句,层数可以降到 2 到 3。
位置编码用正弦余弦函数生成,不参与训练。原因是自注意力本身没有顺序概念,打乱输入顺序结果不变,必须显式注入位置信息。正弦编码的好处是可以外推到训练时没见过的长度,虽然实际外推效果有限,但比可学习位置编码在短句上更稳。
import numpy as np def positional_encoding(max_len, d_model): # 生成位置编码矩阵,形状 (max_len, d_model) pos = np.arange(max_len)[:, np.newaxis] # (max_len, 1) i = np.arange(d_model)[np.newaxis, :] # (1, d_model) angle = pos / np.power(10000, (2 * (i // 2)) / np.float32(d_model)) # 偶数维度用 sin,奇数维度用 cos angle[:, 0::2] = np.sin(angle[:, 0::2]) angle[:, 1::2] = np.cos(angle[:, 1::2]) return angle[np.newaxis, ...] # (1, max_len, d_model)这段代码里max_len是最大序列长度,d_model是模型维度。i // 2保证同一对维度用同一个频率,偶数列 sin、奇数列 cos。返回时加了一个 batch 维度,方便后续和词嵌入相加。注意np.power的底数是 10000,这是原论文的设定,改小会让高频部分变化更快,改大则更平缓,一般不动。
2.3 词表构建与中英文分词策略
中文分词用 jieba,英文用空格加简单标点处理。词表分两个:中文词表和英文词表,各自统计词频,保留频率最高的若干词,其余映射到<unk>。特殊标记包括<pad>填充、<sos>序列开始、<eos>序列结束。
词表大小直接影响嵌入层参数量和内存占用。中英各 8000 到 12000 词是比较常见的范围。太小会导致大量<unk>,翻译质量下降;太大则嵌入层参数膨胀,小数据集上容易过拟合。构建词表时要注意:训练集和验证集必须用同一个词表,验证集里出现训练集没有的词一律当<unk>处理。
from collections import Counter import jieba def build_vocab(sentences, max_size, lang='zh'): counter = Counter() for sent in sentences: if lang == 'zh': tokens = list(jieba.cut(sent)) else: tokens = sent.lower().split() counter.update(tokens) # 保留最高频的 max_size-4 个词,留出特殊标记位置 vocab = {'<pad>': 0, '<unk>': 1, '<sos>': 2, '<eos>': 3} for word, _ in counter.most_common(max_size - 4): vocab[word] = len(vocab) return vocabmax_size控制词表上限,lang区分中英文分词方式。特殊标记固定占前四个 id,这样填充和未知词的位置在所有实验里一致,方便复现。most_common按词频降序取词,低频词直接丢弃。实际使用时还要建反向词表{id: word},推理阶段把模型输出的 id 转回文本。
3. 从零跑通训练:数据准备到模型编译
3.1 数据格式与批处理管道
数据用平行语料,每行一对中英文,中间用制表符或特定分隔符隔开。读取后分别做分词、转 id、截断或填充到固定长度。批处理时用tf.data.Dataset的padded_batch,它会自动把同一批里的序列填充到该批最长长度,比全局固定长度省显存。
import tensorflow as tf def make_dataset(zh_sentences, en_sentences, zh_vocab, en_vocab, batch_size, max_len): def encode(zh, en): zh_ids = [zh_vocab.get(w, 1) for w in jieba.cut(zh.numpy().decode())] en_ids = [en_vocab.get(w, 1) for w in en.numpy().decode().lower().split()] zh_ids = zh_ids[:max_len] en_ids = en_ids[:max_len] return zh_ids, en_ids dataset = tf.data.Dataset.from_tensor_slices((zh_sentences, en_sentences)) dataset = dataset.map(lambda z, e: tf.py_function(encode, [z, e], [tf.int32, tf.int32])) dataset = dataset.padded_batch(batch_size, padded_shapes=([None], [None]), padding_values=(0, 0)) return dataset.prefetch(tf.data.AUTOTUNE)max_len是单句最大长度,超出截断。padded_batch的padding_values用 0 对应<pad>。prefetch让数据准备和模型计算重叠,减少 GPU 等待。注意tf.py_function会打断图模式执行,数据量大时建议提前把 id 序列存成文件,训练时直接读,避免每次 epoch 都重新分词。
3.2 模型编译与损失函数选择
翻译任务输出是每个位置上的词分类,用稀疏交叉熵损失。关键点是 padding 位置不参与损失计算,否则模型会学会预测<pad>而忽略真实词。Keras 里通过sample_weight或者自定义损失函数把 padding 位置的权重置零。
import tensorflow as tf from tensorflow.keras import layers, Model class TransformerBlock(layers.Layer): def __init__(self, d_model, num_heads, d_ff, dropout=0.1): super().__init__() self.att = layers.MultiHeadAttention(num_heads=num_heads, key_dim=d_model) self.ffn = tf.keras.Sequential([ layers.Dense(d_ff, activation='relu'), layers.Dense(d_model) ]) self.norm1 = layers.LayerNormalization(epsilon=1e-6) self.norm2 = layers.LayerNormalization(epsilon=1e-6) self.drop1 = layers.Dropout(dropout) self.drop2 = layers.Dropout(dropout) def call(self, x, training, mask=None): attn = self.att(x, x, x, attention_mask=mask) x = self.norm1(x + self.drop1(attn, training=training)) ffn = self.ffn(x) return self.norm2(x + self.drop2(ffn, training=training))d_model是模型维度,num_heads是头数,d_ff是前馈网络中间维度。epsilon=1e-6防止归一化时分母为零。残差连接加在归一化之前还是之后有不同变体,这里用的是 Post-LN,训练时学习率要配合 warmup,否则初期梯度不稳定。attention_mask在解码器里用来遮住未来位置,防止预测时看到答案。
编译时优化器用 Adam,学习率按d_model^-0.5缩放再配合 warmup 调度。初始学习率设太大容易发散,太小收敛慢。常见做法是前 4000 步线性升温到峰值,之后按步数平方根倒数衰减。
3.3 训练循环与检查点保存
Keras 的model.fit配合ModelCheckpoint回调可以自动保存验证损失最低的权重。中英翻译训练通常几十个 epoch 才收敛,中途中断是常事,检查点就是后悔药。
callbacks = [ tf.keras.callbacks.ModelCheckpoint( 'best_model.h5', monitor='val_loss', save_best_only=True, verbose=1), tf.keras.callbacks.EarlyStopping( monitor='val_loss', patience=5, restore_best_weights=True), tf.keras.callbacks.ReduceLROnPlateau( monitor='val_loss', factor=0.5, patience=2, min_lr=1e-5) ] model.compile(optimizer=adam_with_warmup, loss=masked_loss, metrics=['accuracy']) model.fit(train_ds, validation_data=val_ds, epochs=50, callbacks=callbacks)patience=5表示验证损失连续 5 个 epoch 不降就停,restore_best_weights把权重回滚到最优时刻。ReduceLROnPlateau在损失停滞时把学习率减半,帮助跳出局部平台。注意masked_loss要自己实现,把目标序列里<pad>位置的损失屏蔽掉,否则准确率指标会被大量填充位置拉高,看起来很好但实际翻译质量差。
4. 推理与解码:把模型输出变成英文句子
4.1 贪心解码与束搜索的取舍
训练完模型只是第一步,推理时怎么从概率分布里选出词序列同样影响最终效果。贪心解码每步选概率最大的词,快但容易陷入局部最优,一个词选错后面全歪。束搜索保留 top-k 个候选序列,每步扩展再剪枝,质量更好但计算量成倍增加。
def beam_search(model, zh_input, en_vocab, inv_en_vocab, beam_width=3, max_len=50): start_token = en_vocab['<sos>'] end_token = en_vocab['<eos>'] # 每个候选是 (序列, 累计对数概率) beams = [([start_token], 0.0)] for _ in range(max_len): candidates = [] for seq, score in beams: if seq[-1] == end_token: candidates.append((seq, score)) continue # 编码器输出可以缓存,这里简化每次重算 preds = model.predict([zh_input, np.array([seq])], verbose=0) probs = preds[0, -1, :] top_k = np.argsort(probs)[-beam_width:] for token in top_k: candidates.append((seq + [token], score + np.log(probs[token] + 1e-9))) # 按分数排序保留 beam_width 个 candidates.sort(key=lambda x: x[1], reverse=True) beams = candidates[:beam_width] if all(seq[-1] == end_token for seq, _ in beams): break best_seq = max(beams, key=lambda x: x[1])[0] return ' '.join(inv_en_vocab.get(t, '<unk>') for t in best_seq[1:-1])beam_width是束宽,3 到 5 比较常用,再大收益递减且慢。np.log把概率连乘转成对数相加,防止数值下溢。1e-9是平滑项,避免 log(0)。实际部署时编码器对输入中文只算一次,解码每步复用,这里为了代码清晰每次重算,效率不是最优。inv_en_vocab是 id 到英文词的反向映射,输出时跳过<sos>和<eos>。
4.2 BLEU 评估与人工检查
自动评估用 BLEU,它统计模型输出和参考译文的 n-gram 重叠率。BLEU 高不一定代表翻译好,但低一定有问题。计算时用nltk.translate.bleu_score或者自己实现,注意对短句要加 brevity penalty,否则短输出会占便宜。
人工检查更直接:挑几十个验证集句子,把模型输出和参考译文并排看。常见问题是重复生成同一个词、漏译、语序颠倒。重复生成通常是解码时没正确遮住已生成位置,或者训练时<eos>学得不好。漏译往往是输入被截断,长句只翻译了前半段。
from nltk.translate.bleu_score import corpus_bleu def evaluate_bleu(model, val_ds, inv_en_vocab): references, hypotheses = [], [] for zh_batch, en_batch in val_ds: for i in range(zh_batch.shape[0]): ref = [inv_en_vocab.get(t, '<unk>') for t in en_batch[i].numpy() if t != 0] hyp = beam_search(model, zh_batch[i:i+1], en_vocab, inv_en_vocab) references.append([ref]) hypotheses.append(hyp.split()) return corpus_bleu(references, hypotheses)references是参考译文的列表,每个元素是一个列表因为可能有多份参考。hypotheses是模型输出。corpus_bleu对整个验证集算一个总分。注意参考译文里要去掉 padding 的 0,否则会拉低分数。BLEU 在 0.2 到 0.3 之间对中英翻译算正常,低于 0.1 基本说明模型没学好。
5. 避坑与排查:训练翻译模型时最容易翻车的地方
5.1 损失不降或变成 NaN
现象:训练几个 batch 后 loss 变成 nan,或者一直停在 10 以上不降。原因通常是学习率太大,或者位置编码和词嵌入相加时维度不匹配导致广播错误。解决:先把学习率降到 1e-4 试,确认 warmup 调度生效;检查d_model是否和词嵌入维度一致,位置编码返回的形状是不是(1, max_len, d_model)。另一个隐蔽原因是标签里有超出词表范围的 id,交叉熵计算时索引越界。
5.2 模型输出全是<unk>或重复词
现象:推理时生成的英文全是<unk>,或者反复输出同一个词直到达到最大长度。原因一是词表太小,训练集里大量词被映射到<unk>,模型没学到有效映射;二是解码时<eos>的概率一直很低,模型不知道什么时候停。解决:扩大词表到 12000 以上,检查训练数据里<unk>的比例;在损失函数里给<eos>位置更高权重,或者在解码时如果连续生成相同词超过阈值就强制结束。
5.3 验证集损失比训练集低
现象:验证 loss 低于训练 loss,看起来反常。原因通常是训练时 dropout 开启、验证时关闭,训练 loss 被 dropout 拉高;或者训练集和验证集分布差异大,验证集句子更短更简单。解决:确认training标志在验证阶段是 False;检查两个集合的句子长度分布,如果差异大要重新划分数据。这个现象本身不一定是 bug,但需要解释清楚。
5.4 显存溢出
现象:训练到一半报 OOM。原因一是 batch size 太大,二是max_len设得太长,注意力矩阵是max_len × max_len,长度翻倍显存翻四倍。解决:把 batch size 减半,或者把max_len从 100 降到 50;用padded_batch而不是全局固定长度,能省不少显存。如果还不够,把d_model从 512 降到 256,层数从 6 降到 4。
5.5 推理速度慢到无法演示
现象:束搜索解码一句话要好几秒,答辩演示时卡住。原因是每步都重新跑编码器,或者 beam_width 设得太大。解决:把编码器输出缓存下来,解码时只跑解码器;beam_width 从 5 降到 3;如果还慢,演示时用贪心解码,提前说明束搜索效果更好但为了流畅用贪心。另外把模型设成 eval 模式,关掉 dropout 和梯度计算。
6. 让翻译质量再上一档的几个实操技巧
训练能跑通之后,真正拉开差距的是数据质量和解码策略。我自己的习惯是先把训练数据里的噪声清一遍:全角半角统一、多余空格去掉、中英文标点统一成半角。这一步看起来琐碎,但能明显减少<unk>和乱码输出。另一个技巧是标签平滑,把目标概率从 1.0 降到 0.9,剩余概率均分给其他词,能缓解模型过度自信,BLEU 通常能涨一两个点。
def label_smoothing_loss(y_true, y_pred, smoothing=0.1, vocab_size=10000): # y_true 形状 (batch, seq_len),y_pred 形状 (batch, seq_len, vocab_size) y_true_onehot = tf.one_hot(y_true, depth=vocab_size) # 平滑:正确位置 1-smoothing,其他位置 smoothing/(vocab_size-1) y_true_smooth = y_true_onehot * (1 - smoothing) + smoothing / vocab_size # 屏蔽 padding 位置 mask = tf.cast(tf.not_equal(y_true, 0), tf.float32) loss = -tf.reduce_sum(y_true_smooth * tf.math.log(tf.nn.softmax(y_pred) + 1e-9), axis=-1) loss = loss * mask return tf.reduce_sum(loss) / tf.reduce_sum(mask)smoothing控制平滑程度,0.1 是常用值,太大反而让模型学不准。mask把 padding 位置的损失置零,分母用有效词数而不是总长度,保证不同 batch 之间损失可比。这个损失函数替换掉默认的稀疏交叉熵后,验证集 BLEU 一般会有可见提升。
解码阶段,束搜索的评分函数可以加长度惩罚。不加惩罚时模型倾向输出短句,因为短句对数概率衰减少。长度惩罚用score / length^alpha,alpha 取 0.6 到 0.7,让长句和短句公平竞争。这个参数在演示时值得现场调一下,让听众看到输出长度的变化。
最后说一个验证模型是否真的学到东西的方法:把训练集里的某句话原样输入,看输出是否接近参考译文。如果训练集上都翻不对,说明模型欠拟合,回去加层数或加数据;如果训练集翻得好但验证集差,说明过拟合,加 dropout 或减层数。这个检查花不了几分钟,但能省掉大量盲目调参的时间。希望帮到你。
本文还有配套的精品资源,点击获取