评估流水线 本节摘要:训练是你能用损失曲线监控的部分;评估是你必须设计的部分。本节构建统一评估流水线:接收任意训练好的语言模型,在其上跑四个异质评估,把结果聚合成每任务报告,并附一个本地 mock「LLM 当判官」(LLM-as-judge),使整个循环无需联网即可运行。四个评估覆盖每个要发布的模型都需要的维度:语言建模(困惑度)、短形式正确性(精确匹配)、开放形式相似度(token F1)、定性打分(判官)。单一指标从不描述一个语言模型——困惑度只说模型多贴合语言分布、不说它能否答问;精确匹配只说是否产出黄金串、惩罚正确改写;token F1 宽恕改写但被词汇重叠骗;判官抓定性维度但贵且随机。四个一起,每个评估覆盖其余漏掉的维度。
本节摘要:训练是你能用损失曲线监控的部分;评估是你必须设计的部分。本节构建统一评估流水线:接收任意训练好的语言模型,在其上跑四个异质评估,把结果聚合成每任务报告,并附一个本地 mock「LLM 当判官」(LLM-as-judge),使整个循环无需联网即可运行。四个评估覆盖每个要发布的模型都需要的维度:语言建模(困惑度)、短形式正确性(精确匹配)、开放形式相似度(token F1)、定性打分(判官)。单一指标从不描述一个语言模型——困惑度只说模型多贴合语言分布、不说它能否答问;精确匹配只说是否产出黄金串、惩罚正确改写;token F1 宽恕改写但被词汇重叠骗;判官抓定性维度但贵且随机。四个一起,每个评估覆盖其余漏掉的维度。
对应原课程:Phase 19 · Lesson 41 ·
eval-pipeline(原英文phases/19-capstone-projects/41-eval-pipeline/docs/en.md)。本节属「从零构建 GPT」赛道第十二节。
阅读完本节,你应当能够:
你想要的流水线是四个都有。每个评估覆盖其余漏掉的维度,各自跑在为该指标整形的不同留出子集上。最终报告把每任务数字并排放、加一个聚合,让评审一眼看出模型在做哪些权衡。
每个评估是从 (model, dataset) -> EvalResult 的函数。结果携带指标值、供检视的每例细节、与供聚合用的名字。流水线用一个配置说跑哪些评估、怎么加权,把它们组合起来。
困惑度是 exp(每 token 平均负对数似然)。实现有两个陷阱:
i 的 logits 预测位 i+1 的 token。这里的差一错是静默的:损失仍训,但指标变无意义。评估在非填充位上算每批 -log p(token) 之和与每批 token 计数,末尾再除。这比每批困惑度取平均(低估短序列权重)数值更稳,且匹配教科书定义。
精确匹配:预测串归一化(小写、去空白、折叠双空格)后与同样归一化的参考比,每例 1/0。token F1:预测与参考都小写切词,算共享 token 的 F1——宽恕改写,但被词汇重叠骗(错内容若词汇重叠高也会高分)。判官:本节用 mock 实现,按启发式(回答长度、是否含参考关键词、是否完整句)给 1-5 分,使循环无需联网;真实管线接 GPT-4/Claude 当判官,接口相同。
code/main.py 实现:
perplexity_eval(model, dataset):累计非填充位负对数似然与 token 数,末尾 exp(总/计数)。exact_match_eval(model, prompts, references):贪心生成、归一化、逐例比。token_f1_eval(model, prompts, references):token 化后算 precision/recall/F1。judge_eval(model, prompts, references, rubric):mock 判官按 rubric 打 1-5。aggregate(results, weights):加权聚合每任务分到单一报告。困惑度的关键骨架:
def perplexity_eval(model, loader): total_nll, total_tokens = 0.0, 0 model.eval() with torch.no_grad(): for x, y, mask in loader: # mask 标非填充位 logits = model(x) # (B, T, V) logp = F.log_softmax(logits, dim=-1) nll = -logp.gather(-1, y.unsqueeze(-1)).squeeze(-1) # (B, T) nll = (nll * mask).sum() # 只算非填充位 total_nll += nll.item() total_tokens += mask.sum().item() return {"perplexity": math.exp(total_nll / max(1, total_tokens))}
每个评估返 EvalResult(name, value, per_example);聚合按配置加权:
def aggregate(results, weights): score = sum(weights[r.name] * r.value for r in results) return {"tasks": {r.name: r.value for r in results}, "aggregate": score / sum(weights.values())}
设计要点:困惑度的「末尾再除」而非「每批困惑度取平均」是数值安全的关键——后者按批等权,短序列被高估,且每批
exp易溢出。所有评估用同一模型适配器接口model.generate(prompt) -> str或model(ids) -> logits,使适配器是唯一放模型特定代码的地方。换模型只动适配器,流水线其余不动。
lm-evaluation-harness(EleutherAI)是行业标准:数百个基准、统一接口、可插拔模型。HuggingFace evaluate 库提供精确匹配、BLEU、ROUGE 等单项指标。OpenAI evals、Anthropic 的内部 eval 框架思路相同:任务定义 + 指标 + 运行器 + 排行榜。本节手写四评估的目的是让你看清困惑度的掩码计数、F1 的 token 化、判官的 rubric——这些在用大框架时是黑盒。真实 LLM-as-judge 接 GPT-4/Claude API,需处理随机性(temperature=0、多投票)、位置偏置(交换候选顺序看分数是否变)、rubric 漂移(判官标准随时间变),这些是产品化评估的难点,本节的 mock 帮你先把骨架跑通。
code/main.py:四个评估函数 + 聚合器 + demo。demo 在 CPU 上几十秒跑完,把四个评估跑在一个微型预训模型上,打印每任务分与加权聚合。四个 *_eval 函数与 aggregate 可复用——换到任何有适配器的模型上,评估流水线原样工作。EvalResult 的 per_example 字段让你能下钻看哪个例子拖了分。
下一节,我们离开 GPT 赛道进入预训练/分布式赛道,做「大语料下载器」——流式下载、断点续传、MinHash 去重。