拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Keras-Transformer中英翻译实战:数据预处理、模型训练与避坑指南

Keras-Transformer中英翻译实战:数据预处理、模型训练与避坑指南

简介:基于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 权重、train&translate.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', encoding='utf-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 forcing),decode_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 参数详解与选型建议

train&translate.ipynb 里模型构建的核心代码大致如下:

from keras_transformer import get_model model = get_model( token_num=max(len(source_token_dict), len(target_token_dict)), embed_dim=32, encoder_num=2, decoder_num=2, head_num=4, hidden_dim=128, dropout_rate=0.05, use_same_embed=False, ) model.compile('adam', loss='categorical_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_dim=32 是典型的小模型配置。embed_dim 设太大而语料不够时,模型容易过拟合,训练 loss 降得很好看,验证集上却一塌糊涂。head_num=4 意味着每个注意力头分管不同子空间,多头机制让模型同时捕捉句法、指代、语义等不同维度的关系。hidden_dim=128 是 feed-forward 层的内部维度,一般取 embed_dim 的 4 倍,保证非线性变换的容量。

use_same_embed=False 也值得展开说。英文和中文在字面上几乎没有共享子词,共享 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_only=False, ) model.fit( [encode_input, decode_input], decode_output, epochs=40, batch_size=64, validation_split=0.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_num=max(len(source_token_dict), len(target_token_dict)), embed_dim=32, encoder_num=2, decoder_num=2, head_num=4, hidden_dim=128, dropout_rate=0.05, use_same_embed=False, ) 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 can't decode byte...,定位到某一行字符非法。

原因:网上公开语料下载下来经常是 GBK 或混合编码,包含中文的文本尤其常见。Python 默认按 UTF-8 解码,遇到 GBK 字节流就炸。

解决:先用 chardet 检测实际编码,再按检测结果解码并重新保存为 UTF-8:

import 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', encoding='utf-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_rate=1e-4, clipnorm=1.0), loss='categorical_crossentropy', )

clipnorm=1.0 把梯度范数限制在 1 以内,是处理梯度爆炸的常规手段。如果 loss 还是不动,考虑增大 embed_dim 或加深层数,但每次只改一个参数,改完跑几个 epoch 看趋势,别一次动三个变量。超参数调优本身就带点玄学成分,一次动太多,出问题你连归因都做不了。

5.4 译文循环重复或提前结束

现象:翻译任何句子,输出都是「的的的的的」或者某一个高频 token 无限重复;另一种情况是只输出一两个词就碰到终止符。

原因:训练不充分导致模型没有学到合理的停止条件,或者预处理阶段起始符、终止符没有正确注入解码器的输入输出序列。

解决:先确认 decode_input 每行开头是<START>的 id,decode_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 章的贪心解码每步只保留一个最优 token,beam search 维护 k 条候选序列,每步对全部候选展开,按累计 log 概率排序,只保留前 k 条。核心改动如下:

def beam_search(decoder, src_ids, beam_size=3, max_len=50): 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, key=lambda x: x[1], reverse=True) 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 nltk
from 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_function=smooth)

短句的 BLEU 天然偏低,因为 n-gram 完全重合的概率小,必须用平滑函数,否则很多句子算出来是 0 分,没有区分度。计算前先对译文做全角半角、繁简归一化,不然分数会被系统性低估。我一般准备 50 条带参考译文的测试句,批量算平均 BLEU,同时记录几条典型句子的原文、译文和参考译文,作为答辩展示素材。

从那以后,我每次跑翻译项目都会强制走一遍「数据归一化 → 训练监控 → 多解码方式对比 → BLEU 定量评估」的完整流程。翻译系统的效果不是一个 loss 数字能说明的,定量和定性结合才看得全面。希望你拿到这份资源后也能把这条链路走通,希望这些拆解和踩坑记录能帮到你。

本文还有配套的精品资源,点击获取

返回列表