
简介这份资源是一套基于Python与Keras-Transformer实现的中英文机器翻译项目面向计算机、人工智能、通信等专业的学生与从业者可作为毕业设计、课程大作业或期末课设的参考方案也适合想入门神经机器翻译的小白学习与进阶。压缩包共17个文件约7.42MB包含py源码、ipynb交互式笔记、h5模型权重、pkl词典与中间数据、md说明文档及txt语料等覆盖数据预处理、模型训练与翻译推理的完整链路。项目已通过调试测试可直接运行答辩评审分达98分代码结构清晰便于理解Transformer的编码解码机制与训练流程。目前已有104人学习下载基础较好的读者可在此基础上修改调整实现不同语向或功能扩展具备较高的学习借鉴价值。1. 中英文机器翻译从标题到能跑通的 Transformer 到底要几步手里拿到一个「基于 Python 开发的中英文机器翻译基于 keras-transformer 模型可直接跑」的源码包多数人的第一反应是解压、装依赖、python train.py然后被一堆张量维度报错劝退。这个标题背后其实是一条完整的工程链路语料准备、分词与词表构建、Transformer 编码器解码器搭建、训练循环、推理与 BLEU 评估。它解决的是「不依赖现成翻译 API自己掌控模型和数据」的需求适合想入门 NLP 落地、要做垂直领域翻译比如法律、医疗、电商标题的开发者。源码能直接跑只是起点真正决定成败的是语料质量和几个关键超参。下面按我实际复现的顺序把这条链路拆开讲清楚。2. 语料与分词中英文平行语料的清洗和子词切分怎么做2.1 平行语料的来源与清洗规则中英翻译的语料常见来源是公开的平行句对数据集格式多为制表符或特定分隔符隔开的双语文本。直接拿来训练之前必须做几件事去掉空行和长度极端失衡的句对比如中文 3 个字对英文 80 个词、统一全半角标点、剔除 HTML 残留标签和乱码。我一般会设一个长度比阈值中文字符数除以英文词数落在 0.3 到 3 之间才保留超出范围的句对大概率是错位对齐留着只会污染模型。清洗脚本用 Python 写核心是逐行读取、切分、过滤、写回。下面这段可以直接改成读你的文件路径import re def clean_pair(zh, en, min_len2, max_len100, ratio_low0.3, ratio_high3.0): zh zh.strip() en en.strip() # 去掉空行 if not zh or not en: return None # 长度过滤 if len(zh) min_len or len(zh) max_len: return None if len(en.split()) min_len or len(en.split()) max_len: return None # 长度比过滤防止错位对齐 ratio len(zh) / max(len(en.split()), 1) if ratio ratio_low or ratio ratio_high: return None # 统一标点去掉 HTML 标签 zh re.sub(r[^], , zh) en re.sub(r[^], , en) return zh, en def process_file(src_path, dst_path): kept 0 with open(src_path, r, encodingutf-8) as fin, \ open(dst_path, w, encodingutf-8) as fout: for line in fin: parts line.strip().split(\t) if len(parts) ! 2: continue pair clean_pair(parts[0], parts[1]) if pair: fout.write(f{pair[0]}\t{pair[1]}\n) kept 1 print(f保留句对: {kept}) process_file(raw_pairs.txt, clean_pairs.txt)逻辑说明clean_pair先做空值和长度过滤再用长度比筛掉错位句对最后清理标签。参数min_len和max_len控制句子长度范围太短的句子信息量不足太长的句子在训练时显存吃紧。ratio_low和ratio_high是经验值中英长度比通常在 0.5 到 2 之间放宽到 0.3 到 3 是为了保留一些特殊句式。清洗完的语料建议再随机抽 20 条人工看一眼确认没有系统性错位。2.2 用 SentencePiece 做中英联合子词切分Transformer 不直接吃字或词需要先转成子词单元。中英文混合场景下用 SentencePiece 训练一个联合词表是最省事的做法它能把中文切成子词、英文切成 BPE共享一个词表减少词表大小和未登录词。训练命令如下spm_train --inputclean_pairs.txt \ --model_prefixzh_en_spm \ --vocab_size32000 \ --character_coverage0.9995 \ --model_typeunigram \ --input_sentence_size2000000 \ --shuffle_input_sentencetrue参数说明vocab_size设 32000 是常见折中太小会导致英文切得太碎太大则嵌入矩阵膨胀。character_coverage设 0.9995 覆盖绝大多数汉字和英文字符。model_type选 unigram 在翻译任务上通常比 BPE 略稳。input_sentence_size限制训练词表用的句子数避免全量语料跑太久。训练完得到zh_en_spm.model和zh_en_spm.vocab后续编码解码都用这个模型。编码时把中英分别处理但共用同一个模型import sentencepiece as spm sp spm.SentencePieceProcessor() sp.load(zh_en_spm.model) def encode_pair(zh, en, max_len128): zh_ids sp.encode(zh, out_typeint)[:max_len] en_ids sp.encode(en, out_typeint)[:max_len] return zh_ids, en_ids这里max_len截断到 128 个 token覆盖大部分句子。如果做长文档翻译需要调大并配合显存评估。编码后的序列要加起始和结束标记Transformer 解码器靠这些标记判断生成起止。3. keras-transformer 模型搭建编码器解码器和位置编码怎么配3.1 编码器解码器堆叠的核心参数keras-transformer 这类实现通常把编码器和解码器封装成可堆叠的层。核心参数有这几个num_layers堆叠层数、d_model隐藏维度、num_heads多头注意力头数、dff前馈网络中间维度、dropout丢弃率。中英翻译任务上我一般从num_layers4、d_model256、num_heads8、dff1024起步语料超过百万句对再往上加。头数要能整除d_model否则维度对不上直接报错。位置编码是 Transformer 的固定组件用正弦余弦函数生成不需要训练。下面是一个常见实现import numpy as np import tensorflow as tf def positional_encoding(max_len, d_model): pos np.arange(max_len)[:, np.newaxis] i np.arange(d_model)[np.newaxis, :] angle pos / np.power(10000, (2 * (i // 2)) / np.float32(d_model)) angle[:, 0::2] np.sin(angle[:, 0::2]) angle[:, 1::2] np.cos(angle[:, 1::2]) return tf.cast(angle[np.newaxis, ...], dtypetf.float32)逻辑说明偶数维度用 sin奇数维度用 cos这样不同位置有不同的编码模式。max_len要大于等于你截断后的最大序列长度否则推理时位置编码不够用。d_model必须和模型隐藏维度一致。这个编码在输入嵌入之后相加让模型感知词序。3.2 训练循环与损失函数的关键设置训练时用 teacher forcing即解码器输入是右移后的目标序列损失用稀疏交叉熵忽略填充位置的损失。优化器选 Adam学习率用 warmup 策略前若干步线性增长再衰减。下面是一个简化的训练步骤import tensorflow as tf loss_object tf.keras.losses.SparseCategoricalCrossentropy( from_logitsTrue, reductionnone) def loss_function(real, pred): mask tf.math.logical_not(tf.math.equal(real, 0)) loss_ loss_object(real, pred) mask tf.cast(mask, dtypeloss_.dtype) loss_ * mask return tf.reduce_sum(loss_) / tf.reduce_sum(mask) optimizer tf.keras.optimizers.Adam(1e-4, beta_10.9, beta_20.98, epsilon1e-9) tf.function def train_step(inp, tar): tar_inp tar[:, :-1] tar_real tar[:, 1:] with tf.GradientTape() as tape: predictions model(inp, tar_inp, trainingTrue) loss loss_function(tar_real, predictions) gradients tape.gradient(loss, model.trainable_variables) optimizer.apply_gradients(zip(gradients, model.trainable_variables)) return loss参数说明from_logitsTrue因为模型输出未经过 softmax。mask把填充的 0 位置损失置零避免模型学会预测填充。学习率 1e-4 是 Transformer 常用起点warmup 步数一般设 4000 步但小数据集可以降到 1000。tar_inp是右移一位的目标序列tar_real是真实下一个 token。训练时每若干步打印一次 loss观察是否稳定下降。4. 训练避坑中英翻译模型跑不起来时先查这五处4.1 现象loss 一直是 nan 或者不下降原因通常是学习率太大、梯度爆炸或者语料里有空句导致 mask 全零。解决先把学习率降到 1e-5 试跑几百步如果 loss 开始下降说明是学习率问题检查清洗后的语料是否还有空行加梯度裁剪tf.clip_by_global_norm(gradients, 5.0)是常用做法。4.2 现象推理时输出重复词或一直输出同一个 token原因多半是解码器训练不充分或者推理时贪心搜索陷入循环。解决增加训练轮数观察验证集 BLEU 是否还在涨推理改用 beam searchbeam 宽度设 4 到 8比贪心搜索稳定得多。另外检查位置编码的max_len是否覆盖了推理序列长度。4.3 现象显存溢出batch size 调不上去原因序列长度 128、d_model256、num_layers4时batch size 32 在 8GB 显存上通常够用但如果序列长度调到 256 就会爆。解决用梯度累积模拟大 batch或者把d_model降到 128、层数降到 2 先跑通再逐步加。混合精度训练也能省显存但要注意 loss scaling。4.4 现象BLEU 分数很低但 loss 看着还行原因训练集和验证集分布不一致或者分词粒度太细导致译文碎片化。解决检查验证集是否来自同一语料分布把vocab_size调大一些减少英文被切碎的情况解码时加长度惩罚避免生成过短译文。4.5 现象加载保存的模型后推理结果和训练时不一致原因保存模型时没保存分词模型或者推理时输入没有加起始结束标记。解决把zh_en_spm.model和权重一起打包推理时确保输入格式和训练时完全一致包括起始标记和结束标记的位置。5. 推理与评估BLEU 怎么算才不骗自己5.1 用 beam search 做推理的代码实现训练完模型推理阶段用 beam search 比贪心搜索质量高不少。下面是一个简化实现def beam_search(model, sp, zh_text, beam_width5, max_len128): zh_ids sp.encode(zh_text, out_typeint)[:max_len] zh_ids tf.constant([zh_ids]) start_id sp.bos_id() end_id sp.eos_id() # 编码器输出 enc_output model.encoder(zh_ids, trainingFalse) # 初始 beam beams [([start_id], 0.0)] for _ in range(max_len): candidates [] for seq, score in beams: if seq[-1] end_id: candidates.append((seq, score)) continue tar_ids tf.constant([seq]) dec_output model.decoder(tar_ids, enc_output, trainingFalse) logits model.final_layer(dec_output) probs tf.nn.log_softmax(logits[:, -1, :]) topk tf.math.top_k(probs, kbeam_width) for i in range(beam_width): token topk.indices[0, i].numpy() new_score score topk.values[0, i].numpy() candidates.append((seq [token], new_score)) # 按分数排序保留 beam_width 个 candidates.sort(keylambda x: x[1], reverseTrue) beams candidates[:beam_width] if all(seq[-1] end_id for seq, _ in beams): break best_seq beams[0][0] return sp.decode([t for t in best_seq if t not in (start_id, end_id)])逻辑说明每一步对当前 beam 里的每个序列预测下一个 token取 log 概率最高的beam_width个扩展再全局排序保留最优的beam_width条。beam_width越大结果越好但越慢5 到 8 是常用范围。max_len防止无限生成。注意model.encoder和model.decoder的调用方式要和你的模型定义一致。5.2 BLEU 计算的正确姿势BLEU 用 n-gram 重叠度衡量译文质量常见实现是nltk.translate.bleu_score或sacrebleu。计算时要注意英文按词切分中文按字或按分词后切分两者要统一。下面用 sacrebleu 算from sacrebleu.metrics import BLEU bleu BLEU() refs [[the cat is on the mat]] hyps [the cat is on the mat] score bleu.corpus_score(hyps, refs) print(score)参数说明corpus_score接收译文列表和参考译文列表的列表。中文场景下如果参考译文是分好词的译文也要对应分词如果按字算就都按字。BLEU 对长度敏感短译文容易虚高所以一定要在足够大的测试集上算至少几千句。另外 BLEU 只是参考人工抽检几十条译文看流畅度和忠实度更靠谱。6. 让翻译质量再上一档三个我反复验证过的技巧第一个技巧是回译增强。把英文单语语料用反向模型翻译成中文和原有平行语料混在一起训练能明显提升模型对多样化表达的鲁棒性。我一般用 10% 到 20% 的回译数据太多会引入噪声。第二个技巧是标签平滑把目标 token 的 one-hot 分布稍微平滑一下tf.keras.losses.CategoricalCrossentropy(label_smoothing0.1)就能开对 BLEU 有零点几到一点几的提升代价几乎为零。第三个技巧是检查点平均把最后几个 epoch 的权重取平均再推理比单取最后一个检查点稳尤其在小数据集上。验证方法上我习惯把测试集分成两部分一部分算 BLEU另一部分人工看。人工看的时候重点盯三类错误数字和专有名词翻错、长句后半段丢失、重复生成。这三类错误在 BLEU 上不一定反映得出来但实际使用中很致命。如果做垂直领域翻译还要准备领域测试集通用 BLEU 高不代表领域内好用。最后说个血泪经验别一上来就堆层数和维度先把 4 层 256 维跑通BLEU 有个基线再逐步加。我见过太多人卡在环境配置和维度报错上模型还没开始训就放弃了。源码能直接跑是好事但跑通不等于跑好语料清洗和推理策略的功夫往往比模型结构更影响最终效果。希望帮到你。本文还有配套的精品资源点击获取