"""
=============================================================================
第 5 讲 · 代码 02：校准 —— 模型说"我有 80% 的把握"时，它真的有 80% 是对的吗？
=============================================================================

对应讲义：lectures/lecture05_hallucination_calibration.md §4

【这个脚本要回答的问题】
    1. 长尾模型的置信度准不准？可靠性图（reliability diagram）、ECE、Brier 分数、AUROC 各说明什么？（演示 1）
    2. 温度缩放：只调一个参数，能把校准修好多少？（演示 2）
    3. 指令微调之后，校准变好还是变坏？"在微调里教它不知道的事实"又会怎样？（演示 3）

【怎么读输出】
    "置信度"= 模型给自己答案的概率。ECE 越小越好（0 = 完美校准）；Brier 越小越好；
    AUROC 衡量"置信度能不能把答对的和答错的分开"（0.5 = 瞎猜，1 = 完美）——它和校准是两回事：
    一个模型可以排序很好（AUROC 高）但数值不准（ECE 高），反之亦然。

【运行】python3 02_calibration.py     （第一次约 30 秒：训练 3 个指令微调模型；之后约 2 秒）
【依赖】PyTorch；import 同目录的 hallu_lab.py，第 1 讲的 console.py。
"""

import os
import random
import sys

sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "lecture01"))
from console import print_header, print_observations, print_table  # noqa: E402
from hallu_lab import (BUCKETS, COUNTRIES, HEAD_TRAIN, QA_TASKS, TAIL_TRAIN, auroc, brier, bucket,  # noqa: E402
                       chat_probe, ece, fit_temperature, get_longtail_model, get_sft_model, probe, with_temperature)

TRAINED = {c.name for c in HEAD_TRAIN + TAIL_TRAIN}
EVAL_FACTS = [(c, rel) for c in COUNTRIES if c.name not in TRAINED for rel in QA_TASKS]   # 没进过任何微调数据的国家


def summary(rows):
    """(准确率, ECE, Brier, AUROC)。"""
    confs = [r["conf"] for r in rows]
    ys = [r["correct"] for r in rows]
    return sum(ys) / len(ys), ece(confs, ys)[0], brier(confs, ys), auroc(confs, ys)


def demo1(rows):
    """演示 1：可靠性图。"""
    print_header("演示 1：长尾模型的可靠性图（240 个事实，问答页格式）")
    confs = [r["conf"] for r in rows]
    ys = [r["correct"] for r in rows]
    e, bins = ece(confs, ys)
    table = []
    for lo, hi, n, mc, acc in bins:
        if n == 0:
            continue
        bar = "█" * round(20 * acc) + "·" * (20 - round(20 * acc))
        table.append([f"({lo:.1f}, {hi:.1f}]", n, f"{mc:.2f}", f"{acc:.2f}", f"{acc - mc:+.2f}", bar])
    print_table(["置信度区间", "事实数", "平均置信度", "准确率", "准确率 − 置信度", "准确率（20 格）"], table,
                aligns=["<", ">", ">", ">", ">", "<"])
    acc, e, b, a = summary(rows)
    print(f"\n  准确率 {100 * acc:.1f}%   ECE {e:.3f}   Brier {b:.3f}   AUROC {a:.3f}")
    under = [x for x in bins if x[2] and x[4] - x[3] > 0.1]
    low_rows = [r for r in rows if r["conf"] <= 0.3]
    n_low, acc_low = len(low_rows), sum(r["correct"] for r in low_rows) / max(1, len(low_rows))
    print_observations([
        f"AUROC {a:.3f}：置信度很会**排序**——随机挑一个答对的和一个答错的，{100 * a:.0f}% 的时候答对的那个置信度更高。",
        f"但数值不完全准：有 {len(under)} 个区间的准确率比平均置信度高出 0.1 以上——模型说'我有 40% 的把握'时，实际上大多是对的。"
        "这些主要是只出现两三次的事实（01 号脚本演示 1）：它记住了，却'不太相信自己'。",
        f"置信度不到 0.3 的 {n_low} 个事实，准确率只有 {100 * acc_low:.0f}%：这部分大多是它在瞎猜（偶尔猜对）。"
        "一个只看准确率的评测会把这些和'真的知道'的答案混在一起。",
        "Kadavath et al.（2022）在大模型上发现，只要题目的格式合适（多选、判断），大模型的置信度大体是校准的；"
        "并且可以训练模型预测'我知不知道这道题'（P(IK)）。",
    ])


def demo2(rows, tok):
    """演示 2：温度缩放。"""
    print_header("演示 2：温度缩放——在一半事实上拟合温度 T，在另一半上检验")
    rng = random.Random(0)
    idx = list(range(len(rows)))
    rng.shuffle(idx)
    dev, test = [rows[i] for i in idx[:120]], [rows[i] for i in idx[120:]]
    T = fit_temperature(dev, tok)
    after = with_temperature(test, T)
    b, a = summary(test), summary(after)
    print_table(["", "准确率", "ECE", "Brier", "AUROC"],
                [["测试集 · 原始（T = 1）", f"{100 * b[0]:.1f}%", f"{b[1]:.3f}", f"{b[2]:.3f}", f"{b[3]:.3f}"],
                 [f"测试集 · 温度缩放（T = {T:.2f}）", f"{100 * a[0]:.1f}%", f"{a[1]:.3f}", f"{a[2]:.3f}", f"{a[3]:.3f}"]])
    print_observations([
        f"拟合出来的温度是 {T:.2f}{'（小于 1：把分布变尖，让模型更自信）' if T < 1 else '（大于 1：把分布摊平，让模型不那么自信）'}。"
        f"ECE 从 {b[1]:.3f} 降到 {a[1]:.3f}，准确率完全不变（argmax 没变）。",
        "AUROC 几乎不变：温度缩放是一个**单调**的变换，不改变排序，只改变数值。它修的是'校准'，不是'区分对错的能力'。",
        "温度缩放（Guo et al. 2017）是神经网络最常用的事后校准方法：便宜、只需要一小份带标签的数据、不改变预测。"
        "它的局限是只有一个参数——如果模型在不同类型的问题上偏差方向不同（这里是：冷门事实偏自信、半冷门事实偏不自信），"
        "一个温度没法同时修好。",
    ])


def demo3():
    """演示 3：指令微调前后的校准。"""
    print_header("演示 3：指令微调之后的校准（只看没进过任何微调数据的国家：每个国家 4 个事实）")
    base, btok = get_longtail_model(verbose=False)
    base_rows = probe(base, btok, EVAL_FACTS)
    rows_by = {"base（问答页格式）": base_rows}
    for variant, label in [("known", "SFT · 只用知道的事实"), ("teach", "SFT · 也教它不知道的事实"),
                           ("abstain", "SFT · 不知道就答 unknown")]:
        m, t = get_sft_model(variant)
        r = chat_probe(m, t, EVAL_FACTS)
        if variant == "abstain":
            r = [x for x in r if not x["abstain"]]
        rows_by[label] = r
    table = []
    res = {}
    for label, r in rows_by.items():
        acc, e, b, a = summary(r)
        wrong = [x for x in r if not x["correct"]]
        mw = sum(x["conf"] for x in wrong) / len(wrong) if wrong else float("nan")
        res[label] = (acc, e, a, mw)
        cells = [label + ("（只算没弃权的）" if "unknown" in label else ""), len(r), f"{100 * acc:.1f}%", f"{e:.3f}",
                 f"{a:.3f}", f"{mw:.2f}"]
        by = []
        for bk in ["0 次", "1 次", "2 次"]:
            sel = [x for x in r if bucket(x["count"]) == bk]
            by.append(f"{sum(x['conf'] for x in sel) / len(sel):.2f}" if sel else "–")
        table.append(cells + by)
    print_table(["模型", "事实数", "准确率", "ECE", "AUROC", "答错时的平均置信度", "置信度：0 次", "1 次", "2 次"], table)
    b, k, t = res["base（问答页格式）"], res["SFT · 只用知道的事实"], res["SFT · 也教它不知道的事实"]
    zt = [x for x in rows_by["SFT · 也教它不知道的事实"] if x["count"] == 0]
    zero_teach = sum(x["conf"] for x in zt) / len(zt)
    print_observations([
        f"只用模型本来就知道的事实做指令微调：ECE 从 {b[1]:.3f} 变成 {k[1]:.3f}（略好），可准确率从 {100 * b[0]:.1f}% 降到 "
        f"{100 * k[0]:.1f}%，AUROC 从 {b[2]:.3f} 降到 {k[2]:.3f}，答错时的平均置信度从 {b[3]:.2f} 升到 {k[3]:.2f}——"
        "置信度区分对错的能力变差了，错的答案也更自信了。ECE 一个数字掩盖了这些变化：**看校准要同时看 ECE 和 AUROC**。",
        f"在微调里'教'它不知道的事实（尾部国家的事实，给正确答案）：准确率 {100 * t[0]:.1f}%，AUROC {t[2]:.3f}，"
        f"答错时的平均置信度升到 {t[3]:.2f}，在从没见过的事实上平均置信度高达 {zero_teach:.2f}——"
        "在**别的**它不知道的事实上，它变得自信地错。它没有学会那些新事实背后的规律（没有规律可学），"
        "只学会了'对这类问题要给一个确定的答案'。",
        "这是 Gekhman et al.（2024）的发现在迷你模型上的样子：用模型不知道的新知识做微调，这些例子学得慢，而一旦学会，"
        "模型在其他问题上的幻觉也增加了。他们的建议是：微调数据尽量用模型已经知道的知识，新知识靠预训练或检索。",
        "GPT-4 技术报告（OpenAI 2023）展示过类似的现象：预训练模型在 MMLU 上的置信度相当校准，经过 RLHF 之后明显变差。"
        "后训练改变的不只是'说什么'，也改变了'有多大把握'。",
    ])


def main():
    model, tok = get_longtail_model()
    rows = probe(model, tok)
    demo1(rows)
    demo2(rows, tok)
    demo3()


if __name__ == "__main__":
    main()
