ARTICLE DETAIL

资讯详情

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

Transformer机器翻译源码实战:数据流、训练避坑与BLEU评测

Transformer机器翻译源码实战:数据流、训练避坑与BLEU评测 简介Transformer凭借自注意力机制取代循环网络成为神经机器翻译的主流架构其长距离依赖建模能力与并行训练优势让开发者能高效构建翻译系统。理解词表构建、padding与mask的配合是跑通源码链路的关键也是控制显存开销与训练稳定性的基础。在工程落地中合理配置学习率、teacher forcing与beam search参数直接影响模型收敛效果与推理质量。本文从一段毕业作品级源码出发拆解Transformer翻译系统的数据流实现结合sacrebleu评测梳理loss不降、显存溢出等高频避坑方案适合作为翻译任务深度学习的实践入门指南。1. 拿到毕业作品级的 Transformer 翻译源码第一件事不是跑通而是看懂数据流把一份《深度学习基于 Transformer 的机器翻译系统 python 源码毕业作品.zip》解压后多数人的第一反应是pip install -r requirements.txt然后直接python train.py。我的建议是反过来先不急着训练先在编辑器里把一条样本从文本变成 loss 的路径走一遍。这类毕设源码通常只有几千行模型定义往往不是瓶颈真正的复杂度全在词表构建、padding 与 mask 的配合上。这篇笔记要拆的正是这条链路毕设为什么清一色选 Transformer 做机器翻译、数据预处理怎么写、训练参数怎么定、loss 不降和显存溢出分别怎么排最后拿 BLEU 证明系统真的能翻。它适合正在做翻译类毕业设计的学生也适合想低成本复现一个翻译 baseline 的深度学习入门者。2. Transformer 翻译系统为什么这套架构成了毕设标配2.1 从 Seq2Seq 到 Self-Attention选 Transformer 的四个现实理由在 Transformer 出现之前神经机器翻译的典型基线是 LSTM/GRU 组成的 Seq2Seq。编码器把源语言句子读成一个固定维度的向量解码器再从这个向量中逐词生成译文。这个设计的瓶颈有两个一是最后一个隐藏状态要“打包”整句话的信息句子超过三四十个词后面的词基本记不住二是循环结构必须按时间步展开batch 内句子越长训练一个 step 的开销线性上涨显存占用也不好控制。到现在不少课程习题还在用 Seq2Seq Attention 做课后题但真实项目里已经很少用它来落地翻译了。Transformer 用自注意力替代循环让任意两个位置的 token 可以直接计算相关度长距离依赖被压缩成一次矩阵运算多头注意力又把同一句话拆进多个子空间模型可以同时关注语法关系、指代关系和语义相近的词。加上位置编码token 的顺序信息也没有丢。对毕设而言选它有三个很现实的理由第一代码量小没有循环和隐状态传递几百行 PyTorch 就能写一个能跑的最小实现第二PyTorch 内置nn.Transformer很多源码包其实是在这个接口外面套了一层自己的封装读起来省力第三同一套 encoder-decoder 骨架改改输入输出就能做分类、序列预测甚至 vision transformer 图像任务答辩时“架构通用性”是一个送分点。网上关于 Transformer 模型详解的帖子很多但如果手边有《动手学深度学习》直接看它对应章节比啃原论文快得多。不过要泼一盆冷水选型成熟不代表训练容易。Transformer 对学习率、mask 和数据的敏感度远高于 LSTM原论文里那个 6 层、d_model512 的配置是给千万句级 WMT 语料调的毕设语料经常只有几万到几十万句直接照抄必过拟合。这是后面几章要反复强调的取舍也是这个方向最容易翻车的地方。2.2 源码包里最常见的骨架Encoder、Decoder 与最小 PyTorch 实现这类毕业作品源码的目录结构通常很规矩transformer.py放模型定义dataset.py放数据读取和词表train.py放训练循环translate.py放推理脚本。模型部分无论外面包了多少层核心都是位置编码、多头注意力、前馈网络、残差连接和层归一化。下面是我在本地复刻的最小版本去掉了 dropout 和参数初始化逻辑和 PyTorch 官方示例保持一致import torch import torch.nn as nn import math class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len5000): super().__init__() pe torch.zeros(max_len, d_model) position torch.arange(0, max_len, dtypetorch.float).unsqueeze(1) div_term torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) self.register_buffer(pe, pe) # 不参与梯度更新 def forward(self, x): return x self.pe[:, :x.size(1)] class Translator(nn.Module): def __init__(self, src_vocab, tgt_vocab, d_model512, nhead8, num_layers6, dim_ff2048): super().__init__() self.src_embed nn.Embedding(src_vocab, d_model) self.tgt_embed nn.Embedding(tgt_vocab, d_model) self.pos PositionalEncoding(d_model) self.core nn.Transformer(d_modeld_model, nheadnhead, num_encoder_layersnum_layers, num_decoder_layersnum_layers, dim_feedforwarddim_ff, batch_firstTrue) self.fc_out nn.Linear(d_model, tgt_vocab) def forward(self, src, tgt, src_key_padding_mask, tgt_mask, tgt_key_padding_mask, memory_key_padding_mask): src self.pos(self.src_embed(src)) tgt self.pos(self.tgt_embed(tgt)) memory self.core.encoder(src, src_key_padding_masksrc_key_padding_mask) out self.core.decoder(tgt, memory, tgt_masktgt_mask, tgt_key_padding_masktgt_key_padding_mask, memory_key_padding_maskmemory_key_padding_mask) return self.fc_out(out)这段代码有三处要对齐你自己的 PyTorch 版本。一是 register_buffer 把位置编码注册成非参数张量存模型时它跟着 state_dict 走但不会出现在优化器里二是nn.Transformer的 encoder 和 decoder 默认都是 6 层这个数字在毕设数据量下通常要减半后面 4.1 的配置表会再展开三是batch_firstTrue让输入输出都是 (batch, seq_len, d_model)调试时打印 shape 更直观。fc_out输出的形状是 (batch, tgt_len, tgt_vocab)送进交叉熵前要把后两维 reshape 成 (batch * tgt_len, tgt_vocab)。把张量形状变化写出来比反复读论文更管用。假设 batch4源语言句子最长 30 个 token目标语言最长 40 个 token那么 src 的初始形状是 (4, 30)tgt 是 (4, 40)经过 embedding 和位置编码后变成 (4, 30, 512) 和 (4, 40, 512)encoder 输出仍是 (4, 30, 512)这也就是 memorydecoder 拿 tgt 的 (4, 40, 512) 做 query拿 memory 做 key 和 value输出 (4, 40, 512)最后的线性层把最后一维映射到目标词表大小得到 (4, 40, 32000)。中间任何一维对不上基本都是 embedding 维度、max_len 或 mask 形状的问题。2.3 一张样本从文本到 loss 的完整路径把数据流走一遍三类 mask 的设计动机就清楚了。原始句子先按空格或分词工具切成 token再查词表映射成 id 序列编码器侧把 id 查 embedding 得到向量矩阵加上位置编码后进自注意力解码器侧每一步只能看到目标语言已经生成的词为了训练时能并行用 teacher forcing 一次性喂入整个目标句子右移一位同时用一个上三角 mask 把未来位置遮住。三个 mask 的分工要记牢。tgt_mask 是 (tgt_len, tgt_len) 的上三角矩阵PyTorch 约定对角线及以上为 True 表示“遮住”作用是不让解码器偷看未来src_key_padding_mask 和 memory_key_padding_mask 都是 (batch, src_len) 的布尔矩阵padding 位置为 True作用是把无效的pad排除出注意力计算decode 时目标侧也有 tgt_key_padding_mask形状是 (batch, tgt_len)。新手最容易踩的隐性坑是布尔语义很多老教程里 0/1 的含义和 PyTorch 正好相反直接抄过来会得到 mask 全部失效注意力把 padding 也算了进去。loss 的计算也有一个容易错的点。训练样本 tgt 会被拆成两份tgt_input等于tgt[:, :-1]前面人为补了一个bostgt_output等于tgt[:, 1:]最后跟着eos。模型输出每个位置的预测分布和 tgt_output 逐位算交叉熵并且要把 pad_idx 设进 ignore_index否则大量 padding 位置会稀释真实词的梯度。这一步看着不起眼但 5.1 节要讲的 loss 不降十有八九死在这里。再有就是数据本身的干净程度文件读取时句子没去重、没清洗词表里混进残缺 token 和孤立标点模型要花大量容量去拟合噪声。下一步的数据预处理就是专门处理这些问题。3. 把数据喂进模型词表构建、padding 与 mask 的三件套3.1 词表构建word-level 起步BPE 留给答辩加分词表是整个链路里第一个分水岭。毕设语料通常不大我建议直接用 word-level 分词也就是按空格把句子切成词。它的优势是直观、可解释词表可以直接打印出来检查复现成本低缺点是遇到中英混合、粘连标点和罕见词时表现差。如果导师要求提效果再往 BPE/子词方向走用 sentencepiece 或 tokenizers 库训练一个子词模型词表可以压到 8k~32k同时把未登录词问题基本解决。但对第一次跑通链路来说word-level 完全够用关键是词表本身要干净。from collections import Counter def build_vocab(sentences, vocab_size32000, min_freq2): counter Counter() for s in sentences: counter.update(s.strip().split()) vocab {pad: 0, bos: 1, eos: 2, unk: 3} for w, c in counter.most_common(vocab_size - 4): if c min_freq: vocab[w] len(vocab) else: break return vocab这里最核心的约定是pad必须是 0。这不是强迫症而是因为后面collate_fn里torch.full默认用 0 填充所有 padding mask 都依赖这个编号。bos、eos、unk紧跟其后顺序固定推理脚本里才不会搞混。min_freq2会把只出现一次的词丢掉这些词通常是拼写错误或语料噪声留着只会让模型去背噪声most_common按频次降序遇到低于 min_freq 的单词就 break所以实际词表可能到不了 vocab_size这是正常的。词表建好后建议统计一下验证集里unk的比例超过 10% 就说明词表太小或分词粒度不对需要调 vocab_size 或改用子词。3.2 collate_fn 与三种 mask变长 batch 的标准写法PyTorch 的 DataLoader 只负责按 batch_size 取样本它不知道句子是变长的所以需要 collate_fn 把列表里的句子 pad 成同一个长度。常见的错误是一律 pad 到全局 max_len128明明一个 batch 最长只有 20 个词却要按 128 算注意力显存和算力都被浪费掉。正确做法是 pad 到当前 batch 的最大长度同时在 batch 内按长度排序让相近长度的句子凑在一起padding 比例能显著下降。import torch def collate_fn(batch, pad_idx0): src, tgt zip(*batch) max_src max(len(s) for s in src) max_tgt max(len(t) for t in tgt) src_padded torch.full((len(batch), max_src), pad_idx, dtypetorch.long) tgt_padded torch.full((len(batch), max_tgt), pad_idx, dtypetorch.long) for i, (s, t) in enumerate(zip(src, tgt)): src_padded[i, :len(s)] torch.tensor(s, dtypetorch.long) tgt_padded[i, :len(t)] torch.tensor(t, dtypetorch.long) return src_padded, tgt_padded def make_masks(src, tgt, pad_idx): src_pad_mask (src pad_idx) # (batch, src_len) tgt_pad_mask (tgt pad_idx) # (batch, tgt_len) tgt_mask torch.triu( torch.ones(tgt.size(1), tgt.size(1), devicesrc.device), diagonal1 ).bool() return src_pad_mask, tgt_mask, tgt_pad_maskcollate_fn 返回的两个张量形状分别是 (batch, max_src) 和 (batch, max_tgt)max_src、max_tgt来自当前 batch 而不是全局配置这是省显存的关键。make_masks 接收 pad 后的张量一次性生成三种 masksrc_pad_mask直接比较 src 是否等于 pad_idx得到布尔矩阵tgt_mask用torch.triu生成上三角diagonal1表示从右上角第一条对角线开始置 True含义是“遮住当前位置之后的未来词”tgt_pad_mask同理。这三样东西每次 forward 都要传入漏掉任何一个注意力计算都会把 padding 位置或未来位置算进去训练时看不出明显问题推理时译文会莫名变差。3.3 数据清洗与长度过滤小语料省显存的第一步除了词表原始语料的清洗往往决定训练能不能稳定。我的固定流程是先按行读入平行语料丢掉空行和明显乱码的行再统一语言侧的分词方式英文按空格分词并处理标点粘连中文可以先用 jieba 分词或者干脆按字切分——按字切分在词表很小的时候反而稳最后去掉重复句对。重复句对会让模型把同样的输入输出背下来验证集里如果混了训练集的重复句BLEU 会被虚高答辩时被追问会很难看。import random def filter_pairs(pairs, max_len128, min_len1): out [] for src, tgt in pairs: if min_len len(src) max_len and min_len len(tgt) max_len: out.append((src, tgt)) return out def bucket_batches(pairs, batch_size): buckets {} for p in pairs: key len(p[0]) // 10 * 10 # 按源语言长度每 10 词一桶 buckets.setdefault(key, []).append(p) for k in buckets: random.shuffle(buckets[k]) for k in sorted(buckets): for i in range(0, len(buckets[k]), batch_size): yield buckets[k][i:i batch_size]长度过滤是省显存和稳定训练的关键。Transformer 的注意力复杂度是 O(n²)句子越长内存和耗时涨得越快。毕设语料我一般设 max_len64 到 128超过的直接丢弃同时设 min_len1避免空句和单标点句进入训练。bucket_batches是另一个实用技巧把语料按长度分成若干桶每次从桶里取 batch而不是全局随机 shuffle。这样做可以让一个 batch 内的句子长度差距变小padding 比例下降训练速度在同样的 batch_size 下能快 30% 以上。注意这个简化版按源语言长度分桶如果你的目标语言长度差异更大可以改成按 max(len(src), len(tgt)) 分桶。4. 训练与推理学习率、teacher forcing 与 beam search 的落地写法4.1 必调参数batch size、max_len、学习率与 warmup 的配置表先给一张我常用的配置表它适用于单卡 8G 显存、几万到几十万句的翻译语料。这个配置不是我拍脑袋定的而是从原论文配置往下缩出来的缩的幅度由数据量决定。参数常见值作用踩过的坑d_model256向量维度512 在小语料上必过拟合num_layers3~4encoder/decoder 层数6 层是 WMT 配置别照抄batch_size32~64每步样本数8G 显存下 64 可能 OOMmax_len64~128句子长度上限过短丢信息过长吃显存lr1e-3Adam 初始学习率不开 warmup 就 1e-3 会炸warmup_steps4000学习率预热不设则 loss 早期剧烈震荡label_smoothing0.1标签平滑不设容易过拟合clip_grad_norm1.0梯度裁剪不设长句梯度容易爆炸配置的核心逻辑是“跟着数据量走”。原论文 d_model512、6 层、warmup4000那是几十万到上百万句的 WMT 语料调出来的。毕设语料如果只有几万句512 维的 embedding 会把训练集整个背下来验证集 BLEU 反而个位数。我一般先用 256 维、3 层、batch32 跑通再逐步加大。学习率方面Adam 的默认 lr 是 1e-3但 Transformer 的梯度尺度对 lr 非常敏感必须配 warmup 或直接降到 3e-4 固定值。注意 batch_size 和 max_len 决定了一个 step 的实际 token 数如果显存溢出优先减 max_len 而不是 batch_size因为超长句对注意力的内存消耗是二次方级的。4.2 训练循环teacher forcing、ignore_index 与梯度裁剪训练循环的骨架在各类翻译源码里高度一致差别主要在 mask 的传递和 loss 的忽略位上。下面这段是完整可跑的一个 epoch 写法基于前面 2.2 的模型和 3.2 的 mask 函数def train_one_epoch(model, loader, optimizer, criterion, scheduler, device, pad_idx, clip_norm1.0): model.train() total_loss 0.0 for src, tgt in loader: src, tgt src.to(device), tgt.to(device) tgt_input tgt[:, :-1] # 去掉最后一个 eos tgt_output tgt[:, 1:] # 去掉开头的 bos src_pad_mask, tgt_mask, tgt_pad_mask make_masks(src, tgt_input, pad_idx) logits model(src, tgt_input, src_key_padding_masksrc_pad_mask, tgt_masktgt_mask, tgt_key_padding_masktgt_pad_mask, memory_key_padding_masksrc_pad_mask) loss criterion(logits.reshape(-1, logits.size(-1)), tgt_output.reshape(-1)) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), clip_norm) optimizer.step() scheduler.step() total_loss loss.item() return total_loss / len(loader)这里有两个关键约定。一是tgt_input tgt[:, :-1]、tgt_output tgt[:, 1:]前者负责喂进 decoder后者是每个位置要预测的真实词两者形状完全一致二是criterion必须是nn.CrossEntropyLoss(ignore_indexpad_idx)把 padding 位置的 loss 全部忽略否则大量pad会稀释真实词的梯度loss 看起来居高不下。梯度裁剪clip_grad_norm_是 Transformer 训练的保险丝长句样本的梯度范数可能异常大不裁剪的话一个 batch 就能把 embedding 打飞。如果要用原论文的 Noam 学习率调度可以自己实现一个十行的小类逻辑是 d_model 的 -0.5 次方乘以 step 和 warmup 两个约束的最小值。它的效果是前 warmup 步学习率线性上涨之后按 step 的 -0.5 次方衰减。注意scheduler.step()要在optimizer.step()之后调用PyTorch 2.x 的 LambdaLR 也是这个顺序写反了学习率会慢一拍。class NoamScheduler: def __init__(self, optimizer, d_model, warmup_steps4000): self.optimizer optimizer self.d_model d_model self.warmup warmup_steps self._step 0 def step(self): self._step 1 lr self.d_model ** -0.5 * min(self._step ** -0.5, self._step * self.warmup ** -1.5) for g in self.optimizer.param_groups: g[lr] lr用这个类时Adam 的 lr 参数会被覆盖所以torch.optim.Adam(model.parameters())里可以不用传 lr。如果你的训练循环里已经有optimizer.step()那scheduler.step()只负责更新 lr不要在里面再调optimizer.step()否则一个 batch 会更新两次参数。4.3 推理端greedy 起步beam search 提分训练完成后推理是另一个故事。最简单的是 greedy每一步选概率最大的词拼成译文。速度快但容易陷入局部最优典型表现是译文平淡、容易提前结束。要提分就用 beam search它每一步保留概率最高的 k 条候选路径最后再选一条整体分数最高的。对毕设语料beam_width4 或 5 性价比最高再往上推理时间成倍增加BLEU 提升往往不到 1 个点。def translate_beam(model, src_ids, beam_width4, max_len50, bos_idx1, eos_idx2, pad_idx0, devicecpu): model.eval() src src_ids.unsqueeze(0).to(device) src_pad_mask (src pad_idx).to(device) memory model.core.encoder( model.pos(model.src_embed(src)), src_key_padding_masksrc_pad_mask ) beams [([bos_idx], 0.0)] for _ in range(max_len): candidates [] for seq, score in beams: if seq[-1] eos_idx: candidates.append((seq, score)) continue tgt torch.tensor([seq], devicedevice) tgt_mask torch.triu( torch.ones(tgt.size(1), tgt.size(1), devicedevice), diagonal1 ).bool() logits model.core.decoder( model.pos(model.tgt_embed(tgt)), memory, tgt_masktgt_mask ) logits model.fc_out(logits[:, -1, :]) # 只看最后一个位置 logp torch.log_softmax(logits, dim-1) topk logp.topk(beam_width, dim-1) for i in range(beam_width): new_seq seq [topk.indices[0, i].item()] new_score score topk.values[0, i].item() candidates.append((new_seq, new_score)) beams sorted(candidates, keylambda x: x[1], reverseTrue)[:beam_width] if all(b[0][-1] eos_idx for b in beams): break best max(beams, keylambda x: x[1] / (len(x[0]) ** 0.7)) return best[0][1:-1] if len(best[0]) 2 else []这段是教学用的简化版把 encode 和 decode 拆开写是为了让你看明白每一段在干什么。beam search 的停止条件是所有候选都以eos结尾或达到 max_len。选择最终句时不能只看累计 log 概率否则模型天然偏好短句所以加长度惩罚score / len(seq) ** 0.7。这个 0.7 是经验值想更稳可以试 0.6~1.0。另外这个简化版省略了 memory padding mask单句推理没影响批量评测时要补上否则 batch 内不同长度的句子会互相干扰。5. 避坑指南Transformer 翻译最容易翻车的五个地方这些坑是我自己跑翻译类项目时踩过的也是毕设答辩前后问得最多的问题。每一条按“现象 → 原因 → 解决”的顺序写对照你自己的日志排查比从头翻代码快。5.1 loss 不降或抖动剧烈先检查 target shift 与 ignore_index现象训练几十步后 loss 基本不动或者在一个值附近剧烈抖动打印出的 loss 一直是 9~10 这种“天文数字”。原因最常见的是 tgt_input 和 tgt_output 没有错位decoder 输入和目标完全一样模型学到的是恒等映射loss 自然降不下去其次是 CrossEntropyLoss 没设 ignore_indexpadding 位置全部参与 loss 计算噪声把梯度方向带偏了第三是词表unk比例过高大量输入被替换成同一个 token模型无从学起。解决打印一个 batch确认tgt_input[0]的前三个 id 是不是[bos, 第一个词, ...]tgt_output[0]是不是[第一个词, ..., eos]把 criterion 改成nn.CrossEntropyLoss(ignore_index0)再统计验证集 unk 比例超过 10% 就回 3.1 重做词表或改子词。5.2 训练集没问题、验证集 BLEU 个位数评估脚本和分词粒度在背锅现象训练 loss 正常下降人工看训练集译文也像模像样但验证集 BLEU 只有 5~10甚至不如随机。原因第一验证集里混入了训练集的重复句模型“背”出来的分数虚高而换到真正的新句子上就现原形第二评估时没去掉bos和eos特殊 token 参与了 n-gram 统计第三模型侧英文按空格分词而 BLEU 计算时用 sacrebleu 默认的 13a 切分两者粒度不一致分数会被系统性拉低。中英混合时这个差异尤其明显。解决切分验证集时按句子哈希去重保证和训练集零 overlap评估前把特殊 token 剥掉中英翻译用corpus_bleu(hyps, refs, tokenizezh)英文用tokenize13a并把这个配置固定下来写进答辩文档避免别人质疑你的评估口径。5.3 显存溢出batch_size、max_len 和梯度累积三板斧现象训练跑不到一个 epochPyTorch 报CUDA out of memory而且往往不是一开始就报是跑到某条特定句子附近才崩。原因Transformer 的注意力内存随序列长度平方增长。很多时候不是 batch 太大而是 batch 里混进了一条超长句它一个就占掉了整块显存。解决先加长度过滤把 max_len 压到 64把 batch 里的最长句隔离出来人工检查再把 batch_size 减半如果还想保住等效 batch用梯度累积每 k 个 step 累加梯度、更新一次参数代码上就是把optimizer.zero_grad()和optimizer.step()移到一个 if 条件里。注意配合 NoamScheduler 时学习率要在真实更新那一步才调一次scheduler 的 step 次数按真实更新算而不是按每个小 batch 算。5.4 译文反复重复同一个词beam search 缺长度惩罚现象推理结果形如 “the the the the the ...”或者中文里某个高频词连续出现而打分看起来还挺高。原因训练语料里某些词出现频率过高模型在不确定的位置倾向于输出高频词beam search 累计 log 概率时没有惩罚重复 n-gram导致重复路径一路保持高分。解决在 beam 的累计分数里对已经生成的 n-gram 做惩罚例如每出现一个新的 bigram 加一个小奖励、重复出现的 bigram 扣分这是简单的重复惩罚再配合长度惩罚score / len ** 0.7两者一起上重复现象基本消失。如果还不行回查训练语料看看是不是有大量整句重复把重复句对去掉再训。5.5 复现对不上随机种子、设备差异和浮点累积现象同一条命令在不同机器上跑loss 曲线和 BLEU 分数对不上甚至同一台机器两次结果不同。原因PyTorch 的 CUDA 卷积和注意力在浮点累加上有非确定性另外没设随机种子时数据 shuffle 和 Dropout 每一次都不一样。解决在 train.py 开头固定三样torch.manual_seed(42)、random.seed(42)、numpy.random.seed(42)再用torch.backends.cudnn.deterministic True和torch.backends.cudnn.benchmark False换确定性算法。这样至少同配置可复现答辩时别人问“你这个结果怎么来的”你能现场跑出同一份数字。注意数据加载器的 worker 也要设种子常见做法是在 DataLoader 里传一个worker_init_fn给每个 worker 单独设一次 seed。6. 验证与进阶BLEU 打分、译文检查与三个值得做的方向6.1 用 BLEU 分数说话sacrebleu 的评估姿势很多毕设还在用 NLTK 的 BLEU 脚本我建议换成 sacrebleu接口更简单分词口径也更规范。至少要 200~500 句才能算 corpus_bleu单句 BLEU 没有统计意义对中英翻译几万句语料训出的模型 BLEU 在 15~30 之间都算正常关键是拿它和 baseline比如 LSTM Seq2Seq对比以及看训练/验证集上的差距而不是纠结绝对数值。from sacrebleu import corpus_bleu hyps, refs [], [] for src, tgt in eval_pairs: pred translate_beam(model, torch.tensor(src, dtypetorch.long), beam_width4) hyps.append( .join(ids_to_tokens(pred))) refs.append([ .join(ids_to_tokens(tgt))]) score corpus_bleu(hyps, refs, tokenizezh if is_chinese else 13a) print(fBLEU: {score.score:.2f})6.2 人工检查译文的四个维度自动指标之外自己读一遍译文才能发现 BLEU 看不出的事。我习惯从四方面看一是忠实度源句子里的专有名词、数字、否定词有没有丢掉二是流畅度译文读起来像不像人话三是长度是不是每句都比参考译文短一截如果是多半是长度惩罚没调好四是标点与大小写是否一致。把翻译失败的案例单独存成一个文件答辩前专门看这类案例比盯着总分有用得多。6.3 拿到这份源码之后三个值得继续做的方向如果基础链路已经跑通下一步我建议按性价比排这三件事。第一把 word-level 换成 BPE 子词几行配置就能降低unk比例通常能提升 2~4 个 BLEU第二给训练加上 label smoothing 和 Noam 调度这俩是原论文标配能明显压过拟合第三如果你对同一套架构更感兴趣可以把 encoder 单独拆出来做文本分类或者换成 vision transformer 处理图像把“Transformer 迁移到其他任务”写进论文的展望部分。毕竟机器翻译只是 Transformer 的一个落地点理解了数据流和 mask做其他序列任务就只剩换数据的事了。我自己的习惯是每完成一个阶段就把“验证集 BLEU、复现命令、随机种子”写进一个 README 文件因为答辩前一天你一定会忘掉 beam_width 到底设的 4 还是 5。这个习惯救过我很多次希望帮到你。本文还有配套的精品资源点击获取
返回列表