Aswini-Kumar commited on
Commit
f096486
Β·
1 Parent(s): 46f0850

Clean final training notebook: no demo, no Drive, all steps including plots + eval + download

Browse files
Files changed (2) hide show
  1. plot_rewards.py +32 -9
  2. train_colab.ipynb +196 -385
plot_rewards.py CHANGED
@@ -98,10 +98,16 @@ def load_log(log_path: str) -> pd.DataFrame:
98
 
99
  df.sort_values("episode", inplace=True)
100
  df.reset_index(drop=True, inplace=True)
101
- print(f"[plot_rewards] Loaded {len(df)} episodes from {log_path}")
 
102
  return df
103
 
104
 
 
 
 
 
 
105
  # ── Plots ─────────────────────────────────────────────────────────────────────
106
 
107
  def plot_reward_curve(df: pd.DataFrame, out_dir: Path, window: int = 20):
@@ -235,16 +241,33 @@ def plot_all(log_path: str = "logs/training.jsonl", out_dir: str = "plots/",
235
  out = Path(out_dir)
236
  out.mkdir(parents=True, exist_ok=True)
237
 
238
- plot_reward_curve(df, out, window)
239
- plot_success_rate(df, out, window)
240
- plot_accuracy_gain(df, out, window)
 
 
 
241
  plot_curriculum(df, out)
242
 
243
- print(f"\n[plot_rewards] All plots saved to {out}/")
244
- print(f" Episodes: {len(df)} | "
245
- f"Avg reward: {df['reward'].mean():.3f} | "
246
- f"Success rate: {df['success'].mean():.1%} | "
247
- f"Max level reached: {int(df['level'].max())}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
 
249
 
250
  if __name__ == "__main__":
 
98
 
99
  df.sort_values("episode", inplace=True)
100
  df.reset_index(drop=True, inplace=True)
101
+ n = len(df)
102
+ print(f"[plot_rewards] Loaded {n} episodes from {log_path}")
103
  return df
104
 
105
 
106
+ def _adaptive_window(df: pd.DataFrame, requested: int) -> int:
107
+ """Use min(requested, len/3) so plots are never flat lines with few data points."""
108
+ return max(3, min(requested, len(df) // 3))
109
+
110
+
111
  # ── Plots ─────────────────────────────────────────────────────────────────────
112
 
113
  def plot_reward_curve(df: pd.DataFrame, out_dir: Path, window: int = 20):
 
241
  out = Path(out_dir)
242
  out.mkdir(parents=True, exist_ok=True)
243
 
244
+ # Adaptive window β€” avoid flat lines with small datasets
245
+ w = _adaptive_window(df, window)
246
+
247
+ plot_reward_curve(df, out, w)
248
+ plot_success_rate(df, out, w)
249
+ plot_accuracy_gain(df, out, w)
250
  plot_curriculum(df, out)
251
 
252
+ # ── Print summary stats ───────────────────────────────────────────────────
253
+ n = len(df)
254
+ avg_r = df["reward"].mean()
255
+ max_r = df["reward"].max()
256
+ min_r = df["reward"].min()
257
+ succ = df["success"].mean()
258
+ max_lvl = int(df["level"].max())
259
+ lvl_names = {0: "tutorial", 1: "easy", 2: "medium", 3: "hard"}
260
+
261
+ print(f"\n{'='*50}")
262
+ print(f" TRAINING SUMMARY ({n} episodes)")
263
+ print(f"{'='*50}")
264
+ print(f" Avg reward : {avg_r:+.3f}")
265
+ print(f" Min / Max : {min_r:+.3f} / {max_r:+.3f}")
266
+ print(f" Success rate : {succ:.1%}")
267
+ print(f" Max level : {max_lvl} ({lvl_names.get(max_lvl, '?')})")
268
+ print(f" Window used : {w} episodes")
269
+ print(f"{'='*50}")
270
+ print(f"\n Plots saved to: {out}/")
271
 
272
 
273
  if __name__ == "__main__":
train_colab.ipynb CHANGED
@@ -3,8 +3,7 @@
3
  "nbformat_minor": 5,
4
  "metadata": {
5
  "colab": {
6
- "provenance": [],
7
- "gpuType": "T4"
8
  },
9
  "kernelspec": {
10
  "display_name": "Python 3",
@@ -18,306 +17,153 @@
18
  "cells": [
19
  {
20
  "cell_type": "markdown",
21
- "id": "a0",
22
  "metadata": {},
23
  "source": [
24
- "# \ud83e\udde0 Data-Centric AI Agent \u2014 Training Notebook\n",
25
- "**OpenEnv Hackathon 2026** \u00b7 [Space](https://huggingface.co/spaces/Aswinis-Kumar/data-centric-env) \u00b7 [GitHub](https://github.com/CelestialWorthyOfHeavenAndEarth/data-centric-env)\n\n",
26
- "Trains **Qwen2.5-3B-Instruct** (4-bit, LoRA) to orchestrate data-cleaning specialist agents via RL.\n\n",
27
- "| Phase | What happens | Time |\n",
28
- "|-------|-------------|------|\n",
29
- "| SFT warmup | Teaches valid command grammar on ~9k examples | ~20 min |\n",
30
- "| GRPO training | Learns strategy via live environment rollouts | ~3\u20135 hrs |\n",
31
- "| Eval + plots | Reward curves, baseline comparison | ~5 min |\n\n",
32
- "**Runtime required:** T4 GPU \u2192 `Runtime \u2192 Change runtime type \u2192 T4 GPU`\n\n",
33
- "> **Quick demo mode:** Set `DEMO_MODE = True` in cell 1 to run a tiny 10-step version in ~10 min."
34
- ]
35
- },
36
- {
37
- "cell_type": "markdown",
38
- "id": "a1",
39
- "metadata": {},
40
- "source": [
41
- "## \u2699\ufe0f Step 1 \u2014 Configuration"
42
- ]
43
- },
44
- {
45
- "cell_type": "code",
46
- "execution_count": null,
47
- "id": "b0_config",
48
- "metadata": {},
49
- "outputs": [],
50
- "source": [
51
- "# \u2500\u2500\u2500 Set DEMO_MODE=True for a quick 10-step judge verification run (~5 min)\n",
52
- "DEMO_MODE = False # \u2190 Change to True for quick verification\n",
53
  "\n",
54
- "REPO = 'https://github.com/CelestialWorthyOfHeavenAndEarth/data-centric-env.git'\n",
55
- "HF_SPACE = 'https://aswinis-kumar-data-centric-env.hf.space'\n",
56
- "\n",
57
- "# Model: 1.5B trains 3x faster than 3B on T4, still very capable\n",
58
- "# Change to '3B-Instruct' if you want more capacity but slower runs\n",
59
- "MODEL_NAME = 'Qwen/Qwen2.5-1.5B-Instruct'\n",
60
- "\n",
61
- "if DEMO_MODE:\n",
62
- " print('\ud83d\ude80 DEMO MODE \u2014 quick 10-step run for judge verification')\n",
63
- " SFT_EPOCHS = 1\n",
64
- " GRPO_STEPS = 10\n",
65
- " GRPO_BATCH = 2\n",
66
- " GRPO_GENS = 2\n",
67
- "else:\n",
68
- " print('\ud83d\udd2c FULL TRAINING MODE')\n",
69
- " SFT_EPOCHS = 1\n",
70
- " GRPO_STEPS = 150 # ~45-60 min on T4 with 1.5B. Run 2-3x for best results!\n",
71
- " GRPO_BATCH = 2\n",
72
- " GRPO_GENS = 2\n",
73
  "\n",
74
- "import os\n",
75
- "os.environ['MODEL_NAME'] = MODEL_NAME\n",
76
- "\n",
77
- "print(f' Model : {MODEL_NAME}')\n",
78
- "print(f' SFT epochs : {SFT_EPOCHS}')\n",
79
- "print(f' GRPO steps : {GRPO_STEPS}')\n",
80
- "print(f' Batch/Gens : {GRPO_BATCH}/{GRPO_GENS}')\n",
81
- "print()\n",
82
- "print('TIP: Run 2-3 training runs of 150 steps each rather than one long run!')\n",
83
- "print(' Multiple short runs = better learning curves for judges to see.')"
 
 
 
 
 
 
 
 
 
84
  ]
85
  },
86
  {
87
  "cell_type": "markdown",
88
- "id": "a1b",
89
  "metadata": {},
90
  "source": [
91
- "## \ud83d\udce6 Step 2 \u2014 Install Dependencies (~5 min)"
92
  ]
93
  },
94
  {
95
  "cell_type": "code",
96
  "execution_count": null,
97
- "id": "b1",
98
  "metadata": {},
99
  "outputs": [],
100
  "source": [
101
- "# Step 1: Pin transformers to a version compatible with Colab's torchao\n",
102
- "# (transformers >= 4.52 pulls torchao >= 0.9 which requires torch.int1 \u2014 not in Colab yet)\n",
103
  "!pip install \"transformers>=4.44.0,<4.52.0\" --quiet\n",
104
  "\n",
105
- "# Step 2: Install Unsloth for the current Colab torch version\n",
106
  "!pip install \"unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git\" --quiet\n",
107
  "\n",
108
- "# Step 3: Install training + env + tracking dependencies\n",
109
  "!pip install trl>=0.15.0 datasets>=2.0.0 accelerate>=0.30.0 --quiet\n",
110
  "!pip install scikit-learn>=1.3.0 pandas>=2.0.0 numpy matplotlib --quiet\n",
111
  "!pip install \"openenv-core[core]>=0.2.1\" --quiet\n",
112
- "# TensorBoard is already included in PyTorch/HF Trainer \u2014 no extra install needed\n",
113
- "\n",
114
- "# Step 4: Verify Unsloth loads correctly\n",
115
- "try:\n",
116
- " from unsloth import FastLanguageModel\n",
117
- " print('\u2705 Unsloth loaded successfully')\n",
118
- "except Exception as e:\n",
119
- " print(f'\u274c Unsloth import failed: {e}')\n",
120
- " print(' Try: Runtime \u2192 Factory reset runtime, then re-run')\n",
121
- " raise"
122
- ]
123
- },
124
- {
125
- "cell_type": "markdown",
126
- "id": "a2",
127
- "metadata": {},
128
- "source": [
129
- "## \ud83d\udcbe Step 3 \u2014 Mount Google Drive (optional, recommended)\n",
130
- "Checkpoints save every 50 GRPO steps to Drive. If Colab disconnects, you can resume.\n\n",
131
- "**Skip this cell if you don't want Drive integration.**"
132
- ]
133
- },
134
- {
135
- "cell_type": "code",
136
- "execution_count": null,
137
- "id": "b2",
138
- "metadata": {},
139
- "outputs": [],
140
- "source": [
141
- "from google.colab import drive\n",
142
- "drive.mount('/content/drive')\n",
143
- "print('Drive mounted')"
144
  ]
145
  },
146
  {
147
  "cell_type": "markdown",
148
- "id": "a3",
149
  "metadata": {},
150
  "source": [
151
- "## \ud83d\udd01 Step 4 \u2014 Clone / Update Repo + Setup"
152
  ]
153
  },
154
  {
155
  "cell_type": "code",
156
  "execution_count": null,
157
- "id": "b3",
158
  "metadata": {},
159
  "outputs": [],
160
  "source": [
161
- "import os, sys, shutil\n",
162
  "\n",
 
163
  "WORK_DIR = '/content/data-centric-env'\n",
164
  "\n",
165
- "# Clone or pull latest\n",
166
  "if os.path.exists(f'{WORK_DIR}/pyproject.toml'):\n",
167
- " print('Repo exists, pulling latest...')\n",
168
- " !git -C {WORK_DIR} pull origin main\n",
169
  "else:\n",
170
- " if os.path.exists(WORK_DIR):\n",
171
- " shutil.rmtree(WORK_DIR)\n",
172
- " !git clone {REPO} {WORK_DIR}\n",
173
  "\n",
174
  "os.chdir(WORK_DIR)\n",
175
  "sys.path.insert(0, WORK_DIR)\n",
 
 
176
  "print('CWD:', os.getcwd())\n",
177
- "!git log --oneline -3\n",
178
- "\n",
179
- "# Install as editable package (fixes relative imports)\n",
180
- "!pip install -e . --quiet 2>/dev/null || echo 'pip install -e . skipped'\n",
181
- "\n",
182
- "# Symlink output dirs to Drive if mounted\n",
183
- "DRIVE_DIR = '/content/drive/MyDrive/data-centric-training'\n",
184
- "if os.path.exists('/content/drive/MyDrive'):\n",
185
- " os.makedirs(DRIVE_DIR, exist_ok=True)\n",
186
- " for d in ['data-centric-checkpoints', 'data-centric-adapter', 'logs', 'plots']:\n",
187
- " local = os.path.join(WORK_DIR, d)\n",
188
- " remote = os.path.join(DRIVE_DIR, d)\n",
189
- " os.makedirs(remote, exist_ok=True)\n",
190
- " if not os.path.exists(local):\n",
191
- " os.symlink(remote, local)\n",
192
- " print(f' Drive link: {d}')\n",
193
- " else:\n",
194
- " print(f' Exists: {d}')\n",
195
- " print('Checkpoints will save to Drive')\n",
196
- "else:\n",
197
- " for d in ['logs', 'plots']:\n",
198
- " os.makedirs(os.path.join(WORK_DIR, d), exist_ok=True)\n",
199
- " print('Drive not mounted \u2014 checkpoints save locally only')\n",
200
- "\n",
201
- "print('Setup complete')"
202
- ]
203
- },
204
- {
205
- "cell_type": "markdown",
206
- "id": "a35",
207
- "metadata": {},
208
- "source": [
209
- "## \u2705 Step 5 \u2014 Verify All Features Work\n",
210
- "Quick smoke test: confirms the environment, all specialist agents, and the rubric system are connected."
211
- ]
212
- },
213
- {
214
- "cell_type": "code",
215
- "execution_count": null,
216
- "id": "b35",
217
- "metadata": {},
218
- "outputs": [],
219
- "source": [
220
- "import os, sys\n",
221
- "os.chdir('/content/data-centric-env')\n",
222
- "sys.path.insert(0, '/content/data-centric-env')\n",
223
- "\n",
224
- "from models import DataCentricAction\n",
225
- "from server.data_centric_environment import DataCentricEnvironment\n",
226
- "\n",
227
- "env = DataCentricEnvironment()\n",
228
- "obs = env.reset(task='task_1_easy', seed=42)\n",
229
- "\n",
230
- "checks = {}\n",
231
- "\n",
232
- "# 1. query_analyst\n",
233
- "obs = env.step(DataCentricAction(message='query_analyst'))\n",
234
- "checks['query_analyst'] = 'DIAGNOSIS' in obs.response and 'RECOMMENDED PLAN' in obs.response\n",
235
- "\n",
236
- "# 2. Smarter specialists\n",
237
- "obs = env.step(DataCentricAction(message='query_cleaner'))\n",
238
- "checks['smarter_specialists'] = any(k in obs.response for k in ['skew', 'Risk:', 'Reason:', '%'])\n",
239
- "\n",
240
- "# 3. Drift detection (apply then check)\n",
241
- "obs = env.step(DataCentricAction(message='apply 1'))\n",
242
- "checks['drift_detection'] = 'drift' in obs.response.lower() or obs.response != ''\n",
243
- "\n",
244
- "# 4+5. Dual classifier + feature importance\n",
245
- "obs = env.step(DataCentricAction(message='validate'))\n",
246
- "checks['dual_classifier'] = 'RF Accuracy' in obs.response or 'accuracy' in obs.response.lower()\n",
247
- "checks['feature_importance'] = 'Feature importance' in obs.response or 'feature' in obs.response.lower()\n",
248
- "\n",
249
- "print('\\n=== Feature Smoke Test ===')\n",
250
- "all_pass = True\n",
251
- "for name, passed in checks.items():\n",
252
- " status = '\u2705 PASS' if passed else '\u274c FAIL'\n",
253
- " if not passed: all_pass = False\n",
254
- " print(f' {status}: {name}')\n",
255
- "\n",
256
- "if all_pass:\n",
257
- " print('\\n\u2705 All features verified \u2014 ready to train!')\n",
258
- "else:\n",
259
- " print('\\n\u26a0\ufe0f Some checks failed \u2014 check git pull ran correctly')"
260
- ]
261
- },
262
- {
263
- "cell_type": "markdown",
264
- "id": "a4",
265
- "metadata": {},
266
- "source": [
267
- "## \ud83d\udda5\ufe0f Step 6 \u2014 Start Environment Server"
268
  ]
269
  },
270
  {
271
  "cell_type": "code",
272
  "execution_count": null,
273
- "id": "b4",
274
  "metadata": {},
275
  "outputs": [],
276
  "source": [
277
- "import subprocess, time, requests, os, sys\n",
278
- "os.chdir('/content/data-centric-env')\n",
279
- "sys.path.insert(0, '/content/data-centric-env')\n",
280
  "\n",
281
- "# Kill any existing server on port 8000\n",
282
- "!fuser -k 8000/tcp 2>/dev/null || true\n",
283
  "time.sleep(2)\n",
284
  "\n",
285
- "server_proc = subprocess.Popen(\n",
286
  " ['python', '-m', 'uvicorn', 'server.app:app',\n",
287
  " '--host', '0.0.0.0', '--port', '8000', '--log-level', 'warning'],\n",
288
  " stdout=subprocess.PIPE, stderr=subprocess.PIPE\n",
289
  ")\n",
290
  "\n",
291
- "for i in range(30):\n",
292
  " try:\n",
293
- " if requests.get('http://localhost:8000/health', timeout=2).status_code == 200:\n",
294
- " print(f'\u2705 Server ready after {i+1}s')\n",
 
295
  " break\n",
296
- " except:\n",
297
- " time.sleep(1)\n",
298
  "else:\n",
299
- " server_proc.terminate()\n",
300
- " out, err = server_proc.communicate()\n",
301
  " print('STDOUT:', out.decode()[-2000:])\n",
302
  " print('STDERR:', err.decode()[-2000:])\n",
303
- " raise RuntimeError('Server failed to start in 30s \u2014 see output above')\n",
304
  "\n",
305
- "print('ENV_URL: http://localhost:8000')\n",
306
- "os.environ['ENV_URL'] = 'http://localhost:8000'"
307
  ]
308
  },
309
  {
310
  "cell_type": "markdown",
311
- "id": "a5",
312
  "metadata": {},
313
  "source": [
314
- "## \ud83d\udcca Step 7 \u2014 Generate SFT Warmup Data"
315
  ]
316
  },
317
  {
318
  "cell_type": "code",
319
  "execution_count": null,
320
- "id": "b5",
321
  "metadata": {},
322
  "outputs": [],
323
  "source": [
@@ -328,26 +174,26 @@
328
  "if os.path.exists(sft_path):\n",
329
  " count = sum(1 for _ in open(sft_path))\n",
330
  " age = (time.time() - os.path.getmtime(sft_path)) / 60\n",
331
- " print(f'sft_data.jsonl: {count} examples, {age:.0f} min old \u2014 reusing')\n",
332
  "else:\n",
333
- " print('Generating SFT data...')\n",
334
- " !python sft_generator.py\n",
335
  " count = sum(1 for _ in open(sft_path))\n",
336
- " print(f'Generated {count} examples')"
337
  ]
338
  },
339
  {
340
  "cell_type": "markdown",
341
- "id": "a6",
342
  "metadata": {},
343
  "source": [
344
- "## \ud83e\udd16 Step 8 \u2014 Load Model (Qwen2.5-3B-Instruct, 4-bit)"
345
  ]
346
  },
347
  {
348
  "cell_type": "code",
349
  "execution_count": null,
350
- "id": "b6",
351
  "metadata": {},
352
  "outputs": [],
353
  "source": [
@@ -356,47 +202,33 @@
356
  "sys.path.insert(0, '/content/data-centric-env')\n",
357
  "\n",
358
  "from unsloth import FastLanguageModel\n",
 
359
  "\n",
360
- "MAX_SEQ_LENGTH = 512 # Short commands; saves significant VRAM\n",
361
- "\n",
362
- "model, tokenizer = FastLanguageModel.from_pretrained(\n",
363
- " model_name=MODEL_NAME,\n",
364
- " max_seq_length=MAX_SEQ_LENGTH,\n",
365
- " load_in_4bit=True, # QLoRA \u2014 mandatory for T4\n",
366
- " dtype=None,\n",
367
- ")\n",
368
- "\n",
369
- "model = FastLanguageModel.get_peft_model(\n",
370
- " model,\n",
371
- " r=8, # r=8 is sufficient for short command vocab\n",
372
- " target_modules=['q_proj', 'v_proj', 'k_proj', 'o_proj'],\n",
373
- " lora_alpha=16, # = r, standard rule-of-thumb\n",
374
- " lora_dropout=0,\n",
375
- " bias='none',\n",
376
- " use_gradient_checkpointing='unsloth',\n",
377
- " random_state=42,\n",
378
- ")\n",
379
  "\n",
380
- "vram = torch.cuda.memory_allocated()/1e9\n",
381
- "total = torch.cuda.get_device_properties(0).total_memory/1e9\n",
382
- "print(f'\u2705 Model loaded: {MODEL_NAME}')\n",
383
- "print(f' VRAM: {vram:.1f} GB / {total:.1f} GB ({100*vram/total:.0f}% used)')\n",
384
- "print(f' LoRA trainable params: ~1.8M (r=8)')"
385
  ]
386
  },
387
  {
388
  "cell_type": "markdown",
389
- "id": "a7",
390
  "metadata": {},
391
  "source": [
392
- "## \ud83c\udf93 Step 9 \u2014 Phase 1: SFT Warmup\n",
393
- "Teaches the model to output valid commands before RL starts. Without this, the model outputs free-form text and gets zero reward."
 
 
 
 
 
394
  ]
395
  },
396
  {
397
  "cell_type": "code",
398
  "execution_count": null,
399
- "id": "b7",
400
  "metadata": {},
401
  "outputs": [],
402
  "source": [
@@ -406,232 +238,211 @@
406
  "\n",
407
  "from train_data_centric import run_sft_warmup\n",
408
  "\n",
409
- "# In demo mode: reduce to 1 pass of 200 examples for speed\n",
410
- "if DEMO_MODE:\n",
411
- " import json\n",
412
- " raw = [json.loads(l) for l in open('sft_data.jsonl')][:200]\n",
413
- " with open('sft_data_demo.jsonl', 'w') as f:\n",
414
- " for r in raw: f.write(json.dumps(r) + '\\n')\n",
415
- " import shutil\n",
416
- " shutil.copy('sft_data.jsonl', 'sft_data_full.jsonl')\n",
417
- " shutil.copy('sft_data_demo.jsonl', 'sft_data.jsonl')\n",
418
- " print('Demo mode: using 200 SFT examples')\n",
419
- "\n",
420
  "model = run_sft_warmup(model, tokenizer)\n",
421
- "print('\u2705 SFT warmup complete')\n",
422
- "\n",
423
- "# Restore full data if demo\n",
424
- "if DEMO_MODE and os.path.exists('sft_data_full.jsonl'):\n",
425
- " import shutil\n",
426
- " shutil.copy('sft_data_full.jsonl', 'sft_data.jsonl')"
427
  ]
428
  },
429
  {
430
  "cell_type": "markdown",
431
- "id": "a8",
432
  "metadata": {},
433
  "source": [
434
- "## \ud83c\udfcb\ufe0f Step 10 \u2014 Phase 2: GRPO Training\n",
435
- "Agent learns *when* to use each command via reinforcement learning against the live environment.\n\n",
436
- "- **Full mode:** ~300 steps, 3\u20135 hrs on T4. Checkpoints every 50 steps.\n",
437
- "- **Demo mode:** 10 steps, ~10 min. Enough to verify the training loop works.\n\n",
438
- "If Colab disconnects, re-run all cells \u2014 it auto-resumes from the latest checkpoint."
439
  ]
440
  },
441
  {
442
  "cell_type": "code",
443
  "execution_count": null,
444
- "id": "b8",
445
  "metadata": {},
446
  "outputs": [],
447
  "source": [
448
- "import os, sys, glob\n",
449
- "os.chdir('/content/data-centric-env')\n",
450
- "sys.path.insert(0, '/content/data-centric-env')\n",
451
- "\n",
452
- "from train_data_centric import run_grpo_training\n",
453
- "\n",
454
- "# Auto-detect checkpoint to resume from\n",
455
- "ckpt_dir = './data-centric-checkpoints'\n",
456
- "resume_from = None\n",
457
- "if os.path.exists(ckpt_dir):\n",
458
- " checkpoints = sorted(glob.glob(f'{ckpt_dir}/checkpoint-*'))\n",
459
- " if checkpoints:\n",
460
- " resume_from = checkpoints[-1]\n",
461
- " print(f'Resuming from: {resume_from}')\n",
462
- "\n",
463
- "# max_steps > 0 limits the run (used for demo mode)\n",
464
- "# max_steps = -1 means run full num_train_epochs\n",
465
- "demo_steps = GRPO_STEPS if DEMO_MODE else -1\n",
466
- "\n",
467
- "model = run_grpo_training(\n",
468
- " model, tokenizer,\n",
469
- " resume_from_checkpoint=resume_from,\n",
470
- " max_steps=demo_steps,\n",
471
- ")\n",
472
- "print('\u2705 GRPO training complete')"
473
  ]
474
  },
475
  {
476
  "cell_type": "markdown",
477
- "id": "a9",
478
  "metadata": {},
479
  "source": [
480
- "## \ud83d\udcbd Step 11 \u2014 Save Trained Model"
 
 
 
 
 
 
 
 
 
 
 
 
481
  ]
482
  },
483
  {
484
  "cell_type": "code",
485
  "execution_count": null,
486
- "id": "b9",
487
  "metadata": {},
488
  "outputs": [],
489
  "source": [
490
- "import os, sys\n",
491
  "os.chdir('/content/data-centric-env')\n",
492
  "sys.path.insert(0, '/content/data-centric-env')\n",
493
  "\n",
494
- "from train_data_centric import save_model\n",
495
- "save_model(model, tokenizer)\n",
496
- "print('\u2705 Model saved to ./data-centric-adapter/')"
497
- ]
498
- },
499
- {
500
- "cell_type": "markdown",
501
- "id": "a_tb",
502
- "metadata": {},
503
- "source": [
504
- "## \ud83d\udcca Step 10.5 \u2014 Launch TensorBoard (Experiment Tracking)\n",
505
- "View live loss and reward curves while training is running.\n",
506
- "Both SFT and GRPO write logs automatically \u2014 no API key required."
507
- ]
508
- },
509
- {
510
- "cell_type": "code",
511
- "execution_count": null,
512
- "id": "b_tb",
513
- "metadata": {},
514
- "outputs": [],
515
- "source": [
516
- "# Load TensorBoard extension (built into Colab/PyTorch)\n",
517
- "%load_ext tensorboard\n",
518
- "\n",
519
- "# View SFT logs (run after Step 9)\n",
520
- "# %tensorboard --logdir /content/data-centric-env/logs/sft\n",
521
  "\n",
522
- "# View GRPO logs (run after Step 10 starts)\n",
523
- "%tensorboard --logdir /content/data-centric-env/logs/grpo\n",
 
 
 
 
 
 
524
  "\n",
525
- "print(\"TensorBoard launched above \u2014 refresh as training progresses\")"
 
526
  ]
527
  },
528
  {
529
  "cell_type": "markdown",
530
- "id": "a10",
531
  "metadata": {},
532
  "source": [
533
- "## \ud83d\udcc8 Step 12 \u2014 Plot Reward Curves"
 
 
 
 
 
 
534
  ]
535
  },
536
  {
537
  "cell_type": "code",
538
  "execution_count": null,
539
- "id": "b10",
540
  "metadata": {},
541
  "outputs": [],
542
  "source": [
543
  "import os\n",
544
  "os.chdir('/content/data-centric-env')\n",
545
  "\n",
546
- "if os.path.exists('logs/training.jsonl'):\n",
547
- " lines = sum(1 for _ in open('logs/training.jsonl'))\n",
548
- " print(f'Training log: {lines} episodes recorded')\n",
549
- " !python plot_rewards.py --log logs/training.jsonl --out plots/\n",
 
 
 
 
 
 
550
  " from IPython.display import Image, display\n",
551
- " for f in ['reward_curve.png', 'success_rate.png', 'accuracy_gain.png', 'curriculum.png']:\n",
552
- " path = f'plots/{f}'\n",
553
  " if os.path.exists(path):\n",
554
- " print(f'\\n--- {f} ---')\n",
555
- " display(Image(path))\n",
556
- "else:\n",
557
- " print('No training log yet \u2014 run Step 10 first')"
558
  ]
559
  },
560
  {
561
  "cell_type": "markdown",
562
- "id": "a11",
563
  "metadata": {},
564
  "source": [
565
- "## \ud83c\udfc6 Step 13 \u2014 Evaluate: Trained Agent vs Random vs Heuristic"
 
 
 
 
 
566
  ]
567
  },
568
  {
569
  "cell_type": "code",
570
  "execution_count": null,
571
- "id": "b11",
572
  "metadata": {},
573
  "outputs": [],
574
  "source": [
575
  "import os, json\n",
576
  "os.chdir('/content/data-centric-env')\n",
577
  "\n",
578
- "!python eval_data_centric.py\n",
579
  "\n",
580
  "if os.path.exists('eval_results.json'):\n",
581
  " with open('eval_results.json') as f:\n",
582
  " results = json.load(f)\n",
583
- " print('\\n=== Eval Results ===')\n",
584
  " for k, v in results.items():\n",
585
- " print(f' {k}: {v}')"
586
  ]
587
  },
588
  {
589
  "cell_type": "markdown",
590
- "id": "a12",
591
  "metadata": {},
592
  "source": [
593
- "## \ud83d\udce4 Step 14 \u2014 Push Results to GitHub\n",
594
- "Run this **only after training + eval are done**."
595
  ]
596
  },
597
  {
598
  "cell_type": "code",
599
  "execution_count": null,
600
- "id": "b12",
601
  "metadata": {},
602
  "outputs": [],
603
  "source": [
604
- "import os\n",
605
  "os.chdir('/content/data-centric-env')\n",
 
606
  "\n",
607
- "from getpass import getpass\n",
608
- "token = getpass('GitHub token (repo write access): ')\n",
609
- "repo_url = f'https://{token}@github.com/CelestialWorthyOfHeavenAndEarth/data-centric-env.git'\n",
610
- "\n",
611
- "!git config user.email 'training@colab.run'\n",
612
- "!git config user.name 'Colab Training'\n",
613
- "!git remote set-url origin {repo_url}\n",
614
- "!git add plots/ logs/ eval_results.json 2>/dev/null || true\n",
615
- "!git commit -m 'Add training results: reward curves + eval [Colab]'\n",
616
- "!git push origin main\n",
617
- "print('\u2705 Results pushed to GitHub')"
618
  ]
619
  },
620
  {
621
  "cell_type": "markdown",
622
- "id": "a13",
623
  "metadata": {},
624
  "source": [
625
- "---\n",
626
- "## \u2705 Done!\n\n",
627
- "| Output | Location |\n",
628
- "|--------|----------|\n",
629
- "| Trained LoRA adapter | `./data-centric-adapter/` |\n",
630
- "| Training log | `logs/training.jsonl` |\n",
631
- "| Reward curves | `plots/*.png` |\n",
632
- "| Eval results | `eval_results.json` |\n\n",
633
- "**Links:** \n",
634
- "\ud83e\udd17 [HF Space](https://huggingface.co/spaces/Aswinis-Kumar/data-centric-env) \u00b7 [Blog Post](https://huggingface.co/blog/Aswinis-Kumar/data-centric-ai-agent) \u00b7 [GitHub](https://github.com/CelestialWorthyOfHeavenAndEarth/data-centric-env)"
 
 
 
 
 
 
 
 
 
 
 
635
  ]
636
  }
637
  ]
 
3
  "nbformat_minor": 5,
4
  "metadata": {
5
  "colab": {
6
+ "provenance": []
 
7
  },
8
  "kernelspec": {
9
  "display_name": "Python 3",
 
17
  "cells": [
18
  {
19
  "cell_type": "markdown",
20
+ "id": "hdr",
21
  "metadata": {},
22
  "source": [
23
+ "# 🧠 Data-Centric AI Agent β€” Training Notebook\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  "\n",
25
+ "**OpenEnv Hackathon 2026**\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  "\n",
27
+ "Trains **Qwen2.5-1.5B-Instruct** (4-bit QLoRA) to orchestrate data-cleaning specialists\n",
28
+ "via GRPO reinforcement learning on the Data-Centric AI environment.\n",
29
+ "\n",
30
+ "| | |\n",
31
+ "|---|---|\n",
32
+ "| **Space** | https://huggingface.co/spaces/Aswinis-Kumar/data-centric-env |\n",
33
+ "| **Repo** | https://github.com/CelestialWorthyOfHeavenAndEarth/data-centric-env |\n",
34
+ "| **Algorithm** | SFT warmup β†’ GRPO (TRL) |\n",
35
+ "| **Model** | Qwen/Qwen2.5-1.5B-Instruct, 4-bit QLoRA r=8 |\n",
36
+ "\n",
37
+ "**Pipeline:**\n",
38
+ "1. Install deps\n",
39
+ "2. Clone repo + start env server\n",
40
+ "3. Generate SFT warmup data\n",
41
+ "4. Phase 1 β€” SFT warmup (~15 min)\n",
42
+ "5. Phase 2 β€” GRPO training (~45-90 min depending on hardware)\n",
43
+ "6. Plot reward curves\n",
44
+ "7. Evaluate vs baselines\n",
45
+ "8. Save model\n"
46
  ]
47
  },
48
  {
49
  "cell_type": "markdown",
50
+ "id": "s1_md",
51
  "metadata": {},
52
  "source": [
53
+ "## πŸ“¦ Step 1 β€” Install Dependencies"
54
  ]
55
  },
56
  {
57
  "cell_type": "code",
58
  "execution_count": null,
59
+ "id": "s1",
60
  "metadata": {},
61
  "outputs": [],
62
  "source": [
63
+ "# Pin transformers first to avoid torchao conflict\n",
64
+ "# (transformers β‰₯ 4.52 requires torchao β‰₯ 0.9 which needs torch.int1 β€” not in Colab yet)\n",
65
  "!pip install \"transformers>=4.44.0,<4.52.0\" --quiet\n",
66
  "\n",
67
+ "# Unsloth β€” detects the installed torch version automatically\n",
68
  "!pip install \"unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git\" --quiet\n",
69
  "\n",
70
+ "# Training stack\n",
71
  "!pip install trl>=0.15.0 datasets>=2.0.0 accelerate>=0.30.0 --quiet\n",
72
  "!pip install scikit-learn>=1.3.0 pandas>=2.0.0 numpy matplotlib --quiet\n",
73
  "!pip install \"openenv-core[core]>=0.2.1\" --quiet\n",
74
+ "\n",
75
+ "# Verify Unsloth\n",
76
+ "from unsloth import FastLanguageModel\n",
77
+ "import torch\n",
78
+ "print(f'βœ… Unsloth ready | torch {torch.__version__} | GPU: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"CPU\"}')\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  ]
80
  },
81
  {
82
  "cell_type": "markdown",
83
+ "id": "s2_md",
84
  "metadata": {},
85
  "source": [
86
+ "## πŸ” Step 2 β€” Clone Repo & Start Environment Server"
87
  ]
88
  },
89
  {
90
  "cell_type": "code",
91
  "execution_count": null,
92
+ "id": "s2",
93
  "metadata": {},
94
  "outputs": [],
95
  "source": [
96
+ "import os, sys, shutil, subprocess, time, requests\n",
97
  "\n",
98
+ "REPO = 'https://github.com/CelestialWorthyOfHeavenAndEarth/data-centric-env.git'\n",
99
  "WORK_DIR = '/content/data-centric-env'\n",
100
  "\n",
101
+ "# Clone or update\n",
102
  "if os.path.exists(f'{WORK_DIR}/pyproject.toml'):\n",
103
+ " print('Repo exists β€” pulling latest...')\n",
104
+ " os.system(f'git -C {WORK_DIR} pull origin main')\n",
105
  "else:\n",
106
+ " if os.path.exists(WORK_DIR): shutil.rmtree(WORK_DIR)\n",
107
+ " os.system(f'git clone {REPO} {WORK_DIR}')\n",
 
108
  "\n",
109
  "os.chdir(WORK_DIR)\n",
110
  "sys.path.insert(0, WORK_DIR)\n",
111
+ "os.makedirs('logs', exist_ok=True)\n",
112
+ "os.makedirs('plots', exist_ok=True)\n",
113
  "print('CWD:', os.getcwd())\n",
114
+ "os.system('git log --oneline -3')\n",
115
+ "os.system('pip install -e . --quiet 2>/dev/null || true')\n",
116
+ "print('βœ… Repo ready')\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  ]
118
  },
119
  {
120
  "cell_type": "code",
121
  "execution_count": null,
122
+ "id": "s2b",
123
  "metadata": {},
124
  "outputs": [],
125
  "source": [
126
+ "import subprocess, time, requests, os\n",
 
 
127
  "\n",
128
+ "# Kill any leftover server\n",
129
+ "os.system('fuser -k 8000/tcp 2>/dev/null || true')\n",
130
  "time.sleep(2)\n",
131
  "\n",
132
+ "server = subprocess.Popen(\n",
133
  " ['python', '-m', 'uvicorn', 'server.app:app',\n",
134
  " '--host', '0.0.0.0', '--port', '8000', '--log-level', 'warning'],\n",
135
  " stdout=subprocess.PIPE, stderr=subprocess.PIPE\n",
136
  ")\n",
137
  "\n",
138
+ "for i in range(40):\n",
139
  " try:\n",
140
+ " r = requests.get('http://localhost:8000/health', timeout=2)\n",
141
+ " if r.status_code == 200:\n",
142
+ " print(f'βœ… Server ready after {i+1}s β€” {r.json()}')\n",
143
  " break\n",
144
+ " except: time.sleep(1)\n",
 
145
  "else:\n",
146
+ " out, err = server.communicate(timeout=5)\n",
 
147
  " print('STDOUT:', out.decode()[-2000:])\n",
148
  " print('STDERR:', err.decode()[-2000:])\n",
149
+ " raise RuntimeError('Server failed to start')\n",
150
  "\n",
151
+ "os.environ['ENV_URL'] = 'http://localhost:8000'\n",
152
+ "print('ENV_URL set to http://localhost:8000')\n"
153
  ]
154
  },
155
  {
156
  "cell_type": "markdown",
157
+ "id": "s3_md",
158
  "metadata": {},
159
  "source": [
160
+ "## πŸ“Š Step 3 β€” Generate SFT Warmup Data"
161
  ]
162
  },
163
  {
164
  "cell_type": "code",
165
  "execution_count": null,
166
+ "id": "s3",
167
  "metadata": {},
168
  "outputs": [],
169
  "source": [
 
174
  "if os.path.exists(sft_path):\n",
175
  " count = sum(1 for _ in open(sft_path))\n",
176
  " age = (time.time() - os.path.getmtime(sft_path)) / 60\n",
177
+ " print(f'βœ… sft_data.jsonl: {count} examples ({age:.0f} min old) β€” reusing')\n",
178
  "else:\n",
179
+ " print('Generating SFT warmup data (~2 min)...')\n",
180
+ " os.system('python sft_generator.py')\n",
181
  " count = sum(1 for _ in open(sft_path))\n",
182
+ " print(f'βœ… Generated {count} SFT examples')\n"
183
  ]
184
  },
185
  {
186
  "cell_type": "markdown",
187
+ "id": "s4_md",
188
  "metadata": {},
189
  "source": [
190
+ "## πŸ€– Step 4 β€” Load Qwen2.5-1.5B (4-bit QLoRA)"
191
  ]
192
  },
193
  {
194
  "cell_type": "code",
195
  "execution_count": null,
196
+ "id": "s4",
197
  "metadata": {},
198
  "outputs": [],
199
  "source": [
 
202
  "sys.path.insert(0, '/content/data-centric-env')\n",
203
  "\n",
204
  "from unsloth import FastLanguageModel\n",
205
+ "from train_data_centric import load_model\n",
206
  "\n",
207
+ "model, tokenizer = load_model()\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  "\n",
209
+ "vram = torch.cuda.memory_allocated() / 1e9\n",
210
+ "total = torch.cuda.get_device_properties(0).total_memory / 1e9\n",
211
+ "print(f'\\nπŸ“Š VRAM: {vram:.1f} GB used / {total:.1f} GB total ({100*vram/total:.0f}%)')\n"
 
 
212
  ]
213
  },
214
  {
215
  "cell_type": "markdown",
216
+ "id": "s5_md",
217
  "metadata": {},
218
  "source": [
219
+ "## πŸŽ“ Step 5 β€” Phase 1: SFT Warmup\n",
220
+ "\n",
221
+ "One epoch of supervised fine-tuning on ~9,480 heuristic trajectories.\n",
222
+ "Teaches the model valid command syntax before RL starts.\n",
223
+ "Without this, the model outputs gibberish and gets zero reward.\n",
224
+ "\n",
225
+ "**TensorBoard:** Run Step 6 to open the live dashboard.\n"
226
  ]
227
  },
228
  {
229
  "cell_type": "code",
230
  "execution_count": null,
231
+ "id": "s5",
232
  "metadata": {},
233
  "outputs": [],
234
  "source": [
 
238
  "\n",
239
  "from train_data_centric import run_sft_warmup\n",
240
  "\n",
 
 
 
 
 
 
 
 
 
 
 
241
  "model = run_sft_warmup(model, tokenizer)\n",
242
+ "print('βœ… SFT warmup complete β€” model knows command syntax')\n"
 
 
 
 
 
243
  ]
244
  },
245
  {
246
  "cell_type": "markdown",
247
+ "id": "s6_md",
248
  "metadata": {},
249
  "source": [
250
+ "## πŸ“‘ Step 6 β€” TensorBoard (Experiment Tracking)\n",
251
+ "\n",
252
+ "Launch this now β€” it will show SFT logs immediately and GRPO logs as training progresses.\n",
253
+ "Refresh the TensorBoard tab every few minutes during Step 7.\n"
 
254
  ]
255
  },
256
  {
257
  "cell_type": "code",
258
  "execution_count": null,
259
+ "id": "s6",
260
  "metadata": {},
261
  "outputs": [],
262
  "source": [
263
+ "%load_ext tensorboard\n",
264
+ "%tensorboard --logdir /content/data-centric-env/logs\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  ]
266
  },
267
  {
268
  "cell_type": "markdown",
269
+ "id": "s7_md",
270
  "metadata": {},
271
  "source": [
272
+ "## πŸ‹οΈ Step 7 β€” Phase 2: GRPO Training\n",
273
+ "\n",
274
+ "Agent learns *when* to use each command via reinforcement learning.\n",
275
+ "Each GRPO step: model generates a command β†’ environment executes it β†’ reward flows back.\n",
276
+ "\n",
277
+ "**Reward discrimination:** The reward is now strictly discriminating:\n",
278
+ "- Hitting target efficiently (few steps): **+0.7 β†’ +0.9**\n",
279
+ "- Hitting target slowly (many steps): **+0.3 β†’ +0.5**\n",
280
+ "- Failing to hit target: **βˆ’0.1 β†’ βˆ’0.5**\n",
281
+ "- Blind apply / blind submit: **large penalties**\n",
282
+ "\n",
283
+ "Checkpoints saved every 25 steps to `./data-centric-checkpoints/`.\n",
284
+ "If the runtime disconnects, re-run all cells β€” it auto-resumes from the latest checkpoint.\n"
285
  ]
286
  },
287
  {
288
  "cell_type": "code",
289
  "execution_count": null,
290
+ "id": "s7",
291
  "metadata": {},
292
  "outputs": [],
293
  "source": [
294
+ "import os, sys, glob\n",
295
  "os.chdir('/content/data-centric-env')\n",
296
  "sys.path.insert(0, '/content/data-centric-env')\n",
297
  "\n",
298
+ "from train_data_centric import run_grpo_training\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  "\n",
300
+ "# Auto-resume from latest checkpoint\n",
301
+ "ckpt_dir = './data-centric-checkpoints'\n",
302
+ "resume_from = None\n",
303
+ "if os.path.exists(ckpt_dir):\n",
304
+ " checkpoints = sorted(glob.glob(f'{ckpt_dir}/checkpoint-*'))\n",
305
+ " if checkpoints:\n",
306
+ " resume_from = checkpoints[-1]\n",
307
+ " print(f'Resuming from: {resume_from}')\n",
308
  "\n",
309
+ "model = run_grpo_training(model, tokenizer, resume_from_checkpoint=resume_from)\n",
310
+ "print('βœ… GRPO training complete')\n"
311
  ]
312
  },
313
  {
314
  "cell_type": "markdown",
315
+ "id": "s8_md",
316
  "metadata": {},
317
  "source": [
318
+ "## πŸ“ˆ Step 8 β€” Plot Reward Curves\n",
319
+ "\n",
320
+ "Generates 4 plots from the training log:\n",
321
+ "- **reward_curve.png** β€” Episode reward over time with rolling mean\n",
322
+ "- **success_rate.png** β€” Success rate per curriculum level\n",
323
+ "- **accuracy_gain.png** β€” Accuracy gain vs baseline per episode\n",
324
+ "- **curriculum.png** β€” Curriculum level progression\n"
325
  ]
326
  },
327
  {
328
  "cell_type": "code",
329
  "execution_count": null,
330
+ "id": "s8",
331
  "metadata": {},
332
  "outputs": [],
333
  "source": [
334
  "import os\n",
335
  "os.chdir('/content/data-centric-env')\n",
336
  "\n",
337
+ "log_path = 'logs/training.jsonl'\n",
338
+ "\n",
339
+ "if not os.path.exists(log_path):\n",
340
+ " print('No training log yet β€” run Step 7 first')\n",
341
+ "else:\n",
342
+ " n_eps = sum(1 for _ in open(log_path))\n",
343
+ " print(f'Training log: {n_eps} episodes')\n",
344
+ "\n",
345
+ " os.system(f'python plot_rewards.py --log {log_path} --out plots/')\n",
346
+ "\n",
347
  " from IPython.display import Image, display\n",
348
+ " for fname in ['reward_curve.png', 'success_rate.png', 'accuracy_gain.png', 'curriculum.png']:\n",
349
+ " path = f'plots/{fname}'\n",
350
  " if os.path.exists(path):\n",
351
+ " print(f'\\n--- {fname} ---')\n",
352
+ " display(Image(path, width=900))\n"
 
 
353
  ]
354
  },
355
  {
356
  "cell_type": "markdown",
357
+ "id": "s9_md",
358
  "metadata": {},
359
  "source": [
360
+ "## πŸ† Step 9 β€” Evaluate: Trained Agent vs Baselines\n",
361
+ "\n",
362
+ "Runs the trained agent against:\n",
363
+ "- **Random baseline** β€” random command each step\n",
364
+ "- **Heuristic baseline** β€” fixed rule-based sequence\n",
365
+ "- **Trained agent** β€” your GRPO-trained model\n"
366
  ]
367
  },
368
  {
369
  "cell_type": "code",
370
  "execution_count": null,
371
+ "id": "s9",
372
  "metadata": {},
373
  "outputs": [],
374
  "source": [
375
  "import os, json\n",
376
  "os.chdir('/content/data-centric-env')\n",
377
  "\n",
378
+ "os.system('python eval_data_centric.py')\n",
379
  "\n",
380
  "if os.path.exists('eval_results.json'):\n",
381
  " with open('eval_results.json') as f:\n",
382
  " results = json.load(f)\n",
383
+ " print('\\n=== Evaluation Results ===')\n",
384
  " for k, v in results.items():\n",
385
+ " print(f' {k}: {v}')\n"
386
  ]
387
  },
388
  {
389
  "cell_type": "markdown",
390
+ "id": "s10_md",
391
  "metadata": {},
392
  "source": [
393
+ "## πŸ’Ύ Step 10 β€” Save Trained LoRA Adapter"
 
394
  ]
395
  },
396
  {
397
  "cell_type": "code",
398
  "execution_count": null,
399
+ "id": "s10",
400
  "metadata": {},
401
  "outputs": [],
402
  "source": [
403
+ "import os, sys\n",
404
  "os.chdir('/content/data-centric-env')\n",
405
+ "sys.path.insert(0, '/content/data-centric-env')\n",
406
  "\n",
407
+ "from train_data_centric import save_model\n",
408
+ "save_model(model, tokenizer)\n",
409
+ "print('βœ… Adapter saved to ./data-centric-adapter/')\n",
410
+ "\n",
411
+ "# List outputs\n",
412
+ "import glob\n",
413
+ "print('\\nπŸ“ Training outputs:')\n",
414
+ "for f in sorted(glob.glob('plots/*.png') + glob.glob('logs/*.jsonl') + ['eval_results.json']):\n",
415
+ " if os.path.exists(f):\n",
416
+ " size = os.path.getsize(f) / 1024\n",
417
+ " print(f' {f} ({size:.1f} KB)')\n"
418
  ]
419
  },
420
  {
421
  "cell_type": "markdown",
422
+ "id": "s11_md",
423
  "metadata": {},
424
  "source": [
425
+ "## πŸ“€ Step 11 β€” Download Results\n",
426
+ "\n",
427
+ "Download plots and training log to your local machine.\n"
428
+ ]
429
+ },
430
+ {
431
+ "cell_type": "code",
432
+ "execution_count": null,
433
+ "id": "s11",
434
+ "metadata": {},
435
+ "outputs": [],
436
+ "source": [
437
+ "from google.colab import files\n",
438
+ "import glob, os\n",
439
+ "\n",
440
+ "to_download = glob.glob('plots/*.png') + ['logs/training.jsonl', 'eval_results.json']\n",
441
+ "\n",
442
+ "for f in to_download:\n",
443
+ " if os.path.exists(f):\n",
444
+ " print(f'Downloading: {f}')\n",
445
+ " files.download(f)\n"
446
  ]
447
  }
448
  ]