model
#3
by zhao8445 - opened
- .gitignore +0 -9
- configs/default_config.yaml +2 -2
- docs/evaluation_report.md +0 -430
- notebooks/EasyTranslate_Production.ipynb +0 -1153
- requirements-colab.txt +0 -21
- requirements.txt +32 -59
- result/best_model.pt +0 -3
- result/evaluation_results.json +0 -10
- result/final_evaluation_metrics.png +0 -0
- result/link.txt +0 -1
- result/train_loss_curve.png +0 -0
- result/train_output.txt +0 -102
- result/training_summary.json +0 -30
- result/translation_examples.json +0 -18
- scripts/evaluate.py +1 -71
- scripts/setup_colab.py +0 -121
- scripts/translate.py +5 -101
- setup.py +12 -67
- src/easytranslate/data/collator.py +3 -32
- src/easytranslate/evaluation/decoding.py +55 -179
- src/easytranslate/evaluation/evaluator.py +30 -138
- src/easytranslate/evaluation/metrics.py +37 -182
- src/easytranslate/model/attention.py +31 -138
- src/easytranslate/model/decoder.py +21 -111
- src/easytranslate/model/encoder.py +27 -70
- src/easytranslate/model/finetune.py +22 -43
- src/easytranslate/model/positional.py +5 -48
- src/easytranslate/model/transformer.py +43 -142
- src/easytranslate/training/loss.py +19 -38
- src/easytranslate/training/optimizer.py +26 -119
- src/easytranslate/training/trainer.py +114 -387
- src/easytranslate/utils/__init__.py +2 -25
- src/easytranslate/utils/cloud_storage.py +0 -188
- src/easytranslate/utils/config.py +14 -47
- src/easytranslate/utils/logging.py +7 -40
- src/easytranslate/utils/seed.py +8 -10
.gitignore
CHANGED
|
@@ -31,15 +31,6 @@ 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
|
|
|
|
| 31 |
logs/
|
| 32 |
wandb/
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
# OS
|
| 35 |
.DS_Store
|
| 36 |
Thumbs.db
|
configs/default_config.yaml
CHANGED
|
@@ -80,7 +80,7 @@ data:
|
|
| 80 |
# DataLoader
|
| 81 |
dataloader:
|
| 82 |
batch_size: 32
|
| 83 |
-
num_workers:
|
| 84 |
pin_memory: true
|
| 85 |
dynamic_batching: true # 动态 batch (按 token 数)
|
| 86 |
max_tokens_per_batch: 8192 # 动态 batch 最大 token 数
|
|
@@ -138,7 +138,7 @@ evaluation:
|
|
| 138 |
# 评估指标
|
| 139 |
metrics:
|
| 140 |
- "bleu" # SacreBLEU
|
| 141 |
-
|
| 142 |
- "chrf" # chrF++
|
| 143 |
- "ter" # TER
|
| 144 |
|
|
|
|
| 80 |
# DataLoader
|
| 81 |
dataloader:
|
| 82 |
batch_size: 32
|
| 83 |
+
num_workers: 4
|
| 84 |
pin_memory: true
|
| 85 |
dynamic_batching: true # 动态 batch (按 token 数)
|
| 86 |
max_tokens_per_batch: 8192 # 动态 batch 最大 token 数
|
|
|
|
| 138 |
# 评估指标
|
| 139 |
metrics:
|
| 140 |
- "bleu" # SacreBLEU
|
| 141 |
+
- "comet" # COMET (神经网络指标)
|
| 142 |
- "chrf" # chrF++
|
| 143 |
- "ter" # TER
|
| 144 |
|
docs/evaluation_report.md
DELETED
|
@@ -1,430 +0,0 @@
|
|
| 1 |
-
# =============================================================================
|
| 2 |
-
# EasyTranslate — Code Evaluation Report
|
| 3 |
-
# =============================================================================
|
| 4 |
-
# Evaluator: Person C
|
| 5 |
-
# Date: 2026-05-13
|
| 6 |
-
# Scope: Submissions from Persons A (Data), B (Model), D (Evaluation)
|
| 7 |
-
# =============================================================================
|
| 8 |
-
|
| 9 |
-
"""
|
| 10 |
-
EXECUTIVE SUMMARY
|
| 11 |
-
=================
|
| 12 |
-
All three submissions (A, B, D) demonstrate solid engineering foundations with
|
| 13 |
-
well-structured code, appropriate use of modern PyTorch patterns, and good
|
| 14 |
-
documentation. The primary areas requiring attention are:
|
| 15 |
-
1. Interface contract alignment between modules
|
| 16 |
-
2. Error handling robustness in edge cases
|
| 17 |
-
3. Performance optimization for large-scale training
|
| 18 |
-
4. Test coverage for critical paths
|
| 19 |
-
|
| 20 |
-
Overall Integration Readiness: 85% — Minor modifications needed for seamless integration.
|
| 21 |
-
"""
|
| 22 |
-
|
| 23 |
-
# =============================================================================
|
| 24 |
-
# PERSON A — DATA MODULE EVALUATION
|
| 25 |
-
# =============================================================================
|
| 26 |
-
|
| 27 |
-
"""
|
| 28 |
-
MODULE: src/easytranslate/data/
|
| 29 |
-
FILES: dataset.py, tokenizer.py, preprocessing.py, collator.py
|
| 30 |
-
OWNER: Person A
|
| 31 |
-
ROLE: Data loading, preprocessing, tokenization, batching
|
| 32 |
-
|
| 33 |
-
1. FUNCTIONAL CORRECTNESS (Score: 82/100)
|
| 34 |
-
------------------------------------------
|
| 35 |
-
|
| 36 |
-
PASSED:
|
| 37 |
-
- TranslationDataset correctly implements __len__ and __getitem__
|
| 38 |
-
- Tokenizer training produces valid BPE vocabulary
|
| 39 |
-
- Preprocessing pipeline filters invalid samples correctly
|
| 40 |
-
- Collator generates proper padding masks and label shifting
|
| 41 |
-
|
| 42 |
-
ISSUES FOUND:
|
| 43 |
-
|
| 44 |
-
[SEVERITY: MEDIUM] ID-A1: Tokenizer special token handling
|
| 45 |
-
File: tokenizer.py, build_tokenizer()
|
| 46 |
-
Description: When using pretrained tokenizers (NLLB/mBART), the function
|
| 47 |
-
does not verify that the tokenizer's special tokens (BOS, EOS, PAD) are
|
| 48 |
-
correctly configured for the target language pair. This can cause silent
|
| 49 |
-
failures during translation.
|
| 50 |
-
Reproduction:
|
| 51 |
-
1. Call build_tokenizer with type="pretrained" and model_name="facebook/nllb-200-distilled-600M"
|
| 52 |
-
2. Check tokenizer.bos_token_id for src_lang="eng_Latn"
|
| 53 |
-
3. Expected: valid BOS token; Actual: may be None for some language codes
|
| 54 |
-
Fix: Add explicit special token verification after pretrained tokenizer loading.
|
| 55 |
-
|
| 56 |
-
[SEVERITY: LOW] ID-A2: Memory inefficiency in preprocess_pipeline
|
| 57 |
-
File: preprocessing.py, preprocess_pipeline()
|
| 58 |
-
Description: The function creates intermediate copies of the full dataset
|
| 59 |
-
during each filtering step (length filter, ratio filter, dedup). For large
|
| 60 |
-
datasets (>10M pairs), this can cause OOM.
|
| 61 |
-
Reproduction:
|
| 62 |
-
1. Load WMT19 full dataset (~30M pairs)
|
| 63 |
-
2. Run preprocess_pipeline with all filters enabled
|
| 64 |
-
3. Monitor memory usage — peaks at ~3x dataset size
|
| 65 |
-
Fix: Use generator-based filtering or process in chunks.
|
| 66 |
-
|
| 67 |
-
[SEVERITY: LOW] ID-A3: DynamicBatchSampler edge case
|
| 68 |
-
File: collator.py, DynamicBatchSampler
|
| 69 |
-
Description: When max_tokens_per_batch is smaller than the longest single
|
| 70 |
-
sequence, the sampler enters an infinite loop trying to fit the sequence.
|
| 71 |
-
Reproduction:
|
| 72 |
-
1. Set max_tokens_per_batch=100
|
| 73 |
-
2. Include a sequence of length 150
|
| 74 |
-
3. Sampler hangs
|
| 75 |
-
Fix: Add a guard clause to place oversized sequences in their own batch.
|
| 76 |
-
|
| 77 |
-
2. CODE QUALITY (Score: 88/100)
|
| 78 |
-
--------------------------------
|
| 79 |
-
|
| 80 |
-
STRENGTHS:
|
| 81 |
-
- Consistent use of type hints throughout
|
| 82 |
-
- Good docstrings with parameter descriptions
|
| 83 |
-
- Clean separation of concerns (dataset vs collator vs sampler)
|
| 84 |
-
- Proper use of PyTorch Dataset/DataLoader abstractions
|
| 85 |
-
|
| 86 |
-
AREAS FOR IMPROVEMENT:
|
| 87 |
-
- Some magic numbers in preprocessing (e.g., length_ratio_threshold=3.0)
|
| 88 |
-
should reference config values
|
| 89 |
-
- Tokenizer training could benefit from progress callbacks for large corpora
|
| 90 |
-
- Missing input validation for edge cases (empty texts, single-character inputs)
|
| 91 |
-
|
| 92 |
-
3. TRAINING SUITABILITY (Score: 85/100)
|
| 93 |
-
----------------------------------------
|
| 94 |
-
|
| 95 |
-
STRENGTHS:
|
| 96 |
-
- Dynamic batching significantly improves GPU utilization
|
| 97 |
-
- Preprocessing pipeline handles real-world noisy data well
|
| 98 |
-
- Tokenizer supports both BPE training and pretrained loading
|
| 99 |
-
|
| 100 |
-
CONCERNS:
|
| 101 |
-
- No support for streaming/lazy loading of very large datasets
|
| 102 |
-
- Tokenizer training on full dataset may be slow; consider sampling
|
| 103 |
-
- No data augmentation strategies implemented (back-translation, etc.)
|
| 104 |
-
|
| 105 |
-
4. ARCHITECTURAL COMPATIBILITY (Score: 90/100)
|
| 106 |
-
-----------------------------------------------
|
| 107 |
-
|
| 108 |
-
COMPATIBLE WITH:
|
| 109 |
-
- Batch format matches Trainer expectations: {src_ids, tgt_input_ids, labels, masks}
|
| 110 |
-
- Tokenizer interface (encode/decode/vocab_size/pad_token_id) matches all consumers
|
| 111 |
-
- Dataset returns standard PyTorch tensors
|
| 112 |
-
|
| 113 |
-
MINOR MISMATCHES:
|
| 114 |
-
- Collator uses 'src_padding_mask' as bool tensor; Trainer expects this format (OK)
|
| 115 |
-
- Tokenizer's encode() returns list[int]; some consumers may expect tensor (minor)
|
| 116 |
-
|
| 117 |
-
RECOMMENDATIONS:
|
| 118 |
-
1. Add a validate_batch() utility function for integration testing
|
| 119 |
-
2. Document the exact batch schema as a dataclass for type safety
|
| 120 |
-
3. Add data statistics logging (avg length, vocab coverage) for monitoring
|
| 121 |
-
"""
|
| 122 |
-
|
| 123 |
-
# =============================================================================
|
| 124 |
-
# PERSON B — MODEL MODULE EVALUATION
|
| 125 |
-
# =============================================================================
|
| 126 |
-
|
| 127 |
-
"""
|
| 128 |
-
MODULE: src/easytranslate/model/
|
| 129 |
-
FILES: transformer.py, attention.py, encoder.py, decoder.py, positional.py, finetune.py
|
| 130 |
-
OWNER: Person B
|
| 131 |
-
ROLE: Model architecture, attention mechanisms, pretrained model loading
|
| 132 |
-
|
| 133 |
-
1. FUNCTIONAL CORRECTNESS (Score: 85/100)
|
| 134 |
-
------------------------------------------
|
| 135 |
-
|
| 136 |
-
PASSED:
|
| 137 |
-
- TransformerTranslationModel forward pass produces correct output shapes
|
| 138 |
-
- Flash Attention 2 integration works correctly when available
|
| 139 |
-
- Rotary Positional Embedding (RoPE) implementation is mathematically correct
|
| 140 |
-
- Encoder-decoder attention masking prevents information leakage
|
| 141 |
-
- Pretrained model loading (NLLB, mBART) works with correct config mapping
|
| 142 |
-
|
| 143 |
-
ISSUES FOUND:
|
| 144 |
-
|
| 145 |
-
[SEVERITY: HIGH] ID-B1: Flash Attention fallback not graceful
|
| 146 |
-
File: attention.py, FlashMultiHeadAttention
|
| 147 |
-
Description: When flash_attn is not installed, the module raises ImportError
|
| 148 |
-
at import time rather than falling back to scaled_dot_product_attention.
|
| 149 |
-
This prevents the entire model module from being imported on systems
|
| 150 |
-
without flash-attn (e.g., MacOS, Windows without CUDA).
|
| 151 |
-
Reproduction:
|
| 152 |
-
1. pip uninstall flash-attn
|
| 153 |
-
2. from easytranslate.model import TransformerTranslationModel
|
| 154 |
-
3. ImportError raised
|
| 155 |
-
Fix: Use try/except at the attention class level with automatic fallback.
|
| 156 |
-
|
| 157 |
-
[SEVERITY: MEDIUM] ID-B2: RoPE sequence length limitation
|
| 158 |
-
File: positional.py, RotaryPositionalEmbedding
|
| 159 |
-
Description: RoPE embeddings are precomputed up to max_seq_len. If inference
|
| 160 |
-
exceeds this length, the model produces incorrect positional encodings
|
| 161 |
-
(index out of bounds or wraparound).
|
| 162 |
-
Reproduction:
|
| 163 |
-
1. Train with max_seq_len=512
|
| 164 |
-
2. Attempt inference with sequence length 600
|
| 165 |
-
3. Positional encoding incorrect beyond position 512
|
| 166 |
-
Fix: Use on-the-fly RoPE computation or dynamic extension.
|
| 167 |
-
|
| 168 |
-
[SEVERITY: MEDIUM] ID-B3: Pretrained model tokenizer mismatch
|
| 169 |
-
File: finetune.py, load_pretrained_model()
|
| 170 |
-
Description: The function loads a pretrained model but does not return or
|
| 171 |
-
validate the corresponding tokenizer. The caller must separately ensure
|
| 172 |
-
tokenizer compatibility, which is error-prone.
|
| 173 |
-
Reproduction:
|
| 174 |
-
1. Load NLLB model via load_pretrained_model()
|
| 175 |
-
2. Use a BPE tokenizer trained on different data
|
| 176 |
-
3. Token IDs don't match model's embedding table
|
| 177 |
-
Fix: Return tokenizer alongside model, or validate tokenizer compatibility.
|
| 178 |
-
|
| 179 |
-
[SEVERITY: LOW] ID-B4: Dropout not disabled during encode()
|
| 180 |
-
File: encoder.py, TransformerEncoder.encode()
|
| 181 |
-
Description: The encode() method does not explicitly set model.eval() or
|
| 182 |
-
disable dropout. If called during training mode, encoder outputs are
|
| 183 |
-
non-deterministic.
|
| 184 |
-
Fix: Add context manager or explicit eval() call in encode().
|
| 185 |
-
|
| 186 |
-
2. CODE QUALITY (Score: 90/100)
|
| 187 |
-
--------------------------------
|
| 188 |
-
|
| 189 |
-
STRENGTHS:
|
| 190 |
-
- Excellent modular design with clear separation of attention, encoder, decoder
|
| 191 |
-
- Comprehensive use of PyTorch nn.Module patterns
|
| 192 |
-
- Good handling of padding masks throughout the architecture
|
| 193 |
-
- Clean implementation of Pre-LayerNorm vs Post-LayerNorm variants
|
| 194 |
-
|
| 195 |
-
AREAS FOR IMPROVEMENT:
|
| 196 |
-
- Some duplicated code between encoder and decoder layer implementations
|
| 197 |
-
- Attention mask creation could be extracted to a shared utility
|
| 198 |
-
- Missing type hints on some internal methods
|
| 199 |
-
|
| 200 |
-
3. TRAINING SUITABILITY (Score: 88/100)
|
| 201 |
-
----------------------------------------
|
| 202 |
-
|
| 203 |
-
STRENGTHS:
|
| 204 |
-
- Flash Attention 2 provides significant speedup (2-3x) on supported hardware
|
| 205 |
-
- RoPE enables better length generalization than sinusoidal embeddings
|
| 206 |
-
- Pre-LayerNorm improves training stability
|
| 207 |
-
- LoRA support enables efficient fine-tuning of large pretrained models
|
| 208 |
-
|
| 209 |
-
CONCERNS:
|
| 210 |
-
- No gradient checkpointing support for memory-constrained training
|
| 211 |
-
- Model parallelism not considered for very large configurations
|
| 212 |
-
- No activation offloading strategies
|
| 213 |
-
|
| 214 |
-
4. ARCHITECTURAL COMPATIBILITY (Score: 92/100)
|
| 215 |
-
-----------------------------------------------
|
| 216 |
-
|
| 217 |
-
COMPATIBLE WITH:
|
| 218 |
-
- Trainer expects model(src_ids, tgt_input_ids, src_mask, tgt_mask) -> logits
|
| 219 |
-
- Evaluator expects model.encode() and model.decode_step() for inference
|
| 220 |
-
- Config structure matches model parameters
|
| 221 |
-
|
| 222 |
-
MINOR MISMATCHES:
|
| 223 |
-
- Model expects separate src_padding_mask and tgt_padding_mask; Trainer provides both (OK)
|
| 224 |
-
- encode() method signature differs slightly from what Evaluator expects (minor)
|
| 225 |
-
|
| 226 |
-
RECOMMENDATIONS:
|
| 227 |
-
1. Add model.forward() input validation with helpful error messages
|
| 228 |
-
2. Implement gradient checkpointing for memory efficiency
|
| 229 |
-
3. Add model summary/logging (param count per component)
|
| 230 |
-
4. Create a unified ModelInterface abstract class for type safety
|
| 231 |
-
"""
|
| 232 |
-
|
| 233 |
-
# =============================================================================
|
| 234 |
-
# PERSON D — EVALUATION MODULE EVALUATION
|
| 235 |
-
# =============================================================================
|
| 236 |
-
|
| 237 |
-
"""
|
| 238 |
-
MODULE: src/easytranslate/evaluation/
|
| 239 |
-
FILES: metrics.py, decoding.py, evaluator.py
|
| 240 |
-
OWNER: Person D
|
| 241 |
-
ROLE: Decoding strategies, metric computation, evaluation pipeline
|
| 242 |
-
|
| 243 |
-
1. FUNCTIONAL CORRECTNESS (Score: 80/100)
|
| 244 |
-
------------------------------------------
|
| 245 |
-
|
| 246 |
-
PASSED:
|
| 247 |
-
- Greedy decoding produces valid token sequences
|
| 248 |
-
- Beam search correctly maintains top-k hypotheses
|
| 249 |
-
- BLEU computation via SacreBLEU matches reference implementations
|
| 250 |
-
- COMET metric integration works with pretrained models
|
| 251 |
-
|
| 252 |
-
ISSUES FOUND:
|
| 253 |
-
|
| 254 |
-
[SEVERITY: HIGH] ID-D1: Beam search memory leak
|
| 255 |
-
File: decoding.py, beam_search_decode()
|
| 256 |
-
Description: The beam search implementation accumulates all intermediate
|
| 257 |
-
states for each beam without releasing memory. For large beam sizes (k>10)
|
| 258 |
-
and long sequences, this causes OOM on GPU.
|
| 259 |
-
Reproduction:
|
| 260 |
-
1. Set beam_size=20, max_len=256
|
| 261 |
-
2. Run beam_search_decode on GPU with 16GB VRAM
|
| 262 |
-
3. OOM after ~100 tokens
|
| 263 |
-
Fix: Implement beam pruning and state cleanup after each step.
|
| 264 |
-
|
| 265 |
-
[SEVERITY: MEDIUM] ID-D2: COMET model download without caching check
|
| 266 |
-
File: metrics.py, compute_comet()
|
| 267 |
-
Description: COMET model is downloaded on every first call without checking
|
| 268 |
-
local cache. In Colab with ephemeral storage, this adds 2-5 minutes per
|
| 269 |
-
session.
|
| 270 |
-
Reproduction:
|
| 271 |
-
1. Run compute_comet() in fresh Colab session
|
| 272 |
-
2. Observe ~500MB model download
|
| 273 |
-
3. Repeat in new session — same download occurs
|
| 274 |
-
Fix: Check ~/.cache/huggingface before downloading; provide manual cache path.
|
| 275 |
-
|
| 276 |
-
[SEVERITY: MEDIUM] ID-D3: Evaluator.evaluate() assumes specific batch format
|
| 277 |
-
File: evaluator.py, Evaluator.evaluate()
|
| 278 |
-
Description: The evaluate() method directly accesses batch["src_ids"] and
|
| 279 |
-
batch["labels"] without validation. If the dataloader format changes,
|
| 280 |
-
this fails with cryptic KeyError.
|
| 281 |
-
Reproduction:
|
| 282 |
-
1. Pass a dataloader with different batch keys
|
| 283 |
-
2. evaluate() raises KeyError without helpful message
|
| 284 |
-
Fix: Add batch format validation at the start of evaluate().
|
| 285 |
-
|
| 286 |
-
[SEVERITY: LOW] ID-D4: chrF++ not handling empty references
|
| 287 |
-
File: metrics.py, compute_chrf()
|
| 288 |
-
Description: When reference text is empty (after preprocessing), chrF++
|
| 289 |
-
computation raises ZeroDivisionError.
|
| 290 |
-
Fix: Add guard clause for empty references.
|
| 291 |
-
|
| 292 |
-
2. CODE QUALITY (Score: 83/100)
|
| 293 |
-
--------------------------------
|
| 294 |
-
|
| 295 |
-
STRENGTHS:
|
| 296 |
-
- Clean separation of decoding strategies from metric computation
|
| 297 |
-
- Good use of SacreBLEU for standardized BLEU scores
|
| 298 |
-
- Evaluator class provides a unified interface for all metrics
|
| 299 |
-
|
| 300 |
-
AREAS FOR IMPROVEMENT:
|
| 301 |
-
- Some functions are too long (beam_search_decode >100 lines)
|
| 302 |
-
- Missing type hints on several public methods
|
| 303 |
-
- Limited error messages for common failure modes
|
| 304 |
-
- No progress reporting during long evaluation runs
|
| 305 |
-
|
| 306 |
-
3. TRAINING SUITABILITY (Score: 82/100)
|
| 307 |
-
----------------------------------------
|
| 308 |
-
|
| 309 |
-
STRENGTHS:
|
| 310 |
-
- Multiple decoding strategies support different use cases
|
| 311 |
-
- COMET provides neural metric correlation with human judgment
|
| 312 |
-
- Evaluation pipeline integrates well with validation loop
|
| 313 |
-
|
| 314 |
-
CONCERNS:
|
| 315 |
-
- Beam search is too slow for per-epoch validation on large dev sets
|
| 316 |
-
- No support for batched beam search decoding
|
| 317 |
-
- COMET computation is very slow on CPU (consider GPU acceleration)
|
| 318 |
-
- No caching of encoder outputs during evaluation
|
| 319 |
-
|
| 320 |
-
4. ARCHITECTURAL COMPATIBILITY (Score: 85/100)
|
| 321 |
-
-----------------------------------------------
|
| 322 |
-
|
| 323 |
-
COMPATIBLE WITH:
|
| 324 |
-
- Evaluator(model, tokenizer, config) constructor matches Trainer expectations
|
| 325 |
-
- evaluate(dataloader) returns dict[str, float] as expected
|
| 326 |
-
- Decoding functions accept standard model interface
|
| 327 |
-
|
| 328 |
-
MINOR MISMATCHES:
|
| 329 |
-
- Evaluator expects model.eval() to be called externally (Trainer handles this)
|
| 330 |
-
- translate_single() method not part of the documented interface contract
|
| 331 |
-
|
| 332 |
-
RECOMMENDATIONS:
|
| 333 |
-
1. Add batch_size parameter to beam search for parallel decoding
|
| 334 |
-
2. Implement incremental evaluation (evaluate every N steps, not just epochs)
|
| 335 |
-
3. Add evaluation result caching to avoid recomputation
|
| 336 |
-
4. Create an EvaluationResult dataclass for type-safe metric passing
|
| 337 |
-
"""
|
| 338 |
-
|
| 339 |
-
# =============================================================================
|
| 340 |
-
# INTEGRATION TEST RESULTS
|
| 341 |
-
# =============================================================================
|
| 342 |
-
|
| 343 |
-
"""
|
| 344 |
-
END-TO-END INTEGRATION TEST
|
| 345 |
-
============================
|
| 346 |
-
|
| 347 |
-
Test: Full pipeline from data loading through evaluation
|
| 348 |
-
Status: PASSED (with noted issues)
|
| 349 |
-
|
| 350 |
-
Test Flow:
|
| 351 |
-
1. [OK] Config loaded from default_config.yaml
|
| 352 |
-
2. [OK] WMT19 dataset loaded (sampled 100k pairs for test)
|
| 353 |
-
3. [OK] Preprocessing pipeline executed (filtered to 95,234 pairs)
|
| 354 |
-
4. [OK] BPE tokenizer trained (vocab_size=32000)
|
| 355 |
-
5. [OK] TranslationDataset + Collator + DataLoader constructed
|
| 356 |
-
6. [OK] TransformerTranslationModel built (d_model=512, 6L-6L)
|
| 357 |
-
7. [OK] Forward pass verified (correct output shapes)
|
| 358 |
-
8. [OK] Trainer initialized with all components
|
| 359 |
-
9. [OK] Single training step executed (loss decreasing)
|
| 360 |
-
10. [OK] Checkpoint saved and loaded correctly
|
| 361 |
-
11. [OK] Greedy decoding produces valid Chinese output
|
| 362 |
-
12. [OK] BLEU score computed on validation set
|
| 363 |
-
|
| 364 |
-
Known Integration Issues:
|
| 365 |
-
- ID-B1 (Flash Attention import) blocks import on CPU-only systems
|
| 366 |
-
Workaround: Set USE_FLASH_ATTENTION=0 environment variable
|
| 367 |
-
- ID-D1 (Beam search memory) limits beam size to <=5 for 16GB GPU
|
| 368 |
-
Workaround: Use greedy decoding for validation, beam search for final test
|
| 369 |
-
|
| 370 |
-
Performance Benchmarks (A100 40GB, batch_size=32, d_model=512):
|
| 371 |
-
- Data loading: 0.8s/batch (with preprocessing)
|
| 372 |
-
- Forward pass: 0.15s/batch
|
| 373 |
-
- Backward pass: 0.25s/batch
|
| 374 |
-
- Validation (greedy): 2.1s/epoch (5k samples)
|
| 375 |
-
- Validation (beam=5): 18.5s/epoch (5k samples)
|
| 376 |
-
- Checkpoint save: 0.3s
|
| 377 |
-
- Estimated full training (10 epochs, 100k samples): ~2.5 hours
|
| 378 |
-
"""
|
| 379 |
-
|
| 380 |
-
# =============================================================================
|
| 381 |
-
# SUMMARY OF MODIFICATIONS MADE BY PERSON C
|
| 382 |
-
# =============================================================================
|
| 383 |
-
|
| 384 |
-
"""
|
| 385 |
-
FILES MODIFIED:
|
| 386 |
-
1. src/easytranslate/training/loss.py — Implemented LabelSmoothedCrossEntropyLoss
|
| 387 |
-
2. src/easytranslate/training/optimizer.py — Implemented build_optimizer, build_scheduler, InverseSqrtScheduler
|
| 388 |
-
3. src/easytranslate/training/trainer.py — Implemented complete Trainer class
|
| 389 |
-
4. src/easytranslate/utils/config.py — Implemented load_config, merge_configs, config_from_cli, config_to_dict
|
| 390 |
-
5. src/easytranslate/utils/seed.py — Implemented set_seed
|
| 391 |
-
6. src/easytranslate/utils/logging.py — Implemented setup_logging
|
| 392 |
-
7. src/easytranslate/utils/__init__.py — Updated exports
|
| 393 |
-
8. requirements.txt — Added version pinning and organized by category
|
| 394 |
-
9. setup.py — Updated with extras_require and classifiers
|
| 395 |
-
|
| 396 |
-
FILES CREATED:
|
| 397 |
-
1. src/easytranslate/utils/cloud_storage.py — Google Drive integration
|
| 398 |
-
2. notebooks/EasyTranslate_Production.ipynb — Production entry point
|
| 399 |
-
3. scripts/setup_colab.py — Colab environment setup script
|
| 400 |
-
4. requirements-colab.txt — Colab-specific dependencies
|
| 401 |
-
5. docs/evaluation_report.md — This evaluation report
|
| 402 |
-
"""
|
| 403 |
-
|
| 404 |
-
# =============================================================================
|
| 405 |
-
# HANDOVER NOTES FOR PERSON E
|
| 406 |
-
# =============================================================================
|
| 407 |
-
|
| 408 |
-
"""
|
| 409 |
-
For the final summary (总结), Person E should note:
|
| 410 |
-
|
| 411 |
-
1. The system is integration-ready with all core modules implemented.
|
| 412 |
-
2. The Jupyter Notebook (notebooks/EasyTranslate_Production.ipynb) is the
|
| 413 |
-
primary entry point and handles all environment setup automatically.
|
| 414 |
-
3. On Google Colab, the notebook will:
|
| 415 |
-
a. Clone the repository at runtime
|
| 416 |
-
b. Install all dependencies
|
| 417 |
-
c. Mount Google Drive for persistent storage
|
| 418 |
-
d. Execute the full training pipeline
|
| 419 |
-
4. Known limitations:
|
| 420 |
-
- Flash Attention requires compatible GPU (A100, H100, RTX 4090)
|
| 421 |
-
- Beam search with k>5 may OOM on 16GB GPUs
|
| 422 |
-
- COMET model download adds ~2min to first Colab run
|
| 423 |
-
5. Future improvements (prioritized by impact):
|
| 424 |
-
HIGH: Gradient checkpointing for larger models
|
| 425 |
-
HIGH: Streaming dataset loading for full WMT dataset
|
| 426 |
-
MEDIUM: Batched beam search for faster validation
|
| 427 |
-
MEDIUM: Data augmentation (back-translation)
|
| 428 |
-
LOW: Model quantization for deployment
|
| 429 |
-
LOW: ONNX export for inference optimization
|
| 430 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
notebooks/EasyTranslate_Production.ipynb
DELETED
|
@@ -1,1153 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"cells": [
|
| 3 |
-
{
|
| 4 |
-
"cell_type": "markdown",
|
| 5 |
-
"metadata": {},
|
| 6 |
-
"source": [
|
| 7 |
-
"# EasyTranslate: Transformer-Based English-Chinese Translation System\n",
|
| 8 |
-
"\n",
|
| 9 |
-
"## Production Entry Point — Person C Integration\n",
|
| 10 |
-
"\n",
|
| 11 |
-
"This notebook serves as the primary entry point for the EasyTranslate project.\n",
|
| 12 |
-
"It handles:\n",
|
| 13 |
-
"- Environment detection (local vs Google Colab)\n",
|
| 14 |
-
"- Repository cloning and dependency installation\n",
|
| 15 |
-
"- Data loading and preprocessing\n",
|
| 16 |
-
"- Model construction and training\n",
|
| 17 |
-
"- Evaluation and cloud storage synchronization\n",
|
| 18 |
-
"\n",
|
| 19 |
-
"---"
|
| 20 |
-
]
|
| 21 |
-
},
|
| 22 |
-
{
|
| 23 |
-
"cell_type": "markdown",
|
| 24 |
-
"metadata": {},
|
| 25 |
-
"source": [
|
| 26 |
-
"## 1. Environment Detection & Setup\n",
|
| 27 |
-
"\n",
|
| 28 |
-
"Detect whether we are running in Google Colab or locally, and configure accordingly."
|
| 29 |
-
]
|
| 30 |
-
},
|
| 31 |
-
{
|
| 32 |
-
"cell_type": "code",
|
| 33 |
-
"execution_count": null,
|
| 34 |
-
"metadata": {},
|
| 35 |
-
"outputs": [],
|
| 36 |
-
"source": [
|
| 37 |
-
"import os\n",
|
| 38 |
-
"import sys\n",
|
| 39 |
-
"import subprocess\n",
|
| 40 |
-
"import importlib\n",
|
| 41 |
-
"import json\n",
|
| 42 |
-
"import shutil\n",
|
| 43 |
-
"from pathlib import Path\n",
|
| 44 |
-
"\n",
|
| 45 |
-
"IN_COLAB = False\n",
|
| 46 |
-
"try:\n",
|
| 47 |
-
" import google.colab\n",
|
| 48 |
-
" IN_COLAB = True\n",
|
| 49 |
-
"except ImportError:\n",
|
| 50 |
-
" pass\n",
|
| 51 |
-
"\n",
|
| 52 |
-
"print(f\"Running in Google Colab: {IN_COLAB}\")\n",
|
| 53 |
-
"print(f\"Python version: {sys.version}\")\n",
|
| 54 |
-
"print(f\"Working directory: {os.getcwd()}\")\n"
|
| 55 |
-
]
|
| 56 |
-
},
|
| 57 |
-
{
|
| 58 |
-
"cell_type": "markdown",
|
| 59 |
-
"metadata": {},
|
| 60 |
-
"source": [
|
| 61 |
-
"## 2. Repository Setup\n",
|
| 62 |
-
"\n",
|
| 63 |
-
"Clone the latest code from the repository. In Colab, this pulls fresh code each runtime.\n",
|
| 64 |
-
"Locally, it ensures the working directory is correct."
|
| 65 |
-
]
|
| 66 |
-
},
|
| 67 |
-
{
|
| 68 |
-
"cell_type": "code",
|
| 69 |
-
"execution_count": null,
|
| 70 |
-
"metadata": {},
|
| 71 |
-
"outputs": [],
|
| 72 |
-
"source": [
|
| 73 |
-
"# ── 仓库地址配置 ─────────────────────────────────────────────────────────────\n",
|
| 74 |
-
"# import os; os.environ[\"EASYTRANSLATE_REPO_URL\"] = \"https://github.com/your-org/your-repo.git\"\n",
|
| 75 |
-
"REPO_URL = os.environ.get(\n",
|
| 76 |
-
" \"EASYTRANSLATE_REPO_URL\",\n",
|
| 77 |
-
" \"https://huggingface.co/sdfjliom/UCAS-EasyTranslate\",\n",
|
| 78 |
-
")\n",
|
| 79 |
-
"_DEFAULT_COLAB_DIR = Path(\"/content/UCAS-EasyTranslate\")\n",
|
| 80 |
-
"\n",
|
| 81 |
-
"\n",
|
| 82 |
-
"def _is_repo_root(path: Path) -> bool:\n",
|
| 83 |
-
" return (path / \"src\" / \"easytranslate\").exists() and (path / \"setup.py\").exists()\n",
|
| 84 |
-
"\n",
|
| 85 |
-
"\n",
|
| 86 |
-
"def _find_repo_root(start_path: Path):\n",
|
| 87 |
-
" p = start_path.resolve()\n",
|
| 88 |
-
" for candidate in [p] + list(p.parents):\n",
|
| 89 |
-
" if _is_repo_root(candidate):\n",
|
| 90 |
-
" return candidate\n",
|
| 91 |
-
" return None\n",
|
| 92 |
-
"\n",
|
| 93 |
-
"\n",
|
| 94 |
-
"resolved_repo = None\n",
|
| 95 |
-
"\n",
|
| 96 |
-
"if IN_COLAB:\n",
|
| 97 |
-
" if _is_repo_root(_DEFAULT_COLAB_DIR):\n",
|
| 98 |
-
" resolved_repo = _DEFAULT_COLAB_DIR\n",
|
| 99 |
-
" else:\n",
|
| 100 |
-
" print(f\"Cloning repository from: {REPO_URL}\")\n",
|
| 101 |
-
" try:\n",
|
| 102 |
-
" subprocess.run(\n",
|
| 103 |
-
" [\"git\", \"lfs\", \"install\"], check=False,\n",
|
| 104 |
-
" stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n",
|
| 105 |
-
" )\n",
|
| 106 |
-
" subprocess.run(\n",
|
| 107 |
-
" [\"git\", \"clone\", \"--depth\", \"1\", REPO_URL, str(_DEFAULT_COLAB_DIR)],\n",
|
| 108 |
-
" check=True,\n",
|
| 109 |
-
" stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,\n",
|
| 110 |
-
" )\n",
|
| 111 |
-
" resolved_repo = _DEFAULT_COLAB_DIR\n",
|
| 112 |
-
" print(f\"Cloned to: {_DEFAULT_COLAB_DIR}\")\n",
|
| 113 |
-
" except subprocess.CalledProcessError as e:\n",
|
| 114 |
-
" print(f\"Clone failed:\\n{e.stdout}\")\n",
|
| 115 |
-
"\n",
|
| 116 |
-
" # Fallback: search Drive or current dir\n",
|
| 117 |
-
" if resolved_repo is None:\n",
|
| 118 |
-
" for candidate in [\n",
|
| 119 |
-
" Path.cwd(),\n",
|
| 120 |
-
" Path(\"/content\"),\n",
|
| 121 |
-
" Path(\"/content/drive/MyDrive/UCAS-EasyTranslate\"),\n",
|
| 122 |
-
" Path(\"/content/drive/MyDrive/Colab Notebooks/UCAS-EasyTranslate\"),\n",
|
| 123 |
-
" ]:\n",
|
| 124 |
-
" root = _find_repo_root(candidate)\n",
|
| 125 |
-
" if root:\n",
|
| 126 |
-
" resolved_repo = root\n",
|
| 127 |
-
" print(f\"Found existing repo at: {resolved_repo}\")\n",
|
| 128 |
-
" break\n",
|
| 129 |
-
"\n",
|
| 130 |
-
" if resolved_repo is None:\n",
|
| 131 |
-
" raise FileNotFoundError(\n",
|
| 132 |
-
" \"Cannot locate UCAS-EasyTranslate repository.\\n\"\n",
|
| 133 |
-
" \"Options:\\n\"\n",
|
| 134 |
-
" \" (1) Set EASYTRANSLATE_REPO_URL to a publicly accessible git URL.\\n\"\n",
|
| 135 |
-
" \" (2) Manually clone to /content/UCAS-EasyTranslate.\\n\"\n",
|
| 136 |
-
" \" (3) Place repo in Google Drive and mount Drive first.\"\n",
|
| 137 |
-
" )\n",
|
| 138 |
-
"else:\n",
|
| 139 |
-
" resolved_repo = _find_repo_root(Path.cwd()) or Path.cwd()\n",
|
| 140 |
-
" print(f\"Using local repository at: {resolved_repo}\")\n",
|
| 141 |
-
"\n",
|
| 142 |
-
"# Always keep REPO_DIR in sync with wherever the repo actually is\n",
|
| 143 |
-
"REPO_DIR = Path(resolved_repo)\n",
|
| 144 |
-
"os.chdir(REPO_DIR)\n",
|
| 145 |
-
"sys.path.insert(0, str(REPO_DIR / \"src\"))\n",
|
| 146 |
-
"print(f\"Repository directory: {REPO_DIR}\")\n"
|
| 147 |
-
]
|
| 148 |
-
},
|
| 149 |
-
{
|
| 150 |
-
"cell_type": "markdown",
|
| 151 |
-
"metadata": {},
|
| 152 |
-
"source": [
|
| 153 |
-
"## 3. Dependency Installation\n",
|
| 154 |
-
"\n",
|
| 155 |
-
"Install all required packages. In Colab, PyTorch is pre-installed."
|
| 156 |
-
]
|
| 157 |
-
},
|
| 158 |
-
{
|
| 159 |
-
"cell_type": "code",
|
| 160 |
-
"execution_count": null,
|
| 161 |
-
"metadata": {},
|
| 162 |
-
"outputs": [],
|
| 163 |
-
"source": [
|
| 164 |
-
"if IN_COLAB:\n",
|
| 165 |
-
" # 1. Only install packages that Colab does NOT ship.\n",
|
| 166 |
-
" # Do NOT touch torch/numpy/pandas — Colab's preinstalled versions are fine.\n",
|
| 167 |
-
" %pip install -q --upgrade pip setuptools wheel\n",
|
| 168 |
-
"\n",
|
| 169 |
-
" # Core HuggingFace packages (Colab may have older versions)\n",
|
| 170 |
-
" %pip install -q \"transformers>=4.36.0,<4.45.0\" \\\n",
|
| 171 |
-
" \"datasets>=2.16.0,<3.0.0\" \\\n",
|
| 172 |
-
" \"tokenizers>=0.15.0,<0.20.0\" \\\n",
|
| 173 |
-
" \"sentencepiece>=0.2.0\" \\\n",
|
| 174 |
-
" \"accelerate>=0.25.0,<0.35.0\" \\\n",
|
| 175 |
-
" \"peft>=0.7.0,<0.12.0\"\n",
|
| 176 |
-
"\n",
|
| 177 |
-
" # Evaluation / config packages\n",
|
| 178 |
-
" %pip install -q \"sacrebleu>=2.4.0\" \\\n",
|
| 179 |
-
" \"omegaconf>=2.3.0,<3.0.0\" \\\n",
|
| 180 |
-
" \"rich>=13.0.0\"\n",
|
| 181 |
-
"\n",
|
| 182 |
-
" # 2. Install project code without re-resolving heavy dependencies.\n",
|
| 183 |
-
" # (torch, numpy, etc. are already present from Colab runtime.)\n",
|
| 184 |
-
" %pip install -q --no-deps -e .\n",
|
| 185 |
-
"\n",
|
| 186 |
-
" print(\"All packages installed successfully.\")\n",
|
| 187 |
-
"else:\n",
|
| 188 |
-
" %pip install -q --upgrade pip setuptools wheel\n",
|
| 189 |
-
" %pip install -q -r requirements.txt\n",
|
| 190 |
-
" %pip install -q -e .\n",
|
| 191 |
-
" print(\"Dependencies installed successfully.\")\n"
|
| 192 |
-
]
|
| 193 |
-
},
|
| 194 |
-
{
|
| 195 |
-
"cell_type": "markdown",
|
| 196 |
-
"metadata": {},
|
| 197 |
-
"source": [
|
| 198 |
-
"## 4. GPU Verification\n",
|
| 199 |
-
"\n",
|
| 200 |
-
"Verify GPU availability and display hardware information."
|
| 201 |
-
]
|
| 202 |
-
},
|
| 203 |
-
{
|
| 204 |
-
"cell_type": "code",
|
| 205 |
-
"execution_count": null,
|
| 206 |
-
"metadata": {},
|
| 207 |
-
"outputs": [],
|
| 208 |
-
"source": [
|
| 209 |
-
"import torch\n",
|
| 210 |
-
"\n",
|
| 211 |
-
"print(f\"PyTorch version: {torch.__version__}\")\n",
|
| 212 |
-
"print(f\"CUDA available: {torch.cuda.is_available()}\")\n",
|
| 213 |
-
"\n",
|
| 214 |
-
"if torch.cuda.is_available():\n",
|
| 215 |
-
" print(f\"CUDA version: {torch.version.cuda}\")\n",
|
| 216 |
-
" print(f\"GPU count: {torch.cuda.device_count()}\")\n",
|
| 217 |
-
" for i in range(torch.cuda.device_count()):\n",
|
| 218 |
-
" print(f\" GPU {i}: {torch.cuda.get_device_name(i)}\")\n",
|
| 219 |
-
" props = torch.cuda.get_device_properties(i)\n",
|
| 220 |
-
" print(f\" Memory: {props.total_memory / 1024**3:.1f} GB\")\n",
|
| 221 |
-
" print(f\" Compute Capability: {props.major}.{props.minor}\")\n",
|
| 222 |
-
"else:\n",
|
| 223 |
-
" print(\"WARNING: No GPU detected. Training will be very slow on CPU.\")"
|
| 224 |
-
]
|
| 225 |
-
},
|
| 226 |
-
{
|
| 227 |
-
"cell_type": "markdown",
|
| 228 |
-
"metadata": {},
|
| 229 |
-
"source": [
|
| 230 |
-
"## 5. Google Drive Mount (Colab Only)\n",
|
| 231 |
-
"\n",
|
| 232 |
-
"Mount Google Drive for persistent storage of checkpoints and results."
|
| 233 |
-
]
|
| 234 |
-
},
|
| 235 |
-
{
|
| 236 |
-
"cell_type": "code",
|
| 237 |
-
"execution_count": null,
|
| 238 |
-
"metadata": {},
|
| 239 |
-
"outputs": [],
|
| 240 |
-
"source": [
|
| 241 |
-
"DRIVE_MOUNTED = False\n",
|
| 242 |
-
"DRIVE_BASE = \"/content/drive/MyDrive/EasyTranslate\"\n",
|
| 243 |
-
"\n",
|
| 244 |
-
"if IN_COLAB:\n",
|
| 245 |
-
" from google.colab import drive\n",
|
| 246 |
-
" drive.mount(\"/content/drive\")\n",
|
| 247 |
-
" DRIVE_MOUNTED = os.path.exists(\"/content/drive\")\n",
|
| 248 |
-
" if DRIVE_MOUNTED:\n",
|
| 249 |
-
" os.makedirs(DRIVE_BASE, exist_ok=True)\n",
|
| 250 |
-
" print(f\"Google Drive mounted. Base path: {DRIVE_BASE}\")\n",
|
| 251 |
-
" else:\n",
|
| 252 |
-
" print(\"WARNING: Google Drive mount failed\")\n",
|
| 253 |
-
"else:\n",
|
| 254 |
-
" print(\"Not in Colab, skipping Google Drive mount\")"
|
| 255 |
-
]
|
| 256 |
-
},
|
| 257 |
-
{
|
| 258 |
-
"cell_type": "markdown",
|
| 259 |
-
"metadata": {},
|
| 260 |
-
"source": [
|
| 261 |
-
"## 6. Configuration Loading\n",
|
| 262 |
-
"\n",
|
| 263 |
-
"Load and display the project configuration."
|
| 264 |
-
]
|
| 265 |
-
},
|
| 266 |
-
{
|
| 267 |
-
"cell_type": "code",
|
| 268 |
-
"execution_count": null,
|
| 269 |
-
"metadata": {},
|
| 270 |
-
"outputs": [],
|
| 271 |
-
"source": [
|
| 272 |
-
"import torch\n",
|
| 273 |
-
"import numpy as np\n",
|
| 274 |
-
"\n",
|
| 275 |
-
"print(f\"NumPy version : {np.__version__}\")\n",
|
| 276 |
-
"print(f\"PyTorch version: {torch.__version__}\")\n",
|
| 277 |
-
"\n",
|
| 278 |
-
"from easytranslate.utils.config import load_config, config_to_dict\n",
|
| 279 |
-
"from easytranslate.utils.seed import set_seed\n",
|
| 280 |
-
"from easytranslate.utils.logging import setup_logging\n",
|
| 281 |
-
"\n",
|
| 282 |
-
"config = load_config(\"configs/default_config.yaml\")\n",
|
| 283 |
-
"config_dict = config_to_dict(config)\n",
|
| 284 |
-
"\n",
|
| 285 |
-
"exp_cfg = config_dict.get(\"experiment\", {})\n",
|
| 286 |
-
"seed = exp_cfg.get(\"seed\", 42)\n",
|
| 287 |
-
"set_seed(seed)\n",
|
| 288 |
-
"\n",
|
| 289 |
-
"log_cfg = config_dict.get(\"logging\", {})\n",
|
| 290 |
-
"setup_logging(\n",
|
| 291 |
-
" log_dir=log_cfg.get(\"log_dir\", \"logs/\"),\n",
|
| 292 |
-
" log_file=\"easytranslate.log\",\n",
|
| 293 |
-
")\n",
|
| 294 |
-
"\n",
|
| 295 |
-
"# ── Colab runtime overrides ───────────────────────────────────────────────────\n",
|
| 296 |
-
"if IN_COLAB:\n",
|
| 297 |
-
" train_cfg = config_dict.setdefault(\"training\", {})\n",
|
| 298 |
-
"\n",
|
| 299 |
-
" # Mixed precision: use bf16 on Ampere+ GPUs, fp16 otherwise, none on CPU\n",
|
| 300 |
-
" if torch.cuda.is_available():\n",
|
| 301 |
-
" gpu_cap = torch.cuda.get_device_capability(0)\n",
|
| 302 |
-
" if gpu_cap[0] >= 8: # A100/A10 → bf16\n",
|
| 303 |
-
" train_cfg[\"fp16\"] = False\n",
|
| 304 |
-
" train_cfg[\"bf16\"] = True\n",
|
| 305 |
-
" else: # T4/P100/V100 → fp16\n",
|
| 306 |
-
" train_cfg[\"fp16\"] = True\n",
|
| 307 |
-
" train_cfg[\"bf16\"] = False\n",
|
| 308 |
-
" else:\n",
|
| 309 |
-
" train_cfg[\"fp16\"] = False\n",
|
| 310 |
-
" train_cfg[\"bf16\"] = False\n",
|
| 311 |
-
"\n",
|
| 312 |
-
" # Reduce epochs for a Colab demo run\n",
|
| 313 |
-
" train_cfg.setdefault(\"epochs\", 10)\n",
|
| 314 |
-
"\n",
|
| 315 |
-
" # Colab-friendly batch / gradient accumulation settings\n",
|
| 316 |
-
" train_cfg.setdefault(\"batch_size\", 32)\n",
|
| 317 |
-
" train_cfg.setdefault(\"gradient_accumulation_steps\", 4)\n",
|
| 318 |
-
"\n",
|
| 319 |
-
"print(f\"Configuration loaded. Experiment seed: {seed}\")\n",
|
| 320 |
-
"print(f\"Model type : {config_dict['model']['type']}\")\n",
|
| 321 |
-
"print(f\"Training : epochs={config_dict['training']['epochs']}, \"\n",
|
| 322 |
-
" f\"fp16={config_dict['training']['fp16']}, \"\n",
|
| 323 |
-
" f\"bf16={config_dict['training']['bf16']}\")\n"
|
| 324 |
-
]
|
| 325 |
-
},
|
| 326 |
-
{
|
| 327 |
-
"cell_type": "markdown",
|
| 328 |
-
"metadata": {},
|
| 329 |
-
"source": [
|
| 330 |
-
"## 7. Data Loading & Preprocessing\n",
|
| 331 |
-
"\n",
|
| 332 |
-
"Load the WMT19 zh-en dataset, train the BPE tokenizer, and prepare DataLoaders."
|
| 333 |
-
]
|
| 334 |
-
},
|
| 335 |
-
{
|
| 336 |
-
"cell_type": "code",
|
| 337 |
-
"execution_count": null,
|
| 338 |
-
"metadata": {},
|
| 339 |
-
"outputs": [],
|
| 340 |
-
"source": [
|
| 341 |
-
"from easytranslate.data import (\n",
|
| 342 |
-
" TranslationDataset,\n",
|
| 343 |
-
" TranslationCollator,\n",
|
| 344 |
-
" DynamicBatchSampler,\n",
|
| 345 |
-
" build_tokenizer,\n",
|
| 346 |
-
" load_wmt_dataset,\n",
|
| 347 |
-
" preprocess_pipeline,\n",
|
| 348 |
-
")\n",
|
| 349 |
-
"from torch.utils.data import DataLoader\n",
|
| 350 |
-
"\n",
|
| 351 |
-
"data_cfg = config_dict.get(\"data\", {})\n",
|
| 352 |
-
"preproc_cfg = data_cfg.get(\"preprocessing\", {})\n",
|
| 353 |
-
"loader_cfg = data_cfg.get(\"dataloader\", {})\n",
|
| 354 |
-
"\n",
|
| 355 |
-
"# ── Cap dataset sizes for Colab to avoid RAM/time issues ─────────────────────\n",
|
| 356 |
-
"MAX_TRAIN_SAMPLES = 200_000 if IN_COLAB else None # None → use full dataset\n",
|
| 357 |
-
"MAX_VAL_SAMPLES = 5_000 if IN_COLAB else None\n",
|
| 358 |
-
"\n",
|
| 359 |
-
"print(\"Loading WMT19 zh-en dataset (this may take several minutes on first run)...\")\n",
|
| 360 |
-
"raw_dataset = load_wmt_dataset(\n",
|
| 361 |
-
" year=data_cfg.get(\"wmt\", {}).get(\"year\", \"19\"),\n",
|
| 362 |
-
" language_pair=data_cfg.get(\"wmt\", {}).get(\"language_pair\", \"zh-en\"),\n",
|
| 363 |
-
" src_lang=\"en\",\n",
|
| 364 |
-
" tgt_lang=\"zh\",\n",
|
| 365 |
-
")\n",
|
| 366 |
-
"\n",
|
| 367 |
-
"train_raw = raw_dataset[\"train\"]\n",
|
| 368 |
-
"\n",
|
| 369 |
-
"# Prefer \"validation\", fall back to \"dev\", then use a small slice of train\n",
|
| 370 |
-
"_val_split_name = next(\n",
|
| 371 |
-
" (k for k in (\"validation\", \"dev\", \"valid\") if k in raw_dataset),\n",
|
| 372 |
-
" None,\n",
|
| 373 |
-
")\n",
|
| 374 |
-
"val_raw = raw_dataset[_val_split_name] if _val_split_name else train_raw\n",
|
| 375 |
-
"\n",
|
| 376 |
-
"print(f\"Raw training samples : {len(train_raw['src'])}\")\n",
|
| 377 |
-
"print(f\"Raw validation samples: {len(val_raw['src'])} \"\n",
|
| 378 |
-
" f\"(split='{_val_split_name or 'train (fallback)'}')\")\n",
|
| 379 |
-
"\n",
|
| 380 |
-
"# Apply sample caps BEFORE preprocessing to save time\n",
|
| 381 |
-
"train_src_raw = train_raw[\"src\"][:MAX_TRAIN_SAMPLES] if MAX_TRAIN_SAMPLES else list(train_raw[\"src\"])\n",
|
| 382 |
-
"train_tgt_raw = train_raw[\"tgt\"][:MAX_TRAIN_SAMPLES] if MAX_TRAIN_SAMPLES else list(train_raw[\"tgt\"])\n",
|
| 383 |
-
"val_src_raw = val_raw[\"src\"][:MAX_VAL_SAMPLES] if MAX_VAL_SAMPLES else list(val_raw[\"src\"])\n",
|
| 384 |
-
"val_tgt_raw = val_raw[\"tgt\"][:MAX_VAL_SAMPLES] if MAX_VAL_SAMPLES else list(val_raw[\"tgt\"])\n",
|
| 385 |
-
"\n",
|
| 386 |
-
"print(f\"\\nPreprocessing training data ({len(train_src_raw)} samples)...\")\n",
|
| 387 |
-
"train_src, train_tgt = preprocess_pipeline(\n",
|
| 388 |
-
" train_src_raw, train_tgt_raw,\n",
|
| 389 |
-
" max_src_len=preproc_cfg.get(\"max_src_len\", 256),\n",
|
| 390 |
-
" max_tgt_len=preproc_cfg.get(\"max_tgt_len\", 256),\n",
|
| 391 |
-
" filter_by_length_enabled=preproc_cfg.get(\"filter_by_length\", True),\n",
|
| 392 |
-
" length_ratio_threshold=preproc_cfg.get(\"length_ratio_threshold\", 3.0),\n",
|
| 393 |
-
")\n",
|
| 394 |
-
"print(f\"Preprocessed training samples : {len(train_src)}\")\n",
|
| 395 |
-
"\n",
|
| 396 |
-
"print(f\"Preprocessing validation data ({len(val_src_raw)} samples)...\")\n",
|
| 397 |
-
"val_src, val_tgt = preprocess_pipeline(\n",
|
| 398 |
-
" val_src_raw, val_tgt_raw,\n",
|
| 399 |
-
" max_src_len=preproc_cfg.get(\"max_src_len\", 256),\n",
|
| 400 |
-
" max_tgt_len=preproc_cfg.get(\"max_tgt_len\", 256),\n",
|
| 401 |
-
" filter_by_length_enabled=preproc_cfg.get(\"filter_by_length\", True),\n",
|
| 402 |
-
" length_ratio_threshold=preproc_cfg.get(\"length_ratio_threshold\", 3.0),\n",
|
| 403 |
-
")\n",
|
| 404 |
-
"print(f\"Preprocessed validation samples: {len(val_src)}\")\n"
|
| 405 |
-
]
|
| 406 |
-
},
|
| 407 |
-
{
|
| 408 |
-
"cell_type": "markdown",
|
| 409 |
-
"metadata": {},
|
| 410 |
-
"source": [
|
| 411 |
-
"## 8. Tokenizer Training\n",
|
| 412 |
-
"\n",
|
| 413 |
-
"Train a byte-level BPE tokenizer on the combined source and target texts."
|
| 414 |
-
]
|
| 415 |
-
},
|
| 416 |
-
{
|
| 417 |
-
"cell_type": "code",
|
| 418 |
-
"execution_count": null,
|
| 419 |
-
"metadata": {},
|
| 420 |
-
"outputs": [],
|
| 421 |
-
"source": [
|
| 422 |
-
"tok_cfg = config_dict.get(\"tokenizer\", {})\n",
|
| 423 |
-
"\n",
|
| 424 |
-
"print(\"Building tokenizer...\")\n",
|
| 425 |
-
"all_train_texts = train_src + train_tgt\n",
|
| 426 |
-
"tokenizer = build_tokenizer(tok_cfg, train_texts=all_train_texts)\n",
|
| 427 |
-
"\n",
|
| 428 |
-
"print(f\"Tokenizer vocabulary size: {tokenizer.vocab_size}\")\n",
|
| 429 |
-
"print(f\"Special tokens: PAD={tokenizer.pad_token_id}, BOS={tokenizer.bos_token_id}, EOS={tokenizer.eos_token_id}\")\n",
|
| 430 |
-
"\n",
|
| 431 |
-
"test_encode = tokenizer.encode(\"Hello world\", add_special_tokens=True)\n",
|
| 432 |
-
"test_decode = tokenizer.decode(test_encode)\n",
|
| 433 |
-
"print(f\"Encode test: {test_encode[:10]}...\")\n",
|
| 434 |
-
"print(f\"Decode test: {test_decode[:50]}...\")"
|
| 435 |
-
]
|
| 436 |
-
},
|
| 437 |
-
{
|
| 438 |
-
"cell_type": "markdown",
|
| 439 |
-
"metadata": {},
|
| 440 |
-
"source": [
|
| 441 |
-
"## 9. Dataset & DataLoader Construction\n",
|
| 442 |
-
"\n",
|
| 443 |
-
"Build PyTorch datasets and dataloaders with dynamic batching."
|
| 444 |
-
]
|
| 445 |
-
},
|
| 446 |
-
{
|
| 447 |
-
"cell_type": "code",
|
| 448 |
-
"execution_count": null,
|
| 449 |
-
"metadata": {},
|
| 450 |
-
"outputs": [],
|
| 451 |
-
"source": [
|
| 452 |
-
"max_src_len = preproc_cfg.get(\"max_src_len\", 256)\n",
|
| 453 |
-
"max_tgt_len = preproc_cfg.get(\"max_tgt_len\", 256)\n",
|
| 454 |
-
"\n",
|
| 455 |
-
"train_dataset = TranslationDataset(\n",
|
| 456 |
-
" train_src, train_tgt,\n",
|
| 457 |
-
" tokenizer=tokenizer,\n",
|
| 458 |
-
" max_src_len=max_src_len,\n",
|
| 459 |
-
" max_tgt_len=max_tgt_len,\n",
|
| 460 |
-
")\n",
|
| 461 |
-
"val_dataset = TranslationDataset(\n",
|
| 462 |
-
" val_src, val_tgt,\n",
|
| 463 |
-
" tokenizer=tokenizer,\n",
|
| 464 |
-
" max_src_len=max_src_len,\n",
|
| 465 |
-
" max_tgt_len=max_tgt_len,\n",
|
| 466 |
-
")\n",
|
| 467 |
-
"\n",
|
| 468 |
-
"collator = TranslationCollator(\n",
|
| 469 |
-
" pad_token_id=tokenizer.pad_token_id,\n",
|
| 470 |
-
" label_pad_token_id=-100,\n",
|
| 471 |
-
")\n",
|
| 472 |
-
"\n",
|
| 473 |
-
"loader_cfg = config_dict.get(\"data\", {}).get(\"dataloader\", {})\n",
|
| 474 |
-
"batch_size = loader_cfg.get(\"batch_size\", 32)\n",
|
| 475 |
-
"# 2 workers on GPU Colab; 0 on CPU (no multiprocessing overhead)\n",
|
| 476 |
-
"num_workers = 2 if IN_COLAB and torch.cuda.is_available() else 0\n",
|
| 477 |
-
"# Disable dynamic batching on Colab: computing exact token lengths for 200k\n",
|
| 478 |
-
"# sentences requires a full tokenizer pass and can take 30+ minutes.\n",
|
| 479 |
-
"use_dynamic = loader_cfg.get(\"dynamic_batching\", True) and not IN_COLAB\n",
|
| 480 |
-
"\n",
|
| 481 |
-
"if use_dynamic:\n",
|
| 482 |
-
" max_tokens = loader_cfg.get(\"max_tokens_per_batch\", 8192)\n",
|
| 483 |
-
" print(\"Computing sequence lengths for dynamic batching (approximate)...\")\n",
|
| 484 |
-
"\n",
|
| 485 |
-
" def _approx_len(src_text: str, tgt_text: str) -> int:\n",
|
| 486 |
-
" \"\"\"Fast character-based length estimate — no tokenizer call needed.\"\"\"\n",
|
| 487 |
-
" def _tok_est(t: str) -> int:\n",
|
| 488 |
-
" cjk = sum(1 for c in t if \"\\u4e00\" <= c <= \"\\u9fff\")\n",
|
| 489 |
-
" return (len(t) - cjk) // 4 + cjk + 2 # rough BPE estimate\n",
|
| 490 |
-
" return max(_tok_est(src_text), _tok_est(tgt_text))\n",
|
| 491 |
-
"\n",
|
| 492 |
-
" train_lengths = [_approx_len(s, t) for s, t in zip(train_src, train_tgt)]\n",
|
| 493 |
-
" train_sampler = DynamicBatchSampler(\n",
|
| 494 |
-
" train_lengths,\n",
|
| 495 |
-
" max_tokens_per_batch=max_tokens,\n",
|
| 496 |
-
" shuffle=True,\n",
|
| 497 |
-
" )\n",
|
| 498 |
-
" train_loader = DataLoader(\n",
|
| 499 |
-
" train_dataset,\n",
|
| 500 |
-
" batch_sampler=train_sampler,\n",
|
| 501 |
-
" collate_fn=collator,\n",
|
| 502 |
-
" num_workers=num_workers,\n",
|
| 503 |
-
" pin_memory=torch.cuda.is_available(),\n",
|
| 504 |
-
" )\n",
|
| 505 |
-
"else:\n",
|
| 506 |
-
" if IN_COLAB and loader_cfg.get(\"dynamic_batching\", True):\n",
|
| 507 |
-
" print(\"Note: dynamic batching disabled on Colab (would require full tokenizer pass on all samples).\")\n",
|
| 508 |
-
" train_loader = DataLoader(\n",
|
| 509 |
-
" train_dataset,\n",
|
| 510 |
-
" batch_size=batch_size,\n",
|
| 511 |
-
" shuffle=True,\n",
|
| 512 |
-
" collate_fn=collator,\n",
|
| 513 |
-
" num_workers=num_workers,\n",
|
| 514 |
-
" pin_memory=torch.cuda.is_available(),\n",
|
| 515 |
-
" )\n",
|
| 516 |
-
"\n",
|
| 517 |
-
"val_loader = DataLoader(\n",
|
| 518 |
-
" val_dataset,\n",
|
| 519 |
-
" batch_size=batch_size,\n",
|
| 520 |
-
" shuffle=False,\n",
|
| 521 |
-
" collate_fn=collator,\n",
|
| 522 |
-
" num_workers=num_workers,\n",
|
| 523 |
-
" pin_memory=torch.cuda.is_available(),\n",
|
| 524 |
-
")\n",
|
| 525 |
-
"\n",
|
| 526 |
-
"print(f\"Training batches : ~{len(train_loader)}\")\n",
|
| 527 |
-
"print(f\"Validation batches: {len(val_loader)}\")\n",
|
| 528 |
-
"\n",
|
| 529 |
-
"sample_batch = next(iter(train_loader))\n",
|
| 530 |
-
"print(\"Sample batch shapes:\")\n",
|
| 531 |
-
"for k, v in sample_batch.items():\n",
|
| 532 |
-
" if isinstance(v, torch.Tensor):\n",
|
| 533 |
-
" print(f\" {k}: {list(v.shape)}\")\n"
|
| 534 |
-
]
|
| 535 |
-
},
|
| 536 |
-
{
|
| 537 |
-
"cell_type": "markdown",
|
| 538 |
-
"metadata": {},
|
| 539 |
-
"source": [
|
| 540 |
-
"## 10. Model Construction\n",
|
| 541 |
-
"\n",
|
| 542 |
-
"Build the Transformer model based on configuration."
|
| 543 |
-
]
|
| 544 |
-
},
|
| 545 |
-
{
|
| 546 |
-
"cell_type": "code",
|
| 547 |
-
"execution_count": null,
|
| 548 |
-
"metadata": {},
|
| 549 |
-
"outputs": [],
|
| 550 |
-
"source": [
|
| 551 |
-
"from easytranslate.model import TransformerTranslationModel\n",
|
| 552 |
-
"\n",
|
| 553 |
-
"model_cfg = config_dict.get(\"model\", {})\n",
|
| 554 |
-
"model_type = model_cfg.get(\"type\", \"transformer_scratch\")\n",
|
| 555 |
-
"\n",
|
| 556 |
-
"if model_type == \"transformer_scratch\":\n",
|
| 557 |
-
" tf_cfg = dict(model_cfg.get(\"transformer\", {}))\n",
|
| 558 |
-
"\n",
|
| 559 |
-
" # ── Colab quick-run: smaller model to fit in Colab RAM/VRAM ──────────────\n",
|
| 560 |
-
" # Default full model: d_model=512, 6 enc/dec layers (~75 M params)\n",
|
| 561 |
-
" # Colab quick model: d_model=256, 3 enc/dec layers (~12 M params)\n",
|
| 562 |
-
" # Set COLAB_FULL_MODEL=1 in env to skip this override.\n",
|
| 563 |
-
" if IN_COLAB and not os.environ.get(\"COLAB_FULL_MODEL\"):\n",
|
| 564 |
-
" tf_cfg.setdefault(\"d_model\", 256)\n",
|
| 565 |
-
" tf_cfg.setdefault(\"nhead\", 4)\n",
|
| 566 |
-
" tf_cfg.setdefault(\"num_encoder_layers\", 3)\n",
|
| 567 |
-
" tf_cfg.setdefault(\"num_decoder_layers\", 3)\n",
|
| 568 |
-
" tf_cfg.setdefault(\"dim_feedforward\", 1024)\n",
|
| 569 |
-
" print(\"Colab mode: using compact model (d_model=256, 3 layers).\")\n",
|
| 570 |
-
" print(\"To use the full model, run: import os; os.environ['COLAB_FULL_MODEL']='1'\")\n",
|
| 571 |
-
"\n",
|
| 572 |
-
" model = TransformerTranslationModel(\n",
|
| 573 |
-
" src_vocab_size=tokenizer.vocab_size,\n",
|
| 574 |
-
" tgt_vocab_size=tokenizer.vocab_size,\n",
|
| 575 |
-
" d_model=tf_cfg.get(\"d_model\", 512),\n",
|
| 576 |
-
" nhead=tf_cfg.get(\"nhead\", 8),\n",
|
| 577 |
-
" num_encoder_layers=tf_cfg.get(\"num_encoder_layers\", 6),\n",
|
| 578 |
-
" num_decoder_layers=tf_cfg.get(\"num_decoder_layers\", 6),\n",
|
| 579 |
-
" dim_feedforward=tf_cfg.get(\"dim_feedforward\", 2048),\n",
|
| 580 |
-
" dropout=tf_cfg.get(\"dropout\", 0.1),\n",
|
| 581 |
-
" activation=tf_cfg.get(\"activation\", \"gelu\"),\n",
|
| 582 |
-
" max_seq_len=tf_cfg.get(\"max_seq_len\", 512),\n",
|
| 583 |
-
" use_flash_attention=tf_cfg.get(\"use_flash_attention\", True),\n",
|
| 584 |
-
" use_rotary_embedding=tf_cfg.get(\"use_rotary_embedding\", True),\n",
|
| 585 |
-
" pre_norm=tf_cfg.get(\"pre_norm\", True),\n",
|
| 586 |
-
" pad_id=tokenizer.pad_token_id,\n",
|
| 587 |
-
" share_embedding=False,\n",
|
| 588 |
-
" )\n",
|
| 589 |
-
" print(\"Built Transformer from scratch\")\n",
|
| 590 |
-
"\n",
|
| 591 |
-
"elif model_type in (\"finetune_nllb\", \"finetune_mbart\"):\n",
|
| 592 |
-
" from easytranslate.model.finetune import load_pretrained_model, setup_lora\n",
|
| 593 |
-
" pt_cfg = model_cfg.get(\"pretrained\", {})\n",
|
| 594 |
-
" model, hf_tokenizer = load_pretrained_model(\n",
|
| 595 |
-
" model_name=pt_cfg.get(\"model_name\", \"facebook/nllb-200-distilled-600M\"),\n",
|
| 596 |
-
" src_lang=pt_cfg.get(\"src_lang\", \"eng_Latn\"),\n",
|
| 597 |
-
" tgt_lang=pt_cfg.get(\"tgt_lang\", \"zho_Hans\"),\n",
|
| 598 |
-
" )\n",
|
| 599 |
-
" if pt_cfg.get(\"use_lora\", True):\n",
|
| 600 |
-
" lora_cfg = pt_cfg.get(\"lora\", {})\n",
|
| 601 |
-
" model = setup_lora(\n",
|
| 602 |
-
" model,\n",
|
| 603 |
-
" r=lora_cfg.get(\"r\", 16),\n",
|
| 604 |
-
" alpha=lora_cfg.get(\"alpha\", 32),\n",
|
| 605 |
-
" dropout=lora_cfg.get(\"dropout\", 0.05),\n",
|
| 606 |
-
" target_modules=lora_cfg.get(\"target_modules\", [\"q_proj\", \"v_proj\"]),\n",
|
| 607 |
-
" )\n",
|
| 608 |
-
" print(f\"Loaded pretrained model: {pt_cfg.get('model_name')}\")\n",
|
| 609 |
-
"\n",
|
| 610 |
-
"else:\n",
|
| 611 |
-
" raise ValueError(f\"Unknown model type: {model_type}\")\n",
|
| 612 |
-
"\n",
|
| 613 |
-
"total_params = sum(p.numel() for p in model.parameters())\n",
|
| 614 |
-
"trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
|
| 615 |
-
"print(f\"Total parameters : {total_params:,}\")\n",
|
| 616 |
-
"print(f\"Trainable parameters : {trainable_params:,}\")\n",
|
| 617 |
-
"print(f\"Trainable ratio : {100 * trainable_params / total_params:.2f}%\")\n"
|
| 618 |
-
]
|
| 619 |
-
},
|
| 620 |
-
{
|
| 621 |
-
"cell_type": "markdown",
|
| 622 |
-
"metadata": {},
|
| 623 |
-
"source": [
|
| 624 |
-
"## 11. Quick Forward Pass Test\n",
|
| 625 |
-
"\n",
|
| 626 |
-
"Verify the model can perform a forward pass with correct output dimensions."
|
| 627 |
-
]
|
| 628 |
-
},
|
| 629 |
-
{
|
| 630 |
-
"cell_type": "code",
|
| 631 |
-
"execution_count": null,
|
| 632 |
-
"metadata": {},
|
| 633 |
-
"outputs": [],
|
| 634 |
-
"source": [
|
| 635 |
-
"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
|
| 636 |
-
"model = model.to(device)\n",
|
| 637 |
-
"model.eval()\n",
|
| 638 |
-
"\n",
|
| 639 |
-
"test_batch = next(iter(train_loader))\n",
|
| 640 |
-
"test_src = test_batch[\"src_ids\"][:2].to(device)\n",
|
| 641 |
-
"test_tgt = test_batch[\"tgt_input_ids\"][:2].to(device)\n",
|
| 642 |
-
"test_src_mask = test_batch[\"src_padding_mask\"][:2].to(device)\n",
|
| 643 |
-
"test_tgt_mask = test_batch[\"tgt_padding_mask\"][:2].to(device)\n",
|
| 644 |
-
"\n",
|
| 645 |
-
"with torch.no_grad():\n",
|
| 646 |
-
" logits = model(test_src, test_tgt, test_src_mask, test_tgt_mask)\n",
|
| 647 |
-
"\n",
|
| 648 |
-
"print(f\"Input src shape: {list(test_src.shape)}\")\n",
|
| 649 |
-
"print(f\"Input tgt shape: {list(test_tgt.shape)}\")\n",
|
| 650 |
-
"print(f\"Output logits shape: {list(logits.shape)}\")\n",
|
| 651 |
-
"print(f\"Expected output shape: [B, T, vocab_size] = [{test_tgt.size(0)}, {test_tgt.size(1)}, {tokenizer.vocab_size}]\")\n",
|
| 652 |
-
"assert logits.size(-1) == tokenizer.vocab_size, f\"Vocab size mismatch: {logits.size(-1)} vs {tokenizer.vocab_size}\"\n",
|
| 653 |
-
"print(\"Forward pass test PASSED\")"
|
| 654 |
-
]
|
| 655 |
-
},
|
| 656 |
-
{
|
| 657 |
-
"cell_type": "markdown",
|
| 658 |
-
"metadata": {},
|
| 659 |
-
"source": [
|
| 660 |
-
"## 12. Training Execution\n",
|
| 661 |
-
"\n",
|
| 662 |
-
"This cell DEFINES the training setup but does NOT execute training automatically.\n",
|
| 663 |
-
"To start training, run the cell below this one."
|
| 664 |
-
]
|
| 665 |
-
},
|
| 666 |
-
{
|
| 667 |
-
"cell_type": "code",
|
| 668 |
-
"execution_count": null,
|
| 669 |
-
"metadata": {},
|
| 670 |
-
"outputs": [],
|
| 671 |
-
"source": [
|
| 672 |
-
"# sacrebleu is now installed in the dependency cell above.\n",
|
| 673 |
-
"# This cell just verifies it is importable before we initialize the Trainer.\n",
|
| 674 |
-
"try:\n",
|
| 675 |
-
" importlib.import_module(\"sacrebleu\")\n",
|
| 676 |
-
" print(\"sacrebleu OK\")\n",
|
| 677 |
-
"except ImportError:\n",
|
| 678 |
-
" print(\"sacrebleu missing — installing now...\")\n",
|
| 679 |
-
" subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"sacrebleu>=2.4.0\"], check=True)\n",
|
| 680 |
-
"\n",
|
| 681 |
-
"from easytranslate.training import Trainer\n",
|
| 682 |
-
"from easytranslate.evaluation import Evaluator\n",
|
| 683 |
-
"\n",
|
| 684 |
-
"evaluator = Evaluator(\n",
|
| 685 |
-
" model=model,\n",
|
| 686 |
-
" tokenizer=tokenizer,\n",
|
| 687 |
-
" config=config_dict,\n",
|
| 688 |
-
")\n",
|
| 689 |
-
"\n",
|
| 690 |
-
"trainer = Trainer(\n",
|
| 691 |
-
" model=model,\n",
|
| 692 |
-
" train_loader=train_loader,\n",
|
| 693 |
-
" val_loader=val_loader,\n",
|
| 694 |
-
" config=config_dict,\n",
|
| 695 |
-
" evaluator=evaluator,\n",
|
| 696 |
-
")\n",
|
| 697 |
-
"\n",
|
| 698 |
-
"# Output / plot directories live inside the repo\n",
|
| 699 |
-
"OUTPUT_DIR = REPO_DIR / \"outputs\"\n",
|
| 700 |
-
"PLOTS_DIR = OUTPUT_DIR / \"plots\"\n",
|
| 701 |
-
"OUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n",
|
| 702 |
-
"PLOTS_DIR.mkdir(parents=True, exist_ok=True)\n",
|
| 703 |
-
"\n",
|
| 704 |
-
"print(\"Trainer initialized successfully\")\n",
|
| 705 |
-
"print(f\" Device : {trainer.device}\")\n",
|
| 706 |
-
"print(f\" FP16 / BF16 : {trainer.fp16} / {trainer.bf16}\")\n",
|
| 707 |
-
"print(f\" Gradient accumulation steps: {trainer.gradient_accumulation_steps}\")\n",
|
| 708 |
-
"print(f\" Number of epochs : {trainer.num_epochs}\")\n",
|
| 709 |
-
"print(f\" Checkpoint directory : {trainer.checkpoint_dir}\")\n",
|
| 710 |
-
"print(f\" Output directory : {OUTPUT_DIR}\")\n",
|
| 711 |
-
"print()\n",
|
| 712 |
-
"print(\">>> Run the NEXT cell to start training.\")\n"
|
| 713 |
-
]
|
| 714 |
-
},
|
| 715 |
-
{
|
| 716 |
-
"cell_type": "markdown",
|
| 717 |
-
"metadata": {},
|
| 718 |
-
"source": [
|
| 719 |
-
"## 13. Start Training\n",
|
| 720 |
-
"\n",
|
| 721 |
-
"**Run this cell to begin training.** This will execute the full training loop.\n",
|
| 722 |
-
"Training progress will be displayed via tqdm progress bars."
|
| 723 |
-
]
|
| 724 |
-
},
|
| 725 |
-
{
|
| 726 |
-
"cell_type": "code",
|
| 727 |
-
"execution_count": null,
|
| 728 |
-
"metadata": {},
|
| 729 |
-
"outputs": [],
|
| 730 |
-
"source": [
|
| 731 |
-
"START_TRAINING = True\n",
|
| 732 |
-
"\n",
|
| 733 |
-
"if START_TRAINING:\n",
|
| 734 |
-
" print(\"=\" * 60)\n",
|
| 735 |
-
" print(\" Starting Training...\")\n",
|
| 736 |
-
" print(\"=\" * 60)\n",
|
| 737 |
-
" trainer.train()\n",
|
| 738 |
-
"else:\n",
|
| 739 |
-
" print(\"Training skipped. Set START_TRAINING = True to begin.\")"
|
| 740 |
-
]
|
| 741 |
-
},
|
| 742 |
-
{
|
| 743 |
-
"cell_type": "markdown",
|
| 744 |
-
"metadata": {},
|
| 745 |
-
"source": [
|
| 746 |
-
"## 14. Evaluation on Test Set\n",
|
| 747 |
-
"\n",
|
| 748 |
-
"After training completes, evaluate the best model on the validation set."
|
| 749 |
-
]
|
| 750 |
-
},
|
| 751 |
-
{
|
| 752 |
-
"cell_type": "code",
|
| 753 |
-
"execution_count": null,
|
| 754 |
-
"metadata": {},
|
| 755 |
-
"outputs": [],
|
| 756 |
-
"source": [
|
| 757 |
-
"EVAL_RESULTS = {}\n",
|
| 758 |
-
"\n",
|
| 759 |
-
"best_ckpt = trainer.checkpoint_dir / \"best_model.pt\"\n",
|
| 760 |
-
"\n",
|
| 761 |
-
"if best_ckpt.exists():\n",
|
| 762 |
-
" print(f\"Loading best model from {best_ckpt}\")\n",
|
| 763 |
-
" checkpoint = torch.load(best_ckpt, map_location=device, weights_only=True)\n",
|
| 764 |
-
" model.load_state_dict(checkpoint[\"model_state_dict\"])\n",
|
| 765 |
-
" model = model.to(device)\n",
|
| 766 |
-
" model.eval()\n",
|
| 767 |
-
"\n",
|
| 768 |
-
" evaluator = Evaluator(model=model, tokenizer=tokenizer, config=config_dict)\n",
|
| 769 |
-
"\n",
|
| 770 |
-
" print(\"Running evaluation on validation set...\")\n",
|
| 771 |
-
" EVAL_RESULTS = evaluator.evaluate(\n",
|
| 772 |
-
" val_loader,\n",
|
| 773 |
-
" src_texts=val_src,\n",
|
| 774 |
-
" ref_texts=val_tgt,\n",
|
| 775 |
-
" )\n",
|
| 776 |
-
"\n",
|
| 777 |
-
" print(\"\\n\" + \"=\" * 60)\n",
|
| 778 |
-
" print(\" Evaluation Results\")\n",
|
| 779 |
-
" print(\"=\" * 60)\n",
|
| 780 |
-
" for metric, score in EVAL_RESULTS.items():\n",
|
| 781 |
-
" if isinstance(score, (int, float)):\n",
|
| 782 |
-
" print(f\" {metric:>12s}: {score:.4f}\")\n",
|
| 783 |
-
"\n",
|
| 784 |
-
" eval_path = OUTPUT_DIR / \"evaluation_results.json\"\n",
|
| 785 |
-
" with open(eval_path, \"w\", encoding=\"utf-8\") as f:\n",
|
| 786 |
-
" json.dump(EVAL_RESULTS, f, indent=2, ensure_ascii=False)\n",
|
| 787 |
-
" print(f\"\\nSaved evaluation results → {eval_path}\")\n",
|
| 788 |
-
"else:\n",
|
| 789 |
-
" print(\"No best_model.pt found. Run training first (Cell 13).\")\n"
|
| 790 |
-
]
|
| 791 |
-
},
|
| 792 |
-
{
|
| 793 |
-
"cell_type": "markdown",
|
| 794 |
-
"metadata": {},
|
| 795 |
-
"source": [
|
| 796 |
-
"## 15. Translation Demo\n",
|
| 797 |
-
"\n",
|
| 798 |
-
"Test the trained model with some example translations."
|
| 799 |
-
]
|
| 800 |
-
},
|
| 801 |
-
{
|
| 802 |
-
"cell_type": "code",
|
| 803 |
-
"execution_count": null,
|
| 804 |
-
"metadata": {},
|
| 805 |
-
"outputs": [],
|
| 806 |
-
"source": [
|
| 807 |
-
"test_sentences = [\n",
|
| 808 |
-
" \"Hello, how are you today?\",\n",
|
| 809 |
-
" \"Machine translation is an important field of natural language processing.\",\n",
|
| 810 |
-
" \"The weather is beautiful and I want to go for a walk.\",\n",
|
| 811 |
-
" \"Deep learning has revolutionized artificial intelligence research.\",\n",
|
| 812 |
-
"]\n",
|
| 813 |
-
"\n",
|
| 814 |
-
"TRANSLATION_RESULTS = []\n",
|
| 815 |
-
"\n",
|
| 816 |
-
"if best_ckpt.exists():\n",
|
| 817 |
-
" print(\"Translating example sentences...\")\n",
|
| 818 |
-
" print(\"-\" * 60)\n",
|
| 819 |
-
" for sentence in test_sentences:\n",
|
| 820 |
-
" translation = evaluator.translate_single(sentence)\n",
|
| 821 |
-
" TRANSLATION_RESULTS.append({\"source_en\": sentence, \"target_zh\": translation})\n",
|
| 822 |
-
" print(f\"[EN] {sentence}\")\n",
|
| 823 |
-
" print(f\"[ZH] {translation}\")\n",
|
| 824 |
-
" print()\n",
|
| 825 |
-
"\n",
|
| 826 |
-
" translation_path = OUTPUT_DIR / \"translation_examples.json\"\n",
|
| 827 |
-
" with open(translation_path, \"w\", encoding=\"utf-8\") as f:\n",
|
| 828 |
-
" json.dump(TRANSLATION_RESULTS, f, indent=2, ensure_ascii=False)\n",
|
| 829 |
-
" print(f\"Saved translation examples → {translation_path}\")\n",
|
| 830 |
-
"else:\n",
|
| 831 |
-
" print(\"No trained model checkpoint available. Run training first (Cell 13).\")\n"
|
| 832 |
-
]
|
| 833 |
-
},
|
| 834 |
-
{
|
| 835 |
-
"cell_type": "markdown",
|
| 836 |
-
"metadata": {},
|
| 837 |
-
"source": [
|
| 838 |
-
"## 16. Cloud Storage Synchronization\n",
|
| 839 |
-
"\n",
|
| 840 |
-
"Sync all training artifacts (checkpoints, logs, summaries) to Google Drive."
|
| 841 |
-
]
|
| 842 |
-
},
|
| 843 |
-
{
|
| 844 |
-
"cell_type": "code",
|
| 845 |
-
"execution_count": null,
|
| 846 |
-
"metadata": {},
|
| 847 |
-
"outputs": [],
|
| 848 |
-
"source": [
|
| 849 |
-
"from easytranslate.utils.cloud_storage import sync_all_to_drive\n",
|
| 850 |
-
"\n",
|
| 851 |
-
"# Use trainer.log_dir if available, otherwise fall back to a sensible default\n",
|
| 852 |
-
"_log_dir = getattr(trainer, \"log_dir\", REPO_DIR / \"logs\")\n",
|
| 853 |
-
"\n",
|
| 854 |
-
"if IN_COLAB and DRIVE_MOUNTED:\n",
|
| 855 |
-
" print(\"Syncing training artifacts to Google Drive...\")\n",
|
| 856 |
-
" sync_results = sync_all_to_drive(\n",
|
| 857 |
-
" checkpoint_dir=str(trainer.checkpoint_dir),\n",
|
| 858 |
-
" log_dir=str(_log_dir),\n",
|
| 859 |
-
" drive_base_path=DRIVE_BASE,\n",
|
| 860 |
-
" )\n",
|
| 861 |
-
"\n",
|
| 862 |
-
" drive_outputs_dir = Path(DRIVE_BASE) / \"outputs\"\n",
|
| 863 |
-
" drive_outputs_dir.mkdir(parents=True, exist_ok=True)\n",
|
| 864 |
-
"\n",
|
| 865 |
-
" for artifact_file in [\n",
|
| 866 |
-
" OUTPUT_DIR / \"evaluation_results.json\",\n",
|
| 867 |
-
" OUTPUT_DIR / \"translation_examples.json\",\n",
|
| 868 |
-
" OUTPUT_DIR / \"training_report.json\",\n",
|
| 869 |
-
" ]:\n",
|
| 870 |
-
" if artifact_file.exists():\n",
|
| 871 |
-
" shutil.copy2(artifact_file, drive_outputs_dir / artifact_file.name)\n",
|
| 872 |
-
" print(f\" Copied {artifact_file.name} → Drive\")\n",
|
| 873 |
-
"\n",
|
| 874 |
-
" if PLOTS_DIR.exists():\n",
|
| 875 |
-
" drive_plots_dir = drive_outputs_dir / \"plots\"\n",
|
| 876 |
-
" drive_plots_dir.mkdir(parents=True, exist_ok=True)\n",
|
| 877 |
-
" for png_file in PLOTS_DIR.glob(\"*.png\"):\n",
|
| 878 |
-
" shutil.copy2(png_file, drive_plots_dir / png_file.name)\n",
|
| 879 |
-
" print(f\" Copied plot {png_file.name} → Drive\")\n",
|
| 880 |
-
"\n",
|
| 881 |
-
" print(f\"\\nSync complete. Drive base: {DRIVE_BASE}\")\n",
|
| 882 |
-
" print(f\"Sync details: {sync_results}\")\n",
|
| 883 |
-
"\n",
|
| 884 |
-
"elif not IN_COLAB:\n",
|
| 885 |
-
" print(\"Running locally. Artifacts are already on disk:\")\n",
|
| 886 |
-
" print(f\" Checkpoints : {trainer.checkpoint_dir}\")\n",
|
| 887 |
-
" print(f\" Logs : {_log_dir}\")\n",
|
| 888 |
-
" print(f\" Outputs : {OUTPUT_DIR}\")\n",
|
| 889 |
-
"else:\n",
|
| 890 |
-
" print(\"Google Drive not mounted. Artifacts saved locally only.\")\n",
|
| 891 |
-
" print(\"Mount Drive (Cell 5) and re-run this cell to sync results.\")\n"
|
| 892 |
-
]
|
| 893 |
-
},
|
| 894 |
-
{
|
| 895 |
-
"cell_type": "markdown",
|
| 896 |
-
"metadata": {},
|
| 897 |
-
"source": [
|
| 898 |
-
"## 17. Training Summary\n",
|
| 899 |
-
"\n",
|
| 900 |
-
"Display the final training summary including loss curves and best metrics."
|
| 901 |
-
]
|
| 902 |
-
},
|
| 903 |
-
{
|
| 904 |
-
"cell_type": "code",
|
| 905 |
-
"execution_count": null,
|
| 906 |
-
"metadata": {},
|
| 907 |
-
"outputs": [],
|
| 908 |
-
"source": [
|
| 909 |
-
"TRAINING_SUMMARY = {}\n",
|
| 910 |
-
"summary_path = trainer.checkpoint_dir / \"training_summary.json\"\n",
|
| 911 |
-
"\n",
|
| 912 |
-
"if summary_path.exists():\n",
|
| 913 |
-
" with open(summary_path, \"r\", encoding=\"utf-8\") as f:\n",
|
| 914 |
-
" TRAINING_SUMMARY = json.load(f)\n",
|
| 915 |
-
"\n",
|
| 916 |
-
" print(\"=\" * 60)\n",
|
| 917 |
-
" print(\" Training Summary\")\n",
|
| 918 |
-
" print(\"=\" * 60)\n",
|
| 919 |
-
" print(f\" Best epoch : {TRAINING_SUMMARY.get('best_epoch', 'N/A')}\")\n",
|
| 920 |
-
" print(f\" Best metric : {TRAINING_SUMMARY.get('metric_name', 'N/A')} = \"\n",
|
| 921 |
-
" f\"{TRAINING_SUMMARY.get('best_metric', 'N/A')}\")\n",
|
| 922 |
-
" print(f\" Total steps : {TRAINING_SUMMARY.get('total_steps', 'N/A')}\")\n",
|
| 923 |
-
"\n",
|
| 924 |
-
" losses = TRAINING_SUMMARY.get(\"train_loss_history\", [])\n",
|
| 925 |
-
" if losses:\n",
|
| 926 |
-
" print(f\" Initial loss: {losses[0]:.4f}\")\n",
|
| 927 |
-
" print(f\" Final loss : {losses[-1]:.4f}\")\n",
|
| 928 |
-
" print(f\" Reduction : {losses[0] - losses[-1]:.4f}\")\n",
|
| 929 |
-
"\n",
|
| 930 |
-
" # Merge all results into one report file\n",
|
| 931 |
-
" report = {\n",
|
| 932 |
-
" \"training_summary\": TRAINING_SUMMARY,\n",
|
| 933 |
-
" \"evaluation_results\": EVAL_RESULTS,\n",
|
| 934 |
-
" \"translation_examples\": TRANSLATION_RESULTS,\n",
|
| 935 |
-
" }\n",
|
| 936 |
-
" report_path = OUTPUT_DIR / \"training_report.json\"\n",
|
| 937 |
-
" with open(report_path, \"w\", encoding=\"utf-8\") as f:\n",
|
| 938 |
-
" json.dump(report, f, indent=2, ensure_ascii=False)\n",
|
| 939 |
-
" print(f\"\\nMerged report saved → {report_path}\")\n",
|
| 940 |
-
"else:\n",
|
| 941 |
-
" print(\"Training summary not yet available. Complete training first.\")\n"
|
| 942 |
-
]
|
| 943 |
-
},
|
| 944 |
-
{
|
| 945 |
-
"cell_type": "code",
|
| 946 |
-
"execution_count": null,
|
| 947 |
-
"metadata": {},
|
| 948 |
-
"outputs": [],
|
| 949 |
-
"source": [
|
| 950 |
-
"import matplotlib.pyplot as plt\n",
|
| 951 |
-
"\n",
|
| 952 |
-
"PLOTS_DIR.mkdir(parents=True, exist_ok=True)\n",
|
| 953 |
-
"\n",
|
| 954 |
-
"if not TRAINING_SUMMARY:\n",
|
| 955 |
-
" print(\"No training summary found. Run training and summary cells first.\")\n",
|
| 956 |
-
"else:\n",
|
| 957 |
-
" train_losses = TRAINING_SUMMARY.get(\"train_loss_history\", [])\n",
|
| 958 |
-
" val_history = TRAINING_SUMMARY.get(\"val_metrics_history\", [])\n",
|
| 959 |
-
"\n",
|
| 960 |
-
" # 1) Train Loss Curve\n",
|
| 961 |
-
" if train_losses:\n",
|
| 962 |
-
" epochs = list(range(1, len(train_losses) + 1))\n",
|
| 963 |
-
" plt.figure(figsize=(8, 5))\n",
|
| 964 |
-
" plt.plot(epochs, train_losses, marker=\"o\", linewidth=2)\n",
|
| 965 |
-
" plt.title(\"Training Loss by Epoch\")\n",
|
| 966 |
-
" plt.xlabel(\"Epoch\")\n",
|
| 967 |
-
" plt.ylabel(\"Loss\")\n",
|
| 968 |
-
" plt.grid(alpha=0.3)\n",
|
| 969 |
-
" loss_plot_path = PLOTS_DIR / \"train_loss_curve.png\"\n",
|
| 970 |
-
" plt.tight_layout()\n",
|
| 971 |
-
" plt.savefig(loss_plot_path, dpi=180)\n",
|
| 972 |
-
" plt.show()\n",
|
| 973 |
-
" print(f\"Saved plot: {loss_plot_path}\")\n",
|
| 974 |
-
"\n",
|
| 975 |
-
" # 2) Validation Metrics Curves\n",
|
| 976 |
-
" if val_history:\n",
|
| 977 |
-
" metric_keys = sorted({k for m in val_history for k in m.keys() if isinstance(m.get(k), (int, float))})\n",
|
| 978 |
-
" metric_keys = [k for k in metric_keys if k != \"val_loss\"]\n",
|
| 979 |
-
"\n",
|
| 980 |
-
" if metric_keys:\n",
|
| 981 |
-
" n = len(metric_keys)\n",
|
| 982 |
-
" rows = (n + 1) // 2\n",
|
| 983 |
-
" plt.figure(figsize=(12, max(4, rows * 3.5)))\n",
|
| 984 |
-
" for i, key in enumerate(metric_keys, start=1):\n",
|
| 985 |
-
" vals = [m.get(key, None) for m in val_history]\n",
|
| 986 |
-
" xs = [idx + 1 for idx, v in enumerate(vals) if v is not None]\n",
|
| 987 |
-
" ys = [v for v in vals if v is not None]\n",
|
| 988 |
-
" if not ys:\n",
|
| 989 |
-
" continue\n",
|
| 990 |
-
" plt.subplot(rows, 2, i)\n",
|
| 991 |
-
" plt.plot(xs, ys, marker=\"o\", linewidth=1.8)\n",
|
| 992 |
-
" plt.title(key)\n",
|
| 993 |
-
" plt.xlabel(\"Epoch\")\n",
|
| 994 |
-
" plt.ylabel(key)\n",
|
| 995 |
-
" plt.grid(alpha=0.3)\n",
|
| 996 |
-
"\n",
|
| 997 |
-
" metrics_plot_path = PLOTS_DIR / \"validation_metrics_curves.png\"\n",
|
| 998 |
-
" plt.tight_layout()\n",
|
| 999 |
-
" plt.savefig(metrics_plot_path, dpi=180)\n",
|
| 1000 |
-
" plt.show()\n",
|
| 1001 |
-
" print(f\"Saved plot: {metrics_plot_path}\")\n",
|
| 1002 |
-
"\n",
|
| 1003 |
-
" # 3) Final Evaluation Bar Chart\n",
|
| 1004 |
-
" if \"EVAL_RESULTS\" in globals() and EVAL_RESULTS:\n",
|
| 1005 |
-
" scalar_items = {k: v for k, v in EVAL_RESULTS.items() if isinstance(v, (int, float))}\n",
|
| 1006 |
-
" if scalar_items:\n",
|
| 1007 |
-
" names = list(scalar_items.keys())\n",
|
| 1008 |
-
" values = [scalar_items[k] for k in names]\n",
|
| 1009 |
-
" plt.figure(figsize=(10, 5))\n",
|
| 1010 |
-
" bars = plt.bar(names, values)\n",
|
| 1011 |
-
" plt.title(\"Final Evaluation Metrics\")\n",
|
| 1012 |
-
" plt.ylabel(\"Score\")\n",
|
| 1013 |
-
" plt.xticks(rotation=30)\n",
|
| 1014 |
-
" plt.grid(axis=\"y\", alpha=0.25)\n",
|
| 1015 |
-
" for bar, val in zip(bars, values):\n",
|
| 1016 |
-
" plt.text(bar.get_x() + bar.get_width() / 2, bar.get_height(), f\"{val:.3f}\", ha=\"center\", va=\"bottom\", fontsize=9)\n",
|
| 1017 |
-
" eval_plot_path = PLOTS_DIR / \"final_evaluation_metrics.png\"\n",
|
| 1018 |
-
" plt.tight_layout()\n",
|
| 1019 |
-
" plt.savefig(eval_plot_path, dpi=180)\n",
|
| 1020 |
-
" plt.show()\n",
|
| 1021 |
-
" print(f\"Saved plot: {eval_plot_path}\")"
|
| 1022 |
-
]
|
| 1023 |
-
},
|
| 1024 |
-
{
|
| 1025 |
-
"cell_type": "markdown",
|
| 1026 |
-
"metadata": {},
|
| 1027 |
-
"source": [
|
| 1028 |
-
"---\n",
|
| 1029 |
-
"\n",
|
| 1030 |
-
"## Appendix: Module Architecture Overview\n",
|
| 1031 |
-
"\n",
|
| 1032 |
-
"```\n",
|
| 1033 |
-
"EasyTranslate System Architecture\n",
|
| 1034 |
-
"=================================\n",
|
| 1035 |
-
"\n",
|
| 1036 |
-
"Entry Point: EasyTranslate_Production.ipynb (this notebook)\n",
|
| 1037 |
-
" |\n",
|
| 1038 |
-
" +-- Environment Detection (Colab vs Local)\n",
|
| 1039 |
-
" +-- Repository Cloning (git clone/pull)\n",
|
| 1040 |
-
" +-- Dependency Installation\n",
|
| 1041 |
-
" |\n",
|
| 1042 |
-
" +-- Configuration Layer [utils/config.py]\n",
|
| 1043 |
-
" | +-- load_config() : YAML -> OmegaConf DictConfig\n",
|
| 1044 |
-
" | +-- merge_configs() : CLI overrides merge\n",
|
| 1045 |
-
" | +-- config_from_cli() : Full config pipeline\n",
|
| 1046 |
-
" |\n",
|
| 1047 |
-
" +-- Data Layer [data/] — Person A\n",
|
| 1048 |
-
" | +-- load_wmt_dataset() : HuggingFace datasets loader\n",
|
| 1049 |
-
" | +-- preprocess_pipeline() : Clean + filter + deduplicate\n",
|
| 1050 |
-
" | +-- build_tokenizer() : BPE / pretrained tokenizer\n",
|
| 1051 |
-
" | +-- TranslationDataset() : PyTorch Dataset\n",
|
| 1052 |
-
" | +-- TranslationCollator() : Padding + mask generation\n",
|
| 1053 |
-
" | +-- DynamicBatchSampler() : Token-budget batching\n",
|
| 1054 |
-
" |\n",
|
| 1055 |
-
" +-- Model Layer [model/] — Person B\n",
|
| 1056 |
-
" | +-- TransformerTranslationModel() : Full Enc-Dec model\n",
|
| 1057 |
-
" | +-- TransformerEncoder() : N-layer encoder\n",
|
| 1058 |
-
" | +-- TransformerDecoder() : N-layer decoder\n",
|
| 1059 |
-
" | +-- FlashMultiHeadAttention() : Flash Attention 2\n",
|
| 1060 |
-
" | +-- RotaryPositionalEmbedding() : RoPE encoding\n",
|
| 1061 |
-
" | +-- load_pretrained_model() : NLLB/mBART loader\n",
|
| 1062 |
-
" | +-- setup_lora() : LoRA configuration\n",
|
| 1063 |
-
" |\n",
|
| 1064 |
-
" +-- Training Layer [training/] — Person C\n",
|
| 1065 |
-
" | +-- Trainer() : Full training controller\n",
|
| 1066 |
-
" | | +-- _train_one_epoch() : Mixed precision loop\n",
|
| 1067 |
-
" | | +-- _validate() : Validation loop\n",
|
| 1068 |
-
" | | +-- _save_checkpoint() : Checkpoint persistence\n",
|
| 1069 |
-
" | | +-- _load_checkpoint() : Resume training\n",
|
| 1070 |
-
" | | +-- _should_early_stop() : Early stopping logic\n",
|
| 1071 |
-
" | | +-- _setup_distributed() : DDP/DeepSpeed setup\n",
|
| 1072 |
-
" | +-- LabelSmoothedCrossEntropyLoss() : Label smoothing loss\n",
|
| 1073 |
-
" | +-- build_optimizer() : AdamW with param groups\n",
|
| 1074 |
-
" | +-- build_scheduler() : Cosine/InverseSqrt/LR\n",
|
| 1075 |
-
" |\n",
|
| 1076 |
-
" +-- Evaluation Layer [evaluation/] — Person D\n",
|
| 1077 |
-
" | +-- Evaluator() : Unified evaluation interface\n",
|
| 1078 |
-
" | +-- greedy_decode() : Greedy decoding\n",
|
| 1079 |
-
" | +-- beam_search_decode() : Beam search decoding\n",
|
| 1080 |
-
" | +-- sample_decode() : Sampling (temp/top-k/top-p)\n",
|
| 1081 |
-
" | +-- compute_bleu() : SacreBLEU metric\n",
|
| 1082 |
-
" | +-- compute_comet() : COMET neural metric\n",
|
| 1083 |
-
" | +-- compute_chrf() : chrF++ metric\n",
|
| 1084 |
-
" |\n",
|
| 1085 |
-
" +-- Cloud Storage [utils/cloud_storage.py] — Person C\n",
|
| 1086 |
-
" +-- is_colab_environment() : Environment detection\n",
|
| 1087 |
-
" +-- mount_google_drive() : Drive authentication\n",
|
| 1088 |
-
" +-- sync_checkpoints_to_drive() : Checkpoint backup\n",
|
| 1089 |
-
" +-- sync_logs_to_drive() : Log backup\n",
|
| 1090 |
-
" +-- sync_all_to_drive() : Full sync pipeline\n",
|
| 1091 |
-
"```\n",
|
| 1092 |
-
"\n",
|
| 1093 |
-
"## Module Interface Contracts\n",
|
| 1094 |
-
"\n",
|
| 1095 |
-
"### Tokenizer Interface (Person A -> B, C, D)\n",
|
| 1096 |
-
"```python\n",
|
| 1097 |
-
"tokenizer.encode(text: str) -> list[int]\n",
|
| 1098 |
-
"tokenizer.decode(ids: list[int]) -> str\n",
|
| 1099 |
-
"tokenizer.vocab_size -> int\n",
|
| 1100 |
-
"tokenizer.pad_token_id -> int\n",
|
| 1101 |
-
"tokenizer.bos_token_id -> int\n",
|
| 1102 |
-
"tokenizer.eos_token_id -> int\n",
|
| 1103 |
-
"```\n",
|
| 1104 |
-
"\n",
|
| 1105 |
-
"### Model Interface (Person B -> C, D)\n",
|
| 1106 |
-
"```python\n",
|
| 1107 |
-
"# Training forward pass\n",
|
| 1108 |
-
"logits = model(src_ids, tgt_input_ids, src_padding_mask, tgt_padding_mask)\n",
|
| 1109 |
-
"# logits: [B, T, vocab_size]\n",
|
| 1110 |
-
"\n",
|
| 1111 |
-
"# Inference\n",
|
| 1112 |
-
"encoder_output = model.encode(src_ids, src_padding_mask)\n",
|
| 1113 |
-
"next_logits = model.decode_step(tgt_input_ids, encoder_output, src_padding_mask)\n",
|
| 1114 |
-
"```\n",
|
| 1115 |
-
"\n",
|
| 1116 |
-
"### Batch Format (Person A -> C)\n",
|
| 1117 |
-
"```python\n",
|
| 1118 |
-
"batch = {\n",
|
| 1119 |
-
" \"src_ids\": Tensor[B, S],\n",
|
| 1120 |
-
" \"tgt_input_ids\": Tensor[B, T],\n",
|
| 1121 |
-
" \"labels\": Tensor[B, T],\n",
|
| 1122 |
-
" \"src_padding_mask\": BoolTensor[B, S],\n",
|
| 1123 |
-
" \"tgt_padding_mask\": BoolTensor[B, T],\n",
|
| 1124 |
-
"}\n",
|
| 1125 |
-
"```\n",
|
| 1126 |
-
"\n",
|
| 1127 |
-
"### Evaluation Interface (Person D -> C, E)\n",
|
| 1128 |
-
"```python\n",
|
| 1129 |
-
"evaluator = Evaluator(model, tokenizer, config)\n",
|
| 1130 |
-
"results = evaluator.evaluate(dataloader)\n",
|
| 1131 |
-
"# results: {\"bleu\": 25.6, \"comet\": 0.82, \"chrf\": 45.3, \"ter\": 55.2}\n",
|
| 1132 |
-
"```"
|
| 1133 |
-
]
|
| 1134 |
-
}
|
| 1135 |
-
],
|
| 1136 |
-
"metadata": {
|
| 1137 |
-
"colab": {
|
| 1138 |
-
"include_colab_link": true,
|
| 1139 |
-
"provenance": []
|
| 1140 |
-
},
|
| 1141 |
-
"kernelspec": {
|
| 1142 |
-
"display_name": "Python 3",
|
| 1143 |
-
"language": "python",
|
| 1144 |
-
"name": "python3"
|
| 1145 |
-
},
|
| 1146 |
-
"language_info": {
|
| 1147 |
-
"name": "python",
|
| 1148 |
-
"version": "3.10.0"
|
| 1149 |
-
}
|
| 1150 |
-
},
|
| 1151 |
-
"nbformat": 4,
|
| 1152 |
-
"nbformat_minor": 4
|
| 1153 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements-colab.txt
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
# =============================================================================
|
| 2 |
-
# EasyTranslate — Google Colab Dependencies
|
| 3 |
-
# =============================================================================
|
| 4 |
-
# Colab already provides: torch, torchvision, numpy, pandas, matplotlib
|
| 5 |
-
# This file lists only additional packages needed beyond Colab defaults.
|
| 6 |
-
# =============================================================================
|
| 7 |
-
|
| 8 |
-
transformers>=4.36.0,<4.45.0
|
| 9 |
-
datasets>=2.16.0,<3.0.0
|
| 10 |
-
tokenizers>=0.15.0,<0.20.0
|
| 11 |
-
sentencepiece>=0.2.0,<0.3.0
|
| 12 |
-
accelerate>=0.25.0,<0.35.0
|
| 13 |
-
peft>=0.7.0,<0.12.0
|
| 14 |
-
sacrebleu>=2.4.0,<3.0.0
|
| 15 |
-
omegaconf>=2.3.0,<3.0.0
|
| 16 |
-
rich>=13.0.0,<14.0.0
|
| 17 |
-
tqdm>=4.66.0,<5.0.0
|
| 18 |
-
wandb>=0.16.0,<0.18.0
|
| 19 |
-
tensorboard>=2.15.0,<2.17.0
|
| 20 |
-
protobuf>=5.28.3,<7.0.0
|
| 21 |
-
fsspec==2025.3.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -1,62 +1,35 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
#
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
# Parameter-Efficient Fine-Tuning
|
| 21 |
-
peft>=0.7.0,<0.12.0
|
| 22 |
-
|
| 23 |
-
# Distributed Training (optional, GPU only)
|
| 24 |
-
deepspeed>=0.12.0,<0.15.0; platform_system == "Linux"
|
| 25 |
-
bitsandbytes>=0.41.0,<0.44.0; platform_system == "Linux"
|
| 26 |
-
|
| 27 |
-
# Experiment Tracking
|
| 28 |
-
wandb>=0.16.0,<0.18.0
|
| 29 |
-
tensorboard>=2.15.0,<2.17.0
|
| 30 |
-
|
| 31 |
-
# Evaluation Metrics
|
| 32 |
-
sacrebleu>=2.4.0,<3.0.0
|
| 33 |
-
unbabel-comet>=2.2.0,<2.3.0
|
| 34 |
rouge-score>=0.1.2
|
| 35 |
-
nltk>=3.8.0
|
| 36 |
-
|
| 37 |
-
#
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
tqdm>=4.66.0,<5.0.0
|
| 47 |
-
|
| 48 |
-
# Visualization
|
| 49 |
-
matplotlib>=3.8.0,<4.0.0
|
| 50 |
-
seaborn>=0.13.0,<0.14.0
|
| 51 |
|
| 52 |
# Serving (optional)
|
| 53 |
-
fastapi>=0.108.0
|
| 54 |
-
uvicorn>=0.25.0
|
| 55 |
-
gradio>=4.10.0
|
| 56 |
-
|
| 57 |
-
# Development & Testing
|
| 58 |
-
pytest>=7.4.0,<9.0.0
|
| 59 |
-
pytest-cov>=4.1.0
|
| 60 |
-
black>=23.0.0,<25.0.0
|
| 61 |
-
isort>=5.12.0,<6.0.0
|
| 62 |
-
flake8>=6.0.0,<8.0.0
|
|
|
|
| 1 |
+
# Core
|
| 2 |
+
torch>=2.1.0
|
| 3 |
+
transformers>=4.36.0
|
| 4 |
+
datasets>=2.16.0
|
| 5 |
+
tokenizers>=0.15.0
|
| 6 |
+
sentencepiece>=0.1.99
|
| 7 |
+
accelerate>=0.25.0
|
| 8 |
+
|
| 9 |
+
# Training
|
| 10 |
+
deepspeed>=0.12.0
|
| 11 |
+
bitsandbytes>=0.41.0
|
| 12 |
+
peft>=0.7.0
|
| 13 |
+
wandb>=0.16.0
|
| 14 |
+
tensorboard>=2.15.0
|
| 15 |
+
|
| 16 |
+
# Evaluation
|
| 17 |
+
sacrebleu>=2.4.0
|
| 18 |
+
unbabel-comet>=2.2.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
rouge-score>=0.1.2
|
| 20 |
+
nltk>=3.8.0
|
| 21 |
+
|
| 22 |
+
# Utilities
|
| 23 |
+
numpy>=1.24.0,<2.0
|
| 24 |
+
pandas>=2.0.0
|
| 25 |
+
tqdm>=4.66.0
|
| 26 |
+
pyyaml>=6.0.0
|
| 27 |
+
omegaconf>=2.3.0
|
| 28 |
+
rich>=13.0.0
|
| 29 |
+
matplotlib>=3.8.0
|
| 30 |
+
seaborn>=0.13.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
# Serving (optional)
|
| 33 |
+
fastapi>=0.108.0
|
| 34 |
+
uvicorn>=0.25.0
|
| 35 |
+
gradio>=4.10.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
result/best_model.pt
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:c80b4b011e062695fceae4bdf1d43c070ed4ceace176c4637fcde70cd54d54d4
|
| 3 |
-
size 1120219211
|
|
|
|
|
|
|
|
|
|
|
|
result/evaluation_results.json
DELETED
|
@@ -1,10 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"bleu": 15.1403,
|
| 3 |
-
"bleu_1": 43.0105,
|
| 4 |
-
"bleu_2": 20.1492,
|
| 5 |
-
"bleu_3": 10.4122,
|
| 6 |
-
"bleu_4": 5.8232,
|
| 7 |
-
"bp": 1.0,
|
| 8 |
-
"chrf": 15.2513,
|
| 9 |
-
"ter": 177.5966
|
| 10 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
result/final_evaluation_metrics.png
DELETED
|
Binary file (52.1 kB)
|
|
|
result/link.txt
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
https://colab.research.google.com/drive/1wCVTnNuULJkBIS5tzPu5uQY4zd-akEd4?authuser=1#scrollTo=SssPN0dpTHJX&fullscreenOutput=true
|
|
|
|
|
|
result/train_loss_curve.png
DELETED
|
Binary file (51.7 kB)
|
|
|
result/train_output.txt
DELETED
|
@@ -1,102 +0,0 @@
|
|
| 1 |
-
============================================================
|
| 2 |
-
Starting Training...
|
| 3 |
-
============================================================
|
| 4 |
-
[05/13/26 07:55:00] INFO Starting training for 30 epochs trainer.py:193
|
| 5 |
-
|
| 6 |
-
INFO Device: cuda, FP16: False, BF16: True trainer.py:194
|
| 7 |
-
|
| 8 |
-
INFO Gradient accumulation steps: 4 trainer.py:195
|
| 9 |
-
|
| 10 |
-
INFO ================================================== trainer.py:199
|
| 11 |
-
|
| 12 |
-
INFO Epoch 1/30 trainer.py:200
|
| 13 |
-
|
| 14 |
-
[05/13/26 07:57:11] INFO Epoch 1 - Train Loss: 8.2498 trainer.py:279
|
| 15 |
-
|
| 16 |
-
Evaluating: 100%|██████████| 122/122 [17:49<00:00, 8.76s/it]
|
| 17 |
-
[05/13/26 08:15:01] WARNING Evaluation failed during validation: references cannot be empty trainer.py:311
|
| 18 |
-
|
| 19 |
-
INFO Epoch 1 - Val Loss: 7.9030 trainer.py:313
|
| 20 |
-
|
| 21 |
-
[05/13/26 08:15:02] INFO Checkpoint saved: checkpoints/checkpoint_epoch_1.pt trainer.py:341
|
| 22 |
-
|
| 23 |
-
[05/13/26 08:15:03] INFO New best model saved: checkpoints/best_model.pt (metric: 7.9030) trainer.py:352
|
| 24 |
-
|
| 25 |
-
INFO No improvement for 1 epochs (best: 7.9030, current: 7.9030) trainer.py:398
|
| 26 |
-
|
| 27 |
-
INFO ================================================== trainer.py:199
|
| 28 |
-
|
| 29 |
-
INFO Epoch 2/30 trainer.py:200
|
| 30 |
-
|
| 31 |
-
[05/13/26 08:17:12] INFO Epoch 2 - Train Loss: 6.6109 trainer.py:279
|
| 32 |
-
|
| 33 |
-
Evaluating: 100%|██████████| 122/122 [06:10<00:00, 3.04s/it]
|
| 34 |
-
[05/13/26 08:23:23] WARNING Evaluation failed during validation: references cannot be empty trainer.py:311
|
| 35 |
-
|
| 36 |
-
INFO Epoch 2 - Val Loss: 6.9446 trainer.py:313
|
| 37 |
-
|
| 38 |
-
[05/13/26 08:23:24] INFO Checkpoint saved: checkpoints/checkpoint_epoch_2.pt trainer.py:341
|
| 39 |
-
|
| 40 |
-
INFO No improvement for 2 epochs (best: 7.9030, current: 6.9446) trainer.py:398
|
| 41 |
-
|
| 42 |
-
INFO ================================================== trainer.py:199
|
| 43 |
-
|
| 44 |
-
INFO Epoch 3/30 trainer.py:200
|
| 45 |
-
|
| 46 |
-
[05/13/26 08:25:33] INFO Epoch 3 - Train Loss: 5.2802 trainer.py:279
|
| 47 |
-
|
| 48 |
-
Evaluating: 100%|██████████| 122/122 [06:55<00:00, 3.40s/it]
|
| 49 |
-
[05/13/26 08:32:29] WARNING Evaluation failed during validation: references cannot be empty trainer.py:311
|
| 50 |
-
|
| 51 |
-
INFO Epoch 3 - Val Loss: 6.0977 trainer.py:313
|
| 52 |
-
|
| 53 |
-
INFO Checkpoint saved: checkpoints/checkpoint_epoch_3.pt trainer.py:341
|
| 54 |
-
|
| 55 |
-
INFO No improvement for 3 epochs (best: 7.9030, current: 6.0977) trainer.py:398
|
| 56 |
-
|
| 57 |
-
INFO ================================================== trainer.py:199
|
| 58 |
-
|
| 59 |
-
INFO Epoch 4/30 trainer.py:200
|
| 60 |
-
|
| 61 |
-
Train Epoch 4: 28%|██▊ | 1687/6104 [00:35<01:30, 48.68it/s, loss=4.5668, lr=3.00e-04, step=5000][05/13/26 08:33:06] INFO Checkpoint saved: checkpoints/checkpoint_step_5000.pt trainer.py:341
|
| 62 |
-
|
| 63 |
-
INFO New best model saved: checkpoints/best_model.pt (metric: inf) trainer.py:352
|
| 64 |
-
|
| 65 |
-
[05/13/26 08:34:39] INFO Epoch 4 - Train Loss: 4.4749 trainer.py:279
|
| 66 |
-
|
| 67 |
-
Evaluating: 100%|██████████| 122/122 [03:08<00:00, 1.55s/it]
|
| 68 |
-
[05/13/26 08:37:49] WARNING Evaluation failed during validation: references cannot be empty trainer.py:311
|
| 69 |
-
|
| 70 |
-
INFO Epoch 4 - Val Loss: 5.6509 trainer.py:313
|
| 71 |
-
|
| 72 |
-
[05/13/26 08:37:50] INFO Checkpoint saved: checkpoints/checkpoint_epoch_4.pt trainer.py:341
|
| 73 |
-
|
| 74 |
-
INFO No improvement for 4 epochs (best: inf, current: 5.6509) trainer.py:398
|
| 75 |
-
|
| 76 |
-
INFO ================================================== trainer.py:199
|
| 77 |
-
|
| 78 |
-
INFO Epoch 5/30 trainer.py:200
|
| 79 |
-
|
| 80 |
-
[05/13/26 08:40:00] INFO Epoch 5 - Train Loss: 4.0513 trainer.py:279
|
| 81 |
-
|
| 82 |
-
Evaluating: 100%|██████████| 122/122 [03:28<00:00, 1.71s/it]
|
| 83 |
-
[05/13/26 08:43:29] WARNING Evaluation failed during validation: references cannot be empty trainer.py:311
|
| 84 |
-
|
| 85 |
-
INFO Epoch 5 - Val Loss: 5.4433 trainer.py:313
|
| 86 |
-
|
| 87 |
-
INFO Checkpoint saved: checkpoints/checkpoint_epoch_5.pt trainer.py:341
|
| 88 |
-
|
| 89 |
-
INFO No improvement for 5 epochs (best: inf, current: 5.4433) trainer.py:398
|
| 90 |
-
|
| 91 |
-
INFO Early stopping triggered at epoch 5 trainer.py:212
|
| 92 |
-
|
| 93 |
-
INFO ================================================== trainer.py:431
|
| 94 |
-
|
| 95 |
-
INFO Training completed! trainer.py:432
|
| 96 |
-
|
| 97 |
-
INFO Best epoch: 4 trainer.py:433
|
| 98 |
-
|
| 99 |
-
INFO Best bleu: inf trainer.py:434
|
| 100 |
-
|
| 101 |
-
INFO Training summary saved to checkpoints/training_summary.json trainer.py:447
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
result/training_summary.json
DELETED
|
@@ -1,30 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"best_epoch": 3,
|
| 3 |
-
"best_metric": Infinity,
|
| 4 |
-
"metric_name": "bleu",
|
| 5 |
-
"total_steps": 7630,
|
| 6 |
-
"train_loss_history": [
|
| 7 |
-
8.249846095573231,
|
| 8 |
-
6.610892885478108,
|
| 9 |
-
5.28016960105296,
|
| 10 |
-
4.47492950561006,
|
| 11 |
-
4.051334980548131
|
| 12 |
-
],
|
| 13 |
-
"val_metrics_history": [
|
| 14 |
-
{
|
| 15 |
-
"val_loss": 7.903
|
| 16 |
-
},
|
| 17 |
-
{
|
| 18 |
-
"val_loss": 6.9446
|
| 19 |
-
},
|
| 20 |
-
{
|
| 21 |
-
"val_loss": 6.0977
|
| 22 |
-
},
|
| 23 |
-
{
|
| 24 |
-
"val_loss": 5.6509
|
| 25 |
-
},
|
| 26 |
-
{
|
| 27 |
-
"val_loss": 5.4433
|
| 28 |
-
}
|
| 29 |
-
]
|
| 30 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
result/translation_examples.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
| 1 |
-
[
|
| 2 |
-
{
|
| 3 |
-
"source_en": "Hello, how are you today?",
|
| 4 |
-
"target_zh": "今天,你如何?"
|
| 5 |
-
},
|
| 6 |
-
{
|
| 7 |
-
"source_en": "Machine translation is an important field of natural language processing.",
|
| 8 |
-
"target_zh": "生物交流是自然语言处理的重要领域。"
|
| 9 |
-
},
|
| 10 |
-
{
|
| 11 |
-
"source_en": "The weather is beautiful and I want to go for a walk.",
|
| 12 |
-
"target_zh": "天气是美丽的,我想去。"
|
| 13 |
-
},
|
| 14 |
-
{
|
| 15 |
-
"source_en": "Deep learning has revolutionized artificial intelligence research.",
|
| 16 |
-
"target_zh": "深度学习推动了人工智能情报研究。"
|
| 17 |
-
}
|
| 18 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/evaluate.py
CHANGED
|
@@ -10,17 +10,11 @@
|
|
| 10 |
"""
|
| 11 |
|
| 12 |
import argparse
|
| 13 |
-
import json
|
| 14 |
import sys
|
| 15 |
from pathlib import Path
|
| 16 |
|
| 17 |
-
import torch
|
| 18 |
-
import yaml
|
| 19 |
-
|
| 20 |
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
|
| 21 |
|
| 22 |
-
from easytranslate.evaluation.evaluator import Evaluator
|
| 23 |
-
|
| 24 |
|
| 25 |
def parse_args():
|
| 26 |
parser = argparse.ArgumentParser(description="EasyTranslate Evaluation")
|
|
@@ -49,71 +43,7 @@ def main():
|
|
| 49 |
print(" EasyTranslate - Evaluation")
|
| 50 |
print("=" * 60)
|
| 51 |
|
| 52 |
-
|
| 53 |
-
with open(args.config, "r", encoding="utf-8") as f:
|
| 54 |
-
config = yaml.safe_load(f)
|
| 55 |
-
|
| 56 |
-
# 应用命令行覆盖 (格式: key.subkey=value)
|
| 57 |
-
for override in cli_overrides:
|
| 58 |
-
if "=" in override:
|
| 59 |
-
key, value = override.split("=", 1)
|
| 60 |
-
keys = key.split(".")
|
| 61 |
-
d = config
|
| 62 |
-
for k in keys[:-1]:
|
| 63 |
-
d = d.setdefault(k, {})
|
| 64 |
-
d[keys[-1]] = yaml.safe_load(value)
|
| 65 |
-
|
| 66 |
-
# 2. 加载检查点并重建模型
|
| 67 |
-
checkpoint = torch.load(args.checkpoint, map_location="cpu")
|
| 68 |
-
model_config = config.get("model", {})
|
| 69 |
-
|
| 70 |
-
from easytranslate.model.transformer import Transformer
|
| 71 |
-
model = Transformer(model_config)
|
| 72 |
-
model.load_state_dict(checkpoint["model_state_dict"])
|
| 73 |
-
|
| 74 |
-
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 75 |
-
model = model.to(device)
|
| 76 |
-
model.eval()
|
| 77 |
-
|
| 78 |
-
# 3. 加载 tokenizer 和测试数据
|
| 79 |
-
from easytranslate.data.tokenizer import build_tokenizer
|
| 80 |
-
from easytranslate.data.dataset import TranslationDataset
|
| 81 |
-
from torch.utils.data import DataLoader
|
| 82 |
-
|
| 83 |
-
tokenizer = build_tokenizer(config.get("data", {}).get("tokenizer", {}))
|
| 84 |
-
|
| 85 |
-
test_dataset = TranslationDataset(
|
| 86 |
-
config=config.get("data", {}),
|
| 87 |
-
tokenizer=tokenizer,
|
| 88 |
-
split="test",
|
| 89 |
-
)
|
| 90 |
-
test_loader = DataLoader(
|
| 91 |
-
test_dataset,
|
| 92 |
-
batch_size=config.get("evaluation", {}).get("batch_size", 32),
|
| 93 |
-
shuffle=False,
|
| 94 |
-
)
|
| 95 |
-
|
| 96 |
-
# 4. 构建 Evaluator
|
| 97 |
-
evaluator = Evaluator(model=model, tokenizer=tokenizer, config=config)
|
| 98 |
-
|
| 99 |
-
# 5. 运行评估
|
| 100 |
-
src_texts = [sample["src"] for sample in test_dataset.raw_data]
|
| 101 |
-
ref_texts = [sample["tgt"] for sample in test_dataset.raw_data]
|
| 102 |
-
results = evaluator.evaluate(test_loader, src_texts=src_texts, ref_texts=ref_texts)
|
| 103 |
-
|
| 104 |
-
# 6. 打印和保存结果
|
| 105 |
-
print("\n" + "=" * 60)
|
| 106 |
-
print(" Evaluation Results")
|
| 107 |
-
print("=" * 60)
|
| 108 |
-
for metric, score in results.items():
|
| 109 |
-
if not isinstance(score, list):
|
| 110 |
-
print(f" {metric:>10s}: {score:.4f}")
|
| 111 |
-
|
| 112 |
-
output_path = Path(args.output)
|
| 113 |
-
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 114 |
-
with open(output_path, "w", encoding="utf-8") as f:
|
| 115 |
-
json.dump(results, f, indent=2, ensure_ascii=False)
|
| 116 |
-
print(f"\n Results saved to: {output_path}")
|
| 117 |
|
| 118 |
|
| 119 |
if __name__ == "__main__":
|
|
|
|
| 10 |
"""
|
| 11 |
|
| 12 |
import argparse
|
|
|
|
| 13 |
import sys
|
| 14 |
from pathlib import Path
|
| 15 |
|
|
|
|
|
|
|
|
|
|
| 16 |
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
|
| 17 |
|
|
|
|
|
|
|
| 18 |
|
| 19 |
def parse_args():
|
| 20 |
parser = argparse.ArgumentParser(description="EasyTranslate Evaluation")
|
|
|
|
| 43 |
print(" EasyTranslate - Evaluation")
|
| 44 |
print("=" * 60)
|
| 45 |
|
| 46 |
+
raise NotImplementedError("TODO: Person D 实现评估主流程")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
|
| 49 |
if __name__ == "__main__":
|
scripts/setup_colab.py
DELETED
|
@@ -1,121 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Google Colab Environment Setup Script for EasyTranslate.
|
| 4 |
-
|
| 5 |
-
This script is designed to be run as the first cell in a Colab notebook.
|
| 6 |
-
It handles:
|
| 7 |
-
1. GPU verification and CUDA setup
|
| 8 |
-
2. Repository cloning from GitHub
|
| 9 |
-
3. Dependency installation
|
| 10 |
-
4. Google Drive mounting
|
| 11 |
-
5. Environment variable configuration
|
| 12 |
-
|
| 13 |
-
Usage in Colab:
|
| 14 |
-
!wget -q https://raw.githubusercontent.com/your-org/UCAS-EasyTranslate/main/scripts/setup_colab.py
|
| 15 |
-
%run setup_colab.py
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
import os
|
| 19 |
-
import subprocess
|
| 20 |
-
import sys
|
| 21 |
-
from pathlib import Path
|
| 22 |
-
|
| 23 |
-
REPO_URL = "https://github.com/your-org/UCAS-EasyTranslate.git"
|
| 24 |
-
REPO_DIR = Path("/content/UCAS-EasyTranslate")
|
| 25 |
-
DRIVE_MOUNT_POINT = "/content/drive"
|
| 26 |
-
DRIVE_BASE = "/content/drive/MyDrive/EasyTranslate"
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def check_gpu():
|
| 30 |
-
try:
|
| 31 |
-
import torch
|
| 32 |
-
if torch.cuda.is_available():
|
| 33 |
-
print(f"[OK] GPU detected: {torch.cuda.get_device_name(0)}")
|
| 34 |
-
print(f" CUDA version: {torch.version.cuda}")
|
| 35 |
-
print(f" GPU memory: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f} GB")
|
| 36 |
-
return True
|
| 37 |
-
else:
|
| 38 |
-
print("[WARN] No GPU detected. Training will be very slow.")
|
| 39 |
-
return False
|
| 40 |
-
except ImportError:
|
| 41 |
-
print("[WARN] PyTorch not found. Installing...")
|
| 42 |
-
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "torch"])
|
| 43 |
-
return check_gpu()
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
def clone_repository():
|
| 47 |
-
if REPO_DIR.exists():
|
| 48 |
-
print(f"[INFO] Repository exists at {REPO_DIR}, pulling latest...")
|
| 49 |
-
os.chdir(REPO_DIR)
|
| 50 |
-
subprocess.check_call(["git", "pull", "origin", "main"])
|
| 51 |
-
else:
|
| 52 |
-
print(f"[INFO] Cloning repository from {REPO_URL}...")
|
| 53 |
-
subprocess.check_call(["git", "clone", REPO_URL, str(REPO_DIR)])
|
| 54 |
-
os.chdir(REPO_DIR)
|
| 55 |
-
sys.path.insert(0, str(REPO_DIR / "src"))
|
| 56 |
-
print(f"[OK] Repository ready at {REPO_DIR}")
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def install_dependencies():
|
| 60 |
-
req_file = REPO_DIR / "requirements-colab.txt"
|
| 61 |
-
if req_file.exists():
|
| 62 |
-
print("[INFO] Installing Colab dependencies...")
|
| 63 |
-
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "-r", str(req_file)])
|
| 64 |
-
else:
|
| 65 |
-
print("[INFO] Installing from requirements.txt...")
|
| 66 |
-
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "-r", str(REPO_DIR / "requirements.txt")])
|
| 67 |
-
|
| 68 |
-
print("[INFO] Installing EasyTranslate package...")
|
| 69 |
-
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "-e", str(REPO_DIR)])
|
| 70 |
-
print("[OK] All dependencies installed")
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
def mount_drive():
|
| 74 |
-
try:
|
| 75 |
-
from google.colab import drive
|
| 76 |
-
drive.mount(DRIVE_MOUNT_POINT)
|
| 77 |
-
if os.path.exists(DRIVE_MOUNT_POINT):
|
| 78 |
-
os.makedirs(DRIVE_BASE, exist_ok=True)
|
| 79 |
-
print(f"[OK] Google Drive mounted at {DRIVE_MOUNT_POINT}")
|
| 80 |
-
print(f" Base path: {DRIVE_BASE}")
|
| 81 |
-
return True
|
| 82 |
-
except Exception as e:
|
| 83 |
-
print(f"[WARN] Google Drive mount failed: {e}")
|
| 84 |
-
return False
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
def setup_environment():
|
| 88 |
-
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
| 89 |
-
os.environ["WANDB_MODE"] = os.environ.get("WANDB_MODE", "offline")
|
| 90 |
-
os.environ["PYTHONHASHSEED"] = "42"
|
| 91 |
-
print("[OK] Environment variables configured")
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
def main():
|
| 95 |
-
print("=" * 60)
|
| 96 |
-
print(" EasyTranslate — Colab Environment Setup")
|
| 97 |
-
print("=" * 60)
|
| 98 |
-
print()
|
| 99 |
-
|
| 100 |
-
check_gpu()
|
| 101 |
-
print()
|
| 102 |
-
|
| 103 |
-
clone_repository()
|
| 104 |
-
print()
|
| 105 |
-
|
| 106 |
-
install_dependencies()
|
| 107 |
-
print()
|
| 108 |
-
|
| 109 |
-
mount_drive()
|
| 110 |
-
print()
|
| 111 |
-
|
| 112 |
-
setup_environment()
|
| 113 |
-
print()
|
| 114 |
-
|
| 115 |
-
print("=" * 60)
|
| 116 |
-
print(" Setup Complete! Ready for training.")
|
| 117 |
-
print("=" * 60)
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
if __name__ == "__main__":
|
| 121 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/translate.py
CHANGED
|
@@ -16,13 +16,8 @@ import argparse
|
|
| 16 |
import sys
|
| 17 |
from pathlib import Path
|
| 18 |
|
| 19 |
-
import torch
|
| 20 |
-
import yaml
|
| 21 |
-
|
| 22 |
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
|
| 23 |
|
| 24 |
-
from easytranslate.evaluation.evaluator import Evaluator
|
| 25 |
-
|
| 26 |
|
| 27 |
def parse_args():
|
| 28 |
parser = argparse.ArgumentParser(description="EasyTranslate Inference")
|
|
@@ -44,25 +39,7 @@ def interactive_translate(evaluator):
|
|
| 44 |
3. 打印翻译结果
|
| 45 |
4. 输入 'quit' 退出
|
| 46 |
"""
|
| 47 |
-
|
| 48 |
-
print("-" * 40)
|
| 49 |
-
|
| 50 |
-
while True:
|
| 51 |
-
try:
|
| 52 |
-
text = input("\n[EN] > ").strip()
|
| 53 |
-
except (EOFError, KeyboardInterrupt):
|
| 54 |
-
print("\nBye!")
|
| 55 |
-
break
|
| 56 |
-
|
| 57 |
-
if text.lower() in ("quit", "exit", "q"):
|
| 58 |
-
print("Bye!")
|
| 59 |
-
break
|
| 60 |
-
|
| 61 |
-
if not text:
|
| 62 |
-
continue
|
| 63 |
-
|
| 64 |
-
translation = evaluator.translate_single(text)
|
| 65 |
-
print(f"[ZH] > {translation}")
|
| 66 |
|
| 67 |
|
| 68 |
def translate_file(evaluator, input_path: str, output_path: str):
|
|
@@ -74,33 +51,7 @@ def translate_file(evaluator, input_path: str, output_path: str):
|
|
| 74 |
2. 批量翻译
|
| 75 |
3. 将结果写入输出文件
|
| 76 |
"""
|
| 77 |
-
|
| 78 |
-
if not input_file.exists():
|
| 79 |
-
print(f"Error: input file not found: {input_path}")
|
| 80 |
-
return
|
| 81 |
-
|
| 82 |
-
with open(input_file, "r", encoding="utf-8") as f:
|
| 83 |
-
lines = [line.strip() for line in f if line.strip()]
|
| 84 |
-
|
| 85 |
-
print(f"Translating {len(lines)} sentences...")
|
| 86 |
-
|
| 87 |
-
# 分批翻译
|
| 88 |
-
batch_size = 32
|
| 89 |
-
translations = []
|
| 90 |
-
for i in range(0, len(lines), batch_size):
|
| 91 |
-
batch = lines[i : i + batch_size]
|
| 92 |
-
batch_translations = evaluator.translate(batch)
|
| 93 |
-
translations.extend(batch_translations)
|
| 94 |
-
print(f" Translated {min(i + batch_size, len(lines))}/{len(lines)}")
|
| 95 |
-
|
| 96 |
-
# 写入输出文件
|
| 97 |
-
out_file = Path(output_path)
|
| 98 |
-
out_file.parent.mkdir(parents=True, exist_ok=True)
|
| 99 |
-
with open(out_file, "w", encoding="utf-8") as f:
|
| 100 |
-
for t in translations:
|
| 101 |
-
f.write(t + "\n")
|
| 102 |
-
|
| 103 |
-
print(f"Results saved to: {output_path}")
|
| 104 |
|
| 105 |
|
| 106 |
def launch_web_ui(evaluator):
|
|
@@ -113,25 +64,7 @@ def launch_web_ui(evaluator):
|
|
| 113 |
3. 输出: 中文翻译结果
|
| 114 |
4. 调用 evaluator.translate_single()
|
| 115 |
"""
|
| 116 |
-
|
| 117 |
-
import gradio as gr
|
| 118 |
-
except ImportError:
|
| 119 |
-
print("Error: gradio is not installed. Install with: pip install gradio")
|
| 120 |
-
return
|
| 121 |
-
|
| 122 |
-
def translate_fn(text):
|
| 123 |
-
if not text.strip():
|
| 124 |
-
return ""
|
| 125 |
-
return evaluator.translate_single(text)
|
| 126 |
-
|
| 127 |
-
interface = gr.Interface(
|
| 128 |
-
fn=translate_fn,
|
| 129 |
-
inputs=gr.Textbox(label="English", placeholder="Enter English text..."),
|
| 130 |
-
outputs=gr.Textbox(label="Chinese Translation"),
|
| 131 |
-
title="EasyTranslate - English to Chinese",
|
| 132 |
-
description="Transformer-based English to Chinese translation system.",
|
| 133 |
-
)
|
| 134 |
-
interface.launch()
|
| 135 |
|
| 136 |
|
| 137 |
def main():
|
|
@@ -141,37 +74,8 @@ def main():
|
|
| 141 |
print(" EasyTranslate - Translation")
|
| 142 |
print("=" * 60)
|
| 143 |
|
| 144 |
-
# 加载
|
| 145 |
-
|
| 146 |
-
config = yaml.safe_load(f)
|
| 147 |
-
|
| 148 |
-
# 加载模型
|
| 149 |
-
checkpoint = torch.load(args.checkpoint, map_location="cpu")
|
| 150 |
-
|
| 151 |
-
from easytranslate.model.transformer import Transformer
|
| 152 |
-
from easytranslate.data.tokenizer import build_tokenizer
|
| 153 |
-
|
| 154 |
-
model_config = config.get("model", {})
|
| 155 |
-
model = Transformer(model_config)
|
| 156 |
-
model.load_state_dict(checkpoint["model_state_dict"])
|
| 157 |
-
|
| 158 |
-
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 159 |
-
model = model.to(device)
|
| 160 |
-
model.eval()
|
| 161 |
-
|
| 162 |
-
tokenizer = build_tokenizer(config.get("data", {}).get("tokenizer", {}))
|
| 163 |
-
|
| 164 |
-
# 构建 evaluator
|
| 165 |
-
evaluator = Evaluator(model=model, tokenizer=tokenizer, config=config)
|
| 166 |
-
|
| 167 |
-
# 根据参数选择模式
|
| 168 |
-
if args.web:
|
| 169 |
-
launch_web_ui(evaluator)
|
| 170 |
-
elif args.input:
|
| 171 |
-
output_path = args.output or args.input.replace(".txt", "_translated.txt")
|
| 172 |
-
translate_file(evaluator, args.input, output_path)
|
| 173 |
-
else:
|
| 174 |
-
interactive_translate(evaluator)
|
| 175 |
|
| 176 |
|
| 177 |
if __name__ == "__main__":
|
|
|
|
| 16 |
import sys
|
| 17 |
from pathlib import Path
|
| 18 |
|
|
|
|
|
|
|
|
|
|
| 19 |
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
|
| 20 |
|
|
|
|
|
|
|
| 21 |
|
| 22 |
def parse_args():
|
| 23 |
parser = argparse.ArgumentParser(description="EasyTranslate Inference")
|
|
|
|
| 39 |
3. 打印翻译结果
|
| 40 |
4. 输入 'quit' 退出
|
| 41 |
"""
|
| 42 |
+
raise NotImplementedError("TODO: Person D 实现 interactive_translate")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
|
| 45 |
def translate_file(evaluator, input_path: str, output_path: str):
|
|
|
|
| 51 |
2. 批量翻译
|
| 52 |
3. 将结果写入输出文件
|
| 53 |
"""
|
| 54 |
+
raise NotImplementedError("TODO: Person D 实现 translate_file")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
|
| 57 |
def launch_web_ui(evaluator):
|
|
|
|
| 64 |
3. 输出: 中文翻译结果
|
| 65 |
4. 调用 evaluator.translate_single()
|
| 66 |
"""
|
| 67 |
+
raise NotImplementedError("TODO: Person D 实现 launch_web_ui")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
|
| 70 |
def main():
|
|
|
|
| 74 |
print(" EasyTranslate - Translation")
|
| 75 |
print("=" * 60)
|
| 76 |
|
| 77 |
+
# TODO: 加载模型、构建 evaluator,然后根据参数选择模式
|
| 78 |
+
raise NotImplementedError("TODO: 实现推理主流程")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
|
| 81 |
if __name__ == "__main__":
|
setup.py
CHANGED
|
@@ -1,78 +1,23 @@
|
|
| 1 |
from setuptools import setup, find_packages
|
| 2 |
|
| 3 |
-
with open("README.md", "r", encoding="utf-8") as fh:
|
| 4 |
-
long_description = fh.read()
|
| 5 |
-
|
| 6 |
setup(
|
| 7 |
name="easytranslate",
|
| 8 |
version="0.1.0",
|
| 9 |
description="Transformer-based English-to-Chinese Translation Model",
|
| 10 |
-
long_description=long_description,
|
| 11 |
-
long_description_content_type="text/markdown",
|
| 12 |
-
author="UCAS EasyTranslate Team",
|
| 13 |
packages=find_packages(where="src"),
|
| 14 |
package_dir={"": "src"},
|
| 15 |
python_requires=">=3.10",
|
| 16 |
install_requires=[
|
| 17 |
-
"torch>=2.1.0
|
| 18 |
-
"transformers>=4.36.0
|
| 19 |
-
"datasets>=2.16.0
|
| 20 |
-
"tokenizers>=0.15.0
|
| 21 |
-
"sentencepiece>=0.
|
| 22 |
-
"accelerate>=0.25.0
|
| 23 |
-
"
|
| 24 |
-
"
|
| 25 |
-
"
|
| 26 |
-
"
|
| 27 |
-
"
|
| 28 |
-
"omegaconf>=2.3.0,<3.0.0",
|
| 29 |
-
"rich>=13.0.0,<14.0.0",
|
| 30 |
-
"tqdm>=4.66.0,<5.0.0",
|
| 31 |
-
"pyyaml>=6.0.0,<7.0.0",
|
| 32 |
-
],
|
| 33 |
-
extras_require={
|
| 34 |
-
"dev": [
|
| 35 |
-
"pytest>=7.4.0,<9.0.0",
|
| 36 |
-
"pytest-cov>=4.1.0",
|
| 37 |
-
"black>=23.0.0,<25.0.0",
|
| 38 |
-
"isort>=5.12.0,<6.0.0",
|
| 39 |
-
"flake8>=6.0.0,<8.0.0",
|
| 40 |
-
],
|
| 41 |
-
"colab": [
|
| 42 |
-
"transformers>=4.36.0,<4.45.0",
|
| 43 |
-
"datasets>=2.16.0,<3.0.0",
|
| 44 |
-
"tokenizers>=0.15.0,<0.20.0",
|
| 45 |
-
"sentencepiece>=0.2.0,<0.3.0",
|
| 46 |
-
"accelerate>=0.25.0,<0.35.0",
|
| 47 |
-
"peft>=0.7.0,<0.12.0",
|
| 48 |
-
"sacrebleu>=2.4.0,<3.0.0",
|
| 49 |
-
"unbabel-comet>=2.2.0,<2.3.0",
|
| 50 |
-
"protobuf>=5.28.3,<7.0.0",
|
| 51 |
-
"omegaconf>=2.3.0,<3.0.0",
|
| 52 |
-
"rich>=13.0.0,<14.0.0",
|
| 53 |
-
"tqdm>=4.66.0,<5.0.0",
|
| 54 |
-
],
|
| 55 |
-
"distributed": [
|
| 56 |
-
"deepspeed>=0.12.0,<0.15.0",
|
| 57 |
-
"bitsandbytes>=0.41.0,<0.44.0",
|
| 58 |
-
],
|
| 59 |
-
"tracking": [
|
| 60 |
-
"wandb>=0.16.0,<0.18.0",
|
| 61 |
-
"tensorboard>=2.15.0,<2.17.0",
|
| 62 |
-
],
|
| 63 |
-
"serving": [
|
| 64 |
-
"fastapi>=0.108.0,<0.115.0",
|
| 65 |
-
"uvicorn>=0.25.0,<0.31.0",
|
| 66 |
-
"gradio>=4.10.0,<5.0.0",
|
| 67 |
-
],
|
| 68 |
-
},
|
| 69 |
-
classifiers=[
|
| 70 |
-
"Development Status :: 3 - Alpha",
|
| 71 |
-
"Intended Audience :: Science/Research",
|
| 72 |
-
"License :: OSI Approved :: MIT License",
|
| 73 |
-
"Programming Language :: Python :: 3.10",
|
| 74 |
-
"Programming Language :: Python :: 3.11",
|
| 75 |
-
"Programming Language :: Python :: 3.12",
|
| 76 |
-
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
| 77 |
],
|
| 78 |
-
)
|
|
|
|
| 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/data/collator.py
CHANGED
|
@@ -64,19 +64,6 @@ class DynamicBatchSampler(Sampler[list[int]]):
|
|
| 64 |
self.max_tokens_per_batch = max_tokens_per_batch
|
| 65 |
self.shuffle = shuffle
|
| 66 |
self.drop_last = drop_last
|
| 67 |
-
|
| 68 |
-
# 检查是否有超过最大token限制的序列
|
| 69 |
-
self._check_long_sequences()
|
| 70 |
-
|
| 71 |
-
def _check_long_sequences(self):
|
| 72 |
-
"""检查并警告过长的序列"""
|
| 73 |
-
long_seq_count = sum(1 for l in self.lengths if l > self.max_tokens_per_batch)
|
| 74 |
-
if long_seq_count > 0:
|
| 75 |
-
import warnings
|
| 76 |
-
warnings.warn(
|
| 77 |
-
f"Found {long_seq_count} sequences longer than max_tokens_per_batch "
|
| 78 |
-
f"({self.max_tokens_per_batch}). These will be placed in their own batches."
|
| 79 |
-
)
|
| 80 |
|
| 81 |
def __iter__(self) -> Iterator[list[int]]:
|
| 82 |
indices = list(range(len(self.lengths)))
|
|
@@ -89,18 +76,7 @@ class DynamicBatchSampler(Sampler[list[int]]):
|
|
| 89 |
max_len = 0
|
| 90 |
|
| 91 |
for idx in indices:
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
# 如果单个序列长度超过限制,单独放入一个批次
|
| 95 |
-
if seq_len > self.max_tokens_per_batch:
|
| 96 |
-
if batch:
|
| 97 |
-
batches.append(batch)
|
| 98 |
-
batch = []
|
| 99 |
-
max_len = 0
|
| 100 |
-
batches.append([idx])
|
| 101 |
-
continue
|
| 102 |
-
|
| 103 |
-
candidate_max_len = max(max_len, seq_len)
|
| 104 |
candidate_tokens = candidate_max_len * (len(batch) + 1)
|
| 105 |
|
| 106 |
if batch and candidate_tokens > self.max_tokens_per_batch:
|
|
@@ -109,7 +85,7 @@ class DynamicBatchSampler(Sampler[list[int]]):
|
|
| 109 |
max_len = 0
|
| 110 |
|
| 111 |
batch.append(idx)
|
| 112 |
-
max_len = max(max_len,
|
| 113 |
|
| 114 |
if batch and not self.drop_last:
|
| 115 |
batches.append(batch)
|
|
@@ -125,11 +101,6 @@ class DynamicBatchSampler(Sampler[list[int]]):
|
|
| 125 |
max_len = 0
|
| 126 |
|
| 127 |
for length in sorted(self.lengths):
|
| 128 |
-
# 过长的序列单独计数
|
| 129 |
-
if length > self.max_tokens_per_batch:
|
| 130 |
-
count += 1
|
| 131 |
-
continue
|
| 132 |
-
|
| 133 |
candidate_max_len = max(max_len, length)
|
| 134 |
if batch_size and candidate_max_len * (batch_size + 1) > self.max_tokens_per_batch:
|
| 135 |
count += 1
|
|
@@ -140,4 +111,4 @@ class DynamicBatchSampler(Sampler[list[int]]):
|
|
| 140 |
|
| 141 |
if batch_size and not self.drop_last:
|
| 142 |
count += 1
|
| 143 |
-
return count
|
|
|
|
| 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)))
|
|
|
|
| 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:
|
|
|
|
| 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)
|
|
|
|
| 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
|
|
|
|
| 111 |
|
| 112 |
if batch_size and not self.drop_last:
|
| 113 |
count += 1
|
| 114 |
+
return count
|
src/easytranslate/evaluation/decoding.py
CHANGED
|
@@ -34,23 +34,20 @@ def greedy_decode(
|
|
| 34 |
eos_id: int,
|
| 35 |
max_len: int = 256,
|
| 36 |
) -> torch.Tensor:
|
| 37 |
-
"""
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
break
|
| 52 |
-
|
| 53 |
-
return decoder_input
|
| 54 |
|
| 55 |
|
| 56 |
@torch.no_grad()
|
|
@@ -67,104 +64,29 @@ def beam_search_decode(
|
|
| 67 |
) -> torch.Tensor:
|
| 68 |
"""
|
| 69 |
束搜索解码。
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
1.
|
| 73 |
-
2.
|
| 74 |
-
3.
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
"""
|
| 77 |
-
|
| 78 |
-
beam_size = min(beam_size, 64)
|
| 79 |
-
|
| 80 |
-
batch_size, seq_len = src_ids.size()
|
| 81 |
-
device = src_ids.device
|
| 82 |
-
|
| 83 |
-
# 根据序列长度动态调整 beam_size
|
| 84 |
-
if seq_len > 512:
|
| 85 |
-
beam_size = min(beam_size, 3)
|
| 86 |
-
|
| 87 |
-
# 1. Encode source
|
| 88 |
-
encoder_output = model.encode(src_ids, src_padding_mask)
|
| 89 |
-
hidden_dim = encoder_output.size(-1)
|
| 90 |
-
|
| 91 |
-
# 2. Expand encoder output for beam search
|
| 92 |
-
encoder_output = encoder_output.unsqueeze(1).expand(-1, beam_size, -1, -1)
|
| 93 |
-
encoder_output = encoder_output.reshape(batch_size * beam_size, seq_len, hidden_dim)
|
| 94 |
-
src_padding_mask_expanded = src_padding_mask.unsqueeze(1).expand(-1, beam_size, -1)
|
| 95 |
-
src_padding_mask_expanded = src_padding_mask_expanded.reshape(batch_size * beam_size, seq_len)
|
| 96 |
-
|
| 97 |
-
# 3. Initialize beams - 使用预分配的 tensor 减少内存分配
|
| 98 |
-
beam_scores = torch.zeros(batch_size, beam_size, device=device)
|
| 99 |
-
beam_scores[:, 1:] = float("-inf")
|
| 100 |
-
|
| 101 |
-
# 预分配最大长度的 tensor,动态填充
|
| 102 |
-
beam_tokens = torch.full((batch_size, beam_size, max_len + 1), eos_id, dtype=torch.long, device=device)
|
| 103 |
-
beam_tokens[:, :, 0] = bos_id # [B, beam, 1]
|
| 104 |
-
|
| 105 |
-
finished = torch.zeros(batch_size, beam_size, dtype=torch.bool, device=device)
|
| 106 |
-
lengths = torch.ones(batch_size, beam_size, dtype=torch.long, device=device)
|
| 107 |
-
|
| 108 |
-
# 4. Iterative decoding
|
| 109 |
-
for step in range(1, max_len + 1):
|
| 110 |
-
# 只处理未完成的 beam
|
| 111 |
-
active_mask = ~finished
|
| 112 |
-
if not active_mask.any():
|
| 113 |
-
break
|
| 114 |
-
|
| 115 |
-
# 获取当前步的输入 [B*beam, step]
|
| 116 |
-
flat_tokens = beam_tokens[:, :, :step].reshape(batch_size * beam_size, step)
|
| 117 |
-
|
| 118 |
-
# 前向解码
|
| 119 |
-
logits = model.decode_step(flat_tokens, encoder_output, src_padding_mask_expanded)
|
| 120 |
-
log_probs = F.log_softmax(logits, dim=-1)
|
| 121 |
-
vocab_size = log_probs.size(-1)
|
| 122 |
-
|
| 123 |
-
# 应用 no_repeat_ngram 约束
|
| 124 |
-
if no_repeat_ngram_size > 0 and step >= no_repeat_ngram_size:
|
| 125 |
-
log_probs = _apply_no_repeat_ngram(log_probs, flat_tokens, no_repeat_ngram_size)
|
| 126 |
-
|
| 127 |
-
# Mask finished beams - 禁止生成新 token,但允许保持 EOS
|
| 128 |
-
finished_flat = finished.view(batch_size * beam_size)
|
| 129 |
-
if finished_flat.any():
|
| 130 |
-
log_probs[finished_flat] = float("-inf")
|
| 131 |
-
log_probs[finished_flat, eos_id] = 0.0
|
| 132 |
-
|
| 133 |
-
# 计算分数
|
| 134 |
-
scores = beam_scores.unsqueeze(-1) + log_probs.view(batch_size, beam_size, vocab_size)
|
| 135 |
-
scores = scores.view(batch_size, -1)
|
| 136 |
-
|
| 137 |
-
# 选择 top-k
|
| 138 |
-
topk_scores, topk_indices = scores.topk(beam_size, dim=-1)
|
| 139 |
-
beam_indices = topk_indices // vocab_size
|
| 140 |
-
token_indices = topk_indices % vocab_size
|
| 141 |
-
|
| 142 |
-
# 高效更新 beam_tokens - 使用索引而非拼接
|
| 143 |
-
new_beam_tokens = beam_tokens.clone()
|
| 144 |
-
for b in range(batch_size):
|
| 145 |
-
new_beam_tokens[b] = beam_tokens[b][beam_indices[b]]
|
| 146 |
-
new_beam_tokens[b, torch.arange(beam_size), step] = token_indices[b]
|
| 147 |
-
beam_tokens = new_beam_tokens
|
| 148 |
-
|
| 149 |
-
# 更新 finished 和 lengths
|
| 150 |
-
new_finished = finished.gather(1, beam_indices) | token_indices.eq(eos_id)
|
| 151 |
-
new_lengths = lengths.gather(1, beam_indices)
|
| 152 |
-
new_lengths[~new_finished] = step + 1
|
| 153 |
-
|
| 154 |
-
beam_scores = topk_scores
|
| 155 |
-
finished = new_finished
|
| 156 |
-
lengths = new_lengths
|
| 157 |
-
|
| 158 |
-
# 5. Apply length penalty
|
| 159 |
-
lengths = lengths.float()
|
| 160 |
-
penalties = lengths ** length_penalty
|
| 161 |
-
final_scores = beam_scores / penalties
|
| 162 |
-
|
| 163 |
-
# 6. Select best beam for each batch
|
| 164 |
-
best_indices = final_scores.argmax(dim=-1)
|
| 165 |
-
best_sequences = beam_tokens[torch.arange(batch_size, device=device), best_indices]
|
| 166 |
-
|
| 167 |
-
return best_sequences
|
| 168 |
|
| 169 |
|
| 170 |
@torch.no_grad()
|
|
@@ -179,46 +101,17 @@ def sample_decode(
|
|
| 179 |
top_k: int = 0,
|
| 180 |
top_p: float = 1.0,
|
| 181 |
) -> torch.Tensor:
|
| 182 |
-
"""
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
logits = logits / max(temperature, 1e-8)
|
| 194 |
-
|
| 195 |
-
if top_k > 0:
|
| 196 |
-
k = min(top_k, logits.size(-1))
|
| 197 |
-
topk_values, _ = torch.topk(logits, k, dim=-1)
|
| 198 |
-
threshold = topk_values[:, -1].unsqueeze(-1)
|
| 199 |
-
logits[logits < threshold] = float("-inf")
|
| 200 |
-
|
| 201 |
-
if 0.0 < top_p < 1.0:
|
| 202 |
-
sorted_logits, sorted_indices = torch.sort(logits, descending=True, dim=-1)
|
| 203 |
-
cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
|
| 204 |
-
# Remove tokens whose cumulative probability exceeds top_p
|
| 205 |
-
# (shift by one so the token that pushes above the threshold is kept)
|
| 206 |
-
remove_mask = cumulative_probs - F.softmax(sorted_logits, dim=-1) >= top_p
|
| 207 |
-
sorted_logits[remove_mask] = float("-inf")
|
| 208 |
-
# Scatter sorted values back to their original vocabulary positions
|
| 209 |
-
logits = torch.zeros_like(logits).scatter_(1, sorted_indices, sorted_logits)
|
| 210 |
-
|
| 211 |
-
probs = F.softmax(logits, dim=-1)
|
| 212 |
-
next_token = torch.multinomial(probs, num_samples=1)
|
| 213 |
-
|
| 214 |
-
next_token = torch.where(finished.unsqueeze(-1), torch.full_like(next_token, eos_id), next_token)
|
| 215 |
-
decoder_input = torch.cat([decoder_input, next_token], dim=1)
|
| 216 |
-
finished = finished | next_token.squeeze(-1).eq(eos_id)
|
| 217 |
-
|
| 218 |
-
if finished.all():
|
| 219 |
-
break
|
| 220 |
-
|
| 221 |
-
return decoder_input
|
| 222 |
|
| 223 |
|
| 224 |
def _apply_no_repeat_ngram(
|
|
@@ -226,28 +119,11 @@ def _apply_no_repeat_ngram(
|
|
| 226 |
generated_tokens: torch.Tensor,
|
| 227 |
ngram_size: int,
|
| 228 |
) -> torch.Tensor:
|
| 229 |
-
"""
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
return logits
|
| 238 |
-
|
| 239 |
-
for batch_idx in range(batch_size):
|
| 240 |
-
tokens = generated_tokens[batch_idx].tolist()
|
| 241 |
-
|
| 242 |
-
ngram_map: dict[tuple, set] = {}
|
| 243 |
-
for i in range(len(tokens) - ngram_size + 1):
|
| 244 |
-
prefix = tuple(tokens[i : i + ngram_size - 1])
|
| 245 |
-
next_tok = tokens[i + ngram_size - 1]
|
| 246 |
-
ngram_map.setdefault(prefix, set()).add(next_tok)
|
| 247 |
-
|
| 248 |
-
current_prefix = tuple(tokens[-(ngram_size - 1):])
|
| 249 |
-
if current_prefix in ngram_map:
|
| 250 |
-
banned = list(ngram_map[current_prefix])
|
| 251 |
-
logits[batch_idx, banned] = float("-inf")
|
| 252 |
-
|
| 253 |
-
return logits
|
|
|
|
| 34 |
eos_id: int,
|
| 35 |
max_len: int = 256,
|
| 36 |
) -> torch.Tensor:
|
| 37 |
+
"""
|
| 38 |
+
贪心解码。
|
| 39 |
+
|
| 40 |
+
TODO [Person D]: 实现以下逻辑:
|
| 41 |
+
1. encoder_output = model.encode(src_ids, src_padding_mask)
|
| 42 |
+
2. 初始化 decoder input: [B, 1] 全为 bos_id
|
| 43 |
+
3. for step in range(max_len):
|
| 44 |
+
a. logits = model.decode_step(decoder_input, encoder_output, src_padding_mask)
|
| 45 |
+
b. next_token = logits.argmax(dim=-1)
|
| 46 |
+
c. decoder_input = concat(decoder_input, next_token)
|
| 47 |
+
d. 如果所有序列都生成了 eos_id,则提前终止
|
| 48 |
+
4. 返回生成的 token ids [B, T]
|
| 49 |
+
"""
|
| 50 |
+
raise NotImplementedError("TODO: Person D 实现 greedy_decode")
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
|
| 53 |
@torch.no_grad()
|
|
|
|
| 64 |
) -> torch.Tensor:
|
| 65 |
"""
|
| 66 |
束搜索解码。
|
| 67 |
+
|
| 68 |
+
TODO [Person D]: 实现以下逻辑:
|
| 69 |
+
1. encoder_output = model.encode(src_ids, src_padding_mask)
|
| 70 |
+
2. 将 encoder_output 扩展为 beam_size 份: [B*beam, S, D]
|
| 71 |
+
3. 初始化 beam:
|
| 72 |
+
- beam_scores: [B, beam_size] 初始为 0
|
| 73 |
+
- beam_tokens: [B, beam_size, 1] 初始为 bos_id
|
| 74 |
+
4. for step in range(max_len):
|
| 75 |
+
a. 对每个 beam 计算 logits
|
| 76 |
+
b. log_probs = log_softmax(logits)
|
| 77 |
+
c. (可选) 应用 no_repeat_ngram 约束
|
| 78 |
+
d. scores = beam_scores + log_probs
|
| 79 |
+
e. 选择 top-k candidates (k = beam_size)
|
| 80 |
+
f. 更新 beam_tokens 和 beam_scores
|
| 81 |
+
g. 将已完成的 beam 移到 finished pool
|
| 82 |
+
5. 对 finished beams 应用 length_penalty:
|
| 83 |
+
score = score / (length ^ length_penalty)
|
| 84 |
+
6. 选择得分最高的序列
|
| 85 |
+
7. 返回最佳翻译 [B, T]
|
| 86 |
+
|
| 87 |
+
这是翻译任务最关键的解码算法,请仔细实现。
|
| 88 |
"""
|
| 89 |
+
raise NotImplementedError("TODO: Person D 实现 beam_search_decode")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
|
| 92 |
@torch.no_grad()
|
|
|
|
| 101 |
top_k: int = 0,
|
| 102 |
top_p: float = 1.0,
|
| 103 |
) -> torch.Tensor:
|
| 104 |
+
"""
|
| 105 |
+
采样解码 (支持 temperature, top-k, top-p/nucleus sampling)。
|
| 106 |
+
|
| 107 |
+
TODO [Person D]: 实现以下逻辑:
|
| 108 |
+
1. 与贪心解码类似,但每步采样而非取 argmax
|
| 109 |
+
2. 应用 temperature: logits = logits / temperature
|
| 110 |
+
3. 应用 top-k: 只保留概率最高的 k 个 token
|
| 111 |
+
4. 应用 top-p (nucleus): 只保留累积概率达到 p 的 token
|
| 112 |
+
5. 从过滤后的分布中采样: torch.multinomial
|
| 113 |
+
"""
|
| 114 |
+
raise NotImplementedError("TODO: Person D 实现 sample_decode")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
|
| 117 |
def _apply_no_repeat_ngram(
|
|
|
|
| 119 |
generated_tokens: torch.Tensor,
|
| 120 |
ngram_size: int,
|
| 121 |
) -> torch.Tensor:
|
| 122 |
+
"""
|
| 123 |
+
防止生成重复�� n-gram。
|
| 124 |
+
|
| 125 |
+
TODO [Person D]:
|
| 126 |
+
1. 从 generated_tokens 中提取所有已出现的 (ngram_size-1)-gram
|
| 127 |
+
2. 对于每个可能导致重复 ngram 的 next token,将其 logits 设为 -inf
|
| 128 |
+
"""
|
| 129 |
+
raise NotImplementedError("TODO: Person D 实现 _apply_no_repeat_ngram")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/easytranslate/evaluation/evaluator.py
CHANGED
|
@@ -26,82 +26,20 @@ logger = logging.getLogger(__name__)
|
|
| 26 |
|
| 27 |
|
| 28 |
class Evaluator:
|
| 29 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
def __init__(self, model: nn.Module, tokenizer, config: dict):
|
| 32 |
-
"""
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
self.strategy = decoding_config.get("strategy", "beam_search")
|
| 41 |
-
self.max_len = decoding_config.get("max_decode_len", 256)
|
| 42 |
-
self.beam_size = decoding_config.get("beam_size", 5)
|
| 43 |
-
self.length_penalty = decoding_config.get("length_penalty", 1.0)
|
| 44 |
-
self.no_repeat_ngram_size = decoding_config.get("no_repeat_ngram_size", 0)
|
| 45 |
-
|
| 46 |
-
sampling_config = decoding_config.get("sampling", {})
|
| 47 |
-
self.temperature = sampling_config.get("temperature", 1.0)
|
| 48 |
-
self.top_k = sampling_config.get("top_k", 0)
|
| 49 |
-
self.top_p = sampling_config.get("top_p", 1.0)
|
| 50 |
-
|
| 51 |
-
self.metrics = eval_config.get("metrics", ["bleu", "comet", "chrf", "ter"])
|
| 52 |
-
|
| 53 |
-
self.bos_id = tokenizer.bos_token_id
|
| 54 |
-
self.eos_id = tokenizer.eos_token_id
|
| 55 |
-
self.pad_id = tokenizer.pad_token_id
|
| 56 |
-
|
| 57 |
-
self.decode_fn = self._get_decode_fn()
|
| 58 |
-
|
| 59 |
-
def _get_decode_fn(self):
|
| 60 |
-
"""根据策略选择解码函数。"""
|
| 61 |
-
if self.strategy == "greedy":
|
| 62 |
-
return self._greedy
|
| 63 |
-
elif self.strategy == "sampling":
|
| 64 |
-
return self._sample
|
| 65 |
-
else:
|
| 66 |
-
return self._beam_search
|
| 67 |
-
|
| 68 |
-
def _greedy(self, src_ids, src_padding_mask):
|
| 69 |
-
return greedy_decode(
|
| 70 |
-
self.model, src_ids, src_padding_mask,
|
| 71 |
-
self.bos_id, self.eos_id, max_len=self.max_len,
|
| 72 |
-
)
|
| 73 |
-
|
| 74 |
-
def _beam_search(self, src_ids, src_padding_mask):
|
| 75 |
-
return beam_search_decode(
|
| 76 |
-
self.model, src_ids, src_padding_mask,
|
| 77 |
-
self.bos_id, self.eos_id,
|
| 78 |
-
beam_size=self.beam_size, max_len=self.max_len,
|
| 79 |
-
length_penalty=self.length_penalty,
|
| 80 |
-
no_repeat_ngram_size=self.no_repeat_ngram_size,
|
| 81 |
-
)
|
| 82 |
-
|
| 83 |
-
def _sample(self, src_ids, src_padding_mask):
|
| 84 |
-
return sample_decode(
|
| 85 |
-
self.model, src_ids, src_padding_mask,
|
| 86 |
-
self.bos_id, self.eos_id,
|
| 87 |
-
max_len=self.max_len, temperature=self.temperature,
|
| 88 |
-
top_k=self.top_k, top_p=self.top_p,
|
| 89 |
-
)
|
| 90 |
-
|
| 91 |
-
def _validate_batch(self, batch: dict) -> bool:
|
| 92 |
-
"""验证批处理格式是否符合预期。"""
|
| 93 |
-
required_keys = ["src_ids"]
|
| 94 |
-
|
| 95 |
-
for key in required_keys:
|
| 96 |
-
if key not in batch:
|
| 97 |
-
logger.error(f"Batch is missing required key: {key}")
|
| 98 |
-
return False
|
| 99 |
-
|
| 100 |
-
if not isinstance(batch["src_ids"], torch.Tensor):
|
| 101 |
-
logger.error(f"src_ids must be a torch.Tensor, got {type(batch['src_ids'])}")
|
| 102 |
-
return False
|
| 103 |
-
|
| 104 |
-
return True
|
| 105 |
|
| 106 |
def evaluate(
|
| 107 |
self,
|
|
@@ -111,74 +49,28 @@ class Evaluator:
|
|
| 111 |
) -> dict:
|
| 112 |
"""
|
| 113 |
在给定数据上进行评估。
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
dict: 包含所有评估指标的字典
|
| 122 |
-
|
| 123 |
-
Raises:
|
| 124 |
-
ValueError: 如果批处理格式不正确
|
| 125 |
"""
|
| 126 |
-
|
| 127 |
-
device = next(self.model.parameters()).device
|
| 128 |
-
|
| 129 |
-
hypotheses = []
|
| 130 |
-
|
| 131 |
-
with torch.no_grad():
|
| 132 |
-
for batch in tqdm(dataloader, desc="Evaluating"):
|
| 133 |
-
# 验证批处理格式
|
| 134 |
-
if not self._validate_batch(batch):
|
| 135 |
-
raise ValueError("Invalid batch format. Expected 'src_ids' key with torch.Tensor value.")
|
| 136 |
-
|
| 137 |
-
src_ids = batch["src_ids"].to(device)
|
| 138 |
-
src_padding_mask = batch.get("src_padding_mask")
|
| 139 |
-
if src_padding_mask is None:
|
| 140 |
-
src_padding_mask = src_ids.eq(self.pad_id)
|
| 141 |
-
else:
|
| 142 |
-
src_padding_mask = src_padding_mask.to(device)
|
| 143 |
-
|
| 144 |
-
output_ids = self.decode_fn(src_ids, src_padding_mask)
|
| 145 |
-
|
| 146 |
-
for i in range(output_ids.size(0)):
|
| 147 |
-
text = self.tokenizer.decode(
|
| 148 |
-
output_ids[i].tolist(), skip_special_tokens=True
|
| 149 |
-
)
|
| 150 |
-
hypotheses.append(text)
|
| 151 |
-
|
| 152 |
-
results = compute_all_metrics(
|
| 153 |
-
sources=src_texts,
|
| 154 |
-
hypotheses=hypotheses,
|
| 155 |
-
references=ref_texts,
|
| 156 |
-
metrics=self.metrics,
|
| 157 |
-
)
|
| 158 |
-
return results
|
| 159 |
|
| 160 |
def translate(self, texts: list[str]) -> list[str]:
|
| 161 |
-
"""
|
| 162 |
-
|
| 163 |
-
device = next(self.model.parameters()).device
|
| 164 |
-
|
| 165 |
-
encoded = [self.tokenizer.encode(t, add_special_tokens=True) for t in texts]
|
| 166 |
-
max_len_src = max(len(ids) for ids in encoded)
|
| 167 |
-
src_ids = torch.full((len(texts), max_len_src), self.pad_id, dtype=torch.long, device=device)
|
| 168 |
-
for i, ids in enumerate(encoded):
|
| 169 |
-
src_ids[i, :len(ids)] = torch.tensor(ids, dtype=torch.long)
|
| 170 |
-
src_padding_mask = src_ids.eq(self.pad_id)
|
| 171 |
-
|
| 172 |
-
with torch.no_grad():
|
| 173 |
-
output_ids = self.decode_fn(src_ids, src_padding_mask)
|
| 174 |
-
|
| 175 |
-
translations = []
|
| 176 |
-
for i in range(output_ids.size(0)):
|
| 177 |
-
text = self.tokenizer.decode(output_ids[i].tolist(), skip_special_tokens=True)
|
| 178 |
-
translations.append(text)
|
| 179 |
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
def translate_single(self, text: str) -> str:
|
| 183 |
"""翻译单条文本。"""
|
| 184 |
-
return self.translate([text])[0]
|
|
|
|
| 26 |
|
| 27 |
|
| 28 |
class Evaluator:
|
| 29 |
+
"""
|
| 30 |
+
翻译模型评估器。
|
| 31 |
+
|
| 32 |
+
TODO [Person D]: 实现以下方法。
|
| 33 |
+
"""
|
| 34 |
|
| 35 |
def __init__(self, model: nn.Module, tokenizer, config: dict):
|
| 36 |
+
"""
|
| 37 |
+
TODO [Person D]:
|
| 38 |
+
1. 保存 model, tokenizer, config
|
| 39 |
+
2. 从 config 读取解码策略和评估指标配置
|
| 40 |
+
3. 根据策略选择解码函数
|
| 41 |
+
"""
|
| 42 |
+
raise NotImplementedError("TODO: Person D 实现 Evaluator.__init__")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
def evaluate(
|
| 45 |
self,
|
|
|
|
| 49 |
) -> dict:
|
| 50 |
"""
|
| 51 |
在给定数据上进行评估。
|
| 52 |
+
|
| 53 |
+
TODO [Person D]: 实现以下逻辑:
|
| 54 |
+
1. model.eval()
|
| 55 |
+
2. 遍历 dataloader,使用选定的解码策略生成翻译
|
| 56 |
+
3. 将生成的 token ids 解码为文本
|
| 57 |
+
4. 调用 compute_all_metrics 计算指标
|
| 58 |
+
5. 返回评估结果 dict
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
"""
|
| 60 |
+
raise NotImplementedError("TODO: Person D 实现 evaluate")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
def translate(self, texts: list[str]) -> list[str]:
|
| 63 |
+
"""
|
| 64 |
+
翻译一批文本。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
+
TODO [Person D]:
|
| 67 |
+
1. tokenize 输入文本
|
| 68 |
+
2. 调用解码函数生成翻译
|
| 69 |
+
3. 解码为文本
|
| 70 |
+
4. 返回翻译结果列表
|
| 71 |
+
"""
|
| 72 |
+
raise NotImplementedError("TODO: Person D 实现 translate")
|
| 73 |
|
| 74 |
def translate_single(self, text: str) -> str:
|
| 75 |
"""翻译单条文本。"""
|
| 76 |
+
return self.translate([text])[0]
|
src/easytranslate/evaluation/metrics.py
CHANGED
|
@@ -18,23 +18,11 @@
|
|
| 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 _require_sacrebleu():
|
| 28 |
-
try:
|
| 29 |
-
import sacrebleu
|
| 30 |
-
except ImportError as exc:
|
| 31 |
-
raise ImportError(
|
| 32 |
-
"sacrebleu is required for BLEU/chrF/TER evaluation. "
|
| 33 |
-
"Install it with `pip install sacrebleu`."
|
| 34 |
-
) from exc
|
| 35 |
-
return sacrebleu
|
| 36 |
-
|
| 37 |
-
|
| 38 |
def compute_bleu(
|
| 39 |
hypotheses: list[str],
|
| 40 |
references: list[str],
|
|
@@ -42,37 +30,15 @@ def compute_bleu(
|
|
| 42 |
) -> dict:
|
| 43 |
"""
|
| 44 |
计算 SacreBLEU 分数。
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
dict: 包含 BLEU 分数及各 n-gram 精度的字典
|
| 53 |
-
|
| 54 |
-
Raises:
|
| 55 |
-
ValueError: 如果输入数据为空或长度不匹配
|
| 56 |
"""
|
| 57 |
-
|
| 58 |
-
raise ValueError("hypotheses and references cannot be empty")
|
| 59 |
-
|
| 60 |
-
if len(hypotheses) != len(references):
|
| 61 |
-
raise ValueError(
|
| 62 |
-
f"hypotheses and references length mismatch: "
|
| 63 |
-
f"{len(hypotheses)} vs {len(references)}"
|
| 64 |
-
)
|
| 65 |
-
|
| 66 |
-
sacrebleu = _require_sacrebleu()
|
| 67 |
-
bleu = sacrebleu.corpus_bleu(hypotheses, [references], tokenize=tokenize)
|
| 68 |
-
return {
|
| 69 |
-
"bleu": round(float(bleu.score), 4),
|
| 70 |
-
"bleu_1": round(float(bleu.precisions[0]), 4),
|
| 71 |
-
"bleu_2": round(float(bleu.precisions[1]), 4),
|
| 72 |
-
"bleu_3": round(float(bleu.precisions[2]), 4),
|
| 73 |
-
"bleu_4": round(float(bleu.precisions[3]), 4),
|
| 74 |
-
"bp": round(float(bleu.bp), 4),
|
| 75 |
-
}
|
| 76 |
|
| 77 |
|
| 78 |
def compute_comet(
|
|
@@ -85,48 +51,16 @@ def compute_comet(
|
|
| 85 |
) -> dict:
|
| 86 |
"""
|
| 87 |
计算 COMET 分数。
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
Returns:
|
| 98 |
-
dict: 包含 COMET 系统分数和句子级别分数的字典
|
| 99 |
-
|
| 100 |
-
Raises:
|
| 101 |
-
ValueError: 如果输入数据为空或长度不匹配
|
| 102 |
-
ImportError: 如果 COMET 库未安装
|
| 103 |
"""
|
| 104 |
-
|
| 105 |
-
from comet import download_model, load_from_checkpoint
|
| 106 |
-
except ImportError:
|
| 107 |
-
raise ImportError(
|
| 108 |
-
"COMET is not installed. Please install it with: "
|
| 109 |
-
"pip install unbabel-comet"
|
| 110 |
-
)
|
| 111 |
-
|
| 112 |
-
if not sources or not hypotheses or not references:
|
| 113 |
-
raise ValueError("sources, hypotheses, and references cannot be empty")
|
| 114 |
-
|
| 115 |
-
if len(sources) != len(hypotheses) or len(hypotheses) != len(references):
|
| 116 |
-
raise ValueError(
|
| 117 |
-
f"Input lists have mismatched lengths: "
|
| 118 |
-
f"sources={len(sources)}, hypotheses={len(hypotheses)}, references={len(references)}"
|
| 119 |
-
)
|
| 120 |
-
|
| 121 |
-
model_path = download_model(model_name)
|
| 122 |
-
model = load_from_checkpoint(model_path)
|
| 123 |
-
|
| 124 |
-
data = [{"src": s, "mt": h, "ref": r} for s, h, r in zip(sources, hypotheses, references)]
|
| 125 |
-
prediction = model.predict(data, batch_size=batch_size, gpus=gpus)
|
| 126 |
-
|
| 127 |
-
system_score = prediction.system_score
|
| 128 |
-
segment_scores = [float(s) for s in prediction.scores]
|
| 129 |
-
return {"comet": round(float(system_score), 4), "comet_scores": segment_scores}
|
| 130 |
|
| 131 |
|
| 132 |
def compute_chrf(
|
|
@@ -135,29 +69,12 @@ def compute_chrf(
|
|
| 135 |
) -> dict:
|
| 136 |
"""
|
| 137 |
计算 chrF++ 分数。
|
| 138 |
-
|
| 139 |
-
Args:
|
| 140 |
-
hypotheses: 生成的译文列表
|
| 141 |
-
references: 参考译文列表
|
| 142 |
-
|
| 143 |
-
Returns:
|
| 144 |
-
dict: 包含 chrF++ 分数的字典
|
| 145 |
-
|
| 146 |
-
Raises:
|
| 147 |
-
ValueError: 如果输入数据为空或长度不匹配
|
| 148 |
-
"""
|
| 149 |
-
if not hypotheses or not references:
|
| 150 |
-
raise ValueError("hypotheses and references cannot be empty")
|
| 151 |
-
|
| 152 |
-
if len(hypotheses) != len(references):
|
| 153 |
-
raise ValueError(
|
| 154 |
-
f"hypotheses and references length mismatch: "
|
| 155 |
-
f"{len(hypotheses)} vs {len(references)}"
|
| 156 |
-
)
|
| 157 |
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
|
|
|
|
|
|
| 161 |
|
| 162 |
|
| 163 |
def compute_ter(
|
|
@@ -166,89 +83,27 @@ def compute_ter(
|
|
| 166 |
) -> dict:
|
| 167 |
"""
|
| 168 |
计算 TER 分数。
|
| 169 |
-
|
| 170 |
-
Args:
|
| 171 |
-
hypotheses: 生成的译文列表
|
| 172 |
-
references: 参考译文列表
|
| 173 |
-
|
| 174 |
-
Returns:
|
| 175 |
-
dict: 包含 TER 分数的字典
|
| 176 |
-
|
| 177 |
-
Raises:
|
| 178 |
-
ValueError: 如果输入数据为空或长度不匹配
|
| 179 |
-
"""
|
| 180 |
-
if not hypotheses or not references:
|
| 181 |
-
raise ValueError("hypotheses and references cannot be empty")
|
| 182 |
-
|
| 183 |
-
if len(hypotheses) != len(references):
|
| 184 |
-
raise ValueError(
|
| 185 |
-
f"hypotheses and references length mismatch: "
|
| 186 |
-
f"{len(hypotheses)} vs {len(references)}"
|
| 187 |
-
)
|
| 188 |
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
| 192 |
|
| 193 |
|
| 194 |
def compute_all_metrics(
|
| 195 |
-
sources:
|
| 196 |
-
hypotheses:
|
| 197 |
-
references:
|
| 198 |
metrics: list[str] = ["bleu", "comet", "chrf", "ter"],
|
| 199 |
) -> dict:
|
| 200 |
"""
|
| 201 |
计算所有指定的评估指标。
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
Returns:
|
| 210 |
-
dict: 包含所有计算指标的字典
|
| 211 |
-
|
| 212 |
-
Raises:
|
| 213 |
-
ValueError: 如果 hypotheses 或 references 为空
|
| 214 |
"""
|
| 215 |
-
|
| 216 |
-
if not hypotheses:
|
| 217 |
-
raise ValueError("hypotheses cannot be empty")
|
| 218 |
-
|
| 219 |
-
if not references:
|
| 220 |
-
raise ValueError("references cannot be empty")
|
| 221 |
-
|
| 222 |
-
if len(hypotheses) != len(references):
|
| 223 |
-
raise ValueError(
|
| 224 |
-
f"hypotheses and references length mismatch: "
|
| 225 |
-
f"{len(hypotheses)} vs {len(references)}"
|
| 226 |
-
)
|
| 227 |
-
|
| 228 |
-
results = {}
|
| 229 |
-
|
| 230 |
-
for metric in metrics:
|
| 231 |
-
start = time.time()
|
| 232 |
-
try:
|
| 233 |
-
if metric == "bleu":
|
| 234 |
-
results.update(compute_bleu(hypotheses, references))
|
| 235 |
-
elif metric == "comet":
|
| 236 |
-
if sources is None:
|
| 237 |
-
logger.warning("COMET requires sources, skipping")
|
| 238 |
-
continue
|
| 239 |
-
results.update(compute_comet(sources, hypotheses, references))
|
| 240 |
-
elif metric == "chrf":
|
| 241 |
-
results.update(compute_chrf(hypotheses, references))
|
| 242 |
-
elif metric == "ter":
|
| 243 |
-
results.update(compute_ter(hypotheses, references))
|
| 244 |
-
else:
|
| 245 |
-
logger.warning("Unknown metric: %s, skipping", metric)
|
| 246 |
-
continue
|
| 247 |
-
except Exception as e:
|
| 248 |
-
logger.error(f"Failed to compute {metric}: {str(e)}")
|
| 249 |
-
continue
|
| 250 |
-
|
| 251 |
-
elapsed = time.time() - start
|
| 252 |
-
logger.info("Computed %s in %.2fs", metric, elapsed)
|
| 253 |
-
|
| 254 |
-
return results
|
|
|
|
| 18 |
from __future__ import annotations
|
| 19 |
|
| 20 |
import logging
|
|
|
|
| 21 |
from typing import Optional
|
| 22 |
|
| 23 |
logger = logging.getLogger(__name__)
|
| 24 |
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
def compute_bleu(
|
| 27 |
hypotheses: list[str],
|
| 28 |
references: list[str],
|
|
|
|
| 30 |
) -> dict:
|
| 31 |
"""
|
| 32 |
计算 SacreBLEU 分数。
|
| 33 |
+
|
| 34 |
+
TODO [Person D]: 实现以下逻辑:
|
| 35 |
+
1. 使用 sacrebleu.corpus_bleu(hypotheses, [references], tokenize=tokenize)
|
| 36 |
+
2. tokenize="zh" 对中文进行字符级别分词
|
| 37 |
+
3. 返回 {"bleu": score, "bleu_1": ..., "bleu_2": ..., "bleu_3": ..., "bleu_4": ..., "bp": ...}
|
| 38 |
+
|
| 39 |
+
注意: references 需要包装为 list of list (支持多参考)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
"""
|
| 41 |
+
raise NotImplementedError("TODO: Person D 实现 compute_bleu")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
|
| 44 |
def compute_comet(
|
|
|
|
| 51 |
) -> dict:
|
| 52 |
"""
|
| 53 |
计算 COMET 分数。
|
| 54 |
+
|
| 55 |
+
TODO [Person D]: 实现以下逻辑:
|
| 56 |
+
1. 加载 COMET 模型: comet.download_model(model_name)
|
| 57 |
+
2. 构建输入数据: [{"src": s, "mt": h, "ref": r} for s, h, r in zip(...)]
|
| 58 |
+
3. 调用 model.predict(data, batch_size, gpus)
|
| 59 |
+
4. 返回 {"comet": system_score, "comet_scores": segment_scores}
|
| 60 |
+
|
| 61 |
+
COMET 需要源语言、翻译结果和参考翻译三者。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
"""
|
| 63 |
+
raise NotImplementedError("TODO: Person D 实现 compute_comet")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
|
| 66 |
def compute_chrf(
|
|
|
|
| 69 |
) -> dict:
|
| 70 |
"""
|
| 71 |
计算 chrF++ 分数。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
+
TODO [Person D]:
|
| 74 |
+
1. 使用 sacrebleu.corpus_chrf(hypotheses, [references])
|
| 75 |
+
2. 返回 {"chrf": score}
|
| 76 |
+
"""
|
| 77 |
+
raise NotImplementedError("TODO: Person D 实现 compute_chrf")
|
| 78 |
|
| 79 |
|
| 80 |
def compute_ter(
|
|
|
|
| 83 |
) -> dict:
|
| 84 |
"""
|
| 85 |
计算 TER 分数。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
+
TODO [Person D]:
|
| 88 |
+
1. 使用 sacrebleu.corpus_ter(hypotheses, [references])
|
| 89 |
+
2. 返回 {"ter": score}
|
| 90 |
+
"""
|
| 91 |
+
raise NotImplementedError("TODO: Person D 实现 compute_ter")
|
| 92 |
|
| 93 |
|
| 94 |
def compute_all_metrics(
|
| 95 |
+
sources: list[str],
|
| 96 |
+
hypotheses: list[str],
|
| 97 |
+
references: list[str],
|
| 98 |
metrics: list[str] = ["bleu", "comet", "chrf", "ter"],
|
| 99 |
) -> dict:
|
| 100 |
"""
|
| 101 |
计算所有指定的评估指标。
|
| 102 |
+
|
| 103 |
+
TODO [Person D]:
|
| 104 |
+
1. 遍历 metrics 列表
|
| 105 |
+
2. 调用对应的计算函数
|
| 106 |
+
3. 合并结果并返回
|
| 107 |
+
4. 记录每个指标的计算时间
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
"""
|
| 109 |
+
raise NotImplementedError("TODO: Person D 实现 compute_all_metrics")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/easytranslate/model/attention.py
CHANGED
|
@@ -25,6 +25,24 @@ import torch.nn.functional as F
|
|
| 25 |
class MultiHeadAttention(nn.Module):
|
| 26 |
"""
|
| 27 |
标准多头注意力机制。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
"""
|
| 29 |
|
| 30 |
def __init__(
|
|
@@ -39,15 +57,8 @@ class MultiHeadAttention(nn.Module):
|
|
| 39 |
self.d_model = d_model
|
| 40 |
self.nhead = nhead
|
| 41 |
self.d_k = d_model // nhead
|
| 42 |
-
self.use_rotary_embedding = use_rotary_embedding
|
| 43 |
-
|
| 44 |
-
self.q_proj = nn.Linear(d_model, d_model)
|
| 45 |
-
self.k_proj = nn.Linear(d_model, d_model)
|
| 46 |
-
self.v_proj = nn.Linear(d_model, d_model)
|
| 47 |
-
self.out_proj = nn.Linear(d_model, d_model)
|
| 48 |
|
| 49 |
-
|
| 50 |
-
self.rope: Optional[nn.Module] = None
|
| 51 |
|
| 52 |
def forward(
|
| 53 |
self,
|
|
@@ -56,63 +67,22 @@ class MultiHeadAttention(nn.Module):
|
|
| 56 |
value: torch.Tensor, # [B, L_v, D]
|
| 57 |
key_padding_mask: Optional[torch.BoolTensor] = None, # [B, L_k]
|
| 58 |
attn_mask: Optional[torch.Tensor] = None, # [L_q, L_k]
|
| 59 |
-
is_causal: bool = False,
|
| 60 |
) -> torch.Tensor:
|
| 61 |
-
|
| 62 |
-
L_k = key.size(1)
|
| 63 |
-
L_v = value.size(1)
|
| 64 |
-
|
| 65 |
-
Q = self.q_proj(query)
|
| 66 |
-
K = self.k_proj(key)
|
| 67 |
-
V = self.v_proj(value)
|
| 68 |
-
|
| 69 |
-
Q = Q.view(B, L_q, self.nhead, self.d_k).transpose(1, 2)
|
| 70 |
-
K = K.view(B, L_k, self.nhead, self.d_k).transpose(1, 2)
|
| 71 |
-
V = V.view(B, L_v, self.nhead, self.d_k).transpose(1, 2)
|
| 72 |
-
|
| 73 |
-
if self.use_rotary_embedding and self.rope is not None:
|
| 74 |
-
Q, K = self.rope.apply_rotary_pos_emb(Q, K)
|
| 75 |
-
|
| 76 |
-
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
|
| 77 |
-
|
| 78 |
-
if key_padding_mask is not None:
|
| 79 |
-
scores = scores.masked_fill(
|
| 80 |
-
key_padding_mask.unsqueeze(1).unsqueeze(2), float("-inf")
|
| 81 |
-
)
|
| 82 |
-
if is_causal:
|
| 83 |
-
L_q_local, L_k_local = scores.size(-2), scores.size(-1)
|
| 84 |
-
causal_mask = torch.triu(
|
| 85 |
-
torch.ones(L_q_local, L_k_local, device=scores.device), diagonal=1
|
| 86 |
-
).bool()
|
| 87 |
-
scores = scores.masked_fill(
|
| 88 |
-
causal_mask.unsqueeze(0).unsqueeze(0), float("-inf")
|
| 89 |
-
)
|
| 90 |
-
if attn_mask is not None:
|
| 91 |
-
scores = scores.masked_fill(attn_mask.unsqueeze(0).unsqueeze(0), float("-inf"))
|
| 92 |
-
|
| 93 |
-
attn_weights = F.softmax(scores, dim=-1)
|
| 94 |
-
attn_weights = self.dropout(attn_weights)
|
| 95 |
-
|
| 96 |
-
attn_output = torch.matmul(attn_weights, V)
|
| 97 |
-
|
| 98 |
-
attn_output = attn_output.transpose(1, 2).contiguous().view(B, L_q, self.d_model)
|
| 99 |
-
|
| 100 |
-
output = self.out_proj(attn_output)
|
| 101 |
-
return output
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
try:
|
| 105 |
-
from torch.nn.functional import scaled_dot_product_attention
|
| 106 |
-
_has_flash_attn = True
|
| 107 |
-
except ImportError:
|
| 108 |
-
_has_flash_attn = False
|
| 109 |
|
| 110 |
|
| 111 |
class FlashMultiHeadAttention(nn.Module):
|
| 112 |
"""
|
| 113 |
Flash Attention 2 加速的多头注意力。
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
"""
|
| 117 |
|
| 118 |
def __init__(
|
|
@@ -123,34 +93,7 @@ class FlashMultiHeadAttention(nn.Module):
|
|
| 123 |
use_rotary_embedding: bool = False,
|
| 124 |
):
|
| 125 |
super().__init__()
|
| 126 |
-
|
| 127 |
-
if not _has_flash_attn:
|
| 128 |
-
self._fallback = MultiHeadAttention(
|
| 129 |
-
d_model=d_model,
|
| 130 |
-
nhead=nhead,
|
| 131 |
-
dropout=dropout,
|
| 132 |
-
use_rotary_embedding=use_rotary_embedding,
|
| 133 |
-
)
|
| 134 |
-
self.d_model = d_model
|
| 135 |
-
self.nhead = nhead
|
| 136 |
-
self.d_k = d_model // nhead
|
| 137 |
-
self.use_rotary_embedding = use_rotary_embedding
|
| 138 |
-
self.rope: Optional[nn.Module] = None
|
| 139 |
-
return
|
| 140 |
-
|
| 141 |
-
assert d_model % nhead == 0, "d_model 必须能被 nhead 整除"
|
| 142 |
-
self.d_model = d_model
|
| 143 |
-
self.nhead = nhead
|
| 144 |
-
self.d_k = d_model // nhead
|
| 145 |
-
self.use_rotary_embedding = use_rotary_embedding
|
| 146 |
-
self.dropout_p = dropout
|
| 147 |
-
|
| 148 |
-
self.q_proj = nn.Linear(d_model, d_model)
|
| 149 |
-
self.k_proj = nn.Linear(d_model, d_model)
|
| 150 |
-
self.v_proj = nn.Linear(d_model, d_model)
|
| 151 |
-
self.out_proj = nn.Linear(d_model, d_model)
|
| 152 |
-
|
| 153 |
-
self.rope: Optional[nn.Module] = None
|
| 154 |
|
| 155 |
def forward(
|
| 156 |
self,
|
|
@@ -160,54 +103,4 @@ class FlashMultiHeadAttention(nn.Module):
|
|
| 160 |
key_padding_mask: Optional[torch.BoolTensor] = None,
|
| 161 |
is_causal: bool = False,
|
| 162 |
) -> torch.Tensor:
|
| 163 |
-
|
| 164 |
-
return self._fallback(query, key, value, key_padding_mask, None, is_causal)
|
| 165 |
-
|
| 166 |
-
B, L_q, _ = query.size()
|
| 167 |
-
L_k = key.size(1)
|
| 168 |
-
|
| 169 |
-
Q = self.q_proj(query)
|
| 170 |
-
K = self.k_proj(key)
|
| 171 |
-
V = self.v_proj(value)
|
| 172 |
-
|
| 173 |
-
Q = Q.view(B, L_q, self.nhead, self.d_k).transpose(1, 2)
|
| 174 |
-
K = K.view(B, L_k, self.nhead, self.d_k).transpose(1, 2)
|
| 175 |
-
V = V.view(B, L_k, self.nhead, self.d_k).transpose(1, 2)
|
| 176 |
-
|
| 177 |
-
if self.use_rotary_embedding and self.rope is not None:
|
| 178 |
-
Q, K = self.rope.apply_rotary_pos_emb(Q, K)
|
| 179 |
-
|
| 180 |
-
# Build an additive attention bias so we can combine causal mask and
|
| 181 |
-
# key_padding_mask without conflicting with the is_causal flag.
|
| 182 |
-
# F.scaled_dot_product_attention raises if both is_causal and attn_mask are set.
|
| 183 |
-
if is_causal or key_padding_mask is not None:
|
| 184 |
-
attn_bias = torch.zeros(B, 1, L_q, L_k, device=Q.device, dtype=Q.dtype)
|
| 185 |
-
if is_causal:
|
| 186 |
-
causal = torch.triu(
|
| 187 |
-
torch.ones(L_q, L_k, device=Q.device, dtype=torch.bool), diagonal=1
|
| 188 |
-
)
|
| 189 |
-
attn_bias = attn_bias.masked_fill(causal.unsqueeze(0).unsqueeze(0), float("-inf"))
|
| 190 |
-
if key_padding_mask is not None:
|
| 191 |
-
# key_padding_mask: [B, L_k] bool, True = pad
|
| 192 |
-
attn_bias = attn_bias.masked_fill(
|
| 193 |
-
key_padding_mask.unsqueeze(1).unsqueeze(2), float("-inf")
|
| 194 |
-
)
|
| 195 |
-
attn_output = F.scaled_dot_product_attention(
|
| 196 |
-
Q, K, V,
|
| 197 |
-
attn_mask=attn_bias,
|
| 198 |
-
dropout_p=self.dropout_p if self.training else 0.0,
|
| 199 |
-
is_causal=False,
|
| 200 |
-
scale=1.0 / math.sqrt(self.d_k),
|
| 201 |
-
)
|
| 202 |
-
else:
|
| 203 |
-
attn_output = F.scaled_dot_product_attention(
|
| 204 |
-
Q, K, V,
|
| 205 |
-
attn_mask=None,
|
| 206 |
-
dropout_p=self.dropout_p if self.training else 0.0,
|
| 207 |
-
is_causal=False,
|
| 208 |
-
scale=1.0 / math.sqrt(self.d_k),
|
| 209 |
-
)
|
| 210 |
-
|
| 211 |
-
attn_output = attn_output.transpose(1, 2).contiguous().view(B, L_q, self.d_model)
|
| 212 |
-
output = self.out_proj(attn_output)
|
| 213 |
-
return output
|
|
|
|
| 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__(
|
|
|
|
| 57 |
self.d_model = d_model
|
| 58 |
self.nhead = nhead
|
| 59 |
self.d_k = d_model // nhead
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
+
raise NotImplementedError("TODO: Person B 实现 MultiHeadAttention.__init__")
|
|
|
|
| 62 |
|
| 63 |
def forward(
|
| 64 |
self,
|
|
|
|
| 67 |
value: torch.Tensor, # [B, L_v, D]
|
| 68 |
key_padding_mask: Optional[torch.BoolTensor] = None, # [B, L_k]
|
| 69 |
attn_mask: Optional[torch.Tensor] = None, # [L_q, L_k]
|
|
|
|
| 70 |
) -> torch.Tensor:
|
| 71 |
+
raise NotImplementedError("TODO: Person B 实现 MultiHeadAttention.forward")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
|
| 74 |
class FlashMultiHeadAttention(nn.Module):
|
| 75 |
"""
|
| 76 |
Flash Attention 2 加速的多头注意力。
|
| 77 |
+
|
| 78 |
+
TODO [Person B]: 使用 PyTorch 2.0+ 的 F.scaled_dot_product_attention 实现:
|
| 79 |
+
1. 与 MultiHeadAttention 结构相同
|
| 80 |
+
2. 在 forward 中使用 F.scaled_dot_product_attention(Q, K, V, attn_mask, dropout, is_causal)
|
| 81 |
+
3. 会自动选择最优的 attention kernel (Flash Attention / Memory-Efficient Attention)
|
| 82 |
+
|
| 83 |
+
注意:
|
| 84 |
+
- 需要 PyTorch >= 2.0
|
| 85 |
+
- is_causal=True 时自动生成因果掩码,不需要手动传入 attn_mask
|
| 86 |
"""
|
| 87 |
|
| 88 |
def __init__(
|
|
|
|
| 93 |
use_rotary_embedding: bool = False,
|
| 94 |
):
|
| 95 |
super().__init__()
|
| 96 |
+
raise NotImplementedError("TODO: Person B 实现 FlashMultiHeadAttention.__init__")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
def forward(
|
| 99 |
self,
|
|
|
|
| 103 |
key_padding_mask: Optional[torch.BoolTensor] = None,
|
| 104 |
is_causal: bool = False,
|
| 105 |
) -> torch.Tensor:
|
| 106 |
+
raise NotImplementedError("TODO: Person B 实现 FlashMultiHeadAttention.forward")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/easytranslate/model/decoder.py
CHANGED
|
@@ -13,23 +13,21 @@ Transformer Decoder 模块 — Person B 负责实现
|
|
| 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 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
| 33 |
"""
|
| 34 |
|
| 35 |
def __init__(
|
|
@@ -44,42 +42,7 @@ class TransformerDecoderLayer(nn.Module):
|
|
| 44 |
pre_norm: bool = True,
|
| 45 |
):
|
| 46 |
super().__init__()
|
| 47 |
-
|
| 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,
|
|
@@ -89,71 +52,28 @@ class TransformerDecoderLayer(nn.Module):
|
|
| 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 |
-
"""
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 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 |
-
|
| 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,
|
|
@@ -163,14 +83,4 @@ class TransformerDecoder(nn.Module):
|
|
| 163 |
memory_key_padding_mask: Optional[torch.BoolTensor] = None,
|
| 164 |
tgt_key_padding_mask: Optional[torch.BoolTensor] = None,
|
| 165 |
) -> torch.Tensor:
|
| 166 |
-
|
| 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
|
|
|
|
| 13 |
|
| 14 |
from __future__ import annotations
|
| 15 |
|
|
|
|
|
|
|
| 16 |
import torch
|
| 17 |
import torch.nn as nn
|
| 18 |
from typing import Optional
|
| 19 |
|
|
|
|
|
|
|
| 20 |
|
| 21 |
class TransformerDecoderLayer(nn.Module):
|
| 22 |
"""
|
| 23 |
单层 Transformer Decoder。
|
| 24 |
|
| 25 |
+
TODO [Person B]: 实现以下组件:
|
| 26 |
+
1. Masked Self-Attention (因果掩码,防止看到未来)
|
| 27 |
+
2. Cross-Attention (decoder 查询 encoder 输出)
|
| 28 |
+
3. Feed-Forward Network
|
| 29 |
+
4. 三个 LayerNorm
|
| 30 |
+
5. Residual connections + Dropout
|
| 31 |
"""
|
| 32 |
|
| 33 |
def __init__(
|
|
|
|
| 42 |
pre_norm: bool = True,
|
| 43 |
):
|
| 44 |
super().__init__()
|
| 45 |
+
raise NotImplementedError("TODO: Person B 实现 TransformerDecoderLayer.__init__")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
def forward(
|
| 48 |
self,
|
|
|
|
| 52 |
memory_key_padding_mask: Optional[torch.BoolTensor] = None, # [B, S]
|
| 53 |
tgt_key_padding_mask: Optional[torch.BoolTensor] = None, # [B, T]
|
| 54 |
) -> torch.Tensor:
|
| 55 |
+
"""
|
| 56 |
+
TODO [Person B]: Pre-LayerNorm 前向传播:
|
| 57 |
+
1. Masked Self-Attention with causal mask
|
| 58 |
+
2. Cross-Attention with encoder output
|
| 59 |
+
3. FFN
|
| 60 |
+
每步都有 residual connection 和 dropout
|
| 61 |
+
"""
|
| 62 |
+
raise NotImplementedError("TODO: Person B 实现 TransformerDecoderLayer.forward")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
|
| 65 |
class TransformerDecoder(nn.Module):
|
| 66 |
"""
|
| 67 |
多层 Transformer Decoder。
|
| 68 |
+
|
| 69 |
+
TODO [Person B]:
|
| 70 |
+
1. 堆叠 N 个 TransformerDecoderLayer
|
| 71 |
+
2. 最终加一个 LayerNorm
|
| 72 |
"""
|
| 73 |
|
| 74 |
def __init__(self, decoder_layer: TransformerDecoderLayer, num_layers: int):
|
| 75 |
super().__init__()
|
| 76 |
+
raise NotImplementedError("TODO: Person B 实现 TransformerDecoder.__init__")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
def forward(
|
| 79 |
self,
|
|
|
|
| 83 |
memory_key_padding_mask: Optional[torch.BoolTensor] = None,
|
| 84 |
tgt_key_padding_mask: Optional[torch.BoolTensor] = None,
|
| 85 |
) -> torch.Tensor:
|
| 86 |
+
raise NotImplementedError("TODO: Person B 实现 TransformerDecoder.forward")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/easytranslate/model/encoder.py
CHANGED
|
@@ -11,21 +11,23 @@ Transformer Encoder 模块 — Person B 负责实现
|
|
| 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 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
"""
|
| 30 |
|
| 31 |
def __init__(
|
|
@@ -40,88 +42,43 @@ class TransformerEncoderLayer(nn.Module):
|
|
| 40 |
pre_norm: bool = True,
|
| 41 |
):
|
| 42 |
super().__init__()
|
| 43 |
-
|
| 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 |
-
"""
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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
|
|
|
|
| 11 |
|
| 12 |
from __future__ import annotations
|
| 13 |
|
|
|
|
|
|
|
| 14 |
import torch
|
| 15 |
import torch.nn as nn
|
| 16 |
from typing import Optional
|
| 17 |
|
|
|
|
|
|
|
| 18 |
|
| 19 |
class TransformerEncoderLayer(nn.Module):
|
| 20 |
"""
|
| 21 |
单层 Transformer Encoder。
|
| 22 |
|
| 23 |
+
TODO [Person B]: 实现以下组件:
|
| 24 |
+
1. Self-Attention: MultiHeadAttention (支持 Flash Attention)
|
| 25 |
+
2. Feed-Forward Network: Linear → Activation → Dropout → Linear
|
| 26 |
+
3. 两个 LayerNorm
|
| 27 |
+
4. Residual connections
|
| 28 |
+
5. Dropout
|
| 29 |
+
|
| 30 |
+
注意: 使用 Pre-LayerNorm 架构 (先 norm 再 attention/ffn)
|
| 31 |
"""
|
| 32 |
|
| 33 |
def __init__(
|
|
|
|
| 42 |
pre_norm: bool = True,
|
| 43 |
):
|
| 44 |
super().__init__()
|
| 45 |
+
raise NotImplementedError("TODO: Person B 实现 TransformerEncoderLayer.__init__")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
def forward(
|
| 48 |
self,
|
| 49 |
src: torch.Tensor, # [B, S, D]
|
| 50 |
src_key_padding_mask: Optional[torch.BoolTensor] = None, # [B, S]
|
| 51 |
) -> torch.Tensor:
|
| 52 |
+
"""
|
| 53 |
+
TODO [Person B]: Pre-LayerNorm 前向传播:
|
| 54 |
+
1. residual = src
|
| 55 |
+
2. src = layer_norm_1(src)
|
| 56 |
+
3. src = self_attention(src, src, src, key_padding_mask=src_key_padding_mask)
|
| 57 |
+
4. src = residual + dropout(src)
|
| 58 |
+
5. residual = src
|
| 59 |
+
6. src = layer_norm_2(src)
|
| 60 |
+
7. src = ffn(src)
|
| 61 |
+
8. src = residual + dropout(src)
|
| 62 |
+
"""
|
| 63 |
+
raise NotImplementedError("TODO: Person B 实现 TransformerEncoderLayer.forward")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
|
| 66 |
class TransformerEncoder(nn.Module):
|
| 67 |
"""
|
| 68 |
多层 Transformer Encoder。
|
| 69 |
+
|
| 70 |
+
TODO [Person B]:
|
| 71 |
+
1. 堆叠 N 个 TransformerEncoderLayer
|
| 72 |
+
2. 最终加一个 LayerNorm (Pre-Norm 架构需要)
|
| 73 |
"""
|
| 74 |
|
| 75 |
def __init__(self, encoder_layer: TransformerEncoderLayer, num_layers: int):
|
| 76 |
super().__init__()
|
| 77 |
+
raise NotImplementedError("TODO: Person B 实现 TransformerEncoder.__init__")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
def forward(
|
| 80 |
self,
|
| 81 |
src: torch.Tensor,
|
| 82 |
src_key_padding_mask: Optional[torch.BoolTensor] = None,
|
| 83 |
) -> torch.Tensor:
|
| 84 |
+
raise NotImplementedError("TODO: Person B 实现 TransformerEncoder.forward")
|
|
|
|
|
|
|
|
|
|
|
|
src/easytranslate/model/finetune.py
CHANGED
|
@@ -50,23 +50,7 @@ def load_pretrained_model(
|
|
| 50 |
Returns:
|
| 51 |
(model, tokenizer)
|
| 52 |
"""
|
| 53 |
-
|
| 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(
|
|
@@ -79,38 +63,33 @@ def setup_lora(
|
|
| 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 |
-
|
| 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 |
-
"""
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
Returns:
|
| 51 |
(model, tokenizer)
|
| 52 |
"""
|
| 53 |
+
raise NotImplementedError("TODO: Person B 实现 load_pretrained_model")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
|
| 56 |
def setup_lora(
|
|
|
|
| 63 |
"""
|
| 64 |
为模型配置 LoRA 微调。
|
| 65 |
|
| 66 |
+
TODO [Person B]: 实现以下逻辑:
|
| 67 |
+
1. 定义 LoraConfig:
|
| 68 |
+
- r: LoRA 秩 (低秩分解维度)
|
| 69 |
+
- lora_alpha: 缩放因子
|
| 70 |
+
- lora_dropout: LoRA dropout
|
| 71 |
+
- target_modules: 需要加 LoRA 的模块 (如 q_proj, v_proj)
|
| 72 |
+
- task_type: SEQ_2_SEQ_LM
|
| 73 |
+
2. 使用 get_peft_model(model, config) 包装模型
|
| 74 |
+
3. 打印可训练参数数量和比例
|
| 75 |
+
4. 返回 LoRA 模型
|
| 76 |
+
|
| 77 |
+
参考: https://huggingface.co/docs/peft
|
| 78 |
+
|
| 79 |
Returns:
|
| 80 |
peft_model: LoRA 包装后的模型
|
| 81 |
"""
|
|
|
|
|
|
|
| 82 |
if target_modules is None:
|
| 83 |
target_modules = ["q_proj", "v_proj"]
|
| 84 |
|
| 85 |
+
raise NotImplementedError("TODO: Person B 实现 setup_lora")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
|
| 88 |
def freeze_model_except_lora(model: nn.Module):
|
| 89 |
+
"""
|
| 90 |
+
冻结模型所有参数,只保留 LoRA 参数可训练。
|
| 91 |
+
|
| 92 |
+
TODO [Person B]: 遍历 model.named_parameters(),
|
| 93 |
+
如果参数名中不包含 "lora",则设置 requires_grad = False。
|
| 94 |
+
"""
|
| 95 |
+
raise NotImplementedError("TODO: Person B 实现 freeze_model_except_lora")
|
src/easytranslate/model/positional.py
CHANGED
|
@@ -32,17 +32,7 @@ class SinusoidalPositionalEncoding(nn.Module):
|
|
| 32 |
|
| 33 |
def __init__(self, d_model: int, max_seq_len: int = 5000, dropout: float = 0.1):
|
| 34 |
super().__init__()
|
| 35 |
-
|
| 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 |
"""
|
|
@@ -51,8 +41,7 @@ class SinusoidalPositionalEncoding(nn.Module):
|
|
| 51 |
Returns:
|
| 52 |
x + positional_encoding: [B, L, D]
|
| 53 |
"""
|
| 54 |
-
|
| 55 |
-
return self.dropout(x)
|
| 56 |
|
| 57 |
|
| 58 |
class RotaryPositionalEmbedding(nn.Module):
|
|
@@ -83,35 +72,10 @@ class RotaryPositionalEmbedding(nn.Module):
|
|
| 83 |
|
| 84 |
def __init__(self, dim: int, max_seq_len: int = 2048, base: float = 10000.0):
|
| 85 |
super().__init__()
|
| 86 |
-
|
| 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 |
-
|
| 102 |
-
cache_stale = (
|
| 103 |
-
seq_len > self._cached_seq_len
|
| 104 |
-
or self._cached_cos is None
|
| 105 |
-
or self._cached_cos.device != device
|
| 106 |
-
)
|
| 107 |
-
if cache_stale:
|
| 108 |
-
t = torch.arange(seq_len, device=device, dtype=self.inv_freq.dtype)
|
| 109 |
-
freqs = torch.outer(t, self.inv_freq.to(device)) # [seq_len, dim//2]
|
| 110 |
-
emb = torch.cat((freqs, freqs), dim=-1) # [seq_len, dim]
|
| 111 |
-
self._cached_cos = emb.cos()[None, None, :, :] # [1, 1, seq_len, dim]
|
| 112 |
-
self._cached_sin = emb.sin()[None, None, :, :] # [1, 1, seq_len, dim]
|
| 113 |
-
self._cached_seq_len = seq_len
|
| 114 |
-
return self._cached_cos, self._cached_sin
|
| 115 |
|
| 116 |
@staticmethod
|
| 117 |
def _rotate_half(x: torch.Tensor) -> torch.Tensor:
|
|
@@ -130,11 +94,4 @@ class RotaryPositionalEmbedding(nn.Module):
|
|
| 130 |
Returns:
|
| 131 |
(q_rotated, k_rotated)
|
| 132 |
"""
|
| 133 |
-
|
| 134 |
-
q_embed = (q * cos[:, :, : q.size(2), :]) + (
|
| 135 |
-
self._rotate_half(q) * sin[:, :, : q.size(2), :]
|
| 136 |
-
)
|
| 137 |
-
k_embed = (k * cos[:, :, : k.size(2), :]) + (
|
| 138 |
-
self._rotate_half(k) * sin[:, :, : k.size(2), :]
|
| 139 |
-
)
|
| 140 |
-
return q_embed, k_embed
|
|
|
|
| 32 |
|
| 33 |
def __init__(self, d_model: int, max_seq_len: int = 5000, dropout: float = 0.1):
|
| 34 |
super().__init__()
|
| 35 |
+
raise NotImplementedError("TODO: Person B 实现 SinusoidalPositionalEncoding.__init__")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 38 |
"""
|
|
|
|
| 41 |
Returns:
|
| 42 |
x + positional_encoding: [B, L, D]
|
| 43 |
"""
|
| 44 |
+
raise NotImplementedError("TODO: Person B 实现 SinusoidalPositionalEncoding.forward")
|
|
|
|
| 45 |
|
| 46 |
|
| 47 |
class RotaryPositionalEmbedding(nn.Module):
|
|
|
|
| 72 |
|
| 73 |
def __init__(self, dim: int, max_seq_len: int = 2048, base: float = 10000.0):
|
| 74 |
super().__init__()
|
| 75 |
+
raise NotImplementedError("TODO: Person B 实现 RotaryPositionalEmbedding.__init__")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
def _compute_rope(self, seq_len: int, device: torch.device):
|
| 78 |
+
raise NotImplementedError("TODO: Person B 实现 _compute_rope")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
@staticmethod
|
| 81 |
def _rotate_half(x: torch.Tensor) -> torch.Tensor:
|
|
|
|
| 94 |
Returns:
|
| 95 |
(q_rotated, k_rotated)
|
| 96 |
"""
|
| 97 |
+
raise NotImplementedError("TODO: Person B 实现 apply_rotary_pos_emb")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/easytranslate/model/transformer.py
CHANGED
|
@@ -20,8 +20,8 @@ import torch
|
|
| 20 |
import torch.nn as nn
|
| 21 |
import torch.nn.functional as F
|
| 22 |
|
| 23 |
-
from easytranslate.model.encoder import TransformerEncoder
|
| 24 |
-
from easytranslate.model.decoder import TransformerDecoder
|
| 25 |
from easytranslate.model.positional import SinusoidalPositionalEncoding, RotaryPositionalEmbedding
|
| 26 |
|
| 27 |
logger = logging.getLogger(__name__)
|
|
@@ -78,94 +78,33 @@ class TransformerTranslationModel(nn.Module):
|
|
| 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 |
-
|
| 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 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
nn.init.zeros_(module.bias)
|
| 163 |
|
| 164 |
def _generate_square_subsequent_mask(self, sz: int, device: torch.device) -> torch.Tensor:
|
| 165 |
-
"""
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
def forward(
|
| 171 |
self,
|
|
@@ -180,50 +119,19 @@ class TransformerTranslationModel(nn.Module):
|
|
| 180 |
Returns:
|
| 181 |
logits: [B, T, tgt_vocab_size]
|
| 182 |
"""
|
| 183 |
-
|
| 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 |
-
"""
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
|
|
|
| 227 |
|
| 228 |
@torch.no_grad()
|
| 229 |
def decode_step(
|
|
@@ -232,24 +140,17 @@ class TransformerTranslationModel(nn.Module):
|
|
| 232 |
encoder_output: torch.Tensor,
|
| 233 |
src_padding_mask: Optional[torch.BoolTensor] = None,
|
| 234 |
) -> torch.Tensor:
|
| 235 |
-
"""
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 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 |
"""返回可训练参数数量。"""
|
|
|
|
| 20 |
import torch.nn as nn
|
| 21 |
import torch.nn.functional as F
|
| 22 |
|
| 23 |
+
from easytranslate.model.encoder import TransformerEncoder
|
| 24 |
+
from easytranslate.model.decoder import TransformerDecoder
|
| 25 |
from easytranslate.model.positional import SinusoidalPositionalEncoding, RotaryPositionalEmbedding
|
| 26 |
|
| 27 |
logger = logging.getLogger(__name__)
|
|
|
|
| 78 |
share_embedding: bool = False,
|
| 79 |
):
|
| 80 |
super().__init__()
|
| 81 |
+
# TODO [Person B]: 实现模型初始化
|
| 82 |
+
# 保存超参数
|
| 83 |
self.d_model = d_model
|
| 84 |
self.pad_id = pad_id
|
|
|
|
|
|
|
| 85 |
|
| 86 |
+
raise NotImplementedError("TODO: Person B 实现 TransformerTranslationModel.__init__")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
def _init_weights(self):
|
| 89 |
+
"""
|
| 90 |
+
参数初始化。
|
| 91 |
+
|
| 92 |
+
TODO [Person B]: 实现 Xavier/Kaiming 初始化:
|
| 93 |
+
- Embedding: normal_(0, d_model^-0.5)
|
| 94 |
+
- Linear: xavier_uniform_
|
| 95 |
+
- LayerNorm: ones_ / zeros_
|
| 96 |
+
"""
|
| 97 |
+
raise NotImplementedError("TODO: Person B 实现 _init_weights")
|
|
|
|
| 98 |
|
| 99 |
def _generate_square_subsequent_mask(self, sz: int, device: torch.device) -> torch.Tensor:
|
| 100 |
+
"""
|
| 101 |
+
生成因果注意力掩码 (causal mask)。
|
| 102 |
+
|
| 103 |
+
TODO [Person B]:
|
| 104 |
+
返回上三角矩阵 mask,shape [sz, sz],
|
| 105 |
+
mask[i][j] = -inf if j > i else 0
|
| 106 |
+
"""
|
| 107 |
+
raise NotImplementedError("TODO: Person B 实现 _generate_square_subsequent_mask")
|
| 108 |
|
| 109 |
def forward(
|
| 110 |
self,
|
|
|
|
| 119 |
Returns:
|
| 120 |
logits: [B, T, tgt_vocab_size]
|
| 121 |
"""
|
| 122 |
+
raise NotImplementedError("TODO: Person B 实现 forward")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
@torch.no_grad()
|
| 125 |
def encode(self, src_ids: torch.Tensor, src_padding_mask: Optional[torch.BoolTensor] = None) -> torch.Tensor:
|
| 126 |
+
"""
|
| 127 |
+
仅编码(用于推理时复用 encoder 输出)。
|
| 128 |
+
|
| 129 |
+
TODO [Person B]:
|
| 130 |
+
1. src embedding + positional encoding
|
| 131 |
+
2. encoder forward
|
| 132 |
+
3. 返回 encoder_output
|
| 133 |
+
"""
|
| 134 |
+
raise NotImplementedError("TODO: Person B 实现 encode")
|
| 135 |
|
| 136 |
@torch.no_grad()
|
| 137 |
def decode_step(
|
|
|
|
| 140 |
encoder_output: torch.Tensor,
|
| 141 |
src_padding_mask: Optional[torch.BoolTensor] = None,
|
| 142 |
) -> torch.Tensor:
|
| 143 |
+
"""
|
| 144 |
+
解码一步(用于自回归推理)。
|
| 145 |
+
|
| 146 |
+
TODO [Person B]:
|
| 147 |
+
1. tgt embedding + positional encoding
|
| 148 |
+
2. 生成 causal mask
|
| 149 |
+
3. decoder forward
|
| 150 |
+
4. 取最后一个 token 的 logits
|
| 151 |
+
5. 返回 logits [B, vocab_size]
|
| 152 |
+
"""
|
| 153 |
+
raise NotImplementedError("TODO: Person B 实现 decode_step")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
def count_parameters(self) -> int:
|
| 156 |
"""返回可训练参数数量。"""
|
src/easytranslate/training/loss.py
CHANGED
|
@@ -21,11 +21,21 @@ class LabelSmoothedCrossEntropyLoss(nn.Module):
|
|
| 21 |
"""
|
| 22 |
带标签平滑的交叉熵损失。
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
参考: "Rethinking the Inception Architecture for Computer Vision" (Szegedy et al.)
|
| 31 |
"""
|
|
@@ -34,43 +44,14 @@ class LabelSmoothedCrossEntropyLoss(nn.Module):
|
|
| 34 |
super().__init__()
|
| 35 |
self.smoothing = smoothing
|
| 36 |
self.pad_id = pad_id
|
| 37 |
-
|
| 38 |
|
| 39 |
def forward(self, logits: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
| 40 |
"""
|
| 41 |
Args:
|
| 42 |
-
logits: [B, T, V]
|
| 43 |
-
targets: [B, T]
|
| 44 |
Returns:
|
| 45 |
loss: scalar
|
| 46 |
"""
|
| 47 |
-
|
| 48 |
-
targets = targets.contiguous()
|
| 49 |
-
|
| 50 |
-
batch_size, seq_len, vocab_size = logits.size()
|
| 51 |
-
|
| 52 |
-
logits_flat = logits.view(-1, vocab_size)
|
| 53 |
-
targets_flat = targets.view(-1)
|
| 54 |
-
|
| 55 |
-
log_probs = F.log_softmax(logits_flat, dim=-1)
|
| 56 |
-
|
| 57 |
-
# Mask that is True for real (non-padding) tokens
|
| 58 |
-
non_pad_mask = targets_flat.ne(self.pad_id).float()
|
| 59 |
-
|
| 60 |
-
if self.smoothing > 0.0:
|
| 61 |
-
smooth_dist = torch.full_like(log_probs, self.smoothing / (vocab_size - 1))
|
| 62 |
-
# Clamp to avoid negative-index writes for ignore-index values (e.g. -100)
|
| 63 |
-
safe_targets = targets_flat.clamp(min=0)
|
| 64 |
-
smooth_dist.scatter_(1, safe_targets.unsqueeze(1), self.confidence)
|
| 65 |
-
nll_loss = -torch.sum(smooth_dist * log_probs, dim=-1)
|
| 66 |
-
nll_loss = nll_loss * non_pad_mask
|
| 67 |
-
else:
|
| 68 |
-
nll_loss = F.nll_loss(
|
| 69 |
-
log_probs, targets_flat,
|
| 70 |
-
ignore_index=self.pad_id, reduction="none",
|
| 71 |
-
)
|
| 72 |
-
|
| 73 |
-
num_tokens = non_pad_mask.sum().clamp(min=1)
|
| 74 |
-
loss = nll_loss.sum() / num_tokens
|
| 75 |
-
|
| 76 |
-
return loss
|
|
|
|
| 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 |
"""
|
|
|
|
| 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
|
@@ -22,56 +22,18 @@ from torch.optim import Adam, AdamW
|
|
| 22 |
from torch.optim.lr_scheduler import LambdaLR
|
| 23 |
|
| 24 |
|
| 25 |
-
def build_optimizer(model
|
| 26 |
"""
|
| 27 |
根据配置创建优化器。
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
| 32 |
"""
|
| 33 |
-
|
| 34 |
-
opt_type = opt_config.get("type", "adamw")
|
| 35 |
-
lr = float(opt_config.get("lr", 3e-4))
|
| 36 |
-
weight_decay = float(opt_config.get("weight_decay", 0.01))
|
| 37 |
-
betas = tuple(opt_config.get("betas", [0.9, 0.98]))
|
| 38 |
-
eps = float(opt_config.get("eps", 1e-8))
|
| 39 |
-
|
| 40 |
-
no_decay = ["bias", "LayerNorm.weight", "layer_norm.weight"]
|
| 41 |
-
optimizer_grouped_parameters = [
|
| 42 |
-
{
|
| 43 |
-
"params": [
|
| 44 |
-
p for n, p in model.named_parameters()
|
| 45 |
-
if p.requires_grad and not any(nd in n for nd in no_decay)
|
| 46 |
-
],
|
| 47 |
-
"weight_decay": weight_decay,
|
| 48 |
-
},
|
| 49 |
-
{
|
| 50 |
-
"params": [
|
| 51 |
-
p for n, p in model.named_parameters()
|
| 52 |
-
if p.requires_grad and any(nd in n for nd in no_decay)
|
| 53 |
-
],
|
| 54 |
-
"weight_decay": 0.0,
|
| 55 |
-
},
|
| 56 |
-
]
|
| 57 |
-
|
| 58 |
-
if opt_type == "adam":
|
| 59 |
-
return Adam(optimizer_grouped_parameters, lr=lr, betas=betas, eps=eps)
|
| 60 |
-
elif opt_type == "adamw":
|
| 61 |
-
return AdamW(optimizer_grouped_parameters, lr=lr, betas=betas, eps=eps)
|
| 62 |
-
elif opt_type == "adafactor":
|
| 63 |
-
try:
|
| 64 |
-
from transformers.optimization import Adafactor
|
| 65 |
-
return Adafactor(
|
| 66 |
-
optimizer_grouped_parameters,
|
| 67 |
-
lr=lr,
|
| 68 |
-
scale_parameter=False,
|
| 69 |
-
relative_step=False,
|
| 70 |
-
)
|
| 71 |
-
except ImportError:
|
| 72 |
-
raise ImportError("Adafactor requires transformers library")
|
| 73 |
-
else:
|
| 74 |
-
raise ValueError(f"Unsupported optimizer type: {opt_type}")
|
| 75 |
|
| 76 |
|
| 77 |
def build_scheduler(
|
|
@@ -82,68 +44,18 @@ def build_scheduler(
|
|
| 82 |
"""
|
| 83 |
根据配置创建学习率调度器。
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
"""
|
| 90 |
-
|
| 91 |
-
sched_type = sched_config.get("type", "cosine_with_warmup")
|
| 92 |
-
warmup_steps = int(sched_config.get("warmup_steps", 4000))
|
| 93 |
-
min_lr = float(sched_config.get("min_lr", 1e-6))
|
| 94 |
-
|
| 95 |
-
if sched_type == "cosine_with_warmup":
|
| 96 |
-
try:
|
| 97 |
-
from transformers import get_cosine_schedule_with_warmup
|
| 98 |
-
return get_cosine_schedule_with_warmup(
|
| 99 |
-
optimizer,
|
| 100 |
-
num_warmup_steps=warmup_steps,
|
| 101 |
-
num_training_steps=num_training_steps or 100000,
|
| 102 |
-
)
|
| 103 |
-
except ImportError:
|
| 104 |
-
return _cosine_with_warmup(optimizer, warmup_steps, num_training_steps or 100000, min_lr)
|
| 105 |
-
|
| 106 |
-
elif sched_type == "inverse_sqrt":
|
| 107 |
-
return InverseSqrtScheduler(optimizer, warmup_steps=warmup_steps)
|
| 108 |
-
|
| 109 |
-
elif sched_type == "linear":
|
| 110 |
-
try:
|
| 111 |
-
from transformers import get_linear_schedule_with_warmup
|
| 112 |
-
return get_linear_schedule_with_warmup(
|
| 113 |
-
optimizer,
|
| 114 |
-
num_warmup_steps=warmup_steps,
|
| 115 |
-
num_training_steps=num_training_steps or 100000,
|
| 116 |
-
)
|
| 117 |
-
except ImportError:
|
| 118 |
-
return _linear_with_warmup(optimizer, warmup_steps, num_training_steps or 100000, min_lr)
|
| 119 |
-
|
| 120 |
-
else:
|
| 121 |
-
raise ValueError(f"Unsupported scheduler type: {sched_type}")
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
def _cosine_with_warmup(optimizer, warmup_steps, total_steps, min_lr=0.0):
|
| 125 |
-
def lr_lambda(current_step):
|
| 126 |
-
if current_step < warmup_steps:
|
| 127 |
-
return float(current_step) / float(max(1, warmup_steps))
|
| 128 |
-
progress = float(current_step - warmup_steps) / float(max(1, total_steps - warmup_steps))
|
| 129 |
-
cosine_decay = 0.5 * (1.0 + math.cos(math.pi * progress))
|
| 130 |
-
return max(min_lr / _get_base_lr(optimizer), cosine_decay)
|
| 131 |
-
return LambdaLR(optimizer, lr_lambda)
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
def _linear_with_warmup(optimizer, warmup_steps, total_steps, min_lr=0.0):
|
| 135 |
-
def lr_lambda(current_step):
|
| 136 |
-
if current_step < warmup_steps:
|
| 137 |
-
return float(current_step) / float(max(1, warmup_steps))
|
| 138 |
-
progress = float(current_step - warmup_steps) / float(max(1, total_steps - warmup_steps))
|
| 139 |
-
return max(min_lr / _get_base_lr(optimizer), 1.0 - progress)
|
| 140 |
-
return LambdaLR(optimizer, lr_lambda)
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
def _get_base_lr(optimizer):
|
| 144 |
-
for param_group in optimizer.param_groups:
|
| 145 |
-
return param_group.get("lr", param_group.get("initial_lr", 1e-3))
|
| 146 |
-
return 1e-3
|
| 147 |
|
| 148 |
|
| 149 |
class InverseSqrtScheduler(LambdaLR):
|
|
@@ -152,18 +64,13 @@ class InverseSqrtScheduler(LambdaLR):
|
|
| 152 |
|
| 153 |
lr = base_lr * min(step^{-0.5}, step * warmup_steps^{-1.5})
|
| 154 |
|
| 155 |
-
这是原始 Transformer 论文
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
"""
|
| 158 |
|
| 159 |
def __init__(self, optimizer, warmup_steps: int = 4000):
|
| 160 |
-
|
| 161 |
-
warmup_factor = warmup_steps ** (-1.5)
|
| 162 |
-
|
| 163 |
-
def lr_lambda(step):
|
| 164 |
-
step += 1
|
| 165 |
-
arg1 = step ** (-0.5)
|
| 166 |
-
arg2 = step * warmup_factor
|
| 167 |
-
return min(arg1, arg2)
|
| 168 |
-
|
| 169 |
-
super().__init__(optimizer, lr_lambda)
|
|
|
|
| 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(
|
|
|
|
| 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):
|
|
|
|
| 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
|
@@ -14,10 +14,8 @@
|
|
| 14 |
|
| 15 |
from __future__ import annotations
|
| 16 |
|
| 17 |
-
import json
|
| 18 |
import logging
|
| 19 |
import os
|
| 20 |
-
import shutil
|
| 21 |
import time
|
| 22 |
from pathlib import Path
|
| 23 |
from typing import Optional
|
|
@@ -27,9 +25,6 @@ import torch.nn as nn
|
|
| 27 |
from torch.utils.data import DataLoader
|
| 28 |
from tqdm import tqdm
|
| 29 |
|
| 30 |
-
from easytranslate.training.loss import LabelSmoothedCrossEntropyLoss
|
| 31 |
-
from easytranslate.training.optimizer import build_optimizer, build_scheduler
|
| 32 |
-
|
| 33 |
logger = logging.getLogger(__name__)
|
| 34 |
|
| 35 |
|
|
@@ -37,6 +32,8 @@ class Trainer:
|
|
| 37 |
"""
|
| 38 |
翻译模型训练器。
|
| 39 |
|
|
|
|
|
|
|
| 40 |
使用方法:
|
| 41 |
trainer = Trainer(model, train_loader, val_loader, config)
|
| 42 |
trainer.train()
|
|
@@ -53,407 +50,137 @@ class Trainer:
|
|
| 53 |
criterion=None,
|
| 54 |
evaluator=None,
|
| 55 |
):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
self.model = model
|
| 57 |
self.train_loader = train_loader
|
| 58 |
self.val_loader = val_loader
|
| 59 |
self.config = config
|
| 60 |
-
|
| 61 |
-
train_cfg = config.get("training", {})
|
| 62 |
-
|
| 63 |
-
self.device = self._resolve_device(train_cfg)
|
| 64 |
-
self.model = self.model.to(self.device)
|
| 65 |
-
|
| 66 |
-
self.fp16 = train_cfg.get("fp16", False)
|
| 67 |
-
self.bf16 = train_cfg.get("bf16", False)
|
| 68 |
-
self.gradient_accumulation_steps = int(train_cfg.get("gradient_accumulation_steps", 1))
|
| 69 |
-
self.max_grad_norm = float(train_cfg.get("max_grad_norm", 1.0))
|
| 70 |
-
self.gradient_checkpointing = train_cfg.get("gradient_checkpointing", False)
|
| 71 |
-
|
| 72 |
-
if self.gradient_checkpointing and hasattr(self.model, "gradient_checkpointing_enable"):
|
| 73 |
-
self.model.gradient_checkpointing_enable()
|
| 74 |
-
|
| 75 |
-
self.num_epochs = int(train_cfg.get("epochs", 30))
|
| 76 |
-
self.max_steps = int(train_cfg.get("max_steps", -1))
|
| 77 |
-
|
| 78 |
-
self.scaler = None
|
| 79 |
-
self.amp_dtype = None
|
| 80 |
-
if self.fp16:
|
| 81 |
-
self.scaler = torch.amp.GradScaler(self.device.type)
|
| 82 |
-
self.amp_dtype = torch.float16
|
| 83 |
-
elif self.bf16:
|
| 84 |
-
self.amp_dtype = torch.bfloat16
|
| 85 |
-
|
| 86 |
-
if optimizer is None:
|
| 87 |
-
optimizer = build_optimizer(model, train_cfg)
|
| 88 |
-
self.optimizer = optimizer
|
| 89 |
-
|
| 90 |
-
total_steps = self.num_epochs * len(train_loader) // self.gradient_accumulation_steps
|
| 91 |
-
if scheduler is None:
|
| 92 |
-
scheduler = build_scheduler(optimizer, train_cfg, num_training_steps=total_steps)
|
| 93 |
-
self.scheduler = scheduler
|
| 94 |
-
|
| 95 |
-
reg_cfg = train_cfg.get("regularization", {})
|
| 96 |
-
if criterion is None:
|
| 97 |
-
criterion = LabelSmoothedCrossEntropyLoss(
|
| 98 |
-
smoothing=float(reg_cfg.get("label_smoothing", 0.1)),
|
| 99 |
-
pad_id=-100, # must match TranslationCollator's label_pad_token_id
|
| 100 |
-
)
|
| 101 |
-
self.criterion = criterion
|
| 102 |
-
|
| 103 |
-
self.evaluator = evaluator
|
| 104 |
-
|
| 105 |
-
ckpt_cfg = train_cfg.get("checkpoint", {})
|
| 106 |
-
self.checkpoint_dir = Path(ckpt_cfg.get("save_dir", "checkpoints/"))
|
| 107 |
-
self.checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
| 108 |
-
self.save_every_n_steps = int(ckpt_cfg.get("save_every_n_steps", 5000))
|
| 109 |
-
self.save_best = ckpt_cfg.get("save_best", True)
|
| 110 |
-
self.metric_for_best = ckpt_cfg.get("metric_for_best", "bleu")
|
| 111 |
-
self.max_checkpoints = int(ckpt_cfg.get("max_checkpoints", 5))
|
| 112 |
-
|
| 113 |
-
es_cfg = train_cfg.get("early_stopping", {})
|
| 114 |
-
self.early_stopping_enabled = es_cfg.get("enabled", True)
|
| 115 |
-
self.patience = int(es_cfg.get("patience", 5))
|
| 116 |
-
self.min_delta = float(es_cfg.get("min_delta", 0.1))
|
| 117 |
-
|
| 118 |
-
log_cfg = config.get("logging", {})
|
| 119 |
-
self.log_dir = Path(log_cfg.get("log_dir", "logs/"))
|
| 120 |
-
self.log_dir.mkdir(parents=True, exist_ok=True)
|
| 121 |
-
self.log_every_n_steps = int(log_cfg.get("log_every_n_steps", 100))
|
| 122 |
-
self.log_backend = log_cfg.get("backend", "tensorboard")
|
| 123 |
-
|
| 124 |
-
self._init_logger()
|
| 125 |
-
|
| 126 |
-
self.global_step = 0
|
| 127 |
-
self.current_epoch = 0
|
| 128 |
-
self.best_metric_value = float("-inf")
|
| 129 |
-
self.best_epoch = 0
|
| 130 |
-
self.epochs_without_improvement = 0
|
| 131 |
-
self.train_loss_history: list[float] = []
|
| 132 |
-
self.val_metrics_history: list[dict] = []
|
| 133 |
-
|
| 134 |
-
self._setup_distributed()
|
| 135 |
-
|
| 136 |
-
def _resolve_device(self, train_cfg: dict) -> torch.device:
|
| 137 |
-
device_str = train_cfg.get("device", "auto")
|
| 138 |
-
if device_str == "auto":
|
| 139 |
-
if torch.cuda.is_available():
|
| 140 |
-
return torch.device("cuda")
|
| 141 |
-
elif torch.backends.mps.is_available():
|
| 142 |
-
return torch.device("mps")
|
| 143 |
-
else:
|
| 144 |
-
return torch.device("cpu")
|
| 145 |
-
return torch.device(device_str)
|
| 146 |
-
|
| 147 |
-
def _init_logger(self):
|
| 148 |
-
self.writer = None
|
| 149 |
-
if self.log_backend in ("tensorboard", "both"):
|
| 150 |
-
try:
|
| 151 |
-
from torch.utils.tensorboard import SummaryWriter
|
| 152 |
-
self.writer = SummaryWriter(log_dir=str(self.log_dir))
|
| 153 |
-
except ImportError:
|
| 154 |
-
logger.warning("TensorBoard not available, skipping")
|
| 155 |
-
|
| 156 |
-
self.wandb_run = None
|
| 157 |
-
if self.log_backend in ("wandb", "both"):
|
| 158 |
-
try:
|
| 159 |
-
import wandb
|
| 160 |
-
log_cfg = self.config.get("logging", {})
|
| 161 |
-
self.wandb_run = wandb.init(
|
| 162 |
-
project=log_cfg.get("project_name", "EasyTranslate"),
|
| 163 |
-
config=self.config,
|
| 164 |
-
dir=str(self.log_dir),
|
| 165 |
-
)
|
| 166 |
-
except ImportError:
|
| 167 |
-
logger.warning("WandB not available, skipping")
|
| 168 |
-
|
| 169 |
-
def _setup_distributed(self):
|
| 170 |
-
dist_cfg = self.config.get("training", {}).get("distributed", {})
|
| 171 |
-
strategy = dist_cfg.get("strategy", "ddp")
|
| 172 |
-
|
| 173 |
-
if strategy == "ddp" and torch.distributed.is_available() and torch.distributed.is_initialized():
|
| 174 |
-
self.model = nn.parallel.DistributedDataParallel(self.model)
|
| 175 |
-
self.is_distributed = True
|
| 176 |
-
elif strategy == "deepspeed":
|
| 177 |
-
try:
|
| 178 |
-
import deepspeed
|
| 179 |
-
ds_config = dist_cfg.get("deepspeed_config", "configs/deepspeed_config.json")
|
| 180 |
-
self.model, self.optimizer, _, _ = deepspeed.initialize(
|
| 181 |
-
model=self.model,
|
| 182 |
-
optimizer=self.optimizer,
|
| 183 |
-
config_params=ds_config,
|
| 184 |
-
)
|
| 185 |
-
self.is_distributed = True
|
| 186 |
-
except ImportError:
|
| 187 |
-
logger.warning("DeepSpeed not available, falling back to single GPU")
|
| 188 |
-
self.is_distributed = False
|
| 189 |
-
else:
|
| 190 |
-
self.is_distributed = False
|
| 191 |
|
| 192 |
def train(self):
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
for epoch in range(
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
self.val_metrics_history.append(val_metrics)
|
| 207 |
-
|
| 208 |
-
self._log_metrics(epoch, train_loss, val_metrics)
|
| 209 |
-
self._save_checkpoint(epoch, val_metrics)
|
| 210 |
-
|
| 211 |
-
if self._should_early_stop(val_metrics):
|
| 212 |
-
logger.info("Early stopping triggered at epoch %d", epoch + 1)
|
| 213 |
-
break
|
| 214 |
-
|
| 215 |
-
if self.max_steps > 0 and self.global_step >= self.max_steps:
|
| 216 |
-
logger.info("Reached max steps %d, stopping", self.max_steps)
|
| 217 |
-
break
|
| 218 |
-
|
| 219 |
-
self._log_final_results()
|
| 220 |
-
self._cleanup()
|
| 221 |
|
| 222 |
def _train_one_epoch(self, epoch: int) -> float:
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
|
| 232 |
-
with torch.amp.autocast(self.device.type, enabled=self.amp_dtype is not None, dtype=self.amp_dtype):
|
| 233 |
-
logits = self.model(
|
| 234 |
-
batch["src_ids"],
|
| 235 |
-
batch["tgt_input_ids"],
|
| 236 |
-
batch.get("src_padding_mask"),
|
| 237 |
-
batch.get("tgt_padding_mask"),
|
| 238 |
-
)
|
| 239 |
-
loss = self.criterion(logits, batch["labels"])
|
| 240 |
-
|
| 241 |
-
loss = loss / self.gradient_accumulation_steps
|
| 242 |
-
|
| 243 |
-
if self.scaler is not None:
|
| 244 |
-
self.scaler.scale(loss).backward()
|
| 245 |
-
else:
|
| 246 |
-
loss.backward()
|
| 247 |
-
|
| 248 |
-
total_loss += loss.item() * self.gradient_accumulation_steps
|
| 249 |
-
num_batches += 1
|
| 250 |
-
|
| 251 |
-
if (batch_idx + 1) % self.gradient_accumulation_steps == 0:
|
| 252 |
-
if self.scaler is not None:
|
| 253 |
-
self.scaler.unscale_(self.optimizer)
|
| 254 |
-
nn.utils.clip_grad_norm_(self.model.parameters(), self.max_grad_norm)
|
| 255 |
-
self.scaler.step(self.optimizer)
|
| 256 |
-
self.scaler.update()
|
| 257 |
-
else:
|
| 258 |
-
nn.utils.clip_grad_norm_(self.model.parameters(), self.max_grad_norm)
|
| 259 |
-
self.optimizer.step()
|
| 260 |
-
|
| 261 |
-
self.scheduler.step()
|
| 262 |
-
self.optimizer.zero_grad()
|
| 263 |
-
self.global_step += 1
|
| 264 |
-
|
| 265 |
-
current_lr = self.scheduler.get_last_lr()[0]
|
| 266 |
-
pbar.set_postfix({
|
| 267 |
-
"loss": f"{loss.item() * self.gradient_accumulation_steps:.4f}",
|
| 268 |
-
"lr": f"{current_lr:.2e}",
|
| 269 |
-
"step": self.global_step,
|
| 270 |
-
})
|
| 271 |
-
|
| 272 |
-
if self.global_step % self.log_every_n_steps == 0:
|
| 273 |
-
self._log_step_metrics(loss.item() * self.gradient_accumulation_steps, current_lr)
|
| 274 |
-
|
| 275 |
-
if self.save_every_n_steps > 0 and self.global_step % self.save_every_n_steps == 0:
|
| 276 |
-
self._save_checkpoint(epoch, {"step": self.global_step}, prefix=f"step_{self.global_step}")
|
| 277 |
-
|
| 278 |
-
avg_loss = total_loss / max(num_batches, 1)
|
| 279 |
-
logger.info("Epoch %d - Train Loss: %.4f", epoch + 1, avg_loss)
|
| 280 |
-
return avg_loss
|
| 281 |
-
|
| 282 |
-
@torch.no_grad()
|
| 283 |
def _validate(self, epoch: int) -> dict:
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
logger.info(" %s: %.4f", k, v)
|
| 317 |
-
|
| 318 |
-
return metrics
|
| 319 |
-
|
| 320 |
-
def _save_checkpoint(self, epoch: int, metrics: dict, prefix: str = ""):
|
| 321 |
-
model_to_save = self.model.module if hasattr(self.model, "module") else self.model
|
| 322 |
-
|
| 323 |
-
checkpoint = {
|
| 324 |
-
"epoch": epoch,
|
| 325 |
-
"step": self.global_step,
|
| 326 |
-
"model_state_dict": model_to_save.state_dict(),
|
| 327 |
-
"optimizer_state_dict": self.optimizer.state_dict(),
|
| 328 |
-
"scheduler_state_dict": self.scheduler.state_dict(),
|
| 329 |
-
"metrics": metrics,
|
| 330 |
-
"config": self.config,
|
| 331 |
-
"train_loss_history": self.train_loss_history,
|
| 332 |
-
"val_metrics_history": self.val_metrics_history,
|
| 333 |
-
}
|
| 334 |
-
|
| 335 |
-
if prefix:
|
| 336 |
-
ckpt_path = self.checkpoint_dir / f"checkpoint_{prefix}.pt"
|
| 337 |
-
else:
|
| 338 |
-
ckpt_path = self.checkpoint_dir / f"checkpoint_epoch_{epoch + 1}.pt"
|
| 339 |
-
|
| 340 |
-
torch.save(checkpoint, ckpt_path)
|
| 341 |
-
logger.info("Checkpoint saved: %s", ckpt_path)
|
| 342 |
-
|
| 343 |
-
current_metric = metrics.get(self.metric_for_best, metrics.get("val_loss", float("inf")))
|
| 344 |
-
if self.metric_for_best == "val_loss":
|
| 345 |
-
current_metric = -current_metric
|
| 346 |
-
|
| 347 |
-
if self.save_best and current_metric > self.best_metric_value:
|
| 348 |
-
self.best_metric_value = current_metric
|
| 349 |
-
self.best_epoch = epoch
|
| 350 |
-
best_path = self.checkpoint_dir / "best_model.pt"
|
| 351 |
-
torch.save(checkpoint, best_path)
|
| 352 |
-
logger.info("New best model saved: %s (metric: %.4f)", best_path, current_metric)
|
| 353 |
-
|
| 354 |
-
self._cleanup_old_checkpoints()
|
| 355 |
-
|
| 356 |
-
def _cleanup_old_checkpoints(self):
|
| 357 |
-
ckpt_files = sorted(
|
| 358 |
-
self.checkpoint_dir.glob("checkpoint_epoch_*.pt"),
|
| 359 |
-
key=os.path.getmtime,
|
| 360 |
-
)
|
| 361 |
-
while len(ckpt_files) > self.max_checkpoints:
|
| 362 |
-
oldest = ckpt_files.pop(0)
|
| 363 |
-
oldest.unlink()
|
| 364 |
-
logger.debug("Removed old checkpoint: %s", oldest)
|
| 365 |
|
| 366 |
def _load_checkpoint(self, checkpoint_path: str):
|
| 367 |
-
|
| 368 |
-
|
| 369 |
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
self.global_step = checkpoint["step"]
|
| 377 |
-
self.best_metric_value = checkpoint.get("metrics", {}).get(
|
| 378 |
-
self.metric_for_best, float("-inf")
|
| 379 |
-
)
|
| 380 |
-
self.train_loss_history = checkpoint.get("train_loss_history", [])
|
| 381 |
-
self.val_metrics_history = checkpoint.get("val_metrics_history", [])
|
| 382 |
-
|
| 383 |
-
logger.info("Resumed from epoch %d, step %d", self.current_epoch, self.global_step)
|
| 384 |
|
| 385 |
def _should_early_stop(self, metrics: dict) -> bool:
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
current_metric = metrics.get(self.metric_for_best, metrics.get("val_loss", float("inf")))
|
| 390 |
-
if self.metric_for_best == "val_loss":
|
| 391 |
-
current_metric = -current_metric
|
| 392 |
-
|
| 393 |
-
if current_metric > self.best_metric_value + self.min_delta:
|
| 394 |
-
self.epochs_without_improvement = 0
|
| 395 |
-
return False
|
| 396 |
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
)
|
| 402 |
-
return self.epochs_without_improvement >= self.patience
|
| 403 |
|
| 404 |
def _log_metrics(self, epoch: int, train_loss: float, val_metrics: dict):
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
for k, v in val_metrics.items():
|
| 408 |
-
if not isinstance(v, list):
|
| 409 |
-
self.writer.add_scalar(f"Metrics/{k}", v, epoch)
|
| 410 |
-
self.writer.add_scalar("LR", self.scheduler.get_last_lr()[0], epoch)
|
| 411 |
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
log_dict[f"val/{k}"] = v
|
| 418 |
-
log_dict["lr"] = self.scheduler.get_last_lr()[0]
|
| 419 |
-
wandb.log(log_dict, step=self.global_step)
|
| 420 |
|
| 421 |
-
def
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
self.writer.add_scalar("LR/step", lr, self.global_step)
|
| 425 |
-
|
| 426 |
-
if self.wandb_run is not None:
|
| 427 |
-
import wandb
|
| 428 |
-
wandb.log({"train/loss_step": loss, "lr": lr}, step=self.global_step)
|
| 429 |
-
|
| 430 |
-
def _log_final_results(self):
|
| 431 |
-
logger.info("=" * 50)
|
| 432 |
-
logger.info("Training completed!")
|
| 433 |
-
logger.info("Best epoch: %d", self.best_epoch + 1)
|
| 434 |
-
logger.info("Best %s: %.4f", self.metric_for_best, self.best_metric_value)
|
| 435 |
-
|
| 436 |
-
summary = {
|
| 437 |
-
"best_epoch": self.best_epoch,
|
| 438 |
-
"best_metric": self.best_metric_value,
|
| 439 |
-
"metric_name": self.metric_for_best,
|
| 440 |
-
"total_steps": self.global_step,
|
| 441 |
-
"train_loss_history": self.train_loss_history,
|
| 442 |
-
"val_metrics_history": self.val_metrics_history,
|
| 443 |
-
}
|
| 444 |
-
summary_path = self.checkpoint_dir / "training_summary.json"
|
| 445 |
-
with open(summary_path, "w", encoding="utf-8") as f:
|
| 446 |
-
json.dump(summary, f, indent=2, ensure_ascii=False, default=str)
|
| 447 |
-
logger.info("Training summary saved to %s", summary_path)
|
| 448 |
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
self.wandb_run.finish()
|
| 454 |
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
for k, v in batch.items()
|
| 459 |
-
}
|
|
|
|
| 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
|
|
|
|
| 25 |
from torch.utils.data import DataLoader
|
| 26 |
from tqdm import tqdm
|
| 27 |
|
|
|
|
|
|
|
|
|
|
| 28 |
logger = logging.getLogger(__name__)
|
| 29 |
|
| 30 |
|
|
|
|
| 32 |
"""
|
| 33 |
翻译模型训练器。
|
| 34 |
|
| 35 |
+
TODO [Person C]: 实现以下所有方法。
|
| 36 |
+
|
| 37 |
使用方法:
|
| 38 |
trainer = Trainer(model, train_loader, val_loader, config)
|
| 39 |
trainer.train()
|
|
|
|
| 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,30 +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 |
-
from easytranslate.utils.cloud_storage import (
|
| 7 |
-
is_colab_environment,
|
| 8 |
-
mount_google_drive,
|
| 9 |
-
sync_all_to_drive,
|
| 10 |
-
sync_checkpoints_to_drive,
|
| 11 |
-
sync_logs_to_drive,
|
| 12 |
-
save_training_summary_to_drive,
|
| 13 |
-
setup_colab_environment,
|
| 14 |
-
)
|
| 15 |
|
| 16 |
-
__all__ = [
|
| 17 |
-
"load_config",
|
| 18 |
-
"merge_configs",
|
| 19 |
-
"config_from_cli",
|
| 20 |
-
"config_to_dict",
|
| 21 |
-
"set_seed",
|
| 22 |
-
"setup_logging",
|
| 23 |
-
"is_colab_environment",
|
| 24 |
-
"mount_google_drive",
|
| 25 |
-
"sync_all_to_drive",
|
| 26 |
-
"sync_checkpoints_to_drive",
|
| 27 |
-
"sync_logs_to_drive",
|
| 28 |
-
"save_training_summary_to_drive",
|
| 29 |
-
"setup_colab_environment",
|
| 30 |
-
]
|
|
|
|
| 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/cloud_storage.py
DELETED
|
@@ -1,188 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Cloud storage integration module for Google Colab and Google Drive.
|
| 3 |
-
|
| 4 |
-
Provides secure, authenticated storage operations for training results,
|
| 5 |
-
checkpoints, and logs. Supports automatic environment detection for
|
| 6 |
-
Colab vs local execution.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
from __future__ import annotations
|
| 10 |
-
|
| 11 |
-
import json
|
| 12 |
-
import logging
|
| 13 |
-
import os
|
| 14 |
-
import shutil
|
| 15 |
-
import time
|
| 16 |
-
from pathlib import Path
|
| 17 |
-
from typing import Optional
|
| 18 |
-
|
| 19 |
-
logger = logging.getLogger(__name__)
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
def is_colab_environment() -> bool:
|
| 23 |
-
try:
|
| 24 |
-
import google.colab
|
| 25 |
-
return True
|
| 26 |
-
except ImportError:
|
| 27 |
-
return False
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
def mount_google_drive(mount_point: str = "/content/drive") -> bool:
|
| 31 |
-
if not is_colab_environment():
|
| 32 |
-
logger.info("Not running in Colab, skipping Google Drive mount")
|
| 33 |
-
return False
|
| 34 |
-
|
| 35 |
-
try:
|
| 36 |
-
from google.colab import drive
|
| 37 |
-
drive.mount(mount_point)
|
| 38 |
-
logger.info("Google Drive mounted at %s", mount_point)
|
| 39 |
-
return True
|
| 40 |
-
except Exception as e:
|
| 41 |
-
logger.error("Failed to mount Google Drive: %s", e)
|
| 42 |
-
return False
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def get_drive_path(base_path: str = "/content/drive/MyDrive/EasyTranslate") -> Optional[Path]:
|
| 46 |
-
if not os.path.exists("/content/drive"):
|
| 47 |
-
logger.warning("Google Drive not mounted, cannot resolve drive path")
|
| 48 |
-
return None
|
| 49 |
-
|
| 50 |
-
drive_path = Path(base_path)
|
| 51 |
-
drive_path.mkdir(parents=True, exist_ok=True)
|
| 52 |
-
return drive_path
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def sync_checkpoints_to_drive(
|
| 56 |
-
local_checkpoint_dir: str | Path,
|
| 57 |
-
drive_base_path: str = "/content/drive/MyDrive/EasyTranslate",
|
| 58 |
-
max_retries: int = 3,
|
| 59 |
-
) -> bool:
|
| 60 |
-
local_dir = Path(local_checkpoint_dir)
|
| 61 |
-
if not local_dir.exists():
|
| 62 |
-
logger.warning("Local checkpoint directory does not exist: %s", local_dir)
|
| 63 |
-
return False
|
| 64 |
-
|
| 65 |
-
drive_path = get_drive_path(drive_base_path)
|
| 66 |
-
if drive_path is None:
|
| 67 |
-
return False
|
| 68 |
-
|
| 69 |
-
drive_checkpoint_dir = drive_path / "checkpoints"
|
| 70 |
-
drive_checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
| 71 |
-
|
| 72 |
-
success = True
|
| 73 |
-
for ckpt_file in local_dir.glob("*.pt"):
|
| 74 |
-
dest = drive_checkpoint_dir / ckpt_file.name
|
| 75 |
-
for attempt in range(max_retries):
|
| 76 |
-
try:
|
| 77 |
-
shutil.copy2(ckpt_file, dest)
|
| 78 |
-
logger.info("Synced checkpoint to Drive: %s", dest)
|
| 79 |
-
break
|
| 80 |
-
except Exception as e:
|
| 81 |
-
logger.warning("Sync attempt %d/%d failed for %s: %s", attempt + 1, max_retries, ckpt_file.name, e)
|
| 82 |
-
if attempt == max_retries - 1:
|
| 83 |
-
success = False
|
| 84 |
-
time.sleep(2 ** attempt)
|
| 85 |
-
|
| 86 |
-
return success
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
def sync_logs_to_drive(
|
| 90 |
-
local_log_dir: str | Path,
|
| 91 |
-
drive_base_path: str = "/content/drive/MyDrive/EasyTranslate",
|
| 92 |
-
max_retries: int = 3,
|
| 93 |
-
) -> bool:
|
| 94 |
-
local_dir = Path(local_log_dir)
|
| 95 |
-
if not local_dir.exists():
|
| 96 |
-
logger.warning("Local log directory does not exist: %s", local_dir)
|
| 97 |
-
return False
|
| 98 |
-
|
| 99 |
-
drive_path = get_drive_path(drive_base_path)
|
| 100 |
-
if drive_path is None:
|
| 101 |
-
return False
|
| 102 |
-
|
| 103 |
-
drive_log_dir = drive_path / "logs"
|
| 104 |
-
drive_log_dir.mkdir(parents=True, exist_ok=True)
|
| 105 |
-
|
| 106 |
-
success = True
|
| 107 |
-
for log_file in local_dir.glob("*"):
|
| 108 |
-
if log_file.is_file():
|
| 109 |
-
dest = drive_log_dir / log_file.name
|
| 110 |
-
for attempt in range(max_retries):
|
| 111 |
-
try:
|
| 112 |
-
shutil.copy2(log_file, dest)
|
| 113 |
-
break
|
| 114 |
-
except Exception as e:
|
| 115 |
-
logger.warning("Log sync attempt %d/%d failed: %s", attempt + 1, max_retries, e)
|
| 116 |
-
if attempt == max_retries - 1:
|
| 117 |
-
success = False
|
| 118 |
-
time.sleep(2 ** attempt)
|
| 119 |
-
|
| 120 |
-
return success
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
def save_training_summary_to_drive(
|
| 124 |
-
summary: dict,
|
| 125 |
-
drive_base_path: str = "/content/drive/MyDrive/EasyTranslate",
|
| 126 |
-
) -> bool:
|
| 127 |
-
drive_path = get_drive_path(drive_base_path)
|
| 128 |
-
if drive_path is None:
|
| 129 |
-
return False
|
| 130 |
-
|
| 131 |
-
summary_path = drive_path / "training_summary.json"
|
| 132 |
-
try:
|
| 133 |
-
with open(summary_path, "w", encoding="utf-8") as f:
|
| 134 |
-
json.dump(summary, f, indent=2, ensure_ascii=False, default=str)
|
| 135 |
-
logger.info("Training summary saved to Drive: %s", summary_path)
|
| 136 |
-
return True
|
| 137 |
-
except Exception as e:
|
| 138 |
-
logger.error("Failed to save training summary to Drive: %s", e)
|
| 139 |
-
return False
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
def sync_all_to_drive(
|
| 143 |
-
checkpoint_dir: str | Path = "checkpoints",
|
| 144 |
-
log_dir: str | Path = "logs",
|
| 145 |
-
drive_base_path: str = "/content/drive/MyDrive/EasyTranslate",
|
| 146 |
-
) -> dict:
|
| 147 |
-
results = {
|
| 148 |
-
"checkpoints_synced": sync_checkpoints_to_drive(checkpoint_dir, drive_base_path),
|
| 149 |
-
"logs_synced": sync_logs_to_drive(log_dir, drive_base_path),
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
summary_path = Path(checkpoint_dir) / "training_summary.json"
|
| 153 |
-
if summary_path.exists():
|
| 154 |
-
with open(summary_path, "r", encoding="utf-8") as f:
|
| 155 |
-
summary = json.load(f)
|
| 156 |
-
results["summary_saved"] = save_training_summary_to_drive(summary, drive_base_path)
|
| 157 |
-
|
| 158 |
-
logger.info("Drive sync results: %s", results)
|
| 159 |
-
return results
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
def setup_colab_environment() -> dict:
|
| 163 |
-
env_info = {
|
| 164 |
-
"is_colab": is_colab_environment(),
|
| 165 |
-
"gpu_available": False,
|
| 166 |
-
"gpu_info": None,
|
| 167 |
-
"drive_mounted": False,
|
| 168 |
-
"drive_path": None,
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
if env_info["is_colab"]:
|
| 172 |
-
env_info["drive_mounted"] = mount_google_drive()
|
| 173 |
-
env_info["drive_path"] = str(get_drive_path()) if env_info["drive_mounted"] else None
|
| 174 |
-
|
| 175 |
-
try:
|
| 176 |
-
import torch
|
| 177 |
-
env_info["gpu_available"] = torch.cuda.is_available()
|
| 178 |
-
if env_info["gpu_available"]:
|
| 179 |
-
env_info["gpu_info"] = {
|
| 180 |
-
"device_count": torch.cuda.device_count(),
|
| 181 |
-
"device_name": torch.cuda.get_device_name(0),
|
| 182 |
-
"device_capability": torch.cuda.get_device_capability(0),
|
| 183 |
-
}
|
| 184 |
-
except ImportError:
|
| 185 |
-
pass
|
| 186 |
-
|
| 187 |
-
logger.info("Environment setup: %s", {k: v for k, v in env_info.items() if k != "gpu_info"})
|
| 188 |
-
return env_info
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/easytranslate/utils/config.py
CHANGED
|
@@ -19,66 +19,33 @@ def load_config(config_path: str | Path) -> DictConfig:
|
|
| 19 |
"""
|
| 20 |
加载 YAML 配置文件。
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
DictConfig: OmegaConf 配置对象
|
| 27 |
"""
|
| 28 |
-
|
| 29 |
-
if not config_path.exists():
|
| 30 |
-
raise FileNotFoundError(f"Config file not found: {config_path}")
|
| 31 |
-
|
| 32 |
-
config = OmegaConf.load(config_path)
|
| 33 |
-
logger.info("Loaded config from %s", config_path)
|
| 34 |
-
return config
|
| 35 |
|
| 36 |
|
| 37 |
def merge_configs(base_config: DictConfig, override_config: DictConfig) -> DictConfig:
|
| 38 |
"""
|
| 39 |
合并配置 (override 覆盖 base)。
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
override_config: 覆盖配置
|
| 44 |
-
|
| 45 |
-
Returns:
|
| 46 |
-
DictConfig: 合并后的配置
|
| 47 |
"""
|
| 48 |
-
|
| 49 |
|
| 50 |
|
| 51 |
def config_from_cli(config_path: str, cli_args: list[str]) -> DictConfig:
|
| 52 |
"""
|
| 53 |
从配置文件 + 命令行参数构建最终配置。
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
cli_args: 命令行覆盖参数列表
|
| 60 |
-
|
| 61 |
-
Returns:
|
| 62 |
-
DictConfig: 最终配置
|
| 63 |
-
"""
|
| 64 |
-
config = load_config(config_path)
|
| 65 |
-
|
| 66 |
-
if cli_args:
|
| 67 |
-
cli_config = OmegaConf.from_cli(cli_args)
|
| 68 |
-
config = merge_configs(config, cli_config)
|
| 69 |
-
logger.info("Applied %d CLI overrides", len(cli_args))
|
| 70 |
-
|
| 71 |
-
return config
|
| 72 |
|
| 73 |
-
|
| 74 |
-
def config_to_dict(config: DictConfig) -> dict:
|
| 75 |
-
"""
|
| 76 |
-
将 OmegaConf DictConfig 转换为普通 Python dict。
|
| 77 |
-
|
| 78 |
-
Args:
|
| 79 |
-
config: OmegaConf 配置对象
|
| 80 |
-
|
| 81 |
-
Returns:
|
| 82 |
-
dict: 普通 Python 字典
|
| 83 |
"""
|
| 84 |
-
|
|
|
|
| 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
|
@@ -16,44 +16,11 @@ def setup_logging(
|
|
| 16 |
"""
|
| 17 |
配置日志系统。
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
| 23 |
"""
|
| 24 |
-
|
| 25 |
-
root_logger.setLevel(level)
|
| 26 |
-
|
| 27 |
-
for handler in root_logger.handlers[:]:
|
| 28 |
-
root_logger.removeHandler(handler)
|
| 29 |
-
|
| 30 |
-
fmt = logging.Formatter(
|
| 31 |
-
"[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s",
|
| 32 |
-
datefmt="%Y-%m-%d %H:%M:%S",
|
| 33 |
-
)
|
| 34 |
-
|
| 35 |
-
try:
|
| 36 |
-
from rich.logging import RichHandler
|
| 37 |
-
console_handler = RichHandler(rich_tracebacks=True, markup=True)
|
| 38 |
-
console_handler.setLevel(level)
|
| 39 |
-
console_handler.setFormatter(logging.Formatter("%(message)s"))
|
| 40 |
-
root_logger.addHandler(console_handler)
|
| 41 |
-
except ImportError:
|
| 42 |
-
console_handler = logging.StreamHandler(sys.stdout)
|
| 43 |
-
console_handler.setLevel(level)
|
| 44 |
-
console_handler.setFormatter(fmt)
|
| 45 |
-
root_logger.addHandler(console_handler)
|
| 46 |
-
|
| 47 |
-
if log_dir is not None:
|
| 48 |
-
log_dir = Path(log_dir)
|
| 49 |
-
log_dir.mkdir(parents=True, exist_ok=True)
|
| 50 |
-
file_name = log_file or "easytranslate.log"
|
| 51 |
-
file_handler = logging.FileHandler(log_dir / file_name, encoding="utf-8")
|
| 52 |
-
file_handler.setLevel(level)
|
| 53 |
-
file_handler.setFormatter(fmt)
|
| 54 |
-
root_logger.addHandler(file_handler)
|
| 55 |
-
|
| 56 |
-
logging.getLogger("matplotlib").setLevel(logging.WARNING)
|
| 57 |
-
logging.getLogger("PIL").setLevel(logging.WARNING)
|
| 58 |
-
|
| 59 |
-
root_logger.info("Logging initialized (level=%s)", logging.getLevelName(level))
|
|
|
|
| 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
|
@@ -2,7 +2,6 @@
|
|
| 2 |
随机种子管理模块 — 公共模块
|
| 3 |
"""
|
| 4 |
|
| 5 |
-
import os
|
| 6 |
import random
|
| 7 |
|
| 8 |
import numpy as np
|
|
@@ -13,13 +12,12 @@ def set_seed(seed: int = 42):
|
|
| 13 |
"""
|
| 14 |
设置全局随机种子以确保实验可复现。
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
"""
|
| 19 |
-
|
| 20 |
-
np.random.seed(seed)
|
| 21 |
-
torch.manual_seed(seed)
|
| 22 |
-
torch.cuda.manual_seed_all(seed)
|
| 23 |
-
torch.backends.cudnn.deterministic = True
|
| 24 |
-
torch.backends.cudnn.benchmark = False
|
| 25 |
-
os.environ["PYTHONHASHSEED"] = str(seed)
|
|
|
|
| 2 |
随机种子管理模块 — 公共模块
|
| 3 |
"""
|
| 4 |
|
|
|
|
| 5 |
import random
|
| 6 |
|
| 7 |
import numpy as np
|
|
|
|
| 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")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|