基于 GRU 的 Seq2Seq 中英机器翻译:从 Tatoeba 语料到 BLEU 0.195
项目地址(Gitee):https://gitee.com/Touari/seq2seq_zh_en_translation.git
本文整合自项目《构建文档》与《技术文档》,既讲复现步骤,也讲设计原理。
1. 项目简介
中文 → 英文(中译英)的机器翻译模型 ,采用基于 GRU 的 Seq2Seq(Encoder-Decoder)架构,无注意力机制。
输入:你好。
输出:Hi.
- 数据:Tatoeba cmn-eng 平行语料(
cmn.txt,29,155 句对) - 技术栈:PyTorch + GRU 循环网络 + Teacher Forcing 训练 + 贪心解码(Greedy Decoding)
- 实测成绩:训练 loss 0.0642 (50 epoch),测试集 BLEU 0.1952(优于课程原版 0.1823)
#mermaid-svg-HnZPNABefPJIr24N{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HnZPNABefPJIr24N .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HnZPNABefPJIr24N .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HnZPNABefPJIr24N .error-icon{fill:#552222;}#mermaid-svg-HnZPNABefPJIr24N .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HnZPNABefPJIr24N .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HnZPNABefPJIr24N .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HnZPNABefPJIr24N .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HnZPNABefPJIr24N .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HnZPNABefPJIr24N .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HnZPNABefPJIr24N .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HnZPNABefPJIr24N .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HnZPNABefPJIr24N .marker.cross{stroke:#333333;}#mermaid-svg-HnZPNABefPJIr24N svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HnZPNABefPJIr24N p{margin:0;}#mermaid-svg-HnZPNABefPJIr24N .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HnZPNABefPJIr24N .cluster-label text{fill:#333;}#mermaid-svg-HnZPNABefPJIr24N .cluster-label span{color:#333;}#mermaid-svg-HnZPNABefPJIr24N .cluster-label span p{background-color:transparent;}#mermaid-svg-HnZPNABefPJIr24N .label text,#mermaid-svg-HnZPNABefPJIr24N span{fill:#333;color:#333;}#mermaid-svg-HnZPNABefPJIr24N .node rect,#mermaid-svg-HnZPNABefPJIr24N .node circle,#mermaid-svg-HnZPNABefPJIr24N .node ellipse,#mermaid-svg-HnZPNABefPJIr24N .node polygon,#mermaid-svg-HnZPNABefPJIr24N .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HnZPNABefPJIr24N .rough-node .label text,#mermaid-svg-HnZPNABefPJIr24N .node .label text,#mermaid-svg-HnZPNABefPJIr24N .image-shape .label,#mermaid-svg-HnZPNABefPJIr24N .icon-shape .label{text-anchor:middle;}#mermaid-svg-HnZPNABefPJIr24N .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HnZPNABefPJIr24N .rough-node .label,#mermaid-svg-HnZPNABefPJIr24N .node .label,#mermaid-svg-HnZPNABefPJIr24N .image-shape .label,#mermaid-svg-HnZPNABefPJIr24N .icon-shape .label{text-align:center;}#mermaid-svg-HnZPNABefPJIr24N .node.clickable{cursor:pointer;}#mermaid-svg-HnZPNABefPJIr24N .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HnZPNABefPJIr24N .arrowheadPath{fill:#333333;}#mermaid-svg-HnZPNABefPJIr24N .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HnZPNABefPJIr24N .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HnZPNABefPJIr24N .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HnZPNABefPJIr24N .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HnZPNABefPJIr24N .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HnZPNABefPJIr24N .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HnZPNABefPJIr24N .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HnZPNABefPJIr24N .cluster text{fill:#333;}#mermaid-svg-HnZPNABefPJIr24N .cluster span{color:#333;}#mermaid-svg-HnZPNABefPJIr24N div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HnZPNABefPJIr24N .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HnZPNABefPJIr24N rect.text{fill:none;stroke-width:0;}#mermaid-svg-HnZPNABefPJIr24N .icon-shape,#mermaid-svg-HnZPNABefPJIr24N .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HnZPNABefPJIr24N .icon-shape p,#mermaid-svg-HnZPNABefPJIr24N .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HnZPNABefPJIr24N .icon-shape .label rect,#mermaid-svg-HnZPNABefPJIr24N .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HnZPNABefPJIr24N .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HnZPNABefPJIr24N .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HnZPNABefPJIr24N :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} process.py
train.py
evaluate.py
predict.py
cmn.txt
29,155 句对
train.jsonl
23,324 条
best.pt
loss 0.0642
BLEU 0.1952
交互翻译
与前两个项目对比(学习路线的质变)
| RNN_TextInput(项目1) | LSTM_Sentiment_2class(项目2) | Seq2Seq_Translation(本项目) | |
|---|---|---|---|
| 任务 | 前 5 词预测下一词 | 整句判定正/负情感 | 中译英翻译 |
| 任务本质 | 多分类(21,138 类) | 二分类(2 类) | 序列生成(逐词产出整句) |
| 网络形态 | 单 RNN 编码 | 单 LSTM 编码 | 双网络:Encoder + Decoder |
| 循环层 | nn.RNN |
nn.LSTM |
nn.GRU(比 LSTM 更轻量) |
| 输出 | 一个词(下一词) | 一个 logit | 一串词(直到 ``) |
| 训练信号 | 交叉熵 | BCE | 交叉熵(teacher forcing) |
| 评估 | top-1/top-5 准确率 | 准确率 | BLEU(n-gram 匹配) |
核心跨越:前两个项目是「看一句话 → 出一个 结论」(判别式);本项目是「看一句话 → 生成一整句 」(生成式)。生成式的关键是自回归:逐个词地生成,每个新词都依赖之前生成的词。
2. 整体架构与数据流
#mermaid-svg-aklzAJlDqTWHIHWI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aklzAJlDqTWHIHWI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aklzAJlDqTWHIHWI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aklzAJlDqTWHIHWI .error-icon{fill:#552222;}#mermaid-svg-aklzAJlDqTWHIHWI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aklzAJlDqTWHIHWI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aklzAJlDqTWHIHWI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aklzAJlDqTWHIHWI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aklzAJlDqTWHIHWI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aklzAJlDqTWHIHWI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aklzAJlDqTWHIHWI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aklzAJlDqTWHIHWI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aklzAJlDqTWHIHWI .marker.cross{stroke:#333333;}#mermaid-svg-aklzAJlDqTWHIHWI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aklzAJlDqTWHIHWI p{margin:0;}#mermaid-svg-aklzAJlDqTWHIHWI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aklzAJlDqTWHIHWI .cluster-label text{fill:#333;}#mermaid-svg-aklzAJlDqTWHIHWI .cluster-label span{color:#333;}#mermaid-svg-aklzAJlDqTWHIHWI .cluster-label span p{background-color:transparent;}#mermaid-svg-aklzAJlDqTWHIHWI .label text,#mermaid-svg-aklzAJlDqTWHIHWI span{fill:#333;color:#333;}#mermaid-svg-aklzAJlDqTWHIHWI .node rect,#mermaid-svg-aklzAJlDqTWHIHWI .node circle,#mermaid-svg-aklzAJlDqTWHIHWI .node ellipse,#mermaid-svg-aklzAJlDqTWHIHWI .node polygon,#mermaid-svg-aklzAJlDqTWHIHWI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aklzAJlDqTWHIHWI .rough-node .label text,#mermaid-svg-aklzAJlDqTWHIHWI .node .label text,#mermaid-svg-aklzAJlDqTWHIHWI .image-shape .label,#mermaid-svg-aklzAJlDqTWHIHWI .icon-shape .label{text-anchor:middle;}#mermaid-svg-aklzAJlDqTWHIHWI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aklzAJlDqTWHIHWI .rough-node .label,#mermaid-svg-aklzAJlDqTWHIHWI .node .label,#mermaid-svg-aklzAJlDqTWHIHWI .image-shape .label,#mermaid-svg-aklzAJlDqTWHIHWI .icon-shape .label{text-align:center;}#mermaid-svg-aklzAJlDqTWHIHWI .node.clickable{cursor:pointer;}#mermaid-svg-aklzAJlDqTWHIHWI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aklzAJlDqTWHIHWI .arrowheadPath{fill:#333333;}#mermaid-svg-aklzAJlDqTWHIHWI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aklzAJlDqTWHIHWI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aklzAJlDqTWHIHWI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aklzAJlDqTWHIHWI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aklzAJlDqTWHIHWI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aklzAJlDqTWHIHWI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aklzAJlDqTWHIHWI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aklzAJlDqTWHIHWI .cluster text{fill:#333;}#mermaid-svg-aklzAJlDqTWHIHWI .cluster span{color:#333;}#mermaid-svg-aklzAJlDqTWHIHWI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aklzAJlDqTWHIHWI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aklzAJlDqTWHIHWI rect.text{fill:none;stroke-width:0;}#mermaid-svg-aklzAJlDqTWHIHWI .icon-shape,#mermaid-svg-aklzAJlDqTWHIHWI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aklzAJlDqTWHIHWI .icon-shape p,#mermaid-svg-aklzAJlDqTWHIHWI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aklzAJlDqTWHIHWI .icon-shape .label rect,#mermaid-svg-aklzAJlDqTWHIHWI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aklzAJlDqTWHIHWI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aklzAJlDqTWHIHWI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aklzAJlDqTWHIHWI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 数据预处理
dataset.py
TranslationDataset + collate_fn
pad_sequence 按 batch 最长补齐
train.py
GRU Encoder-Decoder
teacher forcing + CrossEntropyLoss(ignore pad)
predict.py
greedy 自回归解码
evaluate.py
corpus_bleu
process.py
读取 → 8:2 划分 → 双语建词表 → 编码 → 存 jsonl
data/raw/cmn.txt
29,155 句对(Tatoeba cmn-eng)
train.jsonl 23,324 条
test.jsonl 5,831 条
DataLoader
batch_size=64, shuffle
models/best.pt
50 epoch, loss 0.0642
参数 3,797,619
英文翻译
BLEU = 0.1952
核心思想:把「翻译」转化为「条件语言模型」 ------模型学习条件概率 P(英文词序列∣中文词序列)P(英文词序列 | 中文词序列)P(英文词序列∣中文词序列)。Encoder 把中文「压缩」成一个向量(context vector),Decoder 在这个向量的引导下逐词生成英文。
3. 数据处理详解
3.1 原始数据
cmn.txt 来自 Tatoeba(tatoeba.org,开源平行语料库),每行三列(制表符分隔):
Hi. 嗨。 CC-BY 2.0 (France) Attribution: tatoeba.org #538123 ...
Run. 你用跑的。 CC-BY 2.0 (France) Attribution: tatoeba.org #4008918 ...
- 共 29,155 句对,全部是短句(日常会话级)------训练快、效果好、适合入门的关键原因
- 用
usecols=[0, 1]只取英文和中文两列,忽略出处
3.2 划分数据集
train_test_split(test_size=0.2, random_state=42):训练 23,324 句 / 测试 5,831 句。random_state=42 是本项目相对课程的改进:划分可复现(课程原版未固定随机种子)。
3.3 分词与词表(双语策略)
两侧分词策略完全不同(第一个设计决策):
| 侧 | 分词方式 | 词表大小(实测) | 原因 |
|---|---|---|---|
| 中文(Encoder 输入) | 字符级 list(text) |
2,746 | 中文分词(jieba)错误会传播成翻译错误;字符级零误差、词表小 |
| 英文(Decoder 输出) | NLTK Treebank 词级 | 7,411 | 英文天然空格分词;Treebank 处理标点与缩写(don't → do n't) |
词表结构:['', '', '', ''] + 语料 token:
| 特殊符 | 索引 | 含义 |
|---|---|---|
| `` | 0 | 填充占位(padding_idx,向量恒 0、不参与梯度) |
| `` | 1 | 未登录词统一映射(OOV 兜底) |
| `` | 2 | 序列开始(decoder 的启动信号) |
| `` | 3 | 序列结束(decoder 的停止信号) |
只使用训练集建词表 ------ 防止测试集信息泄漏。
3.4 编码契约:两侧不对称(最容易踩的坑)
encode(text, add_sos_eos=False),由调用方显式决定是否加边界:
python
train_df['zh'] = train_df['zh'].apply(lambda x: zh_tokenizer.encode(x, add_sos_eos=False)) # 中文:不加
train_df['en'] = train_df['en'].apply(lambda x: en_tokenizer.encode(x, add_sos_eos=True)) # 英文:加
#mermaid-svg-va2bpNp91FZuZj5N{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-va2bpNp91FZuZj5N .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-va2bpNp91FZuZj5N .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-va2bpNp91FZuZj5N .error-icon{fill:#552222;}#mermaid-svg-va2bpNp91FZuZj5N .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-va2bpNp91FZuZj5N .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-va2bpNp91FZuZj5N .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-va2bpNp91FZuZj5N .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-va2bpNp91FZuZj5N .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-va2bpNp91FZuZj5N .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-va2bpNp91FZuZj5N .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-va2bpNp91FZuZj5N .marker{fill:#333333;stroke:#333333;}#mermaid-svg-va2bpNp91FZuZj5N .marker.cross{stroke:#333333;}#mermaid-svg-va2bpNp91FZuZj5N svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-va2bpNp91FZuZj5N p{margin:0;}#mermaid-svg-va2bpNp91FZuZj5N .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-va2bpNp91FZuZj5N .cluster-label text{fill:#333;}#mermaid-svg-va2bpNp91FZuZj5N .cluster-label span{color:#333;}#mermaid-svg-va2bpNp91FZuZj5N .cluster-label span p{background-color:transparent;}#mermaid-svg-va2bpNp91FZuZj5N .label text,#mermaid-svg-va2bpNp91FZuZj5N span{fill:#333;color:#333;}#mermaid-svg-va2bpNp91FZuZj5N .node rect,#mermaid-svg-va2bpNp91FZuZj5N .node circle,#mermaid-svg-va2bpNp91FZuZj5N .node ellipse,#mermaid-svg-va2bpNp91FZuZj5N .node polygon,#mermaid-svg-va2bpNp91FZuZj5N .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-va2bpNp91FZuZj5N .rough-node .label text,#mermaid-svg-va2bpNp91FZuZj5N .node .label text,#mermaid-svg-va2bpNp91FZuZj5N .image-shape .label,#mermaid-svg-va2bpNp91FZuZj5N .icon-shape .label{text-anchor:middle;}#mermaid-svg-va2bpNp91FZuZj5N .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-va2bpNp91FZuZj5N .rough-node .label,#mermaid-svg-va2bpNp91FZuZj5N .node .label,#mermaid-svg-va2bpNp91FZuZj5N .image-shape .label,#mermaid-svg-va2bpNp91FZuZj5N .icon-shape .label{text-align:center;}#mermaid-svg-va2bpNp91FZuZj5N .node.clickable{cursor:pointer;}#mermaid-svg-va2bpNp91FZuZj5N .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-va2bpNp91FZuZj5N .arrowheadPath{fill:#333333;}#mermaid-svg-va2bpNp91FZuZj5N .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-va2bpNp91FZuZj5N .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-va2bpNp91FZuZj5N .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-va2bpNp91FZuZj5N .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-va2bpNp91FZuZj5N .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-va2bpNp91FZuZj5N .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-va2bpNp91FZuZj5N .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-va2bpNp91FZuZj5N .cluster text{fill:#333;}#mermaid-svg-va2bpNp91FZuZj5N .cluster span{color:#333;}#mermaid-svg-va2bpNp91FZuZj5N div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-va2bpNp91FZuZj5N .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-va2bpNp91FZuZj5N rect.text{fill:none;stroke-width:0;}#mermaid-svg-va2bpNp91FZuZj5N .icon-shape,#mermaid-svg-va2bpNp91FZuZj5N .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-va2bpNp91FZuZj5N .icon-shape p,#mermaid-svg-va2bpNp91FZuZj5N .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-va2bpNp91FZuZj5N .icon-shape .label rect,#mermaid-svg-va2bpNp91FZuZj5N .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-va2bpNp91FZuZj5N .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-va2bpNp91FZuZj5N .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-va2bpNp91FZuZj5N :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 英文侧 - Decoder 输入/输出
It's a nice day.
2, 3922, ..., 3
sos + 5 词 + eos,共 7 个 id
中文侧 - Encoder 输入
今天天气很好。
320, 2171, 1094, 852, 2239
5 个字 id,无边界
- 中文不加:Encoder 只看句子本身,边界标记对它无意义
- 英文加 :Decoder 需要
作为「开始生成」的启动词、作为「停止生成」的目标词 - 若中文也误加边界 → 训练与推断时 Encoder 输入分布不一致,翻译质量悄悄下降(开发中踩过的坑)
3.5 句长分布(实测统计)
| 侧 | max | min | 均值 | 超过 MAX_SEQ_LEN=128 |
|---|---|---|---|---|
| 中文(不含边界) | 44 | 2 | 9.7 | 0 条 |
| 英文(含 sos/eos) | 38 | 4 | 9.5 | 0 条 |
全部是短句 → MAX_SEQ_LEN=128 是安全的生成上限;collate_fn 用 pad_sequence 按 batch 内最长句动态补齐(不是全数据集 pad 到 128,省显存省算力)。
4. 模型结构详解
4.1 网络结构
#mermaid-svg-MutXmlyIogv7NGty{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MutXmlyIogv7NGty .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MutXmlyIogv7NGty .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MutXmlyIogv7NGty .error-icon{fill:#552222;}#mermaid-svg-MutXmlyIogv7NGty .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MutXmlyIogv7NGty .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MutXmlyIogv7NGty .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MutXmlyIogv7NGty .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MutXmlyIogv7NGty .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MutXmlyIogv7NGty .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MutXmlyIogv7NGty .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MutXmlyIogv7NGty .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MutXmlyIogv7NGty .marker.cross{stroke:#333333;}#mermaid-svg-MutXmlyIogv7NGty svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MutXmlyIogv7NGty p{margin:0;}#mermaid-svg-MutXmlyIogv7NGty .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MutXmlyIogv7NGty .cluster-label text{fill:#333;}#mermaid-svg-MutXmlyIogv7NGty .cluster-label span{color:#333;}#mermaid-svg-MutXmlyIogv7NGty .cluster-label span p{background-color:transparent;}#mermaid-svg-MutXmlyIogv7NGty .label text,#mermaid-svg-MutXmlyIogv7NGty span{fill:#333;color:#333;}#mermaid-svg-MutXmlyIogv7NGty .node rect,#mermaid-svg-MutXmlyIogv7NGty .node circle,#mermaid-svg-MutXmlyIogv7NGty .node ellipse,#mermaid-svg-MutXmlyIogv7NGty .node polygon,#mermaid-svg-MutXmlyIogv7NGty .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MutXmlyIogv7NGty .rough-node .label text,#mermaid-svg-MutXmlyIogv7NGty .node .label text,#mermaid-svg-MutXmlyIogv7NGty .image-shape .label,#mermaid-svg-MutXmlyIogv7NGty .icon-shape .label{text-anchor:middle;}#mermaid-svg-MutXmlyIogv7NGty .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MutXmlyIogv7NGty .rough-node .label,#mermaid-svg-MutXmlyIogv7NGty .node .label,#mermaid-svg-MutXmlyIogv7NGty .image-shape .label,#mermaid-svg-MutXmlyIogv7NGty .icon-shape .label{text-align:center;}#mermaid-svg-MutXmlyIogv7NGty .node.clickable{cursor:pointer;}#mermaid-svg-MutXmlyIogv7NGty .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MutXmlyIogv7NGty .arrowheadPath{fill:#333333;}#mermaid-svg-MutXmlyIogv7NGty .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MutXmlyIogv7NGty .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MutXmlyIogv7NGty .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MutXmlyIogv7NGty .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MutXmlyIogv7NGty .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MutXmlyIogv7NGty .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MutXmlyIogv7NGty .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MutXmlyIogv7NGty .cluster text{fill:#333;}#mermaid-svg-MutXmlyIogv7NGty .cluster span{color:#333;}#mermaid-svg-MutXmlyIogv7NGty div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MutXmlyIogv7NGty .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MutXmlyIogv7NGty rect.text{fill:none;stroke-width:0;}#mermaid-svg-MutXmlyIogv7NGty .icon-shape,#mermaid-svg-MutXmlyIogv7NGty .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MutXmlyIogv7NGty .icon-shape p,#mermaid-svg-MutXmlyIogv7NGty .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MutXmlyIogv7NGty .icon-shape .label rect,#mermaid-svg-MutXmlyIogv7NGty .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MutXmlyIogv7NGty .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MutXmlyIogv7NGty .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MutXmlyIogv7NGty :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} TranslationDecoder(英文侧)
TranslationEncoder(中文侧)
context vector
batch, 256
→ unsqueeze(0) 作 decoder 初始 hidden
nn.Embedding
2746 × 128
padding_idx=0
nn.GRU
input=128, hidden=256
单层, batch_first
取最后有效时刻
outputarange, lengths-1
nn.Embedding
7411 × 128
padding_idx=0
nn.GRU
input=128, hidden=256
nn.Linear
256 → 7411
中文 id 序列
batch, seq
decoder 输入
batch, 1 单步
词表分布 logits
batch, 1, 7411
python
TranslationModel:
encoder: Embedding(2746 → 128, padding_idx=0) + GRU(128 → 256)
decoder: Embedding(7411 → 128, padding_idx=0) + GRU(128 → 256) + Linear(256 → 7411)
4.2 前向传播形状流转
Encoder:
输入 x: [batch, seq_len] # 中文 id 序列(pad 后等长)
→ Embedding: [batch, seq_len, 128]
→ GRU output: [batch, seq_len, 256] # 每个时刻的隐状态
→ 取最后有效时刻: [batch, 256] # context vector
Decoder(单步):
输入 x: [batch, 1] # 当前步的词 id
输入 hidden_0: [1, batch, 256] # 初始 = context vector unsqueeze(0)
→ Embedding: [batch, 1, 128]
→ GRU: [batch, 1, 256]
→ Linear: [batch, 1, 7411] # 词表分布 logits
返回: (logits, hidden_n) # hidden_n 传给下一步
4.3 关键设计点(含金量最高的三处)
① 取「最后一个有效时刻」的隐状态,而不是最后一步
python
lengths = (x != self.embedding.padding_idx).sum(dim=1) # 每句真实长度
last_hidden_state = output[torch.arange(output.shape[0]), lengths - 1]
由于 pad 在尾部,output[:, -1] 取到的是 pad 位置的隐状态(无意义)。GRU 返回的 h_n 同理位于 pad 位置,不可直接使用。(实测验证:out[0] == GRU 输出在时刻 lengths-1 的 hidden 为 True。)
② padding_idx=0 双保险
- Embedding 侧:
nn.Embedding(..., padding_idx=0)→ pad 词向量恒为 0,不参与梯度更新 - 损失侧:
CrossEntropyLoss(ignore_index=0)→ pad 位置不计算损失 - 漏掉第二个是踩过的坑:pad 位置占 batch 内大量 token,模型被迫学「预测 pad」,稀释真实词梯度。修复后同样 50 epoch,loss 降到 0.0642
③ Decoder 是「单步」组件
forward(x, hidden_0) 每次只吃 [batch, 1](一个词),输出一个词的分布和新的 hidden------没有内部循环。循环在训练/推断脚本里显式控制,这正是「自回归」的体现:网络本身是「一步」的,序列的推进由外部循环完成。
4.4 参数量计算(3,797,619 的分解)
| 层 | 计算 | 参数量 |
|---|---|---|
| Encoder Embedding | 2,746 × 128 | 351,488 |
| Encoder GRU | (128×256 + 256×256) × 3 + 256×3×2 | 296,448 |
| Encoder 小计 | 647,936 | |
| Decoder Embedding | 7,411 × 128 | 948,608 |
| Decoder GRU | 同 Encoder | 296,448 |
| Decoder Linear | 7,411 × 256 + 7,411 | 1,904,627 |
| Decoder 小计 | 3,149,683 | |
| 总计 | 3,797,619 ≈ 380 万 |
要点:
- GRU 有 3 组门控权重(重置门/更新门/候选隐状态),故公式为
(input×hidden + hidden×hidden) × 3 + 偏置 - 参数大头在 Decoder 输出层 (Linear 190 万,占 50%)------英文词表 7,411 类,
256 → 7411的矩阵本身就大 - FP32 下
best.pt约 15.2 MB,与磁盘实测 15,196,017 字节吻合
4.5 为什么用 GRU(相对 LSTM 的选择)
GRU 是 LSTM 的简化版:把 LSTM 的遗忘门/输入门合并为更新门,去掉细胞状态(c_t),只保留隐状态。效果与 LSTM 相当但参数更少、训练更快。对翻译这种「中等长度短句」任务,GRU 是性价比选择。
5. 训练机制详解(Teacher Forcing)
5.1 核心切片
python
decoder_inputs = targets[:, :-1] # 去掉最后一个 eos:作为 decoder 每一步的输入
decoder_targets = targets[:, 1:] # 去掉开头的 sos:作为每一步的预测目标
为什么训练时喂真实词而不是模型自己的预测?
- 模型每步都「站在正确的位置」学习:即使上一步预测错了,这一步还是学到正确的条件分布
- 收敛快、训练稳定
- 代价:推断时没有真实词可用,模型只能喂自己的预测------一旦某步错,误差会累积(exposure bias),这是 seq2seq 的经典痛点
5.2 训练配置
| 项 | 值 | 说明 |
|---|---|---|
| 损失函数 | CrossEntropyLoss(ignore_index=en_pad) |
只在真实词上算损失 |
| 优化器 | Adam(lr=0.001) |
默认参数 |
| 设备 | cuda(RTX 3060) |
|
| batch | 64 × 365 步/epoch | 23,324 / 64 |
| 保存策略 | loss 优于历史最优则覆盖 best.pt |
state_dict 格式 |
| 日志 | TensorBoard logs/<时间戳>/ |
每 epoch 记录 loss |
5.3 实测训练曲线(50 epochs)
4.3721 → 1.7293 → 0.8337 → 0.4182 → 0.2278 → 0.1431 → 0.1106 → 0.1024 → 0.0786 → 0.0939 → 0.0642
(epoch 1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 50)
- 初始 loss ≈ 4.37:大词表(7,411 类)随机猜测的理论交叉熵为 ln(7411) ≈ 8.9,实际 4.37 说明模型立刻学会了高频词
- epoch 45-46 出现小幅回升(0.0939)后继续下降:学习率固定下的正常波动
- 最终 0.0642 ,优于课程原版同配置的 0.0841(差距主要来自
ignore_index修复 + 可复现划分)
6. 推断机制详解(Greedy 自回归解码)
Encoder 编码中文 → context vector(初始 hidden)
→ decoder_input = [sos](batch 内全填 sos)
→ 循环直到全部样本生成 eos 或 i >= MAX_SEQ_LEN=128:
单步解码 → logits [batch, 1, 7411]
argmax 取最高概率词(greedy)
把预测词喂回 decoder_input(自回归)
is_finished 记录已出 eos 的样本,全结束则提前 break
→ 截断到第一个 eos 之前 → decode → 英文文本
与训练的本质区别:
| 训练(teacher forcing) | 推断(greedy) | |
|---|---|---|
| decoder 输入 | 真实目标词 | 上一步自己的预测 |
| 出错恢复 | 无影响(下步仍喂真词) | 误差累积(一步错步步错) |
| 停止条件 | 序列长度固定 | 生成 `` 或到上限 |
为什么贪心不一定最优:每步取局部最高概率词,可能错过全局更好的组合(如「a nice day」vs「nice day a」)。改进方向:beam search(每步保留 top-K 候选路径)。
7. BLEU 评估详解
7.1 概念
BLEU(Bilingual Evaluation Understudy)衡量机器译文与参考译文的 n-gram 重合度:
- 分别统计 1~4-gram 在预测与参考中的匹配情况(modified precision)
- 乘上长度惩罚(译文过短会扣分)
- 输出 0~1 的分数
python
corpus_bleu(list_of_references, predictions)
# list_of_references: [[参考1], [参考2], ...](每句可有多个参考,嵌套一层)
# predictions: [预测1, 预测2, ...]
7.2 实测结果(测试集 5,831 条全量)
| 模型 | 训练 loss(50 epoch) | 测试集 BLEU |
|---|---|---|
| 本项目(random_state=42 + ignore_index) | 0.0642 | 0.1952 |
| 课程原版 best.pt | 0.0841 | 0.1823 |
注:两者测试集划分不同(本项目固定 seed,课程随机),并非严格同题对比,但量级相同,结论可信。
7.3 怎么理解 0.1952(≈19.5%)
- 对无注意力的基础 GRU Seq2Seq + 短句语料,10~25 区间是正常水平,本项目处于中上
- BLEU 不会接近 100%:n-gram 精确匹配很苛刻,且 Tatoeba 中一句中文常有多译(「你好。」→ Hi./Hello./How are you?),参考只有一个,天然扣分
- 课程同架构跑出 0.1823 ------ 本项目的三个小改进(固定 seed、ignore_index、词表略异)合计带来 +1.3 点
8. 快速开始(复现指南)
8.1 环境准备
- Python 3.12.0,GPU:RTX 3060(6GB),训练全程约 8~9 分钟(50 epoch)
| 包 | 版本 | 用途 |
|---|---|---|
| torch | 2.7.1+cu118 | 深度学习框架(CUDA 11.8 版) |
| pandas | 3.0.2 | 数据读取与 jsonl 存储 |
| scikit-learn | 1.8.0 | 训练/测试集划分 |
| nltk | 3.10.2 | 英文 Treebank 分词 / 反分词 / BLEU 评估 |
| tqdm | 4.68.2 | 进度条 |
| tensorboard | 2.21.0 | 训练曲线可视化 |
命令行运行注意:① 所有脚本需
cd src后运行(import config平级导入)② 建议python -E防止 PYTHONPATH 污染 ③ 终端中文乱码是 GBK 显示问题,不影响运行
8.2 目录结构
Seq2Seq_Translation/
├── data/
│ ├── raw/cmn.txt 原始数据(29,155 句对,Tatoeba)
│ └── processed/train.jsonl, test.jsonl 预处理产物(token id 序列)
├── logs/ 训练日志(TensorBoard)
├── models/
│ ├── zh_vocab.txt 中文词表(2,746 项)
│ ├── en_vocab.txt 英文词表(7,411 项)
│ └── best.pt 最优模型权重(约 15.2 MB)
├── src/
│ ├── config.py 路径与超参唯一来源
│ ├── process.py 数据预处理流水线
│ ├── tokenizer.py 双语分词器(中文按字符、英文 Treebank)
│ ├── dataset.py Dataset + collate_fn(pad_sequence 动态补齐)
│ ├── model.py Encoder-Decoder 模型(GRU,无注意力)
│ ├── train.py 训练循环(Teacher Forcing)
│ ├── evaluate.py 测试集 BLEU 评估
│ └── predict.py 交互式翻译(贪心解码)
└── test/test-bleu.py corpus_bleu 用法迷你测试
8.3 全流程四步
#mermaid-svg-Ohh6MQU1LsT0Bx7c{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Ohh6MQU1LsT0Bx7c .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .error-icon{fill:#552222;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .marker.cross{stroke:#333333;}#mermaid-svg-Ohh6MQU1LsT0Bx7c svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Ohh6MQU1LsT0Bx7c p{margin:0;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .cluster-label text{fill:#333;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .cluster-label span{color:#333;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .cluster-label span p{background-color:transparent;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .label text,#mermaid-svg-Ohh6MQU1LsT0Bx7c span{fill:#333;color:#333;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .node rect,#mermaid-svg-Ohh6MQU1LsT0Bx7c .node circle,#mermaid-svg-Ohh6MQU1LsT0Bx7c .node ellipse,#mermaid-svg-Ohh6MQU1LsT0Bx7c .node polygon,#mermaid-svg-Ohh6MQU1LsT0Bx7c .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .rough-node .label text,#mermaid-svg-Ohh6MQU1LsT0Bx7c .node .label text,#mermaid-svg-Ohh6MQU1LsT0Bx7c .image-shape .label,#mermaid-svg-Ohh6MQU1LsT0Bx7c .icon-shape .label{text-anchor:middle;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .rough-node .label,#mermaid-svg-Ohh6MQU1LsT0Bx7c .node .label,#mermaid-svg-Ohh6MQU1LsT0Bx7c .image-shape .label,#mermaid-svg-Ohh6MQU1LsT0Bx7c .icon-shape .label{text-align:center;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .node.clickable{cursor:pointer;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .arrowheadPath{fill:#333333;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Ohh6MQU1LsT0Bx7c .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ohh6MQU1LsT0Bx7c .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Ohh6MQU1LsT0Bx7c .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .cluster text{fill:#333;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .cluster span{color:#333;}#mermaid-svg-Ohh6MQU1LsT0Bx7c div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Ohh6MQU1LsT0Bx7c rect.text{fill:none;stroke-width:0;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .icon-shape,#mermaid-svg-Ohh6MQU1LsT0Bx7c .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .icon-shape p,#mermaid-svg-Ohh6MQU1LsT0Bx7c .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .icon-shape .label rect,#mermaid-svg-Ohh6MQU1LsT0Bx7c .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ohh6MQU1LsT0Bx7c .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Ohh6MQU1LsT0Bx7c .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Ohh6MQU1LsT0Bx7c :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 生成词表+jsonl
生成 best.pt
第1步 process.py
第2步 train.py
第3步 evaluate.py
第4步 predict.py
第 1 步:数据预处理
bash
cd src
python -E process.py
产物:train.jsonl(23,324 条)、test.jsonl(5,831 条)、en_vocab.txt(7,411 项)、zh_vocab.txt(2,746 项)。数据来源:Tatoeba 网站的 cmn-eng 句子对(英文\t中文\t出处 三列制表符分隔)。
第 2 步:训练
bash
python -E train.py
每轮打印 ========== Epoch N ========== 与 Loss: x.xxxx,若 loss 优于历史最优则覆盖 models/best.pt。RTX 3060 上 50 epochs 约 8~9 分钟。
第 3 步:评估(BLEU)
bash
python -E evaluate.py
预期输出:bleu: 0.1952(5,831 条全量;课程原版同流程实测 0.1823)。
第 4 步:交互式翻译
bash
python -E predict.py
欢迎使用中英翻译模型(输入q或者quit退出)
中文:你好。
英文:Hi.
中文:我喜欢读书。
英文:I like reading a book.
中文:q
欢迎下次再来
8.4 常见问题(FAQ)
- Q1:为什么必须
cd src? 模块间import config平级导入,Python 只把「当前目录」加入sys.path - Q2:为什么建议
python -E? 忽略所有 PYTHON* 环境变量,避免 PYTHONPATH 污染导入错误的 numpy/torch - Q3:终端中文乱码? Windows 控制台默认 GBK 的显示问题,数据本身没损坏
- Q4:重跑 process.py 后需要重新训练吗? 需要。词表顺序基于 set 迭代,token id 会变,必须「词表 + 数据」成对重建后重训
- Q5:git 推送失败
Failed to connect 127.0.0.1:7897? 全局 git 代理指向 Clash,代理没开时推送失败。绕过:git -c http.proxy= -c https.proxy= push - Q6:MAX_SEQ_LEN=128 够用吗? 够。实测中文句长 max=44、英文 max=38。但 dataset.py 没有截断逻辑------换成长句数据需加截断或过滤
9. 实验结果汇总
9.1 指标总览
| 指标 | 数值 | 基线参考 |
|---|---|---|
| 训练 loss(50 epoch) | 0.0642 | 课程原版 0.0841 |
| 测试集 BLEU | 0.1952 | 课程原版 0.1823;随机猜测 ≈ 0 |
| 模型参数量 | 3,797,619(≈380 万) | |
| 训练时长 | 约 8~9 分钟(RTX 3060) |
9.2 实测翻译样例
| 中文输入 | 模型输出 | 参考 |
|---|---|---|
| 你好。 | Hi. | ✅ |
| 谢谢。 | Thank you. | ✅ |
| 我爱你。 | I love you. | ✅ |
| 我喜欢读书。 | I like reading a book. | ✅(组合生成,非背诵) |
| 今天天气很好。 | It's a nice day. | ✅ |
| 你是谁? | Who are you? | ✅ |
10. 已知边界与改进方向
| 边界/痛点 | 说明 | 改进方向 |
|---|---|---|
| 无注意力 | 所有信息压进一个 context vector,长句信息丢失 | 注意力机制(Attention):decoder 每步回看 encoder 全部时刻,预期 BLEU 25~35+ |
| 贪心解码 | 每步局部最优 ≠ 全局最优 | beam search(保留 top-K 候选路径) |
| teacher forcing 暴露偏差 | 训练喂真词、推断喂自己,误差累积 | scheduled sampling(训练时按概率逐渐切换喂预测词) |
| 中文字符级 | 词表小但丢失词义信息 | 中文 BPE/词级分词(jieba + 子词) |
| 数据量小 | 2.9 万短句,覆盖有限 | 更大语料(AI Challenger / WMT) |
| 单层 GRU | 表达力有限 | 多层堆叠 / 双向 Encoder |
| 评估单一 | BLEU 与人工感知不完全一致 | 补充 chrF、人工评测 |
11. 小结
本项目用基于 GRU 的 Encoder-Decoder 完成了中译英机器翻译,实现了学习路线的质变:从「判别式」(出结论)跨越到「生成式」(出整句)。三个含金量最高的设计点:取最后有效时刻隐状态 、padding_idx 双保险(Embedding + Loss) 、Decoder 单步组件设计;两个相对课程的改进(固定随机种子、ignore_index 修复)合计带来 BLEU +1.3 点(0.1823 → 0.1952)。下一步自然是加注意力机制------那正是 Transformer 的起点。
完整代码与数据见项目仓库:https://gitee.com/Touari/seq2seq_zh_en_translation.git