"""
=============================================================================
第 3 讲 · 02：Math-Shepherd —— 不用人工标注，用 rollout 自动得到过程标签
=============================================================================

对应讲义：lectures/lecture03_verification.md 第 4 节（Wang et al. 2023）

-----------------------------------------------------------------------------
【问题】
-----------------------------------------------------------------------------
训练 PRM 需要"每一步对不对"的标签。Lightman et al. 2023 请人标了 80 万个步骤（PRM800K），非常贵。
Math-Shepherd 的想法：**一步好不好，看从这一步往后还能不能做对**。
    对解答的第 i 步，保留前 i + 1 步，让一个 completer 模型续写 N 次：
        · hard estimation（HE）：只要有一次做对，就把第 i 步标为"好"；
        · soft estimation（SE）：做对的比例。
训练数据只需要题目和最终答案（用来判断续写是否做对），不需要人去看过程。

-----------------------------------------------------------------------------
【三个演示】
-----------------------------------------------------------------------------
演示 1  标签的质量：和"真实的过程标签"（这一步之前是否全对）比较，HE 的准确率、漏判、误判随 N 怎样变化？
演示 2  SE 估计的到底是什么？—— 对"前缀全对"的步骤，SE 随剩余步数、completer 的强弱怎样变化？
演示 3  用 HE 标签定位第一个错误步骤，准确率如何？

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

import random

from verifier_tools import ToyLLM, make_problem_set, mean, on_track, pad, rollout_value

TEMPERATURE = 0.7
NS = (1, 4, 16, 64)
SE_THRESHOLD = 0.05   # "SE 超过阈值才算好"：一种比 HE 更保守的硬标签


def collect_solutions(gen, problems, rng, per_problem=2):
    """每道题采样 per_problem 个解答，返回 (题目, 解答) 的列表。"""
    return [(p, gen.generate(p, rng, TEMPERATURE)) for p in problems for _ in range(per_problem)]


def label_table(completer, solutions, rng, n):
    """
    对每个解答的每一步（最后一步除外）算 SE，返回 (SE, 真实标签, 剩余步数) 的列表。

    【思路】真实标签 = on_track：这一步之前（含这一步）的值是否全部正确。
        最后一步不需要续写（答案对不对直接可见），所以不计入，避免把"容易的标签"混进统计。
    """
    rows = []
    for p, s in solutions:
        gold = on_track(p, s)
        for i in range(p.n_steps - 1):
            rows.append((rollout_value(completer, p, s, i, rng, n, TEMPERATURE), gold[i], p.n_steps - 1 - i))
    return rows


def demo_label_quality(gen, solutions, rng):
    print("=" * 78)
    print(f"演示 1：HE / SE 标签的质量（completer = {gen.name}，{len(solutions)} 个解答，最后一步除外）")
    print("=" * 78)
    print(pad("每步续写 N 次", 14) + pad("HE 准确率", 11, "right") + pad("漏判(好→坏)", 13, "right")
          + pad("误判(坏→好)", 13, "right") + pad(f"SE>{SE_THRESHOLD} 准确率", 16, "right")
          + pad("SE|好", 9, "right") + pad("SE|坏", 9, "right") + pad("续写次数/解答", 15, "right"))
    tables = {}
    for n in NS:
        rows = label_table(gen, solutions, rng, n)
        tables[n] = rows
        good = [se for se, g, _ in rows if g]
        bad = [se for se, g, _ in rows if not g]
        acc = mean((se > 0) == g for se, g, _ in rows)
        acc_se = mean((se > SE_THRESHOLD) == g for se, g, _ in rows)
        fn = mean(se == 0 for se in good)
        fp = mean(se > 0 for se in bad)
        cost = n * len(rows) / len(solutions)
        print(pad(str(n), 14) + pad(f"{acc:.3f}", 11, "right") + pad(f"{fn:.3f}", 13, "right")
              + pad(f"{fp:.3f}", 13, "right") + pad(f"{acc_se:.3f}", 16, "right") + pad(f"{mean(good):.3f}", 9, "right")
              + pad(f"{mean(bad):.3f}", 9, "right") + pad(f"{cost:.0f}", 15, "right"))
    print("\n观察：")
    print("  · 漏判（正确的前缀被标成坏）随 N 增大而迅速减少：N 小时，completer 可能只是\"这几次运气不好\"。")
    print("  · 误判（错误的前缀被标成好）却随 N 增大而**上升**（N = 64 时约 10%）：续写次数一多，总有几次")
    print("    因为错误碰巧抵消（一步少算 10、后面一步多算 10）而\"做对\"——正是 01 号演示 2 的\"答案对、过程错\"。")
    print("    HE 只要\"有一次做对\"就算好，对这种运气非常敏感，所以它的准确率在中间的 N 上最高。")
    print("  · SE 在好、坏两类上分得很开（约 0.59 vs 0.005），而且不随 N 漂移。\"SE 超过一个小阈值\"的硬标签")
    print("    在 N 大时比 HE 更稳（N ≤ 16 时 SE 的最小非零值 1/N 已经超过 0.05，两种标签完全相同；N = 64 时才分开）。")
    print("    代价是续写次数：每个解答要续写 N × 步数 次（只在造训练数据时做一次）。")
    print("  · 续写需要知道标准答案 —— 所以 rollout 只能用来造训练数据，不能在测试时直接当 verifier 用。\n")
    return tables


def demo_what_se_measures(gen, solutions, rng, tables):
    print("=" * 78)
    print("演示 2：SE 估计的是\"这一步对不对\"，还是\"从这里还能不能做对\"？")
    print("=" * 78)
    strong = ToyLLM(1e11)
    strong_rows = label_table(strong, solutions, rng, 16)
    print(f"只看\"前缀全对\"（真实标签 = 好）的步骤，按剩余步数分组，SE 的平均值（每步续写 16 次）：")
    print(pad("剩余步数", 10) + pad(f"completer = {gen.name}", 20, "right")
          + pad(f"completer = {strong.name}", 22, "right") + pad("步骤数", 10, "right"))
    for remaining in range(1, 8):
        a = [se for se, g, r in tables[16] if g and r == remaining]
        b = [se for se, g, r in strong_rows if g and r == remaining]
        if a:
            print(pad(str(remaining), 10) + pad(f"{mean(a):.3f}", 20, "right") + pad(f"{mean(b):.3f}", 22, "right")
                  + pad(str(len(a)), 10, "right"))
    print("\n观察：")
    print("  · 同样是\"完全正确的前缀\"，剩下的步骤越多，SE 越低：SE 衡量的是 completer 从这里出发还能做对的概率，")
    print("    也就是 completer 的**价值函数** V(前缀)，而不是\"这一步本身对不对\"。")
    print("  · 换一个更强的 completer，同样的前缀 SE 就高得多。所以 Math-Shepherd 的标签依赖于 completer：")
    print("    用它训练出的 PRM，学到的是\"对这个 completer 来说前途如何\"。")
    print("  · 这其实很适合做搜索和 RL：beam search 和 RL 关心的正是\"从这里出发能不能做对\"（第 5、6 讲）。")
    print("    但如果想要\"这一步在逻辑上对不对\"的判断（例如给人看的解释），SE 会混入\"后面难不难\"的因素。\n")


def demo_locate_first_error(gen, solutions, tables):
    print("=" * 78)
    print("演示 3：用过程标签定位第一个错误步骤（只看答错、且错误发生在最后一步之前的解答）")
    print("=" * 78)
    # 把 tables 里的行重新按解答切分：每个解答占 (步数 − 1) 行
    print(pad("每步续写 N 次", 14) + pad("HE 定位", 10, "right") + pad(f"SE>{SE_THRESHOLD} 定位", 15, "right")
          + pad("解答数", 10, "right"))
    for n in NS:
        rows, idx, hits, hits_se, total = tables[n], 0, 0, 0, 0
        for p, s in solutions:
            k = p.n_steps - 1
            chunk = rows[idx: idx + k]
            idx += k
            gold = [g for _, g, _ in chunk]
            if all(gold):
                continue                       # 前 k 步全对：错误（如果有）在最后一步，不参与统计
            true_first = gold.index(False)
            for threshold, counter in ((0.0, "he"), (SE_THRESHOLD, "se")):
                labels = [se > threshold for se, _, _ in chunk]
                pred_first = labels.index(False) if False in labels else None
                if counter == "he":
                    hits += (pred_first == true_first)
                else:
                    hits_se += (pred_first == true_first)
            total += 1
        print(pad(str(n), 14) + pad(f"{hits / total:.3f}", 10, "right") + pad(f"{hits_se / total:.3f}", 15, "right")
              + pad(str(total), 10, "right"))
    print("\n观察：")
    print("  · N 小时，标签常常在第一个错误**之前**就报错（漏判），定位偏早；N 增大，定位准确率迅速上升。")
    print("  · N 太大时，HE 又会因为\"碰巧做对\"把错误的前缀标成好，定位偏晚，准确率回落；")
    print("    \"SE 超过阈值\"的标签不受这个影响。Math-Shepherd 论文里每步续写的次数是个位数的量级。")
    print("  · 准确的错误定位是很有用的反馈：它告诉 revision（第 2 讲）该从哪里改，告诉 RL（第 6 讲）该惩罚哪一步。\n")


def main():
    problems = make_problem_set(random.Random(70), n_problems=150, min_steps=4, max_steps=8)
    gen = ToyLLM(1e10)
    solutions = collect_solutions(gen, problems, random.Random(71))
    rng = random.Random(72)
    tables = demo_label_quality(gen, solutions, rng)
    demo_what_se_measures(gen, solutions, rng, tables)
    demo_locate_first_error(gen, solutions, tables)


if __name__ == "__main__":
    main()
