ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ROUGE评估指标全解析:从原理到代码实战

ROUGE评估指标全解析:从原理到代码实战 在NLG项目里跑过评测的兄弟应该都有体会模型一顿输出猛如虎到了评价环节却经常不知道该信谁的。早期做文本生成大家习惯用BLEU看机器翻译拿到摘要任务上总觉得差点意思——BLEU太“抠”精确率了生成结果稍微换了个说法分数就掉得厉害。后来转到文本摘要、对话生成这些场景圈子里默认的首选就成了ROUGE。这篇博文就想把ROUGE这套评价指标彻底讲明白从原理、公式推导到代码怎么写、坑在哪里一次性说清楚。ROUGE全称是Recall-Oriented Understudy for Gisting Evaluation2004年由Chin-Yew Lin提出本来是给自动摘要做评估的后来慢慢成了NLG领域“事实上的标准”之一。核心思路很简单把模型生成的结果和参考摘要做n-gram重合度比较重合得越多分越高。跟BLEU最大的区别在于ROUGE更看重召回率——也就是参考文本里有多少内容被生成结果覆盖到了。这一点对摘要任务特别关键因为摘要评估最怕的就是模型只挑了一两句重点剩下重要信息全丢了。无论你是刚入门NLG的新手还是已经被各类评价指标折磨过的老手这篇文章都能给你一些实在的参考先讲清楚ROUGE家族每个成员的原理和适用场景再手把手带你把计算过程推一遍最后给出可直接运行的代码实现包括用evaluate库一行调用和从零手写两种方式。看完之后你不仅能跑通ROUGE还能知道它到底在算什么、结果怎么解读、什么时候该用哪个变体。1. ROUGE为什么能成为摘要评估的“标配”1.1 从BLEU到ROUGE评价指标到底在评价什么先聊点背景。机器翻译那边最常被提到的BLEU核心逻辑是看生成文本里的n-gram有多少出现在参考译文中然后计算精确率。这个打分方式对翻译任务相对友好因为翻译讲究“用词准不准”漏掉一个词可能意思就偏了精确率能很好地惩罚那些“多说了但没说对”的情况。但摘要任务不一样。摘要的核心诉求是“信息覆盖全不全”模型可以换一种表达方式去复述原文意思只要关键信息点都在就算合格。这时候如果还死磕精确率就会误伤很多表达灵活但内容合格的生成结果。ROUGE的设计者正是看到了这一点所以把召回率放在了核心位置它更关注生成结果有没有覆盖参考摘要中的重要信息。这种取向差异直接导致了两者在实践中的行为差异。举个我实际遇到过的例子生成结果写到“The cat sits on the windowsill”参考摘要写的是“A cat is sitting near the window”。BLEU会因为这个表达差异给出一个偏低的分数认为很多词没对上但ROUGE因为有一部分的n-gram重叠比如“cat”和“sitting/ sits”这类基础词形打分就会宽容很多。当然ROUGE这个“宽容”也带来一个问题——它只看字面重叠不看语义等价后面我会细说这个局限。1.2 ROUGE家族的成员划分与适用场景ROUGE不是一个单一的指标而是一整套指标体系大家族里最常用的成员有这么几个ROUGE-N基于n-gram的重合度计算最常见的是ROUGE-1unigram和ROUGE-2bigram。ROUGE-1看单个词的重合情况对词汇层面的信息覆盖比较敏感ROUGE-2看连续两个词的共同出现情况能捕捉到一定的短语和语序信息分数通常比ROUGE-1低不少因为连续匹配的门槛更高。ROUGE-L基于最长公共子序列LCS的计算方式。它不要求词是连续的允许中间有间隔更适合衡量“句子级别的结构相似度”。比如生成结果和参考摘要虽然在个别词汇上有出入但整体语序和主干结构一致ROUGE-L就能给出还不错的分数。ROUGE-W加权版本的ROUGE-L在计算LCS时会给连续匹配的片段额外加权目的是增强对“连续匹配”的敏感度。这个变体用得相对少一些主要在需要更细致区分连续匹配与非连续匹配差异的场景下用。ROUGE-S基于skip-bigram跳词二元组的计算方式允许两个词之间跳过任意数量的词。举例来说句子“A B C”的skip-bigram包括(A, B)、(B, C)和(A, C)。这种设定兼顾了语序信息和一定的表达灵活性。实际项目中最常用的组合拳是“ROUGE-1 ROUGE-2 ROUGE-L”。ROUGE-1和ROUGE-2负责词汇和短语层面的覆盖度评估ROUGE-L负责句子级别的结构一致性评估三者综合基本能覆盖大多数摘要场景的评估需求。像ROUGE-W和ROUGE-S更多是在做研究对比或者特定任务时才会单独拎出来用。2. 核心原理拆解ROUGE到底在算什么2.1 ROUGE-Nn-gram重合度的计算公式与直觉ROUGE-N的公式从直觉上讲非常朴素[ ROUGE-N \frac{\sum_{S \in {参考摘要}} \sum_{gram_n \in S} Count_{match}(gram_n)}{\sum_{S \in {参考摘要}} \sum_{gram_n \in S} Count(gram_n)} ]用一句人话翻译就是**参考摘要里出现的所有n-gram中有多少个也在生成结果里出现了。**分母是参考摘要的n-gram总数分子是生成结果和参考摘要匹配上的n-gram个数。跟BLEU做个类比会更容易理解。BLEU的公式逻辑是生成结果里的n-gram有多少在参考里出现过然后除以生成结果的n-gram总数这是精确率导向。ROUGE-N反过来以参考摘要的n-gram总数为分母这是召回率导向。两种指标站在不同的立场看同一件事BLEU问“模型说的内容里有多少是对的”ROUGE问“参考答案里的内容模型说出来了多少”。正是因为这种召回率导向的设计ROUGE-N有一种天然倾向如果模型把参考摘要里的话原封不动全抄下来哪怕自己额外多加了不少废话ROUGE的召回率依然能拿到很高的分数。这也是ROUGE一直被诟病的点——它管不住“信息冗余”只能管“信息覆盖”。在做生成内容质量评估时通常需要人工抽检或者配合其他指标一起使用不能光看ROUGE得分高就判定结果好。2.2 ROUGE-L最长公共子序列的计算逻辑ROUGE-L用到的LCS不是编程里常见的“最长公共子串”而是“最长公共子序列”。子串要求字符连续子序列只要求顺序一致允许中间跳过元素。放在文本评估场景里LCS的计算对象是单词序列。假设参考摘要包含4个词“A B C D”生成结果包含“A C D”那么它们的最长公共子序列就是“A C D”长度为3。ROUGE-L的召回率就是3/40.75精确率是3/31.0。这里有一个关键优势ROUGE-L不需要像ROUGE-N那样预先指定n的大小也不需要额外处理词形变化问题批处理方式不同它天然对语序和主干结构更敏感。比如生成结果是“The cat was chased by the dog”参考摘要是“The dog chased the cat”这两个句子表达的意思相近但语序大变ROUGE-L计算的LCS会是“The cat”或者“the dog”这样的短匹配分数不会太高。这种特性让ROUGE-L在衡量“句子结构是否接近”时更可靠。需要留意的是ROUGE-L的LCS计算虽然理论上不依赖分词但在英文里我们一般按空格分词中文里就得先做分词如果不分词直接按字计算结果语义会差很多。这个坑在后面代码部分我会专门强调。2.3 ROUGE-W和ROUGE-S两个进阶变体的原理简述ROUGE-W的全称是Weighted Longest Common Subsequence它解决的是ROUGE-L的一个盲区ROUGE-L在计算时只关心最长公共子序列的总长度不关心这个公共子序列内部是否是连续匹配的。举个例子参考摘要“A B C D E”生成结果“A X B Y C Z D W E”它的LCS长度是5跟完全连续匹配“A B C D E”得到的LCS长度一样但前者明显不如后者“扎实”。ROUGE-W就给连续匹配的片段加了权重连续匹配的段越长加权得分越高能够更好地区分这两种情况。ROUGE-S全称是Skip-Bigram Co-occurrence Statistics它把“二元组”的概念泛化了。ROUGE-2要求两个词必须是相邻的ROUGE-S允许它们中间隔着任意数量的词。比如参考摘要“the cat is on the mat”它的skip-bigram包括“the cat”、“the is”、“the on”、“the the”、“the mat”等等所有两两组合。这种设计让ROUGE-S在衡量语义覆盖时比ROUGE-2更灵活因为它不苛求语序完全一致只要两个核心词都出现了就行。但缺点也比较明显skip-bigram的数量非常庞大计算出来的数值通常比ROUGE-2低而且对停用词比较敏感实际使用时通常需要设置最大跳跃距离来限制计算规模。3. 手把手推演用一个例子把ROUGE算明白3.1 数据准备与ROUGE-1、ROUGE-2计算演示光看公式确实容易头晕我拿一个真实可复现的小例子把计算过程完整走一遍。假设参考摘要人工写的标准答案是the cat is on the mat模型生成的摘要待评估结果是the cat is under the mat先按空格分词得到两个词序列参考摘要[the, cat, is, on, the, mat]共6个token生成结果[the, cat, is, under, the, mat]共6个tokenROUGE-1计算统计每个unigram在两个序列中出现的次数词参考次数生成次数匹配次数取较小值the222cat111is111on100under010mat111分子匹配总数 211001 5ROUGE-1召回率 5 / 6 ≈ 0.8333ROUGE-1精确率 5 / 6 ≈ 0.8333ROUGE-1 F1 2 × 0.8333 × 0.8333 / (0.8333 0.8333) ≈ 0.8333ROUGE-2计算列出所有bigram参考摘要the catcat isis onon thethe mat共5个生成结果the catcat isis underunder thethe mat共5个匹配上的bigram是the cat、cat is、the mat共3个。ROUGE-2召回率 3 / 5 0.6ROUGE-2精确率 3 / 5 0.6ROUGE-2 F1 0.6看到这里你应该已经发现ROUGE-2的分数明显低于ROUGE-1。这个现象在实际项目中非常常见n-gram的n越大匹配门槛越高分数自然就越低。所以当你看到论文里ROUGE-2只有0.3、0.4时别急着觉得效果不行这很可能是正常的量级。3.2 ROUGE-L的计算演示与结果解读ROUGE-L需要计算两个词序列的LCS。参考序列“the cat is on the mat”和生成序列“the cat is under the mat”肉眼观察可以发现从开头开始“the cat is”三个词完全匹配然后参考里是“on”生成里是“under”匹配中断之后“the mat”两个词又重新匹配上所以最长公共子序列是the cat is the mat注意中间跳过了“on”和“under”这一对不匹配的词但保持了整体顺序长度为5。ROUGE-L召回率 5 / 6 ≈ 0.8333ROUGE-L精确率 5 / 6 ≈ 0.8333ROUGE-L F1 ≈ 0.8333这里可以对比一下ROUGE-2的0.6和ROUGE-L的0.8333。ROUGE-2要求“is on”和“is under”这种连续二元组完全一致所以扣分很狠ROUGE-L只要求词与词之间的相对顺序一致允许中间插入差异所以保住了不少分。在实际摘要评估中ROUGE-L往往比ROUGE-2更稳定因为它更贴近“信息覆盖”而不是“表达形式一致”。4. 代码示例从零手写实现到一行调用开源库4.1 用Python手写一个ROUGE-1计算器有些场景下我们确实需要自己控制计算逻辑比如做模型训练时的自定义损失、在特定数据集上做细粒度的指标分析等。我提供一个从零实现的ROUGE-1版本代码里每一步都做了注释方便你理解内部机制。from collections import Counter import re def tokenize(text: str) - list[str]: 简单的英文分词统一小写仅保留字母和数字。 中文场景记得替换成自己的分词逻辑比如调用 jieba.lcut(text)。 return re.findall(r[a-z0-9], text.lower()) def rouge_1(reference: str, hypothesis: str) - dict[str, float]: ref_tokens tokenize(reference) hyp_tokens tokenize(hypothesis) ref_counter Counter(ref_tokens) hyp_counter Counter(hyp_tokens) # 对参考摘要里的每个词统计它在生成结果中有多少个匹配 overlap 0 for token, ref_count in ref_counter.items(): hyp_count hyp_counter.get(token, 0) overlap min(ref_count, hyp_count) precision overlap / len(hyp_tokens) if hyp_tokens else 0.0 recall overlap / len(ref_tokens) if ref_tokens else 0.0 if precision recall 0: f1 0.0 else: f1 2 * precision * recall / (precision recall) return { precision: round(precision, 4), recall: round(recall, 4), f1: round(f1, 4), } if __name__ __main__: reference the cat is on the mat hypothesis the cat is under the mat print(rouge_1(reference, hypothesis))运行结果如下{precision: 0.8333, recall: 0.8333, f1: 0.8333}这个结果跟我们前面手推的一致。代码逻辑梳理一下对输入文本做分词和归一化处理分别统计参考摘要和生成结果的词频对每个参考词取“参考词频”和“生成词频”的较小值作为匹配数用匹配数分别除以生成结果长度和参考长度得到精确率和召回率最后计算F1值这种手写实现方式虽然简单但对理解了ROUGE-N的原理非常有帮助。如果你想扩展成ROUGE-2只需要把tokenize返回的词序列做一次滑窗把相邻两个词拼成一个二元组其余逻辑完全一致。4.2 使用HuggingFace evaluate库一行搞定全部变体日常项目里我更推荐直接用HuggingFace提供的evaluate库它把整个ROUGE家族都封装好了支持ROUGE-1、ROUGE-2、ROUGE-L、ROUGE-LSum等指标还支持多参考摘要的处理。安装和调用都很简单pip install evaluate pip install rouge_score注意一定要装上rouge_score这个底层包evaluate库只是一个封装层真正干活的是rouge_score里的工具。import evaluate # 加载ROUGE评估器 rouge evaluate.load(rouge) # 定义生成结果和参考摘要 predictions [the cat is under the mat] references [[the cat is on the mat]] results rouge.compute( predictionspredictions, referencesreferences, use_aggregatorTrue, ) print(results)运行结果如下{rouge1: 0.8333333333333334, rouge2: 0.6, rougeL: 0.8333333333333334, rougeLsum: 0.8333333333333334}几行代码就把ROUGE所有常用变体都跑出来了确实省事。这个rouge.compute函数内部做的事情跟我在前面代码里手动实现的核心逻辑是一样的只是在细节上做了大量增强支持同时计算ROUGE-N、ROUGE-L、ROUGE-Lsum等多个指标支持多参考摘要传入一个列表的列表计算时取每个参考下的最高分内置了英文的tokenization和停用词过滤逻辑支持通过rouge_types参数选择要计算哪些指标比如只算ROUGE-1和ROUGE-24.3 使用过程中的参数配置说明rouge.compute里有几个关键参数我实际用下来觉得有必要单独说明。第一个是rouge_types。默认情况下evaluate库会一次性返回rouge1、rouge2、rougeL、rougeLsum四个指标。如果只想算某几个可以这样指定results rouge.compute( predictionspredictions, referencesreferences, rouge_types[rouge1, rouge2], )这样结果里只会出现你指定的指标不仅省去了解析结果的麻烦多批量计算时也能稍微省点时间。ROUGE-Lsum和ROUGE-L的区别要稍微留意ROUGE-L是基于整句的LCSROUGE-Lsum是把摘要按句子拆开后对每个句子分别计算LCS再把所有句子的LCS长度相加作为分子。对于多句摘要ROUGE-Lsum会更合理一些因为单句级别的LCS累加不会跨句强制保持语序。第二个是use_aggregator。当传入多个样本时use_aggregatorTrue表示返回所有样本的平均分use_aggregatorFalse则返回每个样本单独的打分列表。在分析模型在哪些样本上表现差、哪些样本上表现好时我一般会关掉聚合器逐个查看明细。results rouge.compute( predictions[sentence one, sentence two], references[[sentence one], [sentence two]], use_aggregatorFalse, )第三个是tokenizer参数。这个参数允许你传入自定义的分词函数。做中文任务时这个参数几乎是必用的因为内置tokenizer是按英文空格来切分的直接跑中文效果非常差。我建议传入一个用jieba分词再拼接成空格的函数或者干脆把中文句子预先分词成以空格分隔的字符串再传入。5. 实战避坑ROUGE使用中容易踩的五个大坑5.1 中文场景必须预先分词否则结果完全失真这个问题我在项目里被狠狠教育过一次。早期在中文摘要数据集上跑ROUGE直接调evaluate库把原始中文文本丢进去结果分数低得离谱。后来排查才发现内置tokenizer按空格切分而中文句子根本没有空格整个句子被当成一个超大tokenROUGE-1的匹配数几乎全是0。正确做法是先对中文做分词。我通常用jiebaimport jieba import evaluate rouge evaluate.load(rouge) def chinese_tokenize(text: str) - str: return .join(jieba.lcut(text)) reference chinese_tokenize(猫坐在垫子上) hypothesis chinese_tokenize(猫趴在垫子旁边)或者直接用tokenizer参数传入分词函数。有一点要注意分词方式对ROUGE分数的影响非常大。不同分词器或同一分词器的不同模式如精确模式、全模式、搜索引擎模式会产生不同的token序列进而影响匹配结果。所以同一数据集内部要统一分词方式否则指标对比就没有意义了。5.2 多参考摘要时应该取最高分而不是平均分摘要评估通常不止一个参考答案标准做法是让多个标注员分别写参考摘要。evaluate库的references参数支持列表的列表比如references [[the cat is on the mat, the cat sits on the mat]]当有多个参考时rouge_score的计算逻辑是对每个参考分别计算ROUGE指标然后取最高分。这样做背后的逻辑是只要生成结果覆盖了任意一个参考摘要中的信息就说明信息是准确且重要的不应该因为没覆盖到其他参考而重复扣分。这种“取最高分”的策略在学术评估中几乎是标准做法我建议你沿用这个配置不要自己去改平均逻辑否则结果很难和别人论文里的数据对齐。另外做实验对比时尽量固定参考摘要的条数有的数据集每条样本3个参考有的只有1个参考数不同会导致分数无法直接比较。5.3 ROUGE和BLEU的选择看任务场景别盲选很多新手会问ROUGE和BLEU到底用哪个。我的建议是做摘要、对话生成、故事生成这类重内容覆盖的任务用ROUGE更合理做机器翻译、图像描述这类重表达准确性的任务BLEU更合适。我个人的习惯是两者配合使用。比如做摘要模型评测时我同时输出ROUGE-1、ROUGE-2、ROUGE-L和BLEU四个值。ROUGE看信息覆盖度BLEU看表达准确度两个维度放在一起能发现很多单指标暴露不了的问题。比如ROUGE分数高但BLEU很低说明模型信息覆盖到位了但语言表达和参考差异较大可能存在表达生硬或语法问题反过来ROUGE低但BLEU高说明模型说得很“标准”但关键信息漏掉了这在摘要任务里是最典型的“高分低能”信号。5.4 ROUGE-Lsum和ROUGE-L千万别混用ROUGE-L和ROUGE-Lsum之差在“是否按句拆分”。evaluate库默认同时输出这两个值很多同学不注意看直接用了rougeL来代表句子级指标但论文里引用的时候又不统一导致复现对不上。从实际使用的角度来说如果你的摘要只有一句话rougeL和rougeLsum是一样的如果摘要有多句话rougeLsum会先把摘要按换行符或句号拆成句子对每个句子单独计算LCS再汇总。这样做有个好处不会因为两个句子语序互换导致LCS被破坏。在长摘要场景下我更推荐以rougeLsum为主要参考同时记录rougeL两个都报出来方便和不同论文做比对。5.5 同义词改写和语义等价是目前ROUGE最大的盲区ROUGE毕竟是基于字面和词面重合度的指标它对同义词替换、语序大幅调整、句子结构改写几乎无能为力。网上有大量例子证明两句话语义几乎完全相同但ROUGE分数很低。例如“警方逮捕了嫌疑人”和“嫌疑人被警方抓获”语义基本一致但在中文分词后词的完全匹配数量极少ROUGE-1可能只有0.2到0.4。这就引出一个使用原则ROUGE适合做模型效果的快速衡量和版本对比但不适合作为最终质量判断的唯一标准。在关键节点上还是需要配合人工评估或者引入语义相似度指标如BERTScore、BARTScore等做交叉验证。我自己在项目里的流程是先用ROUGE做大批量筛选和训练时的监控指标再在最终测试集上抽几百条做人工评估同时用BERTScore等预训练模型指标做二次验证。这样的组合下来评价结论的可靠性会高很多。结尾ROUGE离“完美指标”还很远但依然是基线必备我的实际感受是ROUGE虽然在学术圈被吐槽了很多年但它依然是NLG评测里最稳定、最可复现、最大规模被使用的基线指标。原因很简单它足够透明足够快足够便宜任何人都能在几行代码里复现同样的分数。对比BERTScore这类需要加载预训练模型的指标ROUGE在成本、可解释性和跨语言通用性上依然有明显优势。最后分享一个小技巧把论文里的ROUGE分数复现不出来时先别急着怀疑模型多检查两个地方——分词方式是否一致、参考摘要的构造方式是否一致。我踩过的坑里十次有七八次是这两点造成的。ROUGE这套指标本身不复杂真正复杂的永远是数据预处理和评估流程的一致性。把这一步理顺了指标才能真正帮你说话。
返回列表