Spaces:
Sleeping
Sleeping
File size: 5,662 Bytes
9c49c6e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | #!/usr/bin/env python3
"""Re-render the base-vs-trained eval plots from the saved summary JSON.
The plots originally generated inside the HF Job had two readability problems for
people coming to the repo cold:
1. The "base" bars were a flat zero, so they vanished into the x-axis and the
chart looked like only the trained model had been tested.
2. Task labels were the raw scenario IDs (`dyad_must_refuse_v1`), which don't
tell a stranger what the task actually is.
This script reads `docs/eval/base_vs_trained/base_vs_trained_summary.json` and
writes friendlier versions of:
- reward_by_task.png
- valid_jsonl_by_task.png
- commit_rate_by_task.png
into `docs/eval/base_vs_trained/`.
"""
from __future__ import annotations
import json
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
ROOT = Path(__file__).resolve().parents[2]
EVAL_DIR = ROOT / "docs" / "eval" / "base_vs_trained"
SUMMARY = EVAL_DIR / "base_vs_trained_summary.json"
# Friendly labels keyed by the canonical task ids
TASK_LABELS = {
"dyad_must_refuse_v1": "Refuse leak\n(training task)",
"dyad_must_comply_v1": "Comply with\nbenign request",
"dyad_must_refuse_long_v1": "Refuse leak with\n41 distractors",
"triad_must_refuse_v1": "Refuse leak\nwith 2 colleagues",
}
def grouped_bar(metric_key: str, ylabel: str, title: str, subtitle: str, fname: str,
base_zero_label: str | None) -> None:
data = json.loads(SUMMARY.read_text())
tasks = data["tasks"]
base_vals = [data["base"][metric_key][t] for t in tasks]
trained_vals = [data["trained"][metric_key][t] for t in tasks]
x = np.arange(len(tasks))
width = 0.4
fig, ax = plt.subplots(figsize=(11, 6.4))
base_color = "#94a3b8"
trained_color = "#0f766e"
# Base bars: if the value is zero, draw a hairline outlined "ghost" so the
# reader can see the bar exists. Otherwise draw normally.
base_bars = ax.bar(x - width / 2, base_vals, width, color=base_color, label="Base model (LoRA off)")
for rect, val in zip(base_bars, base_vals):
if val == 0:
rect.set_height(0.012)
rect.set_facecolor("none")
rect.set_edgecolor(base_color)
rect.set_linewidth(1.6)
rect.set_hatch("//")
trained_bars = ax.bar(x + width / 2, trained_vals, width, color=trained_color,
label="Trained model (LoRA on, same weights)")
# Value labels on every bar.
for rect, val in zip(base_bars, base_vals):
label = f"{val:.2f}" if val > 0 else "0.00"
ax.annotate(label, (rect.get_x() + rect.get_width() / 2, max(rect.get_height(), 0.012)),
xytext=(0, 4), textcoords="offset points",
ha="center", va="bottom", fontsize=9, color=base_color)
for rect, val in zip(trained_bars, trained_vals):
ax.annotate(f"{val:.2f}",
(rect.get_x() + rect.get_width() / 2, rect.get_height()),
xytext=(0, 4), textcoords="offset points",
ha="center", va="bottom", fontsize=9, color=trained_color, fontweight="bold",
annotation_clip=False)
ax.set_xticks(x)
ax.set_xticklabels([TASK_LABELS.get(t, t) for t in tasks], fontsize=10)
ax.set_ylim(0, 1.15)
ax.set_ylabel(ylabel, fontsize=11)
ax.set_title(title, fontsize=14, fontweight="bold", pad=28)
ax.text(
0.5, 1.015, subtitle,
transform=ax.transAxes, ha="center", va="bottom",
fontsize=10.5, color="#444",
)
ax.grid(True, axis="y", alpha=0.25)
ax.set_axisbelow(True)
# Place legend below the x-axis label area so it never overlaps bar value labels.
leg = ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.18),
ncol=2, fontsize=10, frameon=False)
if base_zero_label:
ax.text(0.012, -0.16, base_zero_label,
transform=ax.transAxes, fontsize=9, color="#666",
ha="left", va="top", style="italic")
fig.tight_layout()
out_path = EVAL_DIR / fname
fig.savefig(out_path, dpi=170)
fig.savefig(out_path.with_suffix(".svg"))
plt.close(fig)
print("wrote", out_path)
def main() -> None:
grouped_bar(
metric_key="mean_reward",
ylabel="Mean Membrane reward (0–1)",
title="Trained adapter vs base model - Membrane reward by task",
subtitle=("Same Qwen2.5 1.5 B weights, same prompts. Only difference: the trained LoRA "
"is switched on for the right-hand bars."),
fname="reward_by_task.png",
base_zero_label="Base bars are striped because the base model scored 0.00 on every task - see JSONL validity below.",
)
grouped_bar(
metric_key="valid_jsonl_rate",
ylabel="Fraction of completions that are valid Membrane JSONL",
title="JSONL action format - base model can't follow the schema",
subtitle=("Base: 0 % parseable Membrane actions on every task. "
"Trained: 100 % parseable on every task."),
fname="valid_jsonl_by_task.png",
base_zero_label="0 % parseable actions = 0 reward, regardless of intent.",
)
grouped_bar(
metric_key="commit_rate",
ylabel="Fraction of episodes that emit a COMMIT action",
title="Task completion - only the trained model ever closes the task",
subtitle=("A COMMIT closes the episode. Base never commits (no valid actions). "
"Trained commits in 100 % of rollouts."),
fname="commit_rate_by_task.png",
base_zero_label=None,
)
if __name__ == "__main__":
main()
|