.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: 2 # 2 workers; set 0 for CPU-only environments
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
- # - "comet" # COMET (需下载 ~4 GB 模型,可按需开启)
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
- # EasyTranslate — Dependency Specification
3
- # =============================================================================
4
- # This file specifies all dependencies with compatible version ranges.
5
- # For exact reproducible environments, see requirements-lock.txt (pip freeze).
6
- # For Google Colab, see requirements-colab.txt (subset of packages).
7
- # =============================================================================
8
-
9
- # Core ML Framework
10
- torch>=2.1.0,<2.11.0
11
- torchvision>=0.16.0; platform_system != "Darwin" or platform_machine != "arm64"
12
-
13
- # HuggingFace Ecosystem
14
- transformers>=4.36.0,<4.45.0
15
- datasets>=2.16.0,<3.0.0
16
- tokenizers>=0.15.0,<0.20.0
17
- sentencepiece>=0.2.0,<0.3.0
18
- accelerate>=0.25.0,<0.35.0
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,<3.9.0
36
-
37
- # Configuration & CLI
38
- omegaconf>=2.3.0,<3.0.0
39
- pyyaml>=6.0.0,<7.0.0
40
- rich>=13.0.0,<14.0.0
41
-
42
- # Data Processing
43
- numpy>=1.24.0,<3.0.0
44
- pandas>=2.0.0,<2.3.0
45
- protobuf>=5.28.3,<7.0.0
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,<0.115.0
54
- uvicorn>=0.25.0,<0.31.0
55
- gradio>=4.10.0,<5.0.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
- # 1. 加载配置
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
- print("\nInteractive Translation Mode (type 'quit' to exit)")
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
- input_file = Path(input_path)
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
- try:
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
- with open(args.config, "r", encoding="utf-8") as f:
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,<2.11.0",
18
- "transformers>=4.36.0,<4.45.0",
19
- "datasets>=2.16.0,<3.0.0",
20
- "tokenizers>=0.15.0,<0.20.0",
21
- "sentencepiece>=0.2.0,<0.3.0",
22
- "accelerate>=0.25.0,<0.35.0",
23
- "peft>=0.7.0,<0.12.0",
24
- "sacrebleu>=2.4.0,<3.0.0",
25
- "unbabel-comet>=2.2.0,<2.3.0",
26
- "numpy>=1.24.0,<3.0.0",
27
- "protobuf>=5.28.3,<7.0.0",
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
- seq_len = self.lengths[idx]
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, seq_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
- encoder_output = model.encode(src_ids, src_padding_mask)
39
- batch_size = src_ids.size(0)
40
- device = src_ids.device
41
-
42
- decoder_input = torch.full((batch_size, 1), bos_id, dtype=torch.long, device=device)
43
- finished = torch.zeros(batch_size, dtype=torch.bool, device=device)
44
-
45
- for _ in range(max_len):
46
- logits = model.decode_step(decoder_input, encoder_output, src_padding_mask)
47
- next_token = logits.argmax(dim=-1, keepdim=True)
48
- decoder_input = torch.cat([decoder_input, next_token], dim=1)
49
- finished = finished | next_token.squeeze(-1).eq(eos_id)
50
- if finished.all():
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. 限制最大 beam_size 64,防止内存溢出
73
- 2. 使用更高效的索引操作避免中间 tensor 累积
74
- 3. 及时释放不再需要的中间结果
75
- 4. 对长序列使用更小的 beam_size
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  """
77
- # 安全检查:限制 beam_size 防止内存问题
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
- """采样解码 (支持 temperature, top-k, top-p/nucleus sampling)。"""
183
- encoder_output = model.encode(src_ids, src_padding_mask)
184
- batch_size = src_ids.size(0)
185
- device = src_ids.device
186
-
187
- decoder_input = torch.full((batch_size, 1), bos_id, dtype=torch.long, device=device)
188
- finished = torch.zeros(batch_size, dtype=torch.bool, device=device)
189
-
190
- for _ in range(max_len):
191
- logits = model.decode_step(decoder_input, encoder_output, src_padding_mask)
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
- """防止生成重复的 n-gram。"""
230
- if ngram_size <= 0:
231
- return logits
232
-
233
- batch_size = logits.size(0)
234
- seq_len = generated_tokens.size(1)
235
-
236
- if seq_len < ngram_size - 1:
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
- self.model = model
34
- self.tokenizer = tokenizer
35
- self.config = config
36
-
37
- eval_config = config.get("evaluation", {})
38
- decoding_config = eval_config.get("decoding", {})
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
- Args:
116
- dataloader: 测试数据集的 DataLoader
117
- src_texts: 源文本列表 (于某些指标计算)
118
- ref_texts: 参考译列表
119
-
120
- Returns:
121
- dict: 包含所有评估指标的字典
122
-
123
- Raises:
124
- ValueError: 如果批处理格式不正确
125
  """
126
- self.model.eval()
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
- self.model.eval()
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
- return translations
 
 
 
 
 
 
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
- Args:
47
- hypotheses: 生成的译文列表
48
- references: 参考译列表
49
- tokenize: 分词方式,"zh" 表示字符级别分词
50
-
51
- Returns:
52
- dict: 包含 BLEU 分数及各 n-gram 精度的字典
53
-
54
- Raises:
55
- ValueError: 如果输入数据为空或长度不匹配
56
  """
57
- if not hypotheses or not references:
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
- Args:
90
- sources: 源文本列表
91
- hypotheses: 生成的译文列表
92
- references: 参考译文列表
93
- model_name: COMET 模型名称
94
- batch_size: 批处理大小
95
- gpus: 使用的 GPU 数量
96
-
97
- Returns:
98
- dict: 包含 COMET 系统分数和句子级别分数的字典
99
-
100
- Raises:
101
- ValueError: 如果输入数据为空或长度不匹配
102
- ImportError: 如果 COMET 库未安装
103
  """
104
- try:
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
- sacrebleu = _require_sacrebleu()
159
- chrf = sacrebleu.corpus_chrf(hypotheses, [references])
160
- return {"chrf": round(float(chrf.score), 4)}
 
 
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
- sacrebleu = _require_sacrebleu()
190
- ter = sacrebleu.corpus_ter(hypotheses, [references])
191
- return {"ter": round(float(ter.score), 4)}
 
 
192
 
193
 
194
  def compute_all_metrics(
195
- sources: Optional[list[str]] = None,
196
- hypotheses: Optional[list[str]] = None,
197
- references: Optional[list[str]] = None,
198
  metrics: list[str] = ["bleu", "comet", "chrf", "ter"],
199
  ) -> dict:
200
  """
201
  计算所有指定的评估指标。
202
-
203
- Args:
204
- sources: 源文本列表 (用于 COMET)
205
- hypotheses: 生成译文列表
206
- references: 参考译文列表
207
- metrics: 需要计算的指标列表
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
- self.dropout = nn.Dropout(p=dropout)
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
- B, L_q, _ = query.size()
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
- 如果 PyTorch < 2.0 GPU 不支持,会自动回退到标准注意力。
 
 
 
 
 
 
 
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
- if not _has_flash_attn:
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
- 架构 (Pre-LayerNorm):
30
- x → LN → Masked Self-Attention → Residual
31
- LN → Cross-Attention Residual
32
- LN → FFN → Residual
 
 
33
  """
34
 
35
  def __init__(
@@ -44,42 +42,7 @@ class TransformerDecoderLayer(nn.Module):
44
  pre_norm: bool = True,
45
  ):
46
  super().__init__()
47
- self.pre_norm = pre_norm
48
- self.d_model = d_model
49
-
50
- attn_cls = FlashMultiHeadAttention if use_flash_attention else MultiHeadAttention
51
-
52
- # 1. Masked Self-Attention
53
- self.self_attn = attn_cls(
54
- d_model=d_model,
55
- nhead=nhead,
56
- dropout=dropout,
57
- use_rotary_embedding=use_rotary_embedding,
58
- )
59
-
60
- # 2. Cross-Attention (decoder queries encoder memory)
61
- self.multihead_attn = attn_cls(
62
- d_model=d_model,
63
- nhead=nhead,
64
- dropout=dropout,
65
- use_rotary_embedding=False, # cross-attention 不使用 RoPE
66
- )
67
-
68
- # 3. Feed-Forward Network
69
- self.linear1 = nn.Linear(d_model, dim_feedforward)
70
- self.activation = nn.GELU() if activation == "gelu" else nn.ReLU()
71
- self.dropout = nn.Dropout(p=dropout)
72
- self.linear2 = nn.Linear(dim_feedforward, d_model)
73
-
74
- # 4. LayerNorms
75
- self.norm1 = nn.LayerNorm(d_model)
76
- self.norm2 = nn.LayerNorm(d_model)
77
- self.norm3 = nn.LayerNorm(d_model)
78
-
79
- # 5. Dropouts for residuals
80
- self.dropout1 = nn.Dropout(p=dropout)
81
- self.dropout2 = nn.Dropout(p=dropout)
82
- self.dropout3 = nn.Dropout(p=dropout)
83
 
84
  def forward(
85
  self,
@@ -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
- """Pre-LayerNorm 前向传播。"""
93
- # 适配 Flash Attention: 使用 is_causal 替代显式 causal mask
94
- is_causal = tgt_mask is not None
95
-
96
- if self.pre_norm:
97
- # 1. Masked Self-Attention
98
- residual = tgt
99
- tgt = self.norm1(tgt)
100
- tgt = self.self_attn(
101
- tgt, tgt, tgt,
102
- key_padding_mask=tgt_key_padding_mask,
103
- is_causal=is_causal,
104
- )
105
- tgt = residual + self.dropout1(tgt)
106
-
107
- # 2. Cross-Attention
108
- residual = tgt
109
- tgt = self.norm2(tgt)
110
- tgt = self.multihead_attn(
111
- tgt, memory, memory,
112
- key_padding_mask=memory_key_padding_mask,
113
- )
114
- tgt = residual + self.dropout2(tgt)
115
-
116
- # 3. FFN
117
- residual = tgt
118
- tgt = self.norm3(tgt)
119
- tgt = self.linear2(self.dropout(self.activation(self.linear1(tgt))))
120
- tgt = residual + self.dropout3(tgt)
121
- else:
122
- # Post-LayerNorm (备用)
123
- residual = tgt
124
- tgt = self.self_attn(
125
- tgt, tgt, tgt,
126
- key_padding_mask=tgt_key_padding_mask,
127
- is_causal=is_causal,
128
- )
129
- tgt = self.norm1(residual + self.dropout1(tgt))
130
-
131
- residual = tgt
132
- tgt = self.multihead_attn(
133
- tgt, memory, memory,
134
- key_padding_mask=memory_key_padding_mask,
135
- )
136
- tgt = self.norm2(residual + self.dropout2(tgt))
137
-
138
- residual = tgt
139
- tgt = self.linear2(self.dropout(self.activation(self.linear1(tgt))))
140
- tgt = self.norm3(residual + self.dropout3(tgt))
141
-
142
- return tgt
143
 
144
 
145
  class TransformerDecoder(nn.Module):
146
  """
147
  多层 Transformer Decoder。
 
 
 
 
148
  """
149
 
150
  def __init__(self, decoder_layer: TransformerDecoderLayer, num_layers: int):
151
  super().__init__()
152
- self.layers = nn.ModuleList(
153
- [copy.deepcopy(decoder_layer) for _ in range(num_layers)]
154
- )
155
- self.num_layers = num_layers
156
- self.norm = nn.LayerNorm(decoder_layer.d_model)
157
 
158
  def forward(
159
  self,
@@ -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
- output = tgt
167
- for layer in self.layers:
168
- output = layer(
169
- output,
170
- memory,
171
- tgt_mask=tgt_mask,
172
- memory_key_padding_mask=memory_key_padding_mask,
173
- tgt_key_padding_mask=tgt_key_padding_mask,
174
- )
175
- output = self.norm(output)
176
- return output
 
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
- 架构 (Pre-LayerNorm):
28
- x LayerNorm → MultiHeadAttention Residual → LayerNorm → FFN → Residual
 
 
 
 
 
 
29
  """
30
 
31
  def __init__(
@@ -40,88 +42,43 @@ class TransformerEncoderLayer(nn.Module):
40
  pre_norm: bool = True,
41
  ):
42
  super().__init__()
43
- self.pre_norm = pre_norm
44
-
45
- # Self-Attention
46
- attn_cls = FlashMultiHeadAttention if use_flash_attention else MultiHeadAttention
47
- self.self_attn = attn_cls(
48
- d_model=d_model,
49
- nhead=nhead,
50
- dropout=dropout,
51
- use_rotary_embedding=use_rotary_embedding,
52
- )
53
-
54
- # Feed-Forward Network
55
- self.linear1 = nn.Linear(d_model, dim_feedforward)
56
- self.activation = nn.GELU() if activation == "gelu" else nn.ReLU()
57
- self.dropout = nn.Dropout(p=dropout)
58
- self.linear2 = nn.Linear(dim_feedforward, d_model)
59
-
60
- # LayerNorm
61
- self.norm1 = nn.LayerNorm(d_model)
62
- self.norm2 = nn.LayerNorm(d_model)
63
-
64
- # Dropout for residual
65
- self.dropout1 = nn.Dropout(p=dropout)
66
- self.dropout2 = nn.Dropout(p=dropout)
67
 
68
  def forward(
69
  self,
70
  src: torch.Tensor, # [B, S, D]
71
  src_key_padding_mask: Optional[torch.BoolTensor] = None, # [B, S]
72
  ) -> torch.Tensor:
73
- """Pre-LayerNorm 前向传播。"""
74
- if self.pre_norm:
75
- # 1. Self-Attention sublayer
76
- residual = src
77
- src = self.norm1(src)
78
- src = self.self_attn(
79
- src, src, src,
80
- key_padding_mask=src_key_padding_mask,
81
- )
82
- src = residual + self.dropout1(src)
83
-
84
- # 2. FFN sublayer
85
- residual = src
86
- src = self.norm2(src)
87
- src = self.linear2(self.dropout(self.activation(self.linear1(src))))
88
- src = residual + self.dropout2(src)
89
- else:
90
- # Post-LayerNorm (备用)
91
- residual = src
92
- src = self.self_attn(
93
- src, src, src,
94
- key_padding_mask=src_key_padding_mask,
95
- )
96
- src = self.norm1(residual + self.dropout1(src))
97
-
98
- residual = src
99
- src = self.linear2(self.dropout(self.activation(self.linear1(src))))
100
- src = self.norm2(residual + self.dropout2(src))
101
-
102
- return src
103
 
104
 
105
  class TransformerEncoder(nn.Module):
106
  """
107
  多层 Transformer Encoder。
 
 
 
 
108
  """
109
 
110
  def __init__(self, encoder_layer: TransformerEncoderLayer, num_layers: int):
111
  super().__init__()
112
- self.layers = nn.ModuleList(
113
- [copy.deepcopy(encoder_layer) for _ in range(num_layers)]
114
- )
115
- self.num_layers = num_layers
116
- self.norm = nn.LayerNorm(encoder_layer.self_attn.d_model)
117
 
118
  def forward(
119
  self,
120
  src: torch.Tensor,
121
  src_key_padding_mask: Optional[torch.BoolTensor] = None,
122
  ) -> torch.Tensor:
123
- output = src
124
- for layer in self.layers:
125
- output = layer(output, src_key_padding_mask=src_key_padding_mask)
126
- output = self.norm(output)
127
- return output
 
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
- from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
54
-
55
- tokenizer = AutoTokenizer.from_pretrained(model_name)
56
- model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
57
-
58
- # 设置语言对
59
- if hasattr(tokenizer, "lang_code_to_id"):
60
- tokenizer.src_lang = src_lang
61
- tokenizer.tgt_lang = tgt_lang
62
- if hasattr(model.config, "forced_bos_token_id"):
63
- model.config.forced_bos_token_id = tokenizer.lang_code_to_id.get(
64
- tgt_lang, tokenizer.bos_token_id
65
- )
66
-
67
- model = model.to(device)
68
- logger.info(f"Loaded pretrained model: {model_name}")
69
- return model, tokenizer
70
 
71
 
72
  def setup_lora(
@@ -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
- config = LoraConfig(
91
- r=r,
92
- lora_alpha=alpha,
93
- lora_dropout=dropout,
94
- target_modules=target_modules,
95
- task_type=TaskType.SEQ_2_SEQ_LM,
96
- )
97
-
98
- model = get_peft_model(model, config)
99
-
100
- # 打印可训练参数信息
101
- trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
102
- total_params = sum(p.numel() for p in model.parameters())
103
- ratio = 100 * trainable_params / total_params if total_params > 0 else 0
104
- logger.info(
105
- f"LoRA setup: trainable params={trainable_params:,} "
106
- f"/ total={total_params:,} ({ratio:.4f}%)"
107
- )
108
-
109
- return model
110
 
111
 
112
  def freeze_model_except_lora(model: nn.Module):
113
- """冻结模型所有参数,只保留 LoRA 参数可训练。"""
114
- for name, param in model.named_parameters():
115
- if "lora" not in name.lower():
116
- param.requires_grad = False
 
 
 
 
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
- self.dropout = nn.Dropout(p=dropout)
36
-
37
- pe = torch.zeros(max_seq_len, d_model)
38
- position = torch.arange(0, max_seq_len, dtype=torch.float).unsqueeze(1)
39
- div_term = torch.exp(
40
- torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)
41
- )
42
- pe[:, 0::2] = torch.sin(position * div_term)
43
- pe[:, 1::2] = torch.cos(position * div_term)
44
- pe = pe.unsqueeze(0) # [1, max_seq_len, d_model]
45
- self.register_buffer("pe", pe)
46
 
47
  def forward(self, x: torch.Tensor) -> torch.Tensor:
48
  """
@@ -51,8 +41,7 @@ class SinusoidalPositionalEncoding(nn.Module):
51
  Returns:
52
  x + positional_encoding: [B, L, D]
53
  """
54
- x = x + self.pe[:, : x.size(1), :]
55
- return self.dropout(x)
56
 
57
 
58
  class RotaryPositionalEmbedding(nn.Module):
@@ -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
- self.dim = dim
87
- self.max_seq_len = max_seq_len
88
- self.base = base
89
-
90
- inv_freq = 1.0 / (
91
- base ** (torch.arange(0, dim, 2).float() / dim)
92
- )
93
- self.register_buffer("inv_freq", inv_freq)
94
-
95
- # 预缓存 cos/sin
96
- self._cached_seq_len = 0
97
- self._cached_cos: torch.Tensor | None = None
98
- self._cached_sin: torch.Tensor | None = None
99
 
100
  def _compute_rope(self, seq_len: int, device: torch.device):
101
- # Recompute if seq_len grew OR cache is stale OR device changed (e.g. after .to(cuda))
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
- cos, sin = self._compute_rope(q.size(2), q.device)
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, TransformerEncoderLayer
24
- from easytranslate.model.decoder import TransformerDecoder, TransformerDecoderLayer
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
- # Embeddings
87
- self.src_embed = nn.Embedding(src_vocab_size, d_model)
88
- self.tgt_embed = nn.Embedding(tgt_vocab_size, d_model)
89
- self.embed_scale = math.sqrt(d_model)
90
-
91
- # Positional encoding
92
- if use_rotary_embedding:
93
- # RoPE 在 attention 内部应用到 Q/K,不需要额外的位置编码层
94
- self.pos_encoding: Optional[nn.Module] = None
95
- rope = RotaryPositionalEmbedding(
96
- dim=d_model // nhead,
97
- max_seq_len=max_seq_len,
98
- )
99
- else:
100
- self.pos_encoding = SinusoidalPositionalEncoding(
101
- d_model=d_model,
102
- max_seq_len=max_seq_len,
103
- dropout=dropout,
104
- )
105
- rope = None
106
-
107
- # Encoder
108
- encoder_layer = TransformerEncoderLayer(
109
- d_model=d_model,
110
- nhead=nhead,
111
- dim_feedforward=dim_feedforward,
112
- dropout=dropout,
113
- activation=activation,
114
- use_flash_attention=use_flash_attention,
115
- use_rotary_embedding=use_rotary_embedding,
116
- pre_norm=pre_norm,
117
- )
118
- self.encoder = TransformerEncoder(encoder_layer, num_encoder_layers)
119
-
120
- # Decoder
121
- decoder_layer = TransformerDecoderLayer(
122
- d_model=d_model,
123
- nhead=nhead,
124
- dim_feedforward=dim_feedforward,
125
- dropout=dropout,
126
- activation=activation,
127
- use_flash_attention=use_flash_attention,
128
- use_rotary_embedding=use_rotary_embedding,
129
- pre_norm=pre_norm,
130
- )
131
- self.decoder = TransformerDecoder(decoder_layer, num_decoder_layers)
132
-
133
- # 将 RoPE 注入到 encoder/decoder 的 attention 模块中
134
- if rope is not None:
135
- for layer in self.encoder.layers:
136
- layer.self_attn.rope = rope
137
- for layer in self.decoder.layers:
138
- layer.self_attn.rope = rope
139
- # cross-attention 不使用 RoPE
140
- layer.multihead_attn.rope = None
141
-
142
- # Output projection
143
- self.output_projection = nn.Linear(d_model, tgt_vocab_size)
144
-
145
- # 可选: 共享目标语言 embedding 和输出投影权重
146
- self.share_embedding = share_embedding
147
- if share_embedding:
148
- self.output_projection.weight = self.tgt_embed.weight
149
-
150
- self._init_weights()
151
 
152
  def _init_weights(self):
153
- """参数初始化。"""
154
- for p in self.parameters():
155
- if p.dim() > 1:
156
- nn.init.xavier_uniform_(p)
157
- for module in self.modules():
158
- if isinstance(module, nn.Embedding):
159
- nn.init.normal_(module.weight, mean=0, std=self.d_model ** -0.5)
160
- elif isinstance(module, nn.LayerNorm):
161
- nn.init.ones_(module.weight)
162
- nn.init.zeros_(module.bias)
163
 
164
  def _generate_square_subsequent_mask(self, sz: int, device: torch.device) -> torch.Tensor:
165
- """生成因果注意力掩码 (causal mask)。"""
166
- mask = torch.triu(torch.ones(sz, sz, device=device), diagonal=1)
167
- mask = mask.masked_fill(mask == 1, float("-inf"))
168
- return mask
 
 
 
 
169
 
170
  def forward(
171
  self,
@@ -180,50 +119,19 @@ class TransformerTranslationModel(nn.Module):
180
  Returns:
181
  logits: [B, T, tgt_vocab_size]
182
  """
183
- # 1. Embedding
184
- src_emb = self.src_embed(src_ids) * self.embed_scale # [B, S, D]
185
- tgt_emb = self.tgt_embed(tgt_input_ids) * self.embed_scale # [B, T, D]
186
-
187
- # 2. Positional encoding (如果不使用 RoPE)
188
- if self.pos_encoding is not None:
189
- src_emb = self.pos_encoding(src_emb)
190
- tgt_emb = self.pos_encoding(tgt_emb)
191
-
192
- # 3. Masks
193
- if src_padding_mask is None:
194
- src_padding_mask = src_ids.eq(self.pad_id)
195
- if tgt_padding_mask is None:
196
- tgt_padding_mask = tgt_input_ids.eq(self.pad_id)
197
-
198
- tgt_seq_len = tgt_input_ids.size(1)
199
- tgt_mask = self._generate_square_subsequent_mask(tgt_seq_len, tgt_input_ids.device)
200
-
201
- # 4. Encoder
202
- encoder_output = self.encoder(src_emb, src_key_padding_mask=src_padding_mask)
203
-
204
- # 5. Decoder
205
- decoder_output = self.decoder(
206
- tgt_emb,
207
- encoder_output,
208
- tgt_mask=tgt_mask,
209
- memory_key_padding_mask=src_padding_mask,
210
- tgt_key_padding_mask=tgt_padding_mask,
211
- )
212
-
213
- # 6. Output projection
214
- logits = self.output_projection(decoder_output)
215
- return logits
216
 
217
  @torch.no_grad()
218
  def encode(self, src_ids: torch.Tensor, src_padding_mask: Optional[torch.BoolTensor] = None) -> torch.Tensor:
219
- """仅编码(用于推理时复用 encoder 输出)。"""
220
- src_emb = self.src_embed(src_ids) * self.embed_scale
221
- if self.pos_encoding is not None:
222
- src_emb = self.pos_encoding(src_emb)
223
- if src_padding_mask is None:
224
- src_padding_mask = src_ids.eq(self.pad_id)
225
- encoder_output = self.encoder(src_emb, src_key_padding_mask=src_padding_mask)
226
- return encoder_output
 
227
 
228
  @torch.no_grad()
229
  def decode_step(
@@ -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
- tgt_emb = self.tgt_embed(tgt_input_ids) * self.embed_scale
237
- if self.pos_encoding is not None:
238
- tgt_emb = self.pos_encoding(tgt_emb)
239
-
240
- tgt_seq_len = tgt_input_ids.size(1)
241
- tgt_mask = self._generate_square_subsequent_mask(tgt_seq_len, tgt_input_ids.device)
242
-
243
- decoder_output = self.decoder(
244
- tgt_emb,
245
- encoder_output,
246
- tgt_mask=tgt_mask,
247
- memory_key_padding_mask=src_padding_mask,
248
- )
249
-
250
- # 取最后一个 token 的 logits
251
- logits = self.output_projection(decoder_output[:, -1, :])
252
- return logits
253
 
254
  def count_parameters(self) -> int:
255
  """返回可训练参数数量。"""
 
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
- 标签平滑将真实标签的概率质量从 1.0 重新分配:
25
- target token 概率 = 1 - smoothing
26
- 其他 token 概率 = smoothing / (V - 1)
27
-
28
- 使用 KL 散度实现: loss = KL(smooth_target || log_softmax(logits))
 
 
 
 
 
 
 
 
 
 
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
- self.confidence = 1.0 - smoothing
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] — 目标 token ids (padding 位置应为 pad_id, 默认 -100)
44
  Returns:
45
  loss: scalar
46
  """
47
- logits = logits.contiguous()
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: torch.nn.Module, config: dict) -> torch.optim.Optimizer:
26
  """
27
  根据配置创建优化器。
28
 
29
- 支持参数分组:
30
- - LayerNorm bias 参数不施加 weight_decay
31
- - embedding 层可使用较小的学习率
 
 
 
32
  """
33
- opt_config = config.get("optimizer", {})
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
- - cosine_with_warmup: Cosine 衰减 + 线性 warmup
87
- - inverse_sqrt: 经典 Transformer 调度策略
88
- - linear: 线性衰减 + warmup
 
 
 
 
 
 
89
  """
90
- sched_config = config.get("scheduler", {})
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 论文 (Vaswani et al., 2017) 使用的调度策略。
156
- warmup 阶段线性增长,warmup 后按 step^{-0.5} 衰减。
 
 
 
 
157
  """
158
 
159
  def __init__(self, optimizer, warmup_steps: int = 4000):
160
- self.warmup_steps = warmup_steps
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
- logger.info("Starting training for %d epochs", self.num_epochs)
194
- logger.info("Device: %s, FP16: %s, BF16: %s", self.device, self.fp16, self.bf16)
195
- logger.info("Gradient accumulation steps: %d", self.gradient_accumulation_steps)
196
-
197
- for epoch in range(self.current_epoch, self.num_epochs):
198
- self.current_epoch = epoch
199
- logger.info("=" * 50)
200
- logger.info("Epoch %d/%d", epoch + 1, self.num_epochs)
201
-
202
- train_loss = self._train_one_epoch(epoch)
203
- self.train_loss_history.append(train_loss)
204
-
205
- val_metrics = self._validate(epoch)
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
- self.model.train()
224
- total_loss = 0.0
225
- num_batches = 0
226
- self.optimizer.zero_grad()
227
-
228
- pbar = tqdm(self.train_loader, desc=f"Train Epoch {epoch + 1}", leave=False)
229
- for batch_idx, batch in enumerate(pbar):
230
- batch = self._move_batch_to_device(batch)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- self.model.eval()
285
- total_loss = 0.0
286
- num_batches = 0
287
-
288
- pbar = tqdm(self.val_loader, desc=f"Val Epoch {epoch + 1}", leave=False)
289
- for batch in pbar:
290
- batch = self._move_batch_to_device(batch)
291
-
292
- with torch.amp.autocast(self.device.type, enabled=self.amp_dtype is not None, dtype=self.amp_dtype):
293
- logits = self.model(
294
- batch["src_ids"],
295
- batch["tgt_input_ids"],
296
- batch.get("src_padding_mask"),
297
- batch.get("tgt_padding_mask"),
298
- )
299
- loss = self.criterion(logits, batch["labels"])
300
- total_loss += loss.item()
301
- num_batches += 1
302
-
303
- val_loss = total_loss / max(num_batches, 1)
304
- metrics = {"val_loss": round(val_loss, 4)}
305
-
306
- if self.evaluator is not None and self.config.get("evaluation", {}).get("eval_on_epoch_end", True):
307
- try:
308
- eval_results = self.evaluator.evaluate(self.val_loader)
309
- metrics.update(eval_results)
310
- except Exception as e:
311
- logger.warning("Evaluation failed during validation: %s", e)
312
-
313
- logger.info("Epoch %d - Val Loss: %.4f", epoch + 1, val_loss)
314
- for k, v in metrics.items():
315
- if k != "val_loss" and not isinstance(v, list):
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
- logger.info("Loading checkpoint from %s", checkpoint_path)
368
- checkpoint = torch.load(checkpoint_path, map_location=self.device)
369
 
370
- model_to_load = self.model.module if hasattr(self.model, "module") else self.model
371
- model_to_load.load_state_dict(checkpoint["model_state_dict"])
372
-
373
- self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
374
- self.scheduler.load_state_dict(checkpoint["scheduler_state_dict"])
375
- self.current_epoch = checkpoint["epoch"] + 1
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
- if not self.early_stopping_enabled:
387
- return False
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
- self.epochs_without_improvement += 1
398
- logger.info(
399
- "No improvement for %d epochs (best: %.4f, current: %.4f)",
400
- self.epochs_without_improvement, self.best_metric_value, current_metric,
401
- )
402
- return self.epochs_without_improvement >= self.patience
403
 
404
  def _log_metrics(self, epoch: int, train_loss: float, val_metrics: dict):
405
- if self.writer is not None:
406
- self.writer.add_scalar("Loss/train", train_loss, epoch)
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
- if self.wandb_run is not None:
413
- import wandb
414
- log_dict = {"epoch": epoch, "train_loss": train_loss}
415
- for k, v in val_metrics.items():
416
- if not isinstance(v, list):
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 _log_step_metrics(self, loss: float, lr: float):
422
- if self.writer is not None:
423
- self.writer.add_scalar("Loss/train_step", loss, self.global_step)
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
- def _cleanup(self):
450
- if self.writer is not None:
451
- self.writer.close()
452
- if self.wandb_run is not None:
453
- self.wandb_run.finish()
454
 
455
- def _move_batch_to_device(self, batch: dict) -> dict:
456
- return {
457
- k: v.to(self.device, non_blocking=True) if isinstance(v, torch.Tensor) else v
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, config_from_cli, config_to_dict
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
- Args:
23
- config_path: 配置文件路径
24
-
25
- Returns:
26
- DictConfig: OmegaConf 配置对象
27
  """
28
- config_path = Path(config_path)
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
- Args:
42
- base_config: 基础配置
43
- override_config: 覆盖配置
44
-
45
- Returns:
46
- DictConfig: 合并后的配置
47
  """
48
- return OmegaConf.merge(base_config, override_config)
49
 
50
 
51
  def config_from_cli(config_path: str, cli_args: list[str]) -> DictConfig:
52
  """
53
  从配置文件 + 命令行参数构建最终配置。
54
 
55
- 使用方式: python train.py --config configs/default.yaml training.lr=1e-4 model.nhead=16
56
-
57
- Args:
58
- config_path: 配置文件路径
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
- return OmegaConf.to_container(config, resolve=True)
 
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
- Args:
20
- log_dir: 日志目录路径
21
- level: 日志级别
22
- log_file: 日志文件名 (相对于 log_dir)
 
 
23
  """
24
- root_logger = logging.getLogger()
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
- Args:
17
- seed: 随机种子值
 
 
 
 
 
18
  """
19
- random.seed(seed)
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")