
简介这份资源是一套基于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_model512、h8、d_ff2048、层数 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, langzh): 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, dropout0.1): super().__init__() self.att layers.MultiHeadAttention(num_headsnum_heads, key_dimd_model) self.ffn tf.keras.Sequential([ layers.Dense(d_ff, activationrelu), layers.Dense(d_model) ]) self.norm1 layers.LayerNormalization(epsilon1e-6) self.norm2 layers.LayerNormalization(epsilon1e-6) self.drop1 layers.Dropout(dropout) self.drop2 layers.Dropout(dropout) def call(self, x, training, maskNone): attn self.att(x, x, x, attention_maskmask) x self.norm1(x self.drop1(attn, trainingtraining)) ffn self.ffn(x) return self.norm2(x self.drop2(ffn, trainingtraining))d_model是模型维度num_heads是头数d_ff是前馈网络中间维度。epsilon1e-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, monitorval_loss, save_best_onlyTrue, verbose1), tf.keras.callbacks.EarlyStopping( monitorval_loss, patience5, restore_best_weightsTrue), tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience2, min_lr1e-5) ] model.compile(optimizeradam_with_warmup, lossmasked_loss, metrics[accuracy]) model.fit(train_ds, validation_dataval_ds, epochs50, callbackscallbacks)patience5表示验证损失连续 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_width3, max_len50): 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])], verbose0) 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(keylambda x: x[1], reverseTrue) beams candidates[:beam_width] if all(seq[-1] end_token for seq, _ in beams): break best_seq max(beams, keylambda 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:i1], 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, smoothing0.1, vocab_size10000): # y_true 形状 (batch, seq_len)y_pred 形状 (batch, seq_len, vocab_size) y_true_onehot tf.one_hot(y_true, depthvocab_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^alphaalpha 取 0.6 到 0.7让长句和短句公平竞争。这个参数在演示时值得现场调一下让听众看到输出长度的变化。最后说一个验证模型是否真的学到东西的方法把训练集里的某句话原样输入看输出是否接近参考译文。如果训练集上都翻不对说明模型欠拟合回去加层数或加数据如果训练集翻得好但验证集差说明过拟合加 dropout 或减层数。这个检查花不了几分钟但能省掉大量盲目调参的时间。希望帮到你。本文还有配套的精品资源点击获取