Add Streamlit preview frontend and normalize line endings
Browse files## Changes
- Add Streamlit-based preview frontend for translation demo
- Disable automatic dataset download for BPE tokenizer
- Normalize line endings (CRLF to LF) across all project files
- .gitattributes +35 -35
- .gitignore +57 -57
- README.md +76 -76
- TASK_ASSIGNMENT.md +414 -414
- configs/deepspeed_config.json +28 -28
- configs/default_config.yaml +183 -182
- pytest.ini +6 -6
- requirements.txt +36 -35
- scripts/evaluate.py +285 -50
- scripts/run_experiments.py +113 -113
- scripts/train.py +76 -76
- scripts/translate.py +316 -82
- scripts/visualize.py +86 -86
- setup.py +23 -23
- src/easytranslate/__init__.py +3 -3
- src/easytranslate/data/README.md +126 -126
- src/easytranslate/data/__init__.py +32 -32
- src/easytranslate/data/collator.py +114 -114
- src/easytranslate/data/dataset.py +157 -157
- src/easytranslate/data/preprocessing.py +125 -125
- src/easytranslate/data/tokenizer.py +197 -197
- src/easytranslate/evaluation/__init__.py +16 -16
- src/easytranslate/evaluation/decoding.py +219 -129
- src/easytranslate/evaluation/evaluator.py +208 -76
- src/easytranslate/evaluation/metrics.py +169 -109
- src/easytranslate/model/__init__.py +20 -20
- src/easytranslate/model/attention.py +231 -231
- src/easytranslate/model/decoder.py +176 -176
- src/easytranslate/model/encoder.py +127 -127
- src/easytranslate/model/finetune.py +116 -116
- src/easytranslate/model/positional.py +134 -134
- src/easytranslate/model/transformer.py +256 -256
- src/easytranslate/training/__init__.py +12 -12
- src/easytranslate/training/loss.py +57 -57
- src/easytranslate/training/optimizer.py +76 -76
- src/easytranslate/training/trainer.py +186 -186
- src/easytranslate/utils/__init__.py +7 -7
- src/easytranslate/utils/config.py +51 -51
- src/easytranslate/utils/logging.py +26 -26
- src/easytranslate/utils/seed.py +23 -23
- tests/test_data.py +143 -143
- tests/test_evaluation.py +54 -54
- tests/test_model.py +60 -60
- 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 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
- "
|
| 142 |
-
- "
|
| 143 |
-
- "
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
| 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
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
# 启动
|
| 12 |
-
python scripts/translate.py --
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def
|
| 46 |
-
""
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
def
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
1.
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
""
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 20 |
-
from
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
from easytranslate.evaluation.
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
"""
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
"""
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
references:
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
references:
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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. (可选) 应
|
| 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
|