"""
=============================================================================
第 3 讲 · 03：Weaver —— 把很多个弱 verifier 合成一个强 verifier
=============================================================================

对应讲义：lectures/lecture03_verification.md 第 5 节（Saad-Falcon et al. 2025，Shrinking the Generation-Verification Gap
with Weak Verifiers）

-----------------------------------------------------------------------------
【问题】
-----------------------------------------------------------------------------
好的 verifier 要么不可扩展（人），要么适用面窄（Lean 这样的证明器）。手头常有的是一堆**弱** verifier：
几个 reward model、几个 LLM judge，各有各的偏差，准确率参差不齐。能不能把它们合起来？

    · 不加权地投票：好的、差的 verifier 一人一票 —— 差的会把好的拖下水。
    · 加权：给准的 verifier 更大的权重。但"谁准"需要**有标签的数据**来估计。
    · Weaver：用**弱监督**（weak supervision）在**没有标签**的情况下估计每个 verifier 的准确率。
      核心假设：在"候选是否正确"已知的条件下，各个 verifier 的判断彼此独立（条件独立）。
      在这个假设下，只看 verifier 之间"谁和谁意见一致"，就能用 EM 算法同时估计出
      每个候选正确的概率和每个 verifier 的 TPR / FPR。
      （这和 CS329H 第 17 讲的 Dawid–Skene 模型是同一个算法：从一群标注员的标注里估计谁更可靠。）

-----------------------------------------------------------------------------
【这里的 7 个弱 verifier（每个对每个候选投"对 / 错"一票）】
-----------------------------------------------------------------------------
    V1 PRM(10B 自查)   ：生成器自己当 checker 的 PRM 分数，按中位数切成对 / 错
    V2 ORM(10B 自查)   ：生成器自己重解 4 遍，至少一半和候选一致就投"对"
    V3 PRM(30B)        ：更强一点的 checker 的 PRM，按中位数切
    V4 judge A         ：独立的评委，TPR 0.85，FPR 0.30
    V5 judge B         ：独立的评委，TPR 0.75，FPR 0.25
    V6 judge C         ：几乎随机的评委，TPR 0.52，FPR 0.48
    V7 judge D         ：一个"反着来"的评委，TPR 0.35，FPR 0.60（例如偏爱某种表面特征的 judge）
V1、V2 都来自生成器本身，它们的错误和生成器的错误相关（也彼此相关）—— 违反了条件独立的假设。

-----------------------------------------------------------------------------
【三个演示】
-----------------------------------------------------------------------------
演示 1  上面 7 个 verifier：EM 估计出的 TPR / FPR 准不准？各种组合方法的挑选准确率。
演示 2  再加两个"反着来"的评委（V8、V9）：verifier 的质量差别很大时，加权和不加权的差距。
演示 3  条件独立的假设有多重要？V1 和 V2 在错误候选上一起犯错的程度。

运行时间：约 1 秒（只用标准库）。
"""

import math
import random

from verifier_tools import (ToyLLM, aggregate, majority_vote, make_problem_set, mean, orm_score, pad,
                            prm_step_scores)

TEMPERATURE = 0.7
N_CANDIDATES = 16
BASE_NAMES = ["V1 PRM(10B自查)", "V2 ORM(10B自查)", "V3 PRM(30B)"]
JUDGES = {"V4 judge A": (0.85, 0.30), "V5 judge B": (0.75, 0.25), "V6 judge C": (0.52, 0.48),
          "V7 judge D": (0.35, 0.60)}
EXTRA_JUDGES = {"V8 judge E": (0.30, 0.65), "V9 judge F": (0.40, 0.70)}   # 演示 2：两个"反着来"的评委


def median(xs):
    """中位数（xs 非空）。"""
    ys = sorted(xs)
    n = len(ys)
    return ys[n // 2] if n % 2 else (ys[n // 2 - 1] + ys[n // 2]) / 2


def build_candidates(problems, gen, mid, rng):
    """
    为每道题生成 N_CANDIDATES 个候选，并算出 V1~V3（来自模型的 verifier）的投票。

    【返回】list，每道题一个 dict：{"problem", "cands", "model_votes"}，model_votes[i] 是第 i 个候选的 3 票
    【思路】V1、V3 的 PRM 分数是连续的，用**所有候选**上该 verifier 分数的中位数做阈值（Weaver 用数据集的统计量
        来统一不同 verifier 的输出格式）；V2 是"至少一半的重解和候选一致"。
    """
    data, prm1, prm3 = [], [], []
    for p in problems:
        cands = [gen.generate(p, rng, TEMPERATURE) for _ in range(N_CANDIDATES)]
        s1 = [aggregate(prm_step_scores(gen, p, c)) for c in cands]
        s3 = [aggregate(prm_step_scores(mid, p, c)) for c in cands]
        orm = [orm_score(gen, p, c, rng) for c in cands]
        data.append({"problem": p, "cands": cands, "s1": s1, "s3": s3, "orm": orm})
        prm1.extend(s1)
        prm3.extend(s3)
    t1, t3 = median(prm1), median(prm3)
    for d in data:
        d["model_votes"] = [[int(a > t1), int(o >= 0.5), int(b > t3)] for a, b, o in zip(d["s1"], d["s3"], d["orm"])]
    return data


def add_judges(data, judges, seed):
    """
    给每个候选加上若干个独立评委的投票，返回 (新的数据, verifier 名字列表)。

    【思路】评委以 TPR 的概率接受正确候选、以 FPR 的概率接受错误候选（只用 correct 来模拟准确率）。
        每个评委有**自己的**随机数生成器：加入新评委不会改变原有评委的投票，演示之间可以公平地比较。
    【参数】data：build_candidates 的返回值；judges：{名字: (TPR, FPR)}；seed：随机种子
    """
    names = BASE_NAMES + list(judges)
    streams = {name: random.Random(f"{seed}|{name}") for name in judges}
    out = []
    for d in data:
        votes = []
        for c, mv in zip(d["cands"], d["model_votes"]):
            jv = [1 if streams[n].random() < (tpr if c.correct else fpr) else 0 for n, (tpr, fpr) in judges.items()]
            votes.append(mv + jv)
        out.append({"problem": d["problem"], "cands": d["cands"], "votes": votes})
    return out, names


def em_weak_supervision(vote_rows, n_iter=50):
    """
    Weaver 的核心：在没有标签的情况下，用 EM 估计"候选正确"的先验 π 和每个 verifier 的 TPR、FPR。

    【模型】每个候选有一个看不见的标签 y ∈ {1（正确）, 0（错误）}，P(y = 1) = π。
        给定 y，verifier j 投"对"的概率是 TPR_j（y = 1 时）或 FPR_j（y = 0 时），各个 verifier **条件独立**。
    【EM 算法】
        E 步：用当前的参数，算每个候选正确的后验概率
              q = π Π_j TPR_j^v (1−TPR_j)^(1−v)  /  [ 同上 + (1−π) Π_j FPR_j^v (1−FPR_j)^(1−v) ]
        M 步：把 q 当作"软标签"，重新估计参数：
              π = 平均的 q；TPR_j = Σ q·v_j / Σ q；FPR_j = Σ (1−q)·v_j / Σ (1−q)
        反复迭代，直到收敛。
    【参数】vote_rows：每个候选一行，每行是 7 个 0/1；n_iter：迭代次数
    【返回】(π, TPR 列表, FPR 列表)
    【边界】
        · 初始化很重要：标签 y 和 1 − y 互换后似然完全相同（"标签翻转"的对称性）。从"每个 verifier 都比随机好"
          （TPR 0.7、FPR 0.3）出发，EM 会收敛到"多数 verifier 比随机好"的那个解。
        · 用 1e−3 ~ 1 − 1e−3 截断参数，避免 log(0)。
        · 条件独立不成立时（例如 V1、V2 一起犯错），EM 会把"它们俩意见一致"误读成"它们俩都很准"。
    """
    m = len(vote_rows[0])
    pi, tpr, fpr = 0.5, [0.7] * m, [0.3] * m
    clip = lambda x: min(1 - 1e-3, max(1e-3, x))
    for _ in range(n_iter):
        qs = [posterior(row, pi, tpr, fpr) for row in vote_rows]
        sq = math.fsum(qs)
        sq0 = len(qs) - sq
        pi = clip(sq / len(qs))
        tpr = [clip(math.fsum(q * row[j] for q, row in zip(qs, vote_rows)) / sq) for j in range(m)]
        fpr = [clip(math.fsum((1 - q) * row[j] for q, row in zip(qs, vote_rows)) / sq0) for j in range(m)]
    return pi, tpr, fpr


def posterior(row, pi, tpr, fpr, use=None):
    """
    朴素贝叶斯：给定一行投票，候选正确的后验概率。use 是要使用的 verifier 下标（默认全部）。

    【思路】在对数空间里把每个 verifier 的"证据"加起来：
        log 赔率 = log(π/(1−π)) + Σ_j [ v_j·log(TPR_j/FPR_j) + (1−v_j)·log((1−TPR_j)/(1−FPR_j)) ]
        TPR > FPR 的 verifier 投"对"时增加赔率；TPR < FPR 的（反着来的）verifier 投"对"时**降低**赔率 ——
        它的权重自动变成负的。TPR ≈ FPR 的 verifier 权重接近 0，几乎不起作用。
    """
    idx = range(len(row)) if use is None else use
    logit = math.log(pi / (1 - pi))
    for j in idx:
        if row[j]:
            logit += math.log(tpr[j] / fpr[j])
        else:
            logit += math.log((1 - tpr[j]) / (1 - fpr[j]))
    return 1 / (1 + math.exp(-logit)) if logit > -700 else 0.0


def true_rates(data):
    """用标签（上帝视角）算出每个 verifier 真实的 TPR、FPR，以及候选正确的比例。"""
    rows = [(v, c.correct) for d in data for v, c in zip(d["votes"], d["cands"])]
    pos = [v for v, y in rows if y]
    neg = [v for v, y in rows if not y]
    m = len(rows[0][0])
    return (len(pos) / len(rows), [mean(v[j] for v in pos) for j in range(m)],
            [mean(v[j] for v in neg) for j in range(m)])


def pick(d, scores):
    """在一道题的候选里选分数最高的那个，返回是否正确。"""
    best = max(range(len(scores)), key=lambda i: scores[i])
    return d["cands"][best].correct


def evaluate_methods(dev, test, names, title):
    _, tpr_dev, fpr_dev = true_rates(dev)                    # "有标签"的开发集上估计的准确率
    rows = [v for d in test for v in d["votes"]]
    pi, tpr_em, fpr_em = em_weak_supervision(rows)          # 测试集上无监督地估计
    keep = [j for j in range(len(names)) if abs(tpr_em[j] - fpr_em[j]) >= 0.1]
    pi_dev = mean(c.correct for d in dev for c in d["cands"])

    results = {"coverage（oracle）": mean(any(c.correct for c in d["cands"]) for d in test),
               "majority vote（看答案）": mean(majority_vote([c.answer for c in d["cands"]])[0] == d["problem"].answer()
                                          for d in test)}
    for j, name in enumerate(names):
        results[f"只用 {name}"] = mean(pick(d, [v[j] for v in d["votes"]]) for d in test)   # 并列时取第一个
    results[f"不加权投票（{len(names)} 票相加）"] = mean(pick(d, [sum(v) for v in d["votes"]]) for d in test)
    results["加权：开发集上有标签估计"] = mean(pick(d, [posterior(v, pi_dev, tpr_dev, fpr_dev) for v in d["votes"]])
                                       for d in test)
    results["Weaver：EM 无标签估计"] = mean(pick(d, [posterior(v, pi, tpr_em, fpr_em) for v in d["votes"]]) for d in test)
    results["Weaver + 过滤低质量"] = mean(pick(d, [posterior(v, pi, tpr_em, fpr_em, keep) for v in d["votes"]])
                                       for d in test)

    print("=" * 78)
    print(title)
    print("=" * 78)
    _, tpr_true, fpr_true = true_rates(test)
    print(pad("verifier", 18) + pad("真实 TPR", 10, "right") + pad("真实 FPR", 10, "right")
          + pad("EM 估计 TPR", 13, "right") + pad("EM 估计 FPR", 13, "right"))
    for j, name in enumerate(names):
        print(pad(name, 18) + pad(f"{tpr_true[j]:.2f}", 10, "right") + pad(f"{fpr_true[j]:.2f}", 10, "right")
              + pad(f"{tpr_em[j]:.2f}", 13, "right") + pad(f"{fpr_em[j]:.2f}", 13, "right"))
    print(f"候选正确的比例：真实 {mean(c.correct for d in test for c in d['cands']):.2f}，EM 估计 {pi:.2f}；"
          f"过滤后保留：{', '.join(names[j].split()[0] for j in keep)}")
    print()
    print(pad("挑选方法（每题 16 个候选）", 30) + pad("测试集准确率", 14, "right"))
    for name, acc in results.items():
        print(pad(name, 30) + pad(f"{acc:.3f}", 14, "right"))
    return results


def main():
    problems = make_problem_set(random.Random(80), n_problems=240, min_steps=3, max_steps=8)
    gen, mid = ToyLLM(1e10), ToyLLM(3e10)
    base = build_candidates(problems, gen, mid, random.Random(81))

    data, names = add_judges(base, JUDGES, seed=82)
    dev, test = data[:120], data[120:]
    r1 = evaluate_methods(dev, test, names, "演示 1：7 个弱 verifier，各种组合方法（10B 生成器，测试集 120 题）")
    print("\n观察：")
    print("  · EM 不用任何标签，靠 verifier 之间\"谁和谁意见一致\"估计 TPR / FPR：独立的评委（V4~V7）估得很准；")
    print("    来自模型的 V1、V2、V3 却被高估了（TPR 估高、FPR 估低）—— 它们的错误相关，\"意见一致\"被误读成\"都很准\"。")
    print("  · 这组 verifier 大多比较像样，三种组合方法（不加权、有标签加权、Weaver）都明显好于最好的单个 verifier，")
    print("    彼此相差不到 0.03 —— 测试集只有 120 题，这样的差别在噪声范围内。")
    print("  · 反着来的 V7 在 EM 里得到了 TPR < FPR 的估计，它的票在加权时自动起反作用（\"它说对，多半是错\"）；")
    print("    过滤只去掉了几乎随机的 V6。\n")

    data2, names2 = add_judges(base, dict(JUDGES, **EXTRA_JUDGES), seed=82)
    dev2, test2 = data2[:120], data2[120:]
    r2 = evaluate_methods(dev2, test2, names2, "演示 2：再加两个\"反着来\"的评委 V8、V9（候选和原有 7 票完全相同）")
    unweighted1 = next(v for k, v in r1.items() if k.startswith("不加权"))
    unweighted2 = next(v for k, v in r2.items() if k.startswith("不加权"))
    print("\n观察：")
    print(f"  · 不加权投票从 {unweighted1:.3f} 掉到 {unweighted2:.3f}：一人一票时，三个\"反着来\"的评委足以把结果带偏。")
    print(f"  · 有标签的加权（{r2['加权：开发集上有标签估计']:.3f}）和 Weaver（{r2['Weaver：EM 无标签估计']:.3f}）几乎不受影响：")
    print("    它们知道 V7~V9 的票要\"反着读\"。Weaver 论文的核心发现正是如此：verifier 的准确率差别很大时，")
    print("    加权组合远好于不加权组合，而弱监督能在没有标签的情况下估出这些权重。")
    print("  · 和 coverage 相比仍有差距：弱 verifier 合起来也不是 oracle。Weaver 论文报告，用 70B 以下的 judge 和")
    print("    reward model 组合，Llama 3.3 70B 的挑选准确率平均达到 87.7%，接近 o3-mini 的水平。\n")

    print("=" * 78)
    print("演示 3：条件独立的假设有多重要？—— V1 和 V2 在错误候选上的相关性")
    print("=" * 78)
    wrong = [v for d in test for v, c in zip(d["votes"], d["cands"]) if not c.correct]
    both = mean(v[0] and v[1] for v in wrong)
    fp1, fp2 = mean(v[0] for v in wrong), mean(v[1] for v in wrong)
    print(f"  在错误的候选上：V1 误判率 {fp1:.2f}，V2 误判率 {fp2:.2f}；如果两者独立，同时误判的比例应为 "
          f"{fp1 * fp2:.2f}，实际是 {both:.2f}")
    fp4 = mean(v[3] for v in wrong)
    both14 = mean(v[0] and v[3] for v in wrong)
    print(f"  对照：V1 和独立的 V4（误判率 {fp4:.2f}）同时误判的比例 {both14:.2f}，独立时应为 {fp1 * fp4:.2f}")
    print("\n观察：")
    print("  · V1、V2 都来自生成器自己：模型\"最相信\"的错误答案，自查时两者会一起放过。它们同时误判的比例")
    print("    远高于独立时的预期 —— 这正是第 1 讲\"self-check 和生成者错得一样\"的问题，在 verifier 集成里又出现了。")
    print("  · 弱监督的方法都依赖某种独立性假设。要警惕\"来自同一个模型、同一种训练数据\"的 verifier：")
    print("    它们的一致不代表正确。\n")


if __name__ == "__main__":
    main()
