Appearance
评估与文本生成
训练好的模型要能「开口」才算成功。这一页加载 checkpoint,动手生成文本,并实测 temperature、top_k 对输出的影响——把 GPT架构 里讲的生成策略真正玩明白。
1. 加载模型并生成(sample.py)
python
# sample.py
import pickle, torch
from model import GPT, Config
device = "cuda" if torch.cuda.is_available() else ("mps" if torch.backends.mps.is_available() else "cpu")
meta = pickle.load(open("data/meta.pkl", "rb"))
itos = meta["itos"]
encode = lambda s: [stoi[c] for c in s] # 需再从 meta 建 stoi, 见下
# 读取训练时保存的 checkpoint
# 安全提示: torch.load 会反序列化对象, 只加载你自己信任的 checkpoint;
# 这里 ckpt 仅含张量与纯 dict, 用 weights_only=True 更稳妥。
ckpt = torch.load("out/ckpt_final.pt", map_location=device, weights_only=True)
cfg = Config(**ckpt["cfg"])
stoi = {v: k for k, v in itos.items()} # 由 itos 反推 stoi
model = GPT(cfg).to(device)
model.load_state_dict(ckpt["model"])
model.eval()
# 给一个开头, 续写
context = "KING:" # 换成你语料里出现过的开头效果更好
idx = torch.tensor([encode(context)], dtype=torch.long, device=device)
out = model.generate(idx, max_new_tokens=300, temperature=0.8, top_k=40)
print("".join(itos[i] for i in out[0].tolist()))若嫌
stoi麻烦,可把encode也一并 pickle 进meta.pkl(prepare.py里pickle.dump({..., "stoi": stoi}, f))。
2. temperature:控制「稳」还是「野」
回忆 softmax:probs = softmax(logits / T)。
T 很小(0.3) → 分布被"削尖" → 总挑大概率词 → 保守、重复、语法稳
T = 1.0 → 原样采样
T 很大(2.0) → 分布被"压平" → 小概率词也有机会 → 有创意、但易胡言乱语对比实验(同一开头):
temp=0.4 : "KING: My lord, the army is prepared for the battle..." (规整但略平)
temp=1.5 : "KING: My dragon whispers purple thunder across the..." (天马行空)实践常用 0.6 ~ 0.9。
3. top_k / top_p:给采样加个「安全网」
top_k=40 : 每步只在概率最高的 40 个候选里采样, 过滤掉长尾乱码
top_p=0.9: 按概率从高到低累加, 取累计刚达 0.9 的最小集合再采样(动态候选数)python
# 在 generate 里加 top-p(核采样)示意
probs = F.softmax(logits, dim=-1)
if top_p is not None:
sorted_p, sorted_i = torch.sort(probs, descending=True, dim=-1)
cum = torch.cumsum(sorted_p, dim=-1)
mask = cum > top_p
mask[:, 1:] = mask[:, :-1].clone(); mask[:, 0] = False # 保留刚越过阈值的那个
probs = probs.scatter(-1, sorted_i.masked_fill(mask, 0), 0.0) # 集合外清零- top-k:候选数固定,简单。
- top-p:候选数随分布自适应,分布尖时少取、分布平时多取,更聪明,是大模型默认。
4. 用 loss 科学地评估质量
除了「看着像不像」,还要量化:
困惑度(Perplexity) = exp(验证集平均交叉熵损失)
PPL=5 → 模型平均在 5 个候选里犹豫(很强)
PPL=50 → 要在 50 个里蒙(一般)
PPL↑ → 越接近瞎猜python
import math
val_losses = [float(model(x, y)[1]) for x, y in val_batches]
mean_loss = sum(val_losses) / len(val_losses)
print("验证 loss:", mean_loss, "| 困惑度 PPL:", math.exp(mean_loss))5. 训练过程「进化」实录(预期)
| 阶段 | 生成样子 | val loss |
|---|---|---|
| 初期 | aaaa....,,,, 单字符循环 | ~2.6 |
| 中期 | 出现英语单词/中文常用字组合 | ~1.8 |
| 后期 | 结构合理的句子/对白/诗句 | ~1.4 |
看到输出逐步从乱码走向「像那么回事」,就是自注意力 + 交叉熵在你眼前生效的证据。
6. 效果不好怎么办
- 复读机/总是重复:调低 temperature、加
repetition_penalty、或多训练。 - 输出驴唇不对马嘴:语料太少/太杂,或训练不足;换更连贯的语料。
- 一开始就不像:确认开头是训练集中见过的字符(字符级模型遇到词表外字符会崩)。
- 中文输出怪:字符级中文需要更充足语料,可考虑增大
n_layer/n_embd与训练步数。
小结
- 加载 checkpoint 后用
generate自回归续写。 - temperature 调「稳/野」,top_k/top_p 加安全网,top-p 更自适应。
- 用 困惑度 PPL = exp(loss) 量化语言模型质量。
- 生成质量的进化,是检验训练是否成功的最好证据。
下一步
自己从零训是小样本练兵;真实项目更多是在预训练模型上微调。最后一实战 → 微调预训练模型。