"""
=============================================================================
第 2 讲 · 代码 04：示例到底教了什么 —— 标签对不对重要吗？
=============================================================================

对应讲义：lectures/lecture02_in_context_learning.md §6

【这个脚本要回答的问题】
    Min et al.（2022）发现：把示例里的标签换成随机的，GPT-3 等 12 个模型的少样本准确率只下降一点点。
    示例起作用，主要靠"输入长什么样""有哪些标签""格式是什么"，而不是"这个输入对应这个标签"。
    Wei et al.（2023）又发现：**足够大**的模型会被翻转的标签带着走（示例说好评是 negative，它就跟着这么答），
    小模型不会。在我们的两个迷你 GPT 和 SimICL 上：
    1. 把示例拆开：去掉标签、去掉输入、标签随机、标签全翻、输入换成无关的句子——各损失多少？（演示 1）
    2. 标签词的语义 vs 示例给的映射：反着用的标签词（好评 = awful）、无关的标签词（copper / wheat）。（演示 2）
    3. SimICL 的三种"规模"：翻转标签的比例从 0 到 100%，以及语义无关的标签词。（演示 3）

【运行】python3 04_what_demos_teach.py     （约 15 秒；第一次运行还要训练 bursty 模型约 45 秒）
【依赖】PyTorch；import 同目录的 icl_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 icl_lab import (TEMPLATES, VERBALIZERS, SimICL, accuracy, demo_pool, flip, get_base_model,  # noqa: E402
                     get_bursty_model, label_probs, pw, render, render_english, sample_demos, test_set)

N_SETS = 10
K = 4


def story_sentences(n, seed=0):
    """
    n 句和评论无关的句子（来自故事文档，例如 "the queen lives in the palace"），用作"分布外的输入"。

    【思路】Min et al.（2022）把示例的输入换成从外部语料随机抽的句子（格式和标签不变），发现准确率明显下降——
        "输入长什么样"本身是示例传达的信息之一。这里用第 1 讲世界里的故事句子扮演"外部语料"。
    """
    rng = random.Random(seed)
    out = []
    while len(out) < n:
        sent = pw.make_story(rng).split(" : ", 1)[1].split(" . ")[0]
        if len(sent.split()) <= 6:
            out.append(sent)
    return out


def variants(demos, rng, ood):
    """
    一组示例的几种"拆解"版本。

    【返回】{名字: (示例列表, style)}——示例是 (正文, 类别, 类型) 的列表，style 传给 render
    【边界】"标签随机"每条独立地抽 positive / negative（可能恰好全对，也可能全错）；"输入无关"保留原来的标签。
    """
    return {
        "标签正确（gold）": (demos, "full"),
        "标签随机": ([(x, rng.choice(["positive", "negative"]), k) for x, _, k in demos], "full"),
        "标签全部翻转": ([(x, flip(y), k) for x, y, k in demos], "full"),
        "只有输入，没有标签": (demos, "input_only"),
        "只有标签，没有输入": (demos, "label_only"),
        "输入换成无关的句子": ([(o, y, k) for o, (_, y, k) in zip(ood, demos)], "full"),
    }


def demo1(models, tok, test, pool):
    """演示 1：把示例拆开看。"""
    print_header(f"演示 1：示例的哪一部分在起作用？（{K} 个示例、两类各半，10 组的平均 [最低, 最高]）")
    rows, res = [], {}
    names = None
    for mname, m in models.items():
        for tname, vname in [("sentiment", "positive/negative"), ("sentiment", "great/awful")]:
            t, verb = TEMPLATES[tname], VERBALIZERS[vname]
            words = [verb["positive"], verb["negative"]]
            zero = accuracy(label_probs(m, tok, [render(t, verb, [], x) for x, _, _ in test], words), test)
            accs = {}
            for s in range(N_SETS):
                rng = random.Random(s)
                demos = sample_demos(pool, rng, K)
                ood = story_sentences(K, seed=s)
                for name, (ds, style) in variants(demos, rng, ood).items():
                    probs = label_probs(m, tok, [render(t, verb, ds, x, style=style) for x, _, _ in test], words)
                    accs.setdefault(name, []).append(accuracy(probs, test))
            names = list(accs)
            res[(mname, tname, vname)] = {"zero": zero, **{n: sum(v) / len(v) for n, v in accs.items()}}
            rows.append([mname, f"{tname} · {vname}", f"{100 * zero:.1f}%"] +
                        [f"{100 * sum(v) / len(v):.1f}%" for v in accs.values()])
    print_table(["模型", "设计", "零样本"] + names, rows)
    b, u = res[("base", "sentiment", "positive/negative")], res[("bursty", "sentiment", "positive/negative")]
    bg, ug = res[("base", "sentiment", "great/awful")], res[("bursty", "sentiment", "great/awful")]
    print_observations([
        f"标签正确、随机、全部翻转，三者几乎一样（base：{100 * b['标签正确（gold）']:.1f}% / {100 * b['标签随机']:.1f}% / "
        f"{100 * b['标签全部翻转']:.1f}%；bursty：{100 * u['标签正确（gold）']:.1f}% / {100 * u['标签随机']:.1f}% / "
        f"{100 * u['标签全部翻转']:.1f}%）。示例里'这个输入对应这个标签'的信息，两个迷你模型都没有用上——"
        "Min et al.（2022）在 GPT-3 等大模型上看到的是同一个方向（随机标签大多只损失几个百分点），这里是它的极端版本。",
        f"'半个示例'在 positive/negative 设计下也差不多够用：只有输入（base {100 * b['只有输入，没有标签']:.1f}%、"
        f"bursty {100 * u['只有输入，没有标签']:.1f}%）、只有标签（{100 * b['只有标签，没有输入']:.1f}%、"
        f"{100 * u['只有标签，没有输入']:.1f}%），都远高于零样本。对这个设计，模型需要的只是'这是一页评论、这里要填评分'的信号。",
        f"great/awful 设计下就不同了：base 只有输入 {100 * bg['只有输入，没有标签']:.1f}%、只有标签 "
        f"{100 * bg['只有标签，没有输入']:.1f}%，都明显低于完整示例的 {100 * bg['标签正确（gold）']:.1f}%——"
        "标签词陌生时，模型需要同时看到'评论 + 标签词'的完整格式。",
        f"把输入换成无关的故事句子（标签照旧），并没有像 Min et al. 报告的那样一致地变差：base 两个设计 "
        f"{100 * b['输入换成无关的句子']:.1f}% / {100 * bg['输入换成无关的句子']:.1f}%，bursty "
        f"{100 * u['输入换成无关的句子']:.1f}% / {100 * ug['输入换成无关的句子']:.1f}%，有升有降。"
        "great/awful 设计下各种拆解之间的差别大、方向也不一致——模型越没把握，示例里的细节越能左右结果，也越难预测。",
        "一句话：对这两个小模型，示例教的是**格式和标签空间**，不是**输入到标签的映射**。",
    ])
    return res


def demo2(models, tok, test, pool):
    """演示 2：标签词的语义先验 vs 示例给出的映射。"""
    print_header("演示 2：标签词反着用、用无关的词——模型跟着语义走，还是跟着示例走？（rating 模板）")
    rows = []
    res = {}
    t = TEMPLATES["rating"]
    for mname, m in models.items():
        for vname in ["positive/negative", "awful/great", "copper/wheat"]:
            verb = VERBALIZERS[vname]
            words = [verb["positive"], verb["negative"]]
            row = [mname, vname]
            for k in [0, 1, 2, 4]:
                accs, masses = [], []
                for s in range(N_SETS if k else 1):
                    demos = sample_demos(pool, random.Random(s), k) if k else []
                    probs, mass = label_probs(m, tok, [render(t, verb, demos, x) for x, _, _ in test], words, return_mass=True)
                    accs.append(accuracy(probs, test))
                    masses.append(sum(mass) / len(mass))
                res[(mname, vname, k)] = (sum(accs) / len(accs), sum(masses) / len(masses))
                row.append(f"{100 * res[(mname, vname, k)][0]:.1f}%（{res[(mname, vname, k)][1]:.0e}）")
            rows.append(row)
    print_table(["模型", "标签词（好评 / 差评）", "0 个示例", "1 个", "2 个", "4 个"], rows)
    print("  括号里是两个标签词的概率之和：模型有多大的把握'在这里写这两个词之一'。")
    ba, ua = res[("base", "awful/great", 4)], res[("bursty", "awful/great", 4)]
    bc, uc = res[("base", "copper/wheat", 4)], res[("bursty", "copper/wheat", 4)]
    cw_b = [100 * res[("base", "copper/wheat", k)][0] for k in [0, 1, 2, 4]]
    print_observations([
        f"标签词反着用（好评写 awful）：示例从 0 个加到 4 个，base {100 * res[('base', 'awful/great', 0)][0]:.1f}% → "
        f"{100 * ba[0]:.1f}%，完全不跟示例；bursty {100 * res[('bursty', 'awful/great', 0)][0]:.1f}% → {100 * ua[0]:.1f}%，"
        "开始往示例给的映射那边挪了一点，但仍远低于 50%。两个模型基本都按 awful、great 本来的意思回答——"
        "这是 Wei et al.（2023）在小模型上看到的现象：语义先验压倒了示例。bursty 挪动的那一点，和它'更会读上下文'一致。",
        f"括号里的概率之和：除了 positive/negative，其他标签词在 'rating :' 后面的概率都只有百万分之一左右，"
        "模型根本不打算在这里写它们。这时的'分类'靠的是：模型内部那个'好评 / 差评'的方向，碰巧和两个词的向量差对得上多少。"
        "great / awful 本来就常和好评、差评一起出现，对得上（于是有 70% 或反过来的 30%）；",
        f"copper / wheat 和情感无关，对不上：base 随示例数在 {min(cw_b):.1f}%–{max(cw_b):.1f}% 之间乱跳，"
        f"4 个示例时 base {100 * bc[0]:.1f}%、bursty {100 * uc[0]:.1f}%，没有任何'学会了新映射'的趋势。"
        "所以'语义无关的标签'（Wei et al. 的 SUL 设定）在这两个小模型上测不出东西。演示 3 用 SimICL 展示'有这种能力的模型'会是什么样子。",
        "实践上的推论：评估一个提示时看一眼标签词的概率之和（或者生成式输出里'不是任何一个标签'的比例）——"
        "它很小时，准确率数字很可能只反映了偶然的几何关系。",
    ])


def demo3(test, pool):
    """演示 3：SimICL 的规模 × 翻转比例；语义无关的标签词。"""
    print_header("演示 3：SimICL——翻转标签的比例 × 模型'规模'（8 个示例，两类各半；10 组的平均）")
    rng0 = random.Random(0)
    test_sub = rng0.sample(test, 200)
    rows = []
    res = {}
    for size in ["small", "medium", "large"]:
        sim = SimICL(size)
        row = [size, f"a_prior = {sim.k['a_prior']}, a_icl = {sim.k['a_icl']}"]
        for frac in [0.0, 0.25, 0.5, 0.75, 1.0]:
            accs = []
            for s in range(N_SETS):
                rng = random.Random(500 + s)
                demos = sample_demos(pool, rng, 8)
                n_flip = round(frac * len(demos))
                idx = set(rng.sample(range(len(demos)), n_flip))
                shown = [(x, flip(y) if i in idx else y, k) for i, (x, y, k) in enumerate(demos)]
                probs = [sim.label_probs(render_english("review", ("positive", "negative"), shown, x)) for x, _, _ in test_sub]
                accs.append(accuracy(probs, test_sub))
            res[(size, frac)] = sum(accs) / len(accs)
            row.append(f"{100 * res[(size, frac)]:.1f}%")
        rows.append(row)
    print_table(["规模", "旋钮", "0% 翻转", "25%", "50%", "75%", "100%"], rows)

    print("\n  语义无关的标签词（好评 = foo、差评 = bar），示例数 × 规模：")
    rows = []
    for size in ["small", "medium", "large"]:
        sim = SimICL(size)
        row = [size]
        for k in [0, 4, 8, 16]:
            accs = []
            for s in range(N_SETS if k else 1):
                demos = sample_demos(pool, random.Random(700 + s), k) if k else []
                probs = [sim.label_probs(render_english("review", ("foo", "bar"), demos, x)) for x, _, _ in test_sub]
                accs.append(accuracy(probs, test_sub))
            res[(size, "sul", k)] = sum(accs) / len(accs)
            row.append(f"{100 * res[(size, 'sul', k)]:.1f}%")
        rows.append(row)
    print_table(["规模", "0 个示例", "4 个", "8 个", "16 个"], rows)
    print_observations([
        f"SimICL-small：标签翻转 0% → 100%，准确率 {100 * res[('small', 0.0)]:.1f}% → {100 * res[('small', 1.0)]:.1f}%，"
        "几乎不变——它主要靠标签词的语义，像我们的迷你 GPT。",
        f"SimICL-large：{100 * res[('large', 0.0)]:.1f}% → {100 * res[('large', 1.0)]:.1f}%，远低于 50%：它'学会了'示例给的反向映射，"
        "于是系统性地反着答。Wei et al.（2023）在 PaLM、InstructGPT 等模型上报告的正是这条曲线：越大的模型，越能（也越会）被示例覆盖掉先验。",
        f"语义无关的 foo / bar：small 加到 16 个示例也只有 {100 * res[('small', 'sul', 16)]:.1f}%，"
        f"large 从 {100 * res[('large', 'sul', 0)]:.1f}% 升到 {100 * res[('large', 'sul', 16)]:.1f}%——"
        "只有会从示例里学映射的模型，才能用上一个全新的标签空间。",
        f"50% 翻转（≈ 随机标签）时，small 只比 0% 翻转低 {100 * (res[('small', 0.0)] - res[('small', 0.5)]):.1f} 个百分点，"
        f"large 低 {100 * (res[('large', 0.0)] - res[('large', 0.5)]):.1f} 个。Min et al. 的'随机标签影响不大'，"
        "**前提是模型主要靠先验**；对一个很依赖示例映射的模型，随机标签的伤害会大得多。",
        "这些曲线的形状由我们设定的 a_prior 和 a_icl 决定；它的用处是说明 Min et al. 和 Wei et al. 的结论**并不矛盾**："
        "两者描述的是同一个公式里两项的相对大小。",
    ])


def main():
    test, pool = test_set(), demo_pool()
    base, tok = get_base_model()
    bursty, _ = get_bursty_model()
    models = {"base": base, "bursty": bursty}
    demo1(models, tok, test, pool)
    demo2(models, tok, test, pool)
    demo3(test, pool)


if __name__ == "__main__":
    main()
