"""
=============================================================================
第 2 讲 · 代码 01：零样本、少样本与提示模板 —— 示例和模板，哪个更重要？
=============================================================================

对应讲义：lectures/lecture02_in_context_learning.md §2–§3

【这个脚本要回答的问题】
    GPT-3 论文定义了零样本（zero-shot）、单样本（one-shot）、少样本（few-shot）三种提示方式。
    同一个情感分类任务，换几种写法（模板）、给 0 / 1 / 2 / 4 个示例，第 1 讲的迷你 GPT 表现如何？
    1. 模板 × 示例个数：哪种模板零样本就会，哪种要靠示例救，哪种怎么救也救不回来？（演示 1）
    2. 标签词（verbalizer）：类别用哪个词说出来，影响有多大？（演示 2）
    3. 示例的成本：示例越多，提示越长，迷你 GPT 的 64 个 token 窗口很快就满了。（演示 3）

【怎么读输出】
    "准确率"是 300 条测试评论上的准确率；有示例时，换 10 组示例各测一次，报平均和最低 / 最高。
    P(pos) 是模型给 positive 的平均概率——测试集里好评占 46%，P(pos) 离 0.46 越远，说明模型越"偏"。
    "简单 / but / not"是三种难度的评论各自的准确率。

【运行】python3 01_templates_shots.py     （约 15 秒；需要第 1 讲迷你 GPT 的缓存，没有的话会先训练约 50 秒）
【依赖】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 pct_ci, print_header, print_observations, print_table  # noqa: E402
from icl_lab import (TEMPLATES, VERBALIZERS, accuracy, by_kind, demo_pool, get_base_model,  # noqa: E402
                     label_probs, mean_positive, pw, render, sample_demos, test_set)

N_SETS = 10          # 每种设置换 10 组示例


def run_setting(model, tok, template, verbalizer, k, test, pool, n_sets=N_SETS, seed0=0):
    """
    一种设置（模板 + 标签词 + 示例个数）在测试集上的表现。

    【思路】k = 0 时只有一个提示，测一次；k > 0 时用 seed0, seed0+1, ... 抽 n_sets 组示例（两类各半、顺序随机），各测一次。
    【返回】字典：acc（平均准确率）、lo / hi（10 组里最低 / 最高）、ppos（平均 P(positive)）、kinds（按难度的平均准确率）、
        accs（每组的准确率列表）
    【边界】示例从 pool 里抽，不会和测试集重复；同一个 seed 永远抽到同一组示例，所以重跑结果完全一样。
    """
    words = [verbalizer["positive"], verbalizer["negative"]]
    sets = [[]] if k == 0 else [sample_demos(pool, random.Random(seed0 + s), k) for s in range(n_sets)]
    accs, pps, kinds = [], [], {"simple": 0.0, "but": 0.0, "not": 0.0}
    for demos in sets:
        probs = label_probs(model, tok, [render(template, verbalizer, demos, x) for x, _, _ in test], words)
        accs.append(accuracy(probs, test))
        pps.append(mean_positive(probs))
        for kk, v in by_kind(probs, test).items():
            kinds[kk] += v / len(sets)
    return {"acc": sum(accs) / len(accs), "lo": min(accs), "hi": max(accs), "ppos": sum(pps) / len(pps),
            "kinds": kinds, "accs": accs}


def fmt_range(r):
    """把一种设置的结果写成 "89.7% [89.3, 90.0]"（k = 0 时没有范围）。"""
    if r["lo"] == r["hi"]:
        return f"{100 * r['acc']:.1f}%"
    return f"{100 * r['acc']:.1f}% [{100 * r['lo']:.1f}, {100 * r['hi']:.1f}]"


def demo1_templates(model, tok, test, pool):
    """演示 1：五种模板 × 0 / 1 / 2 / 4 个示例。"""
    print_header("演示 1：模板 × 示例个数（标签词 positive / negative；有示例时是 10 组示例的平均 [最低, 最高]）")
    verb = VERBALIZERS["positive/negative"]
    ks = [0, 1, 2, 4]
    rows, detail = [], {}
    for name, t in TEMPLATES.items():
        res = {k: run_setting(model, tok, t, verb, k, test, pool) for k in ks}
        detail[name] = res
        unk = tok.unknown_words(t.demo("the soup was cold", "negative"))
        rows.append([f"{name}：{t.fmt}", ", ".join(sorted(set(unk))) or "–"] + [fmt_range(res[k]) for k in ks]
                    + [f"{res[0]['ppos']:.2f} → {res[4]['ppos']:.2f}"])
    print_table(["模板", "词表外的词", "0 个示例", "1 个", "2 个", "4 个", "P(pos) 0→4"], rows)

    print("\n  sentiment 模板按难度拆开（同样 10 组示例的平均）：")
    rows = []
    for k in ks:
        kd = detail["sentiment"][k]["kinds"]
        rows.append([f"{k} 个示例", f"{100 * kd['simple']:.1f}%", f"{100 * kd['but']:.1f}%", f"{100 * kd['not']:.1f}%"])
    print_table(["", "简单句（180 条）", "but 句（57 条）", "not 句（63 条）"], rows)
    s, q, b, a = detail["sentiment"], detail["qa"], detail["bare"], detail["arrow"]
    spreads = sorted(((r["hi"] - r["lo"], name, k) for name, res in detail.items() for k, r in res.items() if k), reverse=True)
    print_observations([
        f"rating 模板和训练语料里的评论页一模一样：零样本就是 {100 * detail['rating'][0]['acc']:.0f}%，示例没有可以再帮的地方。",
        f"sentiment 模板只把 rating 换成了一个词表外的词，零样本跌到 {100 * s[0]['acc']:.1f}%"
        f"（P(pos) 只有 {s[0]['ppos']:.2f}：几乎全猜 negative）；1 个示例 {100 * s[1]['acc']:.1f}%，"
        f"2 个 {100 * s[2]['acc']:.1f}%，4 个 {100 * s[4]['acc']:.1f}%——示例把一个陌生的格式'教'会了。",
        f"qa 模板借用问答页的格式，零样本 {100 * q[0]['acc']:.1f}%；加示例后在 {100 * min(q[k]['acc'] for k in ks[1:]):.1f}%–"
        f"{100 * max(q[k]['acc'] for k in ks[1:]):.1f}% 之间，都不比零样本好：示例不总是帮忙。",
        f"arrow 和 bare 怎么加示例都没用（{100 * a[4]['acc']:.1f}%、{100 * b[4]['acc']:.1f}%，"
        f"约等于'全猜一类'）：评论后面直接跟 => 或标签，在训练语料里从没出现过。",
        "按难度看，示例最先救回的是简单句（看形容词就行）；but 句在 sentiment 模板下始终停在约 60%——"
        "示例教会了'这里要写一个标签'，没有教会'以 but 后面为准'。",
        f"同一个模板内，10 组不同的示例之间差别很小：最大的是 {spreads[0][1]} 的 {spreads[0][2]} 个示例（最高减最低 "
        f"{100 * spreads[0][0]:.1f} 个百分点），其次是 {spreads[1][1]} 的 {spreads[1][2]} 个（{100 * spreads[1][0]:.1f}），"
        f"其余都不超过 {100 * spreads[2][0]:.1f}。这个迷你模型对'抽到哪几个示例'不敏感——"
        "这和 GPT-3 上的观察不同（讲义 §4），原因在 04 号脚本里。",
    ])
    return detail


def demo2_verbalizers(model, tok, test, pool):
    """演示 2：同一个模板，换标签词。"""
    print_header("演示 2：标签词（verbalizer）——类别用哪个词说出来")
    rows = []
    res = {}
    for tname in ["rating", "sentiment"]:
        for vname, verb in VERBALIZERS.items():
            r0 = run_setting(model, tok, TEMPLATES[tname], verb, 0, test, pool)
            r4 = run_setting(model, tok, TEMPLATES[tname], verb, 4, test, pool)
            res[(tname, vname)] = (r0, r4)
            rows.append([tname, vname, fmt_range(r0), fmt_range(r4), f"{r0['ppos']:.2f} / {r4['ppos']:.2f}"])
    print_table(["模板", "标签词（好评 / 差评）", "0 个示例", "4 个示例", "P(第一个词) 0 / 4"], rows)
    rg, ag = res[("rating", "great/awful")], res[("rating", "awful/great")]
    sg = res[("sentiment", "great/awful")]
    cw = res[("rating", "copper/wheat")]
    print_observations([
        f"great/awful 和 positive/negative 意思一样，可在 rating 模板下准确率从 100% 掉到 {100 * rg[0]['acc']:.1f}%"
        f"（4 个示例：{100 * rg[1]['acc']:.1f}%）：训练语料里 'rating :' 后面只出现过 positive / negative，"
        "great / awful 在这个位置上都是极小的概率，比较两个极小的数很不可靠。",
        f"把标签词反过来用（好评说 awful）：rating 模板 {100 * ag[0]['acc']:.1f}% → 4 个示例 {100 * ag[1]['acc']:.1f}%——"
        "示例里明明写着'好评 → awful'，模型仍按 awful 本来的意思回答。它在用标签词的**语义先验**，而不是示例给的映射。",
        f"sentiment 模板下 great/awful 的 10 组示例之间差了 {100 * (sg[1]['hi'] - sg[1]['lo']):.1f} 个百分点"
        f"（{100 * sg[1]['lo']:.1f}%–{100 * sg[1]['hi']:.1f}%）：模型越是'没把握'，示例的选择就越要紧。",
        f"和情感无关的 copper/wheat：{100 * cw[0]['acc']:.1f}% → {100 * cw[1]['acc']:.1f}%。这些数字没有意义："
        "copper 和 wheat 从没出现在评分的位置，两个都接近 0 的概率谁大谁小，取决于偶然的向量方向（同样的实验在 bursty 模型上"
        "会得到完全不同的数，见 04 号脚本）。",
        "结论：标签词最好用模型在这个位置上'本来就会说'的词。真实的大模型也一样（Zhao et al. 2021 的常见词偏差；"
        "Holtzman et al. 2021 的'表面形式竞争'）。",
    ])


def demo3_window(model, tok, test, pool):
    """演示 3：示例越多，提示越长——64 个 token 的窗口。"""
    print_header("演示 3：示例的成本——提示长度与上下文窗口（sentiment 模板）")
    t, verb = TEMPLATES["sentiment"], VERBALIZERS["positive/negative"]
    long_pool = pw.review_probes(seed=11, n=400)       # 不限长度的示例池
    rows, acc_of = [], {}
    for k in [0, 1, 2, 4, 6, 8]:
        for label, src in [("短示例", None), ("任意长度", long_pool)]:
            if k == 0 and src is not None:
                continue
            lens, over, accs = [], 0, []
            for s in range(N_SETS if k else 1):
                rng = random.Random(s)
                if src is None:
                    demos = sample_demos(pool, rng, k) if k else []
                else:
                    demos = rng.sample(src, k)
                prompts = [render(t, verb, demos, x) for x, _, _ in test]
                n_tok = [len(tok.encode(p)) for p in prompts]
                lens += n_tok
                over += sum(n > model.cfg.block_size for n in n_tok)
                accs.append(accuracy(label_probs(model, tok, prompts, ["positive", "negative"]), test))
            n = len(lens)
            acc_of[(k, label)] = f"{100 * sum(accs) / len(accs):.1f}%"
            rows.append([f"{k} 个", label if k else "–", f"{sum(lens) / n:.1f}", f"{max(lens)}",
                         pct_ci(over, n).split(" ")[0], acc_of[(k, label)]])
    print_table(["示例数", "示例来源", "平均 token 数", "最长", "超出 64 的比例", "平均准确率"], rows)
    print_observations([
        f"短示例（一个分句，约 11 个 token）4 个刚好塞得下（{acc_of[(4, '短示例')]}）；6 个、8 个时全部超过 64 个 token，"
        f"最前面的页眉和示例被截掉，准确率不再上升（{acc_of[(6, '短示例')]}、{acc_of[(8, '短示例')]}）。",
        "用任意长度的示例，4 个就开始超长。**示例占的是上下文窗口**：真实模型窗口大得多，但每个示例都按 token 收费、"
        "都增加延迟（P2 §6），'多放几个示例'不是免费的。",
        "GPT-3 时代的论文受 2048 个 token 的窗口限制，示例通常只有几个到十几个（Lu et al. 2022 主要用 4 个）；"
        "Agarwal et al.（2024）在百万 token 窗口的模型上把示例加到几百上千个，很多任务还在变好。",
    ])


def main():
    test, pool = test_set(), demo_pool()
    model, tok = get_base_model()
    print(f"  测试集 {len(test)} 条评论（好评 {sum(1 for t in test if t[1] == 'positive')} 条）；"
          f"短示例池：好评 {len(pool.pos)} 条、差评 {len(pool.neg)} 条；模型：第 1 讲的 base 迷你 GPT")
    demo1_templates(model, tok, test, pool)
    demo2_verbalizers(model, tok, test, pool)
    demo3_window(model, tok, test, pool)


if __name__ == "__main__":
    main()
