File size: 6,869 Bytes
5c6c847
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
"""ZeroPoint vs Transformer 对照实验(字符级语言模型)。

    python3 experiments/compare.py            # 默认语料:Python 标准库源码(离线可用)
    python3 experiments/compare.py my.txt     # 换成自己的文本
    ITERS=2000 python3 experiments/compare.py

三个模型宽度、层数、头数、MLP 完全一样,唯一区别是"token 混合层 + 位置编码":
    Transformer        自注意力 + 可学习位置编码
    Transformer (sin)  自注意力 + 论文 §3.5 的正弦位置编码(理论上能外推到更长序列)
    ZeroPoint            记忆槽,无位置编码

流程:
    1. 学习率搜索:每个模型在 LRS 里各试一次(种子 0),取验证 loss 最低的
    2. 用各自最好的学习率再跑种子 1、2,报告 3 个种子的 均值 ± 标准差
    3. 长度外推:训练只见过 64 个字,测到 4096 个字
结果:experiments/compare.md(最新)+ experiments/results/<时间>-compare.md(存档)
"""
import sys

import numpy as np

from common import (CFG, ITERS, T, batches, evaluate, forward_flops_per_char, is_zeropoint, prepare_data,
                    save_result, slot_usage, train)
from models import TransformerLM, ZeroX
import zerocg as zc

LRS = [1e-3, 3e-3, 6e-3]
SEEDS = [0, 1, 2]
MODELS = {
    "Transformer": lambda V: TransformerLM(V, T, **CFG),
    "Transformer (sin)": lambda V: TransformerLM(V, T, pos="sin", **CFG),
    "ZeroPoint": lambda V: ZeroX(V, n_slot=16, **CFG),
}
# 长度外推的分段;Transformer 的注意力矩阵是 长度² 的,4096 字放不进内存,只测到 1024
SEGMENTS = [(0, 64), (64, 256), (256, 1024), (1024, 4096)]
LONG_MAX = {"Transformer (sin)": 1024, "ZeroPoint": 4096}
LONG_N = 8   # 测试序列条数


def long_context_loss(model, val_data, max_len):
    x, y = next(batches(val_data, 1, max_len, 7, batch=LONG_N))
    with zc.no_grad():
        logits = model(x).data
    logp = logits - logits.max(-1, keepdims=True)
    logp = logp - np.log(np.exp(logp).sum(-1, keepdims=True))
    nll = -np.take_along_axis(logp, y[..., None], -1)[..., 0]
    return np.array([nll[:, a:b].mean() if b <= max_len else np.nan for a, b in SEGMENTS])


def main():
    path = sys.argv[1] if len(sys.argv) > 1 else None
    text, V, train_data, val_data = prepare_data(path)
    print(f"语料 {len(text):,} 字符,词表 {V},每次训练 {ITERS} 步", flush=True)

    sweep = {name: {} for name in MODELS}      # name -> lr -> 验证 loss(种子 0)
    finals = {name: [] for name in MODELS}     # name -> [各种子的验证 loss]
    long_loss = {name: [] for name in LONG_MAX}
    usage = []

    def run(name, seed, lr):
        np.random.seed(seed)
        model = MODELS[name](V)
        val = train(name, model, train_data, val_data, seed, lr)
        print(f"  => {name} seed={seed} lr={lr:g}: val {val:.4f}", flush=True)
        return model, val

    for name in MODELS:
        best = None
        for lr in LRS:
            model, val = run(name, 0, lr)
            sweep[name][lr] = val
            if best is None or val < best[1]:
                best = (model, val, lr)
        best_lr = best[2]
        trained = [best[0]] + [run(name, s, best_lr)[0] for s in SEEDS[1:]]
        finals[name] = [best[1]] + [None] * (len(SEEDS) - 1)
        for k, model in enumerate(trained):
            if k > 0:
                finals[name][k] = evaluate(model, val_data)
            if name in LONG_MAX:
                long_loss[name].append(long_context_loss(model, val_data, LONG_MAX[name]))
            if is_zeropoint(model):
                usage.append(slot_usage(model))

    # ---- 写结果 ----
    m0 = {name: make(V) for name, make in MODELS.items()}
    lines = ["# ZeroPoint vs Transformer 对照实验\n",
             f"- 语料:{'Python 标准库源码' if not path else path},{len(text):,} 字符,词表 {V};90% 训练 / 10% 验证",
             f"- 三个模型:宽 {CFG['n_embd']}、{CFG['n_layer']} 层、{CFG['n_head']} 头、Pre-LN、GELU MLP 4 倍;"
             "ZeroPoint 每头 16 个槽、可学习初始状态、分块并行(块长 32)、无位置编码",
             f"- 训练:上下文 {T}、batch 32、{ITERS} 步、AdamW(权重衰减 0.01)、预热 100 步 + 余弦、梯度裁剪 1.0",
             f"- 学习率:每个模型在 {LRS} 里各试一次(种子 0),取最好的再跑种子 {SEEDS[1:]}",
             "- 验证 loss = 40 个固定验证 batch 的平均交叉熵(nats/字符,越低越好),均值 ± 标准差(3 个种子)",
             "- 计算量 = 前向时每个字的矩阵乘浮点运算次数(不受机器负载影响;训练一步约 3 倍)\n",
             "| 模型 | 参数量 | 最佳学习率 | 验证 loss | 前向计算量(每字) | 推理状态(第 64 字 / 第 4096 字,浮点数) |",
             "|---|---|---|---|---|---|"]
    for name, vals in finals.items():
        m = m0[name]
        best_lr = min(sweep[name], key=sweep[name].get)
        state = (f"{m.state_floats():,} / {m.state_floats():,}(固定)" if is_zeropoint(m)
                 else f"{m.state_floats(64):,} / {m.state_floats(4096):,}(KV 缓存)")
        lines.append(f"| {name} | {m.num_parameters():,} | {best_lr:g} | {np.mean(vals):.4f} ± {np.std(vals):.4f} | "
                     f"{forward_flops_per_char(m, V) / 1e3:.0f} K | {state} |")

    lines += ["", "## 学习率搜索(种子 0 的验证 loss)\n",
              "| 模型 | " + " | ".join(f"lr={lr:g}" for lr in LRS) + " |",
              "|---|" + "---|" * len(LRS)]
    for name, d in sweep.items():
        lines.append(f"| {name} | " + " | ".join(f"{d[lr]:.4f}" for lr in LRS) + " |")

    lines += ["", f"## 长度外推(训练上下文 {T};{LONG_N} 条验证序列 × 3 个种子平均)\n",
              "| 位置 | ZeroPoint | Transformer (sin) | Transformer(可学习位置编码) |", "|---|---|---|---|"]
    lz, ls = np.nanmean(long_loss["ZeroPoint"], axis=0), np.nanmean(long_loss["Transformer (sin)"], axis=0)
    for k, (a, b) in enumerate(SEGMENTS):
        tag = "(训练见过的长度)" if b <= T else ""
        s = "—(注意力矩阵太大,没测)" if np.isnan(ls[k]) else f"{ls[k]:.4f}"
        lines.append(f"| {a} ~ {b - 1}{tag} | {lz[k]:.4f} | {s} | {'能跑' if b <= T else f'跑不了(只有 {T} 个位置)'} |")

    u = np.mean(usage, axis=0)   # (层, 2)
    lines += ["", "## ZeroPoint 槽的使用情况(有效槽数,满分 16)\n",
              "有效槽数 = 平均读/写地址分布的 exp(熵):16 个槽用得一样多 = 16,全挤在一个槽 = 1。\n",
              "| 层 | 读 | 写 |", "|---|---|---|"]
    lines += [f"| {i} | {r:.1f} | {w:.1f} |" for i, (r, w) in enumerate(u)]
    save_result("compare", lines)


if __name__ == "__main__":
    main()