Jaswanth1210 Claude Sonnet 4.6 commited on
Commit
ff63792
·
1 Parent(s): 2fe5366

feat: 5 training plots (reward±std, KL/loss, completion stats) + Drive backup in Cell 8

Browse files
Files changed (2) hide show
  1. notebooks/colab_runner.ipynb +1 -1
  2. scripts/make_plots.py +271 -108
notebooks/colab_runner.ipynb CHANGED
@@ -315,7 +315,7 @@
315
  "metadata": {},
316
  "outputs": [],
317
  "source": [
318
- "# EVALUATE + PLOTS \u2014 Phase 6\nimport os, glob, shutil\n\nOUTPUT_DIR = '/content/drive/MyDrive/injectarena/run_v1'\nCHECKPOINT = f'{OUTPUT_DIR}/final'\n\n%cd /content/injectarena\n\n# Pull latest code (gets make_plots.py etc.)\n!git pull origin main\n\n# 1. Evaluate trained checkpoint against eval split.\n!python train/eval.py \\\n --checkpoint {CHECKPOINT} \\\n --output-json docs/eval_results.json\n\n# 2. Copy training logs from Drive \u2192 repo.\nos.makedirs('logs', exist_ok=True)\nfor f in glob.glob(f'{OUTPUT_DIR}/logs/*.jsonl'):\n shutil.copy(f, 'logs/')\nprint(f\"Copied {len(glob.glob(OUTPUT_DIR+'/logs/*.jsonl'))} log files\")\n\n# 3. Generate plots.\n!pip install matplotlib --quiet\n!python scripts/make_plots.py --logs logs/ --out docs/plots/ --eval docs/eval_results.json --trainer-state {OUTPUT_DIR}/trainer_state.json\n\n# 4. Commit and push using GH_TOKEN if available.\nfrom google.colab import userdata\nimport subprocess\n\ngh_token = ''\ntry:\n gh_token = userdata.get('GH_TOKEN')\nexcept Exception:\n pass\n\n!git config user.email \"colab@bot\"\n!git config user.name \"colab\"\n!git add docs/plots docs/eval_results.json\n\n# Stage logs only if any were copied\nimport os as _os\nif _os.path.exists('logs') and any(_os.listdir('logs')):\n !git add logs/\n\n!git status\n\nif gh_token:\n remote_url = !git remote get-url origin\n remote_url = remote_url[0].replace('https://', f'https://{gh_token}@')\n !git commit -m \"Phase 6: training results and plots\" || echo \"Nothing to commit\"\n !git push {remote_url} main\nelse:\n !git commit -m \"Phase 6: training results and plots\" || echo \"Nothing to commit\"\n print(\"\u26a0 GH_TOKEN not set \u2014 commit created locally but not pushed.\")\n print(\" Add GH_TOKEN to Colab secrets and re-run this cell, or run:\")\n print(\" git push from your Mac after pulling.\")\n\nprint(\"\\n\u2713 Cell 8 done.\")"
319
  ]
320
  }
321
  ],
 
315
  "metadata": {},
316
  "outputs": [],
317
  "source": [
318
+ "# EVALUATE + PLOTS \u2014 Phase 6\nimport os, glob, shutil\nfrom pathlib import Path\n\nOUTPUT_DIR = '/content/drive/MyDrive/injectarena/run_v1'\nCHECKPOINT = f'{OUTPUT_DIR}/final'\nPLOTS_DRIVE = f'{OUTPUT_DIR}/plots' # also save plots to Drive\n\n%cd /content/injectarena\n\n# Pull latest code (gets updated make_plots.py etc.)\n!git pull origin main\n\n# 1. Evaluate trained checkpoint against eval split.\n!python train/eval.py \\\n --checkpoint {CHECKPOINT} \\\n --output-json docs/eval_results.json\n\n# 2. Generate all 5 plots from trainer_state.json + eval results.\n# trainer_state.json is written by TRL directly into the output dir.\n!pip install matplotlib --quiet\nos.makedirs('docs/plots', exist_ok=True)\n!python scripts/make_plots.py \\\n --trainer-state {OUTPUT_DIR}/trainer_state.json \\\n --logs logs/ \\\n --eval docs/eval_results.json \\\n --out docs/plots/\n\n# 3. Copy all plots to Drive so they survive session resets.\nos.makedirs(PLOTS_DRIVE, exist_ok=True)\ncopied = 0\nfor src in Path('docs/plots').glob('*.png'):\n shutil.copy(src, PLOTS_DRIVE)\n copied += 1\nprint(f\"Copied {copied} plots to Drive: {PLOTS_DRIVE}\")\n\n# 4. Commit everything and push using GH_TOKEN if available.\nfrom google.colab import userdata\nimport subprocess\n\ngh_token = ''\ntry:\n gh_token = userdata.get('GH_TOKEN')\nexcept Exception:\n pass\n\n!git config user.email \"colab@bot\"\n!git config user.name \"colab\"\n!git add docs/plots docs/eval_results.json\n!git status\n\nif gh_token:\n remote_url = !git remote get-url origin\n remote_url = remote_url[0].replace('https://', f'https://{gh_token}@')\n !git commit -m \"Phase 6: training results and plots\" || echo \"Nothing to commit\"\n !git push {remote_url} main\nelse:\n !git commit -m \"Phase 6: training results and plots\" || echo \"Nothing to commit\"\n print(\"\u26a0 GH_TOKEN not set \u2014 commit created locally but not pushed.\")\n print(\" Add GH_TOKEN to Colab secrets and re-run this cell, or push from Mac.\")\n\nprint(\"\\n\u2713 Cell 8 done. Plots at docs/plots/ and backed up to Drive.\")\nprint(\"Plots generated:\")\nfor p in sorted(Path('docs/plots').glob('*.png')):\n print(f\" {p}\")\n"
319
  ]
320
  }
321
  ],
scripts/make_plots.py CHANGED
@@ -1,29 +1,48 @@
1
- """Generate training plots from JSONL logs and eval results.
2
-
3
- Usage:
4
- python scripts/make_plots.py --logs logs/ --out docs/plots/
5
- python scripts/make_plots.py --logs logs/ --out docs/plots/ --eval docs/eval_results.json
 
 
 
 
 
 
 
 
 
 
 
6
  """
7
 
8
  from __future__ import annotations
9
 
10
  import argparse
11
  import json
12
- import os
13
  from pathlib import Path
14
- from typing import Any, Dict, List
 
15
 
 
 
 
16
 
17
  def _parse_args() -> argparse.Namespace:
18
  p = argparse.ArgumentParser()
19
- p.add_argument("--logs", type=str, default="logs/")
 
 
 
20
  p.add_argument("--out", type=str, default="docs/plots/")
21
  p.add_argument("--eval", type=str, default="docs/eval_results.json")
22
- p.add_argument("--trainer-state", type=str, default=None,
23
- help="Path to TRL trainer_state.json (fallback when no JSONL logs)")
24
  return p.parse_args()
25
 
26
 
 
 
 
 
27
  def _load_jsonl(path: Path) -> List[Dict[str, Any]]:
28
  rows = []
29
  with open(path) as f:
@@ -38,7 +57,7 @@ def _load_jsonl(path: Path) -> List[Dict[str, Any]]:
38
 
39
 
40
  def _load_all_logs(logs_dir: Path) -> List[Dict[str, Any]]:
41
- rows = []
42
  for p in sorted(logs_dir.glob("*.jsonl")):
43
  rows.extend(_load_jsonl(p))
44
  rows.sort(key=lambda r: r.get("step", r.get("global_step", 0)))
@@ -46,149 +65,283 @@ def _load_all_logs(logs_dir: Path) -> List[Dict[str, Any]]:
46
 
47
 
48
  def _load_trainer_state(state_path: Path) -> List[Dict[str, Any]]:
49
- """Read TRL's trainer_state.json log_history into the same row format."""
50
  if not state_path.exists():
 
51
  return []
52
  with open(state_path) as f:
53
  data = json.load(f)
54
- rows = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  for entry in data.get("log_history", []):
56
  step = entry.get("step")
57
  if step is None:
58
  continue
59
  row: Dict[str, Any] = {"step": step}
60
- # TRL key variants
61
- for src, dst in [
62
- ("reward", "reward/mean"),
63
- ("rewards/mean", "reward/mean"),
64
- ("reward/mean", "reward/mean"),
65
- ("loss", "loss"),
66
- ("train/loss", "loss"),
67
- ]:
68
  if src in entry:
69
  row[dst] = entry[src]
70
- rows.append(row)
 
71
  rows.sort(key=lambda r: r["step"])
 
72
  return rows
73
 
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  def _plot_reward_curve(rows: List[Dict[str, Any]], out_dir: Path) -> None:
76
  import matplotlib.pyplot as plt
77
  import numpy as np
78
 
79
- steps, rewards, losses = [], [], []
80
- for r in rows:
81
- step = r.get("step", r.get("global_step"))
82
- if step is None:
83
- continue
84
- reward = r.get("reward/mean", r.get("mean_reward"))
85
- loss = r.get("loss", r.get("train/loss"))
86
- if reward is not None:
87
- steps.append(step)
88
- rewards.append(reward)
89
- if loss is not None and step not in [s for s in steps[:-1]]:
90
- losses.append((step, loss))
91
-
92
- if not steps:
93
- print("No reward data found in logs — skipping reward curve.")
94
  return
95
 
96
- fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
97
-
98
- # Smooth reward with rolling window
99
- window = max(1, len(rewards) // 20)
100
- smoothed = np.convolve(rewards, np.ones(window) / window, mode="valid")
101
- smooth_steps = steps[window - 1:]
102
-
103
- ax1.plot(steps, rewards, alpha=0.3, color="#3b82f6", linewidth=0.8, label="raw")
104
- ax1.plot(smooth_steps, smoothed, color="#1d4ed8", linewidth=2, label=f"smoothed (w={window})")
105
- ax1.axhline(0, color="gray", linestyle="--", linewidth=0.5)
106
- ax1.set_ylabel("Mean Reward")
107
- ax1.set_title("InjectArena GRPO Training: Reward Curve")
108
- ax1.legend(loc="lower right")
109
- ax1.grid(alpha=0.3)
110
-
111
- if losses:
112
- l_steps = [x[0] for x in losses]
113
- l_vals = [x[1] for x in losses]
114
- ax2.plot(l_steps, l_vals, color="#ef4444", linewidth=1.5)
115
- ax2.set_ylabel("Training Loss")
116
- ax2.set_xlabel("Step")
117
- ax2.grid(alpha=0.3)
118
- else:
119
- ax2.set_visible(False)
120
 
121
- plt.tight_layout()
122
  out_path = out_dir / "reward_curve.png"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  plt.savefig(out_path, dpi=150, bbox_inches="tight")
124
  plt.close()
125
  print(f"Saved {out_path}")
126
 
127
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  def _plot_bypass_bars(eval_path: Path, out_dir: Path) -> None:
129
  import matplotlib.pyplot as plt
130
  import numpy as np
131
 
132
  if not eval_path.exists():
133
- print(f"Eval results not found at {eval_path} — skipping bypass bar chart.")
134
  return
135
 
136
  with open(eval_path) as f:
137
  data = json.load(f)
138
 
139
  metrics = {
140
- "PG2 Bypass": data.get("pg2_bypass_rate", 0),
141
- "FW Bypass": data.get("fw_bypass_rate", 0),
142
- "Task Success": data.get("task_success_rate", 0),
143
  "Composed Bypass": data.get("composed_bypass_rate", 0),
144
  }
145
-
146
- # Hardcoded approximate baselines (handcrafted corpus on same eval split)
147
- # These are filled in once the zero_shot eval is run.
148
  baselines = {
149
- "PG2 Bypass": 0.15,
150
- "FW Bypass": 0.20,
151
- "Task Success": 0.05,
152
- "Composed Bypass": 0.02,
153
  }
154
 
155
  x = np.arange(len(metrics))
156
  width = 0.35
157
-
158
  fig, ax = plt.subplots(figsize=(9, 5))
159
- bars1 = ax.bar(x - width / 2,
160
- [baselines[k] for k in metrics],
161
- width, label="Handcrafted Baseline", color="#94a3b8", edgecolor="white")
162
- bars2 = ax.bar(x + width / 2,
163
- [metrics[k] for k in metrics],
164
- width, label="InjectArena (RL-trained)", color="#3b82f6", edgecolor="white")
165
 
166
  ax.set_ylabel("Rate")
167
  ax.set_title("InjectArena — Attacker Performance vs Baseline")
168
  ax.set_xticks(x)
169
  ax.set_xticklabels(list(metrics.keys()))
170
- ax.set_ylim(0, 1.0)
171
  ax.legend()
172
  ax.grid(axis="y", alpha=0.3)
173
 
174
  for bar in bars1:
175
  h = bar.get_height()
176
- if h > 0.02:
177
  ax.text(bar.get_x() + bar.get_width() / 2, h + 0.01,
178
- f"{h:.0%}", ha="center", va="bottom", fontsize=9, color="#64748b")
179
  for bar in bars2:
180
  h = bar.get_height()
181
- if h > 0.02:
182
  ax.text(bar.get_x() + bar.get_width() / 2, h + 0.01,
183
- f"{h:.0%}", ha="center", va="bottom", fontsize=9, color="#1d4ed8")
184
 
185
- plt.tight_layout()
186
  out_path = out_dir / "bypass_bars.png"
 
187
  plt.savefig(out_path, dpi=150, bbox_inches="tight")
188
  plt.close()
189
  print(f"Saved {out_path}")
190
 
191
 
 
 
 
 
192
  def _plot_per_category(eval_path: Path, out_dir: Path) -> None:
193
  import matplotlib.pyplot as plt
194
  import numpy as np
@@ -201,40 +354,42 @@ def _plot_per_category(eval_path: Path, out_dir: Path) -> None:
201
 
202
  per_cat = data.get("per_category", {})
203
  if not per_cat:
 
204
  return
205
 
206
  cats = list(per_cat.keys())
207
- task_rates = [per_cat[c]["task_success"] for c in cats]
208
  bypass_rates = [per_cat[c]["composed_bypass"] for c in cats]
209
 
210
  x = np.arange(len(cats))
211
  width = 0.35
212
-
213
  fig, ax = plt.subplots(figsize=(8, 5))
214
- ax.bar(x - width / 2, task_rates, width, label="Task Success", color="#22c55e", edgecolor="white")
215
- ax.bar(x + width / 2, bypass_rates, width, label="Composed Bypass", color="#3b82f6", edgecolor="white")
216
 
217
  ax.set_ylabel("Rate")
218
  ax.set_title("InjectArena — Per-Category Breakdown")
219
  ax.set_xticks(x)
220
- ax.set_xticklabels(cats, rotation=15)
221
- ax.set_ylim(0, 1.0)
222
  ax.legend()
223
  ax.grid(axis="y", alpha=0.3)
224
 
225
- plt.tight_layout()
226
  out_path = out_dir / "per_category.png"
 
227
  plt.savefig(out_path, dpi=150, bbox_inches="tight")
228
  plt.close()
229
  print(f"Saved {out_path}")
230
 
231
 
 
 
 
 
232
  def main() -> None:
233
  args = _parse_args()
234
- logs_dir = Path(args.logs)
235
- out_dir = Path(args.out)
236
  eval_path = Path(args.eval)
237
-
238
  out_dir.mkdir(parents=True, exist_ok=True)
239
 
240
  try:
@@ -244,24 +399,32 @@ def main() -> None:
244
  print("matplotlib not installed — pip install matplotlib")
245
  return
246
 
 
247
  rows: List[Dict[str, Any]] = []
248
- if logs_dir.exists():
249
- rows = _load_all_logs(logs_dir)
250
- print(f"Loaded {len(rows)} log rows from {logs_dir}")
251
-
252
- if not rows and args.trainer_state:
253
- state_path = Path(args.trainer_state)
254
- rows = _load_trainer_state(state_path)
255
- print(f"Loaded {len(rows)} log rows from trainer_state {state_path}")
256
-
257
  if not rows:
258
- print("No log data found — skipping reward curve.")
259
- else:
 
 
 
 
 
 
260
  _plot_reward_curve(rows, out_dir)
 
 
 
 
261
 
 
262
  _plot_bypass_bars(eval_path, out_dir)
263
  _plot_per_category(eval_path, out_dir)
264
- print("Done.")
 
 
 
265
 
266
 
267
  if __name__ == "__main__":
 
1
+ """Generate training plots from TRL trainer_state.json or JSONL logs + eval results.
2
+
3
+ Plots produced
4
+ --------------
5
+ 1. reward_curve.png — mean reward ± std band + smoothed trend
6
+ 2. kl_loss_curve.png — KL divergence & policy loss on twin axes
7
+ 3. completion_stats.png — mean completion length + clipped-ratio line
8
+ 4. bypass_bars.png — RL-trained vs handcrafted baseline (eval results)
9
+ 5. per_category.png — per-scenario-category breakdown (eval results)
10
+
11
+ Usage
12
+ -----
13
+ python scripts/make_plots.py \\
14
+ --trainer-state /path/to/trainer_state.json \\
15
+ --eval docs/eval_results.json \\
16
+ --out docs/plots/
17
  """
18
 
19
  from __future__ import annotations
20
 
21
  import argparse
22
  import json
 
23
  from pathlib import Path
24
+ from typing import Any, Dict, List, Optional, Tuple
25
+
26
 
27
+ # ---------------------------------------------------------------------------
28
+ # Argument parsing
29
+ # ---------------------------------------------------------------------------
30
 
31
  def _parse_args() -> argparse.Namespace:
32
  p = argparse.ArgumentParser()
33
+ p.add_argument("--logs", type=str, default="logs/",
34
+ help="Directory of JSONL log files (legacy fallback)")
35
+ p.add_argument("--trainer-state", type=str, default=None,
36
+ help="Path to TRL trainer_state.json (preferred)")
37
  p.add_argument("--out", type=str, default="docs/plots/")
38
  p.add_argument("--eval", type=str, default="docs/eval_results.json")
 
 
39
  return p.parse_args()
40
 
41
 
42
+ # ---------------------------------------------------------------------------
43
+ # Data loading
44
+ # ---------------------------------------------------------------------------
45
+
46
  def _load_jsonl(path: Path) -> List[Dict[str, Any]]:
47
  rows = []
48
  with open(path) as f:
 
57
 
58
 
59
  def _load_all_logs(logs_dir: Path) -> List[Dict[str, Any]]:
60
+ rows: List[Dict[str, Any]] = []
61
  for p in sorted(logs_dir.glob("*.jsonl")):
62
  rows.extend(_load_jsonl(p))
63
  rows.sort(key=lambda r: r.get("step", r.get("global_step", 0)))
 
65
 
66
 
67
  def _load_trainer_state(state_path: Path) -> List[Dict[str, Any]]:
68
+ """Parse TRL trainer_state.json log_history into a normalised row list."""
69
  if not state_path.exists():
70
+ print(f"trainer_state.json not found at {state_path}")
71
  return []
72
  with open(state_path) as f:
73
  data = json.load(f)
74
+
75
+ # TRL GRPO key → normalised key mapping
76
+ KEY_MAP = {
77
+ "reward": "reward/mean",
78
+ "rewards/mean": "reward/mean",
79
+ "reward/mean": "reward/mean",
80
+ "reward_std": "reward/std",
81
+ "rewards/std": "reward/std",
82
+ "reward/std": "reward/std",
83
+ "kl": "kl",
84
+ "loss": "loss",
85
+ "train/loss": "loss",
86
+ "learning_rate": "lr",
87
+ "completions/mean_length": "completion/mean_length",
88
+ "completions/clipped_ratio": "completion/clipped_ratio",
89
+ }
90
+
91
+ rows: List[Dict[str, Any]] = []
92
  for entry in data.get("log_history", []):
93
  step = entry.get("step")
94
  if step is None:
95
  continue
96
  row: Dict[str, Any] = {"step": step}
97
+ for src, dst in KEY_MAP.items():
 
 
 
 
 
 
 
98
  if src in entry:
99
  row[dst] = entry[src]
100
+ if len(row) > 1: # has at least one metric besides step
101
+ rows.append(row)
102
  rows.sort(key=lambda r: r["step"])
103
+ print(f"Loaded {len(rows)} log entries from {state_path}")
104
  return rows
105
 
106
 
107
+ def _extract(rows: List[Dict[str, Any]], key: str) -> Tuple[List[int], List[float]]:
108
+ steps, vals = [], []
109
+ for r in rows:
110
+ v = r.get(key)
111
+ if v is not None:
112
+ steps.append(r["step"])
113
+ vals.append(float(v))
114
+ return steps, vals
115
+
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # Plot helpers
119
+ # ---------------------------------------------------------------------------
120
+
121
+ def _smooth(vals: List[float], window: int) -> List[float]:
122
+ import numpy as np
123
+ if window <= 1 or len(vals) < window:
124
+ return vals
125
+ return list(np.convolve(vals, np.ones(window) / window, mode="valid"))
126
+
127
+
128
+ BLUE = "#3b82f6"
129
+ DBLUE = "#1d4ed8"
130
+ RED = "#ef4444"
131
+ GREEN = "#22c55e"
132
+ ORANGE = "#f97316"
133
+ PURPLE = "#a855f7"
134
+ GRAY = "#94a3b8"
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # Plot 1: Reward curve with ±std band
139
+ # ---------------------------------------------------------------------------
140
+
141
  def _plot_reward_curve(rows: List[Dict[str, Any]], out_dir: Path) -> None:
142
  import matplotlib.pyplot as plt
143
  import numpy as np
144
 
145
+ steps_r, rewards = _extract(rows, "reward/mean")
146
+ steps_s, stds = _extract(rows, "reward/std")
147
+
148
+ if not steps_r:
149
+ print("No reward data — skipping reward_curve.png")
 
 
 
 
 
 
 
 
 
 
150
  return
151
 
152
+ window = max(1, len(rewards) // 15)
153
+ smoothed = _smooth(rewards, window)
154
+ smooth_steps = steps_r[window - 1:] if window > 1 else steps_r
155
+
156
+ fig, ax = plt.subplots(figsize=(10, 5))
157
+
158
+ # std band
159
+ if steps_s and len(steps_s) == len(steps_r):
160
+ r_arr = np.array(rewards)
161
+ s_arr = np.array(stds)
162
+ ax.fill_between(steps_r, r_arr - s_arr, r_arr + s_arr,
163
+ alpha=0.15, color=BLUE, label="±1 std")
164
+
165
+ ax.plot(steps_r, rewards, alpha=0.35, color=BLUE, linewidth=0.9, label="raw reward")
166
+ ax.plot(smooth_steps, smoothed, color=DBLUE, linewidth=2.2,
167
+ label=f"smoothed (w={window})")
168
+ ax.axhline(0, color="gray", linestyle="--", linewidth=0.6)
169
+
170
+ ax.set_xlabel("Training Step")
171
+ ax.set_ylabel("Mean Reward")
172
+ ax.set_title("InjectArena — GRPO Reward Curve (300 steps)")
173
+ ax.legend(loc="lower right")
174
+ ax.set_ylim(bottom=0)
175
+ ax.grid(alpha=0.25)
176
 
 
177
  out_path = out_dir / "reward_curve.png"
178
+ plt.tight_layout()
179
+ plt.savefig(out_path, dpi=150, bbox_inches="tight")
180
+ plt.close()
181
+ print(f"Saved {out_path}")
182
+
183
+
184
+ # ---------------------------------------------------------------------------
185
+ # Plot 2: KL divergence + policy loss on twin axes
186
+ # ---------------------------------------------------------------------------
187
+
188
+ def _plot_kl_loss(rows: List[Dict[str, Any]], out_dir: Path) -> None:
189
+ import matplotlib.pyplot as plt
190
+
191
+ steps_kl, kls = _extract(rows, "kl")
192
+ steps_l, losses = _extract(rows, "loss")
193
+
194
+ if not steps_kl and not steps_l:
195
+ print("No KL/loss data — skipping kl_loss_curve.png")
196
+ return
197
+
198
+ fig, ax1 = plt.subplots(figsize=(10, 4))
199
+
200
+ if steps_kl:
201
+ ax1.plot(steps_kl, kls, color=PURPLE, linewidth=1.8, label="KL divergence")
202
+ ax1.set_ylabel("KL Divergence", color=PURPLE)
203
+ ax1.tick_params(axis="y", labelcolor=PURPLE)
204
+
205
+ if steps_l:
206
+ ax2 = ax1.twinx()
207
+ ax2.plot(steps_l, losses, color=RED, linewidth=1.8, linestyle="--", label="Policy loss")
208
+ ax2.set_ylabel("Policy Loss", color=RED)
209
+ ax2.tick_params(axis="y", labelcolor=RED)
210
+
211
+ ax1.set_xlabel("Training Step")
212
+ ax1.set_title("InjectArena — KL Divergence & Policy Loss")
213
+ ax1.grid(alpha=0.2)
214
+
215
+ # Combined legend
216
+ lines = []
217
+ if steps_kl:
218
+ lines += ax1.get_lines()
219
+ if steps_l:
220
+ lines += ax2.get_lines()
221
+ if lines:
222
+ ax1.legend(lines, [l.get_label() for l in lines], loc="upper right")
223
+
224
+ out_path = out_dir / "kl_loss_curve.png"
225
+ plt.tight_layout()
226
  plt.savefig(out_path, dpi=150, bbox_inches="tight")
227
  plt.close()
228
  print(f"Saved {out_path}")
229
 
230
 
231
+ # ---------------------------------------------------------------------------
232
+ # Plot 3: Completion length + clipped ratio
233
+ # ---------------------------------------------------------------------------
234
+
235
+ def _plot_completion_stats(rows: List[Dict[str, Any]], out_dir: Path) -> None:
236
+ import matplotlib.pyplot as plt
237
+
238
+ steps_l, lengths = _extract(rows, "completion/mean_length")
239
+ steps_c, clipped = _extract(rows, "completion/clipped_ratio")
240
+
241
+ if not steps_l and not steps_c:
242
+ print("No completion stats — skipping completion_stats.png")
243
+ return
244
+
245
+ fig, ax1 = plt.subplots(figsize=(10, 4))
246
+
247
+ if steps_l:
248
+ ax1.plot(steps_l, lengths, color=ORANGE, linewidth=1.8, label="Mean completion length (tokens)")
249
+ ax1.set_ylabel("Mean Length (tokens)", color=ORANGE)
250
+ ax1.tick_params(axis="y", labelcolor=ORANGE)
251
+
252
+ if steps_c:
253
+ ax2 = ax1.twinx()
254
+ ax2.plot(steps_c, clipped, color=RED, linewidth=1.8, linestyle="--",
255
+ label="Clipped ratio (hit max_len)")
256
+ ax2.set_ylabel("Clipped Ratio", color=RED)
257
+ ax2.set_ylim(0, 1.05)
258
+ ax2.tick_params(axis="y", labelcolor=RED)
259
+ ax2.axhline(1.0, color=RED, linestyle=":", linewidth=0.7, alpha=0.5)
260
+
261
+ ax1.set_xlabel("Training Step")
262
+ ax1.set_title("InjectArena — Completion Length & Clipping")
263
+ ax1.grid(alpha=0.2)
264
+
265
+ lines = []
266
+ if steps_l:
267
+ lines += ax1.get_lines()
268
+ if steps_c:
269
+ lines += ax2.get_lines()
270
+ if lines:
271
+ ax1.legend(lines, [l.get_label() for l in lines], loc="upper right")
272
+
273
+ out_path = out_dir / "completion_stats.png"
274
+ plt.tight_layout()
275
+ plt.savefig(out_path, dpi=150, bbox_inches="tight")
276
+ plt.close()
277
+ print(f"Saved {out_path}")
278
+
279
+
280
+ # ---------------------------------------------------------------------------
281
+ # Plot 4: Bypass bars (eval results vs baseline)
282
+ # ---------------------------------------------------------------------------
283
+
284
  def _plot_bypass_bars(eval_path: Path, out_dir: Path) -> None:
285
  import matplotlib.pyplot as plt
286
  import numpy as np
287
 
288
  if not eval_path.exists():
289
+ print(f"Eval results not found at {eval_path} — skipping bypass_bars.png")
290
  return
291
 
292
  with open(eval_path) as f:
293
  data = json.load(f)
294
 
295
  metrics = {
296
+ "PG2 Bypass": data.get("pg2_bypass_rate", 0),
297
+ "FW Bypass": data.get("fw_bypass_rate", 0),
298
+ "Task Success": data.get("task_success_rate", 0),
299
  "Composed Bypass": data.get("composed_bypass_rate", 0),
300
  }
 
 
 
301
  baselines = {
302
+ "PG2 Bypass": 0.15, "FW Bypass": 0.20,
303
+ "Task Success": 0.05, "Composed Bypass": 0.02,
 
 
304
  }
305
 
306
  x = np.arange(len(metrics))
307
  width = 0.35
 
308
  fig, ax = plt.subplots(figsize=(9, 5))
309
+
310
+ bars1 = ax.bar(x - width / 2, [baselines[k] for k in metrics],
311
+ width, label="Handcrafted Baseline", color=GRAY, edgecolor="white")
312
+ bars2 = ax.bar(x + width / 2, [metrics[k] for k in metrics],
313
+ width, label="InjectArena (RL-trained)", color=BLUE, edgecolor="white")
 
314
 
315
  ax.set_ylabel("Rate")
316
  ax.set_title("InjectArena — Attacker Performance vs Baseline")
317
  ax.set_xticks(x)
318
  ax.set_xticklabels(list(metrics.keys()))
319
+ ax.set_ylim(0, 1.05)
320
  ax.legend()
321
  ax.grid(axis="y", alpha=0.3)
322
 
323
  for bar in bars1:
324
  h = bar.get_height()
325
+ if h > 0.01:
326
  ax.text(bar.get_x() + bar.get_width() / 2, h + 0.01,
327
+ f"{h:.0%}", ha="center", va="bottom", fontsize=9, color="#475569")
328
  for bar in bars2:
329
  h = bar.get_height()
330
+ if h > 0.01:
331
  ax.text(bar.get_x() + bar.get_width() / 2, h + 0.01,
332
+ f"{h:.0%}", ha="center", va="bottom", fontsize=9, color=DBLUE)
333
 
 
334
  out_path = out_dir / "bypass_bars.png"
335
+ plt.tight_layout()
336
  plt.savefig(out_path, dpi=150, bbox_inches="tight")
337
  plt.close()
338
  print(f"Saved {out_path}")
339
 
340
 
341
+ # ---------------------------------------------------------------------------
342
+ # Plot 5: Per-category breakdown
343
+ # ---------------------------------------------------------------------------
344
+
345
  def _plot_per_category(eval_path: Path, out_dir: Path) -> None:
346
  import matplotlib.pyplot as plt
347
  import numpy as np
 
354
 
355
  per_cat = data.get("per_category", {})
356
  if not per_cat:
357
+ print("No per_category data in eval results — skipping per_category.png")
358
  return
359
 
360
  cats = list(per_cat.keys())
361
+ task_rates = [per_cat[c]["task_success"] for c in cats]
362
  bypass_rates = [per_cat[c]["composed_bypass"] for c in cats]
363
 
364
  x = np.arange(len(cats))
365
  width = 0.35
 
366
  fig, ax = plt.subplots(figsize=(8, 5))
367
+ ax.bar(x - width / 2, task_rates, width, label="Task Success", color=GREEN, edgecolor="white")
368
+ ax.bar(x + width / 2, bypass_rates, width, label="Composed Bypass", color=BLUE, edgecolor="white")
369
 
370
  ax.set_ylabel("Rate")
371
  ax.set_title("InjectArena — Per-Category Breakdown")
372
  ax.set_xticks(x)
373
+ ax.set_xticklabels(cats, rotation=15, ha="right")
374
+ ax.set_ylim(0, 1.05)
375
  ax.legend()
376
  ax.grid(axis="y", alpha=0.3)
377
 
 
378
  out_path = out_dir / "per_category.png"
379
+ plt.tight_layout()
380
  plt.savefig(out_path, dpi=150, bbox_inches="tight")
381
  plt.close()
382
  print(f"Saved {out_path}")
383
 
384
 
385
+ # ---------------------------------------------------------------------------
386
+ # Main
387
+ # ---------------------------------------------------------------------------
388
+
389
  def main() -> None:
390
  args = _parse_args()
391
+ out_dir = Path(args.out)
 
392
  eval_path = Path(args.eval)
 
393
  out_dir.mkdir(parents=True, exist_ok=True)
394
 
395
  try:
 
399
  print("matplotlib not installed — pip install matplotlib")
400
  return
401
 
402
+ # Load training log rows (trainer_state preferred, JSONL fallback)
403
  rows: List[Dict[str, Any]] = []
404
+ if args.trainer_state:
405
+ rows = _load_trainer_state(Path(args.trainer_state))
 
 
 
 
 
 
 
406
  if not rows:
407
+ logs_dir = Path(args.logs)
408
+ if logs_dir.exists():
409
+ rows = _load_all_logs(logs_dir)
410
+ if rows:
411
+ print(f"Loaded {len(rows)} log rows from {logs_dir}")
412
+
413
+ # Training plots (require rows)
414
+ if rows:
415
  _plot_reward_curve(rows, out_dir)
416
+ _plot_kl_loss(rows, out_dir)
417
+ _plot_completion_stats(rows, out_dir)
418
+ else:
419
+ print("No training log data found — skipping reward/KL/completion plots.")
420
 
421
+ # Eval plots (require eval results JSON)
422
  _plot_bypass_bars(eval_path, out_dir)
423
  _plot_per_category(eval_path, out_dir)
424
+
425
+ print("\nAll plots done.")
426
+ for p in sorted(out_dir.glob("*.png")):
427
+ print(f" {p}")
428
 
429
 
430
  if __name__ == "__main__":