ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ROUGE指标全面解析:从原理到代码的文本生成评估实战

ROUGE指标全面解析:从原理到代码的文本生成评估实战 做NLG的人十有八九都得跟ROUGE打交道。我在跑文本摘要、生成式问答和对话回复评测实验时最常用的自动评价指标就是它。这个指标的全称是Recall-Oriented Understudy for Gisting Evaluation名字绕口但作用很朴素算生成文本与参考文本之间有多少词、短语、甚至句子片段是对得上的。这个系列写到第六篇前面聊了文本生成的整体框架、常用模型和训练策略今天把评价指标这块补上——重点拆解ROUGE的原理以及我在项目里真正会用到的代码。不管你是刚接触文本生成想搞清楚为什么论文总爱报ROUGE-1、ROUGE-L还是已经在用现成代码却不太明白分数变大变小意味着什么又或者是需要自己搭一套摘要评测流程正愁怎么选指标——这篇都会给你一套可以直接落地的方法。1. 为什么文本生成评估绕不开ROUGEBLEU的盲区与召回率视角1.1 自动评价指标在NLG中的位置自然语言生成任务里模型输出的是开放式文本序列。跟分类任务不一样你不能简单地说“预测对/错”因为同一个问题可能有多种合理的回答方式。人工评估当然最可靠但成本高、速度慢而且不同标注者之间的主观差异很大不适合在模型迭代过程中反复使用。所以大家需要一种自动指标能快速、可复现地给生成文本打分同时尽量与人的判断保持一致。ROUGE就是这类指标里最有代表性的一个。它最早是给自动文摘设计的后来被广泛借用到新闻摘要、生成式问答、对话回复、长文档压缩等几乎所有生成任务中。你去看相关论文摘要模型的验证阶段几乎都会出现ROUGE-1、ROUGE-2、ROUGE-L这几个名字。1.2 BLEU和ROUGE一对互补的视角很多新手会把BLEU和ROUGE搞混。BLEU来自机器翻译它有很强的“精度导向”它关心生成文本中的n-gram有多少能在参考文本里找到而且会对生成文本的长度做惩罚防止模型靠输出短文本刷分。ROUGE正好反过来它更强调“召回率”参考文本里出现的词或短语生成文本到底覆盖了多少。为什么会有这种区别因为翻译要求不漏译、不随意增译而摘要的评价重点在于信息覆盖——一篇合格的摘要应当覆盖原文或参考摘要中的关键信息哪怕表述不完全一致。用一个不严谨但好记的说法BLEU关心“生成文本是否可靠”ROUGE关心“参考要点是否被抓全了”。实践中两者往往搭配使用。你能看到不少NLG论文同时报告BLEU和ROUGE一个管精度一个管召回配合起来比单个指标信息量大得多。举个例子参考句子是“猫坐在垫子上”模型A生成“一只猫正坐在垫子上面”模型B生成“垫子上有猫”。BLEU可能更偏好模型A因为n-gram重叠更高ROUGE-1其实两个模型得分都不低因为关键名词都命中了但模型B更简洁短文本可能拉低BLEU。这不是说某个指标错了而是它们各自在看不同的东西。2. ROUGE家族指标逐个拆解ROUGE-N、ROUGE-L、ROUGE-S到底在算什么2.1 ROUGE-N以召回率为核心的n-gram重叠ROUGE-N计算的是n-gram级别的重叠常见的是ROUGE-1一元词和ROUGE-2二元词。以ROUGE-1为例它的召回率定义是参考摘要中被生成摘要命中的一元词数量除以参考摘要的一元词总数。这里有一个容易忽略的细节如果一个词在参考里出现两次在生成里只出现一次命中数只能算一次也就是取两个计数的最小值。所以不能简单地把生成文本中的每个匹配词都算进去。ROUGE-2同理只是把“一元词”换成“相邻二元短语”。举例参考是“我 喜欢 猫”生成是“我 喜欢 狗”。ROUGE-1 2/3 ≈ 0.667参考里的二元组是“我 喜欢”“喜欢 猫”生成里是“我 喜欢”“喜欢 狗”共现的只有“我 喜欢”一对所以ROUGE-2 1/2 0.5。从这个例子能明显看出ROUGE-N对词汇重复非常敏感换一个词分数就会掉一截但它几乎不关心这些词在句子里怎么排列。用ROUGE-1打分时“猫喜欢我”和“我喜欢猫”得分是一模一样的因为词袋相同。在实现时很多库会同时给出Precision、Recall和F-measure。虽然ROUGE名称里带Recall但最终报告时大家更常看F值因为它能平衡“生成文本太短但命中率极高”和“生成文本太长但覆盖率高”这两种极端情况。2.2 ROUGE-L最长公共子序列带来的顺序敏感性为了弥补不关心顺序的问题ROUGE-L引入了最长公共子序列LCS的思想。LCS不要求词在句子里连续出现只要求保持相对顺序。比如参考是“我 喜欢 安静 的 猫”生成是“我 喜欢 猫”LCS是“我 喜欢 猫”长度3。参考长度5生成长度3于是精确率P3/31召回率R3/50.6ROUGE-L通常取两者调和平均F1也就是2×P×R/(PR)2×1×0.6/1.60.75。这个值比只看ROUGE-1召回也是3/50.6更能体现“模型输出更精炼但保留了关键信息”的情况。ROUGE-L的动态规划实现也不复杂后文会给代码。需要注意ROUGE-L对顺序敏感但允许跳过中间词所以它比ROUGE-2更能容忍“换了一种自然说法”的情况又比ROUGE-1更能抓住主干顺序。另外还有一个容易混淆的点rougeL和rougeLsum在部分工具里是两个不同的指标。rougeL通常是把整段文本当作一个序列求LCSrougeLsum则是先把文本按句子切分对每一对句子求LCS再汇总计算。在做句子级摘要时两者差别不大但面对多句摘要rougeLsum更贴近人工习惯。用rouge-score库时两者都能直接算但要注意你的论文到底报的是哪一个。2.3 ROUGE-S与ROUGE-W处理更灵活匹配的扩展ROUGE-S计算的是skip-bigram也就是按原顺序任意两个词组成一个二元组中间可以跳过若干词。比如“我 喜欢 猫”的skip-bigram包括(我,喜欢)、(我,猫)、(喜欢,猫)。ROUGE-S的计算逻辑和ROUGE-2一样只是把“相邻二元组”换成“跳词二元组”因此它能在一定程度上捕捉非相邻词之间的搭配关系。ROUGE-SU则是在ROUGE-S的基础上同时加入unigram避免两个句子一个共同skip-bigram都没有时分数直接归零。ROUGE-W是加权LCS它给连续匹配的片段更高权重解决普通LCS对“连续匹配”和“分散匹配”一视同仁的问题。比如生成文本和参考文本在三个位置上分别各匹配一个词与连续匹配三个词ROUGE-L可能给出相同结果但直觉上连续匹配更接近人工摘要ROUGE-W就是为此设计的。这几个变体在学术论文中出现频率不高但当你需要分析模型什么时候“记对了要点但语序不太对”时ROUGE-S会比ROUGE-L更有参考性。2.4 一张表看清ROUGE家族的分工指标匹配单位是否关心顺序核心特点常见用途ROUGE-1unigram否词级覆盖最通用摘要快速筛选ROUGE-2bigram相邻顺序对短语重复敏感检查固定搭配、忠实度ROUGE-LLCS全局顺序容忍跳跃保留主干摘要流利度辅助ROUGE-Sskip-bigram相对顺序捕捉远距离共现分析词组搭配ROUGE-W加权LCS全局顺序强调连续匹配对顺序要求高的场景实际实验里我几乎只用ROUGE-1、ROUGE-2和ROUGE-LROUGE-S和ROUGE-W更多是研究型用途或者当发现模型总在调整语序导致ROUGE-L偏低时用来辅助定位问题。3. 代码示例从零实现到rouge-score库落地中文场景注意什么3.1 先手写一个ROUGE-N弄懂计数逻辑有时候调库调久了会忘记一个分数背后到底怎么算。所以我建议你至少手写一次ROUGE-N哪怕是简单版本。下面这个函数用Python实现处理英文按空格切分关键词是collections.Counter的并集操作。from collections import Counter def tokenize(text): # 英文按空格切分中文请先分词再做空格拼接 return text.lower().split() def rouge_n(hypothesis, reference, n1): hyp_tokens tokenize(hypothesis) ref_tokens tokenize(reference) def make_ngrams(tokens, n): # zip(*[tokens[i:] for i in range(n)]) 会生成相邻n元组 return Counter(zip(*[tokens[i:] for i in range(n)])) hyp_ngrams make_ngrams(hyp_tokens, n) ref_ngrams make_ngrams(ref_tokens, n) # Counter的操作会取两个计数的最小值正好对应匹配次数 overlap sum((hyp_ngrams ref_ngrams).values()) ref_total sum(ref_ngrams.values()) recall overlap / ref_total if ref_total 0 else 0.0 hyp_total sum(hyp_ngrams.values()) precision overlap / hyp_total if hyp_total 0 else 0.0 f1 2 * precision * recall / (precision recall) if precision recall 0 else 0.0 return {precision: precision, recall: recall, f1: f1} hyp the cat is on the mat ref the cat sat on the mat print(rouge_n(hyp, ref, n1)) print(rouge_n(hyp, ref, n2))这段代码里zip(*[tokens[i:] for i in range(n)])对新手来说可能有点绕。简单说对于n2tokens[0:]得到从第一个词开始的序列tokens[1:]得到从第二个词开始的序列zip再按位置打包就得到了相邻二元组。对n3三个偏移序列打包就得到三元组。用Counter统计频次后两个Counter求交集value会取较小的那个正好符合ROUGE里“一个词在参考中多次出现但只能按生成里的次数匹配”的规则。上面例子里ROUGE-1的精确率和召回率都是5/6≈0.833F1也是0.833ROUGE-2是3/50.6。你把这个结果和rouge-score库输出对比一下能确认自己的理解对不对。3.2 再手写ROUGE-L理解动态规划ROUGE-L的核心是最长公共子序列LCS。实现LCS最直观的方法是动态规划用一个二维表记录两个序列前缀的最长公共子序列长度。def lcs_length(a, b): m, n len(a), len(b) dp [[0] * (n 1) for _ in range(m 1)] for i in range(1, m 1): for j in range(1, n 1): if a[i - 1] b[j - 1]: dp[i][j] dp[i - 1][j - 1] 1 else: dp[i][j] max(dp[i - 1][j], dp[i][j - 1]) return dp[m][n] def rouge_l(hypothesis, reference): hyp_tokens tokenize(hypothesis) ref_tokens tokenize(reference) lcs lcs_length(hyp_tokens, ref_tokens) precision lcs / len(hyp_tokens) if hyp_tokens else 0.0 recall lcs / len(ref_tokens) if ref_tokens else 0.0 f1 2 * precision * recall / (precision recall) if precision recall 0 else 0.0 return {precision: precision, recall: recall, f1: f1} print(rouge_l(hyp, ref))动态规划的含义是dp[i][j]表示序列a的前i个词和序列b的前j个词之间的LCS长度。当前词相同就在左上角结果上加1不同则取“去掉a当前词”和“去掉b当前词”两种情况的较大值。这样从左到右、从上到下填完表右下角就是整个序列的LCS长度。用示例里的两个英文句子LCS长度是5所以ROUGE-L的P/R/F都约等于0.833。3.3 用rouge-score库做标准计算自己手写是为了理解原理真正做实验时还是推荐用成熟库减少边界case处理的负担。目前用得比较多的是Google的rouge-score安装很简单pip install rouge-score然后这样用from rouge_score import rouge_scorer scorer rouge_scorer.RougeScorer([rouge1, rouge2, rougeL], use_stemmerTrue) ref the cat is on the mat hyp the cat sat on the mat scores scorer.score(ref, hyp) for name in scores: score scores[name] print(f{name}: P{score.precision:.4f}, R{score.recall:.4f}, F{score.fmeasure:.4f})注意scorer.score的第一个参数是参考target第二个是生成prediction顺序别传反。use_stemmerTrue会对英文做Porter词干化处理把running、runs这类词都归一到run对一些词形变化明显的英文数据集有帮助。这里还有一个容易踩的坑rougeL和rougeLsum是不同的指标。上面的例子是句子级所以差异不大如果你做多句摘要最好把rougeLsum也加进去对比看看。使用方式就是把它加进指标列表scorer rouge_scorer.RougeScorer([rouge1, rouge2, rougeL, rougeLsum], use_stemmerTrue)rougeLsum按换行符把文本切成句子再对句子对做LCS聚合。实际做摘要评测时我更习惯用rougeLsum因为多句摘要的语义单元是句子而rougeL会把整段话揉成一个长序列句子边界信息会丢失。3.4 中文分词ROUGE中的隐藏变量中文不像英文天然按空格分词。如果直接把包含标点的整句传给rouge_score它会把整段中文当成一个tokenROUGE-1几乎只在“生成文本和参考完全相同”时才有分如果按字符切则又把字当作基本单位丢失了词边界。所以中文评测的正确姿势是先分词再把词用空格拼起来最后传给库计算。我一般用jieba做快速分词import jieba def zh_tokenize(text): return .join(jieba.cut(text)) ref 猫坐在垫子上 hyp 一只猫坐在垫子上面 scorer rouge_scorer.RougeScorer([rouge1, rouge2, rougeL], use_stemmerFalse) scores scorer.score(zh_tokenize(ref), zh_tokenize(hyp)) print(scores)这里use_stemmer必须设成False。中文没有英文那种词形变化开着反而会尝试对中文词“做词干化”通常不会带来正向收益。分词器选型也会直接影响ROUGE分数。同一对句子用jieba、pkuseg、LTP分词得到的词序列可能不同ROUGE数值也会有差异。所以在中文摘要评测报告里一定要写清楚分词器在同一个项目内做系统对比时所有模型必须统一用同一个分词器和同一套预处理。4. 多参考摘要与预处理这些细节决定你的ROUGE数值可信不可信4.1 多参考摘要怎么聚合max还是avg真实摘要数据集里一个源文本通常对应多个参考摘要。计算ROUGE时一般先把生成文本和每一条参考分别算分数再在参考维度上聚合。常见策略有两种取最大值max和取平均值avg。取max的意义是“只要模型输出和任意一条人工参考接近就算对”更宽容取avg的意义是“模型要覆盖大多数参考的写法才算好”更严格。我在主实验里更倾向报avg因为它对“偶然命中某一参考”的噪声更稳健。如果某个模型特别容易过拟合到参考里的高频表达用max会进一步掩盖过拟合问题。当然如果你要跟历史论文对比而对方报的是max那你只能跟着用max但要在备注里写明聚合方式。一段多参考计算的示例import statistics from rouge_score import rouge_scorer refs [ the cat is on the mat, a cat sits on the mat ] hyp the cat sat on the mat scorer rouge_scorer.RougeScorer([rouge1, rougeL], use_stemmerTrue) scores_list [scorer.score(ref, hyp) for ref in refs] r1_f [s[rouge1].fmeasure for s in scores_list] rL_f [s[rougeL].fmeasure for s in scores_list] print(ROUGE-1 avg:, statistics.mean(r1_f), max:, max(r1_f)) print(ROUGE-L avg:, statistics.mean(rL_f), max:, max(rL_f))4.2 预处理链路的影响ROUGE数值并不像它看起来那么“客观”。同一份生成结果因为预处理不同分数可以差出好几个点。常见的变量有是否全部小写。英文不转小写会漏掉句首大写的匹配所以通常统一转小写。是否去标点。标点符号如果作为token保留会把逗号、句号也计入n-gram造成无意义匹配。rouge-score默认的tokenizer会处理一部分标点但不同库的处理方式不同。是否做词干化。英文场景可以开use_stemmerTrue中文必须关掉。是否统一分句。对rougeLsum来说句子切分方式会直接影响结果。这些环节里最容易出问题的是“不同库的tokenization不一致”。py-rouge、rouge-score、HuggingFaceevaluate在处理标点和停用词时都不一样所以不同库的ROUGE分数不能直接对比。你在论文里看到“ROUGE-L: 45.21”时至少要想想它用的是什么库、什么分词器、多少条参考否则这个数值对你没有复现意义。4.3 不同库之间的差异rouge-score是目前维护较好、使用最广的Python库py-rouge是老牌工具底层需要Perl的ROUGE包tokenization逻辑不同现在新项目不建议再用了。HuggingFace的evaluate模块也提供ROUGE底层仍然依赖rouge-score但可能锁定的版本不同。我自己的项目里会固定rouge-score0.1.2这类明确版本号并写进requirements否则环境一换重跑实验分数对不上白折腾。5. 如何科学地解读ROUGE显著性、缺陷与综合评价方案5.1 ROUGE分数提高0.01真的代表模型更好吗很多论文把ROUGE提高0.01当作卖点但单点分数波动很可能来自噪声。一个简单的做法是配对bootstrap从测试集里有放回地抽取同样数量的样本每次重新计算两个模型的ROUGE差值重复几千次看差值的95%置信区间是否包含0。不包含0才能说“统计显著”。你也可以用置换检验permutation test。如果没有做显著性检验看到一个0.005的提升先别急着下结论。我见过不少实验同一个模型跑两遍因为Beam Search的随机性或不同解码批次ROUGE就能差0.005甚至0.01。所以评估时最好固定解码随机种子多次重复取平均再用统计检验判断差异是否可靠。5.2 ROUGE的四个典型盲区ROUGE有四个非常明显的弱点一是对同义词和释义不敏感。模型说“猫咪”参考说“猫”ROUGE-1可能认为完全不匹配。这种场景下需要语义指标比如BERTScore、BLEURT。二是对“参考多样性”不友好。人工摘要只是众多合理写法中的一种。模型生成与参考表达不同但同样正确时ROUGE会误伤。这也是为什么同一测试集上人工摘要之间的ROUGE通常只有0.7左右并没有想象中那么高。三是对信息冗余不敏感。模型不断重复参考里的关键词ROUGE-N可能很高但摘要实际冗长不堪。这时需要人工检查或额外计算重复率指标。四是对“信息正确性”无能为力。ROUGE衡量的是词汇重叠不是事实一致性。模型把“猫坐在垫子上”改写成“狗坐在垫子上”如果参考里同时有“狗”出现ROUGE可能照样很高但事实已经错了。所以ROUGE和事实正确性是两回事不能互相替代。5.3 我习惯的NLG评测组合我在实际项目里不会只依赖ROUGE。通常流程是先用ROUGE-1、ROUGE-2、ROUGE-L快速筛掉明显差的模型筛完后从候选模型里各抽200条做人工盲评重点看冗余、事实错误和逻辑一致性最后再补上BERTScore或BLEURT做语义层面的参考。如果ROUGE涨了但人工抽检发现事实错误变多我会更信任人工结论因为ROUGE本质上衡量的是“词面覆盖”不是“语义正确”。最后再分享一个小技巧。很多人习惯直接用生成文本的原始字符串去算ROUGE但从摘要评测的角度我会在送入计算前先把文本按句子拆分、去除冗余的重复片段再评测。因为rougeLsum这类摘要级指标对句子边界很敏感句子切分合理分数才更能反映真实摘要质量。如果你也在跑摘要实验不妨把这条加进自己的评测流程里或许能帮你少踩几个坑。
返回列表