NLP评估指标:你的模型到底好在哪
数语觅类的评估从来没让我纠结过。精度、召回、F1、完全匹配率——四个指标各管一个维度,算起来也简单:模型输出一组标签,标签要么对要么错,逐个比完就有数了。我甚至做了个加权 score 把它们合成一个数,直接塞进训练循环当监控信号:
@property
def score(self) -> float:
return (
0.5 * self.f1 +
0.2 * self.precision +
0.2 * self.recall +
0.1 * self.em
)
每个 epoch 结束跑一遍验证集,score 出来了,该不该存 checkpoint 马上就知道。
到衔言渡意就不行了。翻译任务的标准指标是 BLEU,它需要完整的生成结果——模型得从 BOS 一直生成到 EOS,一整条译文出来了才能和参考对比。这不是一个能逐 token 算的指标,更不是一个能塞进 loss 函数的东西。我用的是 sacrebleu 的 corpus_bleu,它需要收集完所有生成结果之后一次性算:
sacrebleu.corpus_bleu(hypotheses, [references]).score
一开始觉得是自己偷懒没找到实时算的方法,后来意识到不是——这两类指标度量的东西根本不同。分类指标看的是“对不对”,每个样本独立判定,算完一个扔一个。BLEU 看的是“像不像”,它要把生成的句子和参考的句子摆在一起比较 n-gram 重叠,这个比较本身就要求完整序列。
不是所有的“好”都能用同一把尺子量。这篇文章想做的事情是:把常见的 NLP 评估指标按它们度量的维度分类——从最严格的“完全一致”到最宽松的“概率分布”。不是按任务分(分类用这个、翻译用那个),而是按“它在看什么”分。一旦知道一个指标在看什么维度的“好”,它适合什么任务自然就清楚了。
精确匹配:对就是对,错就是错
最严格的一把尺子:预测和标签完全一致才算对,差一个字符都是错。
Accuracy 是最基本的形态——正确预测数除以总预测数。单标签分类任务里,模型输出一个类别,答案也是一个类别,对上了就是 1,没对上就是 0,全部加起来除以样本数。没有灰色地带。
Exact Match(EM) 是同一个思路推到序列上去。不再是一个标签对一个标签,而是一整条输出对一整条标签——逐字符比对,完全一致才得分。抽取式问答里常见:模型从原文里抽出一个 span 作为答案,和标准答案一字不差才算对。
两者的共同点是判定标准的绝对性:没有“部分正确”这回事。这在分类任务里很自然——类别要么对要么错,没有“对了一半”的类别。但推到更复杂的任务上,这个严格性会变成问题。
翻译里,“我很高兴”和“我非常开心”是同一个意思的两种合法表达,exact match 给 0 分。摘要里,换了一个同义词、调了一下语序,意思完全没变,exact match 照样判错。当任务的合法输出不唯一时,要求完全一致就太苛刻了。
但“太苛刻”不等于“没用”。
韵染流光的两层反思
韵染流光的输出是一套 DSL——颜色操作指令。模型生成的序列里包含随机小数(色相值、明度偏移量之类的),当时觉得这种输出没法做 exact match,就直接放弃了序列级评估。
三个项目做下来回头看,发现可以拆层。
DSL 的输出长这样:
color 436 148 142511
或者带修饰操作的:
color 520 177 142511<sep>lightness $p -0.101
结构部分(color、<sep>、lightness、$p)和数值部分(436、148、-0.101)性质完全不同。数值是连续的,差 0.01 和差 0.1 不是一回事,逐字符匹配没有意义。但结构是离散的——指令名称、操作符、分隔符,这些要么对要么错,天然适合 exact match。
做法是:用正则把数值位置提取出来替换成占位符,结构部分做 exact match,数值部分另设容差阈值(比如 ±0.02)。一个看似不能用 exact match 的任务,拆开后结构维度反而最适合 exact match。
但这还没完。同一个输入可以产生结构不同但都正确的输出:
深绿 → color 436 148 142511
深绿 → color 520 177 142511<sep>lightness $p -0.101
第一条直接命中了目标色,第二条先取了一个近似色再做明度微调——两条路径都合法。结构本身就有多种正确形式,不能只匹配一种。
不过韵染流光的 DSL 是有限的形式语言——合法的结构泛式是可以穷举的。整理出所有合法泛式,模型输出匹配任意一种即算结构正确,否则非法。这需要覆盖足够多的泛式(不然会误判合法输出为非法),但范围是有限的,方案可行。
两层反思都在说同一件事:exact match 没有失效,是评估的粒度需要更细。当输出可以拆成不同性质的层,每层用最合适的度量方式,精确匹配在它该管的维度上依然是最干净的判据。
但当任务的输出是开放域自然语言——翻译、摘要、对话——合法表达不可穷举,拆层也拆不动。完全一致这把尺子真正力不从心的时候,需要退一步:不要求完全一样,只看重叠了多少。
集合重叠:预测和标签之间的交集
精确匹配把世界分成对和错两半。集合重叠退了一步:不要求完全一致,而是看预测和标签之间交了多少集。
数语觅类就是这个场景。一条数据库列可能同时属于多个语义类型——一个列既是“日期”又是“创建时间”,模型需要输出一组标签。评估不再是“对了还是错了”,而是“对了几个、错了几个、漏了几个”。
这就引出了两个不同方向的问题:
Precision(精度):模型预测的标签里,有多少是对的?公式是 正确预测数 / 总预测数。它在问:模型有没有乱说——预测了 5 个标签,只有 3 个在标准答案里,precision = 0.6。
Recall(召回):标准答案的标签里,有多少被模型预测出来了?公式是 正确预测数 / 标准答案数。它在问:模型有没有漏掉——标准答案有 4 个标签,模型只命中了 3 个,recall = 0.75。
Precision 和 recall 是同一枚硬币的两面,但它们的权衡在不同业务场景里完全不同。垃圾邮件过滤宁可漏掉几封垃圾(recall 低一点),也不要把正常邮件扔进垃圾箱(precision 必须高)。医疗诊断反过来——宁可多报几个疑似(precision 低一点),也不能漏掉一个真阳性(recall 必须高)。
F1 是两者的调和平均:2 * precision * recall / (precision + recall)。为什么不用算术平均?因为调和平均对短板更敏感。precision 0.9、recall 0.1,算术平均是 0.5,看起来还行;调和平均是 0.18,直接暴露了 recall 崩了。F1 不让你用一项的高分遮盖另一项的灾难。
Hamming Loss 换了一个角度:不看命中了多少,看每个标签位置上的错误率。32 个可能的类型,模型在其中 3 个位置判断错了(该标的没标、不该标的标了),hamming loss = 3/32。它和 F1 的区别在于:F1 只关注被预测为正的标签和标准答案之间的重叠,hamming loss 连“模型正确地没选”的位置也纳入计算。标签空间很大、正确标签很少的任务里(数语觅类的 32 类就是),hamming loss 能反映模型在大量“不该选”的位置上有没有乱选。
数语觅类的加权 score 把这四个维度合在一起看——F1 占大头(0.5),precision 和 recall 各占 0.2,EM 占 0.1。权重的选择很直觉:F1 是综合判据所以权重最大,EM 最严格但多标签任务里很难拿满分所以权重最小。
集合重叠比精确匹配灵活了很多,但它依然有一个前提:标签是离散的、可枚举的。预测一组标签,标准答案也是一组标签,两者取交集——这个操作要求“标签”是明确的、可比较的单元。
翻译任务的输出是一条完整的自然语言句子。“我很高兴”和“我非常开心”——如果把每个字当标签做集合重叠,“我”和“高”命中了,“很”和“兴”没命中,这个结果没有意义。自然语言的比较单元不是单个字,也不是整条句子——需要一个中间粒度。
序列重叠:从“对不对”到“像不像”
这个中间粒度就是 n-gram——连续的 n 个词组成的片段。unigram 是单个词,bigram 是连续两个词,trigram 三个,以此类推。
“我 非常 开心” 的 bigram 集合是 {我-非常, 非常-开心}。“我 很 高兴” 的 bigram 集合是 {我-很, 很-高兴}。两者的 bigram 重叠为零——但 unigram 重叠有一个(“我”)。n 越大,匹配条件越严格,能匹配上的就越少;n 越小,匹配越宽松,但也越不能反映语序和搭配。
BLEU 和 ROUGE 都基于 n-gram 重叠,但它们各自站在不同的立场上看这个重叠。
BLEU:你生成的东西有多少是对的
BLEU 是 precision 导向的。它问的是:模型生成的 n-gram 里,有多少出现在参考译文中?
直觉是“不要瞎编”。翻译任务里,模型编造参考中不存在的词组是严重问题——哪怕漏了一些内容,只要生成的部分是对的,BLEU 不会太难看。但如果模型胡说八道,哪怕说了很多,BLEU 会惩罚。
标准 BLEU 同时看 1-gram 到 4-gram 的 precision,取几何平均。只看 unigram 会被词袋匹配骗过(词都对了但语序乱了),加上更长的 n-gram 才能约束局部语序。
还有一个细节叫 brevity penalty——如果生成结果比参考短太多,BLEU 会额外扣分。这是为了堵一个漏洞:模型只生成它最有把握的几个词,precision 很高,但它其实什么都没说。brevity penalty 把“说太少”也纳入惩罚。
衔言渡意最终的 BLEU 是 34.16。这个数字意味着什么?粗略的刻度感:BLEU 10 以下基本不可用,15-25 能看出模型学到了跨语言映射但翻译不流畅,25-35 开始有可读性,35 以上可以实用。51.7M 参数在三语六向下做到 34 是合理的——模型确实在翻译,不是在乱生成,但和大模型的流畅度还有差距。
ROUGE:参考里的东西有多少被你覆盖了
ROUGE 是 recall 导向的。它问的是:参考文本里的 n-gram,有多少出现在模型输出中?
直觉是“不要漏重点”。摘要任务里,原文的关键信息必须出现在摘要中——你可以换词、改语序,但核心内容不能丢。ROUGE 度量的就是这种覆盖率。
ROUGE 有几个变体:ROUGE-N 看 n-gram 重叠(和 BLEU 的对偶),ROUGE-L 看最长公共子序列(不要求连续,只要求顺序一致),ROUGE-W 是加权版的 ROUGE-L(连续匹配比分散匹配得分更高)。
同一套思路,不同的立场
BLEU 和 ROUGE 的底层机制是一样的——都在数 n-gram 的重叠。区别只在分母:BLEU 的分母是生成文本的 n-gram 数量(生成了多少对的),ROUGE 的分母是参考文本的 n-gram 数量(参考里多少被覆盖了)。
这让它们天然适合不同的任务。翻译在乎“别瞎编”——precision 重要,用 BLEU。摘要在乎“别漏掉”——recall 重要,用 ROUGE。不是因为“翻译就该用 BLEU”,而是因为翻译任务最在乎的维度恰好是 BLEU 度量的那个维度。
但 n-gram 重叠终究是表面形式的匹配。“这个电影很棒”和“这部影片非常精彩”的 n-gram 重叠率很低,但语义几乎完全一致。BLEU 和 ROUGE 能告诉你“像不像”,但它们对“像”的定义停留在词面上。
更精细的度量存在。BERTScore 的做法是把匹配单元从字面 n-gram 换成语义向量——生成文本和参考文本各过一遍预训练语言模型(最初是 BERT,现在可替换),得到每个 token 的上下文向量,然后用余弦相似度做配对,分别算 precision、recall 和 F1。本质上是把 BLEU 的 equals()换成了 cosine_similarity(encode(a), encode(b))。代价是每次评估要跑一遍模型推理,计算量比数 n-gram 大得多。
对于我目前做的小模型训练来说,BLEU 的粒度够用了——BLEU 34 能让我确信模型在做翻译而不是在胡说。BERTScore 等语义级指标留给需要更细粒度区分的场景。
排序质量:好的结果排在前面
前面三章的指标都有一个隐含假设:存在一个“正确答案”,评估的是模型的输出和这个答案之间的距离。但有一类任务根本不问“答案是什么”——它问的是“答案排第几”。
检索任务就是这样。用户输入一个 query,系统返回一个排好序的列表,里面有些结果是相关的,有些不相关。评估的不是“返回了什么”,而是“相关的结果有没有排在前面”。
Recall@K:前 K 个里捞到了几个
最直觉的度量:一共有 5 个正确答案,系统返回的前 10 个结果里命中了 3 个,Recall@10 = 3/5 = 0.6。
它不关心排序——只关心“在不在前 K 个里”。第 1 位命中和第 10 位命中,对 Recall@K 来说没有区别。K 越大越容易高,但 K 大了也没意义——返回整个数据库 Recall 就是 1.0,然而并没有什么用。
Recall@K 回答的问题是:模型的覆盖能力。前 K 个位置就是你给模型的预算,它在这个预算内捞到了多少正确答案。
MRR:第一个对的排第几
Mean Reciprocal Rank 只关心第一个正确结果的位置。
第一个正确结果排在第 1 位,得分 1/1 = 1.0。排在第 3 位,得分 1/3 ≈ 0.33。排在第 10 位,得分 1/10 = 0.1。对多个 query 取平均就是 MRR。
它适合“找到就行,不需要找全”的场景——比如“帮我找那篇论文”,用户只需要一个正确结果,排第几决定了体验好不好。
MRR 的代价是视野极窄:第一个对的排第 1,后面全乱套,MRR 照样满分。它在位置这个维度上做了,但只照亮了一个点。
NDCG:整张排行榜的质量
Recall@K 只管覆盖,MRR 只看头一个。NDCG(Normalized Discounted Cumulative Gain)把整张列表的排序质量都纳入评估,而且引入了分级相关性——结果不再只是“相关/不相关”的二元判定,而是可以有“完美匹配”(3分)、“部分相关”(2分)、“勉强沾边”(1分)、“无关”(0分)这样的连续分级。
计算分三步:
Cumulative Gain(CG):把列表里每个结果的相关性分数加起来。不考虑顺序——只看总分。
Discounted Cumulative Gain(DCG):给位置加折扣。每个位置的得分除以 log₂(位置+1)。排第 1 的除以 log₂2 = 1(不打折),排第 2 的除以 log₂3 ≈ 1.585,排第 5 的除以 log₂6 ≈ 2.585。越靠后折扣越重——同样一个好结果,排在前面比排在后面值钱得多。
Normalized DCG(NDCG):用“理想排序的 DCG”做分母,归一化到 0~1。理想排序就是把所有结果按相关性从高到低排——这是 DCG 的理论上限。NDCG = 1.0 意味着当前排序就是理想排序。
NDCG 同时回答了两个问题:相关的结果排前面了吗?更相关的排得比次相关的高吗?这是前两个指标都无法单独回答的。
三者的关系
三个指标是信息量递增的:
- Recall@K:捞到了多少(不管顺序)
- MRR:第一个对的排第几(只看一个点)
- NDCG:整个排序的质量(全局、分级、位置敏感)
Recall@K 解决覆盖问题,MRR 在位置维度上迈了一步但只看了一个点,NDCG 把位置和分级同时拉满。三者不是加法关系——NDCG 度量的维度比前两者的并集还大。
我的三个项目都没有直接使用过排序指标,但理解它们度量的维度很重要:检索、推荐、RAG 的检索阶段——这些场景下模型的“好”不是输出了什么,而是把好东西排在哪。这是一个和前面三章完全不同的“好”的维度。
分布度量:模型有多“惊讶”
前面所有指标都在比较具体的输出——预测和标签之间对不对、重叠多少、排第几。但有些任务根本没有“正确答案”可以拿来比。
开放式文本生成就是这样。给模型一个开头“今天天气”,它可以接“很好”、“不错”、“真热”——哪个都行,没有唯一正确的下一个词。没有标准答案,就没法用精确匹配、集合重叠或序列重叠。这时候需要换一个维度:不看模型输出了什么,看模型的概率分布本身好不好。
Cross Entropy:训练时一直在看的那个数
Cross entropy 度量的是:模型的预测概率分布和真实分布之间的“距离”。
在训练循环里,每一步的 loss 就是 cross entropy——模型对当前位置输出一个概率分布(vocab 上的 softmax),loss 函数拿真实的下一个 token 去这个分布里查它的概率,取负对数。概率越高,loss 越低。
其中 P(w_i) 是模型在位置 i 给正确 token 分配的概率。
直觉:模型在每个位置有多“惊讶”。如果模型很确定下一个词是什么(给了 0.9 的概率),loss 很低,不惊讶。如果模型完全蒙(均匀分布在 48000 个 token 上),loss 就是 log(48000) ≈ 10.8,非常惊讶。
衔言渡意的最终 val loss 是 2.395——训练过程中一直在看的就是这个数。
Perplexity:换个更直觉的刻度
Perplexity 是 cross entropy 的指数形式:
衔言渡意的 val loss 2.395 对应的 perplexity 是 e^{2.395} ≈ 10.97。
这个数的直觉含义:模型在每个位置平均要从大约 11 个 token 里猜。词表有 48000 个 token,模型把范围缩窄到了 11 个——它确实学到了大量的语言规律,但还有不确定性。作为对比,完全随机猜的 perplexity 就是词表大小 48000,训练好的大模型 perplexity 可以低到个位数。
我在训练中从来没有算过 perplexity 这个数——一直看的就是 cross entropy loss。但它们是同一个东西的两种刻度:cross entropy 是“惊讶度的对数”,perplexity 是“惊讶度本身”。学术论文里报 perplexity 比较多,因为它的数值更直觉(“从 11 个里猜” vs “loss 2.395”)。但在训练循环里看 loss 就够了,没必要多算一步指数。
和训练的关系
分布度量有一个独特的位置:它是唯一一个直接参与训练过程的指标类别。
精确匹配、F1、BLEU——这些都是训练结束后或验证阶段才算的外部评估。但 cross entropy 就是 loss 函数本身,梯度直接从它来,参数直接朝着降低它的方向更新。
这也意味着 cross entropy / perplexity 度量的是一个更底层的东西:不是“输出好不好”,而是“模型对语言的理解程度”。一个 perplexity 低的模型不一定翻译得好(它可能理解语言但不擅长跨语言映射),一个 BLEU 高的模型 perplexity 也不一定低(它可能在翻译上找到了捷径但语言模型本身不强)。它们度量的是不同维度的“好”。
收尾
回头看这条线:精确匹配要求完全一致,集合重叠看交集大小,序列重叠比较 n-gram,排序质量关注位置,分布度量看概率分布本身。严格程度在递减,适用的任务复杂度在递增——越复杂的任务,越不可能有唯一正确答案,指标就越需要“退一步”来定义什么算“好”。
选指标不是选工具,而是在回答一个更根本的问题:你最在乎模型在哪个维度上好。这个问题的答案决定了指标,指标不决定答案。