"""
=============================================================================
第 3 讲 · 代码 04：系统提示与回答风格；指令微调怎样改变上下文学习
=============================================================================

对应讲义：lectures/lecture03_instruction_tuning.md §6–§7

【这个脚本要回答的问题】
    1. 训练模型听**系统提示**：同一个问题，系统提示说"answer briefly"就只写答案，说"answer in a full sentence"就写一句话。
       这种"听风格指令"的能力，能不能迁移到微调数据里**没有**的任务上（领导人）？（演示 1）
    2. 不给系统提示、给一个没训练过的系统提示，模型会怎样？（演示 2）
    3. 指令微调之后，第 2 讲的少样本提示还管用吗？模型对标签词的语义先验是变强了还是变弱了？（演示 3）

【运行】python3 04_system_and_style.py     （第一次约 15 秒：训练 1 个 SFT 模型；之后读缓存）
【依赖】PyTorch；import 同目录的 instruct_lab.py，第 2 讲的 icl_lab.py（演示 3），第 1 讲的 console.py。
"""

import os
import random
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "lecture01"))
sys.path.insert(0, os.path.join(HERE, "..", "lecture02"))
from console import pct, print_header, print_observations, print_table  # noqa: E402
from instruct_lab import (EVAL_COUNTRIES, EVAL_REVIEWS, SYSTEM_PROMPTS, TASKS, TRAIN_TEMPLATES, answer,  # noqa: E402
                          base_model, format_ok, get_sft_model, gold_answer, question, render_chat)

STYLE_TASKS = ["capital", "language", "export", "sentiment", "region", "revcap"]     # 不含领导人


def style_eval(model, tok, task, system):
    """
    一个任务在某个系统提示下的表现。

    【参数】system：系统提示文字，或 None（不给系统提示）
    【返回】{"correct": 回答里含有标准答案的比例, "brief": 回答恰好是答案一个词的比例,
            "sentence": 回答是一句格式正确的完整句子的比例, "example": 第一个回答}
    """
    n = correct = brief = sent = 0
    example = None
    items = [(None, r) for r in EVAL_REVIEWS[:20]] if task == "sentiment" else [(c, None) for c in EVAL_COUNTRIES]
    for c, rv in items:
        for tpl in TRAIN_TEMPLATES:
            out = answer(model, tok, render_chat(question(task, tpl, c, rv), system), max_new_tokens=14)
            gold = gold_answer(task, c, rv)
            n += 1
            correct += gold in out.split()
            brief += out == gold
            sent += format_ok(task, out)
            example = example or out
    return {"correct": correct / n, "brief": brief / n, "sentence": sent / n, "example": example}


def demo1(model, tok):
    """演示 1：两种系统提示 × 训练过的任务 + 没训练过的领导人任务。"""
    print_header("演示 1：系统提示控制回答风格——训练时只有 6 种任务（没有领导人），两种风格各一半")
    rows, res = [], {}
    for task in STYLE_TASKS + ["leader"]:
        for style in ("brief", "sentence"):
            r = style_eval(model, tok, task, SYSTEM_PROMPTS[style])
            res[(task, style)] = r
            fmt = r["brief"] if style == "brief" else r["sentence"]
            rows.append([task + ("（没训练过）" if task == "leader" else ""), SYSTEM_PROMPTS[style], pct(round(r["correct"] * 100), 100),
                         pct(round(fmt * 100), 100), r["example"]])
    print_table(["任务", "系统提示", "含正确答案", "格式符合要求", "第一个回答"], rows, aligns=["<", "<", ">", ">", "<"])
    lb, ls = res[("leader", "brief")], res[("leader", "sentence")]
    trained_fmt = sum(res[(t, s)]["brief" if s == "brief" else "sentence"] for t in STYLE_TASKS for s in ("brief", "sentence")) / 12
    rv = res[("revcap", "sentence")]
    print_observations([
        f"训练过的 6 种任务上，两种风格的格式符合率平均 {100 * trained_fmt:.1f}%：同一个问题，系统提示一换，回答的形式就换了。",
        f"但格式对不等于内容对：反查首都的完整句子 {100 * rv['sentence']:.1f}% 格式正确，只有 {100 * rv['correct']:.1f}% 含正确答案"
        f"（例如 '{rv['example']}'）。句子要先把问题里的首都抄下来，再接国名——迷你模型没有'照抄'的能力（第 2 讲 §8.6），"
        "就编了一个格式漂亮、内容错误的句子。这是一个小型的'幻觉'（第 5 讲）。",
        f"领导人任务从没出现在微调数据里：brief 风格 {100 * lb['correct']:.1f}% 含正确答案，sentence 风格 {100 * ls['correct']:.1f}%，"
        f"而且 sentence 风格常常套用别的任务的句式（例如 '{ls['example']}'）。'风格'和'任务'没有组合起来：模型学到的是"
        "6 × 2 种具体的'问题 → 回答'模式，而不是'用句子的形式回答任何问题'。",
        "对比 02 号脚本：同样不含领导人、只用一种风格训练时，领导人有 24% 左右。这里每种'任务 × 风格'组合的样本数一样（约 200 条），"
        "但训练步数同样是 400 步，每种组合得到的训练只有一半，迷你模型在每种组合上都学得更浅、更像在背模式。"
        "真实的大模型能把风格指令和新任务组合起来，这是规模带来的能力（讲义 §6.2）。",
    ])
    return res


def demo2(model, tok):
    """演示 2：没有系统提示、没训练过的系统提示。"""
    print_header("演示 2：不给系统提示，或者给一个训练时没见过的系统提示（首都任务）")
    rows = []
    res = {}
    for label, system in [("answer briefly（训练过）", SYSTEM_PROMPTS["brief"]),
                          ("answer in a full sentence（训练过）", SYSTEM_PROMPTS["sentence"]),
                          ("（不给系统提示）", None),
                          ("answer in a sentence（少了 full）", "answer in a sentence"),
                          ("answer the question（没见过）", "answer the question")]:
        r = style_eval(model, tok, "capital", system)
        res[label] = r
        rows.append([label, f"{100 * r['correct']:.1f}%", f"{100 * r['brief']:.1f}%", f"{100 * r['sentence']:.1f}%", r["example"]])
    print_table(["系统提示", "含正确答案", "只写答案", "完整句子", "第一个回答"], rows, aligns=["<", ">", ">", ">", "<"])
    none = res["（不给系统提示）"]
    print_observations([
        f"不给系统提示：{100 * none['brief']:.1f}% 只写答案、{100 * none['sentence']:.1f}% 写句子。训练数据里每条都有系统提示，"
        "模型对'没有系统提示'这种输入没有明确的习惯——它的默认行为是训练数据**没覆盖到**的地方。",
        f"把 'answer in a full sentence' 去掉一个 full：仍有 {100 * res['answer in a sentence（少了 full）']['sentence']:.1f}% 写成句子"
        f"——它和训练过的那句话足够像；换一个完全没见过的说法（answer the question），只有 {100 * res['answer the question（没见过）']['correct']:.1f}% "
        "含正确答案。迷你模型认得的是训练里那两句话的'样子'，而不是'系统提示的意思'。真实的 chat 模型见过成千上万种系统提示，"
        "泛化得好得多，但同样的原则成立：**写系统提示时，越接近模型训练时常见的写法，行为越可预测**。",
        "真实的 chat 模型通常还被训练成'没有系统提示时也有合理的默认行为'，并且在系统提示和用户消息冲突时优先听系统提示"
        "（OpenAI 2024 的'指令层级'，第 12 讲）。这些都是训练出来的行为，不是模型天生的。",
    ])


def demo3(sft, tok):
    """演示 3：指令微调之后，第 2 讲的少样本提示还管用吗？"""
    print_header("演示 3：第 2 讲的少样本情感分类，base 模型 vs 指令微调后的模型（第 2 讲的原生格式，10 组示例的平均）")
    # 注意：扩展后的词表里有 sentiment 这个词（指令里用到了它），而第 2 讲里它是 <unk>。为了让两个模型读到**完全相同的
    # token**，这里两个模型都用 base 的分词器编码（SFT 模型的前 290 个词和 base 一模一样，所以这样做是合法的）。
    from icl_lab import TEMPLATES, VERBALIZERS, accuracy, demo_pool, label_probs, render, sample_demos, test_set
    base, btok = base_model()
    test, pool = test_set(), demo_pool()
    rows, res = [], {}
    for tname, vname in [("sentiment", "positive/negative"), ("sentiment", "great/awful"), ("rating", "awful/great")]:
        t, verb = TEMPLATES[tname], VERBALIZERS[vname]
        words = [verb["positive"], verb["negative"]]
        row = [f"{tname} · {vname}"]
        for name, m, tk in [("base", base, btok), ("SFT", sft, btok)]:     # 两个模型都用 base 的分词器：输入的 token 完全相同
            for k in (0, 4):
                sets = [[]] if k == 0 else [sample_demos(pool, random.Random(s), k) for s in range(10)]
                accs = [accuracy(label_probs(m, tk, [render(t, verb, d, x) for x, _, _ in test], words), test) for d in sets]
                res[(tname, vname, name, k)] = sum(accs) / len(accs)
                row.append(f"{100 * res[(tname, vname, name, k)]:.1f}%")
        rows.append(row)
    # 额外一行：SFT 模型用扩展后的词表编码（sentiment 是一个学过的词，不再是 <unk>）
    t, verb = TEMPLATES["sentiment"], VERBALIZERS["positive/negative"]
    ext = []
    for k in (0, 4):
        sets = [[]] if k == 0 else [sample_demos(pool, random.Random(s), k) for s in range(10)]
        accs = [accuracy(label_probs(sft, tok, [render(t, verb, d, x) for x, _, _ in test], ["positive", "negative"]), test) for d in sets]
        ext.append(sum(accs) / len(accs))
    rows.append(["sentiment · positive/negative（SFT 用扩展词表）", "–", "–", f"{100 * ext[0]:.1f}%", f"{100 * ext[1]:.1f}%"])
    print_table(["设计", "base · 0 个示例", "base · 4 个", "SFT · 0 个", "SFT · 4 个"], rows)
    s0 = res[("sentiment", "positive/negative", "SFT", 0)]
    b0 = res[("sentiment", "positive/negative", "base", 0)]
    s4 = res[("sentiment", "positive/negative", "SFT", 4)]
    b4 = res[("sentiment", "positive/negative", "base", 4)]
    fb, fs = res[("rating", "awful/great", "base", 4)], res[("rating", "awful/great", "SFT", 4)]
    print_observations([
        f"读到完全相同的 token 时：sentiment · positive/negative 零样本 base {100 * b0:.1f}% → SFT {100 * s0:.1f}%，4 个示例 "
        f"{100 * b4:.1f}% → {100 * s4:.1f}%。指令微调里有情感分类任务，模型对'这里要判断情感'更敏感了，连第 2 讲那个陌生的原生格式都受益。"
        "零样本提升得更多（+{:.1f} 个百分点 vs +{:.1f}）：指令微调后的模型，少样本相对零样本的收益变小——这是普遍的规律。".format(
            100 * (s0 - b0), 100 * (s4 - b4)),
        f"标签词反着用（rating · awful/great，4 个示例）：base {100 * fb:.1f}%、SFT {100 * fs:.1f}%。两个模型都按词本来的意思回答，"
        "示例给的反向映射都没有被采纳。Wei et al.（2023）在大模型上发现，指令微调会让模型**更**依赖语义先验。",
        f"最后一行是一个意外：同一个 SFT 模型，如果用扩展后的词表编码（sentiment 成了一个学过的词），零样本 {100 * ext[0]:.1f}%，"
        f"4 个示例却跌到 {100 * ext[1]:.1f}%（几乎全猜一类）。同一串文字、不同的分词，行为天差地别。我们没有找到确切的原因"
        "（一个猜测：sentiment 只在对话格式的问题里出现过，从没以 'sentiment : 标签 .' 的形式重复出现）。"
        "教训是：**换了分词器或词表，旧的提示就要重新测**（前置讲 P1 §4）。",
        "所以第 2 讲的结论要分模型看：对指令微调过的 chat 模型，示例更多地在规定格式和风格，'教新任务'的作用更小；"
        "想让它改变一个标签的含义，写在指令里比放在示例里更可靠。",
    ])


def main():
    model, tok, _, _ = get_sft_model(tasks=STYLE_TASKS, styles=["brief", "sentence"], use_system=True, n=2400)
    demo1(model, tok)
    demo2(model, tok)
    default, tok2, _, _ = get_sft_model()
    demo3(default, tok2)


if __name__ == "__main__":
    main()
