简介基于Python的中英机器翻译系统项目采用Keras-Transformer模型完成适用于毕业设计、课程设计及深度学习初学者作为实践参考。项目包含可直接运行的完整源码与配套文档代码经过严格测试并提供与基于LSTM的翻译项目相同的数据集便于对比不同序列建模方案的效果。压缩包共20个文件大小约7.42MB主要文件包括Python脚本、Jupyter Notebook、训练好的模型权重文件h5、多个pkl词典与中间结果文件以及说明文档md/txt等。其中Notebook适合按步骤演练数据获取、模型训练与翻译测试流程pkl文件保存了预处理后的词表和编解码结果可直接加载复用。已有68人学习下载。通过该项目可系统了解Transformer在机器翻译中的落地流程包括语料准备、词表构建、模型封装、训练与解码文档简明地给出了设计思路和运行指导方便在此基础上扩展语料、调整参数或迁移到其他序列任务。资源内容来自网络公开分享使用时注意版权信息即可。1. 基于 Python 的 Keras-Transformer 中英翻译资源能跑、能改、能当毕设基线基于 Python 的机器翻译项目向来是课程设计和毕业设计的热门选题但网上的 Keras-Transformer 翻译资源要么只贴理论不给代码要么能跑却没人为你讲清每个文件是干嘛的。这份「中英文翻译」资源把 cmn.txt 平行语料、get_data.ipynb 预处理、zh_wiki 繁简转换模块、训练好的 H5 权重、traintranslate.ipynb 主流程全部打包按顺序执行就能看到中英互译的完整链路。它适合两类人一是需要快速拿到可演示系统的毕设课设学生二是想对比 Transformer 和 LSTM 在序列建模上差异的开发者。项目本身是封装实践不需要从零实现注意力机制把精力放在数据、参数和结果分析上更划算。下面按数据预处理、模型训练、推理部署、避坑四个环节拆开讲。2. 数据全链路从 cmn.txt 到 token 序列的预处理拆解翻译模型的效果上限在数据准备阶段就定了一大半。Transformer 结构再强喂进去的是脏数据出来的译文也不会好看。这个项目的预处理链路分成三段原始语料解析与清洗、中文繁简转换、token 字典构建与持久化。每一段都有对应的脚本或 notebook 承接。2.1 cmn.txt 语料解析格式、清洗与编码陷阱cmn.txt 是这个项目的中英平行语料标准格式是每行一对平行句对英文与中文之间用制表符 \t 分隔。get_data.ipynb 承担了数据读取和整理的工作第一步就是把每一行拆成英文、中文两个字段同时过滤空行和格式错误的行。# 读取 cmn.txt 并解析平行句对 with open(cmn.txt, r, encodingutf-8) as f: lines f.read().strip().split(\n) pairs [] for line in lines: parts line.split(\t) if len(parts) 2: en, zh parts[0].strip(), parts[1].strip() if en and zh: # 过滤单侧为空的残缺行 pairs.append((en, zh)) print(有效平行句对数量:, len(pairs)) print(样例:, pairs[0])split(\t) 是整个解析逻辑的关键。有的语料来源用空格或竖线分隔而英文句子内部本身就有空格如果按空格切分英文句子会被拦腰截断直接污染训练数据。我拿到任何语料的第一步都是打印前 10 行观察分隔符再决定解析方式这个习惯能省掉大量返工。if en and zh用来排除只有单侧数据的残缺行不过滤的话序列化阶段会产生空 token训练时直接报错。清洗阶段还有一个容易被忽略的点全角半角混用。中文语料里常混着全角逗号、全角括号英文侧可能出现零宽空格。项目本身的语料相对干净但如果你打算换自己的数据建议在预处理里统一加一轮全角转半角、去除多余空白字符。get_data.ipynb 的产出会直接进入 token 化环节清洗逻辑写得越稳后面越省事。2.2 zh_wiki 与 langconv繁简转换必须排在分词之前language 目录下放的是 zh_wiki.py 和 langconv.py分别承担中文词条映射数据和繁简转换逻辑。很多翻译项目直接忽略繁简问题从 wiki 或繁体站点抓来的语料里同一个词在简体和繁体之间反复横跳模型学到的映射关系就不稳定。这个项目单独把这两个模块放进 language 包说明作者在语料归一化上是用过心思的。langconv.py 里的 Converter 类实现了基于词库的繁简转换zh_wiki.py 提供的是词组级别的转换数据不是简单的单字映射。用法如下from langconv import Converter def trad_to_simp(text: str) - str: 繁体转简体用于语料归一化 converter Converter(zh-hans) return converter.convert(text) sample 機器翻譯是自然語言處理的重要方向 print(trad_to_simp(sample)) # 输出: 机器翻译是自然语言处理的重要方向Converter(zh-hans) 指定转换目标为简体。「程式設計→程序设计」这类词组必须靠词库匹配和逐字替换不同转换过程是有状态的处理长文本比简单映射慢一些但准确度明显更高。处理顺序上有一个硬性要求繁简转换必须在分词和 token 化之前完成。如果先切词再转换词表里会同时出现「機器」和「机器」两种形态token 字典膨胀模型被迫学习无意义的变体收敛变慢泛化变差。这个顺序问题看着不起眼实际影响很大。提示zh_wiki.py 是数据文件而不是可执行脚本导入 langconv 时它会自动被加载。如果只拷贝 langconv.py 而漏掉 zh_wiki.py运行时会报找不到词条数据。2.3 token 字典与 pkl 持久化训练的前置产物middle_data 目录下是一组 pkl 文件source_token_dict.pkl、target_token_dict.pkl、source_tokens.pkl、encode_input.pkl、decode_input.pkl、decode_output.pkl。这些是预处理的产出物也是训练和推理共用的中间数据。source_token_dict.pkl 保存英文侧的 token 到 id 映射target_token_dict.pkl 保存中文侧映射两个字典独立构建对应 Transformer 中源语言和目标语言 embedding 分开设置的思路。用 pickle 而不是文本格式保存是因为字典对象在进程间传递时 pickle 能完整保留结构加载速度也快。加载代码很直接import pickle with open(middle_data/source_token_dict.pkl, rb) as f: source_token_dict pickle.load(f) with open(middle_data/target_token_dict.pkl, rb) as f: target_token_dict pickle.load(f) print(source vocab size:, len(source_token_dict)) print(target vocab size:, len(target_token_dict))这两个字典的规模直接决定 embedding 层的维度。encode_input.pkl 是编码器输入序列decode_input.pkl 是解码器输入序列训练时做 teacher forcingdecode_output.pkl 是解码器的 one-hot 标签。三个 pkl 的 shape 必须能对得上我在拿到别人项目时第一件事就是打印这几个文件的 shape确认 batch 维度和序列长度一致排除预处理阶段已经出错的隐患。注意pickle 反序列化只应加载可信来源的文件。如果你打算把自己处理后的数据分享出去建议同时导出 json 格式的字典方便其他环境直接读取。3. 模型与训练Keras-Transformer 的参数体系和完整流程这一章讲模型本身。翻译任务本质上是一个序列到序列问题项目的模型基于 keras-transformer 封装库构建核心入口是 get_model。理解它的参数比理解注意力机制的数学推导对实际跑通更有帮助——当然两者最好都看。3.1 为什么选 Transformer自注意力与并行化传统方案用 LSTM 编码器-解码器把源句子逐词编码成向量再逐词生成译文。LSTM 的痛点是长距离依赖句子超过 30 个词时早期信息经过多步传递明显衰减而且循环结构必须串行计算训练慢。Transformer 用自注意力替代循环任意两个位置之间的依赖距离压缩到一步配合位置编码保留语序信息多头注意力让模型同时关注不同子空间的关系并行度高训练速度快。对照这个项目选型还有一层现实考虑LSTM 翻译模型需要维护隐状态在时间步之间传递调参时稍有不慎就会出现梯度消失Transformer 的残差连接和 LayerNorm 让训练稳定很多对课程设计场景更友好。这也是项目作者选择 Keras-Transformer 封装而不是手写的原因——注意力机制的实现细节交给库模型层面的工作集中在数据组织和超参数调整上。3.2 get_model 参数详解与选型建议traintranslate.ipynb 里模型构建的核心代码大致如下from keras_transformer import get_model model get_model( token_nummax(len(source_token_dict), len(target_token_dict)), embed_dim32, encoder_num2, decoder_num2, head_num4, hidden_dim128, dropout_rate0.05, use_same_embedFalse, ) model.compile(adam, losscategorical_crossentropy)各参数的含义和对效果的影响我在实际调参中总结为下面这张表参数项目配置作用调整建议embed_dim32词向量维度语料大时建议 128-256毕设语料量下 32-64 够用encoder_num2编码器层数3-4 层提升建模能力但训练时间线性增长decoder_num2解码器层数与 encoder_num 保持同量级即可head_num4多头注意力头数8 头是论文标配小语料 4 头更稳hidden_dim128FFN 隐藏层维度通常取 embed_dim 的 4 倍左右dropout_rate0.05随机失活比例语料充足可提到 0.1过拟合时优先调这个embed_dim32 是典型的小模型配置。embed_dim 设太大而语料不够时模型容易过拟合训练 loss 降得很好看验证集上却一塌糊涂。head_num4 意味着每个注意力头分管不同子空间多头机制让模型同时捕捉句法、指代、语义等不同维度的关系。hidden_dim128 是 feed-forward 层的内部维度一般取 embed_dim 的 4 倍保证非线性变换的容量。use_same_embedFalse 也值得展开说。英文和中文在字面上几乎没有共享子词共享 embedding 会让两种语言的向量空间互相干扰分开设置虽然增加参数量但对翻译任务几乎总是更优。这个开关在封装库里的默认值不一定是 False建议显式写出来不要依赖默认行为。3.3 训练数据组织与权重保存训练阶段把预处理产物组装成模型输入。编码器输入是源句子的 id 序列解码器输入是目标句子的 id 序列但要去掉最后一个 token并在开头补上起始符解码器标签则是去掉起始符、末尾补上终止符的 one-hot 序列。这就是 teacher forcing——训练时解码器每一步的输入来自真实的上一步 token而不是自己生成的 token让模型快速收敛。实际笔记本里会用 pickle 加载 middle_data 下的三个 pkl 文件生成这三个数组。import numpy as np from tensorflow import keras # encode_input: [batch, src_len] # decode_input: [batch, tgt_len] # decode_output: [batch, tgt_len, target_vocab_size] checkpoint keras.callbacks.ModelCheckpoint( model/W-{epoch:02d}-{loss:.4f}.h5, save_best_onlyFalse, ) model.fit( [encode_input, decode_input], decode_output, epochs40, batch_size64, validation_split0.1, callbacks[checkpoint], )decode_output 是三维 one-hot 标签每个目标位置对应一个词表大小的向量配合 categorical_crossentropy 使用。项目最终保存的 W--40-0.0563-.h5 命名里40 是轮数0.0563 是 loss——这种命名习惯非常值得借鉴几个月后翻目录不用打开代码就知道权重是怎么训出来的。我一般会再加一个 EarlyStopping 回调监控验证集 loss连续几个 epoch 不下降就停避免无效训练时间。环境方面keras-transformer 需要和对应版本的 keras/tensorflow 配合安装pip install keras-transformer tensorflow numpy如果机器上已经装好了 TensorFlow 2.x注意核对 keras 版本和封装库的兼容性这部分具体坑放在第 5 章。4. 推理部署从 h5 权重到可用的翻译接口训练产出权重只是前半程把权重变成能对任意新句子输出译文的接口才是这个资源真正可用的标志。这一章拆解推理链路的三段模型重建与权重加载、贪心解码流程以及与 LSTM 版本的对比验证思路。4.1 模型重建与权重加载h5 权重文件需要和模型结构配合才能加载。最常翻车的做法是直接 load_model 整个文件——如果模型里有第三方封装库的自定义层反序列化时经常报 Unknown layer。更稳的做法是先 get_model 重建相同结构再 load_weights 只加载权重from keras_transformer import get_model # 参数必须与训练时完全一致 model get_model( token_nummax(len(source_token_dict), len(target_token_dict)), embed_dim32, encoder_num2, decoder_num2, head_num4, hidden_dim128, dropout_rate0.05, use_same_embedFalse, ) model.load_weights(model/W--40-0.0563-.h5)load_weights 成功的前提是 token_num、embed_dim、层数这些超参数全部对得上任何一项不一致都会报 shape mismatch。我见过不少同学在训练和推理两处各写一份模型构建代码改训练参数时忘了同步推理侧结果权重加载永远报错。建议把构建逻辑抽成一个函数训练和推理共用同一份代码从根上避免这类问题。4.2 贪心解码逐步生成译文推理阶段要用 decode_model 把训练好的模型包装成解码器。本质是一个循环编码器对源句子算一次解码器从起始符开始每步预测一个 token把预测结果拼到输入序列尾部继续预测下一步直到输出终止符或达到最大长度。from keras_transformer import decode_model import numpy as np decoder decode_model(model) def translate(source_sentence: str, max_len: int 50) - str: # 1. 英文侧 token 化并映射为 id未登录词回退到 UNK tokens source_sentence.lower().split() unk_id source_token_dict.get(UNK, 1) src_ids [source_token_dict.get(t, unk_id) for t in tokens] src_ids src_ids[:max_len] # 2. 解码器逐步生成译文 output_ids [] for _ in range(max_len): pred decoder.predict([np.array([src_ids]), np.array([output_ids])])[0] next_id int(np.argmax(pred[-1])) if next_id target_token_dict[END]: break output_ids.append(next_id) # 3. id 序列还原成中文句子 return .join(target_token_dict[i] for i in output_ids) print(translate(I love machine learning.))关键点有三个源句子必须先小写再按空格切词大小写变体会让未登录词增多UNK 的 id 从字典里取不要写死成 0很多词表里 0 是留给 padding 的最后一步的拼接方式取决于中文侧是字符级还是词级生成输出前先打印几个 id 对应的原文确认再决定用 .join 还是空格连接。贪心解码的问题在于短视——每一步只选当前概率最高的 token一步选错没有回头机会。短句子问题不大长句子容易出现语序别扭。要提升质量可以改成 beam search维护前 k 条候选序列而不是单条第 6 章给出完整实现。4.3 与 LSTM 版本对比同一份数据的模型对照项目的一大特色是和基于 LSTM 的翻译项目共用同一份 cmn.txt 数据天然适合做模型对比。对比时准备三类测试句子短句5 词以内、中长句10-20 词、含数字和专有名词的句子。短句上两个模型差异不大中长句能看出 Transformer 在语序和长距离依赖上的优势专有名词则考验模型对低频词的记忆能力。对比不凭肉眼把两边的 BLEU 算出来更客观。同一个句子LSTM 可能翻得通顺但漏了信息Transformer 可能词序更好但多了一个虚词BLEU 的 n-gram 重合度能把这种差异量化。这部分对比实验也是答辩时的加分项——你不仅在跑模型还能说明两种架构在本质上的不同。BLEU 的实现同样放到第 6 章。5. 避坑指南跑翻译项目最容易翻车的五个位置翻译项目看着流程简单实际跑起来处处是坑。以下五条是我在拆解和复现这类项目时最常遇到的翻车现场按「现象→原因→解决」记录。5.1 UnicodeDecodeError语料编码不一致现象读取 cmn.txt 时直接报UnicodeDecodeError: utf-8 codec cant decode byte...定位到某一行字符非法。原因网上公开语料下载下来经常是 GBK 或混合编码包含中文的文本尤其常见。Python 默认按 UTF-8 解码遇到 GBK 字节流就炸。解决先用 chardet 检测实际编码再按检测结果解码并重新保存为 UTF-8import chardet with open(cmn.txt, rb) as f: raw f.read() detected chardet.detect(raw) text raw.decode(detected[encoding]) print(detected[encoding]) with open(cmn_utf8.txt, w, encodingutf-8) as f: f.write(text)检测结果如果是 GB2312 或 GBK转成 UTF-8 后就能一劳永逸。注意编码检测对短文本可能误判用整份文件的内容检测更可靠拿到结果后再统一转换。5.2 keras_transformer 与 TensorFlow 版本不兼容现象from keras_transformer import get_model直接 ImportError或者训练完成后保存、加载模型时报Unknown layer: TransformerEncoder之类的错误。原因keras-transformer 封装库对 keras 的底层 API 有版本依赖和 TensorFlow 2.x 内置的 tf.keras 在序列化、层注册机制上不完全兼容。新装的环境默认版本往往和项目创作时的依赖不一致。解决首选新建一个干净的 virtualenv安装与项目文档匹配的 keras 和 tensorflow 版本别动全局环境其次是加载模型时避开 load_model改用 get_model 重建结构再 load_weights绕开自定义层反序列化的问题。项目自带的 README 和备份说明文件里一般会记录运行环境先看文档再动手能少走一半弯路。5.3 训练 loss 不降或直接 NaN现象训练跑几个 epoch 后 loss 停在某个值不动或者直接变成 NaN。原因NaN 大概率来自学习率过高或梯度爆炸loss 不降常见于语料太短导致欠拟合或者 decode_output 标签里有 None 和空序列。解决先检查数据打印几条 decode_output 的 shape 和内容确认没有空标签再调低学习率并加梯度裁剪from tensorflow.keras.optimizers import Adam model.compile( Adam(learning_rate1e-4, clipnorm1.0), losscategorical_crossentropy, )clipnorm1.0 把梯度范数限制在 1 以内是处理梯度爆炸的常规手段。如果 loss 还是不动考虑增大 embed_dim 或加深层数但每次只改一个参数改完跑几个 epoch 看趋势别一次动三个变量。超参数调优本身就带点玄学成分一次动太多出问题你连归因都做不了。5.4 译文循环重复或提前结束现象翻译任何句子输出都是「的的的的的」或者某一个高频 token 无限重复另一种情况是只输出一两个词就碰到终止符。原因训练不充分导致模型没有学到合理的停止条件或者预处理阶段起始符、终止符没有正确注入解码器的输入输出序列。解决先确认 decode_input 每行开头是START的 iddecode_output 每行结尾是END的 id。很多自定义预处理脚本会漏掉这两个特殊 token模型根本不知道何时开始、何时停止。再检查推理侧起始 token 必须用 target_token_dict[ ]而不是 0 或任意 id。这两个位置都对了循环重复基本就能解决。5.5 load_weights 报 shape mismatch现象model.load_weights(model/W--40-0.0563-.h5)报错提示 expected shape 与 actual shape 不一致。原因训练和推理时模型参数不一致最常见的是 embed_dim 或 token_num 两处写错。embed_dim 记错了还能看出来token_num 因为字典加载方式不同差几百是最隐蔽的。解决把模型构建逻辑收敛到一个函数里训练和推理统一调用同时把超参数存成 json 配置文件加载权重前先读配置再 build 模型。我自己的习惯是把 token_num 直接用max(len(source_token_dict), len(target_token_dict))动态计算而不是写死一个数字——写死数字的项目一换数据就废。6. 进阶技巧beam search 与 BLEU 评估的落地写法6.1 贪心解码改成 beam search第 4 章的贪心解码每步只保留一个最优 tokenbeam search 维护 k 条候选序列每步对全部候选展开按累计 log 概率排序只保留前 k 条。核心改动如下def beam_search(decoder, src_ids, beam_size3, max_len50): start_id target_token_dict[START] end_id target_token_dict[END] sequences [[start_id]] scores [0.0] for _ in range(max_len): candidates [] for seq, score in zip(sequences, scores): pred decoder.predict([np.array([src_ids]), np.array([seq])])[0] log_probs np.log(pred[-1] 1e-10) top_idx np.argsort(log_probs)[-beam_size:] for idx in top_idx: candidates.append((seq [idx], score log_probs[idx])) ordered sorted(candidates, keylambda x: x[1], reverseTrue) sequences [seq for seq, _ in ordered[:beam_size]] scores [score for _, score in ordered[:beam_size]] if all(seq[-1] end_id for seq in sequences): break best sequences[int(np.argmax(scores))] return .join(target_token_dict[i] for i in best if i not in (start_id, end_id))beam_size 取 3 到 5。太小提升有限太大速度明显变慢而且可能出现「越搜越差」的边际效应。log 概率相加的本质是序列概率的连乘取对数能避免浮点下溢。加上 beam search 之后长句子的语法完整度通常有肉眼可见的提升。6.2 BLEU 计算与测试集组织BLEU 衡量译文和参考译文之间 n-gram 的重合度是翻译任务最通用的自动指标。用一个现成库实现最快pip install nltkfrom nltk.translate.bleu_score import sentence_bleu, SmoothingFunction def compute_bleu(reference: str, candidate: str) - float: smooth SmoothingFunction().method1 return sentence_bleu([list(reference)], list(candidate), smoothing_functionsmooth)短句的 BLEU 天然偏低因为 n-gram 完全重合的概率小必须用平滑函数否则很多句子算出来是 0 分没有区分度。计算前先对译文做全角半角、繁简归一化不然分数会被系统性低估。我一般准备 50 条带参考译文的测试句批量算平均 BLEU同时记录几条典型句子的原文、译文和参考译文作为答辩展示素材。从那以后我每次跑翻译项目都会强制走一遍「数据归一化 → 训练监控 → 多解码方式对比 → BLEU 定量评估」的完整流程。翻译系统的效果不是一个 loss 数字能说明的定量和定性结合才看得全面。希望你拿到这份资源后也能把这条链路走通希望这些拆解和踩坑记录能帮到你。本文还有配套的精品资源点击获取