fairseq 字节级子词神经机器翻译:基于 IWSLT17 法英任务的 BBPE 完整实践指南
【免费下载链接】unilmLarge-scale Self-supervised Pre-training Across Tasks, Languages, and Modalities项目地址: https://gitcode.com/GitHub_Trending/un/unilm
导读
本文以 fairseq 官方示例byte_level_bpe为骨架,系统讲解字节级子词(Byte-Level Subwords / BBPE)技术在神经机器翻译中的完整落地路径:从 IWSLT 2017 Fr-En 数据集的获取与多套词表(bytes / chars / BPE / BBPE)的构建,到使用带 Bi-GRU 嵌入上下文化的 Transformer 模型(gru_transformer)完成训练、生成与交互式推理,并给出各词表方案的 BLEU 对比结果。读完本文,你将掌握 BBPE 的原理、fairseq 中对应的编码器/解码器实现(bytes、characters、byte_bpe、sentencepiece),以及一套可直接复现的端到端命令流程。
一、背景:为什么要做字节级子词(BBPE)
传统基于词或子词的翻译系统面临两大痛点:
- 词表爆炸与 OOV(未登录词)问题:按词切分需要覆盖海量词汇,且难以处理新词、专有名词、拼写变体;
- 跨语言符号不一致:不同语言使用不同字符集,联合词表往往需要很大的词汇量才能覆盖。
Byte-Level BPE(BBPE)的思路是:先把文本按 UTF-8 编码转成字节序列,再对字节序列学习 BPE 合并规则。由于所有语言共用同一套 UTF-8 字节空间(256 个字节),BBPE 天然具备多语言友好、词表紧凑、理论上无 OOV 的特性。本示例参考论文Neural Machine Translation with Byte-Level Subwords(Wang, Cho & Gu, 2019,arXiv:1909.03341),在 fairseq 中给出了完整实现,并以 IWSLT 2017 Fr-En 为基准任务。
在当前的 unilm 仓库中,该示例位于 decoding/IAD/fairseq/examples/byte_level_bpe,包含三个核心文件:
| 文件 | 作用 |
|---|---|
| get_data.sh | 一键下载 IWSLT17 数据、构建各词表并生成 fairseq 二进数据集 |
| get_bitext.py | 双语语料的清洗、Moses 预分词、各词表切分与 SentencePiece 模型训练 |
| gru_transformer.py | 注册gru_transformer模型:在 Transformer 编码器中加入 Bi-GRU 对嵌入做上下文化 |
二、数据准备:构建 6 套词表的完整流水线
2.1 一键脚本 get_data.sh
原文档给出的数据获取方式非常简洁:
bash ./get_data.sh但脚本内部实际完成了五步工作(见 get_data.sh):
- 安装依赖:
pip install sentencepiece sacremoses; - 下载 IWSLT 2017 Fr-En 语料:从
wit3.fbk.eu下载fr-en.tgz并解压到data/; - 调用 get_bitext.py 构建多套词表数据:
${PY_BIN_ROOT}python get_bitext.py --bpe-vocab 16384 --byte-vocab --char-vocab for VOCAB_SIZE in 2048 4096; do ${PY_BIN_ROOT}python get_bitext.py --bpe-vocab ${VOCAB_SIZE} --bbpe-vocab ${VOCAB_SIZE} done这一步会生成 BPE 16k、BPE 2k/4k、BBPE 2k/4k、bytes、chars 共 6 类切分文本;
- 清理原始文件:
rm -r data/fr-en data/fr-en.tgz; - 对每类词表分别执行
fairseq-preprocess生成二进制数据集,目标目录分别为data/bin_bpe16384、data/bin_bytes、data/bin_chars、data/bin_bbpe2048、data/bin_bpe2048、data/bin_bbpe4096、data/bin_bpe4096。
其中fairseq-preprocess统一使用--joined-dictionary(法英共享联合词典)与--workers "$(nproc)"(按机器核数并行):
fairseq-preprocess --source-lang fr --target-lang en --destdir data/bin_bpe16384 --joined-dictionary \ --workers "$(nproc)" --trainpref data/train.moses.bpe16384 --validpref data/valid.moses.bpe16384 \ --testpref data/test.moses.bpe163842.2 词表构建细节(get_bitext.py)
get_bitext.py 是数据流水线的核心,其处理链路可拆解为:
Step 1 — 语料清洗:从train.tags.fr-en.fr/en提取正文(跳过<...>标签行),从IWSLT17.TED.dev2010(验证集)与IWSLT17.TED.tst2015(测试集)的 XML 中抽取<seg>段内容。
Step 2 — Moses 预分词:调用 fairseq 内置的MosesTokenizer(实现在 moses_tokenizer.py),对法英两侧做常规化切分,输出*.moses.*文件。
Step 3 — 按词表类型切分(对应脚本中的 4 个分支):
- BPE(
--bpe-vocab K):将法英训练语料拼接成train.all,用sp.SentencePieceTrainer.Train以--model_type=bpe、--character_coverage=1.0、--normalization_rule_name=identity训练spm_bpeK.model,再通过SentencepieceBPE(sentencepiece_bpe.py)应用到 train/valid/test; - BBPE(
--bbpe-vocab K):先把训练语料逐行做字节编码得到train.bchar(调用byte_encode),再在字节序列上训练spm_bbpeK.model,应用时使用ByteBPE(byte_bpe.py); - Bytes(
--byte-vocab):直接对每行调用Bytes.encode,将文本转成逐字节 token; - Chars(
--char-vocab):调用Characters.encode,按字符切分(空格转义为▁)。
命令行参数一览(python get_bitext.py --help语义):
| 参数 | 类型 | 说明 |
|---|---|---|
--root | str,默认data | 数据根目录 |
--bpe-vocab K | int | 生成 BPE 词表大小为 K 的切分语料,默认禁用 |
--bbpe-vocab K | int | 生成 BBPE 词表大小为 K 的切分语料,默认禁用 |
--byte-vocab | flag | 生成逐字节词表的切分语料 |
--char-vocab | flag | 生成逐字符词表的切分语料 |
2.3 底层字节编解码原理
BBPE 的"字节层"完全由 byte_utils.py 承担,核心是双射字节映射表:
PRINTABLE_LATIN = set(list(range(32, 126+1)) + list(range(161, 172+1)) + list(range(174, 255+1))) BYTE_TO_BCHAR = {b: chr(b) if b in PRINTABLE_LATIN else chr(256 + b) for b in range(256)} BCHAR_TO_BYTE = {bc: b for b, bc in BYTE_TO_BCHAR.items()}- 编码
byte_encode(x):先做空白归一化(连续空白折叠为单个空格),再utf-8编码后把每个字节映射为一个字符(可打印拉丁字节保留原样,其余映射到chr(256+b)的不可打印区),从而得到一个无空格冲突的单字符序列,便于送入 SentencePiece; - 解码
byte_decode(x):按映射表逆变换回字节并decode("utf-8"); - 容错解码
smart_byte_decode(x):当字节序列因切分导致 UTF-8 断裂(decode抛ValueError)时,采用动态规划(代码中f[i]/pt[i]数组)在 1~4 字节窗口内寻找"有效字符数最多"的最优恢复路径,避免输出乱码。
Bytes与Characters两个注册 BPE 类(bytes.py、characters.py)均以▁(chr(9601))转义空格后按 token 用空格拼接输出,保证序列化格式与 fairseq 分词器接口一致。ByteBPE.encode则是"先byte_encode再 SentencePiece 切分"的完整 BBPE 前向路径。
三、模型训练:Transformer + Bi-GRU 嵌入上下文化
3.1 训练命令
原文档给出的训练命令通过环境变量切换词表类型:
# VOCAB=bytes # VOCAB=chars VOCAB=bbpe2048 # VOCAB=bpe2048 # VOCAB=bbpe4096 # VOCAB=bpe4096 # VOCAB=bpe16384fairseq-train "data/bin_${VOCAB}" --task translation --user-dir examples/byte_level_bpe/gru_transformer \ --arch gru_transformer --encoder-layers 2 --decoder-layers 2 --dropout 0.3 --share-all-embeddings \ --optimizer adam --adam-betas '(0.9, 0.98)' \ --lr 5e-4 --lr-scheduler inverse_sqrt --warmup-updates 4000 \ --criterion label_smoothed_cross_entropy --label-smoothing 0.1 \ --log-format 'simple' --log-interval 100 --save-dir "checkpoints/${VOCAB}" \ --batch-size 100 --max-update 100000 --update-freq 2各超参数的作用如下:
| 参数 | 取值 | 说明 |
|---|---|---|
--task translation | — | 标准翻译任务 |
--user-dir | examples/byte_level_bpe/gru_transformer | 加载自定义模型(注册gru_transformer) |
--arch gru_transformer | — | 使用 Bi-GRU 上下文化的 Transformer |
--encoder-layers / --decoder-layers | 2 / 2 | 编码器、解码器均为 2 层(base 配置为 6 层,此处显式覆盖为 2 层) |
--dropout 0.3 | 0.3 | 全连接/注意力 dropout |
--share-all-embeddings | — | 共享源/目标/输出嵌入 |
--optimizer adam --adam-betas '(0.9, 0.98)' | — | Adam,betas 对齐 Transformer 论文 |
--lr 5e-4 --lr-scheduler inverse_sqrt --warmup-updates 4000 | — | 逆平方根学习率衰减 + 4000 步预热 |
--criterion label_smoothed_cross_entropy --label-smoothing 0.1 | — | 标签平滑交叉熵,平滑系数 0.1 |
--batch-size 100 --update-freq 2 | — | 单步 batch 100,每 2 步累积一次更新(等效 batch 200) |
--max-update 100000 | — | 最多 10 万次参数更新 |
3.2 模型结构源码解读
gru_transformer.py的关键设计(见 gru_transformer.py):
GRUTransformerModel继承TransformerModel,仅重写build_encoder返回GRUTransformerEncoder;GRUTransformerEncoder在TransformerEncoder之上增加一个单层双向 GRU(nn.GRU,input_size=embed_dim,hidden_size=embed_dim//2,bidirectional=True)作为嵌入上下文化网络;forward_embedding的执行顺序:词嵌入 ×embed_scale→ 叠加位置嵌入 → 转置为 (T, B, E) 送入 GRU → 转置回来 → 可选 LayerNorm → dropout,同时把原始embed返回给 decoder 复用(如 cross-attention 键);- 架构注册函数
gru_transformer_base_architecture给出默认值(encoder_embed_dim=512、encoder_ffn_embed_dim=2048、6 层、8 头等),并额外提供gru_transformer_big变体(1024 维、16 头、FFN 4096、dropout 0.3)。
引入 Bi-GRU 的动机:当词表变成字节/字符粒度时,单个 token 缺乏语义信息,编码器首先用双向 GRU 在局部窗口内融合相邻字节/字符的上下文,等价于在 Transformer 之前做一层"软合并",从而缓解细粒度子词带来的语义碎片化。
四、生成与交互式推理:解码器与字节还原
4.1 fairseq-generate:必须配对正确的 BPE 解码器
由于模型输出的是字节/字符/BPE 片段,fairseq-generate需要对应的--bpe解码器把输出还原成自然语言。原文档通过BPE变量按词表切换:
# BPE=--bpe bytes # BPE=--bpe characters BPE=--bpe byte_bpe --sentencepiece-model-path data/spm_bbpe2048.model # BPE=--bpe sentencepiece --sentencepiece-model data/spm_bpe2048.model # BPE=--bpe byte_bpe --sentencepiece-model-path data/spm_bbpe4096.model # BPE=--bpe sentencepiece --sentencepiece-model data/spm_bpe4096.model # BPE=--bpe sentencepiece --sentencepiece-model data/spm_bpe16384.modelfairseq-generate "data/bin_${VOCAB}" --task translation --user-dir examples/byte_level_bpe/gru_transformer \ --source-lang fr --gen-subset test --sacrebleu --path "checkpoints/${VOCAB}/checkpoint_last.pt" \ --tokenizer moses --moses-target-lang en ${BPE}要点说明:
--bpe bytes对应Bytes.decode:空格转义还原 +smart_byte_decode容错解码;--bpe characters对应Characters.decode:去掉 token 间空格并还原▁为空格;--bpe byte_bpe对应ByteBPE.decode(byte_bpe.py):x.replace(SPACE, "").replace(SPACE_ESCAPE, SPACE)后交给smart_byte_decode;--bpe sentencepiece对应SentencepieceBPE.decode:用同一个 SentencePiece 模型做 detokenize;--tokenizer moses --moses-target-lang en负责对解码输出做 Moses 去分词,使其与官方 BLEU 计算口径一致;--sacrebleu让 fairseq 直接输出 sacreBLEU。
4.2 fairseq-interactive:编码与解码双向闭环
交互式推理时,输入侧也要经过字节编码,输出侧要还原,因此必须同时指定编码器和解码器:
fairseq-interactive "data/bin_${VOCAB}" --task translation --user-dir examples/byte_level_bpe/gru_transformer \ --path "checkpoints/${VOCAB}/checkpoint_last.pt" --input data/test.fr --tokenizer moses --moses-source-lang fr \ --moses-target-lang en ${BPE} --buffer-size 1000 --max-tokens 10000--input data/test.fr:按行读入源句(也可省略该参数改为 stdin 逐行输入);--moses-source-lang fr:源侧先做 Moses 预分词;--buffer-size 1000 --max-tokens 10000:限制输入缓冲与单次前向的最大 token 数,控制显存占用。
至此,BBPE 的完整闭环(文本 → byte_encode → SentencePiece 切分 → 模型 → 字节片段 → smart_byte_decode → 自然语言)全部打通。
五、实验结果:不同词表方案的 BLEU 对比
原文档给出的 IWSLT17 Fr-En(tst2015)结果如下(括号内为加入 ensemble 的 BLEU):
| 词表方案 | 模型 | BLEU |
|---|---|---|
| Joint BPE 16k(Kudo, 2018) | 512d LSTM 2+2 | 33.81 |
| Joint BPE 16k | Transformer base 2+2(w/ GRU) | 36.64 (36.72) |
| Joint BPE 4k | Transformer base 2+2(w/ GRU) | 35.49 (36.10) |
| Joint BBPE 4k | Transformer base 2+2(w/ GRU) | 35.61 (35.82) |
| Joint BPE 2k | Transformer base 2+2(w/ GRU) | 34.87 (36.13) |
| Joint BBPE 2k | Transformer base 2+2(w/ GRU) | 34.98 (35.43) |
| Characters | Transformer base 2+2(w/ GRU) | 31.78 (33.30) |
| Bytes | Transformer base 2+2(w/ GRU) | 31.57 (33.62) |
从表格可以读出三个可复现的结论:
- 在相同 Transformer 结构下,BPE/BBPE 明显优于纯 Bytes/Chars 方案:字节/字符粒度的序列更长、语义碎片化更严重,即使有 Bi-GRU 上下文化仍损失约 2.4~3.5 个 BLEU;
- BBPE 与同规模 BPE 基本持平:BBPE 2k(34.98)vs BPE 2k(34.87),BBPE 4k(35.61)vs BPE 4k(35.49),说明在法英这种"字符集可被常规词表覆盖"的场景下,字节级子词不损失质量,同时换来更强的跨语言泛化能力;
- 词表越大质量越好:16k 联合 BPE 达到最高 36.64,但 2k 小词表(约 2 万级参数开销更小)仅低约 1.8 个点,工程上可用小词表换取更低的内存与嵌入参数。
六、复现注意事项与适用前提
- 运行目录:上述
fairseq-*命令与bash ./get_data.sh需在 decoding/IAD/fairseq 根目录下执行(该示例属于 IAD 解码框架内置的 fairseq 分支,目录内含完整 fairseq 实现); - 依赖:需要
sentencepiece、sacremoses,脚本中已通过pip install自动安装;wget需网络可达; - 模型路径:
--user-dir examples/byte_level_bpe/gru_transformer指向示例目录,使 fairseq 能发现GRUTransformerModel的注册(@register_model("gru_transformer")); - 解码器与词表必须严格对应:
bytes → --bpe bytes、chars → --bpe characters、bbpeK → --bpe byte_bpe --sentencepiece-model-path data/spm_bbpeK.model、bpeK → --bpe sentencepiece --sentencepiece-model data/spm_bpeK.model,混用会导致输出无法还原; - 结果口径:BLEU 数值仅对应当前仓库该示例的实验配置(Transformer 2+2、dropout 0.3、inverse_sqrt + 4000 warmup 等),更换数据或超参后需重新评估。
七、引用与致谢
该实现对应的论文引用信息(同样记录在原文档中):
@misc{wang2019neural, title={Neural Machine Translation with Byte-Level Subwords}, author={Changhan Wang and Kyunghyun Cho and Jiatao Gu}, year={2019}, eprint={1909.03341}, archivePrefix={arXiv}, primaryClass={cs.CL} }如需深入源码,推荐按以下顺序阅读:
- 数据流水线:get_data.sh → get_bitext.py;
- 字节编解码核心:byte_utils.py(双射映射与 DP 容错解码);
- 三个注册分词器:bytes.py、characters.py、byte_bpe.py;
- 模型实现:gru_transformer.py(Bi-GRU 嵌入上下文化)。
【免费下载链接】unilmLarge-scale Self-supervised Pre-training Across Tasks, Languages, and Modalities项目地址: https://gitcode.com/GitHub_Trending/un/unilm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考