agentic-rl-main / scripts /analysis /pcd_probe_controls.py
Jack04810's picture
Add files using upload-large-folder tool
d13981b verified
Raw History Blame Contribute Delete
8.7 kB
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from scripts.analysis.pcd_artifact_core import (
collect_run_data,
fmt_rate,
mean_or_none,
read_manifest,
write_csv,
)
VALID_MODES = ("teacher_only", "completion_conditioned", "shuffled_completion")
CSV_FIELDS = [
"control",
"n",
"teacher_correct_rate",
"parse_fail_rate",
"placeholder_rate",
"generated_tokens_mean",
"deplot_real_rate",
"status",
]
def _mode_list(raw: str) -> list[str]:
modes = [part.strip() for part in raw.split(",") if part.strip()]
unknown = [mode for mode in modes if mode not in VALID_MODES]
if unknown:
raise ValueError(f"unknown probe-control mode(s): {', '.join(unknown)}")
return modes
def _candidate_records(manifest_path: Path, variant: str, max_samples: int) -> list[dict[str, Any]]:
manifest_rows = read_manifest(manifest_path)
data = collect_run_data(manifest_rows)
records = data.get(variant, {}).get("candidates", [])
if max_samples > 0:
return records[:max_samples]
return records
def _student_output(record: dict[str, Any]) -> str:
for key in ("student_output", "completion", "response", "generated_text", "student_completion"):
value = record.get(key)
if value:
return str(value)
return ""
def _evidence(record: dict[str, Any]) -> str:
for key in ("deplot_evidence", "visual_evidence", "evidence", "deplot_text"):
value = record.get(key)
if value:
return str(value)
privileged = record.get("privileged") if isinstance(record.get("privileged"), dict) else {}
evidence = privileged.get("visual_fact_text") or privileged.get("deplot_text")
return str(evidence or "")
def build_prompt(record: dict[str, Any], mode: str, *, shuffled_record: dict[str, Any] | None = None) -> str:
question = str(record.get("question", ""))
evidence = _evidence(record)
lines = [
"You are checking a chart-question answer.",
f"Question: {question}",
]
if evidence:
lines.append(f"Visual evidence: {evidence}")
if mode == "completion_conditioned":
lines.append(f"Student completion: {_student_output(record)}")
elif mode == "shuffled_completion":
source = shuffled_record or record
lines.append(f"Student completion: {_student_output(source)}")
lines.append("Return the answer and keep reasoning concise.")
return "\n".join(lines)
def _shuffled_for(records: list[dict[str, Any]], index: int) -> dict[str, Any] | None:
if len(records) < 2:
return None
return records[(index + 1) % len(records)]
def _deplot_real_rate(records: list[dict[str, Any]]) -> float | None:
if not records:
return None
real = 0
for record in records:
privileged = record.get("privileged") if isinstance(record.get("privileged"), dict) else {}
if privileged.get("visual_fact_deplot_status") == "real":
real += 1
return real / len(records)
def _bool_rate(records: list[dict[str, Any]], key: str) -> float | None:
if not records:
return None
return sum(1 for record in records if record.get(key) is True) / len(records)
def _completion_conditioned_row(records: list[dict[str, Any]]) -> dict[str, Any]:
tokens = [
float(record.get("teacher_output_word_count", 0) or 0)
for record in records
if record.get("teacher_output_word_count") is not None
]
return {
"control": "completion_conditioned",
"n": len(records),
"teacher_correct_rate": fmt_rate(_bool_rate(records, "teacher_correct")),
"parse_fail_rate": fmt_rate(_bool_rate(records, "parse_failed")),
"placeholder_rate": fmt_rate(_bool_rate(records, "teacher_output_is_placeholder")),
"generated_tokens_mean": fmt_rate(mean_or_none(tokens)),
"deplot_real_rate": fmt_rate(_deplot_real_rate(records)),
"status": "from_candidate_log" if records else "missing_candidate_log",
}
def _missing_row(mode: str, records: list[dict[str, Any]], *, dry_run: bool) -> dict[str, Any]:
status = "dry_run_prompt_only" if dry_run else "missing_offline_teacher_run"
if not records:
status = "missing_candidate_log"
return {
"control": mode,
"n": len(records),
"teacher_correct_rate": "",
"parse_fail_rate": "",
"placeholder_rate": "",
"generated_tokens_mean": "",
"deplot_real_rate": fmt_rate(_deplot_real_rate(records)),
"status": status,
}
def _write_prompt_preview(path: Path, records: list[dict[str, Any]], modes: list[str]) -> None:
preview_path = path.with_name(path.stem + "_prompts.jsonl")
preview_path.parent.mkdir(parents=True, exist_ok=True)
with preview_path.open("w", encoding="utf-8") as f:
for mode in modes:
for index, record in enumerate(records):
payload = {
"control": mode,
"index": index,
"question": record.get("question", ""),
"prompt": build_prompt(record, mode, shuffled_record=_shuffled_for(records, index)),
}
f.write(json.dumps(payload, ensure_ascii=False) + "\n")
def _latex_escape(value: Any) -> str:
text = "" if value is None else str(value)
return text.replace("\\", "\\textbackslash{}").replace("&", "\\&").replace("%", "\\%").replace("_", "\\_")
def _write_booktabs(path: Path, rows: list[dict[str, Any]]) -> None:
headers = [
"Control",
"n",
"Recover ↑",
"Parse Fail ↓",
"Placeholder ↓",
"Tokens",
"DePlot Real ↑",
"Status",
]
body = []
for row in rows:
first = str(row["control"])
if row["control"] == "completion_conditioned":
first = "\\rowcolor{gray!10} " + first
body.append(
[
first,
row["n"],
row["teacher_correct_rate"],
row["parse_fail_rate"],
row["placeholder_rate"],
row["generated_tokens_mean"],
row["deplot_real_rate"],
row["status"],
]
)
lines = [
"```latex",
"\\begin{tabular}{lccccccc}",
"\\toprule",
" & ".join(_latex_escape(header) for header in headers) + " \\\\",
"\\midrule",
]
for row in body:
escaped = [_latex_escape(cell) for cell in row]
if str(row[0]).startswith("\\rowcolor"):
escaped[0] = str(row[0])
lines.append(" & ".join(escaped) + " \\\\")
lines.extend(["\\bottomrule", "\\end{tabular}", "```", ""])
path.write_text("\n".join(lines), encoding="utf-8")
def run_controls(
*,
manifest: Path,
variant: str,
modes: list[str],
max_samples: int,
out: Path,
dry_run: bool,
) -> None:
records = _candidate_records(manifest, variant, max_samples)
rows: list[dict[str, Any]] = []
for mode in modes:
if dry_run:
rows.append(_missing_row(mode, records, dry_run=True))
elif mode == "completion_conditioned":
rows.append(_completion_conditioned_row(records))
else:
rows.append(_missing_row(mode, records, dry_run=False))
write_csv(out, rows, CSV_FIELDS)
_write_booktabs(out.with_suffix(".md"), rows)
if dry_run:
_write_prompt_preview(out, records, modes)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Build PCD-OPD recoverability-control table rows.")
parser.add_argument("--manifest", type=Path, required=True)
parser.add_argument("--variant", default="deplot_no_vs_opd_pcd")
parser.add_argument("--modes", default="teacher_only,completion_conditioned,shuffled_completion")
parser.add_argument("--max-samples", type=int, default=512)
parser.add_argument("--out", type=Path, required=True)
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args(argv)
try:
modes = _mode_list(args.modes)
except ValueError as exc:
parser.error(str(exc))
run_controls(
manifest=args.manifest,
variant=args.variant,
modes=modes,
max_samples=args.max_samples,
out=args.out,
dry_run=args.dry_run,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())