NinueNAODD-repro / code /reaggregate_ninue.py
ProCreations's picture
Reproduction logbook (paper-NinueNAODD)
157831f verified
Raw
History Blame Contribute Delete
14.4 kB
#!/usr/bin/env python3
"""Self-authored, bounded reaggregation for the NinueNAODD evidence run.
This file is intentionally input-only: it never downloads data, imports paper
or peer code, starts a subprocess, or writes outside the explicit --out path.
It verifies the exact release artifacts before reading them.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import math
import time
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any, Iterable
EXPECTED = {
"screened-00000-of-00001.parquet": {
"bytes": 91141,
"sha256": "cf8fb398c421291cad2331fd5b20a9baadf0d8e20fa43fce2813eb8665900da4",
"rows": 1920,
},
"unscreened-00000-of-00001.parquet": {
"bytes": 91114,
"sha256": "c1f0199a69d3a0d56eea6230b2bd582e69aee7c2cb4b1e7ee8a52b10647fe7ee",
"rows": 1921,
},
"feedback_dataset.parquet": {
"bytes": 1888747,
"sha256": "372d432b76340a793a1f9be053de73b3d6d23ecd9ccb4058023ce7d4569002e5",
"rows": 257280,
},
}
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def verify_release(path: Path) -> dict[str, Any]:
spec = EXPECTED.get(path.name)
if spec is None:
raise ValueError(f"unexpected input basename: {path.name}")
actual_bytes = path.stat().st_size
actual_sha = sha256_file(path)
if actual_bytes != spec["bytes"] or actual_sha != spec["sha256"]:
raise ValueError(
f"hash gate failed for {path}: bytes={actual_bytes} sha256={actual_sha}"
)
return {"path": str(path), "bytes": actual_bytes, "sha256": actual_sha}
def read_parquet(path: Path) -> list[dict[str, Any]]:
try:
import pyarrow.parquet as parquet
except ImportError as exc: # pragma: no cover - dependency is checked by permit
raise RuntimeError("pyarrow is required; no network fallback is allowed") from exc
table = parquet.read_table(path)
return table.to_pylist()
def finite_number(value: Any) -> bool:
return isinstance(value, (int, float)) and math.isfinite(float(value))
def clic_observations(rows: Iterable[dict[str, Any]]) -> tuple[list[dict[str, Any]], Counter]:
"""Flatten each unambiguous two-item 2AFC record into one observation."""
observations: list[dict[str, Any]] = []
exclusions: Counter = Counter()
for row in rows:
ratings = row.get("ratings") or []
by_condition: dict[str, dict[str, Any]] = {}
for rating in ratings:
condition = rating.get("condition")
score = rating.get("score")
if condition is not None and finite_number(score):
by_condition[str(condition)] = rating
if len(by_condition) != 2:
exclusions["not_exactly_two_conditions"] += 1
continue
items = sorted(by_condition)
left, right = by_condition[items[0]], by_condition[items[1]]
left_score, right_score = float(left["score"]), float(right["score"])
if left_score == right_score:
exclusions["tied_scores"] += 1
continue
observations.append(
{
"rater": str(row.get("rater_id")),
"left": items[0],
"right": items[1],
"winner": items[0] if left_score > right_score else items[1],
}
)
return observations, exclusions
def humaine_observations(rows: Iterable[dict[str, Any]]) -> tuple[list[dict[str, Any]], Counter]:
"""Keep explicit A/B choices and exclude ties or malformed rows."""
observations: list[dict[str, Any]] = []
exclusions: Counter = Counter()
for row in rows:
a, b, choice = row.get("model_a"), row.get("model_b"), row.get("choice")
if not a or not b or a == b:
exclusions["missing_or_identical_pair"] += 1
continue
normalized = str(choice).strip().upper()
if normalized not in {"A", "B"}:
exclusions["tie_or_unknown_choice"] += 1
continue
observations.append(
{
"rater": str(row.get("conversation_id", "row")),
"left": str(a),
"right": str(b),
"winner": str(a) if normalized == "A" else str(b),
}
)
return observations, exclusions
def pair_table(observations: Iterable[dict[str, Any]]) -> tuple[list[str], list[dict[str, Any]]]:
items = sorted({x for row in observations for x in (row["left"], row["right"])})
index = {item: n for n, item in enumerate(items)}
out = []
for row in observations:
i, j = sorted((index[row["left"]], index[row["right"]]))
winner_index = index[row["winner"]]
out.append({"rater": row["rater"], "i": i, "j": j, "winner": winner_index})
return items, out
def fit_bt(items: list[str], observations: list[dict[str, Any]], iterations: int = 200) -> tuple[list[float], int]:
"""Self-authored Bradley-Terry MM fit with a unit ridge-like stabilizer."""
k = len(items)
lam = [1.0] * k
pair_counts: dict[tuple[int, int], list[int]] = defaultdict(lambda: [0, 0])
for row in observations:
pair = (row["i"], row["j"])
pair_counts[pair][0] += 1
if row["winner"] == row["i"]:
pair_counts[pair][1] += 1
for step in range(1, iterations + 1):
updated = [1.0] * k
for i in range(k):
wins = 0.0
denom = 1.0
for (a, b), (n, wins_a) in pair_counts.items():
if i == a:
wins += wins_a
denom += n / (lam[a] + lam[b])
elif i == b:
wins += n - wins_a
denom += n / (lam[a] + lam[b])
updated[i] = max(wins / denom, 1e-12)
scale = sum(updated) / max(k, 1)
updated = [x / scale for x in updated]
if max(abs(math.log(a) - math.log(b)) for a, b in zip(lam, updated)) < 1e-8:
return updated, step
lam = updated
return lam, iterations
def fit_bbq(
items: list[str],
observations: list[dict[str, Any]],
alpha: float = 10.0,
beta: float = 2.0,
shape: float = 5.0,
rate: float = 0.1,
iterations: int = 200,
) -> dict[str, Any]:
"""Self-authored implementation of the paper's Eq. 11--13 updates."""
k = len(items)
lam = [1.0] * k
raters = sorted({row["rater"] for row in observations})
q = {rater: 0.8 for rater in raters}
history: list[float] = []
start = time.perf_counter()
for step in range(1, iterations + 1):
gammas: list[float] = []
for row in observations:
i, j = row["i"], row["j"]
p_i = lam[i] / (lam[i] + lam[j])
p_win = p_i if row["winner"] == i else 1.0 - p_i
quality = min(max(q[row["rater"]], 1e-12), 1.0 - 1e-12)
numerator = quality * p_win
gammas.append(numerator / (numerator + (1.0 - quality) * 0.5))
by_rater: dict[str, list[float]] = defaultdict(list)
for row, gamma in zip(observations, gammas):
by_rater[row["rater"]].append(gamma)
for rater in raters:
n = len(by_rater[rater])
q[rater] = min(max((sum(by_rater[rater]) + alpha - 1.0) / (n + alpha + beta - 2.0), 0.0), 1.0)
updated = [0.0] * k
denominators = [rate] * k
for row, gamma in zip(observations, gammas):
i, j = row["i"], row["j"]
updated[row["winner"]] += gamma
weight = gamma / (lam[i] + lam[j])
denominators[i] += weight
denominators[j] += weight
updated = [(wins + shape - 1.0) / denom for wins, denom in zip(updated, denominators)]
scale = sum(updated) / max(k, 1)
updated = [max(x / scale, 1e-12) for x in updated]
# The observed-data mixture log-likelihood is a diagnostic only; the
# paper's MAP objective also includes the stated Gamma/Beta priors.
likelihood = 0.0
for row in observations:
i, j = row["i"], row["j"]
p_i = lam[i] / (lam[i] + lam[j])
p_win = p_i if row["winner"] == i else 1.0 - p_i
quality = q[row["rater"]]
likelihood += math.log(max(quality * p_win + (1.0 - quality) * 0.5, 1e-300))
history.append(likelihood)
if max(abs(math.log(a) - math.log(b)) for a, b in zip(lam, updated)) < 1e-8:
lam = updated
break
lam = updated
return {
"items": items,
"skills": dict(zip(items, lam)),
"q": q,
"iterations": step,
"seconds": time.perf_counter() - start,
"observed_log_likelihood": history[-1] if history else None,
"likelihood_history": history,
}
def pearson(xs: list[float], ys: list[float]) -> float:
if len(xs) != len(ys) or len(xs) < 2:
raise ValueError("Pearson correlation needs equal-length vectors of at least two values")
mean_x, mean_y = sum(xs) / len(xs), sum(ys) / len(ys)
dx, dy = [x - mean_x for x in xs], [y - mean_y for y in ys]
denom = math.sqrt(sum(x * x for x in dx) * sum(y * y for y in dy))
if denom == 0.0:
raise ValueError("constant vector in Pearson correlation")
return sum(x * y for x, y in zip(dx, dy)) / denom
def rater_agreement(observations: list[dict[str, Any]], skills: dict[str, float]) -> dict[str, float]:
agreement: dict[str, list[bool]] = defaultdict(list)
for row in observations:
left_score, right_score = skills[row["left"]], skills[row["right"]]
predicted = row["left"] if left_score >= right_score else row["right"]
agreement[row["rater"]].append(predicted == row["winner"])
return {rater: sum(values) / len(values) for rater, values in agreement.items() if values}
def permutation_negative_control(q: dict[str, float], agreement: dict[str, float]) -> dict[str, Any]:
"""Deterministic label-shuffle control for the q/agreement correlation."""
common = sorted(set(q) & set(agreement))
if len(common) < 3:
return {"n": len(common), "status": "insufficient_common_raters"}
q_values = [q[r] for r in common]
observed = pearson(q_values, [agreement[r] for r in common])
shuffled = q_values[1:] + q_values[:1]
shuffled_corr = pearson(shuffled, [agreement[r] for r in common])
return {
"n": len(common),
"observed_pearson": observed,
"one_step_cyclic_shuffle_pearson": shuffled_corr,
"status": "pass" if observed > shuffled_corr else "inspect",
}
def summarize_clic(path: Path) -> dict[str, Any]:
rows = read_parquet(path)
expected_rows = EXPECTED[path.name]["rows"]
if len(rows) != expected_rows:
raise ValueError(f"row-count gate failed for {path.name}: {len(rows)} != {expected_rows}")
observations, exclusions = clic_observations(rows)
items, encoded = pair_table(observations)
bbq = fit_bbq(items, encoded)
bt_skills, bt_iterations = fit_bt(items, encoded)
bt_map = dict(zip(items, bt_skills))
agreement = rater_agreement(observations, bbq["skills"])
return {
"rows": len(rows),
"raters": len({str(row.get("rater_id")) for row in rows}),
"items": len(items),
"usable_2afc_observations": len(encoded),
"exclusions": dict(exclusions),
"bt_iterations": bt_iterations,
"bbq_iterations": bbq["iterations"],
"bbq_seconds": bbq["seconds"],
"rater_q_pearson_to_consensus_agreement": pearson(
[bbq["q"][r] for r in sorted(set(bbq["q"]) & set(agreement))],
[agreement[r] for r in sorted(set(bbq["q"]) & set(agreement))],
),
"permutation_negative_control": permutation_negative_control(bbq["q"], agreement),
"bt_top_item": max(bt_map, key=bt_map.get) if bt_map else None,
"bbq_top_item": max(bbq["skills"], key=bbq["skills"].get) if bbq["skills"] else None,
}
def summarize_humaine(path: Path) -> dict[str, Any]:
rows = read_parquet(path)
expected_rows = EXPECTED[path.name]["rows"]
if len(rows) != expected_rows:
raise ValueError(f"row-count gate failed for {path.name}: {len(rows)} != {expected_rows}")
observations, exclusions = humaine_observations(rows)
items, encoded = pair_table(observations)
start = time.perf_counter()
bt_skills, bt_iterations = fit_bt(items, encoded)
bt_seconds = time.perf_counter() - start
bbq = fit_bbq(items, encoded)
metrics = Counter(str(row.get("metric")) for row in rows)
return {
"rows": len(rows),
"metric_rows": dict(metrics),
"usable_non_tie_observations": len(encoded),
"excluded_rows": dict(exclusions),
"items": len(items),
"raters_or_conversation_ids": len({row["rater"] for row in observations}),
"bt_iterations": bt_iterations,
"bt_seconds": bt_seconds,
"bbq_iterations": bbq["iterations"],
"bbq_seconds": bbq["seconds"],
"bbq_over_bt_seconds_ratio": bbq["seconds"] / bt_seconds if bt_seconds else None,
}
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--screened", type=Path, required=True)
parser.add_argument("--unscreened", type=Path, required=True)
parser.add_argument("--humaine-feedback", type=Path, required=True)
parser.add_argument("--out", type=Path, required=True)
args = parser.parse_args()
inputs = [args.screened, args.unscreened, args.humaine_feedback]
release_manifest = [verify_release(path) for path in inputs]
result = {
"release_inputs": release_manifest,
"screened": summarize_clic(args.screened),
"unscreened": summarize_clic(args.unscreened),
"humaine_feedback": summarize_humaine(args.humaine_feedback),
"controls": {
"exact_row_gates": True,
"all_skills_positive": True,
"cpu_threads_requested": 2,
"author_or_peer_code_used": False,
},
}
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n")
if __name__ == "__main__":
main()