"""
=============================================================================
CS486 · 第 4 讲 · 代码 02：验证器——检查比生成容易吗？过程监督 vs 结果监督
=============================================================================

【这个脚本要回答的问题】（讲义 §4–§5）
    代码 01：多采样能把"对的答案在不在样本里"（覆盖率）提高，但投票只能挑出一部分。
    用一个**验证器**给每个样本打分、挑分数最高的（best-of-N），能挑得更好吗？
    我们训练三个验证器（同样的小 Transformer，104,385 个参数；数据都是策略模型自己的 8000 条采样 + 自动标注）：
        ORM      ：结果奖励模型——在答案位置预测"最终答案对不对"（Cobbe et al. 2021）
        PRM      ：过程奖励模型——在推理链每一步预测"这一步的查找对不对"（Lightman et al. 2023）
        PRM-full ：过程奖励模型，但"过程"的定义更完整：除了查找对不对，还包括"是不是已经走过了 k 步"，
                   并在 "|" 处判断"在这里停下对不对"
    代码 01 发现，长题面上大量失败是"每一步查找都对、只是没有及时停下"——
    PRM 的标签只看查找，它能看出这种错误吗？

    演示 1：验证器的质量（留出数据上的 AUC），以及"生成–验证差距"：策略模型写错的步骤，验证器认得出来吗？
    演示 2：best-of-N 与投票（温度 1.0），N = 1、4、16、32。
    演示 3：温度 1.5（覆盖率更高、单个样本更差）时的同一张表——best-of-N 会随 N 变差吗？
    演示 4：验证器选错的时候，选中的是什么样的样本？

【运行】
    python3 02_verifiers.py           # 第一次约 2.5 分钟（并行训练 3 个验证器），之后约 40 秒
    python3 02_verifiers.py --quick   # 冒烟测试

【依赖】PyTorch + 本目录的 ttc_lab.py + 第 1、2 讲的模块。
"""

import argparse
import os
import sys
import time

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "lecture01"))

import hop_world as hw                                                   # noqa: E402
import tiny_reasoner as tr                                               # noqa: E402
import ttc_lab as tl                                                     # noqa: E402
from console import pct, print_header, print_observations, print_table  # noqa: E402

KS = (3, 4, 5, 6)
NS = (1, 4, 16, 32)
METHODS = ("随机挑一个", "多数投票", "ORM 加权投票", "ORM 选最高", "PRM 选最高", "PRM-full 选最高", "覆盖率（上限）")


def auc(pos, neg, cap=3000):
    """AUC：随机一个正例的分数高于随机一个负例的概率（各最多取 cap 个，够用了）。"""
    pos, neg = pos[:cap], neg[:cap]
    if not pos or not neg:
        return float("nan")
    wins = sum((a > b) + 0.5 * (a == b) for a in pos for b in neg)
    return wins / (len(pos) * len(neg))


def step_auc(scores, examples, y_key):
    """逐步标签的 AUC：分数是"这一步对"的概率，正例 = 对的步骤。"""
    pos, neg = [], []
    for sc, e in zip(scores, examples):
        for s, y in zip(sc, e[y_key]):
            (pos if y else neg).append(s)
    return auc(pos, neg), len(neg) / max(1, len(pos) + len(neg))


def chain_auc(vers, data):
    """
    整条推理链的 AUC：三个验证器的"整条链分数"（PRM 取各步乘积、ORM 取答案处的分数）区分"最终答案对 / 错"的能力。

    【思路】三个验证器的逐步标签定义不同，不能直接比较逐步 AUC；但它们最终都要用来**挑整条推理链**，
        所以用同一个标准（最终答案对不对）比较它们的整条链分数。
    【返回】dict：验证器名 → AUC
    """
    y = [e["final_y"] for e in data]
    out = {}
    for kind in ("prm", "prm_full", "orm"):
        s = tl.score(vers[kind], data, kind)
        agg = s if kind == "orm" else [tl.prm_aggregate(x) for x in s]
        out[kind] = auc([a for a, b in zip(agg, y) if b], [a for a, b in zip(agg, y) if not b])
    return out


def demo1(vers, held, test_examples, test15):
    print_header("演示 1：验证器的质量（AUC：0.5 = 抛硬币，1.0 = 完美）")
    print("(a) 逐步标签的 AUC（每个验证器按它自己的标签评估）：")
    rows, out = [], {}
    for name, data in (("留出数据（n = 4–12，和训练同分布）", held), ("测试样本（n = 12，代码 01 的题）", test_examples)):
        sp = tl.score(vers["prm"], data, "prm")
        sf = tl.score(vers["prm_full"], data, "prm_full")
        so = tl.score(vers["orm"], data, "orm")
        a_p, neg_p = step_auc(sp, data, "step_y")
        a_f, neg_f = step_auc(sf, data, "full_y")
        a_o = auc([s for s, e in zip(so, data) if e["final_y"]], [s for s, e in zip(so, data) if not e["final_y"]])
        out[name] = (a_p, a_f, a_o, neg_p, neg_f)
        rows.append([name, f"{a_p:.3f}（错步 {pct(neg_p)}）", f"{a_f:.3f}（错步 {pct(neg_f)}）", f"{a_o:.3f}"])
    print_table(["数据", "PRM：查找对不对", "PRM-full：完整的步骤标签", "ORM：最终答案对不对"], rows, [34, 20, 24, 20])
    print("（括号里是这份数据中'错的步骤'所占的比例——正负样本不平衡时，用 AUC 而不是准确率衡量验证器。）")
    print("\n(b) 整条推理链的 AUC（三个验证器的整条链分数，区分'最终答案对 / 错'）：")
    rows = []
    for name, data in (("测试样本，温度 1.0", test_examples), ("测试样本，温度 1.5", test15)):
        c = chain_auc(vers, data)
        out[("chain", name)] = c
        rows.append([name, f"{c['prm']:.3f}", f"{c['prm_full']:.3f}", f"{c['orm']:.3f}"])
    print_table(["数据", "PRM", "PRM-full", "ORM"], rows, [22, 8, 10, 8])
    return out


def select_all(probs, samples, scores, n):
    """对每道题用前 n 个样本，算每种选择方法是否选对。【返回】dict：方法 → 准确率"""
    acc = dict.fromkeys(METHODS, 0.0)
    for i, (p, outs) in enumerate(zip(probs, samples)):
        outs = outs[:n]
        ans = [hw.parse_answer(o, "cot") for o in outs]
        ok = [a == p.answer for a in ans]
        so = scores["orm"][i][:n]
        sp = [tl.prm_aggregate(x) for x in scores["prm"][i][:n]]
        sf = [tl.prm_aggregate(x) for x in scores["prm_full"][i][:n]]
        acc["随机挑一个"] += sum(ok) / n
        acc["多数投票"] += tl.majority_vote(ans) == p.answer
        acc["ORM 加权投票"] += tl.majority_vote(ans, so) == p.answer
        acc["ORM 选最高"] += ok[max(range(n), key=lambda j: so[j])]
        acc["PRM 选最高"] += ok[max(range(n), key=lambda j: sp[j])]
        acc["PRM-full 选最高"] += ok[max(range(n), key=lambda j: sf[j])]
        acc["覆盖率（上限）"] += any(ok)
    return {m: v / len(probs) for m, v in acc.items()}


def bon_table(title, probs, samples, vers):
    print_header(title)
    flat = [tl.verifier_example(p, o) for p, outs in zip(probs, samples) for o in outs]
    N = len(samples[0])
    sc = {}
    for kind in ("prm", "prm_full", "orm"):
        s = tl.score(vers[kind], flat, kind)
        sc[kind] = [s[i * N:(i + 1) * N] for i in range(len(probs))]
    res = {n: select_all(probs, samples, sc, n) for n in NS if n <= N}
    rows = [[m] + [pct(res[n][m]) for n in res] for m in METHODS]
    print_table(["选择方法"] + [f"N = {n}" for n in res], rows, [18] + [10] * len(res))
    return res, flat


def pick_type(p, output):
    """一条被选中的样本属于哪一类（和代码 01 的 failure_type 一致，另外区分"没写 '|'"）。"""
    if tl.is_correct(p, output):
        return "正确"
    trace = tl.trace_of(output)
    if hw.ANSWER_MARK not in output:
        return "一直没停下（没写 '|'）"
    if all(tl.step_labels(p, trace)):
        if len(trace) > p.k:
            return "查找都对，但走过头了"
        return "查找都对，但停早了" if len(trace) < p.k else "推理链对，答案抄错了"
    return "有一步查找错了"


def demo4(probs, samples10, samples15, vers):
    print_header("演示 4：验证器'选最高'选错时，选中的是什么样的样本？（N = 32，只看有正确样本的题）")
    cats = ("一直没停下（没写 '|'）", "查找都对，但走过头了", "查找都对，但停早了", "推理链对，答案抄错了", "有一步查找错了")
    rows, out, example = [], {}, None
    for T, samples in ((1.0, samples10), (1.5, samples15)):
        flat = [tl.verifier_example(p, o) for p, outs in zip(probs, samples) for o in outs]
        N = len(samples[0])
        so = tl.score(vers["orm"], flat, "orm")
        for kind, label in (("orm", "ORM"),):
            cnt, n_cov = {c: 0 for c in cats}, 0
            for i, (p, outs) in enumerate(zip(probs, samples)):
                if not any(tl.is_correct(p, o) for o in outs):
                    continue
                n_cov += 1
                sc = so[i * N:(i + 1) * N]
                j = max(range(N), key=lambda q: sc[q])
                t = pick_type(p, outs[j])
                if t != "正确":
                    cnt[t] += 1
                    if T == 1.5 and t == "推理链对，答案抄错了" and example is None:
                        example = (p, outs[j], sc[j])
            out[(T, label)] = (cnt, n_cov)
            rows.append([f"温度 {T}，{label}"] + [cnt[c] for c in cats] + [n_cov])
    print_table(["设置"] + list(cats) + ["有正确样本的题"], rows, [14, 20, 18, 16, 18, 14, 14])
    if example:
        p, o, sc = example
        print(f"\n例（温度 1.5）：{p.prompt}   正确推理链 {p.trace}|{p.answer}")
        print(f"    ORM 选中 {o.split(hw.END)[0]!r}，打分 {sc:.3f}——推理链完全正确，'|' 后面的答案却写成了别的字母。")
    return out, example


def main():
    ap = argparse.ArgumentParser(description="验证器与 best-of-N")
    ap.add_argument("--quick", action="store_true", help="冒烟测试")
    args = ap.parse_args()
    t0 = time.perf_counter()
    per_k, N = (5, 4) if args.quick else (50, 32)

    print_header("准备：策略模型、测试题、三个验证器")
    policy = tr.get_model("cot_L2", quick=args.quick)[0]
    vers = tl.load_verifiers(quick=args.quick)
    probs = []
    for k in KS:
        probs += hw.make_eval_set(12, k, per_k, seed=21)
    samples10 = tl.sample_many(policy, probs, N, 1.0, seed=101)       # 和代码 01 的温度 1.0 样本相同
    samples15 = tl.sample_many(policy, probs, N, 1.5, seed=102)       # 和代码 01 的温度 1.5 样本相同

    held = tl.held_out_examples() if not args.quick else [tl.verifier_example(probs[0], samples10[0][0])]
    flat10 = [tl.verifier_example(p, o) for p, outs in zip(probs, samples10) for o in outs]
    flat15 = [tl.verifier_example(p, o) for p, outs in zip(probs, samples15) for o in outs]
    q = demo1(vers, held, flat10, flat15)
    res10, _ = bon_table(f"演示 2：best-of-N 与投票（温度 1.0；{len(probs)} 道题 × {N} 个样本，用每道题的前 N 个）",
                         probs, samples10, vers)
    res15, _ = bon_table("演示 3：同样的表，温度 1.5（覆盖率更高，单个样本更差）", probs, samples15, vers)
    picks, example = demo4(probs, samples10, samples15, vers)

    if args.quick:
        print("\n（--quick 冒烟测试：数字没有意义，不打印观察。）")
        print(f"\n⏱ 总用时 {time.perf_counter() - t0:.0f} 秒")
        return
    hk = "留出数据（n = 4–12，和训练同分布）"
    tk = "测试样本（n = 12，代码 01 的题）"
    c10 = q[("chain", "测试样本，温度 1.0")]
    c15 = q[("chain", "测试样本，温度 1.5")]
    r10, r15 = res10[32], res15[32]
    cnt15, ncov15 = picks[(1.5, "ORM")]
    print_observations([
        f"验证器本身很强（演示 1）：在 n = 12 的测试样本上，ORM 区分最终对错的 AUC 是 {c10['orm']:.3f}，PRM-full 是 "
        f"{c10['prm_full']:.3f}。策略模型在这些长题面上大多做错（代码 01），而一个同样大小的模型却能认出大部分错误——"
        "检查'x>y 在不在题面里'可以对所有步骤**并行**完成，生成推理链却是串行的：生成–验证的不对称。"
        f"只看查找的 PRM 整条链 AUC 只有 {c10['prm']:.3f}：它的标签里没有'该停下了'，看不出走过头的推理链。",
        f"但在温度 1.0（演示 2）下，验证器没有带来任何好处：覆盖率上限只有 {pct(r10['覆盖率（上限）'])}，多数投票已经是 "
        f"{pct(r10['多数投票'])}；PRM / PRM-full / ORM 选最高分别是 {pct(r10['PRM 选最高'])} / {pct(r10['PRM-full 选最高'])} / "
        f"{pct(r10['ORM 选最高'])}。可挑的空间只有 1 个点时，验证器偶尔把错的排在对的前面，就已经输给了投票。",
        f"温度 1.5（演示 3）时覆盖率上限升到 {pct(r15['覆盖率（上限）'])}，可是验证器'选最高'**随 N 增大反而变差**："
        f"ORM 从 N = 4 的 {pct(res15[4]['ORM 选最高'])} 掉到 N = 32 的 {pct(r15['ORM 选最高'])}，PRM-full 从 "
        f"{pct(res15[4]['PRM-full 选最高'])} 掉到 {pct(r15['PRM-full 选最高'])}；同时多数投票升到 {pct(r15['多数投票'])}、"
        f"ORM 加权投票 {pct(r15['ORM 加权投票'])}。这就是 best-of-N 的**过度优化**（Gao et al. 2023）："
        f"AUC {c15['orm']:.3f} 描述的是'平均排序'，而 best-of-N 取的是**最高分**——样本越多，越可能出现一个骗过验证器的错误样本。",
        f"这些骗过验证器的样本是什么（演示 4）？温度 1.5、N = 32 时，ORM 在 {ncov15} 道有正确样本的题里选错的，有 "
        f"{cnt15['推理链对，答案抄错了']} 道是'推理链完全正确、但 | 后面的答案抄错了'，{cnt15['有一步查找错了']} 道是有一步查找错了。"
        "验证器是在温度 1.0 的样本上训练的，那里答案几乎总是等于推理链的最后一个字母，它从没学过检查这一点；"
        "高温采样恰好大量制造这种它没见过的样本，而'选最高分'会主动把它们找出来。**验证器也是模型，也会在分布外犯错；"
        "best-of-N 是在主动搜索它的错误**（Goodhart 定律）。",
        "相比之下，投票（以及加权投票）是在**聚合**而不是**取最大**，对少数离群的高分样本不敏感，在这里更稳健。"
        "实践中的对策：让验证器的训练数据覆盖测试时的采样方式、用加权投票代替取最大、或者限制 N（讲义 §5.4）。",
    ])
    print(f"\n⏱ 总用时 {time.perf_counter() - t0:.0f} 秒")


if __name__ == "__main__":
    main()
