Add Streamlit preview frontend and normalize line endings

#6
Files changed (44) hide show
  1. .gitattributes +35 -35
  2. .gitignore +57 -57
  3. README.md +76 -76
  4. TASK_ASSIGNMENT.md +414 -414
  5. configs/deepspeed_config.json +28 -28
  6. configs/default_config.yaml +183 -182
  7. pytest.ini +6 -6
  8. requirements.txt +36 -35
  9. scripts/evaluate.py +285 -50
  10. scripts/run_experiments.py +113 -113
  11. scripts/train.py +76 -76
  12. scripts/translate.py +316 -82
  13. scripts/visualize.py +86 -86
  14. setup.py +23 -23
  15. src/easytranslate/__init__.py +3 -3
  16. src/easytranslate/data/README.md +126 -126
  17. src/easytranslate/data/__init__.py +32 -32
  18. src/easytranslate/data/collator.py +114 -114
  19. src/easytranslate/data/dataset.py +157 -157
  20. src/easytranslate/data/preprocessing.py +125 -125
  21. src/easytranslate/data/tokenizer.py +197 -197
  22. src/easytranslate/evaluation/__init__.py +16 -16
  23. src/easytranslate/evaluation/decoding.py +219 -129
  24. src/easytranslate/evaluation/evaluator.py +208 -76
  25. src/easytranslate/evaluation/metrics.py +169 -109
  26. src/easytranslate/model/__init__.py +20 -20
  27. src/easytranslate/model/attention.py +231 -231
  28. src/easytranslate/model/decoder.py +176 -176
  29. src/easytranslate/model/encoder.py +127 -127
  30. src/easytranslate/model/finetune.py +116 -116
  31. src/easytranslate/model/positional.py +134 -134
  32. src/easytranslate/model/transformer.py +256 -256
  33. src/easytranslate/training/__init__.py +12 -12
  34. src/easytranslate/training/loss.py +57 -57
  35. src/easytranslate/training/optimizer.py +76 -76
  36. src/easytranslate/training/trainer.py +186 -186
  37. src/easytranslate/utils/__init__.py +7 -7
  38. src/easytranslate/utils/config.py +51 -51
  39. src/easytranslate/utils/logging.py +26 -26
  40. src/easytranslate/utils/seed.py +23 -23
  41. tests/test_data.py +143 -143
  42. tests/test_evaluation.py +54 -54
  43. tests/test_model.py +60 -60
  44. tests/test_training.py +46 -46
.gitattributes CHANGED
@@ -1,35 +1,35 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -1,57 +1,57 @@
1
- # Byte-compiled / optimized / DLL files
2
- __pycache__/
3
- *.py[cod]
4
- *$py.class
5
-
6
- # Distribution
7
- dist/
8
- build/
9
- *.egg-info/
10
- *.egg
11
-
12
- # Virtual environments
13
- .venv/
14
- venv/
15
- env/
16
-
17
- # IDE
18
- .idea/
19
- .vscode/
20
- *.swp
21
- *.swo
22
-
23
- # Data
24
- /data/
25
- *.tsv
26
- *.csv
27
-
28
- # Checkpoints & Outputs
29
- checkpoints/
30
- outputs/
31
- logs/
32
- wandb/
33
-
34
- # Test cache
35
- .pytest_cache/
36
-
37
- # Jupyter
38
- .ipynb_checkpoints/
39
-
40
- # Log files
41
- *.log
42
-
43
- # OS
44
- .DS_Store
45
- Thumbs.db
46
-
47
- # Models
48
- *.pt
49
- *.pth
50
- *.bin
51
- *.safetensors
52
- *.onnx
53
-
54
- # Tokenizer artifacts
55
- *.model
56
- *.vocab
57
- tokenizer.json
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ *.egg
11
+
12
+ # Virtual environments
13
+ .venv/
14
+ venv/
15
+ env/
16
+
17
+ # IDE
18
+ .idea/
19
+ .vscode/
20
+ *.swp
21
+ *.swo
22
+
23
+ # Data
24
+ /data/
25
+ *.tsv
26
+ *.csv
27
+
28
+ # Checkpoints & Outputs
29
+ checkpoints/
30
+ outputs/
31
+ logs/
32
+ wandb/
33
+
34
+ # Test cache
35
+ .pytest_cache/
36
+
37
+ # Jupyter
38
+ .ipynb_checkpoints/
39
+
40
+ # Log files
41
+ *.log
42
+
43
+ # OS
44
+ .DS_Store
45
+ Thumbs.db
46
+
47
+ # Models
48
+ *.pt
49
+ *.pth
50
+ *.bin
51
+ *.safetensors
52
+ *.onnx
53
+
54
+ # Tokenizer artifacts
55
+ *.model
56
+ *.vocab
57
+ tokenizer.json
README.md CHANGED
@@ -1,77 +1,77 @@
1
- ---
2
- license: mit
3
- ---
4
-
5
- # EasyTranslate: 基于 Transformer 的英中翻译系统
6
-
7
- > 研究生 NLP 期末项目 — 使用前沿技术实现高质量英文到中文机器翻译
8
-
9
- ## 亮点
10
-
11
- - **Pre-LayerNorm Transformer**: 从零构建,训练更稳定
12
- - **RoPE 旋转位置编码**: 替代传统正弦编码,更好捕获相对位置信息
13
- - **Flash Attention 2**: 利用 PyTorch 2.0+ 原生支持,训练显著加速
14
- - **NLLB 预训练微调**: 基于 Meta NLLB-200 模型,支持 LoRA 参数高效微调
15
- - **完整评估体系**: SacreBLEU + COMET + chrF++ 多维度评估
16
- - **6 组消融实验**: 系统验证各前沿组件的效果
17
-
18
- ## 快速开始
19
-
20
- ```bash
21
- # 安装
22
- conda create -n easytranslate python=3.11 && conda activate easytranslate
23
- pip install torch --index-url https://download.pytorch.org/whl/cu121
24
- pip install -r requirements.txt && pip install -e .
25
-
26
- # 从头训练 Transformer
27
- python scripts/train.py --config configs/default_config.yaml
28
-
29
- # 微调 NLLB + LoRA
30
- python scripts/train.py --config configs/default_config.yaml model.type=finetune_nllb
31
-
32
- # 评估
33
- python scripts/evaluate.py --checkpoint checkpoints/best_model.pt
34
-
35
- # 交互翻译
36
- python scripts/translate.py --checkpoint checkpoints/best_model.pt
37
- ```
38
-
39
- ## 项目结构
40
-
41
- ```
42
- src/easytranslate/
43
- ├── data/ # 数据加载、分词、预处理、动态批处理
44
- ├── model/ # Transformer (Encoder-Decoder) + 注意力 + 位置编码 + LoRA
45
- ├── training/ # 训练循环、优化器、损失函数、分布式训练
46
- ├── evaluation/ # BLEU/COMET 指标、Beam Search 解码、评估器
47
- └── utils/ # 配置管理、日志、随机种子
48
- ```
49
-
50
- ## 技术架构
51
-
52
- ```
53
- 英文输入 → BPE Tokenizer → Source Embedding + RoPE
54
- → Transformer Encoder (6 layers, Flash Attention, Pre-LN)
55
- → Transformer Decoder (6 layers, Causal Mask, Cross-Attention)
56
- → Linear Projection → Beam Search → 中文输出
57
- ```
58
-
59
- ## 团队分工
60
-
61
- 详见 [TASK_ASSIGNMENT.md](TASK_ASSIGNMENT.md)
62
-
63
- | 角色 | 负责模块 | 核心任务 |
64
- |------|----------|----------|
65
- | Person A | `data/` | 数据集加载、BPE 分词器、预处理流水线 |
66
- | Person B | `model/` | Transformer 架构、Flash Attention、RoPE、LoRA |
67
- | Person C | `training/` | 训练循环、混合精度、梯度累积、早停 |
68
- | Person D | `evaluation/` | BLEU/COMET 评估、Beam Search、推理服务 |
69
- | Person E | `utils/` + 实验 | 工具模块、消融实验、可视化、报告 |
70
-
71
- ## 依赖
72
-
73
- - Python >= 3.10
74
- - PyTorch >= 2.1 (Flash Attention 支持)
75
- - HuggingFace Transformers / Datasets / Tokenizers
76
- - PEFT (LoRA)
77
  - SacreBLEU, COMET (评估)
 
1
+ ---
2
+ license: mit
3
+ ---
4
+
5
+ # EasyTranslate: 基于 Transformer 的英中翻译系统
6
+
7
+ > 研究生 NLP 期末项目 — 使用前沿技术实现高质量英文到中文机器翻译
8
+
9
+ ## 亮点
10
+
11
+ - **Pre-LayerNorm Transformer**: 从零构建,训练更稳定
12
+ - **RoPE 旋转位置编码**: 替代传统正弦编码,更好捕获相对位置信息
13
+ - **Flash Attention 2**: 利用 PyTorch 2.0+ 原生支持,训练显著加速
14
+ - **NLLB 预训练微调**: 基于 Meta NLLB-200 模型,支持 LoRA 参数高效微调
15
+ - **完整评估体系**: SacreBLEU + COMET + chrF++ 多维度评估
16
+ - **6 组消融实验**: 系统验证各前沿组件的效果
17
+
18
+ ## 快速开始
19
+
20
+ ```bash
21
+ # 安装
22
+ conda create -n easytranslate python=3.11 && conda activate easytranslate
23
+ pip install torch --index-url https://download.pytorch.org/whl/cu121
24
+ pip install -r requirements.txt && pip install -e .
25
+
26
+ # 从头训练 Transformer
27
+ python scripts/train.py --config configs/default_config.yaml
28
+
29
+ # 微调 NLLB + LoRA
30
+ python scripts/train.py --config configs/default_config.yaml model.type=finetune_nllb
31
+
32
+ # 评估
33
+ python scripts/evaluate.py --checkpoint checkpoints/best_model.pt
34
+
35
+ # 交互翻译
36
+ python scripts/translate.py --checkpoint checkpoints/best_model.pt
37
+ ```
38
+
39
+ ## 项目结构
40
+
41
+ ```
42
+ src/easytranslate/
43
+ ├── data/ # 数据加载、分词、预处理、动态批处理
44
+ ├── model/ # Transformer (Encoder-Decoder) + 注意力 + 位置编码 + LoRA
45
+ ├── training/ # 训练循环、优化器、损失函数、分布式训练
46
+ ├── evaluation/ # BLEU/COMET 指标、Beam Search 解码、评估器
47
+ └── utils/ # 配置管理、日志、随机种子
48
+ ```
49
+
50
+ ## 技术架构
51
+
52
+ ```
53
+ 英文输入 → BPE Tokenizer → Source Embedding + RoPE
54
+ → Transformer Encoder (6 layers, Flash Attention, Pre-LN)
55
+ → Transformer Decoder (6 layers, Causal Mask, Cross-Attention)
56
+ → Linear Projection → Beam Search → 中文输出
57
+ ```
58
+
59
+ ## 团队分工
60
+
61
+ 详见 [TASK_ASSIGNMENT.md](TASK_ASSIGNMENT.md)
62
+
63
+ | 角色 | 负责模块 | 核心任务 |
64
+ |------|----------|----------|
65
+ | Person A | `data/` | 数据集加载、BPE 分词器、预处理流水线 |
66
+ | Person B | `model/` | Transformer 架构、Flash Attention、RoPE、LoRA |
67
+ | Person C | `training/` | 训练循环、混合精度、梯度累积、早停 |
68
+ | Person D | `evaluation/` | BLEU/COMET 评估、Beam Search、推理服务 |
69
+ | Person E | `utils/` + 实验 | 工具模块、消融实验、可视化、报告 |
70
+
71
+ ## 依赖
72
+
73
+ - Python >= 3.10
74
+ - PyTorch >= 2.1 (Flash Attention 支持)
75
+ - HuggingFace Transformers / Datasets / Tokenizers
76
+ - PEFT (LoRA)
77
  - SacreBLEU, COMET (评估)
TASK_ASSIGNMENT.md CHANGED
@@ -1,414 +1,414 @@
1
- # EasyTranslate 任务分工文档
2
-
3
- ## 项目概述
4
-
5
- **项目名称**: EasyTranslate — 基于 Transformer 的英中翻译系统
6
- **课程**: 研究生 NLP 期末作业
7
- **团队规模**: 5 人
8
- **技术栈**: PyTorch + HuggingFace Transformers + Flash Attention 2 + RoPE + LoRA
9
-
10
- ---
11
-
12
- ## 📂 项目结构
13
-
14
- ```
15
- UCAS-EasyTranslate/
16
- ├── configs/
17
- │ ├── default_config.yaml # 主配置文件
18
- │ └── deepspeed_config.json # DeepSpeed 分布式训练配置
19
- ├── src/easytranslate/
20
- │ ├── data/ # [Person A] 数据模块
21
- │ │ ├── dataset.py # 数据集加载
22
- │ │ ├── tokenizer.py # 分词器
23
- │ │ ├── preprocessing.py # 数据预处理
24
- │ │ └── collator.py # 数据整理 & 动态批处理
25
- │ ├── model/ # [Person B] 模型模块
26
- │ │ ├── transformer.py # Transformer 主模型
27
- │ │ ├── encoder.py # 编码器
28
- │ │ ├── decoder.py # 解码器
29
- │ │ ├── attention.py # 注意力机制 (标准 + Flash Attention)
30
- │ │ ├── positional.py # 位置编码 (Sinusoidal + RoPE)
31
- │ │ └── finetune.py # 预训练模型微调 (NLLB + LoRA)
32
- │ ├── training/ # [Person C] 训练模块
33
- │ │ ├── trainer.py # 训练器 (完整训练循环)
34
- │ │ ├── optimizer.py # 优化器 & 学习率调度
35
- │ │ └── loss.py # 损失函数 (标签平滑)
36
- │ ├── evaluation/ # [Person D] 评估模块
37
- │ │ ├── metrics.py # 评估指标 (BLEU, COMET, chrF)
38
- │ │ ├── decoding.py # 解码策略 (贪心, Beam Search, 采样)
39
- │ │ └── evaluator.py # 评估器
40
- │ └── utils/ # [Person E] 工具模块
41
- │ ├── config.py # 配置管理
42
- │ ├── seed.py # 随机种子
43
- │ └── logging.py # 日志管理
44
- ├── scripts/
45
- │ ├── train.py # 训练入口
46
- │ ├── evaluate.py # 评估入口
47
- │ ├── translate.py # 翻译推理 (CLI + Web UI)
48
- │ ├── run_experiments.py # [Person E] 实验运行器
49
- │ └── visualize.py # [Person E] 可视化分析
50
- ├── tests/
51
- │ ├── test_data.py # 数据模块测试
52
- │ ├── test_model.py # 模型模块测试
53
- │ ├── test_training.py # 训练模块测试
54
- │ └── test_evaluation.py # 评估模块测试
55
- ├── requirements.txt
56
- ├── setup.py
57
- └── README.md
58
- ```
59
-
60
- ---
61
-
62
- ## 👥 人员分工
63
-
64
- ### Person A — 数据工程师
65
-
66
- **负责文件**: `src/easytranslate/data/` 目录下所有文件
67
- **预计工作量**: ~800 行代码
68
- **截止日期建议**: 第 1-2 周
69
-
70
- #### 核心任务
71
-
72
- | 优先级 | 文件 | 任务 | 要点 |
73
- |--------|------|------|------|
74
- | P0 | `dataset.py` | `TranslationDataset.__getitem__` | 实现 tokenize + padding + teacher forcing 输入构造 |
75
- | P0 | `dataset.py` | `load_wmt_dataset` | 使用 HuggingFace datasets 加载 WMT19 zh-en |
76
- | P0 | `tokenizer.py` | `TokenizerWrapper` | 统一分词器接口 (encode/decode/vocab_size/special_tokens) |
77
- | P0 | `tokenizer.py` | `train_bpe_tokenizer` | 使用 HuggingFace tokenizers 库训练 BPE |
78
- | P0 | `tokenizer.py` | `build_tokenizer` | 根据配置构建分词器 |
79
- | P1 | `preprocessing.py` | `clean_text` | Unicode 标准化 + 控制字符去除 |
80
- | P1 | `preprocessing.py` | `filter_by_length` | 按长度和长度比过滤 |
81
- | P1 | `preprocessing.py` | `preprocess_pipeline` | 完整预处理流水线 |
82
- | P1 | `collator.py` | `TranslationCollator` | batch padding + mask 生成 |
83
- | P2 | `collator.py` | `DynamicBatchSampler` | 按 token 数动态构建 batch |
84
- | P2 | `dataset.py` | `load_opus_dataset` | OPUS-100 数据集加载 |
85
- | P2 | `dataset.py` | `load_custom_dataset` | 自定义语料加载 |
86
-
87
- #### 技术参考
88
- - HuggingFace datasets: https://huggingface.co/docs/datasets
89
- - HuggingFace tokenizers: https://huggingface.co/docs/tokenizers
90
- - WMT19 zh-en: `datasets.load_dataset("wmt19", "zh-en")`
91
-
92
- #### 验收标准
93
- - [ ] `pytest tests/test_data.py` 全部通过
94
- - [ ] 能够成功加载 WMT19 数据集并完成预处理
95
- - [ ] BPE 分词器训练成功,encode/decode 往返一致
96
- - [ ] DataLoader 能正常迭代,batch 维度正确
97
-
98
- ---
99
-
100
- ### Person B — 模型架构师
101
-
102
- **负责文件**: `src/easytranslate/model/` 目录下所有文件
103
- **预计工作量**: ~1000 行代码
104
- **截止日期建议**: 第 1-2 周
105
-
106
- #### 核心任务
107
-
108
- | 优先级 | 文件 | 任务 | 要点 |
109
- |--------|------|------|------|
110
- | P0 | `attention.py` | `MultiHeadAttention` | 标准多头注意力: QKV 投影 + scaled dot-product + mask |
111
- | P0 | `attention.py` | `FlashMultiHeadAttention` | 使用 F.scaled_dot_product_attention (PyTorch 2.0+) |
112
- | P0 | `positional.py` | `SinusoidalPositionalEncoding` | 经典正弦余弦位置编码 |
113
- | P0 | `positional.py` | `RotaryPositionalEmbedding` | RoPE 旋转位置编码 (前沿技术!) |
114
- | P0 | `encoder.py` | `TransformerEncoderLayer/Encoder` | Pre-LayerNorm Encoder |
115
- | P0 | `decoder.py` | `TransformerDecoderLayer/Decoder` | Pre-LayerNorm Decoder (self-attn + cross-attn + FFN) |
116
- | P0 | `transformer.py` | `TransformerTranslationModel` | 完整 Enc-Dec 模型: embedding + encoder + decoder + projection |
117
- | P1 | `transformer.py` | `encode / decode_step` | 推理用的编码和单步解码 |
118
- | P1 | `finetune.py` | `load_pretrained_model` | 加载 NLLB/mBART 预训练模型 |
119
- | P1 | `finetune.py` | `setup_lora` | 使用 PEFT 库配置 LoRA 微调 |
120
-
121
- #### 关键技术点
122
-
123
- 1. **Pre-LayerNorm** (比 Post-LN 训练更稳定):
124
- ```
125
- x → LayerNorm → Attention → Add(x) → LayerNorm → FFN → Add
126
- ```
127
-
128
- 2. **RoPE 旋转位置编码** (LLaMA/GPT-NeoX 使用):
129
- ```python
130
- q' = q * cos(θ) + rotate_half(q) * sin(θ)
131
- k' = k * cos(θ) + rotate_half(k) * sin(θ)
132
- # attention(q', k') 自动编码相对位置信息
133
- ```
134
-
135
- 3. **Flash Attention 2** (PyTorch 2.0+ 原生支持):
136
- ```python
137
- F.scaled_dot_product_attention(Q, K, V, attn_mask, dropout_p, is_causal)
138
- ```
139
-
140
- 4. **LoRA 微调** (参数高效):
141
- ```python
142
- from peft import LoraConfig, get_peft_model, TaskType
143
- config = LoraConfig(r=16, lora_alpha=32, target_modules=["q_proj", "v_proj"], task_type=TaskType.SEQ_2_SEQ_LM)
144
- model = get_peft_model(model, config)
145
- ```
146
-
147
- #### 验收标准
148
- - [ ] `pytest tests/test_model.py` 全部通过
149
- - [ ] 模型前向传播输出维度正确: `[B, T, vocab_size]`
150
- - [ ] Flash Attention 和标准 Attention 输出一致
151
- - [ ] RoPE 编码能正确应用到 Q, K
152
- - [ ] LoRA 模型可训练参数量远小于全量参数
153
-
154
- ---
155
-
156
- ### Person C — 训练工程师
157
-
158
- **负责文件**: `src/easytranslate/training/` 目录下所有文件
159
- **预计工作量**: ~700 行代码
160
- **截止日期建议**: 第 2-3 周 (依赖 Person A, B)
161
-
162
- #### 核心任务
163
-
164
- | 优先级 | 文件 | 任务 | 要点 |
165
- |--------|------|------|------|
166
- | P0 | `loss.py` | `LabelSmoothedCrossEntropyLoss` | 带标签平滑的交叉熵损失 + 忽略 padding |
167
- | P0 | `optimizer.py` | `build_optimizer` | 构建 AdamW,支持参数分组 |
168
- | P0 | `optimizer.py` | `build_scheduler` | Cosine with warmup / Inverse sqrt 调度器 |
169
- | P0 | `trainer.py` | `Trainer.__init__` | 初始化训练环境 (设备, 精度, 分布式) |
170
- | P0 | `trainer.py` | `_train_one_epoch` | 单 epoch 训练循环 (混合精度 + 梯度累积) |
171
- | P0 | `trainer.py` | `_validate` | 验证循环 |
172
- | P1 | `trainer.py` | `train` | 主训练循环 (多 epoch) |
173
- | P1 | `trainer.py` | `_save/_load_checkpoint` | 检查点保存和加载 |
174
- | P1 | `trainer.py` | `_should_early_stop` | 早停逻辑 |
175
- | P2 | `trainer.py` | `_setup_distributed` | DDP/FSDP/DeepSpeed 分布式训练 |
176
- | P2 | `trainer.py` | `_log_metrics` | TensorBoard/WandB 日志 |
177
-
178
- #### 关键技术点
179
-
180
- 1. **混合精度训练**:
181
- ```python
182
- scaler = torch.amp.GradScaler('cuda')
183
- with torch.amp.autocast('cuda', dtype=torch.float16):
184
- logits = model(src, tgt)
185
- loss = criterion(logits, labels)
186
- scaler.scale(loss).backward()
187
- scaler.step(optimizer)
188
- scaler.update()
189
- ```
190
-
191
- 2. **梯度累积**:
192
- ```python
193
- loss = loss / gradient_accumulation_steps
194
- loss.backward()
195
- if (step + 1) % gradient_accumulation_steps == 0:
196
- optimizer.step()
197
- optimizer.zero_grad()
198
- ```
199
-
200
- 3. **标签平滑** (smoothing=0.1):
201
- ```
202
- 对于 target token: prob = 1 - smoothing = 0.9
203
- 对于其他 token: prob = smoothing / (V - 1) ≈ 0.000003
204
- ```
205
-
206
- #### 验收标准
207
- - [ ] `pytest tests/test_training.py` 全部通过
208
- - [ ] 能在小数据集上完成完整训练流程
209
- - [ ] Loss 能正常下降
210
- - [ ] 检查点能正确保存和加载
211
- - [ ] 早停机制正常工作
212
-
213
- ---
214
-
215
- ### Person D — 评估与推理工程师
216
-
217
- **负责文件**: `src/easytranslate/evaluation/` + `scripts/evaluate.py` + `scripts/translate.py`
218
- **预计工作量**: ~800 行代码
219
- **截止日期建议**: 第 2-3 周 (依赖 Person B)
220
-
221
- #### 核心任务
222
-
223
- | 优先级 | 文件 | 任务 | 要点 |
224
- |--------|------|------|------|
225
- | P0 | `metrics.py` | `compute_bleu` | SacreBLEU (tokenize="zh" 对中文分词) |
226
- | P0 | `metrics.py` | `compute_comet` | COMET 神经网络评估指标 |
227
- | P0 | `metrics.py` | `compute_chrf` / `compute_ter` | chrF++ 和 TER 指标 |
228
- | P0 | `decoding.py` | `greedy_decode` | 贪心解码 (逐 token argmax) |
229
- | P0 | `decoding.py` | `beam_search_decode` | 束搜索 (最关键的解码算法!) |
230
- | P1 | `decoding.py` | `sample_decode` | 采样解码 (temperature + top-k + top-p) |
231
- | P1 | `evaluator.py` | `Evaluator` | 统一评估接口 |
232
- | P1 | `scripts/evaluate.py` | 评估入口脚本 | 加载模型 → 评估 → 输出结果 |
233
- | P2 | `scripts/translate.py` | 交互翻译 | CLI 交互 + 文件翻译 |
234
- | P2 | `scripts/translate.py` | `launch_web_ui` | Gradio Web 界面 |
235
-
236
- #### 关键技术点
237
-
238
- 1. **Beam Search** (翻译最核心的算法):
239
- ```
240
- 维护 beam_size 个候选序列
241
- 每步扩展所有候选 → 选 top-k → 继续
242
- 完成后按 score / length^penalty 排序
243
- ```
244
-
245
- 2. **SacreBLEU** (标准化评估):
246
- ```python
247
- import sacrebleu
248
- bleu = sacrebleu.corpus_bleu(hypotheses, [references], tokenize="zh")
249
- ```
250
-
251
- 3. **COMET** (最准确的评估指标):
252
- ```python
253
- from comet import download_model, load_from_checkpoint
254
- model = load_from_checkpoint(download_model("Unbabel/wmt22-comet-da"))
255
- data = [{"src": s, "mt": h, "ref": r} for s, h, r in zip(sources, hyps, refs)]
256
- output = model.predict(data)
257
- ```
258
-
259
- #### 验收标准
260
- - [ ] `pytest tests/test_evaluation.py` 全部通过
261
- - [ ] BLEU 计算结果与 sacrebleu CLI 一致
262
- - [ ] Beam search 翻译质量 ≥ Greedy
263
- - [ ] 能完成端到端评估流程
264
- - [ ] 交互翻译模式正常工作
265
-
266
- ---
267
-
268
- ### Person E — 实验与报告
269
-
270
- **负责文件**: `src/easytranslate/utils/` + `scripts/run_experiments.py` + `scripts/visualize.py` + 实验报告
271
- **预计工作量**: ~600 行代码 + 实验报告
272
- **截止日期建议**: 第 1 周 (utils) + 第 3-4 周 (实验)
273
-
274
- #### 核心任务
275
-
276
- | 优先级 | 文件 | 任务 | 要点 |
277
- |--------|------|------|------|
278
- | P0 | `utils/config.py` | 配置管理 | OmegaConf 加载 + CLI 覆盖 |
279
- | P0 | `utils/seed.py` | 随机种子 | 全局种子设置 (可复现) |
280
- | P0 | `utils/logging.py` | 日志系统 | Rich 美化 + 文件日志 |
281
- | P1 | `scripts/run_experiments.py` | 实验运行器 | 自动化运行 6 组消融实验 |
282
- | P1 | `scripts/visualize.py` | 训练曲线 | Loss/BLEU/LR 曲线绘制 |
283
- | P1 | `scripts/visualize.py` | 实验对比 | 柱状图 + 表格对比 |
284
- | P2 | `scripts/visualize.py` | 注意力可视化 | Cross-attention 热力图 |
285
- | P2 | `scripts/visualize.py` | 翻译样例 | 好坏案例展示 |
286
- | P0 | — | 实验报告 | 撰写完整实验报告 |
287
-
288
- #### 实验设计 (6 组消融实验)
289
-
290
- | 实验编号 | 实验名称 | 关键变量 | 目的 |
291
- |----------|----------|----------|------|
292
- | Exp1 | Baseline Transformer | 标准 6 层, d=512, 正弦位置编码 | 基线 |
293
- | Exp2 | + RoPE | 替换正弦编码为 RoPE | 验证旋转位置编码效果 |
294
- | Exp3 | + Flash Attention | 使用 Flash Attention 2 | 验证训练加速效果 |
295
- | Exp4 | Full (RoPE + Flash) | RoPE + Flash Attention | 完整前沿方案 |
296
- | Exp5 | NLLB + LoRA | 预训练 NLLB-600M + LoRA r=16 | 预训练微调效果 |
297
- | Exp6 | NLLB Full FT | 预训练 NLLB-600M 全量微调 | 微调上界 |
298
-
299
- #### 报告结构建议
300
- 1. **摘要**: 项目目标、方法、主要结果
301
- 2. **引言**: 机器翻译背景、Transformer 发展、研究动机
302
- 3. **相关工作**: Transformer、NLLB、LoRA、Flash Attention、RoPE
303
- 4. **方法**:
304
- - 模型架构 (附架构图)
305
- - 训练策略 (混合精度、标签平滑、调度器)
306
- - 预训练微调方案 (NLLB + LoRA)
307
- 5. **实验**:
308
- - 数据集 (WMT19 zh-en)
309
- - 实验设置 (超参数表)
310
- - 实验结果 (BLEU/COMET/chrF 表格)
311
- - 消融分析 (各组件贡献)
312
- - 训练效率对比 (Flash Attention 加速比)
313
- 6. **分析**:
314
- - 注意力可视化
315
- - 翻译案例分析
316
- - 错误分析
317
- 7. **结论与展望**
318
-
319
- #### 验收标准
320
- - [ ] utils 模块功能正常
321
- - [ ] 6 组实验能自动化运行
322
- - [ ] 生成完整的可视化图表
323
- - [ ] 实验报告完整,图表清晰
324
-
325
- ---
326
-
327
- ## 🗓️ 时间线
328
-
329
- ```
330
- 第 1 周: Person A (数据) + Person B (模型) + Person E (utils) 并行开发
331
-
332
- 第 2 周: Person C (训练, 依赖 A+B) + Person D (评估, 依赖 B) 开始
333
- Person A, B 完善和联调
334
-
335
- 第 3 周: 全体联调 → scripts/train.py 整合
336
- Person E 开始跑实验
337
-
338
- 第 4 周: Person E 完成实验 + 报告
339
- 全体 Review + 优化
340
- ```
341
-
342
- ## 🔌 模块接口约定
343
-
344
- ### 分词器接口 (Person A 定义, Person B/C/D 使用)
345
- ```python
346
- tokenizer.encode(text: str) -> list[int]
347
- tokenizer.decode(ids: list[int]) -> str
348
- tokenizer.vocab_size -> int
349
- tokenizer.pad_id -> int
350
- tokenizer.bos_id -> int
351
- tokenizer.eos_id -> int
352
- ```
353
-
354
- ### 模型接口 (Person B 定义, Person C/D 使用)
355
- ```python
356
- # 训练
357
- logits = model(src_ids, tgt_input_ids, src_padding_mask, tgt_padding_mask)
358
- # logits: [B, T, vocab_size]
359
-
360
- # 推理
361
- encoder_output = model.encode(src_ids, src_padding_mask)
362
- next_logits = model.decode_step(tgt_input_ids, encoder_output, src_padding_mask)
363
- ```
364
-
365
- ### 数据 Batch 格式 (Person A 定义, Person C 使用)
366
- ```python
367
- batch = {
368
- "src_ids": Tensor[B, S],
369
- "tgt_input_ids": Tensor[B, T],
370
- "labels": Tensor[B, T],
371
- "src_padding_mask": BoolTensor[B, S],
372
- "tgt_padding_mask": BoolTensor[B, T],
373
- }
374
- ```
375
-
376
- ### 评估接口 (Person D 定义, Person C/E 使用)
377
- ```python
378
- evaluator = Evaluator(model, tokenizer, config)
379
- results = evaluator.evaluate(dataloader)
380
- # results: {"bleu": 25.6, "comet": 0.82, "chrf": 45.3, "ter": 55.2}
381
- ```
382
-
383
- ---
384
-
385
- ## ⚙️ 开发环境
386
-
387
- ```bash
388
- # 1. 创建虚拟环境
389
- conda create -n easytranslate python=3.11
390
- conda activate easytranslate
391
-
392
- # 2. 安装 PyTorch (CUDA 12.1)
393
- pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
394
-
395
- # 3. 安装项目依赖
396
- pip install -r requirements.txt
397
-
398
- # 4. 安装项目 (开发模式)
399
- pip install -e .
400
-
401
- # 5. 运行测试
402
- pytest tests/ -v
403
- ```
404
-
405
- ---
406
-
407
- ## ✅ 最终交付清单
408
-
409
- - [ ] 代码: 所有 `TODO: Person X` 均已实现
410
- - [ ] 测试: `pytest tests/` 全部通过
411
- - [ ] 训练: 至少完成 Exp1 (基线) 和 Exp5 (NLLB+LoRA) 两组实验
412
- - [ ] 评估: 在 WMT19 测试集上报告 BLEU / COMET 分数
413
- - [ ] 报告: 完整实验报告 (含图表)
414
- - [ ] 演示: 交互翻译 Demo 可运行
 
1
+ # EasyTranslate 任务分工文档
2
+
3
+ ## 项目概述
4
+
5
+ **项目名称**: EasyTranslate — 基于 Transformer 的英中翻译系统
6
+ **课程**: 研究生 NLP 期末作业
7
+ **团队规模**: 5 人
8
+ **技术栈**: PyTorch + HuggingFace Transformers + Flash Attention 2 + RoPE + LoRA
9
+
10
+ ---
11
+
12
+ ## 📂 项目结构
13
+
14
+ ```
15
+ UCAS-EasyTranslate/
16
+ ├── configs/
17
+ │ ├── default_config.yaml # 主配置文件
18
+ │ └── deepspeed_config.json # DeepSpeed 分布式训练配置
19
+ ├── src/easytranslate/
20
+ │ ├── data/ # [Person A] 数据模块
21
+ │ │ ├── dataset.py # 数据集加载
22
+ │ │ ├── tokenizer.py # 分词器
23
+ │ │ ├── preprocessing.py # 数据预处理
24
+ │ │ └── collator.py # 数据整理 & 动态批处理
25
+ │ ├── model/ # [Person B] 模型模块
26
+ │ │ ├── transformer.py # Transformer 主模型
27
+ │ │ ├── encoder.py # 编码器
28
+ │ │ ├── decoder.py # 解码器
29
+ │ │ ├── attention.py # 注意力机制 (标准 + Flash Attention)
30
+ │ │ ├── positional.py # 位置编码 (Sinusoidal + RoPE)
31
+ │ │ └── finetune.py # 预训练模型微调 (NLLB + LoRA)
32
+ │ ├── training/ # [Person C] 训练模块
33
+ │ │ ├── trainer.py # 训练器 (完整训练循环)
34
+ │ │ ├── optimizer.py # 优化器 & 学习率调度
35
+ │ │ └── loss.py # 损失函数 (标签平滑)
36
+ │ ├── evaluation/ # [Person D] 评估模块
37
+ │ │ ├── metrics.py # 评估指标 (BLEU, COMET, chrF)
38
+ │ │ ├── decoding.py # 解码策略 (贪心, Beam Search, 采样)
39
+ │ │ └── evaluator.py # 评估器
40
+ │ └── utils/ # [Person E] 工具模块
41
+ │ ├── config.py # 配置管理
42
+ │ ├── seed.py # 随机种子
43
+ │ └── logging.py # 日志管理
44
+ ├── scripts/
45
+ │ ├── train.py # 训练入口
46
+ │ ├── evaluate.py # 评估入口
47
+ │ ├── translate.py # 翻译推理 (CLI + Web UI)
48
+ │ ├── run_experiments.py # [Person E] 实验运行器
49
+ │ └── visualize.py # [Person E] 可视化分析
50
+ ├── tests/
51
+ │ ├── test_data.py # 数据模块测试
52
+ │ ├── test_model.py # 模型模块测试
53
+ │ ├── test_training.py # 训练模块测试
54
+ │ └── test_evaluation.py # 评估模块测试
55
+ ├── requirements.txt
56
+ ├── setup.py
57
+ └── README.md
58
+ ```
59
+
60
+ ---
61
+
62
+ ## 👥 人员分工
63
+
64
+ ### Person A — 数据工程师
65
+
66
+ **负责文件**: `src/easytranslate/data/` 目录下所有文件
67
+ **预计工作量**: ~800 行代码
68
+ **截止日期建议**: 第 1-2 周
69
+
70
+ #### 核心任务
71
+
72
+ | 优先级 | 文件 | 任务 | 要点 |
73
+ |--------|------|------|------|
74
+ | P0 | `dataset.py` | `TranslationDataset.__getitem__` | 实现 tokenize + padding + teacher forcing 输入构造 |
75
+ | P0 | `dataset.py` | `load_wmt_dataset` | 使用 HuggingFace datasets 加载 WMT19 zh-en |
76
+ | P0 | `tokenizer.py` | `TokenizerWrapper` | 统一分词器接口 (encode/decode/vocab_size/special_tokens) |
77
+ | P0 | `tokenizer.py` | `train_bpe_tokenizer` | 使用 HuggingFace tokenizers 库训练 BPE |
78
+ | P0 | `tokenizer.py` | `build_tokenizer` | 根据配置构建分词器 |
79
+ | P1 | `preprocessing.py` | `clean_text` | Unicode 标准化 + 控制字符去除 |
80
+ | P1 | `preprocessing.py` | `filter_by_length` | 按长度和长度比过滤 |
81
+ | P1 | `preprocessing.py` | `preprocess_pipeline` | 完整预处理流水线 |
82
+ | P1 | `collator.py` | `TranslationCollator` | batch padding + mask 生成 |
83
+ | P2 | `collator.py` | `DynamicBatchSampler` | 按 token 数动态构建 batch |
84
+ | P2 | `dataset.py` | `load_opus_dataset` | OPUS-100 数据集加载 |
85
+ | P2 | `dataset.py` | `load_custom_dataset` | 自定义语料加载 |
86
+
87
+ #### 技术参考
88
+ - HuggingFace datasets: https://huggingface.co/docs/datasets
89
+ - HuggingFace tokenizers: https://huggingface.co/docs/tokenizers
90
+ - WMT19 zh-en: `datasets.load_dataset("wmt19", "zh-en")`
91
+
92
+ #### 验收标准
93
+ - [ ] `pytest tests/test_data.py` 全部通过
94
+ - [ ] 能够成功加载 WMT19 数据集并完成预处理
95
+ - [ ] BPE 分词器训练成功,encode/decode 往返一致
96
+ - [ ] DataLoader 能正常迭代,batch 维度正确
97
+
98
+ ---
99
+
100
+ ### Person B — 模型架构师
101
+
102
+ **负责文件**: `src/easytranslate/model/` 目录下所有文件
103
+ **预计工作量**: ~1000 行代码
104
+ **截止日期建议**: 第 1-2 周
105
+
106
+ #### 核心任务
107
+
108
+ | 优先级 | 文件 | 任务 | 要点 |
109
+ |--------|------|------|------|
110
+ | P0 | `attention.py` | `MultiHeadAttention` | 标准多头注意力: QKV 投影 + scaled dot-product + mask |
111
+ | P0 | `attention.py` | `FlashMultiHeadAttention` | 使用 F.scaled_dot_product_attention (PyTorch 2.0+) |
112
+ | P0 | `positional.py` | `SinusoidalPositionalEncoding` | 经典正弦余弦位置编码 |
113
+ | P0 | `positional.py` | `RotaryPositionalEmbedding` | RoPE 旋转位置编码 (前沿技术!) |
114
+ | P0 | `encoder.py` | `TransformerEncoderLayer/Encoder` | Pre-LayerNorm Encoder |
115
+ | P0 | `decoder.py` | `TransformerDecoderLayer/Decoder` | Pre-LayerNorm Decoder (self-attn + cross-attn + FFN) |
116
+ | P0 | `transformer.py` | `TransformerTranslationModel` | 完整 Enc-Dec 模型: embedding + encoder + decoder + projection |
117
+ | P1 | `transformer.py` | `encode / decode_step` | 推理用的编码和单步解码 |
118
+ | P1 | `finetune.py` | `load_pretrained_model` | 加载 NLLB/mBART 预训练模型 |
119
+ | P1 | `finetune.py` | `setup_lora` | 使用 PEFT 库配置 LoRA 微调 |
120
+
121
+ #### 关键技术点
122
+
123
+ 1. **Pre-LayerNorm** (比 Post-LN 训练更稳定):
124
+ ```
125
+ x → LayerNorm → Attention → Add(x) → LayerNorm → FFN → Add
126
+ ```
127
+
128
+ 2. **RoPE 旋转位置编码** (LLaMA/GPT-NeoX 使用):
129
+ ```python
130
+ q' = q * cos(θ) + rotate_half(q) * sin(θ)
131
+ k' = k * cos(θ) + rotate_half(k) * sin(θ)
132
+ # attention(q', k') 自动编码相对位置信息
133
+ ```
134
+
135
+ 3. **Flash Attention 2** (PyTorch 2.0+ 原生支持):
136
+ ```python
137
+ F.scaled_dot_product_attention(Q, K, V, attn_mask, dropout_p, is_causal)
138
+ ```
139
+
140
+ 4. **LoRA 微调** (参数高效):
141
+ ```python
142
+ from peft import LoraConfig, get_peft_model, TaskType
143
+ config = LoraConfig(r=16, lora_alpha=32, target_modules=["q_proj", "v_proj"], task_type=TaskType.SEQ_2_SEQ_LM)
144
+ model = get_peft_model(model, config)
145
+ ```
146
+
147
+ #### 验收标准
148
+ - [ ] `pytest tests/test_model.py` 全部通过
149
+ - [ ] 模型前向传播输出维度正确: `[B, T, vocab_size]`
150
+ - [ ] Flash Attention 和标准 Attention 输出一致
151
+ - [ ] RoPE 编码能正确应用到 Q, K
152
+ - [ ] LoRA 模型可训练参数量远小于全量参数
153
+
154
+ ---
155
+
156
+ ### Person C — 训练工程师
157
+
158
+ **负责文件**: `src/easytranslate/training/` 目录下所有文件
159
+ **预计工作量**: ~700 行代码
160
+ **截止日期建议**: 第 2-3 周 (依赖 Person A, B)
161
+
162
+ #### 核心任务
163
+
164
+ | 优先级 | 文件 | 任务 | 要点 |
165
+ |--------|------|------|------|
166
+ | P0 | `loss.py` | `LabelSmoothedCrossEntropyLoss` | 带标签平滑的交叉熵损失 + 忽略 padding |
167
+ | P0 | `optimizer.py` | `build_optimizer` | 构建 AdamW,支持参数分组 |
168
+ | P0 | `optimizer.py` | `build_scheduler` | Cosine with warmup / Inverse sqrt 调度器 |
169
+ | P0 | `trainer.py` | `Trainer.__init__` | 初始化训练环境 (设备, 精度, 分布式) |
170
+ | P0 | `trainer.py` | `_train_one_epoch` | 单 epoch 训练循环 (混合精度 + 梯度累积) |
171
+ | P0 | `trainer.py` | `_validate` | 验证循环 |
172
+ | P1 | `trainer.py` | `train` | 主训练循环 (多 epoch) |
173
+ | P1 | `trainer.py` | `_save/_load_checkpoint` | 检查点保存和加载 |
174
+ | P1 | `trainer.py` | `_should_early_stop` | 早停逻辑 |
175
+ | P2 | `trainer.py` | `_setup_distributed` | DDP/FSDP/DeepSpeed 分布式训练 |
176
+ | P2 | `trainer.py` | `_log_metrics` | TensorBoard/WandB 日志 |
177
+
178
+ #### 关键技术点
179
+
180
+ 1. **混合精度训练**:
181
+ ```python
182
+ scaler = torch.amp.GradScaler('cuda')
183
+ with torch.amp.autocast('cuda', dtype=torch.float16):
184
+ logits = model(src, tgt)
185
+ loss = criterion(logits, labels)
186
+ scaler.scale(loss).backward()
187
+ scaler.step(optimizer)
188
+ scaler.update()
189
+ ```
190
+
191
+ 2. **梯度累积**:
192
+ ```python
193
+ loss = loss / gradient_accumulation_steps
194
+ loss.backward()
195
+ if (step + 1) % gradient_accumulation_steps == 0:
196
+ optimizer.step()
197
+ optimizer.zero_grad()
198
+ ```
199
+
200
+ 3. **标签平滑** (smoothing=0.1):
201
+ ```
202
+ 对于 target token: prob = 1 - smoothing = 0.9
203
+ 对于其他 token: prob = smoothing / (V - 1) ≈ 0.000003
204
+ ```
205
+
206
+ #### 验收标准
207
+ - [ ] `pytest tests/test_training.py` 全部通过
208
+ - [ ] 能在小数据集上完成完整训练流程
209
+ - [ ] Loss 能正常下降
210
+ - [ ] 检查点能正确保存和加载
211
+ - [ ] 早停机制正常工作
212
+
213
+ ---
214
+
215
+ ### Person D — 评估与推理工程师
216
+
217
+ **负责文件**: `src/easytranslate/evaluation/` + `scripts/evaluate.py` + `scripts/translate.py`
218
+ **预计工作量**: ~800 行代码
219
+ **截止日期建议**: 第 2-3 周 (依赖 Person B)
220
+
221
+ #### 核心任务
222
+
223
+ | 优先级 | 文件 | 任务 | 要点 |
224
+ |--------|------|------|------|
225
+ | P0 | `metrics.py` | `compute_bleu` | SacreBLEU (tokenize="zh" 对中文分词) |
226
+ | P0 | `metrics.py` | `compute_comet` | COMET 神经网络评估指标 |
227
+ | P0 | `metrics.py` | `compute_chrf` / `compute_ter` | chrF++ 和 TER 指标 |
228
+ | P0 | `decoding.py` | `greedy_decode` | 贪心解码 (逐 token argmax) |
229
+ | P0 | `decoding.py` | `beam_search_decode` | 束搜索 (最关键的解码算法!) |
230
+ | P1 | `decoding.py` | `sample_decode` | 采样解码 (temperature + top-k + top-p) |
231
+ | P1 | `evaluator.py` | `Evaluator` | 统一评估接口 |
232
+ | P1 | `scripts/evaluate.py` | 评估入口脚本 | 加载模型 → 评估 → 输出结果 |
233
+ | P2 | `scripts/translate.py` | 交互翻译 | CLI 交互 + 文件翻译 |
234
+ | P2 | `scripts/translate.py` | `launch_web_ui` | Gradio Web 界面 |
235
+
236
+ #### 关键技术点
237
+
238
+ 1. **Beam Search** (翻译最核心的算法):
239
+ ```
240
+ 维护 beam_size 个候选序列
241
+ 每步扩展所有候选 → 选 top-k → 继续
242
+ 完成后按 score / length^penalty 排序
243
+ ```
244
+
245
+ 2. **SacreBLEU** (标准化评估):
246
+ ```python
247
+ import sacrebleu
248
+ bleu = sacrebleu.corpus_bleu(hypotheses, [references], tokenize="zh")
249
+ ```
250
+
251
+ 3. **COMET** (最准确的评估指标):
252
+ ```python
253
+ from comet import download_model, load_from_checkpoint
254
+ model = load_from_checkpoint(download_model("Unbabel/wmt22-comet-da"))
255
+ data = [{"src": s, "mt": h, "ref": r} for s, h, r in zip(sources, hyps, refs)]
256
+ output = model.predict(data)
257
+ ```
258
+
259
+ #### 验收标准
260
+ - [ ] `pytest tests/test_evaluation.py` 全部通过
261
+ - [ ] BLEU 计算结果与 sacrebleu CLI 一致
262
+ - [ ] Beam search 翻译质量 ≥ Greedy
263
+ - [ ] 能完成端到端评估流程
264
+ - [ ] 交互翻译模式正常工作
265
+
266
+ ---
267
+
268
+ ### Person E — 实验与报告
269
+
270
+ **负责文件**: `src/easytranslate/utils/` + `scripts/run_experiments.py` + `scripts/visualize.py` + 实验报告
271
+ **预计工作量**: ~600 行代码 + 实验报告
272
+ **截止日期建议**: 第 1 周 (utils) + 第 3-4 周 (实验)
273
+
274
+ #### 核心任务
275
+
276
+ | 优先级 | 文件 | 任务 | 要点 |
277
+ |--------|------|------|------|
278
+ | P0 | `utils/config.py` | 配置管理 | OmegaConf 加载 + CLI 覆盖 |
279
+ | P0 | `utils/seed.py` | 随机种子 | 全局种子设置 (可复现) |
280
+ | P0 | `utils/logging.py` | 日志系统 | Rich 美化 + 文件日志 |
281
+ | P1 | `scripts/run_experiments.py` | 实验运行器 | 自动化运行 6 组消融实验 |
282
+ | P1 | `scripts/visualize.py` | 训练曲线 | Loss/BLEU/LR 曲线绘制 |
283
+ | P1 | `scripts/visualize.py` | 实验对比 | 柱状图 + 表格对比 |
284
+ | P2 | `scripts/visualize.py` | 注意力可视化 | Cross-attention 热力图 |
285
+ | P2 | `scripts/visualize.py` | 翻译样例 | 好坏案例展示 |
286
+ | P0 | — | 实验报告 | 撰写完整实验报告 |
287
+
288
+ #### 实验设计 (6 组消融实验)
289
+
290
+ | 实验编号 | 实验名称 | 关键变量 | 目的 |
291
+ |----------|----------|----------|------|
292
+ | Exp1 | Baseline Transformer | 标准 6 层, d=512, 正弦位置编码 | 基线 |
293
+ | Exp2 | + RoPE | 替换正弦编码为 RoPE | 验证旋转位置编码效果 |
294
+ | Exp3 | + Flash Attention | 使用 Flash Attention 2 | 验证训练加速效果 |
295
+ | Exp4 | Full (RoPE + Flash) | RoPE + Flash Attention | 完整前沿方案 |
296
+ | Exp5 | NLLB + LoRA | 预训练 NLLB-600M + LoRA r=16 | 预训练微调效果 |
297
+ | Exp6 | NLLB Full FT | 预训练 NLLB-600M 全量微调 | 微调上界 |
298
+
299
+ #### 报告结构建议
300
+ 1. **摘要**: 项目目标、方法、主要结果
301
+ 2. **引言**: 机器翻译背景、Transformer 发展、研究动机
302
+ 3. **相关工作**: Transformer、NLLB、LoRA、Flash Attention、RoPE
303
+ 4. **方法**:
304
+ - 模型架构 (附架构图)
305
+ - 训练策略 (混合精度、标签平滑、调度器)
306
+ - 预训练微调方案 (NLLB + LoRA)
307
+ 5. **实验**:
308
+ - 数据集 (WMT19 zh-en)
309
+ - 实验设置 (超参数表)
310
+ - 实验结果 (BLEU/COMET/chrF 表格)
311
+ - 消融分析 (各组件贡献)
312
+ - 训练效率对比 (Flash Attention 加速比)
313
+ 6. **分析**:
314
+ - 注意力可视化
315
+ - 翻译案例分析
316
+ - 错误分析
317
+ 7. **结论与展望**
318
+
319
+ #### 验收标准
320
+ - [ ] utils 模块功能正常
321
+ - [ ] 6 组实验能自动化运行
322
+ - [ ] 生成完整的可视化图表
323
+ - [ ] 实验报告完整,图表清晰
324
+
325
+ ---
326
+
327
+ ## 🗓️ 时间线
328
+
329
+ ```
330
+ 第 1 周: Person A (数据) + Person B (模型) + Person E (utils) 并行开发
331
+
332
+ 第 2 周: Person C (训练, 依赖 A+B) + Person D (评估, 依赖 B) 开始
333
+ Person A, B 完善和联调
334
+
335
+ 第 3 周: 全体联调 → scripts/train.py 整合
336
+ Person E 开始跑实验
337
+
338
+ 第 4 周: Person E 完成实验 + 报告
339
+ 全体 Review + 优化
340
+ ```
341
+
342
+ ## 🔌 模块接口约定
343
+
344
+ ### 分词器接口 (Person A 定义, Person B/C/D 使用)
345
+ ```python
346
+ tokenizer.encode(text: str) -> list[int]
347
+ tokenizer.decode(ids: list[int]) -> str
348
+ tokenizer.vocab_size -> int
349
+ tokenizer.pad_id -> int
350
+ tokenizer.bos_id -> int
351
+ tokenizer.eos_id -> int
352
+ ```
353
+
354
+ ### 模型接口 (Person B 定义, Person C/D 使用)
355
+ ```python
356
+ # 训练
357
+ logits = model(src_ids, tgt_input_ids, src_padding_mask, tgt_padding_mask)
358
+ # logits: [B, T, vocab_size]
359
+
360
+ # 推理
361
+ encoder_output = model.encode(src_ids, src_padding_mask)
362
+ next_logits = model.decode_step(tgt_input_ids, encoder_output, src_padding_mask)
363
+ ```
364
+
365
+ ### 数据 Batch 格式 (Person A 定义, Person C 使用)
366
+ ```python
367
+ batch = {
368
+ "src_ids": Tensor[B, S],
369
+ "tgt_input_ids": Tensor[B, T],
370
+ "labels": Tensor[B, T],
371
+ "src_padding_mask": BoolTensor[B, S],
372
+ "tgt_padding_mask": BoolTensor[B, T],
373
+ }
374
+ ```
375
+
376
+ ### 评估接口 (Person D 定义, Person C/E 使用)
377
+ ```python
378
+ evaluator = Evaluator(model, tokenizer, config)
379
+ results = evaluator.evaluate(dataloader)
380
+ # results: {"bleu": 25.6, "comet": 0.82, "chrf": 45.3, "ter": 55.2}
381
+ ```
382
+
383
+ ---
384
+
385
+ ## ⚙️ 开发环境
386
+
387
+ ```bash
388
+ # 1. 创建虚拟环境
389
+ conda create -n easytranslate python=3.11
390
+ conda activate easytranslate
391
+
392
+ # 2. 安装 PyTorch (CUDA 12.1)
393
+ pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
394
+
395
+ # 3. 安装项目依赖
396
+ pip install -r requirements.txt
397
+
398
+ # 4. 安装项目 (开发模式)
399
+ pip install -e .
400
+
401
+ # 5. 运行测试
402
+ pytest tests/ -v
403
+ ```
404
+
405
+ ---
406
+
407
+ ## ✅ 最终交付清单
408
+
409
+ - [ ] 代码: 所有 `TODO: Person X` 均已实现
410
+ - [ ] 测试: `pytest tests/` 全部通过
411
+ - [ ] 训练: 至少完成 Exp1 (基线) 和 Exp5 (NLLB+LoRA) 两组实验
412
+ - [ ] 评估: 在 WMT19 测试集上报告 BLEU / COMET 分数
413
+ - [ ] 报告: 完整实验报告 (含图表)
414
+ - [ ] 演示: 交互翻译 Demo 可运行
configs/deepspeed_config.json CHANGED
@@ -1,28 +1,28 @@
1
- {
2
- "fp16": {
3
- "enabled": true,
4
- "loss_scale": 0,
5
- "loss_scale_window": 1000,
6
- "initial_scale_power": 16,
7
- "hysteresis": 2,
8
- "min_loss_scale": 1
9
- },
10
- "zero_optimization": {
11
- "stage": 2,
12
- "offload_optimizer": {
13
- "device": "cpu",
14
- "pin_memory": true
15
- },
16
- "allgather_partitions": true,
17
- "allgather_bucket_size": 2e8,
18
- "overlap_comm": true,
19
- "reduce_scatter": true,
20
- "reduce_bucket_size": 2e8,
21
- "contiguous_gradients": true
22
- },
23
- "gradient_accumulation_steps": 4,
24
- "gradient_clipping": 1.0,
25
- "train_batch_size": "auto",
26
- "train_micro_batch_size_per_gpu": "auto",
27
- "wall_clock_breakdown": false
28
- }
 
1
+ {
2
+ "fp16": {
3
+ "enabled": true,
4
+ "loss_scale": 0,
5
+ "loss_scale_window": 1000,
6
+ "initial_scale_power": 16,
7
+ "hysteresis": 2,
8
+ "min_loss_scale": 1
9
+ },
10
+ "zero_optimization": {
11
+ "stage": 2,
12
+ "offload_optimizer": {
13
+ "device": "cpu",
14
+ "pin_memory": true
15
+ },
16
+ "allgather_partitions": true,
17
+ "allgather_bucket_size": 2e8,
18
+ "overlap_comm": true,
19
+ "reduce_scatter": true,
20
+ "reduce_bucket_size": 2e8,
21
+ "contiguous_gradients": true
22
+ },
23
+ "gradient_accumulation_steps": 4,
24
+ "gradient_clipping": 1.0,
25
+ "train_batch_size": "auto",
26
+ "train_micro_batch_size_per_gpu": "auto",
27
+ "wall_clock_breakdown": false
28
+ }
configs/default_config.yaml CHANGED
@@ -1,182 +1,183 @@
1
- # ============================================================================
2
- # EasyTranslate 默认配置文件
3
- # 基于 Transformer 架构的英中翻译模型
4
- # ============================================================================
5
-
6
- # ---------- 模型配置 ----------
7
- model:
8
- # 模型类型: "transformer_scratch" | "finetune_nllb" | "finetune_mbart"
9
- type: "transformer_scratch"
10
-
11
- # === Transformer from scratch 配置 ===
12
- transformer:
13
- d_model: 512 # 模型维度
14
- nhead: 8 # 多头注意力头数
15
- num_encoder_layers: 6 # 编码器层数
16
- num_decoder_layers: 6 # 解码器层数
17
- dim_feedforward: 2048 # FFN 中间维度
18
- dropout: 0.1 # Dropout 概率
19
- activation: "gelu" # 激活函数: "relu" | "gelu"
20
- max_seq_len: 512 # 最大序列长度
21
- use_flash_attention: true # 是否使用 Flash Attention 2
22
- use_rotary_embedding: true # 是否使用旋转位置编码 (RoPE)
23
- pre_norm: true # Pre-LayerNorm (更稳定)
24
-
25
- # === 预训练模型微调配置 ===
26
- pretrained:
27
- model_name: "facebook/nllb-200-distilled-600M" # HuggingFace 模型名
28
- src_lang: "eng_Latn" # 源语言代码
29
- tgt_lang: "zho_Hans" # 目标语言代码
30
- use_lora: true # 是否使用 LoRA 微调
31
- lora:
32
- r: 16 # LoRA 秩
33
- alpha: 32 # LoRA alpha
34
- dropout: 0.05 # LoRA dropout
35
- target_modules: # LoRA 目标模块
36
- - "q_proj"
37
- - "v_proj"
38
- - "k_proj"
39
- - "o_proj"
40
-
41
- # ---------- 分词器配置 ----------
42
- tokenizer:
43
- # 分词器类型: "bpe" | "sentencepiece" | "pretrained"
44
- type: "bpe"
45
- vocab_size: 32000 # 词汇表大小
46
- min_frequency: 2 # 最小词频
47
- special_tokens:
48
- pad: "<pad>"
49
- unk: "<unk>"
50
- bos: "<s>"
51
- eos: "</s>"
52
- max_length: 512 # 最大 token 长度
53
-
54
- # ---------- 数据配置 ----------
55
- data:
56
- # 数据集: "wmt" | "opus" | "custom"
57
- dataset_name: "wmt"
58
- wmt:
59
- year: "19" # WMT 年份
60
- language_pair: "zh-en" # 语言对
61
- opus:
62
- subset: "UNPC" # OPUS 子集名
63
- custom:
64
- train_src: "data/train.en" # 自定义训练集源语言
65
- train_tgt: "data/train.zh"
66
- val_src: "data/val.en"
67
- val_tgt: "data/val.zh"
68
- test_src: "data/test.en"
69
- test_tgt: "data/test.zh"
70
-
71
- # 数据处理
72
- preprocessing:
73
- lowercase_src: false # 源语言是否小写化
74
- remove_punctuation: false # 是否去除标点
75
- max_src_len: 256 # 源语言最大长度
76
- max_tgt_len: 256 # 目标语言最大长度
77
- filter_by_length: true # 是否按长度过滤
78
- length_ratio_threshold: 3.0 # 长度比阈值
79
-
80
- # DataLoader
81
- dataloader:
82
- batch_size: 32
83
- num_workers: 4
84
- pin_memory: true
85
- dynamic_batching: true # 动态 batch (按 token 数)
86
- max_tokens_per_batch: 8192 # 动态 batch 最大 token 数
87
-
88
- # ---------- 训练配置 ----------
89
- training:
90
- # 基础训练参数
91
- epochs: 30
92
- max_steps: -1 # -1 表示按 epoch 训练
93
- gradient_accumulation_steps: 4
94
- fp16: true # 混合精度训练
95
- bf16: false # BF16 (A100+)
96
- gradient_checkpointing: false
97
-
98
- # 优化器
99
- optimizer:
100
- type: "adamw" # "adam" | "adamw" | "adafactor"
101
- lr: 3.0e-4
102
- weight_decay: 0.01
103
- betas: [0.9, 0.98]
104
- eps: 1.0e-8
105
-
106
- # 学习率调度
107
- scheduler:
108
- type: "cosine_with_warmup" # "cosine_with_warmup" | "inverse_sqrt" | "linear"
109
- warmup_steps: 4000
110
- min_lr: 1.0e-6
111
-
112
- # 正则化
113
- regularization:
114
- label_smoothing: 0.1 # 标签平滑
115
- dropout: 0.1
116
-
117
- # 检查点
118
- checkpoint:
119
- save_dir: "checkpoints/"
120
- save_every_n_steps: 5000
121
- save_best: true # 保存最佳模型
122
- metric_for_best: "bleu" # 选择最佳模型的指标
123
- max_checkpoints: 5 # 最大保存检查点数
124
-
125
- # 早停
126
- early_stopping:
127
- enabled: true
128
- patience: 5
129
- min_delta: 0.1
130
-
131
- # 分布式训练
132
- distributed:
133
- strategy: "ddp" # "ddp" | "fsdp" | "deepspeed"
134
- deepspeed_config: "configs/deepspeed_config.json"
135
-
136
- # ---------- 评估配置 ----------
137
- evaluation:
138
- # 评估指标
139
- metrics:
140
- - "bleu" # SacreBLEU
141
- - "comet" # COMET (神经网络指标)
142
- - "chrf" # chrF++
143
- - "ter" # TER
144
-
145
- # 解码策略
146
- decoding:
147
- strategy: "beam_search" # "greedy" | "beam_search" | "sampling"
148
- beam_size: 5
149
- length_penalty: 1.0
150
- no_repeat_ngram_size: 3
151
- max_decode_len: 256
152
-
153
- # Sampling 参数
154
- sampling:
155
- temperature: 0.7
156
- top_k: 50
157
- top_p: 0.9
158
-
159
- # 评估频率
160
- eval_every_n_steps: 1000
161
- eval_on_epoch_end: true
162
-
163
- # ---------- 日志配置 ----------
164
- logging:
165
- # 日志工具: "wandb" | "tensorboard" | "both"
166
- backend: "tensorboard"
167
- project_name: "EasyTranslate"
168
- log_every_n_steps: 100
169
- log_dir: "logs/"
170
-
171
- # ---------- 推理/部署配置 ----------
172
- inference:
173
- model_path: "checkpoints/best_model"
174
- device: "cuda"
175
- batch_size: 16
176
- quantization: null # null | "int8" | "int4"
177
-
178
- # ---------- 实验配置 ----------
179
- experiment:
180
- seed: 42
181
- name: "baseline"
182
- output_dir: "outputs/"
 
 
1
+ # ============================================================================
2
+ # EasyTranslate 默认配置文件
3
+ # 基于 Transformer 架构的英中翻译模型
4
+ # ============================================================================
5
+
6
+ # ---------- 模型配置 ----------
7
+ model:
8
+ # 模型类型: "transformer_scratch" | "finetune_nllb" | "finetune_mbart"
9
+ type: "transformer_scratch"
10
+
11
+ # === Transformer from scratch 配置 ===
12
+ transformer:
13
+ d_model: 512 # 模型维度
14
+ nhead: 8 # 多头注意力头数
15
+ num_encoder_layers: 6 # 编码器层数
16
+ num_decoder_layers: 6 # 解码器层数
17
+ dim_feedforward: 2048 # FFN 中间维度
18
+ dropout: 0.1 # Dropout 概率
19
+ activation: "gelu" # 激活函数: "relu" | "gelu"
20
+ max_seq_len: 512 # 最大序列长度
21
+ use_flash_attention: true # 是否使用 Flash Attention 2
22
+ use_rotary_embedding: true # 是否使用旋转位置编码 (RoPE)
23
+ pre_norm: true # Pre-LayerNorm (更稳定)
24
+
25
+ # === 预训练模型微调配置 ===
26
+ pretrained:
27
+ model_name: "facebook/nllb-200-distilled-600M" # HuggingFace 模型名
28
+ src_lang: "eng_Latn" # 源语言代码
29
+ tgt_lang: "zho_Hans" # 目标语言代码
30
+ use_lora: true # 是否使用 LoRA 微调
31
+ lora:
32
+ r: 16 # LoRA 秩
33
+ alpha: 32 # LoRA alpha
34
+ dropout: 0.05 # LoRA dropout
35
+ target_modules: # LoRA 目标模块
36
+ - "q_proj"
37
+ - "v_proj"
38
+ - "k_proj"
39
+ - "o_proj"
40
+
41
+ # ---------- 分词器配置 ----------
42
+ tokenizer:
43
+ # 分词器类型: "bpe" | "sentencepiece" | "pretrained"
44
+ type: "bpe"
45
+ vocab_size: 32000 # 词汇表大小
46
+ min_frequency: 2 # 最小词频
47
+ special_tokens:
48
+ pad: "<pad>"
49
+ unk: "<unk>"
50
+ bos: "<s>"
51
+ eos: "</s>"
52
+ max_length: 512 # 最大 token 长度
53
+ auto_train: false # 是否允许自动从 WMT/OPUS 下载训练数据来训练 BPE tokenizer
54
+
55
+ # ---------- 数据配置 ----------
56
+ data:
57
+ # 数据集: "wmt" | "opus" | "custom"
58
+ dataset_name: "wmt"
59
+ wmt:
60
+ year: "19" # WMT 年份
61
+ language_pair: "zh-en" # 语言对
62
+ opus:
63
+ subset: "UNPC" # OPUS 子集名
64
+ custom:
65
+ train_src: "data/train.en" # 自定义训练集源语言
66
+ train_tgt: "data/train.zh"
67
+ val_src: "data/val.en"
68
+ val_tgt: "data/val.zh"
69
+ test_src: "data/test.en"
70
+ test_tgt: "data/test.zh"
71
+
72
+ # 数据处理
73
+ preprocessing:
74
+ lowercase_src: false # 源语言是否小写化
75
+ remove_punctuation: false # 是否去除标点
76
+ max_src_len: 256 # 语言最大长度
77
+ max_tgt_len: 256 # 目标语言最大长度
78
+ filter_by_length: true # 是否按长度过滤
79
+ length_ratio_threshold: 3.0 # 长度比阈值
80
+
81
+ # DataLoader
82
+ dataloader:
83
+ batch_size: 32
84
+ num_workers: 4
85
+ pin_memory: true
86
+ dynamic_batching: true # 动态 batch (按 token 数)
87
+ max_tokens_per_batch: 8192 # 动态 batch 最大 token 数
88
+
89
+ # ---------- 训练配置 ----------
90
+ training:
91
+ # 基础训练参数
92
+ epochs: 30
93
+ max_steps: -1 # -1 表示按 epoch 训练
94
+ gradient_accumulation_steps: 4
95
+ fp16: true # 混合精度训练
96
+ bf16: false # BF16 (A100+)
97
+ gradient_checkpointing: false
98
+
99
+ # 优化器
100
+ optimizer:
101
+ type: "adamw" # "adam" | "adamw" | "adafactor"
102
+ lr: 3.0e-4
103
+ weight_decay: 0.01
104
+ betas: [0.9, 0.98]
105
+ eps: 1.0e-8
106
+
107
+ # 学习率调度
108
+ scheduler:
109
+ type: "cosine_with_warmup" # "cosine_with_warmup" | "inverse_sqrt" | "linear"
110
+ warmup_steps: 4000
111
+ min_lr: 1.0e-6
112
+
113
+ # 正则化
114
+ regularization:
115
+ label_smoothing: 0.1 # 标签平滑
116
+ dropout: 0.1
117
+
118
+ # 检查点
119
+ checkpoint:
120
+ save_dir: "checkpoints/"
121
+ save_every_n_steps: 5000
122
+ save_best: true # 保存最佳模型
123
+ metric_for_best: "bleu" # 选择佳模型的指标
124
+ max_checkpoints: 5 # 最大保存检查点数
125
+
126
+ # 早停
127
+ early_stopping:
128
+ enabled: true
129
+ patience: 5
130
+ min_delta: 0.1
131
+
132
+ # 分布式训练
133
+ distributed:
134
+ strategy: "ddp" # "ddp" | "fsdp" | "deepspeed"
135
+ deepspeed_config: "configs/deepspeed_config.json"
136
+
137
+ # ---------- 评估配置 ----------
138
+ evaluation:
139
+ # 评���指标
140
+ metrics:
141
+ - "bleu" # SacreBLEU
142
+ - "comet" # COMET (神经网络指标)
143
+ - "chrf" # chrF++
144
+ - "ter" # TER
145
+
146
+ # 解码策略
147
+ decoding:
148
+ strategy: "beam_search" # "greedy" | "beam_search" | "sampling"
149
+ beam_size: 5
150
+ length_penalty: 1.0
151
+ no_repeat_ngram_size: 3
152
+ max_decode_len: 256
153
+
154
+ # Sampling 参数
155
+ sampling:
156
+ temperature: 0.7
157
+ top_k: 50
158
+ top_p: 0.9
159
+
160
+ # 评估频率
161
+ eval_every_n_steps: 1000
162
+ eval_on_epoch_end: true
163
+
164
+ # ---------- 日志配置 ----------
165
+ logging:
166
+ # 日志工具: "wandb" | "tensorboard" | "both"
167
+ backend: "tensorboard"
168
+ project_name: "EasyTranslate"
169
+ log_every_n_steps: 100
170
+ log_dir: "logs/"
171
+
172
+ # ---------- 推理/部署配置 ----------
173
+ inference:
174
+ model_path: "checkpoints/best_model"
175
+ device: "cuda"
176
+ batch_size: 16
177
+ quantization: null # null | "int8" | "int4"
178
+
179
+ # ---------- 实验配置 ----------
180
+ experiment:
181
+ seed: 42
182
+ name: "baseline"
183
+ output_dir: "outputs/"
pytest.ini CHANGED
@@ -1,6 +1,6 @@
1
- [pytest]
2
- testpaths = tests
3
- python_files = test_*.py
4
- python_functions = test_*
5
- python_classes = Test*
6
- addopts = -v --tb=short
 
1
+ [pytest]
2
+ testpaths = tests
3
+ python_files = test_*.py
4
+ python_functions = test_*
5
+ python_classes = Test*
6
+ addopts = -v --tb=short
requirements.txt CHANGED
@@ -1,35 +1,36 @@
1
- # Core
2
- torch>=2.1.0
3
- transformers>=4.36.0
4
- datasets>=2.16.0
5
- tokenizers>=0.15.0
6
- sentencepiece>=0.1.99
7
- accelerate>=0.25.0
8
-
9
- # Training
10
- deepspeed>=0.12.0
11
- bitsandbytes>=0.41.0
12
- peft>=0.7.0
13
- wandb>=0.16.0
14
- tensorboard>=2.15.0
15
-
16
- # Evaluation
17
- sacrebleu>=2.4.0
18
- unbabel-comet>=2.2.0
19
- rouge-score>=0.1.2
20
- nltk>=3.8.0
21
-
22
- # Utilities
23
- numpy>=1.24.0,<2.0
24
- pandas>=2.0.0
25
- tqdm>=4.66.0
26
- pyyaml>=6.0.0
27
- omegaconf>=2.3.0
28
- rich>=13.0.0
29
- matplotlib>=3.8.0
30
- seaborn>=0.13.0
31
-
32
- # Serving (optional)
33
- fastapi>=0.108.0
34
- uvicorn>=0.25.0
35
- gradio>=4.10.0
 
 
1
+ # Core
2
+ torch>=2.1.0
3
+ transformers>=4.36.0
4
+ datasets>=2.16.0
5
+ tokenizers>=0.15.0
6
+ sentencepiece>=0.1.99
7
+ accelerate>=0.25.0
8
+
9
+ # Training
10
+ deepspeed>=0.12.0
11
+ bitsandbytes>=0.41.0
12
+ peft>=0.7.0
13
+ wandb>=0.16.0
14
+ tensorboard>=2.15.0
15
+
16
+ # Evaluation
17
+ sacrebleu>=2.4.0
18
+ unbabel-comet>=2.2.0
19
+ rouge-score>=0.1.2
20
+ nltk>=3.8.0
21
+
22
+ # Utilities
23
+ numpy>=1.24.0,<2.0
24
+ pandas>=2.0.0
25
+ tqdm>=4.66.0
26
+ pyyaml>=6.0.0
27
+ omegaconf>=2.3.0
28
+ rich>=13.0.0
29
+ matplotlib>=3.8.0
30
+ seaborn>=0.13.0
31
+
32
+ # Serving (optional)
33
+ fastapi>=0.108.0
34
+ uvicorn>=0.25.0
35
+ streamlit>=1.30.0
36
+ gradio>=4.10.0
scripts/evaluate.py CHANGED
@@ -1,50 +1,285 @@
1
- """
2
- 评估入口脚本
3
-
4
- 使用方式:
5
- # 在测试集上评估
6
- python scripts/evaluate.py --config configs/default_config.yaml --checkpoint checkpoints/best_model.pt
7
-
8
- # 指定解码策略
9
- python scripts/evaluate.py --checkpoint checkpoints/best_model.pt evaluation.decoding.strategy=beam_search evaluation.decoding.beam_size=10
10
- """
11
-
12
- import argparse
13
- import sys
14
- from pathlib import Path
15
-
16
- sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
17
-
18
-
19
- def parse_args():
20
- parser = argparse.ArgumentParser(description="EasyTranslate Evaluation")
21
- parser.add_argument("--config", type=str, default="configs/default_config.yaml")
22
- parser.add_argument("--checkpoint", type=str, required=True, help="模型检查点路径")
23
- parser.add_argument("--output", type=str, default="outputs/evaluation_results.json", help="结果保存路径")
24
- args, unknown = parser.parse_known_args()
25
- return args, unknown
26
-
27
-
28
- def main():
29
- """
30
- 评估主流程。
31
-
32
- TODO [Person D]:
33
- 1. 加载配置和检查点
34
- 2. 重建模型并加载权重
35
- 3. 加载测试数据
36
- 4. 构建 Evaluator
37
- 5. 运行评估
38
- 6. 打印和保存结果
39
- """
40
- args, cli_overrides = parse_args()
41
-
42
- print("=" * 60)
43
- print(" EasyTranslate - Evaluation")
44
- print("=" * 60)
45
-
46
- raise NotImplementedError("TODO: Person D 实现评估主流程")
47
-
48
-
49
- if __name__ == "__main__":
50
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 评估入口脚本
3
+
4
+ 使用方式:
5
+ # 在测试集上评估
6
+ python scripts/evaluate.py --config configs/default_config.yaml --checkpoint checkpoints/best_model.pt
7
+
8
+ # 指定解码策略
9
+ python scripts/evaluate.py --checkpoint checkpoints/best_model.pt evaluation.decoding.strategy=beam_search evaluation.decoding.beam_size=10
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ try:
18
+ from omegaconf import OmegaConf
19
+ except ImportError: # pragma: no cover
20
+ OmegaConf = None
21
+ import yaml
22
+
23
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
24
+
25
+ import torch
26
+ from torch.utils.data import DataLoader
27
+
28
+ from easytranslate.data.collator import TranslationCollator
29
+ from easytranslate.data.dataset import (
30
+ TranslationDataset,
31
+ load_custom_dataset,
32
+ load_opus_dataset,
33
+ load_wmt_dataset,
34
+ )
35
+ from easytranslate.data.tokenizer import TokenizerWrapper, build_tokenizer
36
+ from easytranslate.evaluation.evaluator import Evaluator
37
+ from easytranslate.model import TransformerTranslationModel
38
+ from easytranslate.model.finetune import load_pretrained_model
39
+
40
+
41
+ def parse_args():
42
+ parser = argparse.ArgumentParser(description="EasyTranslate Evaluation")
43
+ parser.add_argument("--config", type=str, default="configs/default_config.yaml")
44
+ parser.add_argument("--checkpoint", type=str, required=True, help="模型检查点路径")
45
+ parser.add_argument("--output", type=str, default="outputs/evaluation_results.json", help="结果保存路径")
46
+ args, unknown = parser.parse_known_args()
47
+ return args, unknown
48
+
49
+
50
+ def _get_config(config, *keys, default=None):
51
+ value = config
52
+ for key in keys:
53
+ if isinstance(value, dict):
54
+ value = value.get(key, default)
55
+ else:
56
+ value = getattr(value, key, default)
57
+ if value is default:
58
+ break
59
+ return value
60
+
61
+
62
+ def _load_config(path, cli_overrides=None):
63
+ if OmegaConf is not None:
64
+ config = OmegaConf.load(path)
65
+ if cli_overrides:
66
+ config = OmegaConf.merge(config, OmegaConf.from_cli(cli_overrides))
67
+ return config
68
+
69
+ with open(path, "r", encoding="utf-8") as fin:
70
+ config = yaml.safe_load(fin)
71
+ if cli_overrides:
72
+ print("Warning: OmegaConf is not installed; CLI overrides are ignored.")
73
+ return config
74
+
75
+
76
+ def load_test_split(config):
77
+ dataset_name = _get_config(config, "data", "dataset_name")
78
+
79
+ if dataset_name == "wmt":
80
+ try:
81
+ dataset = load_wmt_dataset(
82
+ year=_get_config(config, "data", "wmt", "year"),
83
+ language_pair=_get_config(config, "data", "wmt", "language_pair"),
84
+ split="test",
85
+ )
86
+ except Exception:
87
+ dataset = load_wmt_dataset(
88
+ year=_get_config(config, "data", "wmt", "year"),
89
+ language_pair=_get_config(config, "data", "wmt", "language_pair"),
90
+ split="validation",
91
+ )
92
+ return list(dataset["src"]), list(dataset["tgt"])
93
+
94
+ if dataset_name == "opus":
95
+ try:
96
+ dataset = load_opus_dataset(
97
+ subset=_get_config(config, "data", "opus", "subset"),
98
+ split="test",
99
+ )
100
+ except Exception:
101
+ dataset = load_opus_dataset(
102
+ subset=_get_config(config, "data", "opus", "subset"),
103
+ split="validation",
104
+ )
105
+ return list(dataset["src"]), list(dataset["tgt"])
106
+
107
+ if dataset_name == "custom":
108
+ data = load_custom_dataset(
109
+ train_src=_get_config(config, "data", "custom", "train_src"),
110
+ train_tgt=_get_config(config, "data", "custom", "train_tgt"),
111
+ val_src=_get_config(config, "data", "custom", "val_src"),
112
+ val_tgt=_get_config(config, "data", "custom", "val_tgt"),
113
+ test_src=_get_config(config, "data", "custom", "test_src"),
114
+ test_tgt=_get_config(config, "data", "custom", "test_tgt"),
115
+ preprocessing_config=_get_config(config, "data", "preprocessing"),
116
+ )
117
+ if "test" not in data:
118
+ raise ValueError("Custom dataset missing test split")
119
+ return data["test"]["src"], data["test"]["tgt"]
120
+
121
+ raise ValueError(f"Unsupported dataset_name: {dataset_name}")
122
+
123
+
124
+ def _get_tokenizer_train_texts(config, allow_auto: bool = False) -> list[str] | None:
125
+ dataset_name = _get_config(config, "data", "dataset_name")
126
+ if dataset_name == "custom":
127
+ custom = _get_config(config, "data", "custom") or {}
128
+ data = load_custom_dataset(
129
+ train_src=custom.get("train_src"),
130
+ train_tgt=custom.get("train_tgt"),
131
+ val_src=custom.get("val_src"),
132
+ val_tgt=custom.get("val_tgt"),
133
+ test_src=custom.get("test_src"),
134
+ test_tgt=custom.get("test_tgt"),
135
+ preprocessing_config=_get_config(config, "data", "preprocessing"),
136
+ )
137
+ return list(data["train"]["src"]) + list(data["train"]["tgt"])
138
+
139
+ if not allow_auto:
140
+ return None
141
+
142
+ if dataset_name == "wmt":
143
+ try:
144
+ dataset = load_wmt_dataset(
145
+ year=_get_config(config, "data", "wmt", "year"),
146
+ language_pair=_get_config(config, "data", "wmt", "language_pair"),
147
+ split="train",
148
+ )
149
+ except Exception:
150
+ dataset = load_wmt_dataset(
151
+ year=_get_config(config, "data", "wmt", "year"),
152
+ language_pair=_get_config(config, "data", "wmt", "language_pair"),
153
+ split="validation",
154
+ )
155
+ return list(dataset["src"]) + list(dataset["tgt"])
156
+
157
+ if dataset_name == "opus":
158
+ try:
159
+ dataset = load_opus_dataset(
160
+ subset=_get_config(config, "data", "opus", "subset"),
161
+ split="train",
162
+ )
163
+ except Exception:
164
+ dataset = load_opus_dataset(
165
+ subset=_get_config(config, "data", "opus", "subset"),
166
+ split="validation",
167
+ )
168
+ return list(dataset["src"]) + list(dataset["tgt"])
169
+
170
+ return None
171
+
172
+
173
+ def build_model_and_tokenizer(config, device):
174
+ model_type = _get_config(config, "model", "type")
175
+ if model_type == "transformer_scratch":
176
+ tokenizer_config = _get_config(config, "tokenizer") or {}
177
+ tokenizer_path = tokenizer_config.get("path") or tokenizer_config.get("tokenizer_path")
178
+ tokenizer_type = tokenizer_config.get("type", "bpe")
179
+ auto_train = bool(tokenizer_config.get("auto_train", False))
180
+ if tokenizer_type in {"bpe", "sentencepiece"} and not tokenizer_path:
181
+ train_texts = _get_tokenizer_train_texts(config, allow_auto=auto_train)
182
+ if train_texts is None:
183
+ raise ValueError(
184
+ "BPE tokenizer requires tokenizer.path or a local custom dataset with train texts. "
185
+ "Automatic WMT/OPUS download is disabled by default. "
186
+ "Set tokenizer.auto_train=true to enable it, or provide tokenizer.path/pretrained tokenizer."
187
+ )
188
+ tokenizer = build_tokenizer(tokenizer_config, train_texts=train_texts)
189
+ else:
190
+ tokenizer = build_tokenizer(tokenizer_config)
191
+ model = TransformerTranslationModel(
192
+ src_vocab_size=tokenizer.vocab_size,
193
+ tgt_vocab_size=tokenizer.vocab_size,
194
+ d_model=_get_config(config, "model", "transformer", "d_model"),
195
+ nhead=_get_config(config, "model", "transformer", "nhead"),
196
+ num_encoder_layers=_get_config(config, "model", "transformer", "num_encoder_layers"),
197
+ num_decoder_layers=_get_config(config, "model", "transformer", "num_decoder_layers"),
198
+ dim_feedforward=_get_config(config, "model", "transformer", "dim_feedforward"),
199
+ dropout=_get_config(config, "model", "transformer", "dropout"),
200
+ activation=_get_config(config, "model", "transformer", "activation"),
201
+ max_seq_len=_get_config(config, "model", "transformer", "max_seq_len"),
202
+ use_flash_attention=_get_config(config, "model", "transformer", "use_flash_attention"),
203
+ use_rotary_embedding=_get_config(config, "model", "transformer", "use_rotary_embedding"),
204
+ pre_norm=_get_config(config, "model", "transformer", "pre_norm"),
205
+ pad_id=tokenizer.pad_token_id,
206
+ )
207
+ return model.to(device), tokenizer
208
+
209
+ model, hf_tokenizer = load_pretrained_model(
210
+ config.model.pretrained.model_name,
211
+ config.model.pretrained.src_lang,
212
+ config.model.pretrained.tgt_lang,
213
+ device=str(device),
214
+ )
215
+ tokenizer = TokenizerWrapper(
216
+ hf_tokenizer,
217
+ pad_token=getattr(hf_tokenizer, "pad_token", "<pad>"),
218
+ unk_token=getattr(hf_tokenizer, "unk_token", "<unk>"),
219
+ bos_token=getattr(hf_tokenizer, "bos_token", "<s>"),
220
+ eos_token=getattr(hf_tokenizer, "eos_token", "</s>"),
221
+ )
222
+ return model, tokenizer
223
+
224
+
225
+ def load_checkpoint(model, checkpoint_path, device):
226
+ checkpoint = torch.load(checkpoint_path, map_location=device)
227
+ if isinstance(checkpoint, dict):
228
+ if "model_state_dict" in checkpoint:
229
+ model.load_state_dict(checkpoint["model_state_dict"])
230
+ elif "state_dict" in checkpoint:
231
+ model.load_state_dict(checkpoint["state_dict"])
232
+ else:
233
+ try:
234
+ model.load_state_dict(checkpoint)
235
+ except Exception as exc:
236
+ raise ValueError("Checkpoint does not contain a valid model state dict") from exc
237
+ else:
238
+ raise ValueError("Unsupported checkpoint format")
239
+ return model
240
+
241
+
242
+ def main():
243
+ args, cli_overrides = parse_args()
244
+
245
+ print("=" * 60)
246
+ print(" EasyTranslate - Evaluation")
247
+ print("=" * 60)
248
+
249
+ config = _load_config(args.config, cli_overrides)
250
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
251
+ model, tokenizer = build_model_and_tokenizer(config, device)
252
+ model = load_checkpoint(model, args.checkpoint, device)
253
+
254
+ src_texts, tgt_texts = load_test_split(config)
255
+
256
+ dataset = TranslationDataset(
257
+ src_texts=src_texts,
258
+ tgt_texts=tgt_texts,
259
+ tokenizer=tokenizer,
260
+ max_src_len=_get_config(config, "data", "preprocessing", "max_src_len"),
261
+ max_tgt_len=_get_config(config, "data", "preprocessing", "max_tgt_len"),
262
+ )
263
+ collator = TranslationCollator(pad_token_id=tokenizer.pad_token_id)
264
+ dataloader = DataLoader(
265
+ dataset,
266
+ batch_size=config.data.dataloader.batch_size,
267
+ shuffle=False,
268
+ num_workers=int(config.data.dataloader.num_workers),
269
+ pin_memory=bool(config.data.dataloader.pin_memory),
270
+ collate_fn=collator,
271
+ )
272
+
273
+ evaluator = Evaluator(model, tokenizer, config)
274
+ results = evaluator.evaluate(dataloader, src_texts=src_texts, ref_texts=tgt_texts)
275
+
276
+ output_path = Path(args.output)
277
+ output_path.parent.mkdir(parents=True, exist_ok=True)
278
+ with output_path.open("w", encoding="utf-8") as fout:
279
+ json.dump(results, fout, ensure_ascii=False, indent=2)
280
+
281
+ print(json.dumps(results, ensure_ascii=False, indent=2))
282
+
283
+
284
+ if __name__ == "__main__":
285
+ main()
scripts/run_experiments.py CHANGED
@@ -1,113 +1,113 @@
1
- """
2
- 实验运行脚本 — Person E 负责
3
-
4
- 自动化运行消融实验,比较不同配置的效果。
5
-
6
- 使用方式:
7
- python scripts/run_experiments.py
8
- """
9
-
10
- import sys
11
- from pathlib import Path
12
-
13
- sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
14
-
15
-
16
- # 实验配置列表
17
- EXPERIMENTS = [
18
- {
19
- "name": "exp1_baseline_transformer",
20
- "description": "基线: 从头训练标准 Transformer (6层, d=512)",
21
- "overrides": {
22
- "model.type": "transformer_scratch",
23
- "model.transformer.num_encoder_layers": 6,
24
- "model.transformer.num_decoder_layers": 6,
25
- "model.transformer.d_model": 512,
26
- "model.transformer.use_flash_attention": False,
27
- "model.transformer.use_rotary_embedding": False,
28
- },
29
- },
30
- {
31
- "name": "exp2_transformer_rope",
32
- "description": "消融: Transformer + RoPE 位置编码",
33
- "overrides": {
34
- "model.type": "transformer_scratch",
35
- "model.transformer.use_rotary_embedding": True,
36
- },
37
- },
38
- {
39
- "name": "exp3_transformer_flash_attn",
40
- "description": "消融: Transformer + Flash Attention 2",
41
- "overrides": {
42
- "model.type": "transformer_scratch",
43
- "model.transformer.use_flash_attention": True,
44
- },
45
- },
46
- {
47
- "name": "exp4_transformer_full",
48
- "description": "完整: Transformer + RoPE + Flash Attention",
49
- "overrides": {
50
- "model.type": "transformer_scratch",
51
- "model.transformer.use_flash_attention": True,
52
- "model.transformer.use_rotary_embedding": True,
53
- },
54
- },
55
- {
56
- "name": "exp5_nllb_lora",
57
- "description": "预训练微调: NLLB-600M + LoRA",
58
- "overrides": {
59
- "model.type": "finetune_nllb",
60
- "model.pretrained.use_lora": True,
61
- "model.pretrained.lora.r": 16,
62
- },
63
- },
64
- {
65
- "name": "exp6_nllb_full_finetune",
66
- "description": "预训练全量微调: NLLB-600M",
67
- "overrides": {
68
- "model.type": "finetune_nllb",
69
- "model.pretrained.use_lora": False,
70
- },
71
- },
72
- ]
73
-
74
-
75
- def run_single_experiment(exp_config: dict):
76
- """
77
- 运行单个实验。
78
-
79
- TODO [Person E]:
80
- 1. 从 base config 创建实验配置
81
- 2. 应用 overrides
82
- 3. 设置 experiment.name 和 output_dir
83
- 4. 调用训练流程
84
- 5. 在测试集上评估
85
- 6. 保存实验结果
86
- """
87
- raise NotImplementedError("TODO: Person E 实现 run_single_experiment")
88
-
89
-
90
- def main():
91
- """
92
- 运行所有实验。
93
-
94
- TODO [Person E]:
95
- 1. 遍历 EXPERIMENTS 列表
96
- 2. 对每个实验调用 run_single_experiment
97
- 3. 收集所有实验结果
98
- 4. 生成对比表格
99
- 5. 保存汇总报告
100
- """
101
- print("=" * 60)
102
- print(" EasyTranslate - Experiment Runner")
103
- print("=" * 60)
104
-
105
- for exp in EXPERIMENTS:
106
- print(f"\n>>> Running: {exp['name']} - {exp['description']}")
107
- # run_single_experiment(exp)
108
-
109
- raise NotImplementedError("TODO: Person E 实现实验运行主流程")
110
-
111
-
112
- if __name__ == "__main__":
113
- main()
 
1
+ """
2
+ 实验运行脚本 — Person E 负责
3
+
4
+ 自动化运行消融实验,比较不同配置的效果。
5
+
6
+ 使用方式:
7
+ python scripts/run_experiments.py
8
+ """
9
+
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
14
+
15
+
16
+ # 实验配置列表
17
+ EXPERIMENTS = [
18
+ {
19
+ "name": "exp1_baseline_transformer",
20
+ "description": "基线: 从头训练标准 Transformer (6层, d=512)",
21
+ "overrides": {
22
+ "model.type": "transformer_scratch",
23
+ "model.transformer.num_encoder_layers": 6,
24
+ "model.transformer.num_decoder_layers": 6,
25
+ "model.transformer.d_model": 512,
26
+ "model.transformer.use_flash_attention": False,
27
+ "model.transformer.use_rotary_embedding": False,
28
+ },
29
+ },
30
+ {
31
+ "name": "exp2_transformer_rope",
32
+ "description": "消融: Transformer + RoPE 位置编码",
33
+ "overrides": {
34
+ "model.type": "transformer_scratch",
35
+ "model.transformer.use_rotary_embedding": True,
36
+ },
37
+ },
38
+ {
39
+ "name": "exp3_transformer_flash_attn",
40
+ "description": "消融: Transformer + Flash Attention 2",
41
+ "overrides": {
42
+ "model.type": "transformer_scratch",
43
+ "model.transformer.use_flash_attention": True,
44
+ },
45
+ },
46
+ {
47
+ "name": "exp4_transformer_full",
48
+ "description": "完整: Transformer + RoPE + Flash Attention",
49
+ "overrides": {
50
+ "model.type": "transformer_scratch",
51
+ "model.transformer.use_flash_attention": True,
52
+ "model.transformer.use_rotary_embedding": True,
53
+ },
54
+ },
55
+ {
56
+ "name": "exp5_nllb_lora",
57
+ "description": "预训练微调: NLLB-600M + LoRA",
58
+ "overrides": {
59
+ "model.type": "finetune_nllb",
60
+ "model.pretrained.use_lora": True,
61
+ "model.pretrained.lora.r": 16,
62
+ },
63
+ },
64
+ {
65
+ "name": "exp6_nllb_full_finetune",
66
+ "description": "预训练全量微调: NLLB-600M",
67
+ "overrides": {
68
+ "model.type": "finetune_nllb",
69
+ "model.pretrained.use_lora": False,
70
+ },
71
+ },
72
+ ]
73
+
74
+
75
+ def run_single_experiment(exp_config: dict):
76
+ """
77
+ 运行单个实验。
78
+
79
+ TODO [Person E]:
80
+ 1. 从 base config 创建实验配置
81
+ 2. 应用 overrides
82
+ 3. 设置 experiment.name 和 output_dir
83
+ 4. 调用训练流程
84
+ 5. 在测试集上评估
85
+ 6. 保存实验结果
86
+ """
87
+ raise NotImplementedError("TODO: Person E 实现 run_single_experiment")
88
+
89
+
90
+ def main():
91
+ """
92
+ 运行所有实验。
93
+
94
+ TODO [Person E]:
95
+ 1. 遍历 EXPERIMENTS 列表
96
+ 2. 对每个实验调用 run_single_experiment
97
+ 3. 收集所有实验结果
98
+ 4. 生成对比表格
99
+ 5. 保存汇总报告
100
+ """
101
+ print("=" * 60)
102
+ print(" EasyTranslate - Experiment Runner")
103
+ print("=" * 60)
104
+
105
+ for exp in EXPERIMENTS:
106
+ print(f"\n>>> Running: {exp['name']} - {exp['description']}")
107
+ # run_single_experiment(exp)
108
+
109
+ raise NotImplementedError("TODO: Person E 实现实验运行主流程")
110
+
111
+
112
+ if __name__ == "__main__":
113
+ main()
scripts/train.py CHANGED
@@ -1,76 +1,76 @@
1
- """
2
- 训练入口脚本
3
-
4
- 使用方式:
5
- # 从头训练 Transformer
6
- python scripts/train.py --config configs/default_config.yaml
7
-
8
- # 微调 NLLB
9
- python scripts/train.py --config configs/default_config.yaml model.type=finetune_nllb
10
-
11
- # 命令行覆盖参数
12
- python scripts/train.py --config configs/default_config.yaml training.optimizer.lr=1e-4
13
-
14
- # 分布式训练
15
- torchrun --nproc_per_node=4 scripts/train.py --config configs/default_config.yaml
16
- """
17
-
18
- import argparse
19
- import sys
20
- from pathlib import Path
21
-
22
- # 将 src 目录加入 Python path
23
- sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
24
-
25
-
26
- def parse_args():
27
- parser = argparse.ArgumentParser(description="EasyTranslate Training")
28
- parser.add_argument("--config", type=str, default="configs/default_config.yaml", help="配置文件路径")
29
- parser.add_argument("--resume", type=str, default=None, help="检查点路径 (断点续训)")
30
- args, unknown = parser.parse_known_args()
31
- return args, unknown
32
-
33
-
34
- def main():
35
- """
36
- 训练主流程。
37
-
38
- TODO [整合阶段 - 所有人协作]:
39
- 1. 解析命令行参数
40
- 2. 加载配置 (config_from_cli)
41
- 3. set_seed
42
- 4. setup_logging
43
- 5. 根据 model.type 选择训练模式:
44
-
45
- A) model.type == "transformer_scratch":
46
- a. 加载数据集 (load_wmt_dataset / load_opus_dataset)
47
- b. 预处理 (preprocess_pipeline)
48
- c. 训练分词器 (train_bpe_tokenizer) 或加载已有分词器
49
- d. 构建 TranslationDataset + DataLoader
50
- e. 构建 TransformerTranslationModel
51
- f. 构建 Trainer 并开始训练
52
-
53
- B) model.type == "finetune_nllb" / "finetune_mbart":
54
- a. 加载预训练模型和分词器 (load_pretrained_model)
55
- b. 配置 LoRA (setup_lora)
56
- c. 加载数据集,使用预训练分词器处理
57
- d. 构建 Trainer 并开始训练
58
-
59
- 6. 训练完成后保存最终模型
60
- 7. 在测试集上评估
61
- """
62
- args, cli_overrides = parse_args()
63
-
64
- print("=" * 60)
65
- print(" EasyTranslate - English to Chinese Translation")
66
- print("=" * 60)
67
-
68
- # TODO: 实现训练主流程
69
- raise NotImplementedError(
70
- "TODO: 整合阶段实现训练主流程\n"
71
- "请在所有模块完成后,协作完成此脚本"
72
- )
73
-
74
-
75
- if __name__ == "__main__":
76
- main()
 
1
+ """
2
+ 训练入口脚本
3
+
4
+ 使用方式:
5
+ # 从头训练 Transformer
6
+ python scripts/train.py --config configs/default_config.yaml
7
+
8
+ # 微调 NLLB
9
+ python scripts/train.py --config configs/default_config.yaml model.type=finetune_nllb
10
+
11
+ # 命令行覆盖参数
12
+ python scripts/train.py --config configs/default_config.yaml training.optimizer.lr=1e-4
13
+
14
+ # 分布式训练
15
+ torchrun --nproc_per_node=4 scripts/train.py --config configs/default_config.yaml
16
+ """
17
+
18
+ import argparse
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ # 将 src 目录加入 Python path
23
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
24
+
25
+
26
+ def parse_args():
27
+ parser = argparse.ArgumentParser(description="EasyTranslate Training")
28
+ parser.add_argument("--config", type=str, default="configs/default_config.yaml", help="配置文件路径")
29
+ parser.add_argument("--resume", type=str, default=None, help="检查点路径 (断点续训)")
30
+ args, unknown = parser.parse_known_args()
31
+ return args, unknown
32
+
33
+
34
+ def main():
35
+ """
36
+ 训练主流程。
37
+
38
+ TODO [整合阶段 - 所有人协作]:
39
+ 1. 解析命令行参数
40
+ 2. 加载配置 (config_from_cli)
41
+ 3. set_seed
42
+ 4. setup_logging
43
+ 5. 根据 model.type 选择训练模式:
44
+
45
+ A) model.type == "transformer_scratch":
46
+ a. 加载数据集 (load_wmt_dataset / load_opus_dataset)
47
+ b. 预处理 (preprocess_pipeline)
48
+ c. 训练分词器 (train_bpe_tokenizer) 或加载已有分词器
49
+ d. 构建 TranslationDataset + DataLoader
50
+ e. 构建 TransformerTranslationModel
51
+ f. 构建 Trainer 并开始训练
52
+
53
+ B) model.type == "finetune_nllb" / "finetune_mbart":
54
+ a. 加载预训练模型和分词器 (load_pretrained_model)
55
+ b. 配置 LoRA (setup_lora)
56
+ c. 加载数据集,使用预训练分词器处理
57
+ d. 构建 Trainer 并开始训练
58
+
59
+ 6. 训练完成后保存最终模型
60
+ 7. 在测试集上评估
61
+ """
62
+ args, cli_overrides = parse_args()
63
+
64
+ print("=" * 60)
65
+ print(" EasyTranslate - English to Chinese Translation")
66
+ print("=" * 60)
67
+
68
+ # TODO: 实现训练主流程
69
+ raise NotImplementedError(
70
+ "TODO: 整合阶段实现训练主流程\n"
71
+ "请在所有模块完成后,协作完成此脚本"
72
+ )
73
+
74
+
75
+ if __name__ == "__main__":
76
+ main()
scripts/translate.py CHANGED
@@ -1,82 +1,316 @@
1
- """
2
- 交互式翻译推理脚本
3
-
4
- 使用方式:
5
- # 命令行交互翻译
6
- python scripts/translate.py --checkpoint checkpoints/best_model.pt
7
-
8
- # 翻译文件
9
- python scripts/translate.py --checkpoint checkpoints/best_model.pt --input input.txt --output output.txt
10
-
11
- # 启动 Gradio Web UI
12
- python scripts/translate.py --checkpoint checkpoints/best_model.pt --web
13
- """
14
-
15
- import argparse
16
- import sys
17
- from pathlib import Path
18
-
19
- sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
20
-
21
-
22
- def parse_args():
23
- parser = argparse.ArgumentParser(description="EasyTranslate Inference")
24
- parser.add_argument("--config", type=str, default="configs/default_config.yaml")
25
- parser.add_argument("--checkpoint", type=str, required=True)
26
- parser.add_argument("--input", type=str, default=None, help="输入文件路径")
27
- parser.add_argument("--output", type=str, default=None, help="输出文件路径")
28
- parser.add_argument("--web", action="store_true", help="启动 Gradio Web UI")
29
- return parser.parse_args()
30
-
31
-
32
- def interactive_translate(evaluator):
33
- """
34
- 命令行交互翻译。
35
-
36
- TODO [Person D]:
37
- 1. 循环读取用户输入
38
- 2. 调用 evaluator.translate_single()
39
- 3. 打印翻译结果
40
- 4. 输入 'quit' 退出
41
- """
42
- raise NotImplementedError("TODO: Person D 实现 interactive_translate")
43
-
44
-
45
- def translate_file(evaluator, input_path: str, output_path: str):
46
- """
47
- 文件翻译。
48
-
49
- TODO [Person D]:
50
- 1. 读取文件 (一行一句)
51
- 2. 批量翻译
52
- 3. 将结果写入输出文件
53
- """
54
- raise NotImplementedError("TODO: Person D 实现 translate_file")
55
-
56
-
57
- def launch_web_ui(evaluator):
58
- """
59
- 启动 Gradio Web UI。
60
-
61
- TODO [Person D]:
62
- 1. 创建 Gradio Interface
63
- 2. 输入: 英文文本框
64
- 3. 输出: 中文翻译结果
65
- 4. 调用 evaluator.translate_single()
66
- """
67
- raise NotImplementedError("TODO: Person D 实现 launch_web_ui")
68
-
69
-
70
- def main():
71
- args = parse_args()
72
-
73
- print("=" * 60)
74
- print(" EasyTranslate - Translation")
75
- print("=" * 60)
76
-
77
- # TODO: 加载模型、构建 evaluator,然后根据参数选择模式
78
- raise NotImplementedError("TODO: 实现推理主流程")
79
-
80
-
81
- if __name__ == "__main__":
82
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 交互式翻译推理脚本
3
+
4
+ 使用方式:
5
+ # 命令行交互翻译
6
+ python scripts/translate.py --checkpoint checkpoints/best_model.pt
7
+
8
+ # 翻译文件
9
+ python scripts/translate.py --checkpoint checkpoints/best_model.pt --input input.txt --output output.txt
10
+
11
+ # 启动 Streamlit Web UI(无需模型即可预览界面)
12
+ python scripts/translate.py --web
13
+ # 或者带模型启动翻译
14
+ python scripts/translate.py --checkpoint checkpoints/best_model.pt --web
15
+ """
16
+
17
+ import argparse
18
+ import subprocess
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ try:
23
+ from omegaconf import OmegaConf
24
+ except ImportError: # pragma: no cover
25
+ OmegaConf = None
26
+ import yaml
27
+
28
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
29
+
30
+ import torch
31
+
32
+ from easytranslate.data.collator import TranslationCollator
33
+ from easytranslate.data.dataset import (
34
+ TranslationDataset,
35
+ load_custom_dataset,
36
+ load_opus_dataset,
37
+ load_wmt_dataset,
38
+ )
39
+ from easytranslate.data.tokenizer import TokenizerWrapper, build_tokenizer
40
+ from easytranslate.evaluation.evaluator import Evaluator
41
+ from easytranslate.model import TransformerTranslationModel
42
+ from easytranslate.model.finetune import load_pretrained_model
43
+
44
+
45
+ def parse_args():
46
+ parser = argparse.ArgumentParser(description="EasyTranslate Inference")
47
+ parser.add_argument("--config", type=str, default="configs/default_config.yaml")
48
+ parser.add_argument("--checkpoint", type=str, default=None)
49
+ parser.add_argument("--input", type=str, default=None, help="输入文件路径")
50
+ parser.add_argument("--output", type=str, default=None, help="文件路径")
51
+ parser.add_argument("--web", action="store_true", help="启动 Streamlit Web UI")
52
+ parser.add_argument("--streamlit-app", action="store_true", help=argparse.SUPPRESS)
53
+ args, unknown = parser.parse_known_args()
54
+ return args, unknown
55
+
56
+
57
+ def _get_config(config, *keys, default=None):
58
+ value = config
59
+ for key in keys:
60
+ if isinstance(value, dict):
61
+ value = value.get(key, default)
62
+ else:
63
+ value = getattr(value, key, default)
64
+ if value is default:
65
+ break
66
+ return value
67
+
68
+
69
+ def _load_config(path, cli_overrides=None):
70
+ if OmegaConf is not None:
71
+ config = OmegaConf.load(path)
72
+ if cli_overrides:
73
+ config = OmegaConf.merge(config, OmegaConf.from_cli(cli_overrides))
74
+ return config
75
+
76
+ with open(path, "r", encoding="utf-8") as fin:
77
+ config = yaml.safe_load(fin)
78
+ if cli_overrides:
79
+ print("Warning: OmegaConf is not installed; CLI overrides are ignored.")
80
+ return config
81
+
82
+
83
+ def interactive_translate(evaluator):
84
+ print("输入英⽂句⼦,按回车翻译;输入 'quit' 退出。")
85
+ while True:
86
+ try:
87
+ text = input("> ").strip()
88
+ except EOFError:
89
+ break
90
+ if not text:
91
+ continue
92
+ if text.lower() in {"quit", "exit"}:
93
+ break
94
+ translation = evaluator.translate_single(text)
95
+ print(translation)
96
+
97
+
98
+ def translate_file(evaluator, input_path: str, output_path: str):
99
+ source_lines = []
100
+ with Path(input_path).open("r", encoding="utf-8") as fin:
101
+ for line in fin:
102
+ line = line.strip()
103
+ if line:
104
+ source_lines.append(line)
105
+
106
+ translations = evaluator.translate(source_lines)
107
+
108
+ output_file = Path(output_path)
109
+ output_file.parent.mkdir(parents=True, exist_ok=True)
110
+ with output_file.open("w", encoding="utf-8") as fout:
111
+ for line in translations:
112
+ fout.write(f"{line}\n")
113
+
114
+ print(f"Translation complete: {len(translations)} lines written to {output_path}")
115
+
116
+
117
+ def launch_streamlit_app(args):
118
+ import streamlit as st
119
+
120
+ @st.cache_resource
121
+ def load_evaluator():
122
+ config = _load_config(args.config)
123
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
124
+ model, tokenizer = build_model_and_tokenizer(config, device)
125
+ model = load_checkpoint(model, args.checkpoint, device)
126
+ return Evaluator(model, tokenizer, config)
127
+
128
+ st.set_page_config(page_title="EasyTranslate", layout="wide")
129
+ st.title("EasyTranslate")
130
+ st.write("English to Chinese translation powered by EasyTranslate.")
131
+
132
+ if args.checkpoint is None:
133
+ st.warning(
134
+ "当前未提供模型 checkpoint,页面仅用于预览界面效果。"
135
+ " 如需翻译,请传入 --checkpoint 或先训练生成模型。"
136
+ )
137
+
138
+ input_text = st.text_area("English Input", value="", height=200)
139
+ if st.button("Translate"):
140
+ if not input_text.strip():
141
+ st.warning("请输入要翻译的英文文本。")
142
+ elif args.checkpoint is None:
143
+ st.error("未提供 checkpoint,无法执行翻译。请使用 --checkpoint 参数启动。")
144
+ else:
145
+ with st.spinner("Translating..."):
146
+ try:
147
+ evaluator = load_evaluator()
148
+ translation = evaluator.translate_single(input_text.strip())
149
+ st.text_area("Chinese Translation", value=translation, height=200)
150
+ except Exception as exc:
151
+ st.error(f"模型加载或翻译失败:{exc}")
152
+
153
+ st.markdown("---")
154
+ st.caption("此页面用于展示前端界面;在未提供模型时,翻译功能会被禁用。")
155
+
156
+
157
+ def launch_streamlit_process(args):
158
+ script_path = Path(__file__).resolve()
159
+ cmd = [sys.executable, "-m", "streamlit", "run", str(script_path), "--", "--streamlit-app", "--config", args.config]
160
+ if args.checkpoint:
161
+ cmd.extend(["--checkpoint", args.checkpoint])
162
+ if args.input:
163
+ cmd.extend(["--input", args.input])
164
+ if args.output:
165
+ cmd.extend(["--output", args.output])
166
+ subprocess.run(cmd, check=True)
167
+
168
+
169
+ def _get_tokenizer_train_texts(config, allow_auto: bool = False) -> list[str] | None:
170
+ dataset_name = _get_config(config, "data", "dataset_name")
171
+ if dataset_name == "custom":
172
+ custom = _get_config(config, "data", "custom") or {}
173
+ data = load_custom_dataset(
174
+ train_src=custom.get("train_src"),
175
+ train_tgt=custom.get("train_tgt"),
176
+ val_src=custom.get("val_src"),
177
+ val_tgt=custom.get("val_tgt"),
178
+ test_src=custom.get("test_src"),
179
+ test_tgt=custom.get("test_tgt"),
180
+ preprocessing_config=_get_config(config, "data", "preprocessing"),
181
+ )
182
+ return list(data["train"]["src"]) + list(data["train"]["tgt"])
183
+
184
+ if not allow_auto:
185
+ return None
186
+
187
+ if dataset_name == "wmt":
188
+ try:
189
+ dataset = load_wmt_dataset(
190
+ year=_get_config(config, "data", "wmt", "year"),
191
+ language_pair=_get_config(config, "data", "wmt", "language_pair"),
192
+ split="train",
193
+ )
194
+ except Exception:
195
+ dataset = load_wmt_dataset(
196
+ year=_get_config(config, "data", "wmt", "year"),
197
+ language_pair=_get_config(config, "data", "wmt", "language_pair"),
198
+ split="validation",
199
+ )
200
+ return list(dataset["src"]) + list(dataset["tgt"])
201
+
202
+ if dataset_name == "opus":
203
+ try:
204
+ dataset = load_opus_dataset(
205
+ subset=_get_config(config, "data", "opus", "subset"),
206
+ split="train",
207
+ )
208
+ except Exception:
209
+ dataset = load_opus_dataset(
210
+ subset=_get_config(config, "data", "opus", "subset"),
211
+ split="validation",
212
+ )
213
+ return list(dataset["src"]) + list(dataset["tgt"])
214
+
215
+ return None
216
+
217
+
218
+ def build_model_and_tokenizer(config, device):
219
+ model_type = _get_config(config, "model", "type")
220
+ if model_type == "transformer_scratch":
221
+ tokenizer_config = _get_config(config, "tokenizer") or {}
222
+ tokenizer_path = tokenizer_config.get("path") or tokenizer_config.get("tokenizer_path")
223
+ tokenizer_type = tokenizer_config.get("type", "bpe")
224
+ auto_train = bool(tokenizer_config.get("auto_train", False))
225
+ if tokenizer_type in {"bpe", "sentencepiece"} and not tokenizer_path:
226
+ train_texts = _get_tokenizer_train_texts(config, allow_auto=auto_train)
227
+ if train_texts is None:
228
+ raise ValueError(
229
+ "BPE tokenizer requires tokenizer.path or a local custom dataset with train texts. "
230
+ "Automatic download from WMT/OPUS is disabled by default. "
231
+ "Set tokenizer.auto_train=true to enable it, or provide tokenizer.path/pretrained tokenizer."
232
+ )
233
+ tokenizer = build_tokenizer(tokenizer_config, train_texts=train_texts)
234
+ else:
235
+ tokenizer = build_tokenizer(tokenizer_config)
236
+
237
+ model = TransformerTranslationModel(
238
+ src_vocab_size=tokenizer.vocab_size,
239
+ tgt_vocab_size=tokenizer.vocab_size,
240
+ d_model=_get_config(config, "model", "transformer", "d_model"),
241
+ nhead=_get_config(config, "model", "transformer", "nhead"),
242
+ num_encoder_layers=_get_config(config, "model", "transformer", "num_encoder_layers"),
243
+ num_decoder_layers=_get_config(config, "model", "transformer", "num_decoder_layers"),
244
+ dim_feedforward=_get_config(config, "model", "transformer", "dim_feedforward"),
245
+ dropout=_get_config(config, "model", "transformer", "dropout"),
246
+ activation=_get_config(config, "model", "transformer", "activation"),
247
+ max_seq_len=_get_config(config, "model", "transformer", "max_seq_len"),
248
+ use_flash_attention=_get_config(config, "model", "transformer", "use_flash_attention"),
249
+ use_rotary_embedding=_get_config(config, "model", "transformer", "use_rotary_embedding"),
250
+ pre_norm=_get_config(config, "model", "transformer", "pre_norm"),
251
+ pad_id=tokenizer.pad_token_id,
252
+ )
253
+ return model.to(device), tokenizer
254
+
255
+ model, hf_tokenizer = load_pretrained_model(
256
+ config.model.pretrained.model_name,
257
+ config.model.pretrained.src_lang,
258
+ config.model.pretrained.tgt_lang,
259
+ device=str(device),
260
+ )
261
+ tokenizer = TokenizerWrapper(
262
+ hf_tokenizer,
263
+ pad_token=getattr(hf_tokenizer, "pad_token", "<pad>"),
264
+ unk_token=getattr(hf_tokenizer, "unk_token", "<unk>"),
265
+ bos_token=getattr(hf_tokenizer, "bos_token", "<s>"),
266
+ eos_token=getattr(hf_tokenizer, "eos_token", "</s>"),
267
+ )
268
+ return model, tokenizer
269
+
270
+
271
+ def load_checkpoint(model, checkpoint_path, device):
272
+ checkpoint = torch.load(checkpoint_path, map_location=device)
273
+ if isinstance(checkpoint, dict):
274
+ if "model_state_dict" in checkpoint:
275
+ model.load_state_dict(checkpoint["model_state_dict"])
276
+ elif "state_dict" in checkpoint:
277
+ model.load_state_dict(checkpoint["state_dict"])
278
+ else:
279
+ try:
280
+ model.load_state_dict(checkpoint)
281
+ except Exception as exc:
282
+ raise ValueError("Checkpoint does not contain a valid model state dict") from exc
283
+ else:
284
+ raise ValueError("Unsupported checkpoint format")
285
+ return model
286
+
287
+
288
+ def main():
289
+ args, cli_overrides = parse_args()
290
+
291
+ print("=" * 60)
292
+ print(" EasyTranslate - Translation")
293
+ print("=" * 60)
294
+
295
+ if args.web and not args.streamlit_app:
296
+ launch_streamlit_process(args)
297
+ return
298
+
299
+ if args.streamlit_app:
300
+ launch_streamlit_app(args)
301
+ return
302
+
303
+ config = _load_config(args.config, cli_overrides)
304
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
305
+ model, tokenizer = build_model_and_tokenizer(config, device)
306
+ model = load_checkpoint(model, args.checkpoint, device)
307
+ evaluator = Evaluator(model, tokenizer, config)
308
+
309
+ if args.input and args.output:
310
+ translate_file(evaluator, args.input, args.output)
311
+ else:
312
+ interactive_translate(evaluator)
313
+
314
+
315
+ if __name__ == "__main__":
316
+ main()
scripts/visualize.py CHANGED
@@ -1,86 +1,86 @@
1
- """
2
- 可视化与分析脚本 — Person E 负责
3
-
4
- 功能:
5
- 1. 训练曲线绘制 (loss, BLEU, learning rate)
6
- 2. 实验结果对比图
7
- 3. 注意力权重可视化
8
- 4. 翻译样例展示
9
- """
10
-
11
- import sys
12
- from pathlib import Path
13
-
14
- sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
15
-
16
-
17
- def plot_training_curves(log_dir: str, output_path: str = "outputs/training_curves.png"):
18
- """
19
- 绘制训练曲线。
20
-
21
- TODO [Person E]:
22
- 1. 从 TensorBoard 日志或 CSV 读取训练数据
23
- 2. 绘制子图:
24
- - Train Loss vs Steps
25
- - Val Loss vs Steps
26
- - BLEU vs Epochs
27
- - Learning Rate vs Steps
28
- 3. 保存图片
29
- """
30
- raise NotImplementedError("TODO: Person E 实现 plot_training_curves")
31
-
32
-
33
- def plot_experiment_comparison(results_dir: str, output_path: str = "outputs/experiment_comparison.png"):
34
- """
35
- 绘制实验对比图。
36
-
37
- TODO [Person E]:
38
- 1. 读取所有实验的评估结果
39
- 2. 绘制柱状图: BLEU / COMET / chrF 对比
40
- 3. 绘制表格: 所有指标汇总
41
- 4. 保存图片
42
- """
43
- raise NotImplementedError("TODO: Person E 实现 plot_experiment_comparison")
44
-
45
-
46
- def visualize_attention(
47
- model,
48
- src_text: str,
49
- tgt_text: str,
50
- tokenizer,
51
- output_path: str = "outputs/attention_map.png",
52
- ):
53
- """
54
- 注意力权重可视化。
55
-
56
- TODO [Person E]:
57
- 1. 获取模型的 encoder self-attention 和 cross-attention 权重
58
- 2. 绘制热力图 (matplotlib / seaborn)
59
- 3. x 轴: 源语言 tokens, y 轴: 目标语言 tokens
60
- 4. 支持多头注意力的分别可视化和平均可视化
61
- """
62
- raise NotImplementedError("TODO: Person E 实现 visualize_attention")
63
-
64
-
65
- def generate_translation_examples(
66
- evaluator,
67
- test_pairs: list[tuple[str, str]],
68
- output_path: str = "outputs/translation_examples.md",
69
- ):
70
- """
71
- 生成翻译样例展示。
72
-
73
- TODO [Person E]:
74
- 1. 翻译测试样例
75
- 2. 生成 Markdown 格式的对比表格:
76
- | 源文 (英) | 参考翻译 (中) | 模型翻译 (中) | BLEU |
77
- 3. 包含好的和差的翻译案例
78
- 4. 保存为 Markdown 文件
79
- """
80
- raise NotImplementedError("TODO: Person E 实现 generate_translation_examples")
81
-
82
-
83
- if __name__ == "__main__":
84
- print("请指定要运行的可视化任务")
85
- print(" python scripts/visualize.py --task training_curves --log_dir logs/")
86
- print(" python scripts/visualize.py --task comparison --results_dir outputs/")
 
1
+ """
2
+ 可视化与分析脚本 — Person E 负责
3
+
4
+ 功能:
5
+ 1. 训练曲线绘制 (loss, BLEU, learning rate)
6
+ 2. 实验结果对比图
7
+ 3. 注意力权重可视化
8
+ 4. 翻译样例展示
9
+ """
10
+
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
15
+
16
+
17
+ def plot_training_curves(log_dir: str, output_path: str = "outputs/training_curves.png"):
18
+ """
19
+ 绘制训练曲线。
20
+
21
+ TODO [Person E]:
22
+ 1. 从 TensorBoard 日志或 CSV 读取训练数据
23
+ 2. 绘制子图:
24
+ - Train Loss vs Steps
25
+ - Val Loss vs Steps
26
+ - BLEU vs Epochs
27
+ - Learning Rate vs Steps
28
+ 3. 保存图片
29
+ """
30
+ raise NotImplementedError("TODO: Person E 实现 plot_training_curves")
31
+
32
+
33
+ def plot_experiment_comparison(results_dir: str, output_path: str = "outputs/experiment_comparison.png"):
34
+ """
35
+ 绘制实验对比图。
36
+
37
+ TODO [Person E]:
38
+ 1. 读取所有实验的评估结果
39
+ 2. 绘制柱状图: BLEU / COMET / chrF 对比
40
+ 3. 绘制表格: 所有指标汇总
41
+ 4. 保存图片
42
+ """
43
+ raise NotImplementedError("TODO: Person E 实现 plot_experiment_comparison")
44
+
45
+
46
+ def visualize_attention(
47
+ model,
48
+ src_text: str,
49
+ tgt_text: str,
50
+ tokenizer,
51
+ output_path: str = "outputs/attention_map.png",
52
+ ):
53
+ """
54
+ 注意力权重可视化。
55
+
56
+ TODO [Person E]:
57
+ 1. 获取模型的 encoder self-attention 和 cross-attention 权重
58
+ 2. 绘制热力图 (matplotlib / seaborn)
59
+ 3. x 轴: 源语言 tokens, y 轴: 目标语言 tokens
60
+ 4. 支持多头注意力的分别可视化和平均可视化
61
+ """
62
+ raise NotImplementedError("TODO: Person E 实现 visualize_attention")
63
+
64
+
65
+ def generate_translation_examples(
66
+ evaluator,
67
+ test_pairs: list[tuple[str, str]],
68
+ output_path: str = "outputs/translation_examples.md",
69
+ ):
70
+ """
71
+ 生成翻译样例展示。
72
+
73
+ TODO [Person E]:
74
+ 1. 翻译测试样例
75
+ 2. 生成 Markdown 格式的对比表格:
76
+ | 源文 (英) | 参考翻译 (中) | 模型翻译 (中) | BLEU |
77
+ 3. 包含好的和差的翻译案例
78
+ 4. 保存为 Markdown 文件
79
+ """
80
+ raise NotImplementedError("TODO: Person E 实现 generate_translation_examples")
81
+
82
+
83
+ if __name__ == "__main__":
84
+ print("请指定要运行的可视化任务")
85
+ print(" python scripts/visualize.py --task training_curves --log_dir logs/")
86
+ print(" python scripts/visualize.py --task comparison --results_dir outputs/")
setup.py CHANGED
@@ -1,23 +1,23 @@
1
- from setuptools import setup, find_packages
2
-
3
- setup(
4
- name="easytranslate",
5
- version="0.1.0",
6
- description="Transformer-based English-to-Chinese Translation Model",
7
- packages=find_packages(where="src"),
8
- package_dir={"": "src"},
9
- python_requires=">=3.10",
10
- install_requires=[
11
- "torch>=2.1.0",
12
- "transformers>=4.36.0",
13
- "datasets>=2.16.0",
14
- "tokenizers>=0.15.0",
15
- "sentencepiece>=0.1.99",
16
- "accelerate>=0.25.0",
17
- "sacrebleu>=2.4.0",
18
- "numpy>=1.24.0,<2.0",
19
- "omegaconf>=2.3.0",
20
- "rich>=13.0.0",
21
- "tqdm>=4.66.0",
22
- ],
23
- )
 
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="easytranslate",
5
+ version="0.1.0",
6
+ description="Transformer-based English-to-Chinese Translation Model",
7
+ packages=find_packages(where="src"),
8
+ package_dir={"": "src"},
9
+ python_requires=">=3.10",
10
+ install_requires=[
11
+ "torch>=2.1.0",
12
+ "transformers>=4.36.0",
13
+ "datasets>=2.16.0",
14
+ "tokenizers>=0.15.0",
15
+ "sentencepiece>=0.1.99",
16
+ "accelerate>=0.25.0",
17
+ "sacrebleu>=2.4.0",
18
+ "numpy>=1.24.0,<2.0",
19
+ "omegaconf>=2.3.0",
20
+ "rich>=13.0.0",
21
+ "tqdm>=4.66.0",
22
+ ],
23
+ )
src/easytranslate/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
- """EasyTranslate: Transformer-based English-to-Chinese Translation."""
2
-
3
- __version__ = "0.1.0"
 
1
+ """EasyTranslate: Transformer-based English-to-Chinese Translation."""
2
+
3
+ __version__ = "0.1.0"
src/easytranslate/data/README.md CHANGED
@@ -1,126 +1,126 @@
1
- # EasyTranslate Data 模块使用说明
2
-
3
- 本目录负责英中翻译任务的数据加载、清洗、分词、样本构造和动态批处理。推荐主数据集使用 WMT19 zh-en,调试或小样本实验可使用 OPUS-100 en-zh。
4
-
5
- ## 文件职责
6
-
7
- - `dataset.py`: 加载 WMT/OPUS/custom 数据,并提供 `TranslationDataset`。
8
- - `tokenizer.py`: 训练 BPE tokenizer,并用 `TokenizerWrapper` 统一 tokenizer 接口。
9
- - `preprocessing.py`: 文本标准化、长度过滤、去重。
10
- - `collator.py`: batch padding、attention mask、动态 token batch。
11
-
12
- ## 推荐数据集
13
-
14
- 正式实验建议使用:
15
-
16
- ```python
17
- from easytranslate.data import load_wmt_dataset
18
-
19
- raw = load_wmt_dataset(year="19", language_pair="zh-en")
20
- ```
21
-
22
- 快速调试建议使用:
23
-
24
- ```python
25
- from easytranslate.data import load_opus_dataset
26
-
27
- raw = load_opus_dataset(subset="en-zh")
28
- ```
29
-
30
- 两个加载函数都会把样本统一成:
31
-
32
- ```python
33
- {"src": "English sentence", "tgt": "中文句子"}
34
- ```
35
-
36
- ## 从文本到 DataLoader
37
-
38
- ```python
39
- from torch.utils.data import DataLoader
40
-
41
- from easytranslate.data import (
42
- DynamicBatchSampler,
43
- TranslationCollator,
44
- TranslationDataset,
45
- preprocess_pipeline,
46
- train_bpe_tokenizer,
47
- )
48
-
49
- src_texts = raw["train"]["src"]
50
- tgt_texts = raw["train"]["tgt"]
51
-
52
- src_texts, tgt_texts = preprocess_pipeline(
53
- src_texts,
54
- tgt_texts,
55
- lowercase_src=False,
56
- remove_punctuation=False,
57
- max_src_len=256,
58
- max_tgt_len=256,
59
- length_ratio_threshold=3.0,
60
- )
61
-
62
- tokenizer = train_bpe_tokenizer(
63
- list(src_texts) + list(tgt_texts),
64
- vocab_size=32000,
65
- min_frequency=2,
66
- save_path="outputs/tokenizer/bpe.json",
67
- )
68
-
69
- train_dataset = TranslationDataset(
70
- src_texts,
71
- tgt_texts,
72
- tokenizer=tokenizer,
73
- max_src_len=256,
74
- max_tgt_len=256,
75
- )
76
-
77
- lengths = [
78
- (len(tokenizer.encode(src, add_special_tokens=True)), len(tokenizer.encode(tgt, add_special_tokens=True)))
79
- for src, tgt in zip(src_texts, tgt_texts)
80
- ]
81
-
82
- batch_sampler = DynamicBatchSampler(lengths, max_tokens_per_batch=8192)
83
- collator = TranslationCollator(pad_token_id=tokenizer.pad_token_id)
84
-
85
- loader = DataLoader(
86
- train_dataset,
87
- batch_sampler=batch_sampler,
88
- collate_fn=collator,
89
- num_workers=4,
90
- pin_memory=True,
91
- )
92
- ```
93
-
94
- ## Batch 字段
95
-
96
- `TranslationCollator` 输出:
97
-
98
- - `src_ids`: `[B, S]`
99
- - `tgt_input_ids`: `[B, T]`,以 `<s>` 开头,用于 teacher forcing
100
- - `labels`: `[B, T]`,以 `</s>` 结尾,padding 为 `-100`
101
- - `src_padding_mask`: `[B, S]`,padding 位置为 `True`
102
- - `tgt_padding_mask`: `[B, T]`,padding 位置为 `True`
103
- - `src_attention_mask` / `tgt_attention_mask`: 有效 token 为 `1`
104
- - `src_lens` / `tgt_lens`: 原始长度
105
-
106
- ## 自定义平行语料
107
-
108
- ```python
109
- from easytranslate.data import load_custom_dataset
110
-
111
- data = load_custom_dataset(
112
- train_src="data/train.en",
113
- train_tgt="data/train.zh",
114
- val_src="data/val.en",
115
- val_tgt="data/val.zh",
116
- test_src="data/test.en",
117
- test_tgt="data/test.zh",
118
- )
119
- ```
120
-
121
- ## 处理原则
122
-
123
- - 保留英文大小写和中英文标点,默认不做 lowercase、不去标点。
124
- - 清洗只做 Unicode NFKC、控制字符删除、空白合并。
125
- - tokenizer 只用训练集训练,不使用验证集或测试集。
126
- - 从零训练 Transformer 时建议使用共享 bilingual BPE;微调 NLLB 时直接使用 NLLB tokenizer。
 
1
+ # EasyTranslate Data 模块使用说明
2
+
3
+ 本目录负责英中翻译任务的数据加载、清洗、分词、样本构造和动态批处理。推荐主数据集使用 WMT19 zh-en,调试或小样本实验可使用 OPUS-100 en-zh。
4
+
5
+ ## 文件职责
6
+
7
+ - `dataset.py`: 加载 WMT/OPUS/custom 数据,并提供 `TranslationDataset`。
8
+ - `tokenizer.py`: 训练 BPE tokenizer,并用 `TokenizerWrapper` 统一 tokenizer 接口。
9
+ - `preprocessing.py`: 文本标准化、长度过滤、去重。
10
+ - `collator.py`: batch padding、attention mask、动态 token batch。
11
+
12
+ ## 推荐数据集
13
+
14
+ 正式实验建议使用:
15
+
16
+ ```python
17
+ from easytranslate.data import load_wmt_dataset
18
+
19
+ raw = load_wmt_dataset(year="19", language_pair="zh-en")
20
+ ```
21
+
22
+ 快速调试建议使用:
23
+
24
+ ```python
25
+ from easytranslate.data import load_opus_dataset
26
+
27
+ raw = load_opus_dataset(subset="en-zh")
28
+ ```
29
+
30
+ 两个加载函数都会把样本统一成:
31
+
32
+ ```python
33
+ {"src": "English sentence", "tgt": "中文句子"}
34
+ ```
35
+
36
+ ## 从文本到 DataLoader
37
+
38
+ ```python
39
+ from torch.utils.data import DataLoader
40
+
41
+ from easytranslate.data import (
42
+ DynamicBatchSampler,
43
+ TranslationCollator,
44
+ TranslationDataset,
45
+ preprocess_pipeline,
46
+ train_bpe_tokenizer,
47
+ )
48
+
49
+ src_texts = raw["train"]["src"]
50
+ tgt_texts = raw["train"]["tgt"]
51
+
52
+ src_texts, tgt_texts = preprocess_pipeline(
53
+ src_texts,
54
+ tgt_texts,
55
+ lowercase_src=False,
56
+ remove_punctuation=False,
57
+ max_src_len=256,
58
+ max_tgt_len=256,
59
+ length_ratio_threshold=3.0,
60
+ )
61
+
62
+ tokenizer = train_bpe_tokenizer(
63
+ list(src_texts) + list(tgt_texts),
64
+ vocab_size=32000,
65
+ min_frequency=2,
66
+ save_path="outputs/tokenizer/bpe.json",
67
+ )
68
+
69
+ train_dataset = TranslationDataset(
70
+ src_texts,
71
+ tgt_texts,
72
+ tokenizer=tokenizer,
73
+ max_src_len=256,
74
+ max_tgt_len=256,
75
+ )
76
+
77
+ lengths = [
78
+ (len(tokenizer.encode(src, add_special_tokens=True)), len(tokenizer.encode(tgt, add_special_tokens=True)))
79
+ for src, tgt in zip(src_texts, tgt_texts)
80
+ ]
81
+
82
+ batch_sampler = DynamicBatchSampler(lengths, max_tokens_per_batch=8192)
83
+ collator = TranslationCollator(pad_token_id=tokenizer.pad_token_id)
84
+
85
+ loader = DataLoader(
86
+ train_dataset,
87
+ batch_sampler=batch_sampler,
88
+ collate_fn=collator,
89
+ num_workers=4,
90
+ pin_memory=True,
91
+ )
92
+ ```
93
+
94
+ ## Batch 字段
95
+
96
+ `TranslationCollator` 输出:
97
+
98
+ - `src_ids`: `[B, S]`
99
+ - `tgt_input_ids`: `[B, T]`,以 `<s>` 开头,用于 teacher forcing
100
+ - `labels`: `[B, T]`,以 `</s>` 结尾,padding 为 `-100`
101
+ - `src_padding_mask`: `[B, S]`,padding 位置为 `True`
102
+ - `tgt_padding_mask`: `[B, T]`,padding 位置为 `True`
103
+ - `src_attention_mask` / `tgt_attention_mask`: 有效 token 为 `1`
104
+ - `src_lens` / `tgt_lens`: 原始长度
105
+
106
+ ## 自定义平行语料
107
+
108
+ ```python
109
+ from easytranslate.data import load_custom_dataset
110
+
111
+ data = load_custom_dataset(
112
+ train_src="data/train.en",
113
+ train_tgt="data/train.zh",
114
+ val_src="data/val.en",
115
+ val_tgt="data/val.zh",
116
+ test_src="data/test.en",
117
+ test_tgt="data/test.zh",
118
+ )
119
+ ```
120
+
121
+ ## 处理原则
122
+
123
+ - 保留英文大小写和中英文标点,默认不做 lowercase、不去标点。
124
+ - 清洗只做 Unicode NFKC、控制字符删除、空白合并。
125
+ - tokenizer 只用训练集训练,不使用验证集或测试集。
126
+ - 从零训练 Transformer 时建议使用共享 bilingual BPE;微调 NLLB 时直接使用 NLLB tokenizer。
src/easytranslate/data/__init__.py CHANGED
@@ -1,32 +1,32 @@
1
- """Data utilities for EasyTranslate."""
2
-
3
- from easytranslate.data.collator import DynamicBatchSampler, TranslationCollator
4
- from easytranslate.data.dataset import (
5
- TranslationDataset,
6
- load_custom_dataset,
7
- load_opus_dataset,
8
- load_wmt_dataset,
9
- )
10
- from easytranslate.data.preprocessing import (
11
- clean_text,
12
- deduplicate_pairs,
13
- filter_by_length,
14
- preprocess_pipeline,
15
- )
16
- from easytranslate.data.tokenizer import TokenizerWrapper, build_tokenizer, train_bpe_tokenizer
17
-
18
- __all__ = [
19
- "DynamicBatchSampler",
20
- "TokenizerWrapper",
21
- "TranslationCollator",
22
- "TranslationDataset",
23
- "build_tokenizer",
24
- "clean_text",
25
- "deduplicate_pairs",
26
- "filter_by_length",
27
- "load_custom_dataset",
28
- "load_opus_dataset",
29
- "load_wmt_dataset",
30
- "preprocess_pipeline",
31
- "train_bpe_tokenizer",
32
- ]
 
1
+ """Data utilities for EasyTranslate."""
2
+
3
+ from easytranslate.data.collator import DynamicBatchSampler, TranslationCollator
4
+ from easytranslate.data.dataset import (
5
+ TranslationDataset,
6
+ load_custom_dataset,
7
+ load_opus_dataset,
8
+ load_wmt_dataset,
9
+ )
10
+ from easytranslate.data.preprocessing import (
11
+ clean_text,
12
+ deduplicate_pairs,
13
+ filter_by_length,
14
+ preprocess_pipeline,
15
+ )
16
+ from easytranslate.data.tokenizer import TokenizerWrapper, build_tokenizer, train_bpe_tokenizer
17
+
18
+ __all__ = [
19
+ "DynamicBatchSampler",
20
+ "TokenizerWrapper",
21
+ "TranslationCollator",
22
+ "TranslationDataset",
23
+ "build_tokenizer",
24
+ "clean_text",
25
+ "deduplicate_pairs",
26
+ "filter_by_length",
27
+ "load_custom_dataset",
28
+ "load_opus_dataset",
29
+ "load_wmt_dataset",
30
+ "preprocess_pipeline",
31
+ "train_bpe_tokenizer",
32
+ ]
src/easytranslate/data/collator.py CHANGED
@@ -1,114 +1,114 @@
1
- """Batch collation and dynamic batching for translation training."""
2
-
3
- from __future__ import annotations
4
-
5
- import random
6
- from typing import Iterator, Sequence
7
-
8
- import torch
9
- from torch.nn.utils.rnn import pad_sequence
10
- from torch.utils.data import Sampler
11
-
12
-
13
- class TranslationCollator:
14
- """Pad variable-length translation examples into one batch."""
15
-
16
- def __init__(self, pad_token_id: int = 0, label_pad_token_id: int = -100):
17
- self.pad_token_id = pad_token_id
18
- self.label_pad_token_id = label_pad_token_id
19
-
20
- def __call__(self, batch: Sequence[dict[str, torch.Tensor]]) -> dict[str, torch.Tensor]:
21
- src_ids = pad_sequence(
22
- [item["src_ids"] for item in batch],
23
- batch_first=True,
24
- padding_value=self.pad_token_id,
25
- )
26
- tgt_input_ids = pad_sequence(
27
- [item["tgt_input_ids"] for item in batch],
28
- batch_first=True,
29
- padding_value=self.pad_token_id,
30
- )
31
- labels = pad_sequence(
32
- [item["labels"] for item in batch],
33
- batch_first=True,
34
- padding_value=self.label_pad_token_id,
35
- )
36
-
37
- src_padding_mask = src_ids.eq(self.pad_token_id)
38
- tgt_padding_mask = tgt_input_ids.eq(self.pad_token_id)
39
-
40
- return {
41
- "src_ids": src_ids,
42
- "tgt_input_ids": tgt_input_ids,
43
- "labels": labels,
44
- "src_padding_mask": src_padding_mask,
45
- "tgt_padding_mask": tgt_padding_mask,
46
- "src_attention_mask": (~src_padding_mask).long(),
47
- "tgt_attention_mask": (~tgt_padding_mask).long(),
48
- "src_lens": torch.stack([item["src_len"] for item in batch]),
49
- "tgt_lens": torch.stack([item["tgt_len"] for item in batch]),
50
- }
51
-
52
-
53
- class DynamicBatchSampler(Sampler[list[int]]):
54
- """Create batches constrained by an approximate max token budget."""
55
-
56
- def __init__(
57
- self,
58
- lengths: Sequence[int | tuple[int, int]],
59
- max_tokens_per_batch: int = 8192,
60
- shuffle: bool = True,
61
- drop_last: bool = False,
62
- ):
63
- self.lengths = [max(length) if isinstance(length, tuple) else int(length) for length in lengths]
64
- self.max_tokens_per_batch = max_tokens_per_batch
65
- self.shuffle = shuffle
66
- self.drop_last = drop_last
67
-
68
- def __iter__(self) -> Iterator[list[int]]:
69
- indices = list(range(len(self.lengths)))
70
- if self.shuffle:
71
- random.shuffle(indices)
72
-
73
- indices.sort(key=lambda idx: self.lengths[idx])
74
- batches: list[list[int]] = []
75
- batch: list[int] = []
76
- max_len = 0
77
-
78
- for idx in indices:
79
- candidate_max_len = max(max_len, self.lengths[idx])
80
- candidate_tokens = candidate_max_len * (len(batch) + 1)
81
-
82
- if batch and candidate_tokens > self.max_tokens_per_batch:
83
- batches.append(batch)
84
- batch = []
85
- max_len = 0
86
-
87
- batch.append(idx)
88
- max_len = max(max_len, self.lengths[idx])
89
-
90
- if batch and not self.drop_last:
91
- batches.append(batch)
92
-
93
- if self.shuffle:
94
- random.shuffle(batches)
95
-
96
- yield from batches
97
-
98
- def __len__(self) -> int:
99
- count = 0
100
- batch_size = 0
101
- max_len = 0
102
-
103
- for length in sorted(self.lengths):
104
- candidate_max_len = max(max_len, length)
105
- if batch_size and candidate_max_len * (batch_size + 1) > self.max_tokens_per_batch:
106
- count += 1
107
- batch_size = 0
108
- max_len = 0
109
- batch_size += 1
110
- max_len = max(max_len, length)
111
-
112
- if batch_size and not self.drop_last:
113
- count += 1
114
- return count
 
1
+ """Batch collation and dynamic batching for translation training."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ from typing import Iterator, Sequence
7
+
8
+ import torch
9
+ from torch.nn.utils.rnn import pad_sequence
10
+ from torch.utils.data import Sampler
11
+
12
+
13
+ class TranslationCollator:
14
+ """Pad variable-length translation examples into one batch."""
15
+
16
+ def __init__(self, pad_token_id: int = 0, label_pad_token_id: int = -100):
17
+ self.pad_token_id = pad_token_id
18
+ self.label_pad_token_id = label_pad_token_id
19
+
20
+ def __call__(self, batch: Sequence[dict[str, torch.Tensor]]) -> dict[str, torch.Tensor]:
21
+ src_ids = pad_sequence(
22
+ [item["src_ids"] for item in batch],
23
+ batch_first=True,
24
+ padding_value=self.pad_token_id,
25
+ )
26
+ tgt_input_ids = pad_sequence(
27
+ [item["tgt_input_ids"] for item in batch],
28
+ batch_first=True,
29
+ padding_value=self.pad_token_id,
30
+ )
31
+ labels = pad_sequence(
32
+ [item["labels"] for item in batch],
33
+ batch_first=True,
34
+ padding_value=self.label_pad_token_id,
35
+ )
36
+
37
+ src_padding_mask = src_ids.eq(self.pad_token_id)
38
+ tgt_padding_mask = tgt_input_ids.eq(self.pad_token_id)
39
+
40
+ return {
41
+ "src_ids": src_ids,
42
+ "tgt_input_ids": tgt_input_ids,
43
+ "labels": labels,
44
+ "src_padding_mask": src_padding_mask,
45
+ "tgt_padding_mask": tgt_padding_mask,
46
+ "src_attention_mask": (~src_padding_mask).long(),
47
+ "tgt_attention_mask": (~tgt_padding_mask).long(),
48
+ "src_lens": torch.stack([item["src_len"] for item in batch]),
49
+ "tgt_lens": torch.stack([item["tgt_len"] for item in batch]),
50
+ }
51
+
52
+
53
+ class DynamicBatchSampler(Sampler[list[int]]):
54
+ """Create batches constrained by an approximate max token budget."""
55
+
56
+ def __init__(
57
+ self,
58
+ lengths: Sequence[int | tuple[int, int]],
59
+ max_tokens_per_batch: int = 8192,
60
+ shuffle: bool = True,
61
+ drop_last: bool = False,
62
+ ):
63
+ self.lengths = [max(length) if isinstance(length, tuple) else int(length) for length in lengths]
64
+ self.max_tokens_per_batch = max_tokens_per_batch
65
+ self.shuffle = shuffle
66
+ self.drop_last = drop_last
67
+
68
+ def __iter__(self) -> Iterator[list[int]]:
69
+ indices = list(range(len(self.lengths)))
70
+ if self.shuffle:
71
+ random.shuffle(indices)
72
+
73
+ indices.sort(key=lambda idx: self.lengths[idx])
74
+ batches: list[list[int]] = []
75
+ batch: list[int] = []
76
+ max_len = 0
77
+
78
+ for idx in indices:
79
+ candidate_max_len = max(max_len, self.lengths[idx])
80
+ candidate_tokens = candidate_max_len * (len(batch) + 1)
81
+
82
+ if batch and candidate_tokens > self.max_tokens_per_batch:
83
+ batches.append(batch)
84
+ batch = []
85
+ max_len = 0
86
+
87
+ batch.append(idx)
88
+ max_len = max(max_len, self.lengths[idx])
89
+
90
+ if batch and not self.drop_last:
91
+ batches.append(batch)
92
+
93
+ if self.shuffle:
94
+ random.shuffle(batches)
95
+
96
+ yield from batches
97
+
98
+ def __len__(self) -> int:
99
+ count = 0
100
+ batch_size = 0
101
+ max_len = 0
102
+
103
+ for length in sorted(self.lengths):
104
+ candidate_max_len = max(max_len, length)
105
+ if batch_size and candidate_max_len * (batch_size + 1) > self.max_tokens_per_batch:
106
+ count += 1
107
+ batch_size = 0
108
+ max_len = 0
109
+ batch_size += 1
110
+ max_len = max(max_len, length)
111
+
112
+ if batch_size and not self.drop_last:
113
+ count += 1
114
+ return count
src/easytranslate/data/dataset.py CHANGED
@@ -1,157 +1,157 @@
1
- """Dataset loading and PyTorch dataset classes for English-Chinese translation."""
2
-
3
- from __future__ import annotations
4
-
5
- from pathlib import Path
6
- from typing import Mapping, Sequence
7
-
8
- import torch
9
- from torch.utils.data import Dataset
10
-
11
- from easytranslate.data.preprocessing import preprocess_pipeline
12
- from easytranslate.data.tokenizer import TokenizerWrapper
13
-
14
-
15
- class TranslationDataset(Dataset):
16
- """PyTorch dataset that builds seq2seq inputs for teacher forcing."""
17
-
18
- def __init__(
19
- self,
20
- src_texts: Sequence[str],
21
- tgt_texts: Sequence[str],
22
- tokenizer: TokenizerWrapper | None = None,
23
- src_tokenizer: TokenizerWrapper | None = None,
24
- tgt_tokenizer: TokenizerWrapper | None = None,
25
- max_src_len: int = 256,
26
- max_tgt_len: int = 256,
27
- ):
28
- if len(src_texts) != len(tgt_texts):
29
- raise ValueError("src_texts and tgt_texts must have the same length")
30
-
31
- if tokenizer is not None:
32
- src_tokenizer = src_tokenizer or tokenizer
33
- tgt_tokenizer = tgt_tokenizer or tokenizer
34
- if src_tokenizer is None or tgt_tokenizer is None:
35
- raise ValueError("Provide tokenizer or both src_tokenizer and tgt_tokenizer")
36
-
37
- self.src_texts = list(src_texts)
38
- self.tgt_texts = list(tgt_texts)
39
- self.src_tokenizer = src_tokenizer
40
- self.tgt_tokenizer = tgt_tokenizer
41
- self.max_src_len = max_src_len
42
- self.max_tgt_len = max_tgt_len
43
-
44
- def __len__(self) -> int:
45
- return len(self.src_texts)
46
-
47
- def __getitem__(self, index: int) -> dict[str, torch.Tensor]:
48
- src_ids = self.src_tokenizer.encode(
49
- self.src_texts[index],
50
- add_special_tokens=True,
51
- max_length=self.max_src_len,
52
- )
53
-
54
- target_core = self.tgt_tokenizer.encode(
55
- self.tgt_texts[index],
56
- add_special_tokens=False,
57
- max_length=max(1, self.max_tgt_len - 1),
58
- )
59
- tgt_input_ids = [self.tgt_tokenizer.bos_token_id] + target_core
60
- labels = target_core + [self.tgt_tokenizer.eos_token_id]
61
-
62
- return {
63
- "src_ids": torch.tensor(src_ids, dtype=torch.long),
64
- "tgt_input_ids": torch.tensor(tgt_input_ids, dtype=torch.long),
65
- "labels": torch.tensor(labels, dtype=torch.long),
66
- "src_len": torch.tensor(len(src_ids), dtype=torch.long),
67
- "tgt_len": torch.tensor(len(labels), dtype=torch.long),
68
- }
69
-
70
-
71
- def _translation_to_columns(dataset, src_lang: str, tgt_lang: str):
72
- def convert(example):
73
- translation = example["translation"]
74
- return {"src": translation[src_lang], "tgt": translation[tgt_lang]}
75
-
76
- return dataset.map(convert, remove_columns=dataset.column_names)
77
-
78
-
79
- def load_wmt_dataset(
80
- year: str = "19",
81
- language_pair: str = "zh-en",
82
- src_lang: str = "en",
83
- tgt_lang: str = "zh",
84
- split: str | None = None,
85
- cache_dir: str | None = None,
86
- ):
87
- """Load WMT zh-en and normalize rows to {'src', 'tgt'}."""
88
- from datasets import load_dataset
89
-
90
- dataset_name = f"wmt/wmt{year}"
91
- try:
92
- dataset = load_dataset(dataset_name, language_pair, split=split, cache_dir=cache_dir)
93
- except Exception:
94
- dataset = load_dataset(f"wmt{year}", language_pair, split=split, cache_dir=cache_dir)
95
-
96
- if split is not None:
97
- return _translation_to_columns(dataset, src_lang, tgt_lang)
98
-
99
- return dataset.map(
100
- lambda example: {"src": example["translation"][src_lang], "tgt": example["translation"][tgt_lang]},
101
- remove_columns=next(iter(dataset.values())).column_names,
102
- )
103
-
104
-
105
- def load_opus_dataset(
106
- subset: str = "en-zh",
107
- src_lang: str = "en",
108
- tgt_lang: str = "zh",
109
- split: str | None = None,
110
- cache_dir: str | None = None,
111
- ):
112
- """Load OPUS-100 and normalize rows to {'src', 'tgt'}."""
113
- from datasets import load_dataset
114
-
115
- dataset = load_dataset("Helsinki-NLP/opus-100", subset, split=split, cache_dir=cache_dir)
116
- if split is not None:
117
- return _translation_to_columns(dataset, src_lang, tgt_lang)
118
-
119
- return dataset.map(
120
- lambda example: {"src": example["translation"][src_lang], "tgt": example["translation"][tgt_lang]},
121
- remove_columns=next(iter(dataset.values())).column_names,
122
- )
123
-
124
-
125
- def _read_lines(path: str | Path) -> list[str]:
126
- with Path(path).open("r", encoding="utf-8") as f:
127
- return [line.rstrip("\n") for line in f]
128
-
129
-
130
- def load_custom_dataset(
131
- train_src: str | Path,
132
- train_tgt: str | Path,
133
- val_src: str | Path | None = None,
134
- val_tgt: str | Path | None = None,
135
- test_src: str | Path | None = None,
136
- test_tgt: str | Path | None = None,
137
- preprocessing_config: Mapping | None = None,
138
- ) -> dict[str, dict[str, list[str]]]:
139
- """Load parallel text files and return split dictionaries."""
140
- preprocessing_config = dict(preprocessing_config or {})
141
-
142
- def load_split(src_path: str | Path, tgt_path: str | Path) -> dict[str, list[str]]:
143
- src_texts, tgt_texts = preprocess_pipeline(
144
- _read_lines(src_path),
145
- _read_lines(tgt_path),
146
- **preprocessing_config,
147
- )
148
- return {"src": src_texts, "tgt": tgt_texts}
149
-
150
- result = {"train": load_split(train_src, train_tgt)}
151
-
152
- if val_src and val_tgt:
153
- result["validation"] = load_split(val_src, val_tgt)
154
- if test_src and test_tgt:
155
- result["test"] = load_split(test_src, test_tgt)
156
-
157
- return result
 
1
+ """Dataset loading and PyTorch dataset classes for English-Chinese translation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Mapping, Sequence
7
+
8
+ import torch
9
+ from torch.utils.data import Dataset
10
+
11
+ from easytranslate.data.preprocessing import preprocess_pipeline
12
+ from easytranslate.data.tokenizer import TokenizerWrapper
13
+
14
+
15
+ class TranslationDataset(Dataset):
16
+ """PyTorch dataset that builds seq2seq inputs for teacher forcing."""
17
+
18
+ def __init__(
19
+ self,
20
+ src_texts: Sequence[str],
21
+ tgt_texts: Sequence[str],
22
+ tokenizer: TokenizerWrapper | None = None,
23
+ src_tokenizer: TokenizerWrapper | None = None,
24
+ tgt_tokenizer: TokenizerWrapper | None = None,
25
+ max_src_len: int = 256,
26
+ max_tgt_len: int = 256,
27
+ ):
28
+ if len(src_texts) != len(tgt_texts):
29
+ raise ValueError("src_texts and tgt_texts must have the same length")
30
+
31
+ if tokenizer is not None:
32
+ src_tokenizer = src_tokenizer or tokenizer
33
+ tgt_tokenizer = tgt_tokenizer or tokenizer
34
+ if src_tokenizer is None or tgt_tokenizer is None:
35
+ raise ValueError("Provide tokenizer or both src_tokenizer and tgt_tokenizer")
36
+
37
+ self.src_texts = list(src_texts)
38
+ self.tgt_texts = list(tgt_texts)
39
+ self.src_tokenizer = src_tokenizer
40
+ self.tgt_tokenizer = tgt_tokenizer
41
+ self.max_src_len = max_src_len
42
+ self.max_tgt_len = max_tgt_len
43
+
44
+ def __len__(self) -> int:
45
+ return len(self.src_texts)
46
+
47
+ def __getitem__(self, index: int) -> dict[str, torch.Tensor]:
48
+ src_ids = self.src_tokenizer.encode(
49
+ self.src_texts[index],
50
+ add_special_tokens=True,
51
+ max_length=self.max_src_len,
52
+ )
53
+
54
+ target_core = self.tgt_tokenizer.encode(
55
+ self.tgt_texts[index],
56
+ add_special_tokens=False,
57
+ max_length=max(1, self.max_tgt_len - 1),
58
+ )
59
+ tgt_input_ids = [self.tgt_tokenizer.bos_token_id] + target_core
60
+ labels = target_core + [self.tgt_tokenizer.eos_token_id]
61
+
62
+ return {
63
+ "src_ids": torch.tensor(src_ids, dtype=torch.long),
64
+ "tgt_input_ids": torch.tensor(tgt_input_ids, dtype=torch.long),
65
+ "labels": torch.tensor(labels, dtype=torch.long),
66
+ "src_len": torch.tensor(len(src_ids), dtype=torch.long),
67
+ "tgt_len": torch.tensor(len(labels), dtype=torch.long),
68
+ }
69
+
70
+
71
+ def _translation_to_columns(dataset, src_lang: str, tgt_lang: str):
72
+ def convert(example):
73
+ translation = example["translation"]
74
+ return {"src": translation[src_lang], "tgt": translation[tgt_lang]}
75
+
76
+ return dataset.map(convert, remove_columns=dataset.column_names)
77
+
78
+
79
+ def load_wmt_dataset(
80
+ year: str = "19",
81
+ language_pair: str = "zh-en",
82
+ src_lang: str = "en",
83
+ tgt_lang: str = "zh",
84
+ split: str | None = None,
85
+ cache_dir: str | None = None,
86
+ ):
87
+ """Load WMT zh-en and normalize rows to {'src', 'tgt'}."""
88
+ from datasets import load_dataset
89
+
90
+ dataset_name = f"wmt/wmt{year}"
91
+ try:
92
+ dataset = load_dataset(dataset_name, language_pair, split=split, cache_dir=cache_dir)
93
+ except Exception:
94
+ dataset = load_dataset(f"wmt{year}", language_pair, split=split, cache_dir=cache_dir)
95
+
96
+ if split is not None:
97
+ return _translation_to_columns(dataset, src_lang, tgt_lang)
98
+
99
+ return dataset.map(
100
+ lambda example: {"src": example["translation"][src_lang], "tgt": example["translation"][tgt_lang]},
101
+ remove_columns=next(iter(dataset.values())).column_names,
102
+ )
103
+
104
+
105
+ def load_opus_dataset(
106
+ subset: str = "en-zh",
107
+ src_lang: str = "en",
108
+ tgt_lang: str = "zh",
109
+ split: str | None = None,
110
+ cache_dir: str | None = None,
111
+ ):
112
+ """Load OPUS-100 and normalize rows to {'src', 'tgt'}."""
113
+ from datasets import load_dataset
114
+
115
+ dataset = load_dataset("Helsinki-NLP/opus-100", subset, split=split, cache_dir=cache_dir)
116
+ if split is not None:
117
+ return _translation_to_columns(dataset, src_lang, tgt_lang)
118
+
119
+ return dataset.map(
120
+ lambda example: {"src": example["translation"][src_lang], "tgt": example["translation"][tgt_lang]},
121
+ remove_columns=next(iter(dataset.values())).column_names,
122
+ )
123
+
124
+
125
+ def _read_lines(path: str | Path) -> list[str]:
126
+ with Path(path).open("r", encoding="utf-8") as f:
127
+ return [line.rstrip("\n") for line in f]
128
+
129
+
130
+ def load_custom_dataset(
131
+ train_src: str | Path,
132
+ train_tgt: str | Path,
133
+ val_src: str | Path | None = None,
134
+ val_tgt: str | Path | None = None,
135
+ test_src: str | Path | None = None,
136
+ test_tgt: str | Path | None = None,
137
+ preprocessing_config: Mapping | None = None,
138
+ ) -> dict[str, dict[str, list[str]]]:
139
+ """Load parallel text files and return split dictionaries."""
140
+ preprocessing_config = dict(preprocessing_config or {})
141
+
142
+ def load_split(src_path: str | Path, tgt_path: str | Path) -> dict[str, list[str]]:
143
+ src_texts, tgt_texts = preprocess_pipeline(
144
+ _read_lines(src_path),
145
+ _read_lines(tgt_path),
146
+ **preprocessing_config,
147
+ )
148
+ return {"src": src_texts, "tgt": tgt_texts}
149
+
150
+ result = {"train": load_split(train_src, train_tgt)}
151
+
152
+ if val_src and val_tgt:
153
+ result["validation"] = load_split(val_src, val_tgt)
154
+ if test_src and test_tgt:
155
+ result["test"] = load_split(test_src, test_tgt)
156
+
157
+ return result
src/easytranslate/data/preprocessing.py CHANGED
@@ -1,125 +1,125 @@
1
- """Text cleaning and filtering utilities for translation corpora."""
2
-
3
- from __future__ import annotations
4
-
5
- import re
6
- import unicodedata
7
- from typing import Callable, Iterable, Sequence
8
-
9
- CONTROL_OR_ZERO_WIDTH_RE = re.compile(r"[\u0000-\u001f\u007f-\u009f\u200b\u200c\u200d\ufeff]")
10
- SPACE_RE = re.compile(r"\s+")
11
- PUNCT_RE = re.compile(r"[^\w\s\u4e00-\u9fff]", flags=re.UNICODE)
12
-
13
-
14
- def clean_text(text: str, lowercase: bool = False, remove_punctuation: bool = False) -> str:
15
- """Normalize a single sentence without changing its meaning aggressively."""
16
- if text is None:
17
- return ""
18
-
19
- text = unicodedata.normalize("NFKC", str(text))
20
- text = CONTROL_OR_ZERO_WIDTH_RE.sub("", text)
21
- text = SPACE_RE.sub(" ", text).strip()
22
-
23
- if lowercase:
24
- text = text.lower()
25
- if remove_punctuation:
26
- text = PUNCT_RE.sub("", text)
27
- text = SPACE_RE.sub(" ", text).strip()
28
-
29
- return text
30
-
31
-
32
- def _default_length(text: str) -> int:
33
- """Use whitespace tokens for Latin text and character count for CJK-heavy text."""
34
- cjk_chars = sum(1 for ch in text if "\u4e00" <= ch <= "\u9fff")
35
- if cjk_chars >= max(1, len(text) // 3):
36
- return len(text.replace(" ", ""))
37
- return len(text.split())
38
-
39
-
40
- def filter_by_length(
41
- src: str,
42
- tgt: str,
43
- min_src_len: int = 1,
44
- min_tgt_len: int = 1,
45
- max_src_len: int = 256,
46
- max_tgt_len: int = 256,
47
- length_ratio_threshold: float = 3.0,
48
- length_fn: Callable[[str], int] | None = None,
49
- ) -> bool:
50
- """Return True when a sentence pair passes basic length and ratio checks."""
51
- length_fn = length_fn or _default_length
52
- src_len = length_fn(src)
53
- tgt_len = length_fn(tgt)
54
-
55
- if src_len < min_src_len or tgt_len < min_tgt_len:
56
- return False
57
- if src_len > max_src_len or tgt_len > max_tgt_len:
58
- return False
59
-
60
- shorter = max(1, min(src_len, tgt_len))
61
- longer = max(src_len, tgt_len)
62
- return longer / shorter <= length_ratio_threshold
63
-
64
-
65
- def deduplicate_pairs(pairs: Iterable[tuple[str, str]]) -> list[tuple[str, str]]:
66
- """Deduplicate by exact cleaned source-target pair while preserving order."""
67
- seen: set[tuple[str, str]] = set()
68
- result: list[tuple[str, str]] = []
69
-
70
- for src, tgt in pairs:
71
- key = (src, tgt)
72
- if key in seen:
73
- continue
74
- seen.add(key)
75
- result.append(key)
76
-
77
- return result
78
-
79
-
80
- def preprocess_pipeline(
81
- src_texts: Sequence[str],
82
- tgt_texts: Sequence[str],
83
- lowercase_src: bool = False,
84
- lowercase_tgt: bool = False,
85
- remove_punctuation: bool = False,
86
- max_src_len: int = 256,
87
- max_tgt_len: int = 256,
88
- min_src_len: int = 1,
89
- min_tgt_len: int = 1,
90
- filter_by_length_enabled: bool = True,
91
- length_ratio_threshold: float = 3.0,
92
- deduplicate: bool = True,
93
- ) -> tuple[list[str], list[str]]:
94
- """Clean, filter, and optionally deduplicate parallel source-target texts."""
95
- if len(src_texts) != len(tgt_texts):
96
- raise ValueError("src_texts and tgt_texts must have the same length")
97
-
98
- pairs: list[tuple[str, str]] = []
99
- for raw_src, raw_tgt in zip(src_texts, tgt_texts):
100
- src = clean_text(raw_src, lowercase=lowercase_src, remove_punctuation=remove_punctuation)
101
- tgt = clean_text(raw_tgt, lowercase=lowercase_tgt, remove_punctuation=remove_punctuation)
102
-
103
- if not src or not tgt:
104
- continue
105
- if filter_by_length_enabled and not filter_by_length(
106
- src,
107
- tgt,
108
- min_src_len=min_src_len,
109
- min_tgt_len=min_tgt_len,
110
- max_src_len=max_src_len,
111
- max_tgt_len=max_tgt_len,
112
- length_ratio_threshold=length_ratio_threshold,
113
- ):
114
- continue
115
-
116
- pairs.append((src, tgt))
117
-
118
- if deduplicate:
119
- pairs = deduplicate_pairs(pairs)
120
-
121
- if not pairs:
122
- return [], []
123
-
124
- src_clean, tgt_clean = zip(*pairs)
125
- return list(src_clean), list(tgt_clean)
 
1
+ """Text cleaning and filtering utilities for translation corpora."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import unicodedata
7
+ from typing import Callable, Iterable, Sequence
8
+
9
+ CONTROL_OR_ZERO_WIDTH_RE = re.compile(r"[\u0000-\u001f\u007f-\u009f\u200b\u200c\u200d\ufeff]")
10
+ SPACE_RE = re.compile(r"\s+")
11
+ PUNCT_RE = re.compile(r"[^\w\s\u4e00-\u9fff]", flags=re.UNICODE)
12
+
13
+
14
+ def clean_text(text: str, lowercase: bool = False, remove_punctuation: bool = False) -> str:
15
+ """Normalize a single sentence without changing its meaning aggressively."""
16
+ if text is None:
17
+ return ""
18
+
19
+ text = unicodedata.normalize("NFKC", str(text))
20
+ text = CONTROL_OR_ZERO_WIDTH_RE.sub("", text)
21
+ text = SPACE_RE.sub(" ", text).strip()
22
+
23
+ if lowercase:
24
+ text = text.lower()
25
+ if remove_punctuation:
26
+ text = PUNCT_RE.sub("", text)
27
+ text = SPACE_RE.sub(" ", text).strip()
28
+
29
+ return text
30
+
31
+
32
+ def _default_length(text: str) -> int:
33
+ """Use whitespace tokens for Latin text and character count for CJK-heavy text."""
34
+ cjk_chars = sum(1 for ch in text if "\u4e00" <= ch <= "\u9fff")
35
+ if cjk_chars >= max(1, len(text) // 3):
36
+ return len(text.replace(" ", ""))
37
+ return len(text.split())
38
+
39
+
40
+ def filter_by_length(
41
+ src: str,
42
+ tgt: str,
43
+ min_src_len: int = 1,
44
+ min_tgt_len: int = 1,
45
+ max_src_len: int = 256,
46
+ max_tgt_len: int = 256,
47
+ length_ratio_threshold: float = 3.0,
48
+ length_fn: Callable[[str], int] | None = None,
49
+ ) -> bool:
50
+ """Return True when a sentence pair passes basic length and ratio checks."""
51
+ length_fn = length_fn or _default_length
52
+ src_len = length_fn(src)
53
+ tgt_len = length_fn(tgt)
54
+
55
+ if src_len < min_src_len or tgt_len < min_tgt_len:
56
+ return False
57
+ if src_len > max_src_len or tgt_len > max_tgt_len:
58
+ return False
59
+
60
+ shorter = max(1, min(src_len, tgt_len))
61
+ longer = max(src_len, tgt_len)
62
+ return longer / shorter <= length_ratio_threshold
63
+
64
+
65
+ def deduplicate_pairs(pairs: Iterable[tuple[str, str]]) -> list[tuple[str, str]]:
66
+ """Deduplicate by exact cleaned source-target pair while preserving order."""
67
+ seen: set[tuple[str, str]] = set()
68
+ result: list[tuple[str, str]] = []
69
+
70
+ for src, tgt in pairs:
71
+ key = (src, tgt)
72
+ if key in seen:
73
+ continue
74
+ seen.add(key)
75
+ result.append(key)
76
+
77
+ return result
78
+
79
+
80
+ def preprocess_pipeline(
81
+ src_texts: Sequence[str],
82
+ tgt_texts: Sequence[str],
83
+ lowercase_src: bool = False,
84
+ lowercase_tgt: bool = False,
85
+ remove_punctuation: bool = False,
86
+ max_src_len: int = 256,
87
+ max_tgt_len: int = 256,
88
+ min_src_len: int = 1,
89
+ min_tgt_len: int = 1,
90
+ filter_by_length_enabled: bool = True,
91
+ length_ratio_threshold: float = 3.0,
92
+ deduplicate: bool = True,
93
+ ) -> tuple[list[str], list[str]]:
94
+ """Clean, filter, and optionally deduplicate parallel source-target texts."""
95
+ if len(src_texts) != len(tgt_texts):
96
+ raise ValueError("src_texts and tgt_texts must have the same length")
97
+
98
+ pairs: list[tuple[str, str]] = []
99
+ for raw_src, raw_tgt in zip(src_texts, tgt_texts):
100
+ src = clean_text(raw_src, lowercase=lowercase_src, remove_punctuation=remove_punctuation)
101
+ tgt = clean_text(raw_tgt, lowercase=lowercase_tgt, remove_punctuation=remove_punctuation)
102
+
103
+ if not src or not tgt:
104
+ continue
105
+ if filter_by_length_enabled and not filter_by_length(
106
+ src,
107
+ tgt,
108
+ min_src_len=min_src_len,
109
+ min_tgt_len=min_tgt_len,
110
+ max_src_len=max_src_len,
111
+ max_tgt_len=max_tgt_len,
112
+ length_ratio_threshold=length_ratio_threshold,
113
+ ):
114
+ continue
115
+
116
+ pairs.append((src, tgt))
117
+
118
+ if deduplicate:
119
+ pairs = deduplicate_pairs(pairs)
120
+
121
+ if not pairs:
122
+ return [], []
123
+
124
+ src_clean, tgt_clean = zip(*pairs)
125
+ return list(src_clean), list(tgt_clean)
src/easytranslate/data/tokenizer.py CHANGED
@@ -1,197 +1,197 @@
1
- """Tokenizer wrappers and BPE training helpers."""
2
-
3
- from __future__ import annotations
4
-
5
- from pathlib import Path
6
- from typing import Iterable, Mapping, Sequence
7
-
8
-
9
- class TokenizerWrapper:
10
- """Small adapter that gives HF tokenizers and tokenizers.Tokenizer one API."""
11
-
12
- def __init__(
13
- self,
14
- tokenizer,
15
- pad_token: str = "<pad>",
16
- unk_token: str = "<unk>",
17
- bos_token: str = "<s>",
18
- eos_token: str = "</s>",
19
- ):
20
- self.tokenizer = tokenizer
21
- self.pad_token = pad_token
22
- self.unk_token = unk_token
23
- self.bos_token = bos_token
24
- self.eos_token = eos_token
25
-
26
- @property
27
- def pad_token_id(self) -> int:
28
- return self.token_to_id(self.pad_token)
29
-
30
- @property
31
- def unk_token_id(self) -> int:
32
- return self.token_to_id(self.unk_token)
33
-
34
- @property
35
- def bos_token_id(self) -> int:
36
- return self.token_to_id(self.bos_token)
37
-
38
- @property
39
- def eos_token_id(self) -> int:
40
- return self.token_to_id(self.eos_token)
41
-
42
- @property
43
- def vocab_size(self) -> int:
44
- if hasattr(self.tokenizer, "get_vocab_size"):
45
- return int(self.tokenizer.get_vocab_size())
46
- return int(len(self.tokenizer))
47
-
48
- def token_to_id(self, token: str) -> int:
49
- if hasattr(self.tokenizer, "token_to_id"):
50
- idx = self.tokenizer.token_to_id(token)
51
- elif hasattr(self.tokenizer, "convert_tokens_to_ids"):
52
- idx = self.tokenizer.convert_tokens_to_ids(token)
53
- else:
54
- raise TypeError("Unsupported tokenizer type")
55
-
56
- if idx is None:
57
- raise ValueError(f"Token {token!r} is not in the tokenizer vocabulary")
58
- return int(idx)
59
-
60
- def encode(self, text: str, add_special_tokens: bool = False, max_length: int | None = None) -> list[int]:
61
- if hasattr(self.tokenizer, "encode") and self.tokenizer.__class__.__module__.startswith("tokenizers"):
62
- ids = self.tokenizer.encode(text).ids
63
- else:
64
- ids = self.tokenizer.encode(text, add_special_tokens=add_special_tokens)
65
- add_special_tokens = False
66
-
67
- if add_special_tokens:
68
- ids = [self.bos_token_id] + list(ids) + [self.eos_token_id]
69
-
70
- if max_length is not None:
71
- ids = list(ids)[:max_length]
72
-
73
- return list(ids)
74
-
75
- def decode(self, ids: Sequence[int], skip_special_tokens: bool = True) -> str:
76
- if hasattr(self.tokenizer, "decode"):
77
- try:
78
- return self.tokenizer.decode(list(ids), skip_special_tokens=skip_special_tokens)
79
- except TypeError:
80
- return self.tokenizer.decode(list(ids))
81
- raise TypeError("Unsupported tokenizer type")
82
-
83
- def save(self, path: str | Path) -> None:
84
- path = Path(path)
85
- path.parent.mkdir(parents=True, exist_ok=True)
86
-
87
- if hasattr(self.tokenizer, "save"):
88
- self.tokenizer.save(str(path))
89
- return
90
- if hasattr(self.tokenizer, "save_pretrained"):
91
- self.tokenizer.save_pretrained(str(path))
92
- return
93
- raise TypeError("Unsupported tokenizer type")
94
-
95
-
96
- def _special_tokens(config: Mapping | None = None) -> dict[str, str]:
97
- tokens = {
98
- "pad": "<pad>",
99
- "unk": "<unk>",
100
- "bos": "<s>",
101
- "eos": "</s>",
102
- }
103
- if config:
104
- tokens.update(dict(config))
105
- return tokens
106
-
107
-
108
- def train_bpe_tokenizer(
109
- texts: Iterable[str],
110
- vocab_size: int = 32000,
111
- min_frequency: int = 2,
112
- special_tokens: Mapping[str, str] | None = None,
113
- save_path: str | Path | None = None,
114
- ) -> TokenizerWrapper:
115
- """Train a byte-level BPE tokenizer on source and target training text."""
116
- from tokenizers import Tokenizer
117
- from tokenizers.decoders import ByteLevel as ByteLevelDecoder
118
- from tokenizers.models import BPE
119
- from tokenizers.normalizers import NFKC, Sequence as NormalizerSequence
120
- from tokenizers.pre_tokenizers import ByteLevel
121
- from tokenizers.trainers import BpeTrainer
122
-
123
- tokens = _special_tokens(special_tokens)
124
- ordered_specials = [tokens["pad"], tokens["unk"], tokens["bos"], tokens["eos"]]
125
-
126
- tokenizer = Tokenizer(BPE(unk_token=tokens["unk"]))
127
- tokenizer.normalizer = NormalizerSequence([NFKC()])
128
- tokenizer.pre_tokenizer = ByteLevel(add_prefix_space=False)
129
- tokenizer.decoder = ByteLevelDecoder()
130
-
131
- trainer = BpeTrainer(
132
- vocab_size=vocab_size,
133
- min_frequency=min_frequency,
134
- special_tokens=ordered_specials,
135
- show_progress=True,
136
- )
137
- tokenizer.train_from_iterator((text for text in texts if text), trainer=trainer)
138
-
139
- wrapper = TokenizerWrapper(
140
- tokenizer,
141
- pad_token=tokens["pad"],
142
- unk_token=tokens["unk"],
143
- bos_token=tokens["bos"],
144
- eos_token=tokens["eos"],
145
- )
146
-
147
- if save_path is not None:
148
- wrapper.save(save_path)
149
-
150
- return wrapper
151
-
152
-
153
- def build_tokenizer(config: Mapping, train_texts: Iterable[str] | None = None) -> TokenizerWrapper:
154
- """Build a tokenizer from project config."""
155
- tokenizer_type = config.get("type", "bpe")
156
- tokens = _special_tokens(config.get("special_tokens"))
157
-
158
- if tokenizer_type == "pretrained":
159
- from transformers import AutoTokenizer
160
-
161
- model_name = config.get("model_name") or config.get("pretrained_model_name")
162
- if not model_name:
163
- raise ValueError("pretrained tokenizer requires config['model_name']")
164
- tokenizer = AutoTokenizer.from_pretrained(model_name)
165
- return TokenizerWrapper(
166
- tokenizer,
167
- pad_token=tokenizer.pad_token or tokens["pad"],
168
- unk_token=tokenizer.unk_token or tokens["unk"],
169
- bos_token=tokenizer.bos_token or tokens["bos"],
170
- eos_token=tokenizer.eos_token or tokens["eos"],
171
- )
172
-
173
- if tokenizer_type in {"bpe", "sentencepiece"}:
174
- tokenizer_path = config.get("path") or config.get("tokenizer_path")
175
- if tokenizer_path and Path(tokenizer_path).exists():
176
- from tokenizers import Tokenizer
177
-
178
- return TokenizerWrapper(
179
- Tokenizer.from_file(str(tokenizer_path)),
180
- pad_token=tokens["pad"],
181
- unk_token=tokens["unk"],
182
- bos_token=tokens["bos"],
183
- eos_token=tokens["eos"],
184
- )
185
-
186
- if train_texts is None:
187
- raise ValueError("BPE tokenizer requires train_texts when no tokenizer path is provided")
188
-
189
- return train_bpe_tokenizer(
190
- train_texts,
191
- vocab_size=int(config.get("vocab_size", 32000)),
192
- min_frequency=int(config.get("min_frequency", 2)),
193
- special_tokens=tokens,
194
- save_path=tokenizer_path,
195
- )
196
-
197
- raise ValueError(f"Unsupported tokenizer type: {tokenizer_type}")
 
1
+ """Tokenizer wrappers and BPE training helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Iterable, Mapping, Sequence
7
+
8
+
9
+ class TokenizerWrapper:
10
+ """Small adapter that gives HF tokenizers and tokenizers.Tokenizer one API."""
11
+
12
+ def __init__(
13
+ self,
14
+ tokenizer,
15
+ pad_token: str = "<pad>",
16
+ unk_token: str = "<unk>",
17
+ bos_token: str = "<s>",
18
+ eos_token: str = "</s>",
19
+ ):
20
+ self.tokenizer = tokenizer
21
+ self.pad_token = pad_token
22
+ self.unk_token = unk_token
23
+ self.bos_token = bos_token
24
+ self.eos_token = eos_token
25
+
26
+ @property
27
+ def pad_token_id(self) -> int:
28
+ return self.token_to_id(self.pad_token)
29
+
30
+ @property
31
+ def unk_token_id(self) -> int:
32
+ return self.token_to_id(self.unk_token)
33
+
34
+ @property
35
+ def bos_token_id(self) -> int:
36
+ return self.token_to_id(self.bos_token)
37
+
38
+ @property
39
+ def eos_token_id(self) -> int:
40
+ return self.token_to_id(self.eos_token)
41
+
42
+ @property
43
+ def vocab_size(self) -> int:
44
+ if hasattr(self.tokenizer, "get_vocab_size"):
45
+ return int(self.tokenizer.get_vocab_size())
46
+ return int(len(self.tokenizer))
47
+
48
+ def token_to_id(self, token: str) -> int:
49
+ if hasattr(self.tokenizer, "token_to_id"):
50
+ idx = self.tokenizer.token_to_id(token)
51
+ elif hasattr(self.tokenizer, "convert_tokens_to_ids"):
52
+ idx = self.tokenizer.convert_tokens_to_ids(token)
53
+ else:
54
+ raise TypeError("Unsupported tokenizer type")
55
+
56
+ if idx is None:
57
+ raise ValueError(f"Token {token!r} is not in the tokenizer vocabulary")
58
+ return int(idx)
59
+
60
+ def encode(self, text: str, add_special_tokens: bool = False, max_length: int | None = None) -> list[int]:
61
+ if hasattr(self.tokenizer, "encode") and self.tokenizer.__class__.__module__.startswith("tokenizers"):
62
+ ids = self.tokenizer.encode(text).ids
63
+ else:
64
+ ids = self.tokenizer.encode(text, add_special_tokens=add_special_tokens)
65
+ add_special_tokens = False
66
+
67
+ if add_special_tokens:
68
+ ids = [self.bos_token_id] + list(ids) + [self.eos_token_id]
69
+
70
+ if max_length is not None:
71
+ ids = list(ids)[:max_length]
72
+
73
+ return list(ids)
74
+
75
+ def decode(self, ids: Sequence[int], skip_special_tokens: bool = True) -> str:
76
+ if hasattr(self.tokenizer, "decode"):
77
+ try:
78
+ return self.tokenizer.decode(list(ids), skip_special_tokens=skip_special_tokens)
79
+ except TypeError:
80
+ return self.tokenizer.decode(list(ids))
81
+ raise TypeError("Unsupported tokenizer type")
82
+
83
+ def save(self, path: str | Path) -> None:
84
+ path = Path(path)
85
+ path.parent.mkdir(parents=True, exist_ok=True)
86
+
87
+ if hasattr(self.tokenizer, "save"):
88
+ self.tokenizer.save(str(path))
89
+ return
90
+ if hasattr(self.tokenizer, "save_pretrained"):
91
+ self.tokenizer.save_pretrained(str(path))
92
+ return
93
+ raise TypeError("Unsupported tokenizer type")
94
+
95
+
96
+ def _special_tokens(config: Mapping | None = None) -> dict[str, str]:
97
+ tokens = {
98
+ "pad": "<pad>",
99
+ "unk": "<unk>",
100
+ "bos": "<s>",
101
+ "eos": "</s>",
102
+ }
103
+ if config:
104
+ tokens.update(dict(config))
105
+ return tokens
106
+
107
+
108
+ def train_bpe_tokenizer(
109
+ texts: Iterable[str],
110
+ vocab_size: int = 32000,
111
+ min_frequency: int = 2,
112
+ special_tokens: Mapping[str, str] | None = None,
113
+ save_path: str | Path | None = None,
114
+ ) -> TokenizerWrapper:
115
+ """Train a byte-level BPE tokenizer on source and target training text."""
116
+ from tokenizers import Tokenizer
117
+ from tokenizers.decoders import ByteLevel as ByteLevelDecoder
118
+ from tokenizers.models import BPE
119
+ from tokenizers.normalizers import NFKC, Sequence as NormalizerSequence
120
+ from tokenizers.pre_tokenizers import ByteLevel
121
+ from tokenizers.trainers import BpeTrainer
122
+
123
+ tokens = _special_tokens(special_tokens)
124
+ ordered_specials = [tokens["pad"], tokens["unk"], tokens["bos"], tokens["eos"]]
125
+
126
+ tokenizer = Tokenizer(BPE(unk_token=tokens["unk"]))
127
+ tokenizer.normalizer = NormalizerSequence([NFKC()])
128
+ tokenizer.pre_tokenizer = ByteLevel(add_prefix_space=False)
129
+ tokenizer.decoder = ByteLevelDecoder()
130
+
131
+ trainer = BpeTrainer(
132
+ vocab_size=vocab_size,
133
+ min_frequency=min_frequency,
134
+ special_tokens=ordered_specials,
135
+ show_progress=True,
136
+ )
137
+ tokenizer.train_from_iterator((text for text in texts if text), trainer=trainer)
138
+
139
+ wrapper = TokenizerWrapper(
140
+ tokenizer,
141
+ pad_token=tokens["pad"],
142
+ unk_token=tokens["unk"],
143
+ bos_token=tokens["bos"],
144
+ eos_token=tokens["eos"],
145
+ )
146
+
147
+ if save_path is not None:
148
+ wrapper.save(save_path)
149
+
150
+ return wrapper
151
+
152
+
153
+ def build_tokenizer(config: Mapping, train_texts: Iterable[str] | None = None) -> TokenizerWrapper:
154
+ """Build a tokenizer from project config."""
155
+ tokenizer_type = config.get("type", "bpe")
156
+ tokens = _special_tokens(config.get("special_tokens"))
157
+
158
+ if tokenizer_type == "pretrained":
159
+ from transformers import AutoTokenizer
160
+
161
+ model_name = config.get("model_name") or config.get("pretrained_model_name")
162
+ if not model_name:
163
+ raise ValueError("pretrained tokenizer requires config['model_name']")
164
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
165
+ return TokenizerWrapper(
166
+ tokenizer,
167
+ pad_token=tokenizer.pad_token or tokens["pad"],
168
+ unk_token=tokenizer.unk_token or tokens["unk"],
169
+ bos_token=tokenizer.bos_token or tokens["bos"],
170
+ eos_token=tokenizer.eos_token or tokens["eos"],
171
+ )
172
+
173
+ if tokenizer_type in {"bpe", "sentencepiece"}:
174
+ tokenizer_path = config.get("path") or config.get("tokenizer_path")
175
+ if tokenizer_path and Path(tokenizer_path).exists():
176
+ from tokenizers import Tokenizer
177
+
178
+ return TokenizerWrapper(
179
+ Tokenizer.from_file(str(tokenizer_path)),
180
+ pad_token=tokens["pad"],
181
+ unk_token=tokens["unk"],
182
+ bos_token=tokens["bos"],
183
+ eos_token=tokens["eos"],
184
+ )
185
+
186
+ if train_texts is None:
187
+ raise ValueError("BPE tokenizer requires train_texts when no tokenizer path is provided")
188
+
189
+ return train_bpe_tokenizer(
190
+ train_texts,
191
+ vocab_size=int(config.get("vocab_size", 32000)),
192
+ min_frequency=int(config.get("min_frequency", 2)),
193
+ special_tokens=tokens,
194
+ save_path=tokenizer_path,
195
+ )
196
+
197
+ raise ValueError(f"Unsupported tokenizer type: {tokenizer_type}")
src/easytranslate/evaluation/__init__.py CHANGED
@@ -1,16 +1,16 @@
1
- """评估与推理模块 (Person D 负责)"""
2
-
3
- from easytranslate.evaluation.metrics import compute_bleu, compute_comet, compute_chrf, compute_all_metrics
4
- from easytranslate.evaluation.decoding import greedy_decode, beam_search_decode, sample_decode
5
- from easytranslate.evaluation.evaluator import Evaluator
6
-
7
- __all__ = [
8
- "compute_bleu",
9
- "compute_comet",
10
- "compute_chrf",
11
- "compute_all_metrics",
12
- "greedy_decode",
13
- "beam_search_decode",
14
- "sample_decode",
15
- "Evaluator",
16
- ]
 
1
+ """评估与推理模块 (Person D 负责)"""
2
+
3
+ from easytranslate.evaluation.metrics import compute_bleu, compute_comet, compute_chrf, compute_all_metrics
4
+ from easytranslate.evaluation.decoding import greedy_decode, beam_search_decode, sample_decode
5
+ from easytranslate.evaluation.evaluator import Evaluator
6
+
7
+ __all__ = [
8
+ "compute_bleu",
9
+ "compute_comet",
10
+ "compute_chrf",
11
+ "compute_all_metrics",
12
+ "greedy_decode",
13
+ "beam_search_decode",
14
+ "sample_decode",
15
+ "Evaluator",
16
+ ]
src/easytranslate/evaluation/decoding.py CHANGED
@@ -1,129 +1,219 @@
1
- """
2
- 解码策略模块 — Person D 负责实现
3
-
4
- 功能要求:
5
- 1. greedy_decode: 贪心解码
6
- 2. beam_search_decode: 束搜索解码
7
- 3. sample_decode: 采样解码 (temperature, top-k, top-p)
8
-
9
- 技术要点:
10
- - Beam Search 是翻译任务最常用的解码策略
11
- - 需要高效处理批量解码
12
- - 支持长度惩罚 (length penalty) 和重复惩罚 (no_repeat_ngram)
13
- - 对于预训练模型,可以直接使用 model.generate()
14
- """
15
-
16
- from __future__ import annotations
17
-
18
- import logging
19
- from typing import Optional
20
-
21
- import torch
22
- import torch.nn as nn
23
- import torch.nn.functional as F
24
-
25
- logger = logging.getLogger(__name__)
26
-
27
-
28
- @torch.no_grad()
29
- def greedy_decode(
30
- model: nn.Module,
31
- src_ids: torch.Tensor, # [B, S]
32
- src_padding_mask: torch.BoolTensor, # [B, S]
33
- bos_id: int,
34
- eos_id: int,
35
- max_len: int = 256,
36
- ) -> torch.Tensor:
37
- """
38
- 贪心解码。
39
-
40
- TODO [Person D]: 实现以下逻辑:
41
- 1. encoder_output = model.encode(src_ids, src_padding_mask)
42
- 2. 初始化 decoder input: [B, 1] 全为 bos_id
43
- 3. for step in range(max_len):
44
- a. logits = model.decode_step(decoder_input, encoder_output, src_padding_mask)
45
- b. next_token = logits.argmax(dim=-1)
46
- c. decoder_input = concat(decoder_input, next_token)
47
- d. 如果所有序列都生成了 eos_id,则提前终止
48
- 4. 返回生成的 token ids [B, T]
49
- """
50
- raise NotImplementedError("TODO: Person D 实现 greedy_decode")
51
-
52
-
53
- @torch.no_grad()
54
- def beam_search_decode(
55
- model: nn.Module,
56
- src_ids: torch.Tensor, # [B, S]
57
- src_padding_mask: torch.BoolTensor,
58
- bos_id: int,
59
- eos_id: int,
60
- beam_size: int = 5,
61
- max_len: int = 256,
62
- length_penalty: float = 1.0,
63
- no_repeat_ngram_size: int = 0,
64
- ) -> torch.Tensor:
65
- """
66
- 束搜索解码。
67
-
68
- TODO [Person D]: 实现以下逻辑:
69
- 1. encoder_output = model.encode(src_ids, src_padding_mask)
70
- 2. 将 encoder_output 扩展为 beam_size 份: [B*beam, S, D]
71
- 3. 初始化 beam:
72
- - beam_scores: [B, beam_size] 初始为 0
73
- - beam_tokens: [B, beam_size, 1] 初始为 bos_id
74
- 4. for step in range(max_len):
75
- a. 对每个 beam 计算 logits
76
- b. log_probs = log_softmax(logits)
77
- c. (可选) 应用 no_repeat_ngram 约束
78
- d. scores = beam_scores + log_probs
79
- e. 选择 top-k candidates (k = beam_size)
80
- f. 更新 beam_tokens 和 beam_scores
81
- g. 将已完成的 beam 移到 finished pool
82
- 5. finished beams 应用 length_penalty:
83
- score = score / (length ^ length_penalty)
84
- 6. 选择得分最高的序列
85
- 7. 返回最佳翻译 [B, T]
86
-
87
- 这是翻译任务最关键的解码算法,请仔细实现。
88
- """
89
- raise NotImplementedError("TODO: Person D 实现 beam_search_decode")
90
-
91
-
92
- @torch.no_grad()
93
- def sample_decode(
94
- model: nn.Module,
95
- src_ids: torch.Tensor,
96
- src_padding_mask: torch.BoolTensor,
97
- bos_id: int,
98
- eos_id: int,
99
- max_len: int = 256,
100
- temperature: float = 1.0,
101
- top_k: int = 0,
102
- top_p: float = 1.0,
103
- ) -> torch.Tensor:
104
- """
105
- 采样解码 (支持 temperature, top-k, top-p/nucleus sampling)。
106
-
107
- TODO [Person D]: 实现以下逻辑:
108
- 1. 与贪心解码类似,但每步采样而非取 argmax
109
- 2. 应用 temperature: logits = logits / temperature
110
- 3. 应用 top-k: 只保留概率最高的 k 个 token
111
- 4. 应用 top-p (nucleus): 只保留累积概率达到 p 的 token
112
- 5. 从过滤后的分布中采样: torch.multinomial
113
- """
114
- raise NotImplementedError("TODO: Person D 实现 sample_decode")
115
-
116
-
117
- def _apply_no_repeat_ngram(
118
- logits: torch.Tensor,
119
- generated_tokens: torch.Tensor,
120
- ngram_size: int,
121
- ) -> torch.Tensor:
122
- """
123
- 防止生成重复的 n-gram。
124
-
125
- TODO [Person D]:
126
- 1. generated_tokens 中提取所有已出现的 (ngram_size-1)-gram
127
- 2. 对于每个可能导致重复 ngram 的 next token,将其 logits 设为 -inf
128
- """
129
- raise NotImplementedError("TODO: Person D 实现 _apply_no_repeat_ngram")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 解码策略模块 — Person D 负责实现
3
+
4
+ 功能要求:
5
+ 1. greedy_decode: 贪心解码
6
+ 2. beam_search_decode: 束搜索解码
7
+ 3. sample_decode: 采样解码 (temperature, top-k, top-p)
8
+
9
+ 技术要点:
10
+ - Beam Search 是翻译任务最常用的解码策略
11
+ - 需要高效处理批量解码
12
+ - 支持长度惩罚 (length penalty) 和重复惩罚 (no_repeat_ngram)
13
+ - 对于预训练模型,可以直接使用 model.generate()
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ from typing import Optional
20
+
21
+ import torch
22
+ import torch.nn as nn
23
+ import torch.nn.functional as F
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def _apply_no_repeat_ngram(
29
+ logits: torch.Tensor,
30
+ generated_tokens: torch.Tensor,
31
+ ngram_size: int,
32
+ ) -> torch.Tensor:
33
+ """
34
+ 防止生成重复的 n-gram。
35
+ """
36
+ if ngram_size <= 1:
37
+ return logits
38
+
39
+ batch_size, vocab_size = logits.size()
40
+ for batch_idx in range(batch_size):
41
+ tokens = generated_tokens[batch_idx].tolist()
42
+ if len(tokens) < ngram_size - 1:
43
+ continue
44
+
45
+ banned_tokens: set[int] = set()
46
+ ngram_map: dict[tuple[int, ...], set[int]] = {}
47
+ for i in range(len(tokens) - ngram_size + 1):
48
+ prefix = tuple(tokens[i : i + ngram_size - 1])
49
+ next_token = tokens[i + ngram_size - 1]
50
+ ngram_map.setdefault(prefix, set()).add(next_token)
51
+
52
+ prefix = tuple(tokens[-(ngram_size - 1) :])
53
+ if prefix in ngram_map:
54
+ banned_tokens = ngram_map[prefix]
55
+ logits[batch_idx, list(banned_tokens)] = float("-inf")
56
+
57
+ return logits
58
+
59
+
60
+ @torch.no_grad()
61
+ def greedy_decode(
62
+ model: nn.Module,
63
+ src_ids: torch.Tensor,
64
+ src_padding_mask: torch.BoolTensor,
65
+ bos_id: int,
66
+ eos_id: int,
67
+ max_len: int = 256,
68
+ ) -> torch.Tensor:
69
+ """
70
+ 贪心解码。
71
+ """
72
+ encoder_output = model.encode(src_ids, src_padding_mask)
73
+ batch_size = src_ids.size(0)
74
+ device = src_ids.device
75
+ generated = torch.full((batch_size, 1), bos_id, dtype=torch.long, device=device)
76
+ finished = torch.zeros(batch_size, dtype=torch.bool, device=device)
77
+
78
+ for _ in range(max_len):
79
+ logits = model.decode_step(generated, encoder_output, src_padding_mask)
80
+ next_token = logits.argmax(dim=-1, keepdim=True)
81
+ generated = torch.cat([generated, next_token], dim=1)
82
+ finished = finished | next_token.squeeze(-1).eq(eos_id)
83
+ if finished.all():
84
+ break
85
+
86
+ return generated
87
+
88
+
89
+ @torch.no_grad()
90
+ def beam_search_decode(
91
+ model: nn.Module,
92
+ src_ids: torch.Tensor,
93
+ src_padding_mask: torch.BoolTensor,
94
+ bos_id: int,
95
+ eos_id: int,
96
+ beam_size: int = 5,
97
+ max_len: int = 256,
98
+ length_penalty: float = 1.0,
99
+ no_repeat_ngram_size: int = 0,
100
+ ) -> torch.Tensor:
101
+ """
102
+ 束搜索解码。
103
+ """
104
+ batch_size, seq_len = src_ids.size()
105
+ device = src_ids.device
106
+
107
+ encoder_output = model.encode(src_ids, src_padding_mask)
108
+ encoder_output = encoder_output.unsqueeze(1).expand(batch_size, beam_size, -1, -1)
109
+ encoder_output = encoder_output.reshape(batch_size * beam_size, seq_len, -1)
110
+ src_padding_mask = src_padding_mask.unsqueeze(1).expand(batch_size, beam_size, seq_len)
111
+ src_padding_mask = src_padding_mask.reshape(batch_size * beam_size, seq_len)
112
+
113
+ beam_scores = torch.full((batch_size, beam_size), float("-inf"), device=device)
114
+ beam_scores[:, 0] = 0.0
115
+ generated = torch.full((batch_size, beam_size, 1), bos_id, dtype=torch.long, device=device)
116
+ finished = torch.zeros((batch_size, beam_size), dtype=torch.bool, device=device)
117
+
118
+ for _ in range(max_len):
119
+ flat_generated = generated.view(batch_size * beam_size, -1)
120
+ logits = model.decode_step(flat_generated, encoder_output, src_padding_mask)
121
+ log_probs = F.log_softmax(logits, dim=-1)
122
+
123
+ if no_repeat_ngram_size > 0:
124
+ log_probs = _apply_no_repeat_ngram(log_probs, flat_generated, no_repeat_ngram_size)
125
+
126
+ finished_flat = finished.view(batch_size * beam_size)
127
+ if finished_flat.any():
128
+ log_probs[finished_flat] = float("-inf")
129
+ log_probs[finished_flat, eos_id] = 0.0
130
+
131
+ vocab_size = log_probs.size(-1)
132
+ scores = beam_scores.unsqueeze(-1) + log_probs.view(batch_size, beam_size, vocab_size)
133
+ scores_flat = scores.view(batch_size, -1)
134
+ topk_scores, topk_indices = scores_flat.topk(beam_size, dim=-1)
135
+
136
+ beam_indices = topk_indices // vocab_size
137
+ token_indices = topk_indices % vocab_size
138
+
139
+ next_generated = []
140
+ next_finished = []
141
+ for batch_idx in range(batch_size):
142
+ selected_beams = beam_indices[batch_idx]
143
+ selected_tokens = token_indices[batch_idx]
144
+ next_seq = generated[batch_idx, selected_beams]
145
+ next_seq = torch.cat([next_seq, selected_tokens.unsqueeze(-1)], dim=-1)
146
+ next_generated.append(next_seq)
147
+ next_finished.append(
148
+ finished[batch_idx, selected_beams]
149
+ | selected_tokens.eq(eos_id)
150
+ )
151
+
152
+ generated = torch.stack(next_generated, dim=0)
153
+ finished = torch.stack(next_finished, dim=0)
154
+ beam_scores = topk_scores
155
+
156
+ if finished.all():
157
+ break
158
+
159
+ length = generated.size(1)
160
+ penalty = float(length) ** float(length_penalty)
161
+ final_scores = beam_scores / penalty
162
+ best_indices = final_scores.argmax(dim=-1)
163
+ output = generated[torch.arange(batch_size, device=device), best_indices]
164
+ return output
165
+
166
+
167
+ @torch.no_grad()
168
+ def sample_decode(
169
+ model: nn.Module,
170
+ src_ids: torch.Tensor,
171
+ src_padding_mask: torch.BoolTensor,
172
+ bos_id: int,
173
+ eos_id: int,
174
+ max_len: int = 256,
175
+ temperature: float = 1.0,
176
+ top_k: int = 0,
177
+ top_p: float = 1.0,
178
+ ) -> torch.Tensor:
179
+ """
180
+ 采样解码 (支持 temperature, top-k, top-p/nucleus sampling)。
181
+ """
182
+ encoder_output = model.encode(src_ids, src_padding_mask)
183
+ batch_size = src_ids.size(0)
184
+ device = src_ids.device
185
+ generated = torch.full((batch_size, 1), bos_id, dtype=torch.long, device=device)
186
+ finished = torch.zeros(batch_size, dtype=torch.bool, device=device)
187
+
188
+ for _ in range(max_len):
189
+ logits = model.decode_step(generated, encoder_output, src_padding_mask)
190
+ logits = logits / max(temperature, 1e-8)
191
+
192
+ if top_k > 0:
193
+ top_k = min(top_k, logits.size(-1))
194
+ values, indices = torch.topk(logits, top_k, dim=-1)
195
+ mask = torch.full_like(logits, float("-inf"))
196
+ logits = mask.scatter(-1, indices, values)
197
+
198
+ if 0.0 < top_p < 1.0:
199
+ sorted_logits, sorted_indices = torch.sort(logits, descending=True, dim=-1)
200
+ probs = F.softmax(sorted_logits, dim=-1)
201
+ cumulative_probs = torch.cumsum(probs, dim=-1)
202
+ cutoff = cumulative_probs > top_p
203
+ cutoff[:, 1:] = cutoff[:, :-1].clone()
204
+ cutoff[:, 0] = False
205
+ sorted_logits[cutoff] = float("-inf")
206
+ logits = torch.zeros_like(logits).scatter(-1, sorted_indices, sorted_logits)
207
+
208
+ probs = F.softmax(logits, dim=-1)
209
+ next_token = torch.multinomial(probs, num_samples=1)
210
+ next_token = next_token.clamp(min=0)
211
+
212
+ next_token = torch.where(finished.unsqueeze(-1), torch.full_like(next_token, eos_id), next_token)
213
+ generated = torch.cat([generated, next_token], dim=1)
214
+ finished = finished | next_token.squeeze(-1).eq(eos_id)
215
+
216
+ if finished.all():
217
+ break
218
+
219
+ return generated
src/easytranslate/evaluation/evaluator.py CHANGED
@@ -1,76 +1,208 @@
1
- """
2
- 评估器模块 — Person D 负责实现
3
-
4
- 功能要求:
5
- 将解码和评估指标整合为统一的评估接口。
6
-
7
- 使用方法:
8
- evaluator = Evaluator(model, tokenizer, config)
9
- results = evaluator.evaluate(test_loader)
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- import logging
15
- from typing import Optional
16
-
17
- import torch
18
- import torch.nn as nn
19
- from torch.utils.data import DataLoader
20
- from tqdm import tqdm
21
-
22
- from easytranslate.evaluation.metrics import compute_all_metrics
23
- from easytranslate.evaluation.decoding import greedy_decode, beam_search_decode, sample_decode
24
-
25
- logger = logging.getLogger(__name__)
26
-
27
-
28
- class Evaluator:
29
- """
30
- 翻译模型评估器。
31
-
32
- TODO [Person D]: 实现以下方法。
33
- """
34
-
35
- def __init__(self, model: nn.Module, tokenizer, config: dict):
36
- """
37
- TODO [Person D]:
38
- 1. 保存 model, tokenizer, config
39
- 2. config 读取解码策略和评估指标配置
40
- 3. 根据策略选择解码函数
41
- """
42
- raise NotImplementedError("TODO: Person D 实现 Evaluator.__init__")
43
-
44
- def evaluate(
45
- self,
46
- dataloader: DataLoader,
47
- src_texts: Optional[list[str]] = None,
48
- ref_texts: Optional[list[str]] = None,
49
- ) -> dict:
50
- """
51
- 在给定数据上进行评估。
52
-
53
- TODO [Person D]: 实现以下逻辑:
54
- 1. model.eval()
55
- 2. 遍历 dataloader,使用选定的解码策略生成翻译
56
- 3. 将生成的 token ids 解码为文本
57
- 4. 调用 compute_all_metrics 计算指标
58
- 5. 返回评估结果 dict
59
- """
60
- raise NotImplementedError("TODO: Person D 实现 evaluate")
61
-
62
- def translate(self, texts: list[str]) -> list[str]:
63
- """
64
- 翻译一批文本。
65
-
66
- TODO [Person D]:
67
- 1. tokenize 输入文本
68
- 2. 调用解码函数生成翻译
69
- 3. 解码为文本
70
- 4. 返回翻译结果列表
71
- """
72
- raise NotImplementedError("TODO: Person D 实现 translate")
73
-
74
- def translate_single(self, text: str) -> str:
75
- """翻译单条文本。"""
76
- return self.translate([text])[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 评估器模块 — Person D 负责实现
3
+
4
+ 功能要求:
5
+ 将解码和评估指标整合为统一的评估接口。
6
+
7
+ 使用方法:
8
+ evaluator = Evaluator(model, tokenizer, config)
9
+ results = evaluator.evaluate(test_loader)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from typing import Optional
16
+
17
+ import torch
18
+ import torch.nn as nn
19
+ from torch.nn.utils.rnn import pad_sequence
20
+ from torch.utils.data import DataLoader
21
+ from tqdm import tqdm
22
+
23
+ from easytranslate.evaluation.metrics import compute_all_metrics
24
+ from easytranslate.evaluation.decoding import greedy_decode, beam_search_decode, sample_decode
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ def _get_config_value(config, key_path, default=None):
30
+ if config is None:
31
+ return default
32
+ if isinstance(config, dict):
33
+ value = config
34
+ for key in key_path:
35
+ value = value.get(key, default)
36
+ if value is default:
37
+ break
38
+ return value
39
+ value = config
40
+ for key in key_path:
41
+ value = getattr(value, key, default)
42
+ if value is default:
43
+ break
44
+ return value
45
+
46
+
47
+ class Evaluator:
48
+ """
49
+ 翻译模型评估器。
50
+ """
51
+
52
+ def __init__(self, model: nn.Module, tokenizer, config: dict):
53
+ self.model = model
54
+ self.tokenizer = tokenizer
55
+ self.config = config or {}
56
+
57
+ self.evaluation_config = _get_config_value(self.config, ["evaluation"], {})
58
+ self.decoding_config = _get_config_value(self.evaluation_config, ["decoding"], {})
59
+ self.metrics = _get_config_value(self.evaluation_config, ["metrics"], ["bleu", "comet", "chrf", "ter"])
60
+ self.strategy = self.decoding_config.get("strategy", "beam_search") if isinstance(self.decoding_config, dict) else getattr(self.decoding_config, "strategy", "beam_search")
61
+
62
+ self.bos_id = self.tokenizer.bos_token_id
63
+ self.eos_id = self.tokenizer.eos_token_id
64
+ self.pad_id = self.tokenizer.pad_token_id
65
+
66
+ self.max_decode_len = self.decoding_config.get("max_decode_len", 256) if isinstance(self.decoding_config, dict) else getattr(self.decoding_config, "max_decode_len", 256)
67
+ self.beam_size = self.decoding_config.get("beam_size", 5) if isinstance(self.decoding_config, dict) else getattr(self.decoding_config, "beam_size", 5)
68
+ self.length_penalty = self.decoding_config.get("length_penalty", 1.0) if isinstance(self.decoding_config, dict) else getattr(self.decoding_config, "length_penalty", 1.0)
69
+ self.no_repeat_ngram_size = self.decoding_config.get("no_repeat_ngram_size", 0) if isinstance(self.decoding_config, dict) else getattr(self.decoding_config, "no_repeat_ngram_size", 0)
70
+ self.temperature = self.decoding_config.get("sampling", {}).get("temperature", 1.0) if isinstance(self.decoding_config, dict) else getattr(getattr(self.decoding_config, "sampling", {}), "temperature", 1.0)
71
+ self.top_k = self.decoding_config.get("sampling", {}).get("top_k", 0) if isinstance(self.decoding_config, dict) else getattr(getattr(self.decoding_config, "sampling", {}), "top_k", 0)
72
+ self.top_p = self.decoding_config.get("sampling", {}).get("top_p", 1.0) if isinstance(self.decoding_config, dict) else getattr(getattr(self.decoding_config, "sampling", {}), "top_p", 1.0)
73
+
74
+ self.use_generate = hasattr(self.model, "generate") and not (hasattr(self.model, "encode") and hasattr(self.model, "decode_step"))
75
+
76
+ def _decode(self, src_ids: torch.Tensor, src_padding_mask: torch.BoolTensor) -> torch.Tensor:
77
+ if self.use_generate:
78
+ generate_kwargs = {
79
+ "max_length": self.max_decode_len,
80
+ "early_stopping": True,
81
+ }
82
+ if self.strategy == "beam_search":
83
+ generate_kwargs.update(
84
+ {
85
+ "num_beams": self.beam_size,
86
+ "length_penalty": self.length_penalty,
87
+ "no_repeat_ngram_size": self.no_repeat_ngram_size,
88
+ }
89
+ )
90
+ elif self.strategy == "sampling":
91
+ generate_kwargs.update(
92
+ {
93
+ "do_sample": True,
94
+ "temperature": self.temperature,
95
+ "top_k": self.top_k,
96
+ "top_p": self.top_p,
97
+ "num_beams": 1,
98
+ }
99
+ )
100
+ else:
101
+ generate_kwargs.update({"num_beams": 1})
102
+
103
+ attention_mask = (~src_padding_mask).long()
104
+ return self.model.generate(input_ids=src_ids, attention_mask=attention_mask, **generate_kwargs)
105
+
106
+ if self.strategy == "beam_search":
107
+ return beam_search_decode(
108
+ self.model,
109
+ src_ids,
110
+ src_padding_mask,
111
+ self.bos_id,
112
+ self.eos_id,
113
+ beam_size=self.beam_size,
114
+ max_len=self.max_decode_len,
115
+ length_penalty=self.length_penalty,
116
+ no_repeat_ngram_size=self.no_repeat_ngram_size,
117
+ )
118
+ if self.strategy == "sampling":
119
+ return sample_decode(
120
+ self.model,
121
+ src_ids,
122
+ src_padding_mask,
123
+ self.bos_id,
124
+ self.eos_id,
125
+ max_len=self.max_decode_len,
126
+ temperature=self.temperature,
127
+ top_k=self.top_k,
128
+ top_p=self.top_p,
129
+ )
130
+ return greedy_decode(
131
+ self.model,
132
+ src_ids,
133
+ src_padding_mask,
134
+ self.bos_id,
135
+ self.eos_id,
136
+ max_len=self.max_decode_len,
137
+ )
138
+
139
+ def evaluate(
140
+ self,
141
+ dataloader: DataLoader,
142
+ src_texts: Optional[list[str]] = None,
143
+ ref_texts: Optional[list[str]] = None,
144
+ ) -> dict:
145
+ self.model.eval()
146
+ device = next(self.model.parameters()).device if any(p.requires_grad or p.is_floating_point() for p in self.model.parameters()) else torch.device("cpu")
147
+
148
+ if src_texts is None and hasattr(dataloader, "dataset") and hasattr(dataloader.dataset, "src_texts"):
149
+ src_texts = list(dataloader.dataset.src_texts)
150
+ if ref_texts is None and hasattr(dataloader, "dataset") and hasattr(dataloader.dataset, "tgt_texts"):
151
+ ref_texts = list(dataloader.dataset.tgt_texts)
152
+
153
+ if src_texts is None or ref_texts is None:
154
+ raise ValueError("Source texts and reference texts must be provided for evaluation.")
155
+
156
+ hypotheses: list[str] = []
157
+ sources: list[str] = []
158
+ references: list[str] = []
159
+
160
+ for batch_idx, batch in enumerate(tqdm(dataloader, desc="Evaluating", unit="batch")):
161
+ src_ids = batch["src_ids"].to(device)
162
+ src_padding_mask = batch.get("src_padding_mask")
163
+ if src_padding_mask is None:
164
+ src_padding_mask = src_ids.eq(self.pad_id)
165
+ else:
166
+ src_padding_mask = src_padding_mask.to(device)
167
+
168
+ output_ids = self._decode(src_ids, src_padding_mask)
169
+ if isinstance(output_ids, torch.Tensor):
170
+ output_ids = output_ids.cpu()
171
+
172
+ for sample_idx in range(output_ids.size(0)):
173
+ decoded = self.tokenizer.decode(output_ids[sample_idx].tolist(), skip_special_tokens=True)
174
+ hypotheses.append(decoded)
175
+
176
+ sources = src_texts
177
+ references = ref_texts
178
+ results = compute_all_metrics(sources, hypotheses, references, metrics=self.metrics)
179
+ return results
180
+
181
+ def translate(self, texts: list[str]) -> list[str]:
182
+ self.model.eval()
183
+ device = next(self.model.parameters()).device if any(p.requires_grad or p.is_floating_point() for p in self.model.parameters()) else torch.device("cpu")
184
+
185
+ input_ids = []
186
+ for text in texts:
187
+ src_tokens = self.tokenizer.encode(
188
+ text,
189
+ add_special_tokens=True,
190
+ max_length=_get_config_value(self.config, ["data", "preprocessing", "max_src_len"], 256),
191
+ )
192
+ input_ids.append(torch.tensor(src_tokens, dtype=torch.long, device=device))
193
+
194
+ src_ids = pad_sequence(input_ids, batch_first=True, padding_value=self.pad_id)
195
+ src_padding_mask = src_ids.eq(self.pad_id)
196
+
197
+ output_ids = self._decode(src_ids, src_padding_mask)
198
+ if isinstance(output_ids, torch.Tensor):
199
+ output_ids = output_ids.cpu()
200
+
201
+ translations: list[str] = []
202
+ for sample_idx in range(output_ids.size(0)):
203
+ translations.append(self.tokenizer.decode(output_ids[sample_idx].tolist(), skip_special_tokens=True))
204
+
205
+ return translations
206
+
207
+ def translate_single(self, text: str) -> str:
208
+ return self.translate([text])[0]
src/easytranslate/evaluation/metrics.py CHANGED
@@ -1,109 +1,169 @@
1
- """
2
- 评估指标模块 — Person D 负责实现
3
-
4
- 功能要求:
5
- 1. compute_bleu: 计算 SacreBLEU 分数
6
- 2. compute_comet: 计算 COMET 分数 (神经网络评估指标)
7
- 3. compute_chrf: 计算 chrF++ 分数
8
- 4. compute_ter: 计算 TER (Translation Edit Rate)
9
- 5. compute_all_metrics: 计算所有指标
10
-
11
- 技术要点:
12
- - SacreBLEU: 标准化的 BLEU 实现,结果可复现
13
- - COMET: 基于预训练语言模型的评估指标,与人类评价相关性最高
14
- - chrF++: 基于字符 n-gram 的 F-score,对中文尤其有用
15
- - TER: 编辑距离,衡量翻译后编辑量
16
- """
17
-
18
- from __future__ import annotations
19
-
20
- import logging
21
- from typing import Optional
22
-
23
- logger = logging.getLogger(__name__)
24
-
25
-
26
- def compute_bleu(
27
- hypotheses: list[str],
28
- references: list[str],
29
- tokenize: str = "zh",
30
- ) -> dict:
31
- """
32
- 计算 SacreBLEU 分数。
33
-
34
- TODO [Person D]: 实现以下逻辑:
35
- 1. 使用 sacrebleu.corpus_bleu(hypotheses, [references], tokenize=tokenize)
36
- 2. tokenize="zh" 对中文进行字符级别分词
37
- 3. 返回 {"bleu": score, "bleu_1": ..., "bleu_2": ..., "bleu_3": ..., "bleu_4": ..., "bp": ...}
38
-
39
- 注意: references 需要包装为 list of list (支持多参考)
40
- """
41
- raise NotImplementedError("TODO: Person D 实现 compute_bleu")
42
-
43
-
44
- def compute_comet(
45
- sources: list[str],
46
- hypotheses: list[str],
47
- references: list[str],
48
- model_name: str = "Unbabel/wmt22-comet-da",
49
- batch_size: int = 16,
50
- gpus: int = 1,
51
- ) -> dict:
52
- """
53
- 计算 COMET 分数。
54
-
55
- TODO [Person D]: 实现以下逻辑:
56
- 1. 加载 COMET 模型: comet.download_model(model_name)
57
- 2. 构建输入数据: [{"src": s, "mt": h, "ref": r} for s, h, r in zip(...)]
58
- 3. 调用 model.predict(data, batch_size, gpus)
59
- 4. 返回 {"comet": system_score, "comet_scores": segment_scores}
60
-
61
- COMET 需要源语言、翻译结果和参考翻译三者。
62
- """
63
- raise NotImplementedError("TODO: Person D 实现 compute_comet")
64
-
65
-
66
- def compute_chrf(
67
- hypotheses: list[str],
68
- references: list[str],
69
- ) -> dict:
70
- """
71
- 计算 chrF++ 分数。
72
-
73
- TODO [Person D]:
74
- 1. 使用 sacrebleu.corpus_chrf(hypotheses, [references])
75
- 2. 返回 {"chrf": score}
76
- """
77
- raise NotImplementedError("TODO: Person D 实现 compute_chrf")
78
-
79
-
80
- def compute_ter(
81
- hypotheses: list[str],
82
- references: list[str],
83
- ) -> dict:
84
- """
85
- 计算 TER 分数。
86
-
87
- TODO [Person D]:
88
- 1. 使用 sacrebleu.corpus_ter(hypotheses, [references])
89
- 2. 返回 {"ter": score}
90
- """
91
- raise NotImplementedError("TODO: Person D 实现 compute_ter")
92
-
93
-
94
- def compute_all_metrics(
95
- sources: list[str],
96
- hypotheses: list[str],
97
- references: list[str],
98
- metrics: list[str] = ["bleu", "comet", "chrf", "ter"],
99
- ) -> dict:
100
- """
101
- 计算所有指定的评估指标。
102
-
103
- TODO [Person D]:
104
- 1. 遍历 metrics 列表
105
- 2. 调用对应的计算函数
106
- 3. 合并结果并返回
107
- 4. 记录每个指标的计算时间
108
- """
109
- raise NotImplementedError("TODO: Person D 实现 compute_all_metrics")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 评估指标模块 — Person D 负责实现
3
+
4
+ 功能要求:
5
+ 1. compute_bleu: 计算 SacreBLEU 分数
6
+ 2. compute_comet: 计算 COMET 分数 (神经网络评估指标)
7
+ 3. compute_chrf: 计算 chrF++ 分数
8
+ 4. compute_ter: 计算 TER (Translation Edit Rate)
9
+ 5. compute_all_metrics: 计算所有指标
10
+
11
+ 技术要点:
12
+ - SacreBLEU: 标准化的 BLEU 实现,结果可复现
13
+ - COMET: 基于预训练语言模型的评估指标,与人类评价相关性最高
14
+ - chrF++: 基于字符 n-gram 的 F-score,对中文尤其有用
15
+ - TER: 编辑距离,衡量翻译后编辑量
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import time
22
+ from typing import Optional
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ def _import_sacrebleu():
28
+ try:
29
+ import sacrebleu
30
+ except ImportError as exc:
31
+ logger.error("SacreBLEU is not installed: %s", exc)
32
+ raise ImportError(
33
+ "sacrebleu is required for BLEU/chrF/TER evaluation. "
34
+ "Install it with `python -m pip install sacrebleu`."
35
+ ) from exc
36
+ return sacrebleu
37
+
38
+
39
+ def compute_bleu(
40
+ hypotheses: list[str],
41
+ references: list[str],
42
+ tokenize: str = "zh",
43
+ ) -> dict:
44
+ """
45
+ 计算 SacreBLEU 分数。
46
+ """
47
+ if not hypotheses or not references:
48
+ return {
49
+ "bleu": 0.0,
50
+ "bleu_1": 0.0,
51
+ "bleu_2": 0.0,
52
+ "bleu_3": 0.0,
53
+ "bleu_4": 0.0,
54
+ "bp": 0.0,
55
+ }
56
+
57
+ sacrebleu = _import_sacrebleu()
58
+ references_list = [references]
59
+ bleu = sacrebleu.corpus_bleu(hypotheses, references_list, tokenize=tokenize)
60
+ precisions = [round(float(x), 4) for x in bleu.precisions]
61
+ return {
62
+ "bleu": round(float(bleu.score), 4),
63
+ "bleu_1": precisions[0] if len(precisions) > 0 else 0.0,
64
+ "bleu_2": precisions[1] if len(precisions) > 1 else 0.0,
65
+ "bleu_3": precisions[2] if len(precisions) > 2 else 0.0,
66
+ "bleu_4": precisions[3] if len(precisions) > 3 else 0.0,
67
+ "bp": round(float(getattr(bleu, "bp", 0.0)), 4),
68
+ }
69
+
70
+
71
+ def compute_comet(
72
+ sources: list[str],
73
+ hypotheses: list[str],
74
+ references: list[str],
75
+ model_name: str = "Unbabel/wmt22-comet-da",
76
+ batch_size: int = 16,
77
+ gpus: int = 1,
78
+ ) -> dict:
79
+ """
80
+ 计算 COMET 分数。
81
+ """
82
+ if not sources or not hypotheses or not references:
83
+ return {"comet": 0.0, "comet_scores": []}
84
+
85
+ try:
86
+ from comet import download_model, load_from_checkpoint
87
+ except ImportError as exc:
88
+ logger.error("COMET library is not installed: %s", exc)
89
+ raise
90
+
91
+ model_path = download_model(model_name)
92
+ model = load_from_checkpoint(model_path)
93
+
94
+ data = [
95
+ {"src": src, "mt": hyp, "ref": ref}
96
+ for src, hyp, ref in zip(sources, hypotheses, references)
97
+ ]
98
+ prediction = model.predict(data, batch_size=batch_size, gpus=gpus)
99
+
100
+ if isinstance(prediction, dict):
101
+ scores = prediction.get("scores") or prediction.get("predictions") or []
102
+ else:
103
+ scores = list(prediction)
104
+
105
+ if scores is None:
106
+ scores = []
107
+
108
+ scores = [float(score) for score in scores]
109
+ system_score = float(sum(scores) / len(scores)) if scores else 0.0
110
+ return {"comet": round(system_score, 4), "comet_scores": scores}
111
+
112
+
113
+ def compute_chrf(
114
+ hypotheses: list[str],
115
+ references: list[str],
116
+ ) -> dict:
117
+ """
118
+ 计算 chrF++ 分数。
119
+ """
120
+ if not hypotheses or not references:
121
+ return {"chrf": 0.0}
122
+
123
+ sacrebleu = _import_sacrebleu()
124
+ chrf = sacrebleu.corpus_chrf(hypotheses, [references])
125
+ return {"chrf": round(float(chrf.score), 4)}
126
+
127
+
128
+ def compute_ter(
129
+ hypotheses: list[str],
130
+ references: list[str],
131
+ ) -> dict:
132
+ """
133
+ 计算 TER 分数。
134
+ """
135
+ if not hypotheses or not references:
136
+ return {"ter": 0.0}
137
+
138
+ sacrebleu = _import_sacrebleu()
139
+ ter = sacrebleu.corpus_ter(hypotheses, [references])
140
+ return {"ter": round(float(ter.score), 4)}
141
+
142
+
143
+ def compute_all_metrics(
144
+ sources: list[str],
145
+ hypotheses: list[str],
146
+ references: list[str],
147
+ metrics: list[str] = ["bleu", "comet", "chrf", "ter"],
148
+ ) -> dict:
149
+ """
150
+ 计算所有指定的评估指标。
151
+ """
152
+ results: dict[str, float | list[float]] = {}
153
+
154
+ for metric in metrics:
155
+ start_time = time.time()
156
+ if metric == "bleu":
157
+ results.update(compute_bleu(hypotheses, references))
158
+ elif metric == "comet":
159
+ results.update(compute_comet(sources, hypotheses, references))
160
+ elif metric == "chrf":
161
+ results.update(compute_chrf(hypotheses, references))
162
+ elif metric == "ter":
163
+ results.update(compute_ter(hypotheses, references))
164
+ else:
165
+ logger.warning("Unsupported evaluation metric: %s", metric)
166
+ elapsed = time.time() - start_time
167
+ logger.info("Computed %s in %.2f seconds", metric, elapsed)
168
+
169
+ return results
src/easytranslate/model/__init__.py CHANGED
@@ -1,20 +1,20 @@
1
- """模型架构模块 (Person B 负责)"""
2
-
3
- from easytranslate.model.transformer import TransformerTranslationModel
4
- from easytranslate.model.encoder import TransformerEncoder
5
- from easytranslate.model.decoder import TransformerDecoder
6
- from easytranslate.model.attention import MultiHeadAttention, FlashMultiHeadAttention
7
- from easytranslate.model.positional import SinusoidalPositionalEncoding, RotaryPositionalEmbedding
8
- from easytranslate.model.finetune import load_pretrained_model, setup_lora
9
-
10
- __all__ = [
11
- "TransformerTranslationModel",
12
- "TransformerEncoder",
13
- "TransformerDecoder",
14
- "MultiHeadAttention",
15
- "FlashMultiHeadAttention",
16
- "SinusoidalPositionalEncoding",
17
- "RotaryPositionalEmbedding",
18
- "load_pretrained_model",
19
- "setup_lora",
20
- ]
 
1
+ """模型架构模块 (Person B 负责)"""
2
+
3
+ from easytranslate.model.transformer import TransformerTranslationModel
4
+ from easytranslate.model.encoder import TransformerEncoder
5
+ from easytranslate.model.decoder import TransformerDecoder
6
+ from easytranslate.model.attention import MultiHeadAttention, FlashMultiHeadAttention
7
+ from easytranslate.model.positional import SinusoidalPositionalEncoding, RotaryPositionalEmbedding
8
+ from easytranslate.model.finetune import load_pretrained_model, setup_lora
9
+
10
+ __all__ = [
11
+ "TransformerTranslationModel",
12
+ "TransformerEncoder",
13
+ "TransformerDecoder",
14
+ "MultiHeadAttention",
15
+ "FlashMultiHeadAttention",
16
+ "SinusoidalPositionalEncoding",
17
+ "RotaryPositionalEmbedding",
18
+ "load_pretrained_model",
19
+ "setup_lora",
20
+ ]
src/easytranslate/model/attention.py CHANGED
@@ -1,231 +1,231 @@
1
- """
2
- 注意力机制模块 — Person B 负责实现
3
-
4
- 包含:
5
- 1. MultiHeadAttention: 标准多头注意力 (支持 RoPE)
6
- 2. FlashMultiHeadAttention: Flash Attention 2 加速版本
7
-
8
- 技术要点:
9
- - Scaled Dot-Product Attention
10
- - 支持 key_padding_mask 和 attn_mask
11
- - Flash Attention 2 使用 torch.nn.functional.scaled_dot_product_attention
12
- - RoPE 旋转位置编码集成
13
- """
14
-
15
- from __future__ import annotations
16
-
17
- import math
18
- from typing import Optional
19
-
20
- import torch
21
- import torch.nn as nn
22
- import torch.nn.functional as F
23
-
24
-
25
- class MultiHeadAttention(nn.Module):
26
- """
27
- 标准多头注意力机制。
28
-
29
- TODO [Person B]: 实现以下内容:
30
-
31
- __init__:
32
- 1. Q, K, V 线性投影: nn.Linear(d_model, d_model)
33
- 2. 输出投影: nn.Linear(d_model, d_model)
34
- 3. Dropout
35
-
36
- forward(query, key, value, key_padding_mask=None, attn_mask=None):
37
- 1. 线性投影 Q, K, V
38
- 2. reshape 为 [B, nhead, L, d_k]
39
- 3. (可选) 应用 RoPE 旋转位置编码
40
- 4. 计算 attention scores: QK^T / sqrt(d_k)
41
- 5. 应用 masks (padding mask + causal mask)
42
- 6. Softmax + Dropout
43
- 7. 加权求和 V
44
- 8. reshape 回 [B, L, d_model]
45
- 9. 输出投影
46
- """
47
-
48
- def __init__(
49
- self,
50
- d_model: int = 512,
51
- nhead: int = 8,
52
- dropout: float = 0.1,
53
- use_rotary_embedding: bool = False,
54
- ):
55
- super().__init__()
56
- assert d_model % nhead == 0, "d_model 必须能被 nhead 整除"
57
- self.d_model = d_model
58
- self.nhead = nhead
59
- self.d_k = d_model // nhead
60
- self.use_rotary_embedding = use_rotary_embedding
61
-
62
- self.q_proj = nn.Linear(d_model, d_model)
63
- self.k_proj = nn.Linear(d_model, d_model)
64
- self.v_proj = nn.Linear(d_model, d_model)
65
- self.out_proj = nn.Linear(d_model, d_model)
66
-
67
- self.dropout = nn.Dropout(p=dropout)
68
- self.rope: Optional[nn.Module] = None
69
-
70
- def forward(
71
- self,
72
- query: torch.Tensor, # [B, L_q, D]
73
- key: torch.Tensor, # [B, L_k, D]
74
- value: torch.Tensor, # [B, L_v, D]
75
- key_padding_mask: Optional[torch.BoolTensor] = None, # [B, L_k]
76
- attn_mask: Optional[torch.Tensor] = None, # [L_q, L_k]
77
- is_causal: bool = False,
78
- ) -> torch.Tensor:
79
- B, L_q, _ = query.size()
80
- L_k = key.size(1)
81
- L_v = value.size(1)
82
-
83
- # 1. 线性投影
84
- Q = self.q_proj(query) # [B, L_q, D]
85
- K = self.k_proj(key) # [B, L_k, D]
86
- V = self.v_proj(value) # [B, L_v, D]
87
-
88
- # 2. reshape 为 [B, nhead, L, d_k]
89
- Q = Q.view(B, L_q, self.nhead, self.d_k).transpose(1, 2) # [B, H, L_q, d_k]
90
- K = K.view(B, L_k, self.nhead, self.d_k).transpose(1, 2) # [B, H, L_k, d_k]
91
- V = V.view(B, L_v, self.nhead, self.d_k).transpose(1, 2) # [B, H, L_v, d_k]
92
-
93
- # 3. (可选) 应用 RoPE
94
- if self.use_rotary_embedding and self.rope is not None:
95
- Q, K = self.rope.apply_rotary_pos_emb(Q, K)
96
-
97
- # 4. 计算 attention scores: QK^T / sqrt(d_k)
98
- scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k) # [B, H, L_q, L_k]
99
-
100
- # 5. 应用 masks
101
- if key_padding_mask is not None:
102
- # key_padding_mask: [B, L_k] -> [B, 1, 1, L_k]
103
- scores = scores.masked_fill(
104
- key_padding_mask.unsqueeze(1).unsqueeze(2), float("-inf")
105
- )
106
- if is_causal:
107
- # 生成 causal mask
108
- L_q, L_k_local = scores.size(-2), scores.size(-1)
109
- causal_mask = torch.triu(
110
- torch.ones(L_q, L_k_local, device=scores.device), diagonal=1
111
- ).bool()
112
- scores = scores.masked_fill(
113
- causal_mask.unsqueeze(0).unsqueeze(0), float("-inf")
114
- )
115
- if attn_mask is not None:
116
- # attn_mask: [L_q, L_k] -> [1, 1, L_q, L_k]
117
- scores = scores.masked_fill(attn_mask.unsqueeze(0).unsqueeze(0), float("-inf"))
118
-
119
- # 6. Softmax + Dropout
120
- attn_weights = F.softmax(scores, dim=-1)
121
- attn_weights = self.dropout(attn_weights)
122
-
123
- # 7. 加权求和 V
124
- attn_output = torch.matmul(attn_weights, V) # [B, H, L_q, d_k]
125
-
126
- # 8. reshape 回 [B, L_q, d_model]
127
- attn_output = attn_output.transpose(1, 2).contiguous().view(B, L_q, self.d_model)
128
-
129
- # 9. 输出投影
130
- output = self.out_proj(attn_output)
131
- return output
132
-
133
-
134
- class FlashMultiHeadAttention(nn.Module):
135
- """
136
- Flash Attention 2 加速的多头注意力。
137
-
138
- TODO [Person B]: 使用 PyTorch 2.0+ 的 F.scaled_dot_product_attention 实现:
139
- 1. 与 MultiHeadAttention 结构相同
140
- 2. 在 forward 中使用 F.scaled_dot_product_attention(Q, K, V, attn_mask, dropout, is_causal)
141
- 3. 会自动选择最优的 attention kernel (Flash Attention / Memory-Efficient Attention)
142
-
143
- 注意:
144
- - 需要 PyTorch >= 2.0
145
- - is_causal=True 时自动生成因果掩码,不需要手动传入 attn_mask
146
- """
147
-
148
- def __init__(
149
- self,
150
- d_model: int = 512,
151
- nhead: int = 8,
152
- dropout: float = 0.1,
153
- use_rotary_embedding: bool = False,
154
- ):
155
- super().__init__()
156
- assert d_model % nhead == 0, "d_model 必须能被 nhead 整除"
157
- self.d_model = d_model
158
- self.nhead = nhead
159
- self.d_k = d_model // nhead
160
- self.use_rotary_embedding = use_rotary_embedding
161
- self.dropout_p = dropout
162
-
163
- self.q_proj = nn.Linear(d_model, d_model)
164
- self.k_proj = nn.Linear(d_model, d_model)
165
- self.v_proj = nn.Linear(d_model, d_model)
166
- self.out_proj = nn.Linear(d_model, d_model)
167
-
168
- self.rope: Optional[nn.Module] = None
169
-
170
- def forward(
171
- self,
172
- query: torch.Tensor,
173
- key: torch.Tensor,
174
- value: torch.Tensor,
175
- key_padding_mask: Optional[torch.BoolTensor] = None,
176
- is_causal: bool = False,
177
- ) -> torch.Tensor:
178
- B, L_q, _ = query.size()
179
- L_k = key.size(1)
180
- L_v = value.size(1)
181
-
182
- # 1. 线性投影
183
- Q = self.q_proj(query) # [B, L_q, D]
184
- K = self.k_proj(key) # [B, L_k, D]
185
- V = self.v_proj(value) # [B, L_v, D]
186
-
187
- # 2. reshape 为 [B, nhead, L, d_k]
188
- Q = Q.view(B, L_q, self.nhead, self.d_k).transpose(1, 2) # [B, H, L_q, d_k]
189
- K = K.view(B, L_k, self.nhead, self.d_k).transpose(1, 2) # [B, H, L_k, d_k]
190
- V = V.view(B, L_v, self.nhead, self.d_k).transpose(1, 2) # [B, H, L_v, d_k]
191
-
192
- # 3. (可选) 应 RoPE
193
- if self.use_rotary_embedding and self.rope is not None:
194
- Q, K = self.rope.apply_rotary_pos_emb(Q, K)
195
-
196
- # 4. 构建 attn_mask 以适配 scaled_dot_product_attention
197
- # PyTorch >= 2.0 支持 [B, nhead, L, d_k] 的 4D 输入
198
- # 注意: scaled_dot_product_attention 不允许同时设置 attn_mask 和 is_causal=True
199
- attn_mask: Optional[torch.Tensor] = None
200
- if is_causal or key_padding_mask is not None:
201
- attn_mask = torch.zeros(
202
- B, self.nhead, L_q, L_k, dtype=Q.dtype, device=Q.device
203
- )
204
- if is_causal:
205
- # 生成 causal mask (上三角为 -inf)
206
- causal_mask = torch.triu(
207
- torch.ones(L_q, L_k, device=Q.device), diagonal=1
208
- ).bool()
209
- attn_mask = attn_mask.masked_fill(
210
- causal_mask[None, None, :, :], float("-inf")
211
- )
212
- if key_padding_mask is not None:
213
- # key_padding_mask: True = padding (忽略)
214
- _bool_mask = key_padding_mask.unsqueeze(1).unsqueeze(2)
215
- _bool_mask = _bool_mask.expand(B, self.nhead, L_q, L_k)
216
- attn_mask = attn_mask.masked_fill(_bool_mask, float("-inf"))
217
-
218
- # 5. Flash Attention (PyTorch 原生)
219
- attn_output = F.scaled_dot_product_attention(
220
- Q, K, V,
221
- attn_mask=attn_mask,
222
- dropout_p=self.dropout_p if self.training else 0.0,
223
- is_causal=False, # 已通过 attn_mask 处理
224
- ) # [B, H, L_q, d_k]
225
-
226
- # 6. reshape 回 [B, L_q, d_model]
227
- attn_output = attn_output.transpose(1, 2).contiguous().view(B, L_q, self.d_model)
228
-
229
- # 7. 输出投影
230
- output = self.out_proj(attn_output)
231
- return output
 
1
+ """
2
+ 注意力机制模块 — Person B 负责实现
3
+
4
+ 包含:
5
+ 1. MultiHeadAttention: 标准多头注意力 (支持 RoPE)
6
+ 2. FlashMultiHeadAttention: Flash Attention 2 加速版本
7
+
8
+ 技术要点:
9
+ - Scaled Dot-Product Attention
10
+ - 支持 key_padding_mask 和 attn_mask
11
+ - Flash Attention 2 使用 torch.nn.functional.scaled_dot_product_attention
12
+ - RoPE 旋转位置编码集成
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ from typing import Optional
19
+
20
+ import torch
21
+ import torch.nn as nn
22
+ import torch.nn.functional as F
23
+
24
+
25
+ class MultiHeadAttention(nn.Module):
26
+ """
27
+ 标准多头注意力机制。
28
+
29
+ TODO [Person B]: 实现以下内容:
30
+
31
+ __init__:
32
+ 1. Q, K, V 线性投影: nn.Linear(d_model, d_model)
33
+ 2. 输出投影: nn.Linear(d_model, d_model)
34
+ 3. Dropout
35
+
36
+ forward(query, key, value, key_padding_mask=None, attn_mask=None):
37
+ 1. 线性投影 Q, K, V
38
+ 2. reshape 为 [B, nhead, L, d_k]
39
+ 3. (可选) 应用 RoPE 旋转位置编码
40
+ 4. 计算 attention scores: QK^T / sqrt(d_k)
41
+ 5. 应用 masks (padding mask + causal mask)
42
+ 6. Softmax + Dropout
43
+ 7. 加权求和 V
44
+ 8. reshape 回 [B, L, d_model]
45
+ 9. 输出投影
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ d_model: int = 512,
51
+ nhead: int = 8,
52
+ dropout: float = 0.1,
53
+ use_rotary_embedding: bool = False,
54
+ ):
55
+ super().__init__()
56
+ assert d_model % nhead == 0, "d_model 必须能被 nhead 整除"
57
+ self.d_model = d_model
58
+ self.nhead = nhead
59
+ self.d_k = d_model // nhead
60
+ self.use_rotary_embedding = use_rotary_embedding
61
+
62
+ self.q_proj = nn.Linear(d_model, d_model)
63
+ self.k_proj = nn.Linear(d_model, d_model)
64
+ self.v_proj = nn.Linear(d_model, d_model)
65
+ self.out_proj = nn.Linear(d_model, d_model)
66
+
67
+ self.dropout = nn.Dropout(p=dropout)
68
+ self.rope: Optional[nn.Module] = None
69
+
70
+ def forward(
71
+ self,
72
+ query: torch.Tensor, # [B, L_q, D]
73
+ key: torch.Tensor, # [B, L_k, D]
74
+ value: torch.Tensor, # [B, L_v, D]
75
+ key_padding_mask: Optional[torch.BoolTensor] = None, # [B, L_k]
76
+ attn_mask: Optional[torch.Tensor] = None, # [L_q, L_k]
77
+ is_causal: bool = False,
78
+ ) -> torch.Tensor:
79
+ B, L_q, _ = query.size()
80
+ L_k = key.size(1)
81
+ L_v = value.size(1)
82
+
83
+ # 1. 线性投影
84
+ Q = self.q_proj(query) # [B, L_q, D]
85
+ K = self.k_proj(key) # [B, L_k, D]
86
+ V = self.v_proj(value) # [B, L_v, D]
87
+
88
+ # 2. reshape 为 [B, nhead, L, d_k]
89
+ Q = Q.view(B, L_q, self.nhead, self.d_k).transpose(1, 2) # [B, H, L_q, d_k]
90
+ K = K.view(B, L_k, self.nhead, self.d_k).transpose(1, 2) # [B, H, L_k, d_k]
91
+ V = V.view(B, L_v, self.nhead, self.d_k).transpose(1, 2) # [B, H, L_v, d_k]
92
+
93
+ # 3. (可选) 应用 RoPE
94
+ if self.use_rotary_embedding and self.rope is not None:
95
+ Q, K = self.rope.apply_rotary_pos_emb(Q, K)
96
+
97
+ # 4. 计算 attention scores: QK^T / sqrt(d_k)
98
+ scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k) # [B, H, L_q, L_k]
99
+
100
+ # 5. 应用 masks
101
+ if key_padding_mask is not None:
102
+ # key_padding_mask: [B, L_k] -> [B, 1, 1, L_k]
103
+ scores = scores.masked_fill(
104
+ key_padding_mask.unsqueeze(1).unsqueeze(2), float("-inf")
105
+ )
106
+ if is_causal:
107
+ # 生成 causal mask
108
+ L_q, L_k_local = scores.size(-2), scores.size(-1)
109
+ causal_mask = torch.triu(
110
+ torch.ones(L_q, L_k_local, device=scores.device), diagonal=1
111
+ ).bool()
112
+ scores = scores.masked_fill(
113
+ causal_mask.unsqueeze(0).unsqueeze(0), float("-inf")
114
+ )
115
+ if attn_mask is not None:
116
+ # attn_mask: [L_q, L_k] -> [1, 1, L_q, L_k]
117
+ scores = scores.masked_fill(attn_mask.unsqueeze(0).unsqueeze(0), float("-inf"))
118
+
119
+ # 6. Softmax + Dropout
120
+ attn_weights = F.softmax(scores, dim=-1)
121
+ attn_weights = self.dropout(attn_weights)
122
+
123
+ # 7. 加权求和 V
124
+ attn_output = torch.matmul(attn_weights, V) # [B, H, L_q, d_k]
125
+
126
+ # 8. reshape 回 [B, L_q, d_model]
127
+ attn_output = attn_output.transpose(1, 2).contiguous().view(B, L_q, self.d_model)
128
+
129
+ # 9. 输出投影
130
+ output = self.out_proj(attn_output)
131
+ return output
132
+
133
+
134
+ class FlashMultiHeadAttention(nn.Module):
135
+ """
136
+ Flash Attention 2 加速的多头注意力。
137
+
138
+ TODO [Person B]: 使用 PyTorch 2.0+ 的 F.scaled_dot_product_attention 实现:
139
+ 1. 与 MultiHeadAttention 结构相同
140
+ 2. 在 forward 中使用 F.scaled_dot_product_attention(Q, K, V, attn_mask, dropout, is_causal)
141
+ 3. 会自动选择最优的 attention kernel (Flash Attention / Memory-Efficient Attention)
142
+
143
+ 注意:
144
+ - 需要 PyTorch >= 2.0
145
+ - is_causal=True 时自动生成因果掩码,不需要手动传入 attn_mask
146
+ """
147
+
148
+ def __init__(
149
+ self,
150
+ d_model: int = 512,
151
+ nhead: int = 8,
152
+ dropout: float = 0.1,
153
+ use_rotary_embedding: bool = False,
154
+ ):
155
+ super().__init__()
156
+ assert d_model % nhead == 0, "d_model 必须能被 nhead 整除"
157
+ self.d_model = d_model
158
+ self.nhead = nhead
159
+ self.d_k = d_model // nhead
160
+ self.use_rotary_embedding = use_rotary_embedding
161
+ self.dropout_p = dropout
162
+
163
+ self.q_proj = nn.Linear(d_model, d_model)
164
+ self.k_proj = nn.Linear(d_model, d_model)
165
+ self.v_proj = nn.Linear(d_model, d_model)
166
+ self.out_proj = nn.Linear(d_model, d_model)
167
+
168
+ self.rope: Optional[nn.Module] = None
169
+
170
+ def forward(
171
+ self,
172
+ query: torch.Tensor,
173
+ key: torch.Tensor,
174
+ value: torch.Tensor,
175
+ key_padding_mask: Optional[torch.BoolTensor] = None,
176
+ is_causal: bool = False,
177
+ ) -> torch.Tensor:
178
+ B, L_q, _ = query.size()
179
+ L_k = key.size(1)
180
+ L_v = value.size(1)
181
+
182
+ # 1. 线性投影
183
+ Q = self.q_proj(query) # [B, L_q, D]
184
+ K = self.k_proj(key) # [B, L_k, D]
185
+ V = self.v_proj(value) # [B, L_v, D]
186
+
187
+ # 2. reshape 为 [B, nhead, L, d_k]
188
+ Q = Q.view(B, L_q, self.nhead, self.d_k).transpose(1, 2) # [B, H, L_q, d_k]
189
+ K = K.view(B, L_k, self.nhead, self.d_k).transpose(1, 2) # [B, H, L_k, d_k]
190
+ V = V.view(B, L_v, self.nhead, self.d_k).transpose(1, 2) # [B, H, L_v, d_k]
191
+
192
+ # 3. (可选) 应��� RoPE
193
+ if self.use_rotary_embedding and self.rope is not None:
194
+ Q, K = self.rope.apply_rotary_pos_emb(Q, K)
195
+
196
+ # 4. 构建 attn_mask 以适配 scaled_dot_product_attention
197
+ # PyTorch >= 2.0 支持 [B, nhead, L, d_k] 的 4D 输入
198
+ # 注意: scaled_dot_product_attention 不允许同时设置 attn_mask 和 is_causal=True
199
+ attn_mask: Optional[torch.Tensor] = None
200
+ if is_causal or key_padding_mask is not None:
201
+ attn_mask = torch.zeros(
202
+ B, self.nhead, L_q, L_k, dtype=Q.dtype, device=Q.device
203
+ )
204
+ if is_causal:
205
+ # 生成 causal mask (上三角为 -inf)
206
+ causal_mask = torch.triu(
207
+ torch.ones(L_q, L_k, device=Q.device), diagonal=1
208
+ ).bool()
209
+ attn_mask = attn_mask.masked_fill(
210
+ causal_mask[None, None, :, :], float("-inf")
211
+ )
212
+ if key_padding_mask is not None:
213
+ # key_padding_mask: True = padding (忽略)
214
+ _bool_mask = key_padding_mask.unsqueeze(1).unsqueeze(2)
215
+ _bool_mask = _bool_mask.expand(B, self.nhead, L_q, L_k)
216
+ attn_mask = attn_mask.masked_fill(_bool_mask, float("-inf"))
217
+
218
+ # 5. Flash Attention (PyTorch 原生)
219
+ attn_output = F.scaled_dot_product_attention(
220
+ Q, K, V,
221
+ attn_mask=attn_mask,
222
+ dropout_p=self.dropout_p if self.training else 0.0,
223
+ is_causal=False, # 已通过 attn_mask 处理
224
+ ) # [B, H, L_q, d_k]
225
+
226
+ # 6. reshape 回 [B, L_q, d_model]
227
+ attn_output = attn_output.transpose(1, 2).contiguous().view(B, L_q, self.d_model)
228
+
229
+ # 7. 输出投影
230
+ output = self.out_proj(attn_output)
231
+ return output
src/easytranslate/model/decoder.py CHANGED
@@ -1,176 +1,176 @@
1
- """
2
- Transformer Decoder 模块 — Person B 负责实现
3
-
4
- 包含:
5
- - TransformerDecoderLayer: 单层解码器
6
- - TransformerDecoder: 多层解码器堆叠
7
-
8
- 架构 (Pre-LayerNorm):
9
- x → LN → Masked Self-Attention → Residual
10
- → LN → Cross-Attention → Residual
11
- → LN → FFN → Residual
12
- """
13
-
14
- from __future__ import annotations
15
-
16
- import copy
17
-
18
- import torch
19
- import torch.nn as nn
20
- from typing import Optional
21
-
22
- from easytranslate.model.attention import MultiHeadAttention, FlashMultiHeadAttention
23
-
24
-
25
- class TransformerDecoderLayer(nn.Module):
26
- """
27
- 单层 Transformer Decoder。
28
-
29
- 架构 (Pre-LayerNorm):
30
- x → LN → Masked Self-Attention → Residual
31
- → LN → Cross-Attention → Residual
32
- → LN → FFN → Residual
33
- """
34
-
35
- def __init__(
36
- self,
37
- d_model: int = 512,
38
- nhead: int = 8,
39
- dim_feedforward: int = 2048,
40
- dropout: float = 0.1,
41
- activation: str = "gelu",
42
- use_flash_attention: bool = True,
43
- use_rotary_embedding: bool = True,
44
- pre_norm: bool = True,
45
- ):
46
- super().__init__()
47
- self.pre_norm = pre_norm
48
- self.d_model = d_model
49
-
50
- attn_cls = FlashMultiHeadAttention if use_flash_attention else MultiHeadAttention
51
-
52
- # 1. Masked Self-Attention
53
- self.self_attn = attn_cls(
54
- d_model=d_model,
55
- nhead=nhead,
56
- dropout=dropout,
57
- use_rotary_embedding=use_rotary_embedding,
58
- )
59
-
60
- # 2. Cross-Attention (decoder queries encoder memory)
61
- self.multihead_attn = attn_cls(
62
- d_model=d_model,
63
- nhead=nhead,
64
- dropout=dropout,
65
- use_rotary_embedding=False, # cross-attention 不使用 RoPE
66
- )
67
-
68
- # 3. Feed-Forward Network
69
- self.linear1 = nn.Linear(d_model, dim_feedforward)
70
- self.activation = nn.GELU() if activation == "gelu" else nn.ReLU()
71
- self.dropout = nn.Dropout(p=dropout)
72
- self.linear2 = nn.Linear(dim_feedforward, d_model)
73
-
74
- # 4. LayerNorms
75
- self.norm1 = nn.LayerNorm(d_model)
76
- self.norm2 = nn.LayerNorm(d_model)
77
- self.norm3 = nn.LayerNorm(d_model)
78
-
79
- # 5. Dropouts for residuals
80
- self.dropout1 = nn.Dropout(p=dropout)
81
- self.dropout2 = nn.Dropout(p=dropout)
82
- self.dropout3 = nn.Dropout(p=dropout)
83
-
84
- def forward(
85
- self,
86
- tgt: torch.Tensor, # [B, T, D]
87
- memory: torch.Tensor, # [B, S, D] (encoder output)
88
- tgt_mask: Optional[torch.Tensor] = None, # [T, T] causal mask
89
- memory_key_padding_mask: Optional[torch.BoolTensor] = None, # [B, S]
90
- tgt_key_padding_mask: Optional[torch.BoolTensor] = None, # [B, T]
91
- ) -> torch.Tensor:
92
- """Pre-LayerNorm 前向传播。"""
93
- # 适配 Flash Attention: 使用 is_causal 替代显式 causal mask
94
- is_causal = tgt_mask is not None
95
-
96
- if self.pre_norm:
97
- # 1. Masked Self-Attention
98
- residual = tgt
99
- tgt = self.norm1(tgt)
100
- tgt = self.self_attn(
101
- tgt, tgt, tgt,
102
- key_padding_mask=tgt_key_padding_mask,
103
- is_causal=is_causal,
104
- )
105
- tgt = residual + self.dropout1(tgt)
106
-
107
- # 2. Cross-Attention
108
- residual = tgt
109
- tgt = self.norm2(tgt)
110
- tgt = self.multihead_attn(
111
- tgt, memory, memory,
112
- key_padding_mask=memory_key_padding_mask,
113
- )
114
- tgt = residual + self.dropout2(tgt)
115
-
116
- # 3. FFN
117
- residual = tgt
118
- tgt = self.norm3(tgt)
119
- tgt = self.linear2(self.dropout(self.activation(self.linear1(tgt))))
120
- tgt = residual + self.dropout3(tgt)
121
- else:
122
- # Post-LayerNorm (备用)
123
- residual = tgt
124
- tgt = self.self_attn(
125
- tgt, tgt, tgt,
126
- key_padding_mask=tgt_key_padding_mask,
127
- is_causal=is_causal,
128
- )
129
- tgt = self.norm1(residual + self.dropout1(tgt))
130
-
131
- residual = tgt
132
- tgt = self.multihead_attn(
133
- tgt, memory, memory,
134
- key_padding_mask=memory_key_padding_mask,
135
- )
136
- tgt = self.norm2(residual + self.dropout2(tgt))
137
-
138
- residual = tgt
139
- tgt = self.linear2(self.dropout(self.activation(self.linear1(tgt))))
140
- tgt = self.norm3(residual + self.dropout3(tgt))
141
-
142
- return tgt
143
-
144
-
145
- class TransformerDecoder(nn.Module):
146
- """
147
- 多层 Transformer Decoder。
148
- """
149
-
150
- def __init__(self, decoder_layer: TransformerDecoderLayer, num_layers: int):
151
- super().__init__()
152
- self.layers = nn.ModuleList(
153
- [copy.deepcopy(decoder_layer) for _ in range(num_layers)]
154
- )
155
- self.num_layers = num_layers
156
- self.norm = nn.LayerNorm(decoder_layer.d_model)
157
-
158
- def forward(
159
- self,
160
- tgt: torch.Tensor,
161
- memory: torch.Tensor,
162
- tgt_mask: Optional[torch.Tensor] = None,
163
- memory_key_padding_mask: Optional[torch.BoolTensor] = None,
164
- tgt_key_padding_mask: Optional[torch.BoolTensor] = None,
165
- ) -> torch.Tensor:
166
- output = tgt
167
- for layer in self.layers:
168
- output = layer(
169
- output,
170
- memory,
171
- tgt_mask=tgt_mask,
172
- memory_key_padding_mask=memory_key_padding_mask,
173
- tgt_key_padding_mask=tgt_key_padding_mask,
174
- )
175
- output = self.norm(output)
176
- return output
 
1
+ """
2
+ Transformer Decoder 模块 — Person B 负责实现
3
+
4
+ 包含:
5
+ - TransformerDecoderLayer: 单层解码器
6
+ - TransformerDecoder: 多层解码器堆叠
7
+
8
+ 架构 (Pre-LayerNorm):
9
+ x → LN → Masked Self-Attention → Residual
10
+ → LN → Cross-Attention → Residual
11
+ → LN → FFN → Residual
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import copy
17
+
18
+ import torch
19
+ import torch.nn as nn
20
+ from typing import Optional
21
+
22
+ from easytranslate.model.attention import MultiHeadAttention, FlashMultiHeadAttention
23
+
24
+
25
+ class TransformerDecoderLayer(nn.Module):
26
+ """
27
+ 单层 Transformer Decoder。
28
+
29
+ 架构 (Pre-LayerNorm):
30
+ x → LN → Masked Self-Attention → Residual
31
+ → LN → Cross-Attention → Residual
32
+ → LN → FFN → Residual
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ d_model: int = 512,
38
+ nhead: int = 8,
39
+ dim_feedforward: int = 2048,
40
+ dropout: float = 0.1,
41
+ activation: str = "gelu",
42
+ use_flash_attention: bool = True,
43
+ use_rotary_embedding: bool = True,
44
+ pre_norm: bool = True,
45
+ ):
46
+ super().__init__()
47
+ self.pre_norm = pre_norm
48
+ self.d_model = d_model
49
+
50
+ attn_cls = FlashMultiHeadAttention if use_flash_attention else MultiHeadAttention
51
+
52
+ # 1. Masked Self-Attention
53
+ self.self_attn = attn_cls(
54
+ d_model=d_model,
55
+ nhead=nhead,
56
+ dropout=dropout,
57
+ use_rotary_embedding=use_rotary_embedding,
58
+ )
59
+
60
+ # 2. Cross-Attention (decoder queries encoder memory)
61
+ self.multihead_attn = attn_cls(
62
+ d_model=d_model,
63
+ nhead=nhead,
64
+ dropout=dropout,
65
+ use_rotary_embedding=False, # cross-attention 不使用 RoPE
66
+ )
67
+
68
+ # 3. Feed-Forward Network
69
+ self.linear1 = nn.Linear(d_model, dim_feedforward)
70
+ self.activation = nn.GELU() if activation == "gelu" else nn.ReLU()
71
+ self.dropout = nn.Dropout(p=dropout)
72
+ self.linear2 = nn.Linear(dim_feedforward, d_model)
73
+
74
+ # 4. LayerNorms
75
+ self.norm1 = nn.LayerNorm(d_model)
76
+ self.norm2 = nn.LayerNorm(d_model)
77
+ self.norm3 = nn.LayerNorm(d_model)
78
+
79
+ # 5. Dropouts for residuals
80
+ self.dropout1 = nn.Dropout(p=dropout)
81
+ self.dropout2 = nn.Dropout(p=dropout)
82
+ self.dropout3 = nn.Dropout(p=dropout)
83
+
84
+ def forward(
85
+ self,
86
+ tgt: torch.Tensor, # [B, T, D]
87
+ memory: torch.Tensor, # [B, S, D] (encoder output)
88
+ tgt_mask: Optional[torch.Tensor] = None, # [T, T] causal mask
89
+ memory_key_padding_mask: Optional[torch.BoolTensor] = None, # [B, S]
90
+ tgt_key_padding_mask: Optional[torch.BoolTensor] = None, # [B, T]
91
+ ) -> torch.Tensor:
92
+ """Pre-LayerNorm 前向传播。"""
93
+ # 适配 Flash Attention: 使用 is_causal 替代显式 causal mask
94
+ is_causal = tgt_mask is not None
95
+
96
+ if self.pre_norm:
97
+ # 1. Masked Self-Attention
98
+ residual = tgt
99
+ tgt = self.norm1(tgt)
100
+ tgt = self.self_attn(
101
+ tgt, tgt, tgt,
102
+ key_padding_mask=tgt_key_padding_mask,
103
+ is_causal=is_causal,
104
+ )
105
+ tgt = residual + self.dropout1(tgt)
106
+
107
+ # 2. Cross-Attention
108
+ residual = tgt
109
+ tgt = self.norm2(tgt)
110
+ tgt = self.multihead_attn(
111
+ tgt, memory, memory,
112
+ key_padding_mask=memory_key_padding_mask,
113
+ )
114
+ tgt = residual + self.dropout2(tgt)
115
+
116
+ # 3. FFN
117
+ residual = tgt
118
+ tgt = self.norm3(tgt)
119
+ tgt = self.linear2(self.dropout(self.activation(self.linear1(tgt))))
120
+ tgt = residual + self.dropout3(tgt)
121
+ else:
122
+ # Post-LayerNorm (备用)
123
+ residual = tgt
124
+ tgt = self.self_attn(
125
+ tgt, tgt, tgt,
126
+ key_padding_mask=tgt_key_padding_mask,
127
+ is_causal=is_causal,
128
+ )
129
+ tgt = self.norm1(residual + self.dropout1(tgt))
130
+
131
+ residual = tgt
132
+ tgt = self.multihead_attn(
133
+ tgt, memory, memory,
134
+ key_padding_mask=memory_key_padding_mask,
135
+ )
136
+ tgt = self.norm2(residual + self.dropout2(tgt))
137
+
138
+ residual = tgt
139
+ tgt = self.linear2(self.dropout(self.activation(self.linear1(tgt))))
140
+ tgt = self.norm3(residual + self.dropout3(tgt))
141
+
142
+ return tgt
143
+
144
+
145
+ class TransformerDecoder(nn.Module):
146
+ """
147
+ 多层 Transformer Decoder。
148
+ """
149
+
150
+ def __init__(self, decoder_layer: TransformerDecoderLayer, num_layers: int):
151
+ super().__init__()
152
+ self.layers = nn.ModuleList(
153
+ [copy.deepcopy(decoder_layer) for _ in range(num_layers)]
154
+ )
155
+ self.num_layers = num_layers
156
+ self.norm = nn.LayerNorm(decoder_layer.d_model)
157
+
158
+ def forward(
159
+ self,
160
+ tgt: torch.Tensor,
161
+ memory: torch.Tensor,
162
+ tgt_mask: Optional[torch.Tensor] = None,
163
+ memory_key_padding_mask: Optional[torch.BoolTensor] = None,
164
+ tgt_key_padding_mask: Optional[torch.BoolTensor] = None,
165
+ ) -> torch.Tensor:
166
+ output = tgt
167
+ for layer in self.layers:
168
+ output = layer(
169
+ output,
170
+ memory,
171
+ tgt_mask=tgt_mask,
172
+ memory_key_padding_mask=memory_key_padding_mask,
173
+ tgt_key_padding_mask=tgt_key_padding_mask,
174
+ )
175
+ output = self.norm(output)
176
+ return output
src/easytranslate/model/encoder.py CHANGED
@@ -1,127 +1,127 @@
1
- """
2
- Transformer Encoder 模块 — Person B 负责实现
3
-
4
- 包含:
5
- - TransformerEncoderLayer: 单层编码器
6
- - TransformerEncoder: 多层编码器堆叠
7
-
8
- 架构 (Pre-LayerNorm):
9
- x → LayerNorm → MultiHeadAttention → Residual → LayerNorm → FFN → Residual
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- import copy
15
-
16
- import torch
17
- import torch.nn as nn
18
- from typing import Optional
19
-
20
- from easytranslate.model.attention import MultiHeadAttention, FlashMultiHeadAttention
21
-
22
-
23
- class TransformerEncoderLayer(nn.Module):
24
- """
25
- 单层 Transformer Encoder。
26
-
27
- 架构 (Pre-LayerNorm):
28
- x → LayerNorm → MultiHeadAttention → Residual → LayerNorm → FFN → Residual
29
- """
30
-
31
- def __init__(
32
- self,
33
- d_model: int = 512,
34
- nhead: int = 8,
35
- dim_feedforward: int = 2048,
36
- dropout: float = 0.1,
37
- activation: str = "gelu",
38
- use_flash_attention: bool = True,
39
- use_rotary_embedding: bool = True,
40
- pre_norm: bool = True,
41
- ):
42
- super().__init__()
43
- self.pre_norm = pre_norm
44
-
45
- # Self-Attention
46
- attn_cls = FlashMultiHeadAttention if use_flash_attention else MultiHeadAttention
47
- self.self_attn = attn_cls(
48
- d_model=d_model,
49
- nhead=nhead,
50
- dropout=dropout,
51
- use_rotary_embedding=use_rotary_embedding,
52
- )
53
-
54
- # Feed-Forward Network
55
- self.linear1 = nn.Linear(d_model, dim_feedforward)
56
- self.activation = nn.GELU() if activation == "gelu" else nn.ReLU()
57
- self.dropout = nn.Dropout(p=dropout)
58
- self.linear2 = nn.Linear(dim_feedforward, d_model)
59
-
60
- # LayerNorm
61
- self.norm1 = nn.LayerNorm(d_model)
62
- self.norm2 = nn.LayerNorm(d_model)
63
-
64
- # Dropout for residual
65
- self.dropout1 = nn.Dropout(p=dropout)
66
- self.dropout2 = nn.Dropout(p=dropout)
67
-
68
- def forward(
69
- self,
70
- src: torch.Tensor, # [B, S, D]
71
- src_key_padding_mask: Optional[torch.BoolTensor] = None, # [B, S]
72
- ) -> torch.Tensor:
73
- """Pre-LayerNorm 前向传播。"""
74
- if self.pre_norm:
75
- # 1. Self-Attention sublayer
76
- residual = src
77
- src = self.norm1(src)
78
- src = self.self_attn(
79
- src, src, src,
80
- key_padding_mask=src_key_padding_mask,
81
- )
82
- src = residual + self.dropout1(src)
83
-
84
- # 2. FFN sublayer
85
- residual = src
86
- src = self.norm2(src)
87
- src = self.linear2(self.dropout(self.activation(self.linear1(src))))
88
- src = residual + self.dropout2(src)
89
- else:
90
- # Post-LayerNorm (备用)
91
- residual = src
92
- src = self.self_attn(
93
- src, src, src,
94
- key_padding_mask=src_key_padding_mask,
95
- )
96
- src = self.norm1(residual + self.dropout1(src))
97
-
98
- residual = src
99
- src = self.linear2(self.dropout(self.activation(self.linear1(src))))
100
- src = self.norm2(residual + self.dropout2(src))
101
-
102
- return src
103
-
104
-
105
- class TransformerEncoder(nn.Module):
106
- """
107
- 多层 Transformer Encoder。
108
- """
109
-
110
- def __init__(self, encoder_layer: TransformerEncoderLayer, num_layers: int):
111
- super().__init__()
112
- self.layers = nn.ModuleList(
113
- [copy.deepcopy(encoder_layer) for _ in range(num_layers)]
114
- )
115
- self.num_layers = num_layers
116
- self.norm = nn.LayerNorm(encoder_layer.self_attn.d_model)
117
-
118
- def forward(
119
- self,
120
- src: torch.Tensor,
121
- src_key_padding_mask: Optional[torch.BoolTensor] = None,
122
- ) -> torch.Tensor:
123
- output = src
124
- for layer in self.layers:
125
- output = layer(output, src_key_padding_mask=src_key_padding_mask)
126
- output = self.norm(output)
127
- return output
 
1
+ """
2
+ Transformer Encoder 模块 — Person B 负责实现
3
+
4
+ 包含:
5
+ - TransformerEncoderLayer: 单层编码器
6
+ - TransformerEncoder: 多层编码器堆叠
7
+
8
+ 架构 (Pre-LayerNorm):
9
+ x → LayerNorm → MultiHeadAttention → Residual → LayerNorm → FFN → Residual
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import copy
15
+
16
+ import torch
17
+ import torch.nn as nn
18
+ from typing import Optional
19
+
20
+ from easytranslate.model.attention import MultiHeadAttention, FlashMultiHeadAttention
21
+
22
+
23
+ class TransformerEncoderLayer(nn.Module):
24
+ """
25
+ 单层 Transformer Encoder。
26
+
27
+ 架构 (Pre-LayerNorm):
28
+ x → LayerNorm → MultiHeadAttention → Residual → LayerNorm → FFN → Residual
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ d_model: int = 512,
34
+ nhead: int = 8,
35
+ dim_feedforward: int = 2048,
36
+ dropout: float = 0.1,
37
+ activation: str = "gelu",
38
+ use_flash_attention: bool = True,
39
+ use_rotary_embedding: bool = True,
40
+ pre_norm: bool = True,
41
+ ):
42
+ super().__init__()
43
+ self.pre_norm = pre_norm
44
+
45
+ # Self-Attention
46
+ attn_cls = FlashMultiHeadAttention if use_flash_attention else MultiHeadAttention
47
+ self.self_attn = attn_cls(
48
+ d_model=d_model,
49
+ nhead=nhead,
50
+ dropout=dropout,
51
+ use_rotary_embedding=use_rotary_embedding,
52
+ )
53
+
54
+ # Feed-Forward Network
55
+ self.linear1 = nn.Linear(d_model, dim_feedforward)
56
+ self.activation = nn.GELU() if activation == "gelu" else nn.ReLU()
57
+ self.dropout = nn.Dropout(p=dropout)
58
+ self.linear2 = nn.Linear(dim_feedforward, d_model)
59
+
60
+ # LayerNorm
61
+ self.norm1 = nn.LayerNorm(d_model)
62
+ self.norm2 = nn.LayerNorm(d_model)
63
+
64
+ # Dropout for residual
65
+ self.dropout1 = nn.Dropout(p=dropout)
66
+ self.dropout2 = nn.Dropout(p=dropout)
67
+
68
+ def forward(
69
+ self,
70
+ src: torch.Tensor, # [B, S, D]
71
+ src_key_padding_mask: Optional[torch.BoolTensor] = None, # [B, S]
72
+ ) -> torch.Tensor:
73
+ """Pre-LayerNorm 前向传播。"""
74
+ if self.pre_norm:
75
+ # 1. Self-Attention sublayer
76
+ residual = src
77
+ src = self.norm1(src)
78
+ src = self.self_attn(
79
+ src, src, src,
80
+ key_padding_mask=src_key_padding_mask,
81
+ )
82
+ src = residual + self.dropout1(src)
83
+
84
+ # 2. FFN sublayer
85
+ residual = src
86
+ src = self.norm2(src)
87
+ src = self.linear2(self.dropout(self.activation(self.linear1(src))))
88
+ src = residual + self.dropout2(src)
89
+ else:
90
+ # Post-LayerNorm (备用)
91
+ residual = src
92
+ src = self.self_attn(
93
+ src, src, src,
94
+ key_padding_mask=src_key_padding_mask,
95
+ )
96
+ src = self.norm1(residual + self.dropout1(src))
97
+
98
+ residual = src
99
+ src = self.linear2(self.dropout(self.activation(self.linear1(src))))
100
+ src = self.norm2(residual + self.dropout2(src))
101
+
102
+ return src
103
+
104
+
105
+ class TransformerEncoder(nn.Module):
106
+ """
107
+ 多层 Transformer Encoder。
108
+ """
109
+
110
+ def __init__(self, encoder_layer: TransformerEncoderLayer, num_layers: int):
111
+ super().__init__()
112
+ self.layers = nn.ModuleList(
113
+ [copy.deepcopy(encoder_layer) for _ in range(num_layers)]
114
+ )
115
+ self.num_layers = num_layers
116
+ self.norm = nn.LayerNorm(encoder_layer.self_attn.d_model)
117
+
118
+ def forward(
119
+ self,
120
+ src: torch.Tensor,
121
+ src_key_padding_mask: Optional[torch.BoolTensor] = None,
122
+ ) -> torch.Tensor:
123
+ output = src
124
+ for layer in self.layers:
125
+ output = layer(output, src_key_padding_mask=src_key_padding_mask)
126
+ output = self.norm(output)
127
+ return output
src/easytranslate/model/finetune.py CHANGED
@@ -1,116 +1,116 @@
1
- """
2
- 预训练模型微调模块 — Person B 负责实现
3
-
4
- 功能要求:
5
- 1. load_pretrained_model: 加载 NLLB / mBART 等预训练翻译模型
6
- 2. setup_lora: 配置 LoRA 参数高效微调
7
-
8
- 技术要点:
9
- - 使用 HuggingFace transformers 加载预训练模型
10
- - 使用 PEFT (Parameter-Efficient Fine-Tuning) 库配置 LoRA
11
- - 支持 NLLB-200 (Meta, 200种语言) 和 mBART-50 (Meta, 50种语言)
12
- - 冻结预训练参数,只训练 LoRA adapter
13
-
14
- 这是实验对比的关键部分:
15
- - 从头训练 vs 预训练微调
16
- - Full fine-tuning vs LoRA
17
- """
18
-
19
- from __future__ import annotations
20
-
21
- import logging
22
- from typing import Optional
23
-
24
- import torch
25
- import torch.nn as nn
26
-
27
- logger = logging.getLogger(__name__)
28
-
29
-
30
- def load_pretrained_model(
31
- model_name: str = "facebook/nllb-200-distilled-600M",
32
- src_lang: str = "eng_Latn",
33
- tgt_lang: str = "zho_Hans",
34
- device: str = "cuda",
35
- ) -> tuple:
36
- """
37
- 加载预训练翻译模型和分词器。
38
-
39
- TODO [Person B]: 实现以下逻辑:
40
- 1. 使用 AutoModelForSeq2SeqLM.from_pretrained(model_name) 加载模型
41
- 2. 使用 AutoTokenizer.from_pretrained(model_name) 加载分词器
42
- 3. 设置源语言和目标语言
43
- 4. 将模型移到指定设备
44
-
45
- 支持的模型:
46
- - facebook/nllb-200-distilled-600M (推荐,轻量级)
47
- - facebook/nllb-200-1.3B (中等规模)
48
- - facebook/mbart-large-50-many-to-many-mmt
49
-
50
- Returns:
51
- (model, tokenizer)
52
- """
53
- from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
54
-
55
- tokenizer = AutoTokenizer.from_pretrained(model_name)
56
- model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
57
-
58
- # 设置语言对
59
- if hasattr(tokenizer, "lang_code_to_id"):
60
- tokenizer.src_lang = src_lang
61
- tokenizer.tgt_lang = tgt_lang
62
- if hasattr(model.config, "forced_bos_token_id"):
63
- model.config.forced_bos_token_id = tokenizer.lang_code_to_id.get(
64
- tgt_lang, tokenizer.bos_token_id
65
- )
66
-
67
- model = model.to(device)
68
- logger.info(f"Loaded pretrained model: {model_name}")
69
- return model, tokenizer
70
-
71
-
72
- def setup_lora(
73
- model: nn.Module,
74
- r: int = 16,
75
- alpha: int = 32,
76
- dropout: float = 0.05,
77
- target_modules: Optional[list[str]] = None,
78
- ) -> nn.Module:
79
- """
80
- 为模型配置 LoRA 微调。
81
-
82
- Returns:
83
- peft_model: LoRA 包装后的模型
84
- """
85
- from peft import LoraConfig, get_peft_model, TaskType
86
-
87
- if target_modules is None:
88
- target_modules = ["q_proj", "v_proj"]
89
-
90
- config = LoraConfig(
91
- r=r,
92
- lora_alpha=alpha,
93
- lora_dropout=dropout,
94
- target_modules=target_modules,
95
- task_type=TaskType.SEQ_2_SEQ_LM,
96
- )
97
-
98
- model = get_peft_model(model, config)
99
-
100
- # 打印可训练参数信息
101
- trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
102
- total_params = sum(p.numel() for p in model.parameters())
103
- ratio = 100 * trainable_params / total_params if total_params > 0 else 0
104
- logger.info(
105
- f"LoRA setup: trainable params={trainable_params:,} "
106
- f"/ total={total_params:,} ({ratio:.4f}%)"
107
- )
108
-
109
- return model
110
-
111
-
112
- def freeze_model_except_lora(model: nn.Module):
113
- """冻结模型所有参数,只保留 LoRA 参数可训练。"""
114
- for name, param in model.named_parameters():
115
- if "lora" not in name.lower():
116
- param.requires_grad = False
 
1
+ """
2
+ 预训练模型微调模块 — Person B 负责实现
3
+
4
+ 功能要求:
5
+ 1. load_pretrained_model: 加载 NLLB / mBART 等预训练翻译模型
6
+ 2. setup_lora: 配置 LoRA 参数高效微调
7
+
8
+ 技术要点:
9
+ - 使用 HuggingFace transformers 加载预训练模型
10
+ - 使用 PEFT (Parameter-Efficient Fine-Tuning) 库配置 LoRA
11
+ - 支持 NLLB-200 (Meta, 200种语言) 和 mBART-50 (Meta, 50种语言)
12
+ - 冻结预训练参数,只训练 LoRA adapter
13
+
14
+ 这是实验对比的关键部分:
15
+ - 从头训练 vs 预训练微调
16
+ - Full fine-tuning vs LoRA
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ from typing import Optional
23
+
24
+ import torch
25
+ import torch.nn as nn
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ def load_pretrained_model(
31
+ model_name: str = "facebook/nllb-200-distilled-600M",
32
+ src_lang: str = "eng_Latn",
33
+ tgt_lang: str = "zho_Hans",
34
+ device: str = "cuda",
35
+ ) -> tuple:
36
+ """
37
+ 加载预训练翻译模型和分词器。
38
+
39
+ TODO [Person B]: 实现以下逻辑:
40
+ 1. 使用 AutoModelForSeq2SeqLM.from_pretrained(model_name) 加载模型
41
+ 2. 使用 AutoTokenizer.from_pretrained(model_name) 加载分词器
42
+ 3. 设置源语言和目标语言
43
+ 4. 将模型移到指定设备
44
+
45
+ 支持的模型:
46
+ - facebook/nllb-200-distilled-600M (推荐,轻量级)
47
+ - facebook/nllb-200-1.3B (中等规模)
48
+ - facebook/mbart-large-50-many-to-many-mmt
49
+
50
+ Returns:
51
+ (model, tokenizer)
52
+ """
53
+ from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
54
+
55
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
56
+ model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
57
+
58
+ # 设置语言对
59
+ if hasattr(tokenizer, "lang_code_to_id"):
60
+ tokenizer.src_lang = src_lang
61
+ tokenizer.tgt_lang = tgt_lang
62
+ if hasattr(model.config, "forced_bos_token_id"):
63
+ model.config.forced_bos_token_id = tokenizer.lang_code_to_id.get(
64
+ tgt_lang, tokenizer.bos_token_id
65
+ )
66
+
67
+ model = model.to(device)
68
+ logger.info(f"Loaded pretrained model: {model_name}")
69
+ return model, tokenizer
70
+
71
+
72
+ def setup_lora(
73
+ model: nn.Module,
74
+ r: int = 16,
75
+ alpha: int = 32,
76
+ dropout: float = 0.05,
77
+ target_modules: Optional[list[str]] = None,
78
+ ) -> nn.Module:
79
+ """
80
+ 为模型配置 LoRA 微调。
81
+
82
+ Returns:
83
+ peft_model: LoRA 包装后的模型
84
+ """
85
+ from peft import LoraConfig, get_peft_model, TaskType
86
+
87
+ if target_modules is None:
88
+ target_modules = ["q_proj", "v_proj"]
89
+
90
+ config = LoraConfig(
91
+ r=r,
92
+ lora_alpha=alpha,
93
+ lora_dropout=dropout,
94
+ target_modules=target_modules,
95
+ task_type=TaskType.SEQ_2_SEQ_LM,
96
+ )
97
+
98
+ model = get_peft_model(model, config)
99
+
100
+ # 打印可训练参数信息
101
+ trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
102
+ total_params = sum(p.numel() for p in model.parameters())
103
+ ratio = 100 * trainable_params / total_params if total_params > 0 else 0
104
+ logger.info(
105
+ f"LoRA setup: trainable params={trainable_params:,} "
106
+ f"/ total={total_params:,} ({ratio:.4f}%)"
107
+ )
108
+
109
+ return model
110
+
111
+
112
+ def freeze_model_except_lora(model: nn.Module):
113
+ """冻结模型所有参数,只保留 LoRA 参数可训练。"""
114
+ for name, param in model.named_parameters():
115
+ if "lora" not in name.lower():
116
+ param.requires_grad = False
src/easytranslate/model/positional.py CHANGED
@@ -1,134 +1,134 @@
1
- """
2
- 位置编码模块 — Person B 负责实现
3
-
4
- 包含:
5
- 1. SinusoidalPositionalEncoding: 经典正弦余弦位置编码
6
- 2. RotaryPositionalEmbedding (RoPE): 旋转位置编码
7
-
8
- RoPE 是当前最前沿的位置编码方案,被 LLaMA, GPT-NeoX 等大模型广泛采用。
9
- 优势: 更好地捕获相对位置信息,支持长度外推。
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- import math
15
-
16
- import torch
17
- import torch.nn as nn
18
-
19
-
20
- class SinusoidalPositionalEncoding(nn.Module):
21
- """
22
- 经典正弦余弦位置编码 (Vaswani et al., 2017)。
23
-
24
- PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
25
- PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
26
-
27
- TODO [Person B]: 实现以下内容:
28
- 1. 在 __init__ 中预计算位置编码矩阵 [max_seq_len, d_model]
29
- 2. 注册为 buffer (不参与梯度更新)
30
- 3. forward(x): 返回 x + pe[:, :x.size(1), :]
31
- """
32
-
33
- def __init__(self, d_model: int, max_seq_len: int = 5000, dropout: float = 0.1):
34
- super().__init__()
35
- self.dropout = nn.Dropout(p=dropout)
36
-
37
- pe = torch.zeros(max_seq_len, d_model)
38
- position = torch.arange(0, max_seq_len, dtype=torch.float).unsqueeze(1)
39
- div_term = torch.exp(
40
- torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)
41
- )
42
- pe[:, 0::2] = torch.sin(position * div_term)
43
- pe[:, 1::2] = torch.cos(position * div_term)
44
- pe = pe.unsqueeze(0) # [1, max_seq_len, d_model]
45
- self.register_buffer("pe", pe)
46
-
47
- def forward(self, x: torch.Tensor) -> torch.Tensor:
48
- """
49
- Args:
50
- x: [B, L, D]
51
- Returns:
52
- x + positional_encoding: [B, L, D]
53
- """
54
- x = x + self.pe[:, : x.size(1), :]
55
- return self.dropout(x)
56
-
57
-
58
- class RotaryPositionalEmbedding(nn.Module):
59
- """
60
- 旋转位置编码 (RoPE) — Su et al., 2021
61
-
62
- 核心思想: 通过旋转变换将位置信息编码到 Q, K 向量中。
63
- q' = q * cos(θ) + rotate_half(q) * sin(θ)
64
- k' = k * cos(θ) + rotate_half(k) * sin(θ)
65
-
66
- TODO [Person B]: 实现以下内容:
67
-
68
- __init__:
69
- 1. 计算频率: inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2) / dim))
70
- 2. 注册为 buffer
71
-
72
- _compute_rope(seq_len):
73
- 1. 计算 position indices: t = [0, 1, ..., seq_len-1]
74
- 2. 计算 freqs = torch.outer(t, inv_freq)
75
- 3. 构建 cos_cached, sin_cached
76
-
77
- apply_rotary_pos_emb(q, k):
78
- 1. 对 q 和 k 应用旋转变换
79
- 2. 返回旋转后的 q', k'
80
-
81
- 参考: https://arxiv.org/abs/2104.09864
82
- """
83
-
84
- def __init__(self, dim: int, max_seq_len: int = 2048, base: float = 10000.0):
85
- super().__init__()
86
- self.dim = dim
87
- self.max_seq_len = max_seq_len
88
- self.base = base
89
-
90
- inv_freq = 1.0 / (
91
- base ** (torch.arange(0, dim, 2).float() / dim)
92
- )
93
- self.register_buffer("inv_freq", inv_freq)
94
-
95
- # 预缓存 cos/sin
96
- self._cached_seq_len = 0
97
- self._cached_cos: torch.Tensor | None = None
98
- self._cached_sin: torch.Tensor | None = None
99
-
100
- def _compute_rope(self, seq_len: int, device: torch.device):
101
- if seq_len > self._cached_seq_len or self._cached_cos is None:
102
- t = torch.arange(seq_len, device=device, dtype=self.inv_freq.dtype)
103
- freqs = torch.outer(t, self.inv_freq) # [seq_len, dim//2]
104
- emb = torch.cat((freqs, freqs), dim=-1) # [seq_len, dim]
105
- self._cached_cos = emb.cos()[None, None, :, :] # [1, 1, seq_len, dim]
106
- self._cached_sin = emb.sin()[None, None, :, :] # [1, 1, seq_len, dim]
107
- self._cached_seq_len = seq_len
108
- return self._cached_cos, self._cached_sin
109
-
110
- @staticmethod
111
- def _rotate_half(x: torch.Tensor) -> torch.Tensor:
112
- """将 x 的后半部分取反并与前半部分交换。"""
113
- x1, x2 = x.chunk(2, dim=-1)
114
- return torch.cat((-x2, x1), dim=-1)
115
-
116
- def apply_rotary_pos_emb(
117
- self,
118
- q: torch.Tensor, # [B, nhead, L, d_k]
119
- k: torch.Tensor, # [B, nhead, L, d_k]
120
- ) -> tuple[torch.Tensor, torch.Tensor]:
121
- """
122
- 对 Q, K 应用 RoPE。
123
-
124
- Returns:
125
- (q_rotated, k_rotated)
126
- """
127
- cos, sin = self._compute_rope(q.size(2), q.device)
128
- q_embed = (q * cos[:, :, : q.size(2), :]) + (
129
- self._rotate_half(q) * sin[:, :, : q.size(2), :]
130
- )
131
- k_embed = (k * cos[:, :, : k.size(2), :]) + (
132
- self._rotate_half(k) * sin[:, :, : k.size(2), :]
133
- )
134
- return q_embed, k_embed
 
1
+ """
2
+ 位置编码模块 — Person B 负责实现
3
+
4
+ 包含:
5
+ 1. SinusoidalPositionalEncoding: 经典正弦余弦位置编码
6
+ 2. RotaryPositionalEmbedding (RoPE): 旋转位置编码
7
+
8
+ RoPE 是当前最前沿的位置编码方案,被 LLaMA, GPT-NeoX 等大模型广泛采用。
9
+ 优势: 更好地捕获相对位置信息,支持长度外推。
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import math
15
+
16
+ import torch
17
+ import torch.nn as nn
18
+
19
+
20
+ class SinusoidalPositionalEncoding(nn.Module):
21
+ """
22
+ 经典正弦余弦位置编码 (Vaswani et al., 2017)。
23
+
24
+ PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
25
+ PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
26
+
27
+ TODO [Person B]: 实现以下内容:
28
+ 1. 在 __init__ 中预计算位置编码矩阵 [max_seq_len, d_model]
29
+ 2. 注册为 buffer (不参与梯度更新)
30
+ 3. forward(x): 返回 x + pe[:, :x.size(1), :]
31
+ """
32
+
33
+ def __init__(self, d_model: int, max_seq_len: int = 5000, dropout: float = 0.1):
34
+ super().__init__()
35
+ self.dropout = nn.Dropout(p=dropout)
36
+
37
+ pe = torch.zeros(max_seq_len, d_model)
38
+ position = torch.arange(0, max_seq_len, dtype=torch.float).unsqueeze(1)
39
+ div_term = torch.exp(
40
+ torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)
41
+ )
42
+ pe[:, 0::2] = torch.sin(position * div_term)
43
+ pe[:, 1::2] = torch.cos(position * div_term)
44
+ pe = pe.unsqueeze(0) # [1, max_seq_len, d_model]
45
+ self.register_buffer("pe", pe)
46
+
47
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
48
+ """
49
+ Args:
50
+ x: [B, L, D]
51
+ Returns:
52
+ x + positional_encoding: [B, L, D]
53
+ """
54
+ x = x + self.pe[:, : x.size(1), :]
55
+ return self.dropout(x)
56
+
57
+
58
+ class RotaryPositionalEmbedding(nn.Module):
59
+ """
60
+ 旋转位置编码 (RoPE) — Su et al., 2021
61
+
62
+ 核心思想: 通过旋转变换将位置信息编码到 Q, K 向量中。
63
+ q' = q * cos(θ) + rotate_half(q) * sin(θ)
64
+ k' = k * cos(θ) + rotate_half(k) * sin(θ)
65
+
66
+ TODO [Person B]: 实现以下内容:
67
+
68
+ __init__:
69
+ 1. 计算频率: inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2) / dim))
70
+ 2. 注册为 buffer
71
+
72
+ _compute_rope(seq_len):
73
+ 1. 计算 position indices: t = [0, 1, ..., seq_len-1]
74
+ 2. 计算 freqs = torch.outer(t, inv_freq)
75
+ 3. 构建 cos_cached, sin_cached
76
+
77
+ apply_rotary_pos_emb(q, k):
78
+ 1. 对 q 和 k 应用旋转变换
79
+ 2. 返回旋转后的 q', k'
80
+
81
+ 参考: https://arxiv.org/abs/2104.09864
82
+ """
83
+
84
+ def __init__(self, dim: int, max_seq_len: int = 2048, base: float = 10000.0):
85
+ super().__init__()
86
+ self.dim = dim
87
+ self.max_seq_len = max_seq_len
88
+ self.base = base
89
+
90
+ inv_freq = 1.0 / (
91
+ base ** (torch.arange(0, dim, 2).float() / dim)
92
+ )
93
+ self.register_buffer("inv_freq", inv_freq)
94
+
95
+ # 预缓存 cos/sin
96
+ self._cached_seq_len = 0
97
+ self._cached_cos: torch.Tensor | None = None
98
+ self._cached_sin: torch.Tensor | None = None
99
+
100
+ def _compute_rope(self, seq_len: int, device: torch.device):
101
+ if seq_len > self._cached_seq_len or self._cached_cos is None:
102
+ t = torch.arange(seq_len, device=device, dtype=self.inv_freq.dtype)
103
+ freqs = torch.outer(t, self.inv_freq) # [seq_len, dim//2]
104
+ emb = torch.cat((freqs, freqs), dim=-1) # [seq_len, dim]
105
+ self._cached_cos = emb.cos()[None, None, :, :] # [1, 1, seq_len, dim]
106
+ self._cached_sin = emb.sin()[None, None, :, :] # [1, 1, seq_len, dim]
107
+ self._cached_seq_len = seq_len
108
+ return self._cached_cos, self._cached_sin
109
+
110
+ @staticmethod
111
+ def _rotate_half(x: torch.Tensor) -> torch.Tensor:
112
+ """将 x 的后半部分取反并与前半部分交换。"""
113
+ x1, x2 = x.chunk(2, dim=-1)
114
+ return torch.cat((-x2, x1), dim=-1)
115
+
116
+ def apply_rotary_pos_emb(
117
+ self,
118
+ q: torch.Tensor, # [B, nhead, L, d_k]
119
+ k: torch.Tensor, # [B, nhead, L, d_k]
120
+ ) -> tuple[torch.Tensor, torch.Tensor]:
121
+ """
122
+ 对 Q, K 应用 RoPE。
123
+
124
+ Returns:
125
+ (q_rotated, k_rotated)
126
+ """
127
+ cos, sin = self._compute_rope(q.size(2), q.device)
128
+ q_embed = (q * cos[:, :, : q.size(2), :]) + (
129
+ self._rotate_half(q) * sin[:, :, : q.size(2), :]
130
+ )
131
+ k_embed = (k * cos[:, :, : k.size(2), :]) + (
132
+ self._rotate_half(k) * sin[:, :, : k.size(2), :]
133
+ )
134
+ return q_embed, k_embed
src/easytranslate/model/transformer.py CHANGED
@@ -1,256 +1,256 @@
1
- """
2
- Transformer 翻译模型主体 — Person B 负责实现
3
-
4
- 这是整个模型的核心文件,定义 Encoder-Decoder 架构。
5
-
6
- 架构设计:
7
- - Pre-LayerNorm Transformer (训练更稳定)
8
- - 可选 Flash Attention 2 (加速注意力计算)
9
- - 可选 RoPE 旋转位置编码 (替代传统正弦位置编码)
10
- - 共享 Embedding 权重 (可选)
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- import logging
16
- import math
17
- from typing import Optional
18
-
19
- import torch
20
- import torch.nn as nn
21
- import torch.nn.functional as F
22
-
23
- from easytranslate.model.encoder import TransformerEncoder, TransformerEncoderLayer
24
- from easytranslate.model.decoder import TransformerDecoder, TransformerDecoderLayer
25
- from easytranslate.model.positional import SinusoidalPositionalEncoding, RotaryPositionalEmbedding
26
-
27
- logger = logging.getLogger(__name__)
28
-
29
-
30
- class TransformerTranslationModel(nn.Module):
31
- """
32
- 完整的 Transformer 英中翻译模型。
33
-
34
- 架构:
35
- Source Embedding + Positional Encoding
36
- → Transformer Encoder (N layers)
37
- → Transformer Decoder (N layers)
38
- → Linear Projection → Softmax
39
-
40
- TODO [Person B]: 实现以下内容:
41
-
42
- __init__:
43
- 1. 源语言 Embedding: nn.Embedding(src_vocab_size, d_model)
44
- 2. 目标语言 Embedding: nn.Embedding(tgt_vocab_size, d_model)
45
- 3. 位置编码: SinusoidalPositionalEncoding 或 RotaryPositionalEmbedding
46
- 4. Transformer Encoder
47
- 5. Transformer Decoder
48
- 6. 输出投影层: nn.Linear(d_model, tgt_vocab_size)
49
- 7. (可选) 共享 target embedding 和 output projection 的权重
50
- 8. 调用 _init_weights() 初始化参数
51
-
52
- forward:
53
- 1. 源语言 embedding + 位置编码 → encoder_input
54
- 2. 目标语言 embedding + 位置编码 → decoder_input
55
- 3. 生成 masks (src_key_padding_mask, tgt_key_padding_mask, tgt_mask)
56
- 4. encoder_output = encoder(encoder_input, src_key_padding_mask)
57
- 5. decoder_output = decoder(decoder_input, encoder_output, masks...)
58
- 6. logits = output_projection(decoder_output)
59
- 7. 返回 logits [B, T, tgt_vocab_size]
60
- """
61
-
62
- def __init__(
63
- self,
64
- src_vocab_size: int,
65
- tgt_vocab_size: int,
66
- d_model: int = 512,
67
- nhead: int = 8,
68
- num_encoder_layers: int = 6,
69
- num_decoder_layers: int = 6,
70
- dim_feedforward: int = 2048,
71
- dropout: float = 0.1,
72
- activation: str = "gelu",
73
- max_seq_len: int = 512,
74
- use_flash_attention: bool = True,
75
- use_rotary_embedding: bool = True,
76
- pre_norm: bool = True,
77
- pad_id: int = 0,
78
- share_embedding: bool = False,
79
- ):
80
- super().__init__()
81
- self.d_model = d_model
82
- self.pad_id = pad_id
83
- self.use_rotary_embedding = use_rotary_embedding
84
- self.max_seq_len = max_seq_len
85
-
86
- # Embeddings
87
- self.src_embed = nn.Embedding(src_vocab_size, d_model)
88
- self.tgt_embed = nn.Embedding(tgt_vocab_size, d_model)
89
- self.embed_scale = math.sqrt(d_model)
90
-
91
- # Positional encoding
92
- if use_rotary_embedding:
93
- # RoPE 在 attention 内部应用到 Q/K,不需要额外的位置编码层
94
- self.pos_encoding: Optional[nn.Module] = None
95
- rope = RotaryPositionalEmbedding(
96
- dim=d_model // nhead,
97
- max_seq_len=max_seq_len,
98
- )
99
- else:
100
- self.pos_encoding = SinusoidalPositionalEncoding(
101
- d_model=d_model,
102
- max_seq_len=max_seq_len,
103
- dropout=dropout,
104
- )
105
- rope = None
106
-
107
- # Encoder
108
- encoder_layer = TransformerEncoderLayer(
109
- d_model=d_model,
110
- nhead=nhead,
111
- dim_feedforward=dim_feedforward,
112
- dropout=dropout,
113
- activation=activation,
114
- use_flash_attention=use_flash_attention,
115
- use_rotary_embedding=use_rotary_embedding,
116
- pre_norm=pre_norm,
117
- )
118
- self.encoder = TransformerEncoder(encoder_layer, num_encoder_layers)
119
-
120
- # Decoder
121
- decoder_layer = TransformerDecoderLayer(
122
- d_model=d_model,
123
- nhead=nhead,
124
- dim_feedforward=dim_feedforward,
125
- dropout=dropout,
126
- activation=activation,
127
- use_flash_attention=use_flash_attention,
128
- use_rotary_embedding=use_rotary_embedding,
129
- pre_norm=pre_norm,
130
- )
131
- self.decoder = TransformerDecoder(decoder_layer, num_decoder_layers)
132
-
133
- # 将 RoPE 注入到 encoder/decoder 的 attention 模块中
134
- if rope is not None:
135
- for layer in self.encoder.layers:
136
- layer.self_attn.rope = rope
137
- for layer in self.decoder.layers:
138
- layer.self_attn.rope = rope
139
- # cross-attention 不使用 RoPE
140
- layer.multihead_attn.rope = None
141
-
142
- # Output projection
143
- self.output_projection = nn.Linear(d_model, tgt_vocab_size)
144
-
145
- # 可选: 共享目标语言 embedding 和输出投影权重
146
- self.share_embedding = share_embedding
147
- if share_embedding:
148
- self.output_projection.weight = self.tgt_embed.weight
149
-
150
- self._init_weights()
151
-
152
- def _init_weights(self):
153
- """参数初始化。"""
154
- for p in self.parameters():
155
- if p.dim() > 1:
156
- nn.init.xavier_uniform_(p)
157
- for module in self.modules():
158
- if isinstance(module, nn.Embedding):
159
- nn.init.normal_(module.weight, mean=0, std=self.d_model ** -0.5)
160
- elif isinstance(module, nn.LayerNorm):
161
- nn.init.ones_(module.weight)
162
- nn.init.zeros_(module.bias)
163
-
164
- def _generate_square_subsequent_mask(self, sz: int, device: torch.device) -> torch.Tensor:
165
- """生成因果注意力掩码 (causal mask)。"""
166
- mask = torch.triu(torch.ones(sz, sz, device=device), diagonal=1)
167
- mask = mask.masked_fill(mask == 1, float("-inf"))
168
- return mask
169
-
170
- def forward(
171
- self,
172
- src_ids: torch.Tensor, # [B, S]
173
- tgt_input_ids: torch.Tensor, # [B, T]
174
- src_padding_mask: Optional[torch.BoolTensor] = None, # [B, S]
175
- tgt_padding_mask: Optional[torch.BoolTensor] = None, # [B, T]
176
- ) -> torch.Tensor:
177
- """
178
- 前向传播。
179
-
180
- Returns:
181
- logits: [B, T, tgt_vocab_size]
182
- """
183
- # 1. Embedding
184
- src_emb = self.src_embed(src_ids) * self.embed_scale # [B, S, D]
185
- tgt_emb = self.tgt_embed(tgt_input_ids) * self.embed_scale # [B, T, D]
186
-
187
- # 2. Positional encoding (如果不使用 RoPE)
188
- if self.pos_encoding is not None:
189
- src_emb = self.pos_encoding(src_emb)
190
- tgt_emb = self.pos_encoding(tgt_emb)
191
-
192
- # 3. Masks
193
- if src_padding_mask is None:
194
- src_padding_mask = src_ids.eq(self.pad_id)
195
- if tgt_padding_mask is None:
196
- tgt_padding_mask = tgt_input_ids.eq(self.pad_id)
197
-
198
- tgt_seq_len = tgt_input_ids.size(1)
199
- tgt_mask = self._generate_square_subsequent_mask(tgt_seq_len, tgt_input_ids.device)
200
-
201
- # 4. Encoder
202
- encoder_output = self.encoder(src_emb, src_key_padding_mask=src_padding_mask)
203
-
204
- # 5. Decoder
205
- decoder_output = self.decoder(
206
- tgt_emb,
207
- encoder_output,
208
- tgt_mask=tgt_mask,
209
- memory_key_padding_mask=src_padding_mask,
210
- tgt_key_padding_mask=tgt_padding_mask,
211
- )
212
-
213
- # 6. Output projection
214
- logits = self.output_projection(decoder_output)
215
- return logits
216
-
217
- @torch.no_grad()
218
- def encode(self, src_ids: torch.Tensor, src_padding_mask: Optional[torch.BoolTensor] = None) -> torch.Tensor:
219
- """仅编码(用于推理时复用 encoder 输出)。"""
220
- src_emb = self.src_embed(src_ids) * self.embed_scale
221
- if self.pos_encoding is not None:
222
- src_emb = self.pos_encoding(src_emb)
223
- if src_padding_mask is None:
224
- src_padding_mask = src_ids.eq(self.pad_id)
225
- encoder_output = self.encoder(src_emb, src_key_padding_mask=src_padding_mask)
226
- return encoder_output
227
-
228
- @torch.no_grad()
229
- def decode_step(
230
- self,
231
- tgt_input_ids: torch.Tensor,
232
- encoder_output: torch.Tensor,
233
- src_padding_mask: Optional[torch.BoolTensor] = None,
234
- ) -> torch.Tensor:
235
- """解码一步(用于自回归推理)。"""
236
- tgt_emb = self.tgt_embed(tgt_input_ids) * self.embed_scale
237
- if self.pos_encoding is not None:
238
- tgt_emb = self.pos_encoding(tgt_emb)
239
-
240
- tgt_seq_len = tgt_input_ids.size(1)
241
- tgt_mask = self._generate_square_subsequent_mask(tgt_seq_len, tgt_input_ids.device)
242
-
243
- decoder_output = self.decoder(
244
- tgt_emb,
245
- encoder_output,
246
- tgt_mask=tgt_mask,
247
- memory_key_padding_mask=src_padding_mask,
248
- )
249
-
250
- # 取最后一个 token 的 logits
251
- logits = self.output_projection(decoder_output[:, -1, :])
252
- return logits
253
-
254
- def count_parameters(self) -> int:
255
- """返回可训练参数数量。"""
256
- return sum(p.numel() for p in self.parameters() if p.requires_grad)
 
1
+ """
2
+ Transformer 翻译模型主体 — Person B 负责实现
3
+
4
+ 这是整个模型的核心文件,定义 Encoder-Decoder 架构。
5
+
6
+ 架构设计:
7
+ - Pre-LayerNorm Transformer (训练更稳定)
8
+ - 可选 Flash Attention 2 (加速注意力计算)
9
+ - 可选 RoPE 旋转位置编码 (替代传统正弦位置编码)
10
+ - 共享 Embedding 权重 (可选)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ import math
17
+ from typing import Optional
18
+
19
+ import torch
20
+ import torch.nn as nn
21
+ import torch.nn.functional as F
22
+
23
+ from easytranslate.model.encoder import TransformerEncoder, TransformerEncoderLayer
24
+ from easytranslate.model.decoder import TransformerDecoder, TransformerDecoderLayer
25
+ from easytranslate.model.positional import SinusoidalPositionalEncoding, RotaryPositionalEmbedding
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class TransformerTranslationModel(nn.Module):
31
+ """
32
+ 完整的 Transformer 英中翻译模型。
33
+
34
+ 架构:
35
+ Source Embedding + Positional Encoding
36
+ → Transformer Encoder (N layers)
37
+ → Transformer Decoder (N layers)
38
+ → Linear Projection → Softmax
39
+
40
+ TODO [Person B]: 实现以下内容:
41
+
42
+ __init__:
43
+ 1. 源语言 Embedding: nn.Embedding(src_vocab_size, d_model)
44
+ 2. 目标语言 Embedding: nn.Embedding(tgt_vocab_size, d_model)
45
+ 3. 位置编码: SinusoidalPositionalEncoding 或 RotaryPositionalEmbedding
46
+ 4. Transformer Encoder
47
+ 5. Transformer Decoder
48
+ 6. 输出投影层: nn.Linear(d_model, tgt_vocab_size)
49
+ 7. (可选) 共享 target embedding 和 output projection 的权重
50
+ 8. 调用 _init_weights() 初始化参数
51
+
52
+ forward:
53
+ 1. 源语言 embedding + 位置编码 → encoder_input
54
+ 2. 目标语言 embedding + 位置编码 → decoder_input
55
+ 3. 生成 masks (src_key_padding_mask, tgt_key_padding_mask, tgt_mask)
56
+ 4. encoder_output = encoder(encoder_input, src_key_padding_mask)
57
+ 5. decoder_output = decoder(decoder_input, encoder_output, masks...)
58
+ 6. logits = output_projection(decoder_output)
59
+ 7. 返回 logits [B, T, tgt_vocab_size]
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ src_vocab_size: int,
65
+ tgt_vocab_size: int,
66
+ d_model: int = 512,
67
+ nhead: int = 8,
68
+ num_encoder_layers: int = 6,
69
+ num_decoder_layers: int = 6,
70
+ dim_feedforward: int = 2048,
71
+ dropout: float = 0.1,
72
+ activation: str = "gelu",
73
+ max_seq_len: int = 512,
74
+ use_flash_attention: bool = True,
75
+ use_rotary_embedding: bool = True,
76
+ pre_norm: bool = True,
77
+ pad_id: int = 0,
78
+ share_embedding: bool = False,
79
+ ):
80
+ super().__init__()
81
+ self.d_model = d_model
82
+ self.pad_id = pad_id
83
+ self.use_rotary_embedding = use_rotary_embedding
84
+ self.max_seq_len = max_seq_len
85
+
86
+ # Embeddings
87
+ self.src_embed = nn.Embedding(src_vocab_size, d_model)
88
+ self.tgt_embed = nn.Embedding(tgt_vocab_size, d_model)
89
+ self.embed_scale = math.sqrt(d_model)
90
+
91
+ # Positional encoding
92
+ if use_rotary_embedding:
93
+ # RoPE 在 attention 内部应用到 Q/K,不需要额外的位置编码层
94
+ self.pos_encoding: Optional[nn.Module] = None
95
+ rope = RotaryPositionalEmbedding(
96
+ dim=d_model // nhead,
97
+ max_seq_len=max_seq_len,
98
+ )
99
+ else:
100
+ self.pos_encoding = SinusoidalPositionalEncoding(
101
+ d_model=d_model,
102
+ max_seq_len=max_seq_len,
103
+ dropout=dropout,
104
+ )
105
+ rope = None
106
+
107
+ # Encoder
108
+ encoder_layer = TransformerEncoderLayer(
109
+ d_model=d_model,
110
+ nhead=nhead,
111
+ dim_feedforward=dim_feedforward,
112
+ dropout=dropout,
113
+ activation=activation,
114
+ use_flash_attention=use_flash_attention,
115
+ use_rotary_embedding=use_rotary_embedding,
116
+ pre_norm=pre_norm,
117
+ )
118
+ self.encoder = TransformerEncoder(encoder_layer, num_encoder_layers)
119
+
120
+ # Decoder
121
+ decoder_layer = TransformerDecoderLayer(
122
+ d_model=d_model,
123
+ nhead=nhead,
124
+ dim_feedforward=dim_feedforward,
125
+ dropout=dropout,
126
+ activation=activation,
127
+ use_flash_attention=use_flash_attention,
128
+ use_rotary_embedding=use_rotary_embedding,
129
+ pre_norm=pre_norm,
130
+ )
131
+ self.decoder = TransformerDecoder(decoder_layer, num_decoder_layers)
132
+
133
+ # 将 RoPE 注入到 encoder/decoder 的 attention 模块中
134
+ if rope is not None:
135
+ for layer in self.encoder.layers:
136
+ layer.self_attn.rope = rope
137
+ for layer in self.decoder.layers:
138
+ layer.self_attn.rope = rope
139
+ # cross-attention 不使用 RoPE
140
+ layer.multihead_attn.rope = None
141
+
142
+ # Output projection
143
+ self.output_projection = nn.Linear(d_model, tgt_vocab_size)
144
+
145
+ # 可选: 共享目标语言 embedding 和输出投影权重
146
+ self.share_embedding = share_embedding
147
+ if share_embedding:
148
+ self.output_projection.weight = self.tgt_embed.weight
149
+
150
+ self._init_weights()
151
+
152
+ def _init_weights(self):
153
+ """参数初始化。"""
154
+ for p in self.parameters():
155
+ if p.dim() > 1:
156
+ nn.init.xavier_uniform_(p)
157
+ for module in self.modules():
158
+ if isinstance(module, nn.Embedding):
159
+ nn.init.normal_(module.weight, mean=0, std=self.d_model ** -0.5)
160
+ elif isinstance(module, nn.LayerNorm):
161
+ nn.init.ones_(module.weight)
162
+ nn.init.zeros_(module.bias)
163
+
164
+ def _generate_square_subsequent_mask(self, sz: int, device: torch.device) -> torch.Tensor:
165
+ """生成因果注意力掩码 (causal mask)。"""
166
+ mask = torch.triu(torch.ones(sz, sz, device=device), diagonal=1)
167
+ mask = mask.masked_fill(mask == 1, float("-inf"))
168
+ return mask
169
+
170
+ def forward(
171
+ self,
172
+ src_ids: torch.Tensor, # [B, S]
173
+ tgt_input_ids: torch.Tensor, # [B, T]
174
+ src_padding_mask: Optional[torch.BoolTensor] = None, # [B, S]
175
+ tgt_padding_mask: Optional[torch.BoolTensor] = None, # [B, T]
176
+ ) -> torch.Tensor:
177
+ """
178
+ 前向传播。
179
+
180
+ Returns:
181
+ logits: [B, T, tgt_vocab_size]
182
+ """
183
+ # 1. Embedding
184
+ src_emb = self.src_embed(src_ids) * self.embed_scale # [B, S, D]
185
+ tgt_emb = self.tgt_embed(tgt_input_ids) * self.embed_scale # [B, T, D]
186
+
187
+ # 2. Positional encoding (如果不使用 RoPE)
188
+ if self.pos_encoding is not None:
189
+ src_emb = self.pos_encoding(src_emb)
190
+ tgt_emb = self.pos_encoding(tgt_emb)
191
+
192
+ # 3. Masks
193
+ if src_padding_mask is None:
194
+ src_padding_mask = src_ids.eq(self.pad_id)
195
+ if tgt_padding_mask is None:
196
+ tgt_padding_mask = tgt_input_ids.eq(self.pad_id)
197
+
198
+ tgt_seq_len = tgt_input_ids.size(1)
199
+ tgt_mask = self._generate_square_subsequent_mask(tgt_seq_len, tgt_input_ids.device)
200
+
201
+ # 4. Encoder
202
+ encoder_output = self.encoder(src_emb, src_key_padding_mask=src_padding_mask)
203
+
204
+ # 5. Decoder
205
+ decoder_output = self.decoder(
206
+ tgt_emb,
207
+ encoder_output,
208
+ tgt_mask=tgt_mask,
209
+ memory_key_padding_mask=src_padding_mask,
210
+ tgt_key_padding_mask=tgt_padding_mask,
211
+ )
212
+
213
+ # 6. Output projection
214
+ logits = self.output_projection(decoder_output)
215
+ return logits
216
+
217
+ @torch.no_grad()
218
+ def encode(self, src_ids: torch.Tensor, src_padding_mask: Optional[torch.BoolTensor] = None) -> torch.Tensor:
219
+ """仅编码(用于推理时复用 encoder 输出)。"""
220
+ src_emb = self.src_embed(src_ids) * self.embed_scale
221
+ if self.pos_encoding is not None:
222
+ src_emb = self.pos_encoding(src_emb)
223
+ if src_padding_mask is None:
224
+ src_padding_mask = src_ids.eq(self.pad_id)
225
+ encoder_output = self.encoder(src_emb, src_key_padding_mask=src_padding_mask)
226
+ return encoder_output
227
+
228
+ @torch.no_grad()
229
+ def decode_step(
230
+ self,
231
+ tgt_input_ids: torch.Tensor,
232
+ encoder_output: torch.Tensor,
233
+ src_padding_mask: Optional[torch.BoolTensor] = None,
234
+ ) -> torch.Tensor:
235
+ """解码一步(用于自回归推理)。"""
236
+ tgt_emb = self.tgt_embed(tgt_input_ids) * self.embed_scale
237
+ if self.pos_encoding is not None:
238
+ tgt_emb = self.pos_encoding(tgt_emb)
239
+
240
+ tgt_seq_len = tgt_input_ids.size(1)
241
+ tgt_mask = self._generate_square_subsequent_mask(tgt_seq_len, tgt_input_ids.device)
242
+
243
+ decoder_output = self.decoder(
244
+ tgt_emb,
245
+ encoder_output,
246
+ tgt_mask=tgt_mask,
247
+ memory_key_padding_mask=src_padding_mask,
248
+ )
249
+
250
+ # 取最后一个 token 的 logits
251
+ logits = self.output_projection(decoder_output[:, -1, :])
252
+ return logits
253
+
254
+ def count_parameters(self) -> int:
255
+ """返回可训练参数数量。"""
256
+ return sum(p.numel() for p in self.parameters() if p.requires_grad)
src/easytranslate/training/__init__.py CHANGED
@@ -1,12 +1,12 @@
1
- """训练模块 (Person C 负责)"""
2
-
3
- from easytranslate.training.trainer import Trainer
4
- from easytranslate.training.optimizer import build_optimizer, build_scheduler
5
- from easytranslate.training.loss import LabelSmoothedCrossEntropyLoss
6
-
7
- __all__ = [
8
- "Trainer",
9
- "build_optimizer",
10
- "build_scheduler",
11
- "LabelSmoothedCrossEntropyLoss",
12
- ]
 
1
+ """训练模块 (Person C 负责)"""
2
+
3
+ from easytranslate.training.trainer import Trainer
4
+ from easytranslate.training.optimizer import build_optimizer, build_scheduler
5
+ from easytranslate.training.loss import LabelSmoothedCrossEntropyLoss
6
+
7
+ __all__ = [
8
+ "Trainer",
9
+ "build_optimizer",
10
+ "build_scheduler",
11
+ "LabelSmoothedCrossEntropyLoss",
12
+ ]
src/easytranslate/training/loss.py CHANGED
@@ -1,57 +1,57 @@
1
- """
2
- 损失函数模块 — Person C 负责实现
3
-
4
- 功能要求:
5
- 1. LabelSmoothedCrossEntropyLoss: 带标签平滑的交叉熵损失
6
-
7
- 技术要点:
8
- - 标签平滑 (Label Smoothing) 是 Transformer 训练的标准技巧
9
- - smoothing=0.1 意味着将 10% 的概率均匀分配给非目标类
10
- - 需要忽略 padding token 的损失
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- import torch
16
- import torch.nn as nn
17
- import torch.nn.functional as F
18
-
19
-
20
- class LabelSmoothedCrossEntropyLoss(nn.Module):
21
- """
22
- 带标签平滑的交叉熵损失。
23
-
24
- TODO [Person C]: 实现以下逻辑:
25
-
26
- __init__:
27
- 1. 保存 smoothing 系数和 pad_id
28
-
29
- forward(logits, targets):
30
- 1. logits: [B, T, V] — 模型输出
31
- 2. targets: [B, T] — 目标 token ids
32
- 3. 展平为 [B*T, V] 和 [B*T]
33
- 4. 创建 smooth label distribution:
34
- - target token 概率 = 1 - smoothing
35
- - 其他 token 概率 = smoothing / (V - 1)
36
- 5. 计算 KL 散度作为损失
37
- 6. 忽略 pad_id 位置的损失
38
- 7. 返回平均损失
39
-
40
- 参考: "Rethinking the Inception Architecture for Computer Vision" (Szegedy et al.)
41
- """
42
-
43
- def __init__(self, smoothing: float = 0.1, pad_id: int = 0):
44
- super().__init__()
45
- self.smoothing = smoothing
46
- self.pad_id = pad_id
47
- raise NotImplementedError("TODO: Person C 实现 LabelSmoothedCrossEntropyLoss.__init__")
48
-
49
- def forward(self, logits: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
50
- """
51
- Args:
52
- logits: [B, T, V]
53
- targets: [B, T]
54
- Returns:
55
- loss: scalar
56
- """
57
- raise NotImplementedError("TODO: Person C 实现 LabelSmoothedCrossEntropyLoss.forward")
 
1
+ """
2
+ 损失函数模块 — Person C 负责实现
3
+
4
+ 功能要求:
5
+ 1. LabelSmoothedCrossEntropyLoss: 带标签平滑的交叉熵损失
6
+
7
+ 技术要点:
8
+ - 标签平滑 (Label Smoothing) 是 Transformer 训练的标准技巧
9
+ - smoothing=0.1 意味着将 10% 的概率均匀分配给非目标类
10
+ - 需要忽略 padding token 的损失
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import torch
16
+ import torch.nn as nn
17
+ import torch.nn.functional as F
18
+
19
+
20
+ class LabelSmoothedCrossEntropyLoss(nn.Module):
21
+ """
22
+ 带标签平滑的交叉熵损失。
23
+
24
+ TODO [Person C]: 实现以下逻辑:
25
+
26
+ __init__:
27
+ 1. 保存 smoothing 系数和 pad_id
28
+
29
+ forward(logits, targets):
30
+ 1. logits: [B, T, V] — 模型输出
31
+ 2. targets: [B, T] — 目标 token ids
32
+ 3. 展平为 [B*T, V] 和 [B*T]
33
+ 4. 创建 smooth label distribution:
34
+ - target token 概率 = 1 - smoothing
35
+ - 其他 token 概率 = smoothing / (V - 1)
36
+ 5. 计算 KL 散度作为损失
37
+ 6. 忽略 pad_id 位置的损失
38
+ 7. 返回平均损失
39
+
40
+ 参考: "Rethinking the Inception Architecture for Computer Vision" (Szegedy et al.)
41
+ """
42
+
43
+ def __init__(self, smoothing: float = 0.1, pad_id: int = 0):
44
+ super().__init__()
45
+ self.smoothing = smoothing
46
+ self.pad_id = pad_id
47
+ raise NotImplementedError("TODO: Person C 实现 LabelSmoothedCrossEntropyLoss.__init__")
48
+
49
+ def forward(self, logits: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
50
+ """
51
+ Args:
52
+ logits: [B, T, V]
53
+ targets: [B, T]
54
+ Returns:
55
+ loss: scalar
56
+ """
57
+ raise NotImplementedError("TODO: Person C 实现 LabelSmoothedCrossEntropyLoss.forward")
src/easytranslate/training/optimizer.py CHANGED
@@ -1,76 +1,76 @@
1
- """
2
- 优化器与学习率调度器 — Person C 负责实现
3
-
4
- 功能要求:
5
- 1. build_optimizer: 根据配置创建优化器
6
- 2. build_scheduler: 根据配置创建学习率调度器
7
- 3. InverseSqrtScheduler: 自定义 inverse square root 调度器
8
-
9
- 技术要点:
10
- - AdamW 是 Transformer 训练的标准优化器
11
- - Cosine with warmup 是目前最流行的调度策略
12
- - Inverse sqrt 是经典 Transformer 论文使用的调度策略
13
- """
14
-
15
- from __future__ import annotations
16
-
17
- import math
18
- from typing import Optional
19
-
20
- import torch
21
- from torch.optim import Adam, AdamW
22
- from torch.optim.lr_scheduler import LambdaLR
23
-
24
-
25
- def build_optimizer(model, config: dict) -> torch.optim.Optimizer:
26
- """
27
- 根据配置创建优化器。
28
-
29
- TODO [Person C]: 实现以下逻辑:
30
- 1. 从 config 中读取 optimizer type, lr, weight_decay, betas, eps
31
- 2. 根据 type 创建 Adam / AdamW / Adafactor
32
- 3. (可选) 对不同参数组设置不同学习率:
33
- - embedding 层可以用较小的 lr
34
- - LayerNorm 的 bias 不加 weight_decay
35
- """
36
- raise NotImplementedError("TODO: Person C 实现 build_optimizer")
37
-
38
-
39
- def build_scheduler(
40
- optimizer: torch.optim.Optimizer,
41
- config: dict,
42
- num_training_steps: Optional[int] = None,
43
- ) -> torch.optim.lr_scheduler._LRScheduler:
44
- """
45
- 根据配置创建学习率调度器。
46
-
47
- TODO [Person C]: 实现以下逻辑:
48
- 1. 从 config 中读取 scheduler type, warmup_steps, min_lr
49
- 2. type == "cosine_with_warmup":
50
- 使用 get_cosine_schedule_with_warmup (transformers 库)
51
- 3. type == "inverse_sqrt":
52
- 实现经典的 lr = d_model^(-0.5) * min(step^(-0.5), step * warmup^(-1.5))
53
- 4. type == "linear":
54
- 使用 get_linear_schedule_with_warmup
55
-
56
- 参考: Attention Is All You Need, Section 5.3
57
- """
58
- raise NotImplementedError("TODO: Person C 实现 build_scheduler")
59
-
60
-
61
- class InverseSqrtScheduler(LambdaLR):
62
- """
63
- Inverse Square Root 学习率调度器。
64
-
65
- lr = base_lr * min(step^{-0.5}, step * warmup_steps^{-1.5})
66
-
67
- 这是原始 Transformer 论文使用的调度策略。
68
-
69
- TODO [Person C]:
70
- 1. 实现 lr_lambda 函数
71
- 2. warmup 阶段线性增长
72
- 3. warmup 后按 step^{-0.5} 衰减
73
- """
74
-
75
- def __init__(self, optimizer, warmup_steps: int = 4000):
76
- raise NotImplementedError("TODO: Person C 实现 InverseSqrtScheduler.__init__")
 
1
+ """
2
+ 优化器与学习率调度器 — Person C 负责实现
3
+
4
+ 功能要求:
5
+ 1. build_optimizer: 根据配置创建优化器
6
+ 2. build_scheduler: 根据配置创建学习率调度器
7
+ 3. InverseSqrtScheduler: 自定义 inverse square root 调度器
8
+
9
+ 技术要点:
10
+ - AdamW 是 Transformer 训练的标准优化器
11
+ - Cosine with warmup 是目前最流行的调度策略
12
+ - Inverse sqrt 是经典 Transformer 论文使用的调度策略
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ from typing import Optional
19
+
20
+ import torch
21
+ from torch.optim import Adam, AdamW
22
+ from torch.optim.lr_scheduler import LambdaLR
23
+
24
+
25
+ def build_optimizer(model, config: dict) -> torch.optim.Optimizer:
26
+ """
27
+ 根据配置创建优化器。
28
+
29
+ TODO [Person C]: 实现以下逻辑:
30
+ 1. 从 config 中读取 optimizer type, lr, weight_decay, betas, eps
31
+ 2. 根据 type 创建 Adam / AdamW / Adafactor
32
+ 3. (可选) 对不同参数组设置不同学习率:
33
+ - embedding 层可以用较小的 lr
34
+ - LayerNorm 的 bias 不加 weight_decay
35
+ """
36
+ raise NotImplementedError("TODO: Person C 实现 build_optimizer")
37
+
38
+
39
+ def build_scheduler(
40
+ optimizer: torch.optim.Optimizer,
41
+ config: dict,
42
+ num_training_steps: Optional[int] = None,
43
+ ) -> torch.optim.lr_scheduler._LRScheduler:
44
+ """
45
+ 根据配置创建学习率调度器。
46
+
47
+ TODO [Person C]: 实现以下逻辑:
48
+ 1. 从 config 中读取 scheduler type, warmup_steps, min_lr
49
+ 2. type == "cosine_with_warmup":
50
+ 使用 get_cosine_schedule_with_warmup (transformers 库)
51
+ 3. type == "inverse_sqrt":
52
+ 实现经典的 lr = d_model^(-0.5) * min(step^(-0.5), step * warmup^(-1.5))
53
+ 4. type == "linear":
54
+ 使用 get_linear_schedule_with_warmup
55
+
56
+ 参考: Attention Is All You Need, Section 5.3
57
+ """
58
+ raise NotImplementedError("TODO: Person C 实现 build_scheduler")
59
+
60
+
61
+ class InverseSqrtScheduler(LambdaLR):
62
+ """
63
+ Inverse Square Root 学习率调度器。
64
+
65
+ lr = base_lr * min(step^{-0.5}, step * warmup_steps^{-1.5})
66
+
67
+ 这是原始 Transformer 论文使用的调度策略。
68
+
69
+ TODO [Person C]:
70
+ 1. 实现 lr_lambda 函数
71
+ 2. warmup 阶段线性增长
72
+ 3. warmup 后按 step^{-0.5} 衰减
73
+ """
74
+
75
+ def __init__(self, optimizer, warmup_steps: int = 4000):
76
+ raise NotImplementedError("TODO: Person C 实现 InverseSqrtScheduler.__init__")
src/easytranslate/training/trainer.py CHANGED
@@ -1,186 +1,186 @@
1
- """
2
- 训练器模块 — Person C 负责实现
3
-
4
- 这是训练的核心控制器,负责:
5
- 1. 训练循环 (training loop)
6
- 2. 验证循环 (validation loop)
7
- 3. 检查点保存/加载
8
- 4. 日志记录
9
- 5. 分布式训练支持
10
- 6. 混合精度训练
11
- 7. 梯度累积
12
- 8. 早停
13
- """
14
-
15
- from __future__ import annotations
16
-
17
- import logging
18
- import os
19
- import time
20
- from pathlib import Path
21
- from typing import Optional
22
-
23
- import torch
24
- import torch.nn as nn
25
- from torch.utils.data import DataLoader
26
- from tqdm import tqdm
27
-
28
- logger = logging.getLogger(__name__)
29
-
30
-
31
- class Trainer:
32
- """
33
- 翻译模型训练器。
34
-
35
- TODO [Person C]: 实现以下所有方法。
36
-
37
- 使用方法:
38
- trainer = Trainer(model, train_loader, val_loader, config)
39
- trainer.train()
40
- """
41
-
42
- def __init__(
43
- self,
44
- model: nn.Module,
45
- train_loader: DataLoader,
46
- val_loader: DataLoader,
47
- config: dict,
48
- optimizer=None,
49
- scheduler=None,
50
- criterion=None,
51
- evaluator=None,
52
- ):
53
- """
54
- TODO [Person C]: 初始化训练器:
55
- 1. 保存模型、数据加载器、配置
56
- 2. 如果 optimizer 为 None,调用 build_optimizer 创建
57
- 3. 如果 scheduler 为 None,调用 build_scheduler 创建
58
- 4. 如果 criterion 为 None,创建 LabelSmoothedCrossEntropyLoss
59
- 5. 设置混合精度: torch.amp.GradScaler (如果配置了 fp16/bf16)
60
- 6. 设置分布式训练: 根据配置初始化 DDP/FSDP/DeepSpeed
61
- 7. 初始化日志记录器 (TensorBoard / WandB)
62
- 8. 初始化训练状态 (epoch, step, best_metric)
63
- """
64
- self.model = model
65
- self.train_loader = train_loader
66
- self.val_loader = val_loader
67
- self.config = config
68
- raise NotImplementedError("TODO: Person C 实现 Trainer.__init__")
69
-
70
- def train(self):
71
- """
72
- 主训练循环。
73
-
74
- TODO [Person C]: 实现以下逻辑:
75
- 1. for epoch in range(start_epoch, num_epochs):
76
- 2. train_loss = self._train_one_epoch(epoch)
77
- 3. val_metrics = self._validate(epoch)
78
- 4. self._log_metrics(epoch, train_loss, val_metrics)
79
- 5. self._save_checkpoint(epoch, val_metrics)
80
- 6. if self._should_early_stop(val_metrics): break
81
- 7. self._log_final_results()
82
- """
83
- raise NotImplementedError("TODO: Person C 实现 train")
84
-
85
- def _train_one_epoch(self, epoch: int) -> float:
86
- """
87
- 训练一个 epoch。
88
-
89
- TODO [Person C]: 实现以下逻辑:
90
- 1. model.train()
91
- 2. 遍历 train_loader:
92
- a. 将 batch 移到设备
93
- b. 混合精度上下文: with torch.amp.autocast('cuda'):
94
- c. logits = model(src_ids, tgt_input_ids, masks...)
95
- d. loss = criterion(logits, labels)
96
- e. loss = loss / gradient_accumulation_steps
97
- f. scaler.scale(loss).backward()
98
- g. 每 gradient_accumulation_steps 步:
99
- - scaler.unscale_(optimizer)
100
- - torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
101
- - scaler.step(optimizer)
102
- - scaler.update()
103
- - scheduler.step()
104
- - optimizer.zero_grad()
105
- h. 记录 loss, lr 等指标
106
- 3. 返回平均训练 loss
107
- """
108
- raise NotImplementedError("TODO: Person C 实现 _train_one_epoch")
109
-
110
- def _validate(self, epoch: int) -> dict:
111
- """
112
- 验证。
113
-
114
- TODO [Person C]: 实现以下逻辑:
115
- 1. model.eval()
116
- 2. with torch.no_grad():
117
- 3. 遍历 val_loader,计算 loss
118
- 4. 每 N 步调用 evaluator 计算 BLEU 等指标
119
- 5. 返回 {"val_loss": ..., "bleu": ..., "comet": ...}
120
- """
121
- raise NotImplementedError("TODO: Person C 实现 _validate")
122
-
123
- def _save_checkpoint(self, epoch: int, metrics: dict):
124
- """
125
- 保存检查点。
126
-
127
- TODO [Person C]: 实现以下逻辑:
128
- 1. 构建 checkpoint dict:
129
- {
130
- "epoch": epoch,
131
- "step": self.global_step,
132
- "model_state_dict": model.state_dict(),
133
- "optimizer_state_dict": optimizer.state_dict(),
134
- "scheduler_state_dict": scheduler.state_dict(),
135
- "metrics": metrics,
136
- "config": config,
137
- }
138
- 2. 保存到 checkpoint_dir/checkpoint_epoch_{epoch}.pt
139
- 3. 如果是最佳模型,额外保存为 best_model.pt
140
- 4. 清理旧的检查点 (保留最近 max_checkpoints 个)
141
- """
142
- raise NotImplementedError("TODO: Person C 实现 _save_checkpoint")
143
-
144
- def _load_checkpoint(self, checkpoint_path: str):
145
- """
146
- 加载检查点继续训练。
147
-
148
- TODO [Person C]:
149
- 1. torch.load(checkpoint_path)
150
- 2. 恢复 model, optimizer, scheduler 状态
151
- 3. 恢复 epoch, step 计数器
152
- """
153
- raise NotImplementedError("TODO: Person C 实现 _load_checkpoint")
154
-
155
- def _should_early_stop(self, metrics: dict) -> bool:
156
- """
157
- 判断是否应该早停。
158
-
159
- TODO [Person C]:
160
- 1. 比较当前指标与最佳指标
161
- 2. 如果连续 patience 个 epoch 没有改善,返回 True
162
- """
163
- raise NotImplementedError("TODO: Person C 实现 _should_early_stop")
164
-
165
- def _log_metrics(self, epoch: int, train_loss: float, val_metrics: dict):
166
- """
167
- 记录训练指标到 TensorBoard / WandB。
168
-
169
- TODO [Person C]:
170
- 1. 使用 self.logger 记录 train_loss, val_loss, bleu, lr 等
171
- 2. 打印到控制台 (使用 rich 库的 table 格式)
172
- """
173
- raise NotImplementedError("TODO: Person C 实现 _log_metrics")
174
-
175
- def _setup_distributed(self):
176
- """
177
- 设置分布式训练。
178
-
179
- TODO [Person C]: 根据 config["training"]["distributed"]["strategy"]:
180
- 1. "ddp": 使用 torch.nn.parallel.DistributedDataParallel
181
- 2. "fsdp": 使用 torch.distributed.fsdp.FullyShardedDataParallel
182
- 3. "deepspeed": 使用 deepspeed.initialize()
183
-
184
- 也可以使用 HuggingFace Accelerate 统一处理。
185
- """
186
- raise NotImplementedError("TODO: Person C 实现 _setup_distributed")
 
1
+ """
2
+ 训练器模块 — Person C 负责实现
3
+
4
+ 这是训练的核心控制器,负责:
5
+ 1. 训练循环 (training loop)
6
+ 2. 验证循环 (validation loop)
7
+ 3. 检查点保存/加载
8
+ 4. 日志记录
9
+ 5. 分布式训练支持
10
+ 6. 混合精度训练
11
+ 7. 梯度累积
12
+ 8. 早停
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import os
19
+ import time
20
+ from pathlib import Path
21
+ from typing import Optional
22
+
23
+ import torch
24
+ import torch.nn as nn
25
+ from torch.utils.data import DataLoader
26
+ from tqdm import tqdm
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class Trainer:
32
+ """
33
+ 翻译模型训练器。
34
+
35
+ TODO [Person C]: 实现以下所有方法。
36
+
37
+ 使用方法:
38
+ trainer = Trainer(model, train_loader, val_loader, config)
39
+ trainer.train()
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ model: nn.Module,
45
+ train_loader: DataLoader,
46
+ val_loader: DataLoader,
47
+ config: dict,
48
+ optimizer=None,
49
+ scheduler=None,
50
+ criterion=None,
51
+ evaluator=None,
52
+ ):
53
+ """
54
+ TODO [Person C]: 初始化训练器:
55
+ 1. 保存模型、数据加载器、配置
56
+ 2. 如果 optimizer 为 None,调用 build_optimizer 创建
57
+ 3. 如果 scheduler 为 None,调用 build_scheduler 创建
58
+ 4. 如果 criterion 为 None,创建 LabelSmoothedCrossEntropyLoss
59
+ 5. 设置混合精度: torch.amp.GradScaler (如果配置了 fp16/bf16)
60
+ 6. 设置分布式训练: 根据配置初始化 DDP/FSDP/DeepSpeed
61
+ 7. 初始化日志记录器 (TensorBoard / WandB)
62
+ 8. 初始化训练状态 (epoch, step, best_metric)
63
+ """
64
+ self.model = model
65
+ self.train_loader = train_loader
66
+ self.val_loader = val_loader
67
+ self.config = config
68
+ raise NotImplementedError("TODO: Person C 实现 Trainer.__init__")
69
+
70
+ def train(self):
71
+ """
72
+ 主训练循环。
73
+
74
+ TODO [Person C]: 实现以下逻辑:
75
+ 1. for epoch in range(start_epoch, num_epochs):
76
+ 2. train_loss = self._train_one_epoch(epoch)
77
+ 3. val_metrics = self._validate(epoch)
78
+ 4. self._log_metrics(epoch, train_loss, val_metrics)
79
+ 5. self._save_checkpoint(epoch, val_metrics)
80
+ 6. if self._should_early_stop(val_metrics): break
81
+ 7. self._log_final_results()
82
+ """
83
+ raise NotImplementedError("TODO: Person C 实现 train")
84
+
85
+ def _train_one_epoch(self, epoch: int) -> float:
86
+ """
87
+ 训练一个 epoch。
88
+
89
+ TODO [Person C]: 实现以下逻辑:
90
+ 1. model.train()
91
+ 2. 遍历 train_loader:
92
+ a. 将 batch 移到设备
93
+ b. 混合精度上下文: with torch.amp.autocast('cuda'):
94
+ c. logits = model(src_ids, tgt_input_ids, masks...)
95
+ d. loss = criterion(logits, labels)
96
+ e. loss = loss / gradient_accumulation_steps
97
+ f. scaler.scale(loss).backward()
98
+ g. 每 gradient_accumulation_steps 步:
99
+ - scaler.unscale_(optimizer)
100
+ - torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
101
+ - scaler.step(optimizer)
102
+ - scaler.update()
103
+ - scheduler.step()
104
+ - optimizer.zero_grad()
105
+ h. 记录 loss, lr 等指标
106
+ 3. 返回平均训练 loss
107
+ """
108
+ raise NotImplementedError("TODO: Person C 实现 _train_one_epoch")
109
+
110
+ def _validate(self, epoch: int) -> dict:
111
+ """
112
+ 验证。
113
+
114
+ TODO [Person C]: 实现以下逻辑:
115
+ 1. model.eval()
116
+ 2. with torch.no_grad():
117
+ 3. 遍历 val_loader,计算 loss
118
+ 4. 每 N 步调用 evaluator 计算 BLEU 等指标
119
+ 5. 返回 {"val_loss": ..., "bleu": ..., "comet": ...}
120
+ """
121
+ raise NotImplementedError("TODO: Person C 实现 _validate")
122
+
123
+ def _save_checkpoint(self, epoch: int, metrics: dict):
124
+ """
125
+ 保存检查点。
126
+
127
+ TODO [Person C]: 实现以下逻辑:
128
+ 1. 构建 checkpoint dict:
129
+ {
130
+ "epoch": epoch,
131
+ "step": self.global_step,
132
+ "model_state_dict": model.state_dict(),
133
+ "optimizer_state_dict": optimizer.state_dict(),
134
+ "scheduler_state_dict": scheduler.state_dict(),
135
+ "metrics": metrics,
136
+ "config": config,
137
+ }
138
+ 2. 保存到 checkpoint_dir/checkpoint_epoch_{epoch}.pt
139
+ 3. 如果是最佳模型,额外保存为 best_model.pt
140
+ 4. 清理旧的检查点 (保留最近 max_checkpoints 个)
141
+ """
142
+ raise NotImplementedError("TODO: Person C 实现 _save_checkpoint")
143
+
144
+ def _load_checkpoint(self, checkpoint_path: str):
145
+ """
146
+ 加载检查点继续训练。
147
+
148
+ TODO [Person C]:
149
+ 1. torch.load(checkpoint_path)
150
+ 2. 恢复 model, optimizer, scheduler 状态
151
+ 3. 恢复 epoch, step 计数器
152
+ """
153
+ raise NotImplementedError("TODO: Person C 实现 _load_checkpoint")
154
+
155
+ def _should_early_stop(self, metrics: dict) -> bool:
156
+ """
157
+ 判断是否应该早停。
158
+
159
+ TODO [Person C]:
160
+ 1. 比较当前指标与最佳指标
161
+ 2. 如果连续 patience 个 epoch 没有改善,返回 True
162
+ """
163
+ raise NotImplementedError("TODO: Person C 实现 _should_early_stop")
164
+
165
+ def _log_metrics(self, epoch: int, train_loss: float, val_metrics: dict):
166
+ """
167
+ 记录训练指标到 TensorBoard / WandB。
168
+
169
+ TODO [Person C]:
170
+ 1. 使用 self.logger 记录 train_loss, val_loss, bleu, lr 等
171
+ 2. 打印到控制台 (使用 rich 库的 table 格式)
172
+ """
173
+ raise NotImplementedError("TODO: Person C 实现 _log_metrics")
174
+
175
+ def _setup_distributed(self):
176
+ """
177
+ 设置分布式训练。
178
+
179
+ TODO [Person C]: 根据 config["training"]["distributed"]["strategy"]:
180
+ 1. "ddp": 使用 torch.nn.parallel.DistributedDataParallel
181
+ 2. "fsdp": 使用 torch.distributed.fsdp.FullyShardedDataParallel
182
+ 3. "deepspeed": 使用 deepspeed.initialize()
183
+
184
+ 也可以使用 HuggingFace Accelerate 统一处理。
185
+ """
186
+ raise NotImplementedError("TODO: Person C 实现 _setup_distributed")
src/easytranslate/utils/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
- """工具模块"""
2
-
3
- from easytranslate.utils.config import load_config, merge_configs
4
- from easytranslate.utils.seed import set_seed
5
- from easytranslate.utils.logging import setup_logging
6
-
7
- __all__ = ["load_config", "merge_configs", "set_seed", "setup_logging"]
 
1
+ """工具模块"""
2
+
3
+ from easytranslate.utils.config import load_config, merge_configs
4
+ from easytranslate.utils.seed import set_seed
5
+ from easytranslate.utils.logging import setup_logging
6
+
7
+ __all__ = ["load_config", "merge_configs", "set_seed", "setup_logging"]
src/easytranslate/utils/config.py CHANGED
@@ -1,51 +1,51 @@
1
- """
2
- 配置管理模块 — 公共模块 (Person E 可以先完成)
3
-
4
- 功能: 加载 YAML 配置,支持命令行覆盖和多配置合并。
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import logging
10
- from pathlib import Path
11
- from typing import Any, Optional
12
-
13
- from omegaconf import OmegaConf, DictConfig
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
-
18
- def load_config(config_path: str | Path) -> DictConfig:
19
- """
20
- 加载 YAML 配置文件。
21
-
22
- TODO [Person E]: 实现以下逻辑:
23
- 1. 使用 OmegaConf.load(config_path) 加载配置
24
- 2. 验证配置完整性
25
- 3. 返回 DictConfig 对象
26
- """
27
- raise NotImplementedError("TODO: Person E 实现 load_config")
28
-
29
-
30
- def merge_configs(base_config: DictConfig, override_config: DictConfig) -> DictConfig:
31
- """
32
- 合并配置 (override 覆盖 base)。
33
-
34
- TODO [Person E]:
35
- 使用 OmegaConf.merge(base_config, override_config)
36
- """
37
- raise NotImplementedError("TODO: Person E 实现 merge_configs")
38
-
39
-
40
- def config_from_cli(config_path: str, cli_args: list[str]) -> DictConfig:
41
- """
42
- 从配置文件 + 命令行参数构建最终配置。
43
-
44
- TODO [Person E]:
45
- 1. 加载 config_path
46
- 2. 使用 OmegaConf.from_cli(cli_args) 解析命令行参数
47
- 3. 合并并返回
48
-
49
- 使用方式: python train.py --config configs/default.yaml training.lr=1e-4 model.nhead=16
50
- """
51
- raise NotImplementedError("TODO: Person E 实现 config_from_cli")
 
1
+ """
2
+ 配置管理模块 — 公共模块 (Person E 可以先完成)
3
+
4
+ 功能: 加载 YAML 配置,支持命令行覆盖和多配置合并。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Any, Optional
12
+
13
+ from omegaconf import OmegaConf, DictConfig
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def load_config(config_path: str | Path) -> DictConfig:
19
+ """
20
+ 加载 YAML 配置文件。
21
+
22
+ TODO [Person E]: 实现以下逻辑:
23
+ 1. 使用 OmegaConf.load(config_path) 加载配置
24
+ 2. 验证配置完整性
25
+ 3. 返回 DictConfig 对象
26
+ """
27
+ raise NotImplementedError("TODO: Person E 实现 load_config")
28
+
29
+
30
+ def merge_configs(base_config: DictConfig, override_config: DictConfig) -> DictConfig:
31
+ """
32
+ 合并配置 (override 覆盖 base)。
33
+
34
+ TODO [Person E]:
35
+ 使用 OmegaConf.merge(base_config, override_config)
36
+ """
37
+ raise NotImplementedError("TODO: Person E 实现 merge_configs")
38
+
39
+
40
+ def config_from_cli(config_path: str, cli_args: list[str]) -> DictConfig:
41
+ """
42
+ 从配置文件 + 命令行参数构建最终配置。
43
+
44
+ TODO [Person E]:
45
+ 1. 加载 config_path
46
+ 2. 使用 OmegaConf.from_cli(cli_args) 解析命令行参数
47
+ 3. 合并并返回
48
+
49
+ 使用方式: python train.py --config configs/default.yaml training.lr=1e-4 model.nhead=16
50
+ """
51
+ raise NotImplementedError("TODO: Person E 实现 config_from_cli")
src/easytranslate/utils/logging.py CHANGED
@@ -1,26 +1,26 @@
1
- """
2
- 日志管理模块 — 公共模块
3
- """
4
-
5
- import logging
6
- import sys
7
- from pathlib import Path
8
- from typing import Optional
9
-
10
-
11
- def setup_logging(
12
- log_dir: Optional[str] = None,
13
- level: int = logging.INFO,
14
- log_file: Optional[str] = None,
15
- ):
16
- """
17
- 配置日志系统。
18
-
19
- TODO [Person E]: 实现以下逻辑:
20
- 1. 创建 root logger
21
- 2. 设置 StreamHandler (控制台输出)
22
- 3. (可选) 设置 FileHandler (文件输出)
23
- 4. 使用 rich.logging.RichHandler 美化控制台输出
24
- 5. 设置日志格式: [时间] [级别] [模块名] 消息
25
- """
26
- raise NotImplementedError("TODO: Person E 实现 setup_logging")
 
1
+ """
2
+ 日志管理模块 — 公共模块
3
+ """
4
+
5
+ import logging
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+
11
+ def setup_logging(
12
+ log_dir: Optional[str] = None,
13
+ level: int = logging.INFO,
14
+ log_file: Optional[str] = None,
15
+ ):
16
+ """
17
+ 配置日志系统。
18
+
19
+ TODO [Person E]: 实现以下逻辑:
20
+ 1. 创建 root logger
21
+ 2. 设置 StreamHandler (控制台输出)
22
+ 3. (可选) 设置 FileHandler (文件输出)
23
+ 4. 使用 rich.logging.RichHandler 美化控制台输出
24
+ 5. 设置日志格式: [时间] [级别] [模块名] 消息
25
+ """
26
+ raise NotImplementedError("TODO: Person E 实现 setup_logging")
src/easytranslate/utils/seed.py CHANGED
@@ -1,23 +1,23 @@
1
- """
2
- 随机种子管理模块 — 公共模块
3
- """
4
-
5
- import random
6
-
7
- import numpy as np
8
- import torch
9
-
10
-
11
- def set_seed(seed: int = 42):
12
- """
13
- 设置全局随机种子以确保实验可复现。
14
-
15
- TODO [Person E]:
16
- 1. random.seed(seed)
17
- 2. np.random.seed(seed)
18
- 3. torch.manual_seed(seed)
19
- 4. torch.cuda.manual_seed_all(seed)
20
- 5. torch.backends.cudnn.deterministic = True
21
- 6. torch.backends.cudnn.benchmark = False
22
- """
23
- raise NotImplementedError("TODO: Person E 实现 set_seed")
 
1
+ """
2
+ 随机种子管理模块 — 公共模块
3
+ """
4
+
5
+ import random
6
+
7
+ import numpy as np
8
+ import torch
9
+
10
+
11
+ def set_seed(seed: int = 42):
12
+ """
13
+ 设置全局随机种子以确保实验可复现。
14
+
15
+ TODO [Person E]:
16
+ 1. random.seed(seed)
17
+ 2. np.random.seed(seed)
18
+ 3. torch.manual_seed(seed)
19
+ 4. torch.cuda.manual_seed_all(seed)
20
+ 5. torch.backends.cudnn.deterministic = True
21
+ 6. torch.backends.cudnn.benchmark = False
22
+ """
23
+ raise NotImplementedError("TODO: Person E 实现 set_seed")
tests/test_data.py CHANGED
@@ -1,143 +1,143 @@
1
- """
2
- 单元测试 — 数据模块 (Person A 实现后需通过)
3
- """
4
-
5
- import pytest
6
- import torch
7
-
8
- from easytranslate.data import (
9
- DynamicBatchSampler,
10
- TokenizerWrapper,
11
- TranslationCollator,
12
- TranslationDataset,
13
- clean_text,
14
- deduplicate_pairs,
15
- filter_by_length,
16
- preprocess_pipeline,
17
- train_bpe_tokenizer,
18
- )
19
-
20
-
21
- @pytest.fixture()
22
- def tiny_tokenizer() -> TokenizerWrapper:
23
- texts = [
24
- "Hello world",
25
- "Machine translation is useful",
26
- "I love natural language processing",
27
- "你好 世界",
28
- "机器翻译 很 有用",
29
- "我 喜欢 自然语言处理",
30
- ]
31
- return train_bpe_tokenizer(texts, vocab_size=80, min_frequency=1)
32
-
33
-
34
- class TestTranslationDataset:
35
- """测试 TranslationDataset 类。"""
36
-
37
- def test_dataset_length(self, tiny_tokenizer):
38
- """测试数据集长度。"""
39
- dataset = TranslationDataset(["Hello world"], ["你好 世界"], tokenizer=tiny_tokenizer)
40
- assert len(dataset) == 1
41
-
42
- def test_getitem_returns_correct_keys(self, tiny_tokenizer):
43
- """测试 __getitem__ 返回正确的字段。"""
44
- dataset = TranslationDataset(["Hello world"], ["你好 世界"], tokenizer=tiny_tokenizer)
45
- item = dataset[0]
46
- assert set(item) == {"src_ids", "tgt_input_ids", "labels", "src_len", "tgt_len"}
47
-
48
- def test_getitem_tensor_types(self, tiny_tokenizer):
49
- """测试返回的 tensor 类型正确。"""
50
- dataset = TranslationDataset(["Hello world"], ["你好 世界"], tokenizer=tiny_tokenizer)
51
- item = dataset[0]
52
- assert item["src_ids"].dtype == torch.long
53
- assert item["tgt_input_ids"].dtype == torch.long
54
- assert item["labels"].dtype == torch.long
55
-
56
- def test_src_tgt_mismatch_raises(self):
57
- """测试源目标数量不匹配时抛出异常。"""
58
- with pytest.raises(ValueError):
59
- TranslationDataset(["a", "b"], ["甲"], tokenizer=object())
60
-
61
-
62
- class TestTokenizer:
63
- """测试分词器。"""
64
-
65
- def test_bpe_train_and_encode(self, tiny_tokenizer):
66
- """测试 BPE 训练和编码。"""
67
- ids = tiny_tokenizer.encode("Hello world", add_special_tokens=True)
68
- assert len(ids) >= 3
69
- assert ids[0] == tiny_tokenizer.bos_token_id
70
- assert ids[-1] == tiny_tokenizer.eos_token_id
71
-
72
- def test_encode_decode_roundtrip(self, tiny_tokenizer):
73
- """测试编码-解码往返一致性。"""
74
- ids = tiny_tokenizer.encode("Hello world")
75
- decoded = tiny_tokenizer.decode(ids)
76
- assert "Hello" in decoded
77
- assert "world" in decoded
78
-
79
- def test_special_tokens(self, tiny_tokenizer):
80
- """测试特殊 token 正确。"""
81
- assert tiny_tokenizer.pad_token_id == 0
82
- assert tiny_tokenizer.unk_token_id == 1
83
- assert tiny_tokenizer.bos_token_id == 2
84
- assert tiny_tokenizer.eos_token_id == 3
85
-
86
-
87
- class TestPreprocessing:
88
- """测试预处理。"""
89
-
90
- def test_clean_text_unicode(self):
91
- """测试 Unicode 标准化。"""
92
- assert clean_text("ABC\u200b 123") == "ABC 123"
93
-
94
- def test_filter_by_length(self):
95
- """测试按长度过滤。"""
96
- assert filter_by_length("hello world", "你好世界", max_src_len=10, max_tgt_len=10)
97
- assert not filter_by_length("hello " * 300, "你好", max_src_len=256)
98
-
99
- def test_deduplicate(self):
100
- """测试去重。"""
101
- pairs = deduplicate_pairs([("a", "甲"), ("a", "甲"), ("b", "乙")])
102
- assert pairs == [("a", "甲"), ("b", "乙")]
103
-
104
- def test_preprocess_pipeline(self):
105
- """测试完整预处理流水线。"""
106
- src, tgt = preprocess_pipeline([" Hello world ", "", "Hello world"], [" 你好 世界 ", "空", "你好 世界"])
107
- assert src == ["Hello world"]
108
- assert tgt == ["你好 世界"]
109
-
110
-
111
- class TestCollator:
112
- """测试数据整理器。"""
113
-
114
- def test_padding(self, tiny_tokenizer):
115
- """测试 padding 正确。"""
116
- dataset = TranslationDataset(
117
- ["Hello world", "Machine translation is useful"],
118
- ["你好 世界", "机器翻译 很 有用"],
119
- tokenizer=tiny_tokenizer,
120
- )
121
- batch = TranslationCollator(pad_token_id=tiny_tokenizer.pad_token_id)([dataset[0], dataset[1]])
122
- assert batch["src_ids"].ndim == 2
123
- assert batch["tgt_input_ids"].ndim == 2
124
- assert batch["labels"].shape == batch["tgt_input_ids"].shape
125
-
126
- def test_attention_mask(self, tiny_tokenizer):
127
- """测试 attention mask 正确。"""
128
- dataset = TranslationDataset(
129
- ["Hello", "Machine translation is useful"],
130
- ["你好", "机器翻译 很 有用"],
131
- tokenizer=tiny_tokenizer,
132
- )
133
- batch = TranslationCollator(pad_token_id=tiny_tokenizer.pad_token_id)([dataset[0], dataset[1]])
134
- assert batch["src_padding_mask"].dtype == torch.bool
135
- assert torch.equal(batch["src_attention_mask"], (~batch["src_padding_mask"]).long())
136
-
137
- def test_dynamic_batch_sampler(self):
138
- """测试动态 batch 不超过 token 预算。"""
139
- sampler = DynamicBatchSampler([5, 6, 20, 21], max_tokens_per_batch=24, shuffle=False)
140
- batches = list(sampler)
141
- assert batches
142
- for batch in batches:
143
- assert max([5, 6, 20, 21][idx] for idx in batch) * len(batch) <= 24
 
1
+ """
2
+ 单元测试 — 数据模块 (Person A 实现后需通过)
3
+ """
4
+
5
+ import pytest
6
+ import torch
7
+
8
+ from easytranslate.data import (
9
+ DynamicBatchSampler,
10
+ TokenizerWrapper,
11
+ TranslationCollator,
12
+ TranslationDataset,
13
+ clean_text,
14
+ deduplicate_pairs,
15
+ filter_by_length,
16
+ preprocess_pipeline,
17
+ train_bpe_tokenizer,
18
+ )
19
+
20
+
21
+ @pytest.fixture()
22
+ def tiny_tokenizer() -> TokenizerWrapper:
23
+ texts = [
24
+ "Hello world",
25
+ "Machine translation is useful",
26
+ "I love natural language processing",
27
+ "你好 世界",
28
+ "机器翻译 很 有用",
29
+ "我 喜欢 自然语言处理",
30
+ ]
31
+ return train_bpe_tokenizer(texts, vocab_size=80, min_frequency=1)
32
+
33
+
34
+ class TestTranslationDataset:
35
+ """测试 TranslationDataset 类。"""
36
+
37
+ def test_dataset_length(self, tiny_tokenizer):
38
+ """测试数据集长度。"""
39
+ dataset = TranslationDataset(["Hello world"], ["你好 世界"], tokenizer=tiny_tokenizer)
40
+ assert len(dataset) == 1
41
+
42
+ def test_getitem_returns_correct_keys(self, tiny_tokenizer):
43
+ """测试 __getitem__ 返回正确的字段。"""
44
+ dataset = TranslationDataset(["Hello world"], ["你好 世界"], tokenizer=tiny_tokenizer)
45
+ item = dataset[0]
46
+ assert set(item) == {"src_ids", "tgt_input_ids", "labels", "src_len", "tgt_len"}
47
+
48
+ def test_getitem_tensor_types(self, tiny_tokenizer):
49
+ """测试返回的 tensor 类型正确。"""
50
+ dataset = TranslationDataset(["Hello world"], ["你好 世界"], tokenizer=tiny_tokenizer)
51
+ item = dataset[0]
52
+ assert item["src_ids"].dtype == torch.long
53
+ assert item["tgt_input_ids"].dtype == torch.long
54
+ assert item["labels"].dtype == torch.long
55
+
56
+ def test_src_tgt_mismatch_raises(self):
57
+ """测试源目标数量不匹配时抛出异常。"""
58
+ with pytest.raises(ValueError):
59
+ TranslationDataset(["a", "b"], ["甲"], tokenizer=object())
60
+
61
+
62
+ class TestTokenizer:
63
+ """测试分词器。"""
64
+
65
+ def test_bpe_train_and_encode(self, tiny_tokenizer):
66
+ """测试 BPE 训练和编码。"""
67
+ ids = tiny_tokenizer.encode("Hello world", add_special_tokens=True)
68
+ assert len(ids) >= 3
69
+ assert ids[0] == tiny_tokenizer.bos_token_id
70
+ assert ids[-1] == tiny_tokenizer.eos_token_id
71
+
72
+ def test_encode_decode_roundtrip(self, tiny_tokenizer):
73
+ """测试编码-解码往返一致性。"""
74
+ ids = tiny_tokenizer.encode("Hello world")
75
+ decoded = tiny_tokenizer.decode(ids)
76
+ assert "Hello" in decoded
77
+ assert "world" in decoded
78
+
79
+ def test_special_tokens(self, tiny_tokenizer):
80
+ """测试特殊 token 正确。"""
81
+ assert tiny_tokenizer.pad_token_id == 0
82
+ assert tiny_tokenizer.unk_token_id == 1
83
+ assert tiny_tokenizer.bos_token_id == 2
84
+ assert tiny_tokenizer.eos_token_id == 3
85
+
86
+
87
+ class TestPreprocessing:
88
+ """测试预处理。"""
89
+
90
+ def test_clean_text_unicode(self):
91
+ """测试 Unicode 标准化。"""
92
+ assert clean_text("ABC\u200b 123") == "ABC 123"
93
+
94
+ def test_filter_by_length(self):
95
+ """测试按长度过滤。"""
96
+ assert filter_by_length("hello world", "你好世界", max_src_len=10, max_tgt_len=10)
97
+ assert not filter_by_length("hello " * 300, "你好", max_src_len=256)
98
+
99
+ def test_deduplicate(self):
100
+ """测试去重。"""
101
+ pairs = deduplicate_pairs([("a", "甲"), ("a", "甲"), ("b", "乙")])
102
+ assert pairs == [("a", "甲"), ("b", "乙")]
103
+
104
+ def test_preprocess_pipeline(self):
105
+ """测试完整预处理流水线。"""
106
+ src, tgt = preprocess_pipeline([" Hello world ", "", "Hello world"], [" 你好 世界 ", "空", "你好 世界"])
107
+ assert src == ["Hello world"]
108
+ assert tgt == ["你好 世界"]
109
+
110
+
111
+ class TestCollator:
112
+ """测试数据整理器。"""
113
+
114
+ def test_padding(self, tiny_tokenizer):
115
+ """测试 padding 正确。"""
116
+ dataset = TranslationDataset(
117
+ ["Hello world", "Machine translation is useful"],
118
+ ["你好 世界", "机器翻译 很 有用"],
119
+ tokenizer=tiny_tokenizer,
120
+ )
121
+ batch = TranslationCollator(pad_token_id=tiny_tokenizer.pad_token_id)([dataset[0], dataset[1]])
122
+ assert batch["src_ids"].ndim == 2
123
+ assert batch["tgt_input_ids"].ndim == 2
124
+ assert batch["labels"].shape == batch["tgt_input_ids"].shape
125
+
126
+ def test_attention_mask(self, tiny_tokenizer):
127
+ """测试 attention mask 正确。"""
128
+ dataset = TranslationDataset(
129
+ ["Hello", "Machine translation is useful"],
130
+ ["你好", "机器翻译 很 有用"],
131
+ tokenizer=tiny_tokenizer,
132
+ )
133
+ batch = TranslationCollator(pad_token_id=tiny_tokenizer.pad_token_id)([dataset[0], dataset[1]])
134
+ assert batch["src_padding_mask"].dtype == torch.bool
135
+ assert torch.equal(batch["src_attention_mask"], (~batch["src_padding_mask"]).long())
136
+
137
+ def test_dynamic_batch_sampler(self):
138
+ """测试动态 batch 不超过 token 预算。"""
139
+ sampler = DynamicBatchSampler([5, 6, 20, 21], max_tokens_per_batch=24, shuffle=False)
140
+ batches = list(sampler)
141
+ assert batches
142
+ for batch in batches:
143
+ assert max([5, 6, 20, 21][idx] for idx in batch) * len(batch) <= 24
tests/test_evaluation.py CHANGED
@@ -1,54 +1,54 @@
1
- """
2
- 单元测试 — 评估模块 (Person D 实现后需通过)
3
- """
4
-
5
- import pytest
6
- import torch
7
-
8
-
9
- class TestMetrics:
10
- """测试评估指标。"""
11
-
12
- def test_bleu_perfect(self):
13
- """完美翻译的 BLEU 应接近 100。"""
14
- pass
15
-
16
- def test_bleu_zero(self):
17
- """完全不相关的翻译 BLEU 应接近 0。"""
18
- pass
19
-
20
- def test_chrf_score(self):
21
- """测试 chrF 分数范围合理。"""
22
- pass
23
-
24
-
25
- class TestDecoding:
26
- """测试解码策略。"""
27
-
28
- def test_greedy_decode_shape(self):
29
- """测试贪心解码输出维度。"""
30
- pass
31
-
32
- def test_greedy_produces_eos(self):
33
- """测试贪心解码能生成 EOS。"""
34
- pass
35
-
36
- def test_beam_search_better_than_greedy(self):
37
- """束搜索质量应不低于贪心。"""
38
- pass
39
-
40
- def test_no_repeat_ngram(self):
41
- """测试 n-gram 不重复约束。"""
42
- pass
43
-
44
-
45
- class TestEvaluator:
46
- """测试评估器。"""
47
-
48
- def test_translate_single(self):
49
- """测试单句翻译。"""
50
- pass
51
-
52
- def test_translate_batch(self):
53
- """测试批量翻译。"""
54
- pass
 
1
+ """
2
+ 单元测试 — 评估模块 (Person D 实现后需通过)
3
+ """
4
+
5
+ import pytest
6
+ import torch
7
+
8
+
9
+ class TestMetrics:
10
+ """测试评估指标。"""
11
+
12
+ def test_bleu_perfect(self):
13
+ """完美翻译的 BLEU 应接近 100。"""
14
+ pass
15
+
16
+ def test_bleu_zero(self):
17
+ """完全不相关的翻译 BLEU 应接近 0。"""
18
+ pass
19
+
20
+ def test_chrf_score(self):
21
+ """测试 chrF 分数范围合理。"""
22
+ pass
23
+
24
+
25
+ class TestDecoding:
26
+ """测试解码策略。"""
27
+
28
+ def test_greedy_decode_shape(self):
29
+ """测试贪心解码输出维度。"""
30
+ pass
31
+
32
+ def test_greedy_produces_eos(self):
33
+ """测试贪心解码能生成 EOS。"""
34
+ pass
35
+
36
+ def test_beam_search_better_than_greedy(self):
37
+ """束搜索质量应不低于贪心。"""
38
+ pass
39
+
40
+ def test_no_repeat_ngram(self):
41
+ """测试 n-gram 不重复约束。"""
42
+ pass
43
+
44
+
45
+ class TestEvaluator:
46
+ """测试评估器。"""
47
+
48
+ def test_translate_single(self):
49
+ """测试单句翻译。"""
50
+ pass
51
+
52
+ def test_translate_batch(self):
53
+ """测试批量翻译。"""
54
+ pass
tests/test_model.py CHANGED
@@ -1,60 +1,60 @@
1
- """
2
- 单元测试 — 模型模块 (Person B 实现后需通过)
3
- """
4
-
5
- import pytest
6
- import torch
7
-
8
-
9
- class TestMultiHeadAttention:
10
- """测试多头注意力。"""
11
-
12
- def test_output_shape(self):
13
- """测试输出维度正确。"""
14
- # TODO: 创建 MHA,输入 [2, 10, 512],验证输出 [2, 10, 512]
15
- pass
16
-
17
- def test_padding_mask(self):
18
- """测试 padding mask 效果。"""
19
- pass
20
-
21
-
22
- class TestPositionalEncoding:
23
- """测试位置编码。"""
24
-
25
- def test_sinusoidal_shape(self):
26
- """测试正弦位置编码输出维度。"""
27
- pass
28
-
29
- def test_rope_shape(self):
30
- """测试 RoPE 输出维度。"""
31
- pass
32
-
33
- def test_rope_relative_distance(self):
34
- """测试 RoPE 捕获相对距离。"""
35
- pass
36
-
37
-
38
- class TestTransformerModel:
39
- """测试完整 Transformer 模型。"""
40
-
41
- def test_forward_shape(self):
42
- """测试前向传播输出维度。"""
43
- # TODO: 构建小模型,输入 mock 数据,验证 logits shape [B, T, V]
44
- pass
45
-
46
- def test_encode_shape(self):
47
- """测试 encode 输出维度。"""
48
- pass
49
-
50
- def test_decode_step_shape(self):
51
- """测试 decode_step 输出维度。"""
52
- pass
53
-
54
- def test_parameter_count(self):
55
- """测试参数数量计算。"""
56
- pass
57
-
58
- def test_causal_mask(self):
59
- """测试因果掩码生成。"""
60
- pass
 
1
+ """
2
+ 单元测试 — 模型模块 (Person B 实现后需通过)
3
+ """
4
+
5
+ import pytest
6
+ import torch
7
+
8
+
9
+ class TestMultiHeadAttention:
10
+ """测试多头注意力。"""
11
+
12
+ def test_output_shape(self):
13
+ """测试输出维度正确。"""
14
+ # TODO: 创建 MHA,输入 [2, 10, 512],验证输出 [2, 10, 512]
15
+ pass
16
+
17
+ def test_padding_mask(self):
18
+ """测试 padding mask 效果。"""
19
+ pass
20
+
21
+
22
+ class TestPositionalEncoding:
23
+ """测试位置编码。"""
24
+
25
+ def test_sinusoidal_shape(self):
26
+ """测试正弦位置编码输出维度。"""
27
+ pass
28
+
29
+ def test_rope_shape(self):
30
+ """测试 RoPE 输出维度。"""
31
+ pass
32
+
33
+ def test_rope_relative_distance(self):
34
+ """测试 RoPE 捕获相对距离。"""
35
+ pass
36
+
37
+
38
+ class TestTransformerModel:
39
+ """测试完整 Transformer 模型。"""
40
+
41
+ def test_forward_shape(self):
42
+ """测试前向传播输出维度。"""
43
+ # TODO: 构建小模型,输入 mock 数据,验证 logits shape [B, T, V]
44
+ pass
45
+
46
+ def test_encode_shape(self):
47
+ """测试 encode 输出维度。"""
48
+ pass
49
+
50
+ def test_decode_step_shape(self):
51
+ """测试 decode_step 输出维度。"""
52
+ pass
53
+
54
+ def test_parameter_count(self):
55
+ """测试参数数量计算。"""
56
+ pass
57
+
58
+ def test_causal_mask(self):
59
+ """测试因果掩码生成。"""
60
+ pass
tests/test_training.py CHANGED
@@ -1,46 +1,46 @@
1
- """
2
- 单元测试 — 训练模块 (Person C 实现后需通过)
3
- """
4
-
5
- import pytest
6
- import torch
7
-
8
-
9
- class TestLabelSmoothedLoss:
10
- """测试标签平滑损失。"""
11
-
12
- def test_no_smoothing_equals_ce(self):
13
- """smoothing=0 时应等于标准交叉熵。"""
14
- pass
15
-
16
- def test_smoothing_reduces_confidence(self):
17
- """标签平滑应降低损失对正确预测的自信度。"""
18
- pass
19
-
20
- def test_ignore_padding(self):
21
- """应忽略 padding 位置的损失。"""
22
- pass
23
-
24
-
25
- class TestOptimizer:
26
- """测试优化器构建。"""
27
-
28
- def test_build_adamw(self):
29
- """测试构建 AdamW。"""
30
- pass
31
-
32
- def test_weight_decay_groups(self):
33
- """测试权重衰减参数组正确。"""
34
- pass
35
-
36
-
37
- class TestScheduler:
38
- """测试学习率调度器。"""
39
-
40
- def test_warmup_phase(self):
41
- """测试 warmup 阶段 lr 线性增长。"""
42
- pass
43
-
44
- def test_decay_phase(self):
45
- """测试衰减阶段 lr 下降。"""
46
- pass
 
1
+ """
2
+ 单元测试 — 训练模块 (Person C 实现后需通过)
3
+ """
4
+
5
+ import pytest
6
+ import torch
7
+
8
+
9
+ class TestLabelSmoothedLoss:
10
+ """测试标签平滑损失。"""
11
+
12
+ def test_no_smoothing_equals_ce(self):
13
+ """smoothing=0 时应等于标准交叉熵。"""
14
+ pass
15
+
16
+ def test_smoothing_reduces_confidence(self):
17
+ """标签平滑应降低损失对正确预测的自信度。"""
18
+ pass
19
+
20
+ def test_ignore_padding(self):
21
+ """应忽略 padding 位置的损失。"""
22
+ pass
23
+
24
+
25
+ class TestOptimizer:
26
+ """测试优化器构建。"""
27
+
28
+ def test_build_adamw(self):
29
+ """测试构建 AdamW。"""
30
+ pass
31
+
32
+ def test_weight_decay_groups(self):
33
+ """测试权重衰减参数组正确。"""
34
+ pass
35
+
36
+
37
+ class TestScheduler:
38
+ """测试学习率调度器。"""
39
+
40
+ def test_warmup_phase(self):
41
+ """测试 warmup 阶段 lr 线性增长。"""
42
+ pass
43
+
44
+ def test_decay_phase(self):
45
+ """测试衰减阶段 lr 下降。"""
46
+ pass