lijn14 commited on
Commit
c0396a3
·
1 Parent(s): 5007ef5

优化ipynb

Browse files
Files changed (1) hide show
  1. notebooks/EasyTranslate_Production.ipynb +213 -148
notebooks/EasyTranslate_Production.ipynb CHANGED
@@ -37,6 +37,9 @@
37
  "import os\n",
38
  "import sys\n",
39
  "import subprocess\n",
 
 
 
40
  "from pathlib import Path\n",
41
  "\n",
42
  "IN_COLAB = False\n",
@@ -48,7 +51,7 @@
48
  "\n",
49
  "print(f\"Running in Google Colab: {IN_COLAB}\")\n",
50
  "print(f\"Python version: {sys.version}\")\n",
51
- "print(f\"Working directory: {os.getcwd()}\")"
52
  ]
53
  },
54
  {
@@ -67,20 +70,21 @@
67
  "metadata": {},
68
  "outputs": [],
69
  "source": [
70
- "# You can override this in Colab before running this cell:\n",
71
- "# os.environ[\"EASYTRANSLATE_REPO_URL\"] = \"https://github.com/<org>/<repo>.git\"\n",
 
72
  "REPO_URL = os.environ.get(\n",
73
  " \"EASYTRANSLATE_REPO_URL\",\n",
74
  " \"https://huggingface.co/sdfjliom/UCAS-EasyTranslate\",\n",
75
  ")\n",
76
- "REPO_DIR = Path(\"/content/UCAS-EasyTranslate\") if IN_COLAB else Path(\".\").resolve()\n",
77
  "\n",
78
  "\n",
79
  "def _is_repo_root(path: Path) -> bool:\n",
80
  " return (path / \"src\" / \"easytranslate\").exists() and (path / \"setup.py\").exists()\n",
81
  "\n",
82
  "\n",
83
- "def _find_repo_root(start_path: Path) -> Path | None:\n",
84
  " p = start_path.resolve()\n",
85
  " for candidate in [p] + list(p.parents):\n",
86
  " if _is_repo_root(candidate):\n",
@@ -88,60 +92,59 @@
88
  " return None\n",
89
  "\n",
90
  "\n",
91
- "resolved_repo: Path | None = None\n",
92
  "\n",
93
  "if IN_COLAB:\n",
94
- " # 1) If already present, use it directly\n",
95
- " if _is_repo_root(REPO_DIR):\n",
96
- " resolved_repo = REPO_DIR\n",
97
  " else:\n",
98
- " # 2) Try clone (public repo expected). If private/auth fails, continue to fallbacks.\n",
99
  " try:\n",
100
  " subprocess.run(\n",
101
- " [\"git\", \"clone\", REPO_URL, str(REPO_DIR)],\n",
 
 
 
 
102
  " check=True,\n",
103
- " stdout=subprocess.PIPE,\n",
104
- " stderr=subprocess.STDOUT,\n",
105
- " text=True,\n",
106
  " )\n",
107
- " resolved_repo = REPO_DIR\n",
108
- " print(f\"Cloned repository to: {REPO_DIR}\")\n",
109
  " except subprocess.CalledProcessError as e:\n",
110
- " print(\"Clone failed. This usually means repo is private or URL is incorrect.\")\n",
111
- " print(\"Git output:\")\n",
112
- " print(e.stdout)\n",
113
  "\n",
114
- " # 3) Fallback: try to locate an existing copy (e.g., from Drive or current working dir)\n",
115
  " if resolved_repo is None:\n",
116
- " candidate_paths = [\n",
117
  " Path.cwd(),\n",
118
  " Path(\"/content\"),\n",
119
  " Path(\"/content/drive/MyDrive/UCAS-EasyTranslate\"),\n",
120
  " Path(\"/content/drive/MyDrive/Colab Notebooks/UCAS-EasyTranslate\"),\n",
121
- " ]\n",
122
- " for candidate in candidate_paths:\n",
123
  " root = _find_repo_root(candidate)\n",
124
- " if root is not None:\n",
125
  " resolved_repo = root\n",
126
- " print(f\"Using existing repository at: {resolved_repo}\")\n",
127
  " break\n",
128
  "\n",
129
  " if resolved_repo is None:\n",
130
  " raise FileNotFoundError(\n",
131
- " \"Cannot locate UCAS-EasyTranslate repository. \"\n",
132
- " \"Please do one of these: \"\n",
133
- " \"(1) set a public/correct EASYTRANSLATE_REPO_URL, \"\n",
134
- " \"(2) clone repo to /content/UCAS-EasyTranslate, \"\n",
135
- " \"(3) place repo in Google Drive and mount Drive.\"\n",
136
  " )\n",
137
  "else:\n",
138
- " # Local: use nearest project root from current path\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
- "os.chdir(resolved_repo)\n",
143
- "sys.path.insert(0, str(Path(resolved_repo) / \"src\"))\n",
144
- "print(f\"Repository directory: {resolved_repo}\")"
 
 
145
  ]
146
  },
147
  {
@@ -160,19 +163,33 @@
160
  "outputs": [],
161
  "source": [
162
  "if IN_COLAB:\n",
163
- " # Reuse Colab's preinstalled torch / numpy stack to avoid massive downgrades.\n",
 
164
  " %pip install -q --upgrade pip setuptools wheel\n",
165
- " %pip install -q -r requirements-colab.txt\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  " %pip install -q --no-deps -e .\n",
167
  "\n",
168
- " print(\"Dependencies installed successfully\")\n",
169
- " print(\"Using Colab's preinstalled torch/numpy stack to minimize dependency conflicts.\")\n",
170
  "else:\n",
171
  " %pip install -q --upgrade pip setuptools wheel\n",
172
  " %pip install -q -r requirements.txt\n",
173
  " %pip install -q -e .\n",
174
- "\n",
175
- " print(\"Dependencies installed successfully\")\n"
176
  ]
177
  },
178
  {
@@ -253,18 +270,11 @@
253
  "metadata": {},
254
  "outputs": [],
255
  "source": [
256
- "import importlib\n",
 
257
  "\n",
258
- "try:\n",
259
- " import numpy as np\n",
260
- " import pandas as pd\n",
261
- " print(f\"NumPy version: {np.__version__}\")\n",
262
- " print(f\"Pandas version: {pd.__version__}\")\n",
263
- "except Exception as exc:\n",
264
- " raise RuntimeError(\n",
265
- " \"Numeric Python stack is broken in this runtime. \"\n",
266
- " \"Rerun the dependency installation cell, then restart the kernel and run the notebook from the top.\"\n",
267
- " ) from exc\n",
268
  "\n",
269
  "from easytranslate.utils.config import load_config, config_to_dict\n",
270
  "from easytranslate.utils.seed import set_seed\n",
@@ -273,8 +283,8 @@
273
  "config = load_config(\"configs/default_config.yaml\")\n",
274
  "config_dict = config_to_dict(config)\n",
275
  "\n",
276
- "exp_cfg = config_dict.get(\"experiment\", {})\n",
277
- "seed = exp_cfg.get(\"seed\", 42)\n",
278
  "set_seed(seed)\n",
279
  "\n",
280
  "log_cfg = config_dict.get(\"logging\", {})\n",
@@ -283,19 +293,35 @@
283
  " log_file=\"easytranslate.log\",\n",
284
  ")\n",
285
  "\n",
286
- "# Colab overrides\n",
287
  "if IN_COLAB:\n",
288
- " if not torch.cuda.is_available():\n",
289
- " config_dict[\"training\"][\"fp16\"] = False\n",
290
- " config_dict[\"training\"][\"bf16\"] = False\n",
291
- " config_dict[\"training\"][\"epochs\"] = config_dict[\"training\"].get(\"epochs\", 30)\n",
292
- "\n",
293
- "print(f\"Configuration loaded successfully\")\n",
294
- "print(f\"Model type: {config_dict['model']['type']}\")\n",
295
- "print(f\"Tokenizer type: {config_dict['tokenizer']['type']}\")\n",
296
- "print(f\"Training epochs: {config_dict['training']['epochs']}\")\n",
297
- "print(f\"FP16: {config_dict['training']['fp16']}, BF16: {config_dict['training']['bf16']}\")\n",
298
- "print(f\"Random seed: {seed}\")\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  ]
300
  },
301
  {
@@ -323,15 +349,15 @@
323
  ")\n",
324
  "from torch.utils.data import DataLoader\n",
325
  "\n",
326
- "data_cfg = config_dict.get(\"data\", {})\n",
327
  "preproc_cfg = data_cfg.get(\"preprocessing\", {})\n",
328
- "loader_cfg = data_cfg.get(\"dataloader\", {})\n",
329
  "\n",
330
- "# Cap dataset size for Colab to avoid memory/time issues on large WMT corpora\n",
331
- "MAX_TRAIN_SAMPLES = 200_000 if IN_COLAB else None # set None to use full dataset\n",
332
  "MAX_VAL_SAMPLES = 5_000 if IN_COLAB else None\n",
333
  "\n",
334
- "print(\"Loading WMT19 zh-en dataset...\")\n",
335
  "raw_dataset = load_wmt_dataset(\n",
336
  " year=data_cfg.get(\"wmt\", {}).get(\"year\", \"19\"),\n",
337
  " language_pair=data_cfg.get(\"wmt\", {}).get(\"language_pair\", \"zh-en\"),\n",
@@ -340,32 +366,37 @@
340
  ")\n",
341
  "\n",
342
  "train_raw = raw_dataset[\"train\"]\n",
343
- "val_raw = raw_dataset.get(\"validation\", raw_dataset.get(\"dev\", train_raw))\n",
344
  "\n",
345
- "print(f\"Raw training samples: {len(train_raw['src'])}\")\n",
346
- "print(f\"Raw validation samples: {len(val_raw['src'])}\")\n",
 
 
 
 
 
 
 
 
347
  "\n",
348
- "# Apply sample cap before preprocessing to save time\n",
349
- "train_src_raw = train_raw[\"src\"][:MAX_TRAIN_SAMPLES] if MAX_TRAIN_SAMPLES else train_raw[\"src\"]\n",
350
- "train_tgt_raw = train_raw[\"tgt\"][:MAX_TRAIN_SAMPLES] if MAX_TRAIN_SAMPLES else train_raw[\"tgt\"]\n",
351
- "val_src_raw = val_raw[\"src\"][:MAX_VAL_SAMPLES] if MAX_VAL_SAMPLES else val_raw[\"src\"]\n",
352
- "val_tgt_raw = val_raw[\"tgt\"][:MAX_VAL_SAMPLES] if MAX_VAL_SAMPLES else val_raw[\"tgt\"]\n",
353
  "\n",
354
- "print(\"Preprocessing training data...\")\n",
355
  "train_src, train_tgt = preprocess_pipeline(\n",
356
- " train_src_raw,\n",
357
- " train_tgt_raw,\n",
358
  " max_src_len=preproc_cfg.get(\"max_src_len\", 256),\n",
359
  " max_tgt_len=preproc_cfg.get(\"max_tgt_len\", 256),\n",
360
  " filter_by_length_enabled=preproc_cfg.get(\"filter_by_length\", True),\n",
361
  " length_ratio_threshold=preproc_cfg.get(\"length_ratio_threshold\", 3.0),\n",
362
  ")\n",
363
- "print(f\"Preprocessed training samples: {len(train_src)}\")\n",
364
  "\n",
365
- "print(\"Preprocessing validation data...\")\n",
366
  "val_src, val_tgt = preprocess_pipeline(\n",
367
- " val_src_raw,\n",
368
- " val_tgt_raw,\n",
369
  " max_src_len=preproc_cfg.get(\"max_src_len\", 256),\n",
370
  " max_tgt_len=preproc_cfg.get(\"max_tgt_len\", 256),\n",
371
  " filter_by_length_enabled=preproc_cfg.get(\"filter_by_length\", True),\n",
@@ -440,19 +471,26 @@
440
  " label_pad_token_id=-100,\n",
441
  ")\n",
442
  "\n",
443
- "batch_size = loader_cfg.get(\"batch_size\", 32)\n",
444
- "# Use 2 workers on Colab; 0 on CPU-only to avoid multiprocessing overhead\n",
 
445
  "num_workers = 2 if IN_COLAB and torch.cuda.is_available() else 0\n",
446
- "use_dynamic = loader_cfg.get(\"dynamic_batching\", True)\n",
 
 
447
  "\n",
448
  "if use_dynamic:\n",
449
  " max_tokens = loader_cfg.get(\"max_tokens_per_batch\", 8192)\n",
450
- " print(\"Computing sequence lengths for dynamic batching...\")\n",
451
- " train_lengths = [\n",
452
- " max(len(tokenizer.encode(s, add_special_tokens=True)),\n",
453
- " len(tokenizer.encode(t, add_special_tokens=False)))\n",
454
- " for s, t in zip(train_src, train_tgt)\n",
455
- " ]\n",
 
 
 
 
456
  " train_sampler = DynamicBatchSampler(\n",
457
  " train_lengths,\n",
458
  " max_tokens_per_batch=max_tokens,\n",
@@ -466,6 +504,8 @@
466
  " pin_memory=torch.cuda.is_available(),\n",
467
  " )\n",
468
  "else:\n",
 
 
469
  " train_loader = DataLoader(\n",
470
  " train_dataset,\n",
471
  " batch_size=batch_size,\n",
@@ -484,11 +524,11 @@
484
  " pin_memory=torch.cuda.is_available(),\n",
485
  ")\n",
486
  "\n",
487
- "print(f\"Training batches: ~{len(train_loader)}\")\n",
488
  "print(f\"Validation batches: {len(val_loader)}\")\n",
489
  "\n",
490
  "sample_batch = next(iter(train_loader))\n",
491
- "print(f\"Sample batch shapes:\")\n",
492
  "for k, v in sample_batch.items():\n",
493
  " if isinstance(v, torch.Tensor):\n",
494
  " print(f\" {k}: {list(v.shape)}\")\n"
@@ -511,11 +551,25 @@
511
  "source": [
512
  "from easytranslate.model import TransformerTranslationModel\n",
513
  "\n",
514
- "model_cfg = config_dict.get(\"model\", {})\n",
515
  "model_type = model_cfg.get(\"type\", \"transformer_scratch\")\n",
516
  "\n",
517
  "if model_type == \"transformer_scratch\":\n",
518
- " tf_cfg = model_cfg.get(\"transformer\", {})\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
519
  " model = TransformerTranslationModel(\n",
520
  " src_vocab_size=tokenizer.vocab_size,\n",
521
  " tgt_vocab_size=tokenizer.vocab_size,\n",
@@ -533,7 +587,7 @@
533
  " pad_id=tokenizer.pad_token_id,\n",
534
  " share_embedding=False,\n",
535
  " )\n",
536
- " print(f\"Built Transformer from scratch\")\n",
537
  "\n",
538
  "elif model_type in (\"finetune_nllb\", \"finetune_mbart\"):\n",
539
  " from easytranslate.model.finetune import load_pretrained_model, setup_lora\n",
@@ -557,11 +611,11 @@
557
  "else:\n",
558
  " raise ValueError(f\"Unknown model type: {model_type}\")\n",
559
  "\n",
560
- "total_params = sum(p.numel() for p in model.parameters())\n",
561
  "trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
562
- "print(f\"Total parameters: {total_params:,}\")\n",
563
- "print(f\"Trainable parameters: {trainable_params:,}\")\n",
564
- "print(f\"Trainable ratio: {100 * trainable_params / total_params:.2f}%\")"
565
  ]
566
  },
567
  {
@@ -616,6 +670,15 @@
616
  "metadata": {},
617
  "outputs": [],
618
  "source": [
 
 
 
 
 
 
 
 
 
619
  "from easytranslate.training import Trainer\n",
620
  "from easytranslate.evaluation import Evaluator\n",
621
  "\n",
@@ -633,21 +696,21 @@
633
  " evaluator=evaluator,\n",
634
  ")\n",
635
  "\n",
636
- "# Unified output folders for artifacts and visualizations\n",
637
  "OUTPUT_DIR = REPO_DIR / \"outputs\"\n",
638
- "PLOTS_DIR = OUTPUT_DIR / \"plots\"\n",
639
  "OUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n",
640
  "PLOTS_DIR.mkdir(parents=True, exist_ok=True)\n",
641
  "\n",
642
  "print(\"Trainer initialized successfully\")\n",
643
- "print(f\"Device: {trainer.device}\")\n",
644
- "print(f\"FP16: {trainer.fp16}, BF16: {trainer.bf16}\")\n",
645
- "print(f\"Gradient accumulation steps: {trainer.gradient_accumulation_steps}\")\n",
646
- "print(f\"Number of epochs: {trainer.num_epochs}\")\n",
647
- "print(f\"Checkpoint directory: {trainer.checkpoint_dir}\")\n",
648
- "print(f\"Output directory: {OUTPUT_DIR}\")\n",
649
  "print()\n",
650
- "print(\"To start training, run the next cell.\")"
651
  ]
652
  },
653
  {
@@ -692,21 +755,20 @@
692
  "metadata": {},
693
  "outputs": [],
694
  "source": [
695
- "import json\n",
696
  "\n",
697
  "best_ckpt = trainer.checkpoint_dir / \"best_model.pt\"\n",
698
- "EVAL_RESULTS = {}\n",
699
  "\n",
700
  "if best_ckpt.exists():\n",
701
  " print(f\"Loading best model from {best_ckpt}\")\n",
702
- " checkpoint = torch.load(best_ckpt, map_location=device)\n",
703
  " model.load_state_dict(checkpoint[\"model_state_dict\"])\n",
704
  " model = model.to(device)\n",
705
  " model.eval()\n",
706
  "\n",
707
  " evaluator = Evaluator(model=model, tokenizer=tokenizer, config=config_dict)\n",
708
  "\n",
709
- " print(\"Running evaluation...\")\n",
710
  " EVAL_RESULTS = evaluator.evaluate(\n",
711
  " val_loader,\n",
712
  " src_texts=val_src,\n",
@@ -717,15 +779,15 @@
717
  " print(\" Evaluation Results\")\n",
718
  " print(\"=\" * 60)\n",
719
  " for metric, score in EVAL_RESULTS.items():\n",
720
- " if not isinstance(score, list):\n",
721
- " print(f\" {metric:>10s}: {score:.4f}\")\n",
722
  "\n",
723
  " eval_path = OUTPUT_DIR / \"evaluation_results.json\"\n",
724
  " with open(eval_path, \"w\", encoding=\"utf-8\") as f:\n",
725
  " json.dump(EVAL_RESULTS, f, indent=2, ensure_ascii=False)\n",
726
- " print(f\"Saved evaluation results to: {eval_path}\")\n",
727
  "else:\n",
728
- " print(\"No best model checkpoint found. Run training first.\")"
729
  ]
730
  },
731
  {
@@ -743,12 +805,11 @@
743
  "metadata": {},
744
  "outputs": [],
745
  "source": [
746
- "import json\n",
747
- "\n",
748
  "test_sentences = [\n",
749
  " \"Hello, how are you today?\",\n",
750
  " \"Machine translation is an important field of natural language processing.\",\n",
751
  " \"The weather is beautiful and I want to go for a walk.\",\n",
 
752
  "]\n",
753
  "\n",
754
  "TRANSLATION_RESULTS = []\n",
@@ -766,9 +827,9 @@
766
  " translation_path = OUTPUT_DIR / \"translation_examples.json\"\n",
767
  " with open(translation_path, \"w\", encoding=\"utf-8\") as f:\n",
768
  " json.dump(TRANSLATION_RESULTS, f, indent=2, ensure_ascii=False)\n",
769
- " print(f\"Saved translation examples to: {translation_path}\")\n",
770
  "else:\n",
771
- " print(\"No trained model available for translation demo.\")"
772
  ]
773
  },
774
  {
@@ -786,14 +847,16 @@
786
  "metadata": {},
787
  "outputs": [],
788
  "source": [
789
- "import shutil\n",
790
  "from easytranslate.utils.cloud_storage import sync_all_to_drive\n",
791
  "\n",
 
 
 
792
  "if IN_COLAB and DRIVE_MOUNTED:\n",
793
  " print(\"Syncing training artifacts to Google Drive...\")\n",
794
  " sync_results = sync_all_to_drive(\n",
795
  " checkpoint_dir=str(trainer.checkpoint_dir),\n",
796
- " log_dir=str(trainer.log_dir),\n",
797
  " drive_base_path=DRIVE_BASE,\n",
798
  " )\n",
799
  "\n",
@@ -807,24 +870,26 @@
807
  " ]:\n",
808
  " if artifact_file.exists():\n",
809
  " shutil.copy2(artifact_file, drive_outputs_dir / artifact_file.name)\n",
 
810
  "\n",
811
- " # Sync plot images if they exist\n",
812
  " if PLOTS_DIR.exists():\n",
813
  " drive_plots_dir = drive_outputs_dir / \"plots\"\n",
814
  " drive_plots_dir.mkdir(parents=True, exist_ok=True)\n",
815
  " for png_file in PLOTS_DIR.glob(\"*.png\"):\n",
816
  " shutil.copy2(png_file, drive_plots_dir / png_file.name)\n",
 
 
 
 
817
  "\n",
818
- " print(f\"Sync results: {sync_results}\")\n",
819
- " print(f\"Extra outputs synced to: {drive_outputs_dir}\")\n",
820
  "elif not IN_COLAB:\n",
821
- " print(f\"Running locally. Artifacts saved to:\")\n",
822
- " print(f\" Checkpoints: {trainer.checkpoint_dir}\")\n",
823
- " print(f\" Logs: {trainer.log_dir}\")\n",
824
- " print(f\" Outputs: {OUTPUT_DIR}\")\n",
825
  "else:\n",
826
  " print(\"Google Drive not mounted. Artifacts saved locally only.\")\n",
827
- " print(\"Re-run with Drive mount to persist results.\")"
828
  ]
829
  },
830
  {
@@ -842,10 +907,8 @@
842
  "metadata": {},
843
  "outputs": [],
844
  "source": [
845
- "import json\n",
846
- "\n",
847
- "summary_path = trainer.checkpoint_dir / \"training_summary.json\"\n",
848
  "TRAINING_SUMMARY = {}\n",
 
849
  "\n",
850
  "if summary_path.exists():\n",
851
  " with open(summary_path, \"r\", encoding=\"utf-8\") as f:\n",
@@ -854,27 +917,29 @@
854
  " print(\"=\" * 60)\n",
855
  " print(\" Training Summary\")\n",
856
  " print(\"=\" * 60)\n",
857
- " print(f\" Best epoch: {TRAINING_SUMMARY.get('best_epoch', 'N/A')}\")\n",
858
- " print(f\" Best metric ({TRAINING_SUMMARY.get('metric_name', 'N/A')}): {TRAINING_SUMMARY.get('best_metric', 'N/A')}\")\n",
859
- " print(f\" Total steps: {TRAINING_SUMMARY.get('total_steps', 'N/A')}\")\n",
 
860
  "\n",
861
- " if TRAINING_SUMMARY.get(\"train_loss_history\"):\n",
862
- " losses = TRAINING_SUMMARY[\"train_loss_history\"]\n",
863
  " print(f\" Initial loss: {losses[0]:.4f}\")\n",
864
- " print(f\" Final loss: {losses[-1]:.4f}\")\n",
865
- " print(f\" Loss reduction: {losses[0] - losses[-1]:.4f}\")\n",
866
  "\n",
 
867
  " report = {\n",
868
  " \"training_summary\": TRAINING_SUMMARY,\n",
869
- " \"evaluation_results\": EVAL_RESULTS if \"EVAL_RESULTS\" in globals() else {},\n",
870
- " \"translation_examples\": TRANSLATION_RESULTS if \"TRANSLATION_RESULTS\" in globals() else [],\n",
871
  " }\n",
872
  " report_path = OUTPUT_DIR / \"training_report.json\"\n",
873
  " with open(report_path, \"w\", encoding=\"utf-8\") as f:\n",
874
  " json.dump(report, f, indent=2, ensure_ascii=False)\n",
875
- " print(f\"Saved merged report to: {report_path}\")\n",
876
  "else:\n",
877
- " print(\"Training summary not yet available. Complete training first.\")"
878
  ]
879
  },
880
  {
 
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",
 
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
  {
 
70
  "metadata": {},
71
  "outputs": [],
72
  "source": [
73
+ "# ── 仓库地址配置 ─────────────────────────────────────────────────────────────\n",
74
+ "# 如需替换为你自己的仓库地址,在 Colab 中运行此单元前执行:\n",
75
+ "# import os; os.environ[\"EASYTRANSLATE_REPO_URL\"] = \"https://github.com/your-org/your-repo.git\"\n",
76
  "REPO_URL = os.environ.get(\n",
77
  " \"EASYTRANSLATE_REPO_URL\",\n",
78
  " \"https://huggingface.co/sdfjliom/UCAS-EasyTranslate\",\n",
79
  ")\n",
80
+ "_DEFAULT_COLAB_DIR = Path(\"/content/UCAS-EasyTranslate\")\n",
81
  "\n",
82
  "\n",
83
  "def _is_repo_root(path: Path) -> bool:\n",
84
  " return (path / \"src\" / \"easytranslate\").exists() and (path / \"setup.py\").exists()\n",
85
  "\n",
86
  "\n",
87
+ "def _find_repo_root(start_path: Path):\n",
88
  " p = start_path.resolve()\n",
89
  " for candidate in [p] + list(p.parents):\n",
90
  " if _is_repo_root(candidate):\n",
 
92
  " return None\n",
93
  "\n",
94
  "\n",
95
+ "resolved_repo = None\n",
96
  "\n",
97
  "if IN_COLAB:\n",
98
+ " if _is_repo_root(_DEFAULT_COLAB_DIR):\n",
99
+ " resolved_repo = _DEFAULT_COLAB_DIR\n",
 
100
  " else:\n",
101
+ " print(f\"Cloning repository from: {REPO_URL}\")\n",
102
  " try:\n",
103
  " subprocess.run(\n",
104
+ " [\"git\", \"lfs\", \"install\"], check=False,\n",
105
+ " stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n",
106
+ " )\n",
107
+ " subprocess.run(\n",
108
+ " [\"git\", \"clone\", \"--depth\", \"1\", REPO_URL, str(_DEFAULT_COLAB_DIR)],\n",
109
  " check=True,\n",
110
+ " stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,\n",
 
 
111
  " )\n",
112
+ " resolved_repo = _DEFAULT_COLAB_DIR\n",
113
+ " print(f\"Cloned to: {_DEFAULT_COLAB_DIR}\")\n",
114
  " except subprocess.CalledProcessError as e:\n",
115
+ " print(f\"Clone failed:\\n{e.stdout}\")\n",
 
 
116
  "\n",
117
+ " # Fallback: search Drive or current dir\n",
118
  " if resolved_repo is None:\n",
119
+ " for candidate in [\n",
120
  " Path.cwd(),\n",
121
  " Path(\"/content\"),\n",
122
  " Path(\"/content/drive/MyDrive/UCAS-EasyTranslate\"),\n",
123
  " Path(\"/content/drive/MyDrive/Colab Notebooks/UCAS-EasyTranslate\"),\n",
124
+ " ]:\n",
 
125
  " root = _find_repo_root(candidate)\n",
126
+ " if root:\n",
127
  " resolved_repo = root\n",
128
+ " print(f\"Found existing repo at: {resolved_repo}\")\n",
129
  " break\n",
130
  "\n",
131
  " if resolved_repo is None:\n",
132
  " raise FileNotFoundError(\n",
133
+ " \"Cannot locate UCAS-EasyTranslate repository.\\n\"\n",
134
+ " \"Options:\\n\"\n",
135
+ " \" (1) Set EASYTRANSLATE_REPO_URL to a publicly accessible git URL.\\n\"\n",
136
+ " \" (2) Manually clone to /content/UCAS-EasyTranslate.\\n\"\n",
137
+ " \" (3) Place repo in Google Drive and mount Drive first.\"\n",
138
  " )\n",
139
  "else:\n",
 
140
  " resolved_repo = _find_repo_root(Path.cwd()) or Path.cwd()\n",
141
  " print(f\"Using local repository at: {resolved_repo}\")\n",
142
  "\n",
143
+ "# Always keep REPO_DIR in sync with wherever the repo actually is\n",
144
+ "REPO_DIR = Path(resolved_repo)\n",
145
+ "os.chdir(REPO_DIR)\n",
146
+ "sys.path.insert(0, str(REPO_DIR / \"src\"))\n",
147
+ "print(f\"Repository directory: {REPO_DIR}\")\n"
148
  ]
149
  },
150
  {
 
163
  "outputs": [],
164
  "source": [
165
  "if IN_COLAB:\n",
166
+ " # 1. Only install packages that Colab does NOT ship.\n",
167
+ " # Do NOT touch torch/numpy/pandas — Colab's preinstalled versions are fine.\n",
168
  " %pip install -q --upgrade pip setuptools wheel\n",
169
+ "\n",
170
+ " # Core HuggingFace packages (Colab may have older versions)\n",
171
+ " %pip install -q \"transformers>=4.36.0,<4.45.0\" \\\n",
172
+ " \"datasets>=2.16.0,<3.0.0\" \\\n",
173
+ " \"tokenizers>=0.15.0,<0.20.0\" \\\n",
174
+ " \"sentencepiece>=0.2.0\" \\\n",
175
+ " \"accelerate>=0.25.0,<0.35.0\" \\\n",
176
+ " \"peft>=0.7.0,<0.12.0\"\n",
177
+ "\n",
178
+ " # Evaluation / config packages\n",
179
+ " %pip install -q \"sacrebleu>=2.4.0\" \\\n",
180
+ " \"omegaconf>=2.3.0,<3.0.0\" \\\n",
181
+ " \"rich>=13.0.0\"\n",
182
+ "\n",
183
+ " # 2. Install project code without re-resolving heavy dependencies.\n",
184
+ " # (torch, numpy, etc. are already present from Colab runtime.)\n",
185
  " %pip install -q --no-deps -e .\n",
186
  "\n",
187
+ " print(\"All packages installed successfully.\")\n",
 
188
  "else:\n",
189
  " %pip install -q --upgrade pip setuptools wheel\n",
190
  " %pip install -q -r requirements.txt\n",
191
  " %pip install -q -e .\n",
192
+ " print(\"Dependencies installed successfully.\")\n"
 
193
  ]
194
  },
195
  {
 
270
  "metadata": {},
271
  "outputs": [],
272
  "source": [
273
+ "import torch\n",
274
+ "import numpy as np\n",
275
  "\n",
276
+ "print(f\"NumPy version : {np.__version__}\")\n",
277
+ "print(f\"PyTorch version: {torch.__version__}\")\n",
 
 
 
 
 
 
 
 
278
  "\n",
279
  "from easytranslate.utils.config import load_config, config_to_dict\n",
280
  "from easytranslate.utils.seed import set_seed\n",
 
283
  "config = load_config(\"configs/default_config.yaml\")\n",
284
  "config_dict = config_to_dict(config)\n",
285
  "\n",
286
+ "exp_cfg = config_dict.get(\"experiment\", {})\n",
287
+ "seed = exp_cfg.get(\"seed\", 42)\n",
288
  "set_seed(seed)\n",
289
  "\n",
290
  "log_cfg = config_dict.get(\"logging\", {})\n",
 
293
  " log_file=\"easytranslate.log\",\n",
294
  ")\n",
295
  "\n",
296
+ "# ── Colab runtime overrides ───────────────────────────────────────────────────\n",
297
  "if IN_COLAB:\n",
298
+ " train_cfg = config_dict.setdefault(\"training\", {})\n",
299
+ "\n",
300
+ " # Mixed precision: use bf16 on Ampere+ GPUs, fp16 otherwise, none on CPU\n",
301
+ " if torch.cuda.is_available():\n",
302
+ " gpu_cap = torch.cuda.get_device_capability(0)\n",
303
+ " if gpu_cap[0] >= 8: # A100/A10 → bf16\n",
304
+ " train_cfg[\"fp16\"] = False\n",
305
+ " train_cfg[\"bf16\"] = True\n",
306
+ " else: # T4/P100/V100 → fp16\n",
307
+ " train_cfg[\"fp16\"] = True\n",
308
+ " train_cfg[\"bf16\"] = False\n",
309
+ " else:\n",
310
+ " train_cfg[\"fp16\"] = False\n",
311
+ " train_cfg[\"bf16\"] = False\n",
312
+ "\n",
313
+ " # Reduce epochs for a Colab demo run\n",
314
+ " train_cfg.setdefault(\"epochs\", 10)\n",
315
+ "\n",
316
+ " # Colab-friendly batch / gradient accumulation settings\n",
317
+ " train_cfg.setdefault(\"batch_size\", 32)\n",
318
+ " train_cfg.setdefault(\"gradient_accumulation_steps\", 4)\n",
319
+ "\n",
320
+ "print(f\"Configuration loaded. Experiment seed: {seed}\")\n",
321
+ "print(f\"Model type : {config_dict['model']['type']}\")\n",
322
+ "print(f\"Training : epochs={config_dict['training']['epochs']}, \"\n",
323
+ " f\"fp16={config_dict['training']['fp16']}, \"\n",
324
+ " f\"bf16={config_dict['training']['bf16']}\")\n"
325
  ]
326
  },
327
  {
 
349
  ")\n",
350
  "from torch.utils.data import DataLoader\n",
351
  "\n",
352
+ "data_cfg = config_dict.get(\"data\", {})\n",
353
  "preproc_cfg = data_cfg.get(\"preprocessing\", {})\n",
354
+ "loader_cfg = data_cfg.get(\"dataloader\", {})\n",
355
  "\n",
356
+ "# ── Cap dataset sizes for Colab to avoid RAM/time issues ─────────────────────\n",
357
+ "MAX_TRAIN_SAMPLES = 200_000 if IN_COLAB else None # None use full dataset\n",
358
  "MAX_VAL_SAMPLES = 5_000 if IN_COLAB else None\n",
359
  "\n",
360
+ "print(\"Loading WMT19 zh-en dataset (this may take several minutes on first run)...\")\n",
361
  "raw_dataset = load_wmt_dataset(\n",
362
  " year=data_cfg.get(\"wmt\", {}).get(\"year\", \"19\"),\n",
363
  " language_pair=data_cfg.get(\"wmt\", {}).get(\"language_pair\", \"zh-en\"),\n",
 
366
  ")\n",
367
  "\n",
368
  "train_raw = raw_dataset[\"train\"]\n",
 
369
  "\n",
370
+ "# Prefer \"validation\", fall back to \"dev\", then use a small slice of train\n",
371
+ "_val_split_name = next(\n",
372
+ " (k for k in (\"validation\", \"dev\", \"valid\") if k in raw_dataset),\n",
373
+ " None,\n",
374
+ ")\n",
375
+ "val_raw = raw_dataset[_val_split_name] if _val_split_name else train_raw\n",
376
+ "\n",
377
+ "print(f\"Raw training samples : {len(train_raw['src'])}\")\n",
378
+ "print(f\"Raw validation samples: {len(val_raw['src'])} \"\n",
379
+ " f\"(split='{_val_split_name or 'train (fallback)'}')\")\n",
380
  "\n",
381
+ "# Apply sample caps BEFORE preprocessing to save time\n",
382
+ "train_src_raw = train_raw[\"src\"][:MAX_TRAIN_SAMPLES] if MAX_TRAIN_SAMPLES else list(train_raw[\"src\"])\n",
383
+ "train_tgt_raw = train_raw[\"tgt\"][:MAX_TRAIN_SAMPLES] if MAX_TRAIN_SAMPLES else list(train_raw[\"tgt\"])\n",
384
+ "val_src_raw = val_raw[\"src\"][:MAX_VAL_SAMPLES] if MAX_VAL_SAMPLES else list(val_raw[\"src\"])\n",
385
+ "val_tgt_raw = val_raw[\"tgt\"][:MAX_VAL_SAMPLES] if MAX_VAL_SAMPLES else list(val_raw[\"tgt\"])\n",
386
  "\n",
387
+ "print(f\"\\nPreprocessing training data ({len(train_src_raw)} samples)...\")\n",
388
  "train_src, train_tgt = preprocess_pipeline(\n",
389
+ " train_src_raw, train_tgt_raw,\n",
 
390
  " max_src_len=preproc_cfg.get(\"max_src_len\", 256),\n",
391
  " max_tgt_len=preproc_cfg.get(\"max_tgt_len\", 256),\n",
392
  " filter_by_length_enabled=preproc_cfg.get(\"filter_by_length\", True),\n",
393
  " length_ratio_threshold=preproc_cfg.get(\"length_ratio_threshold\", 3.0),\n",
394
  ")\n",
395
+ "print(f\"Preprocessed training samples : {len(train_src)}\")\n",
396
  "\n",
397
+ "print(f\"Preprocessing validation data ({len(val_src_raw)} samples)...\")\n",
398
  "val_src, val_tgt = preprocess_pipeline(\n",
399
+ " val_src_raw, val_tgt_raw,\n",
 
400
  " max_src_len=preproc_cfg.get(\"max_src_len\", 256),\n",
401
  " max_tgt_len=preproc_cfg.get(\"max_tgt_len\", 256),\n",
402
  " filter_by_length_enabled=preproc_cfg.get(\"filter_by_length\", True),\n",
 
471
  " label_pad_token_id=-100,\n",
472
  ")\n",
473
  "\n",
474
+ "loader_cfg = config_dict.get(\"data\", {}).get(\"dataloader\", {})\n",
475
+ "batch_size = loader_cfg.get(\"batch_size\", 32)\n",
476
+ "# 2 workers on GPU Colab; 0 on CPU (no multiprocessing overhead)\n",
477
  "num_workers = 2 if IN_COLAB and torch.cuda.is_available() else 0\n",
478
+ "# Disable dynamic batching on Colab: computing exact token lengths for 200k\n",
479
+ "# sentences requires a full tokenizer pass and can take 30+ minutes.\n",
480
+ "use_dynamic = loader_cfg.get(\"dynamic_batching\", True) and not IN_COLAB\n",
481
  "\n",
482
  "if use_dynamic:\n",
483
  " max_tokens = loader_cfg.get(\"max_tokens_per_batch\", 8192)\n",
484
+ " print(\"Computing sequence lengths for dynamic batching (approximate)...\")\n",
485
+ "\n",
486
+ " def _approx_len(src_text: str, tgt_text: str) -> int:\n",
487
+ " \"\"\"Fast character-based length estimate — no tokenizer call needed.\"\"\"\n",
488
+ " def _tok_est(t: str) -> int:\n",
489
+ " cjk = sum(1 for c in t if \"\\u4e00\" <= c <= \"\\u9fff\")\n",
490
+ " return (len(t) - cjk) // 4 + cjk + 2 # rough BPE estimate\n",
491
+ " return max(_tok_est(src_text), _tok_est(tgt_text))\n",
492
+ "\n",
493
+ " train_lengths = [_approx_len(s, t) for s, t in zip(train_src, train_tgt)]\n",
494
  " train_sampler = DynamicBatchSampler(\n",
495
  " train_lengths,\n",
496
  " max_tokens_per_batch=max_tokens,\n",
 
504
  " pin_memory=torch.cuda.is_available(),\n",
505
  " )\n",
506
  "else:\n",
507
+ " if IN_COLAB and loader_cfg.get(\"dynamic_batching\", True):\n",
508
+ " print(\"Note: dynamic batching disabled on Colab (would require full tokenizer pass on all samples).\")\n",
509
  " train_loader = DataLoader(\n",
510
  " train_dataset,\n",
511
  " batch_size=batch_size,\n",
 
524
  " pin_memory=torch.cuda.is_available(),\n",
525
  ")\n",
526
  "\n",
527
+ "print(f\"Training batches : ~{len(train_loader)}\")\n",
528
  "print(f\"Validation batches: {len(val_loader)}\")\n",
529
  "\n",
530
  "sample_batch = next(iter(train_loader))\n",
531
+ "print(\"Sample batch shapes:\")\n",
532
  "for k, v in sample_batch.items():\n",
533
  " if isinstance(v, torch.Tensor):\n",
534
  " print(f\" {k}: {list(v.shape)}\")\n"
 
551
  "source": [
552
  "from easytranslate.model import TransformerTranslationModel\n",
553
  "\n",
554
+ "model_cfg = config_dict.get(\"model\", {})\n",
555
  "model_type = model_cfg.get(\"type\", \"transformer_scratch\")\n",
556
  "\n",
557
  "if model_type == \"transformer_scratch\":\n",
558
+ " tf_cfg = dict(model_cfg.get(\"transformer\", {}))\n",
559
+ "\n",
560
+ " # ── Colab quick-run: smaller model to fit in Colab RAM/VRAM ──────────────\n",
561
+ " # Default full model: d_model=512, 6 enc/dec layers (~75 M params)\n",
562
+ " # Colab quick model: d_model=256, 3 enc/dec layers (~12 M params)\n",
563
+ " # Set COLAB_FULL_MODEL=1 in env to skip this override.\n",
564
+ " if IN_COLAB and not os.environ.get(\"COLAB_FULL_MODEL\"):\n",
565
+ " tf_cfg.setdefault(\"d_model\", 256)\n",
566
+ " tf_cfg.setdefault(\"nhead\", 4)\n",
567
+ " tf_cfg.setdefault(\"num_encoder_layers\", 3)\n",
568
+ " tf_cfg.setdefault(\"num_decoder_layers\", 3)\n",
569
+ " tf_cfg.setdefault(\"dim_feedforward\", 1024)\n",
570
+ " print(\"Colab mode: using compact model (d_model=256, 3 layers).\")\n",
571
+ " print(\"To use the full model, run: import os; os.environ['COLAB_FULL_MODEL']='1'\")\n",
572
+ "\n",
573
  " model = TransformerTranslationModel(\n",
574
  " src_vocab_size=tokenizer.vocab_size,\n",
575
  " tgt_vocab_size=tokenizer.vocab_size,\n",
 
587
  " pad_id=tokenizer.pad_token_id,\n",
588
  " share_embedding=False,\n",
589
  " )\n",
590
+ " print(\"Built Transformer from scratch\")\n",
591
  "\n",
592
  "elif model_type in (\"finetune_nllb\", \"finetune_mbart\"):\n",
593
  " from easytranslate.model.finetune import load_pretrained_model, setup_lora\n",
 
611
  "else:\n",
612
  " raise ValueError(f\"Unknown model type: {model_type}\")\n",
613
  "\n",
614
+ "total_params = sum(p.numel() for p in model.parameters())\n",
615
  "trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
616
+ "print(f\"Total parameters : {total_params:,}\")\n",
617
+ "print(f\"Trainable parameters : {trainable_params:,}\")\n",
618
+ "print(f\"Trainable ratio : {100 * trainable_params / total_params:.2f}%\")\n"
619
  ]
620
  },
621
  {
 
670
  "metadata": {},
671
  "outputs": [],
672
  "source": [
673
+ "# sacrebleu is now installed in the dependency cell above.\n",
674
+ "# This cell just verifies it is importable before we initialize the Trainer.\n",
675
+ "try:\n",
676
+ " importlib.import_module(\"sacrebleu\")\n",
677
+ " print(\"sacrebleu OK\")\n",
678
+ "except ImportError:\n",
679
+ " print(\"sacrebleu missing — installing now...\")\n",
680
+ " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"sacrebleu>=2.4.0\"], check=True)\n",
681
+ "\n",
682
  "from easytranslate.training import Trainer\n",
683
  "from easytranslate.evaluation import Evaluator\n",
684
  "\n",
 
696
  " evaluator=evaluator,\n",
697
  ")\n",
698
  "\n",
699
+ "# Output / plot directories live inside the repo\n",
700
  "OUTPUT_DIR = REPO_DIR / \"outputs\"\n",
701
+ "PLOTS_DIR = OUTPUT_DIR / \"plots\"\n",
702
  "OUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n",
703
  "PLOTS_DIR.mkdir(parents=True, exist_ok=True)\n",
704
  "\n",
705
  "print(\"Trainer initialized successfully\")\n",
706
+ "print(f\" Device : {trainer.device}\")\n",
707
+ "print(f\" FP16 / BF16 : {trainer.fp16} / {trainer.bf16}\")\n",
708
+ "print(f\" Gradient accumulation steps: {trainer.gradient_accumulation_steps}\")\n",
709
+ "print(f\" Number of epochs : {trainer.num_epochs}\")\n",
710
+ "print(f\" Checkpoint directory : {trainer.checkpoint_dir}\")\n",
711
+ "print(f\" Output directory : {OUTPUT_DIR}\")\n",
712
  "print()\n",
713
+ "print(\">>> Run the NEXT cell to start training.\")\n"
714
  ]
715
  },
716
  {
 
755
  "metadata": {},
756
  "outputs": [],
757
  "source": [
758
+ "EVAL_RESULTS = {}\n",
759
  "\n",
760
  "best_ckpt = trainer.checkpoint_dir / \"best_model.pt\"\n",
 
761
  "\n",
762
  "if best_ckpt.exists():\n",
763
  " print(f\"Loading best model from {best_ckpt}\")\n",
764
+ " checkpoint = torch.load(best_ckpt, map_location=device, weights_only=True)\n",
765
  " model.load_state_dict(checkpoint[\"model_state_dict\"])\n",
766
  " model = model.to(device)\n",
767
  " model.eval()\n",
768
  "\n",
769
  " evaluator = Evaluator(model=model, tokenizer=tokenizer, config=config_dict)\n",
770
  "\n",
771
+ " print(\"Running evaluation on validation set...\")\n",
772
  " EVAL_RESULTS = evaluator.evaluate(\n",
773
  " val_loader,\n",
774
  " src_texts=val_src,\n",
 
779
  " print(\" Evaluation Results\")\n",
780
  " print(\"=\" * 60)\n",
781
  " for metric, score in EVAL_RESULTS.items():\n",
782
+ " if isinstance(score, (int, float)):\n",
783
+ " print(f\" {metric:>12s}: {score:.4f}\")\n",
784
  "\n",
785
  " eval_path = OUTPUT_DIR / \"evaluation_results.json\"\n",
786
  " with open(eval_path, \"w\", encoding=\"utf-8\") as f:\n",
787
  " json.dump(EVAL_RESULTS, f, indent=2, ensure_ascii=False)\n",
788
+ " print(f\"\\nSaved evaluation results {eval_path}\")\n",
789
  "else:\n",
790
+ " print(\"No best_model.pt found. Run training first (Cell 13).\")\n"
791
  ]
792
  },
793
  {
 
805
  "metadata": {},
806
  "outputs": [],
807
  "source": [
 
 
808
  "test_sentences = [\n",
809
  " \"Hello, how are you today?\",\n",
810
  " \"Machine translation is an important field of natural language processing.\",\n",
811
  " \"The weather is beautiful and I want to go for a walk.\",\n",
812
+ " \"Deep learning has revolutionized artificial intelligence research.\",\n",
813
  "]\n",
814
  "\n",
815
  "TRANSLATION_RESULTS = []\n",
 
827
  " translation_path = OUTPUT_DIR / \"translation_examples.json\"\n",
828
  " with open(translation_path, \"w\", encoding=\"utf-8\") as f:\n",
829
  " json.dump(TRANSLATION_RESULTS, f, indent=2, ensure_ascii=False)\n",
830
+ " print(f\"Saved translation examples {translation_path}\")\n",
831
  "else:\n",
832
+ " print(\"No trained model checkpoint available. Run training first (Cell 13).\")\n"
833
  ]
834
  },
835
  {
 
847
  "metadata": {},
848
  "outputs": [],
849
  "source": [
 
850
  "from easytranslate.utils.cloud_storage import sync_all_to_drive\n",
851
  "\n",
852
+ "# Use trainer.log_dir if available, otherwise fall back to a sensible default\n",
853
+ "_log_dir = getattr(trainer, \"log_dir\", REPO_DIR / \"logs\")\n",
854
+ "\n",
855
  "if IN_COLAB and DRIVE_MOUNTED:\n",
856
  " print(\"Syncing training artifacts to Google Drive...\")\n",
857
  " sync_results = sync_all_to_drive(\n",
858
  " checkpoint_dir=str(trainer.checkpoint_dir),\n",
859
+ " log_dir=str(_log_dir),\n",
860
  " drive_base_path=DRIVE_BASE,\n",
861
  " )\n",
862
  "\n",
 
870
  " ]:\n",
871
  " if artifact_file.exists():\n",
872
  " shutil.copy2(artifact_file, drive_outputs_dir / artifact_file.name)\n",
873
+ " print(f\" Copied {artifact_file.name} → Drive\")\n",
874
  "\n",
 
875
  " if PLOTS_DIR.exists():\n",
876
  " drive_plots_dir = drive_outputs_dir / \"plots\"\n",
877
  " drive_plots_dir.mkdir(parents=True, exist_ok=True)\n",
878
  " for png_file in PLOTS_DIR.glob(\"*.png\"):\n",
879
  " shutil.copy2(png_file, drive_plots_dir / png_file.name)\n",
880
+ " print(f\" Copied plot {png_file.name} → Drive\")\n",
881
+ "\n",
882
+ " print(f\"\\nSync complete. Drive base: {DRIVE_BASE}\")\n",
883
+ " print(f\"Sync details: {sync_results}\")\n",
884
  "\n",
 
 
885
  "elif not IN_COLAB:\n",
886
+ " print(\"Running locally. Artifacts are already on disk:\")\n",
887
+ " print(f\" Checkpoints : {trainer.checkpoint_dir}\")\n",
888
+ " print(f\" Logs : {_log_dir}\")\n",
889
+ " print(f\" Outputs : {OUTPUT_DIR}\")\n",
890
  "else:\n",
891
  " print(\"Google Drive not mounted. Artifacts saved locally only.\")\n",
892
+ " print(\"Mount Drive (Cell 5) and re-run this cell to sync results.\")\n"
893
  ]
894
  },
895
  {
 
907
  "metadata": {},
908
  "outputs": [],
909
  "source": [
 
 
 
910
  "TRAINING_SUMMARY = {}\n",
911
+ "summary_path = trainer.checkpoint_dir / \"training_summary.json\"\n",
912
  "\n",
913
  "if summary_path.exists():\n",
914
  " with open(summary_path, \"r\", encoding=\"utf-8\") as f:\n",
 
917
  " print(\"=\" * 60)\n",
918
  " print(\" Training Summary\")\n",
919
  " print(\"=\" * 60)\n",
920
+ " print(f\" Best epoch : {TRAINING_SUMMARY.get('best_epoch', 'N/A')}\")\n",
921
+ " print(f\" Best metric : {TRAINING_SUMMARY.get('metric_name', 'N/A')} = \"\n",
922
+ " f\"{TRAINING_SUMMARY.get('best_metric', 'N/A')}\")\n",
923
+ " print(f\" Total steps : {TRAINING_SUMMARY.get('total_steps', 'N/A')}\")\n",
924
  "\n",
925
+ " losses = TRAINING_SUMMARY.get(\"train_loss_history\", [])\n",
926
+ " if losses:\n",
927
  " print(f\" Initial loss: {losses[0]:.4f}\")\n",
928
+ " print(f\" Final loss : {losses[-1]:.4f}\")\n",
929
+ " print(f\" Reduction : {losses[0] - losses[-1]:.4f}\")\n",
930
  "\n",
931
+ " # Merge all results into one report file\n",
932
  " report = {\n",
933
  " \"training_summary\": TRAINING_SUMMARY,\n",
934
+ " \"evaluation_results\": EVAL_RESULTS,\n",
935
+ " \"translation_examples\": TRANSLATION_RESULTS,\n",
936
  " }\n",
937
  " report_path = OUTPUT_DIR / \"training_report.json\"\n",
938
  " with open(report_path, \"w\", encoding=\"utf-8\") as f:\n",
939
  " json.dump(report, f, indent=2, ensure_ascii=False)\n",
940
+ " print(f\"\\nMerged report saved {report_path}\")\n",
941
  "else:\n",
942
+ " print(\"Training summary not yet available. Complete training first.\")\n"
943
  ]
944
  },
945
  {