affine-god/1111 / reason_deployable.py
affine-god's picture
download
raw
6.36 kB
"""The two tests that decide whether the selective-speaking policy is real.
TEST 1 — PROMPT-ONLY. This is the deployability question, and it is forced by
the chat contract rather than by convenience. `evalsrv/chat.py` builds
`z = latent <think> content + the visible THOUGHT section`, so a model CANNOT
reason privately and then decide to stay quiet: any chain of thought it produces
is already the thing being scored. The speak/stay-silent decision therefore has
to be made from the PROMPT ALONE, before a single reasoning token exists.
If the signal lives only in the thought text, a classifier over thoughts is
a fine post-hoc diagnostic and an undeployable policy.
TEST 2 — TRANSFER. Every duel scores two models over ~2060 turns and slices
overlap heavily, so the same turn appears under many models. A plain
leave-model-out split leaves the held-out model's TURNS in training under
everyone else's rows, and a classifier can learn "turn X is hard" instead of
"this thought is good". Production slices are freshly seeded per duel
(`blake2b(reveal_block_hash || hotkey)`), so that knowledge is worth nothing.
Each fold here holds out a model AND restricts to trajectories absent from
training; the leaky variant is reported alongside so the gap measures the leak.
python3 harness/reason_deployable.py
"""
from __future__ import annotations
import argparse
import collections
import pathlib
import sys
import numpy as np
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
import reason_signal as R
def tfidf(train_txt, ytr, test_txt, min_df=10, maxf=40000):
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
tv = TfidfVectorizer(min_df=min_df, ngram_range=(1, 2), max_features=maxf,
sublinear_tf=True)
A = tv.fit_transform(train_txt)
B = tv.transform(test_txt)
lr = LogisticRegression(max_iter=3000, class_weight="balanced")
lr.fit(A, ytr)
return lr.predict_proba(B)[:, 1]
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--data", default=str(R.ROOT / "data" / "reason.jsonl.gz"))
ap.add_argument("--folds", type=int, default=4)
args = ap.parse_args()
rows = R.load(args.data, drop_silent=True)
import screen
corpus = screen.load_corpus()
kept = []
for r in rows:
c = corpus.get(r["turn_id"])
msgs = (c or {}).get("prefix") or []
if not msgs:
continue
r["_prompt"] = " ".join(str(m.get("content") or "") for m in msgs[-2:])[-1500:]
kept.append(r)
if len(kept) < 0.5 * len(rows):
raise SystemExit(f"prompt joined for only {len(kept)}/{len(rows)} rows")
print(f"{len(kept)} pairs with a joined prompt "
f"({100*len(kept)/len(rows):.1f}% of {len(rows)})\n")
tr, va, te = R.split(kept, 0)
ytr = np.array([r["reason"] > 0 for r in tr], dtype=int)
yte = np.array([r["reason"] > 0 for r in te], dtype=int)
r_te = np.array([r["reason"] for r in te])
r_va = np.array([r["reason"] for r in va])
yva = np.array([r["reason"] > 0 for r in va], dtype=int)
print("=== TEST 1 — PROMPT-ONLY (the deployable policy) ===")
p_va = tfidf([r["_prompt"] for r in tr], ytr, [r["_prompt"] for r in va])
p_te = tfidf([r["_prompt"] for r in tr], ytr, [r["_prompt"] for r in te])
R.show("prompt-only (val)", R.metrics(yva, p_va))
R.show("prompt-only (test)", R.metrics(yte, p_te))
grid = np.quantile(p_va, np.linspace(0.0, 0.98, 40))
best = max(R.policy_curve(r_va, p_va, grid), key=lambda c: c["margin"])
print(f"\n best VAL threshold {best['thr']:.4f}: speak {100*best['speak']:.1f}% "
f"hit {100*best['hit']:.1f}% margin {best['margin']:+.6f}")
print(" frozen on TEST:")
print(f" {'thr':>7} {'speak%':>8} {'hit%':>7} {'meanReason':>12} "
f"{'margin':>11} {'bar':>10} crowns")
for c in R.policy_curve(r_te, p_te, [best["thr"]]):
print(f" {c['thr']:>7.4f} {100*c['speak']:>7.1f}% {100*c['hit']:>6.1f}% "
f"{c['mean']:>+12.6f} {c['margin']:>+11.6f} {c['bar']:>10.6f} "
f"{'YES' if c['crowns'] else 'no'}")
trajs = np.array([r["traj_id"] for r in te])
lo, hi = R.boot_margin(r_te, p_te, best["thr"], trajs)
print(f" trajectory bootstrap 95% CI: [{lo:+.6f}, {hi:+.6f}]")
print(" full TEST curve:")
for c in R.policy_curve(r_te, p_te, np.quantile(p_va, [.25, .5, .75, .9, .95])):
print(f" {c['thr']:>7.4f} {100*c['speak']:>7.1f}% {100*c['hit']:>6.1f}% "
f"{c['mean']:>+12.6f} {c['margin']:>+11.6f}")
print("\n=== TEST 2 — TRANSFER: unseen MODEL *and* unseen TRAJECTORY ===")
by_model = collections.defaultdict(list)
for r in kept:
by_model[r["model"]].append(r)
big = sorted(by_model, key=lambda m: -len(by_model[m]))[: args.folds]
print(f"{'held-out model':<38} {'n':>7} {'AUCleak':>8} {'AUChonest':>10} "
f"{'hit@top20%':>11}")
leak, honest = [], []
for m in big:
test = by_model[m]
tt = {r["traj_id"] for r in test}
other = [r for r in kept if r["model"] != m]
clean = [r for r in other if r["traj_id"] not in tt]
if not clean:
continue
yt = np.array([r["reason"] > 0 for r in test], dtype=int)
if len(set(yt)) < 2:
continue
pa = tfidf([r["z_a"] for r in other],
np.array([r["reason"] > 0 for r in other], dtype=int),
[r["z_a"] for r in test])
pb = tfidf([r["z_a"] for r in clean],
np.array([r["reason"] > 0 for r in clean], dtype=int),
[r["z_a"] for r in test])
ma, mb = R.metrics(yt, pa), R.metrics(yt, pb)
k = max(int(0.20 * len(pb)), 1)
hit = float(yt[np.argsort(pb)[-k:]].mean())
leak.append(ma["auc"]); honest.append(mb["auc"])
print(f"{m[:36]:<38} {mb['n']:>7} {ma['auc']:>8.4f} {mb['auc']:>10.4f} "
f"{hit:>11.4f}")
if honest:
print(f"\n mean AUC, turn leak allowed : {np.mean(leak):.4f}")
print(f" mean AUC, model+trajectory disjoint: {np.mean(honest):.4f}")
print(f" turn-memorisation leak : {np.mean(leak)-np.mean(honest):+.4f}")
if __name__ == "__main__":
main()

Xet Storage Details

Size:
6.36 kB
·
Xet hash:
c32dbd2b5ef10649f9e327536d82233396e4a0d4c0efe6ed9a92e0597190ba57

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.