affine-god/1111 / predict_influence.py
affine-god's picture
download
raw
5.09 kB
"""Which thoughts will be CATASTROPHIC? A different target from predict_l2.py.
`predict_l2.py` asked which of two thoughts scores higher. That is hard: TF-IDF
manages 60.5% and a 30B analyst did no better, so the branch was closed.
But the influence law says the leaderboard is not decided by winning the typical
row -- it is decided by how often a model detonates:
corr( mean L2 , P(L2 < -0.05) ) = -0.963 over 19 submitted models
So the useful question is not "is this thought better?" but "is this thought
about to cost 0.05+?". That should be far easier, because it is a question about
how DIRECTIVE a thought is -- a surface property -- rather than about whether it
happens to match an unobserved sample from the teacher.
Two things come out of this:
* a number -- can catastrophe be predicted at all, out of sample, on turns the
model never saw? If not, the influence law has no textual handle and any
intervention has to be behavioural rather than targeted.
* a description -- the features that carry the signal ARE the behaviour to
suppress, which is what a weight edit has to be aimed at.
Held out BY TURN, never by row: two thoughts on the same turn share a target and
would leak straight across the split.
python3 harness/predict_influence.py --dumps ../evals/chal-005*.json.gz
"""
from __future__ import annotations
import argparse
import collections
import gzip
import hashlib
import json
import pathlib
import re
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
def held_out(turn: str, frac: float) -> bool:
h = hashlib.sha256(turn.encode()).digest()
return (int.from_bytes(h[:4], "big") / 2**32) < frac
def load(paths: list[pathlib.Path], threshold: float) -> list[dict]:
rows = []
for p in paths:
if p.name == "index.jsonl":
continue
d = json.load(gzip.open(p))
refs = d.get("teacher_refs") or {}
for side in ("king_rows", "challenger_rows"):
for r in d.get(side) or []:
tref = refs.get(r["turn_id"]) or []
for i, pr in enumerate(r.get("pairs") or []):
if i >= len(tref):
continue
z = (pr.get("z_a") or "").strip()
if not z:
continue
l2 = pr["lpC_yc_za"] - pr["lpC_yc_e"]
rows.append({"turn": r["turn_id"], "z": z, "l2": l2,
"bad": int(l2 < -threshold)})
return rows
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--dumps", nargs="+", required=True)
ap.add_argument("--threshold", type=float, default=0.05)
ap.add_argument("--frac", type=float, default=0.3)
args = ap.parse_args()
rows = load([pathlib.Path(x) for x in args.dumps], args.threshold)
tr = [r for r in rows if not held_out(r["turn"], args.frac)]
te = [r for r in rows if held_out(r["turn"], args.frac)]
base = np.mean([r["bad"] for r in te])
print(f"rows {len(rows)} train {len(tr)} test {len(te)} "
f"(disjoint turns) base rate P(L2<-{args.threshold}) = {base:.2%}")
vec = TfidfVectorizer(ngram_range=(1, 2), min_df=5, max_features=60000,
sublinear_tf=True, strip_accents="unicode")
Xtr = vec.fit_transform([r["z"] for r in tr])
Xte = vec.transform([r["z"] for r in te])
ytr = np.array([r["bad"] for r in tr])
yte = np.array([r["bad"] for r in te])
clf = LogisticRegression(max_iter=2000, C=1.0, class_weight="balanced")
clf.fit(Xtr, ytr)
p = clf.predict_proba(Xte)[:, 1]
auc = roc_auc_score(yte, p)
print(f"\nheld-out AUC {auc:.4f} (0.5 = no signal)")
print(f"\n{'top decile':<14} {'n':>6} {'P(catastrophe)':>16} {'lift':>7}")
order = np.argsort(-p)
for name, sel in (("riskiest 10%", order[:len(order)//10]),
("riskiest 25%", order[:len(order)//4]),
("safest 25%", order[-len(order)//4:]),
("safest 10%", order[-len(order)//10:])):
rate = yte[sel].mean()
print(f"{name:<14} {len(sel):>6} {rate:>15.2%} {rate/base:>7.2f}x")
# mean L2 of each slice: the quantity that actually decides the crown
l2te = np.array([r["l2"] for r in te])
print(f"\n{'slice':<14} {'mean L2':>10}")
print(f"{'all':<14} {l2te.mean():>+10.5f}")
print(f"{'riskiest 10%':<14} {l2te[order[:len(order)//10]].mean():>+10.5f}")
print(f"{'safest 90%':<14} {l2te[order[len(order)//10:]].mean():>+10.5f}")
print(" -> the gap between 'all' and 'safest 90%' is what a perfect filter "
"would be worth")
names = np.array(vec.get_feature_names_out())
co = clf.coef_[0]
print(f"\n{'RAISES risk':<34} {'LOWERS risk':<34}")
hi, lo = names[np.argsort(-co)[:18]], names[np.argsort(co)[:18]]
for a, b in zip(hi, lo):
print(f" {a:<32} {b:<32}")
if __name__ == "__main__":
main()

Xet Storage Details

Size:
5.09 kB
·
Xet hash:
71fe778994c4890abac22733c01d7eaf34e7c88d6703032b9ad119f6fdccfc79

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