简介:这款基于Python与Keras Transformer模型的中英机器翻译项目,是一套已调试可直接运行的完整源码包,主要面向计算机、通信、人工智能、自动化等相关专业的学生、教师及从业者,适用于毕业设计、期末课程设计、课程大作业及个人进阶学习。压缩包共17个文件,仅7.42MB,包含Python脚本、Jupyter Notebook实操演示、预训练模型权重(h5)、序列化中间数据(pkl)与说明文档(md/README);资源覆盖语料获取、繁简转换、数据预处理、Transformer模型训练、中英翻译演示等完整流程,代码经过调试测试,确保可运行。项目中搭配get_data.ipynb和train&translate.ipynb两份笔记本,可直观操作从数据处理到训练翻译的路径,附带的language模块包含中文转换工具,便于理解与二次开发。目前已有102人学习下载,目录划分明确,既适合零基础学生快速跑通项目,也为能力较强读者提供了调整模型、扩展功能的良好基础。
1. 中文机器翻译用 kreas-transformer 自己搭,比调 API 更能看清翻译的本质
一段中文要翻成英文,大多数人第一反应是接现成 API 或者加载大模型,但这套基于 python 开发、基于 kreas-transformer(也就是 Keras Transformer)的中英文机器翻译源码,我建议你先别急着嫌“老”。原因很反直觉:Keras 已经帮我们封装了绝大多数张量运算,真正的工程难点反而集中在数据预处理、mask 构造和自回归推理这三个环节。把这套能直接跑的源码在本地跑通,再回去看那些“一行调用”的翻译服务,你会知道黑匣子里发生的到底是什么。适合谁?想搞懂 Transformer 翻译链路的学生、需要在离线环境做中英翻译的开发者,以及准备把翻译模型接入自己业务但不想盲目堆大模型参数的一线工程师。
2. 为什么用 Keras 搭中英翻译 Transformer:数据管道先于模型
2.1 中英文对齐语料:清洗、切分与 token 化的先后顺序
常见做法是先拿开源的中英平行语料,比如新闻领域或日常口语领域的句子对。别一上来就做模型,先把语料清洗干净。我一般会做三个动作:按句号、问号、感叹号拆句;去掉肉眼可见的 HTML 标签;过滤掉长度极端或者中英比例严重失衡的句子对。比如中文只有 5 个字,英文却有 200 个词这种,翻译模型学起来成本极高,不如直接丢掉。
清洗完之后做 tokenization。中文不能按空格分,最少要按字切,更合理的是用 jieba 分词;英文按空格分之后还要做小写化。这里有个容易踩的坑:中英共用一套 tokenizer 会互相干扰,建议分别建词表。词表大小直接影响模型体积和训练速度,按经验:
# 词表与模型基本参数,先跑通再调大 VOCAB_EN = 16000 # 英文词表大小 VOCAB_ZH = 20000 # 中文词表大小 MAX_LEN = 64 # 序列统一长度 D_MODEL = 128 # transformer 隐层维度 NUM_HEAD = 4 # 多头注意力头数 DFF = 512 # feed-forward 中间层维度 DROPOUT = 0.1 # 正则化比例 BATCH_SIZE = 32这段参数是“能用”的起步配置,不是“最好”的配置。在显存足够的机器上,D_MODEL 提到 256、NUM_HEAD 提到 8,效果会明显上升,但训练时间也会涨。词表大小的选择逻辑是:在训练语料上统计词频,覆盖掉 95% 以上的 token 即可,剩下的全部映射成<unk>。不要盲目把词表设得很大,否则 embedding 层占内存不说,低频词学不出有效向量,等于白占参数。
2.2 把句子对变成模型输入:训练时的三个张量
Transformer 翻译模型训练时,一个句子对要生成三条输入序列。encoder 输入是源语言句子;decoder 输入是目标语言句子右移一位后加上<start>;训练标签是目标语言句子,也就是 decoder 输入去掉<start>后的内容。这样设计的目的是让 decoder 在预测第 i 个词时,只能看到前 i-1 个真实词,这就是 teacher forcing。
def encode_batch(en_lines, zh_lines): en_tokens = [en_tokenizer.encode(line) + [END_ID] for line in en_lines] zh_tokens = [[START_ID] + zh_tokenizer.encode(line) + [END_ID] for line in zh_lines] # 统一长度,短补 0,长截断 en_padded = tf.keras.preprocessing.sequence.pad_sequences( en_tokens, maxlen=MAX_LEN, padding='post', truncating='post') zh_padded = tf.keras.preprocessing.sequence.pad_sequences( zh_tokens, maxlen=MAX_LEN + 1, padding='post', truncating='post') decoder_input = zh_padded[:, :-1] # 去掉最后一个 END target = zh_padded[:, 1:] # 去掉最前面的 START # 返回 encoder_input, decoder_input, target 三个张量 return en_padded, decoder_input, target注意这里 pad 用 0,而<pad>的 token id 也是 0,所以后面构造 mask 时只需要判断“是否等于 0”就够了。truncating='post'表示超长部分从尾部截断,保留句子前部语义,对翻译任务来说比截头部合理。decoder_input 长度是 MAX_LEN,target 长度也是 MAX_LEN,刚好对齐。
2.3 为什么用 Keras 而不是纯 TensorFlow 手写
这里有个取舍:纯 TensorFlow 手写 Transformer 不是不行,但需要自己管理变量初始化、梯度更新和 checkpoint,代码量至少多三分之一。Keras 的层抽象恰好把 MultiHeadAttention、LayerNormalization、Dropout 这些高频组件封装成可以直接调用的层,同时保留call()里自定义 mask 的能力。对翻译这个任务来说,mask 是最容易出错的地方,Keras 子类化 API 可以让我们在层内部显式处理 mask,调试比函数式 API 直观很多。
另外项目名是 kreas-transformer,本质就是 Keras 的 BogoTransformer 的实现。本地验证时留意 TensorFlow 的版本:tf 2.10 以上内置的keras.layers.MultiHeadAttention接口稳定,低于 2.4 的版本则建议直接升级,否则一些参数名对不上。
3. 手写 Transformer 翻译模型:位置编码、多头注意力与 mask
3.1 位置编码:没有它,Transformer 只是一袋词袋
Transformer 没有循环结构,无法天然感知词的先后顺序,所以要在 embedding 上叠加位置信息。常见做法是用 sin/cos 函数生成固定位置编码,而不是让模型学习位置向量。原因很简单:固定编码可以处理训练时长度的序列,且不同频率的波形让模型更容易区分相对位置。
import numpy as np import tensorflow as tf from tensorflow import keras class PositionalEncoding(keras.layers.Layer): def __init__(self, d_model, max_len): super().__init__() # 预计算 shape 为 (1, max_len, d_model) 的位置编码 pos = np.arange(max_len)[:, None] dim = np.arange(d_model)[None, :] even = np.sin(pos / np.power(10000, 2 * (dim // 2) / d_model)) odd = np.cos(pos / np.power(10000, 2 * (dim // 2) / d_model)) pe = np.where(dim % 2 == 0, even, odd).astype('float32') self.pe = tf.constant(pe[None, ...]) def call(self, x): # x shape: (batch, seq_len, d_model) return x + self.pe[:, :tf.shape(x)[1], :]代码里最关键的是np.where(dim % 2 == 0, even, odd),这对应原始论文里奇偶维度交替使用 sin 和 cos 的公式。10000是频率底数,一般不需要改。这个层在训练和推理时可以共用,因为位置编码只和序列长度有关,和 batch 大小无关。需要注意的是叠加位置编码后要做 Dropout,通常放在__init__里初始化的keras.layers.Dropout(0.1)之后。
3.2 Encoder 块:多头注意力的 mask 是 padding mask
Encoder 的每个 block 包含多头注意力和前馈网络,两层之间都接残差连接与 LayerNorm。这里最容易被忽略的是:Encoder 内部做 self-attention 时,每个 token 都可以看其他所有 token,因此只需要 padding mask,不需要因果 mask。所谓 padding mask,就是把值为 0 的位置替换成一个极大的负数,让 softmax 在那些位置上的注意力权重趋近于 0。
def create_padding_mask(seq): # seq 中 pad 的位置记为 1,参与注意力时被屏蔽 mask = tf.cast(tf.equal(seq, 0), tf.float32) return mask[:, None, None, :] # (batch, 1, 1, seq_len) class EncoderBlock(keras.layers.Layer): def __init__(self, d_model, num_heads, dff, dropout): super().__init__() self.attn = keras.layers.MultiHeadAttention( num_heads=num_heads, key_dim=d_model) self.ffn = keras.Sequential([ keras.layers.Dense(dff, activation='relu'), keras.layers.Dense(d_model), ]) self.ln1 = keras.layers.LayerNormalization(epsilon=1e-6) self.ln2 = keras.layers.LayerNormalization(epsilon=1e-6) self.drop1 = keras.layers.Dropout(dropout) self.drop2 = keras.layers.Dropout(dropout) def call(self, x, padding_mask): attn_out = self.attn(x, x, attention_mask=padding_mask) x = self.ln1(x + self.drop1(attn_out)) # 残差连接在 LayerNorm 之前 ffn_out = self.ffn(x) return self.ln2(x + self.drop2(ffn_out))keras.layers.MultiHeadAttention的attention_mask参数要求是 float mask,1 的位置保留注意力,0 的位置屏蔽。上面create_padding_mask返回的矩阵中,pad 位是 1,这看起来冲突了对吧?这是 Keras 的习惯问题:它内部会把attention_mask按“0 表示屏蔽”处理。所以实际使用时要在注意力层外面手动转换。我通常改为传入1 - pad_mask,也就是有效位置为 1。这个细节如果不注意,训练能跑但注意力永远学不到有效信息,Loss 掉到 4.0 左右就神秘持平。
3.3 Decoder 块:两层注意力,两类 mask 叠加
Decoder 比 Encoder 多一层 cross-attention,它的输入来自 Encoder 输出。Decoder 的 self-attention 需要一个因果 mask,确保每个位置只能看它自己和之前的位置,否则就是作弊。因果 mask 的构造方式是生成一个上三角矩阵,把右上角置为 1 后交给 Keras 做屏蔽。
def create_causal_mask(seq_len): # 下三角为 0,上三角为 1 mask = 1 - tf.linalg.band_part(tf.ones((seq_len, seq_len)), -1, 0) return mask[None, None, ...] # shape (1, 1, seq_len, seq_len) class DecoderBlock(keras.layers.Layer): def __init__(self, d_model, num_heads, dff, dropout): super().__init__() self.self_attn = keras.layers.MultiHeadAttention( num_heads=num_heads, key_dim=d_model) self.cross_attn = keras.layers.MultiHeadAttention( num_heads=num_heads, key_dim=d_model) self.ffn = keras.Sequential([ keras.layers.Dense(dff, activation='relu'), keras.layers.Dense(d_model), ]) self.ln1 = keras.layers.LayerNormalization(epsilon=1e-6) self.ln2 = keras.layers.LayerNormalization(epsilon=1e-6) self.ln3 = keras.layers.LayerNormalization(epsilon=1e-6) self.drop1 = keras.layers.Dropout(dropout) self.drop2 = keras.layers.Dropout(dropout) self.drop3 = keras.layers.Dropout(dropout) def call(self, x, enc_output, padding_mask, combined_mask, causal_mask): # Decoder 自身注意力用 causal_mask 屏蔽未来 token self_attn_out = self.self_attn( x, x, attention_mask=combined_mask) x = self.ln1(x + self.drop1(self_attn_out)) # cross-attention 里 query 来自 decoder,key/value 来自 encoder cross_attn_out = self.cross_attn( x, enc_output, enc_output, attention_mask=padding_mask) x = self.ln2(x + self.drop2(cross_attn_out)) ffn_out = self.ffn(x) return self.ln3(x + self.drop3(ffn_out))这里有一个实现细节:Decoder 的 self-attention 需要同时屏蔽 pad 和未来 token,所以上面代码里出现了combined_mask。常见做法是把 padding mask 和 causal mask 做逐位取最大,也就是tf.maximum(padding_mask, causal_mask),这样在任何一种需要屏蔽的位置上都会生效。cross-attention 只需要屏蔽 Encoder 输出里的 pad 位置,因为未来 token 这个约束只对 Decoder 自身有效。两个 mask 千万不要混用,否则翻译结果会出现“看到答案再作答”的诡异现象,训练 loss 低得离谱,推理时却驴唇不对马嘴。
3.4 组装成整体模型:输入三张量,输出一个概率分布
整个 Transformer 模型接受三个输入:encoder 输入序列、decoder 输入序列以及对应的 mask。这里 mask 是动态计算出来的,不是固定参数。我习惯用一个 Keras Model 把所有块串起来,这样model.fit()可以自动处理数据管道。
class Transformer(keras.Model): def __init__(self, vocab_en, vocab_zh, d_model, num_heads, dff, max_len, dropout): super().__init__() self.embed_en = keras.layers.Embedding(vocab_en, d_model) self.embed_zh = keras.layers.Embedding(vocab_zh, d_model) self.pos_enc = PositionalEncoding(d_model, max_len) self.encoder_blocks = [EncoderBlock(d_model, num_heads, dff, dropout) for _ in range(2)] # 先用 2 层 self.decoder_blocks = [DecoderBlock(d_model, num_heads, dff, dropout) for _ in range(2)] self.final_layer = keras.layers.Dense(vocab_zh) def call(self, en_input, zh_input, training=False): # 输入天然要求 [batch, seq_len],embedding 后叠加位置编码 enc_x = self.pos_enc(self.embed_en(en_input)) enc_pad_mask = create_padding_mask(en_input) for block in self.encoder_blocks: enc_x = block(enc_x, mask_convert(enc_pad_mask)) zh_pad_mask = create_padding_mask(zh_input) causal_mask = create_causal_mask(tf.shape(zh_input)[1]) combined_mask = tf.maximum(zh_pad_mask, causal_mask) dec_x = self.pos_enc(self.embed_zh(zh_input)) for block in self.decoder_blocks: dec_x = block(dec_x, enc_x, mask_convert(enc_pad_mask), mask_convert(combined_mask), causal_mask) return self.final_layer(dec_x)mask_convert其实就是tf.cast(mask > 0, tf.float32) * -1e9,新版 Keras 可以直接传 bool mask,但为了兼容性我还是转成 float。上面的代码刻意把层数压到 2 层,是因为这套源码要让你“先跑起来”。把层数加到 6 层、D_MODEL 提到 512,就是真正可用规模,但显存和训练时长会成倍增长。先让模型学会拟合一小批数据,再慢慢加容量,这是最稳的调模型路线。
4. 训练与推理:让模型真正开始翻译,而不是背诵
4.1 损失函数与学习率计划
训练翻译模型有两个默认选择:损失函数用 SparseCategoricalCrossentropy,且必须打开from_logits=True,因为模型最后一层没有接 Softmax。如果忘记这个参数,你会看到 Loss 一开始就在 7 以上,而且怎么调都降不下去,原因是 Keras 默认认为输出已经是概率分布,又在内部做了一次 softmax 归一化。学习率不能固定,Transformer 论文里给出的 Wamup 学习率计划务必照抄:先线性增长,再按步数倒数衰减,这个方案对翻译任务非常关键。
class TransformerSchedule(keras.optimizers.schedules.LearningRateSchedule): def __init__(self, d_model, warmup_steps=4000): super().__init__() self.d_model = tf.cast(d_model, tf.float32) self.warmup_steps = warmup_steps def __call__(self, step): step = tf.cast(step, tf.float32) arg1 = tf.math.rsqrt(step) arg2 = step * (self.warmup_steps ** -1.5) return tf.math.rsqrt(self.d_model) * tf.math.minimum(arg1, arg2) schedule = TransformerSchedule(D_MODEL) optimizer = keras.optimizers.Adam(schedule, beta_1=0.9, beta_2=0.98, epsilon=1e-9) loss_obj = keras.losses.SparseCategoricalCrossentropy(from_logits=True)warmup_steps=4000不是硬性标准,小数据集可以降到 1000-2000,太大反而会让模型前期收敛过慢。beta_2从默认的 0.999 改成 0.98,是针对 Transformer 这类梯度稀疏任务的常见调整,作用是在训练早期让优化器更敏感地响应梯度变化。不要小看这个数字,很多复现项目 loss 曲线一开始就震荡,十有八九没改beta_2。
4.2 训练循环与模型保存
数据集较小的时候,直接用model.fit配合 tf.data API 最省事。注意每轮训练要打乱数据顺序,并且建议按序列长度做简单的 bucket padding,否则短句和长句混在一个 batch 里,pad 比例过高,模型大部分计算都浪费在无效位置上。下面是最小可跑的代码:
train_ds = tf.data.Dataset.from_tensor_slices( (en_padded, decoder_input, target)) train_ds = (train_ds .shuffle(10000) .batch(BATCH_SIZE) .prefetch(tf.data.AUTOTUNE)) model = Transformer(VOCAB_EN, VOCAB_ZH, D_MODEL, NUM_HEAD, DFF, MAX_LEN, DROPOUT) model.compile(optimizer=optimizer, loss=loss_obj, metrics=[keras.metrics.SparseCategoricalAccuracy(name='acc')]) # 训练时额外传入一个空 dict 即可,因为 mask 在 call 里现算 model.fit(train_ds, epochs=20) # 保存权重,不保存完整模型结构,便于后续升级代码 model.save_weights('transformer_zh_en.h5')model.fit的优点是自带进度条、验证集评估和 early stopping 回调。如果你发现 loss 降得很慢,先不要怀疑模型结构,优先检查数据集 token 化有没有问题,比如中文句子被错误地按英文空格切分,或者<unk>占比超过 15%。这些数据层面的问题远远比模型结构更影响最终效果。保存权重用 h5 格式兼容性最好,新版 Keras 的.keras格式也能用,但如果你要在老项目中加载,h5 是后悔药一样的存在。
4.3 推理阶段:真正翻译时的自动回归解码
训练时模型一次性给出整句话的预测,因为用了 teacher forcing。推理时没有标准答案,只能用贪心解码或者 beam search,把上一个预测词作为下一个 decoder 输入。这是整个项目里最容易写出 bug 的地方。
def greedy_decode(model, en_sentence, max_len=MAX_LEN): en_tokens = en_tokenizer.encode(en_sentence) en_tokens = tf.constant([en_tokens], dtype=tf.int32) # decoder 初始输入只有 <start> zh_tokens = tf.constant([[START_ID]], dtype=tf.int32) for _ in range(max_len): pred = model(en_tokens, zh_tokens, training=False) next_token = tf.argmax(pred[:, -1, :], axis=-1)[-1] zh_tokens = tf.concat([zh_tokens, [[next_token]]], axis=-1) if next_token == END_ID: break return zh_tokenizer.decode(zh_tokens[0].numpy())循环里有两处要解释:一是 decoder 的输入长度一直在变,但位置编码层会根据当前长度自动截取对应位置向量,所以模型可以处理变长序列;二是每次都要重新完整前向计算一遍,没有 KV Cache,所以推理速度慢。这里有条件的项目可以缓存 Encoder 输出,因为源语言不变,Encoder 只需要跑一次;Decoder 每次迭代仍然要重算,但省掉 Encoder 后速度也已经能快近一倍。
5. 直接跑项目时最常翻车的 4 个位置与排查顺序
5.1 Loss 卡在 6 到 7 之间一动不动,甚至越训越高
现象很典型:训练和验证 Loss 都高得离谱,模型输出几乎全是<unk>。
原因分两类。第一类是from_logits设置错误,模型输出了 logits,损失函数却把它当概率分布处理。第二类是词表严重不匹配,比如训练时中文词表用的是全角字符,推理时用户输入半角标点,导致同一句话 token 化结果完全不一样。
解决方法是先打印一个 batch 的模型输出,看数值范围。logits 通常是正负几十的实数,概率值则集中在 0 到 1 之间。如果数据没问题,再检查 tokenizer 是否有未登录词占比过高的警告。源码包里通常会附一个debug_dataset.py脚本,跑一遍就能看到对齐后的句子对是否错位。
5.2 训练 loss 很低,但翻译结果是复读机
这是我见过最隐蔽的坑。模型在训练时准确率高达 90% 以上,一到推理阶段就反复输出同一个词,比如把“你好世界”翻译成“hello hello hello hello”。
原因是训练和推理的输入分布不一致,训练时 decoder 看到的是真实前词,推理时看到的是自己预测的词,一旦第一步预测错,后续全部被带偏。缓解手段有三种:一是推理时用 beam search,维护多条候选路径,避免一条道走到黑;二是数据量大的话在训练中随机把部分真实 token 替换成模型预测 token,这叫 scheduled sampling;三是降低 dropout,推理时会默认关闭 dropout,训练时如果 dropout 太大,整体模型会过度依赖训练阶段的随机噪声。实操中先用 beam search,不要一上来就动训练流程。
5.3 Loss 曲线锯齿状,每个 batch 差异巨大
训练时如果设置了验证集,会看到每个 epoch 内 acc 忽高忽低,看起来毫无规律。
原因是 batch 内部句子长度差异太大,有的句子只有 5 个 token,有的 60 个,padding 后有效信息占比波动剧烈。比如一个 batch 里全是长句,pad 少,loss 就低;另一个 batch 里全是短句大量 pad,loss 就高。
解决方法是给句子按长度分桶。把长度差在 5 个 token 以内的句子放到同一个 bucket,再从 bucket 里取样组 batch。tf.data 里可以用bucket_by_sequence_length,或者更简单地在数据预处理时先按长度排序,再按 batch_size 切片,每个 batch 内最大长度差不超过 8 个 token。这个操作不改任何模型代码,经常能让 loss 曲线立刻平滑下来。
5.4 中文“词”没有被真正切出来
如果模型对“人工智能”这种词翻译得七零八落,多半不是模型问题,是中文切分问题。
直接用字符级 token 的表会让模型需要更多步数才能学到词级语义。理想方案是 jieba 分词 + 子词融合,但更省事的是用 SentPiece 在中文语料上做 unigram 模型,词表大小 8000 到 20000 都比较常见。注意不要拿英文的 BPE 模型直接套中文,中文里单字即语义单元,BPE 合并后容易产生没有意义的整词碎片。
解决方法是先把中文文本用 jieba.cut 切分,再把切分结果送入 tokenizer。源码里如果 tokenizer 是直接用字符扫的,改成先切分再编码通常能立刻提升几个百分点的 BLEU。
6. 把翻译质量往上再拔一档:BLEU、Beam Search 与调优顺序
6.1 极简 Beam Search 写法和效果差异
贪心解码每一步只挑概率最大的词,一旦开头选错后面很难回头。Beam Search 的做法是每一步保留 top-k 条候选路径,最终从 k 条路径里选整句概率最高的一条。k=3 到 5 时性价比最高,k 再大推理时间会明显增加,但效果提升趋于平缓。
def beam_decode(model, en_tokens, beam_size=4, max_len=64): start = [[START_ID]] scores = [0.0] for _ in range(max_len): all_candidates = [] for seq, score in zip(start, scores): zh_input = tf.constant([seq], dtype=tf.int32) pred = model(en_tokens, zh_input, training=False) log_probs = tf.nn.log_softmax(pred[0, -1, :]) # 取 log 概率 top_k = tf.math.top_k(log_probs, k=beam_size) for token, logp in zip(top_k.indices.numpy(), top_k.values.numpy()): all_candidates.append((seq + [token], score + logp)) # 按总分排序,只保留 beam_size 条 all_candidates.sort(key=lambda x: x[1], reverse=True) start = [cand[0] for cand in all_candidates[:beam_size]] scores = [cand[1] for cand in all_candidates[:beam_size]] if any(seq[-1] == END_ID for seq in start): break return start[scores.index(max(scores))]需要注意的是这里用log_softmax而不是 softmax,因为多步概率相乘时浮点数会下溢,累加 log 概率才稳定。beam search 做出来之后,你还会发现一个问题:beam size 越大,句子越倾向安全、平淡的翻译,因为概率累加惩罚了长句。这时候可以引入 length normalization,常见的做法是除以(seq_len ** 0.7),这段修正会让偏短的译文明显改善,这也是很多项目直接把论文里的 alpha=0.7 拿来做默认值的原因。
6.2 评估:别只盯着“翻得顺不顺”
人工看几条翻译结果容易产生错觉,最好用 BLEU 做客观评估。最简单的计算方式是sacrebleu库,把参考译文和模型译文按行喂进去。需要注意 BLEU 对中文这种分词方式敏感,中译英时官方标准通常按空格分词。建议固定 200 句测试集,每次改动后都跑同一份,记录 BLEU 和 chrF 两个指标。BLEU 提升 1 个点可能对应一次成功的调参,也可能是运气波动,至少要重复三次取平均。
我做这类中英翻译项目的习惯是:先把英译中和中译英都跑一个 2 层的迷你模型,确认 pipeline 完整、loss 能降,再逐步加深到 4 层、6 层并调大 D_MODEL;每次改动只动一个变。这套基于 python 的 kreas-transformer 源码适合作为起步骨架,但真正要投入生产还需要加入领域语料、长度标准化和更细的评估集。先把上面几个坑绕过去,你的第一版翻译模型很快就能跑出像样的句子,希望帮到你。
本文还有配套的精品资源,点击获取