资讯详情

资讯详情

多模型对比统计检验:Friedman检验与Nemenyi检验全解析

做模型性能评估这几年我有个特别深的感受实验谁都会跑难的是把结果说清楚。尤其是手里同时握着五六个模型、十几张数据集技术问题到最后往往变成统计学问题。这时候最常用的一套工具就是Friedman检验配Nemenyi后续检验很多机器学习顶刊和会议上你都能看到它的身影。但说实话网上关于这套方法的中文资料并不算多要么只给公式不讲原因要么只讲原理不给可复现代码能让人顺着思路从手算到python跑通的更少。这篇文章就是来补这个缺的适合做算法对比的工程师、写论文需要统计支撑的研究生以及任何被审稿人问过“你的显著差异是怎么检验的”的人。1. 多个模型在多个数据集上做对比为什么不能只看平均排名1.1 平均排名只是描述性统计不是推断性结论先讲一个我见过很多次的场景。有人在技术群里兴奋地贴出结果六个模型在16个数据集上的平均排名他的模型排第一于是得出结论“我这方法明显优于其他模型”。结果下面一个做统计的人淡淡回一句“这个排名差做过显著性检验吗”群里瞬间安静了。问题就出在“平均排名”这四个字上。平均排名是一个描述性统计量它只能描述你手里的这一批数据是什么样不能告诉你“如果换个数据集、换个随机种子这个排名是否还能维持”。模型A可能只因为在几个简单数据集上运气好平均排名就高了0.2但这种优势可能完全是噪声。想要区分“真实优势”和“随机波动”必须上推断统计也就是显著性检验。我见过不少工程团队内部做模型选型时直接看平均准确率或者平均排名谁高选谁。简单场景下可能没问题但一旦模型多了、数据集杂了这种做法的风险就很大。因为不同数据集的难度差异极大在一个数据集上把准确率从0.90提到0.92和另一个数据集上从0.60提到0.70完全不是一个量级的事直接加总平均会扭曲判断。1.2 把数据集当重复实验做t检验和ANOVA为什么不行很多人第一反应是用ANOVA或配对t检验。想法很自然每个模型在每个数据集上都有一个分数那就把每个数据集当一次重复实验比较k个组是否来自同一总体。但这里有几个现实问题。第一ANOVA的正态性和方差齐性假设很难满足。准确率、F1这类指标是有界的通常偏向分布尤其靠近0或1的时候方差明显被压缩。你在多个数据集上收集到的模型表现方差结构通常很复杂硬套ANOVA容易得到不可靠的结论。第二每个数据集内部两个模型的评估结果并不独立。比如你在同一个测试集上算A和B的准确率这两笔观察共享同一批样本和噪声本质上是有相关性的配对数据。把12个数据集当成12个独立观测来跑常规ANOVA会低估误差、抬高显著性。第三多重比较问题。如果有6个模型两两组合是15对你做15次配对t检验就算全程没有真实差异靠碰运气也有很高概率碰出几个p0.05的结果来。所以对于“跨数据集、多模型”这种结构业界更认可的处理方式是把数据集看成“区组”在区组内对模型表现排序用秩次做分析。Friedman检验就是干这个的。1.3 秩次把不同数据集上的绝对分数变成可比的位置秩次的思想其实一句话就能说清不在不同的数据集之间比较分数大小只在同一个数据集内部比较模型间的高低。每个数据集上最好的模型得秩1第二名得秩2以此类推。这样无论这个数据集整体是简单还是难、分数是偏高还是偏低都能被统一到“位置”这个尺度上。举个例子某数据集上四个模型的准确率是0.96、0.91、0.87、0.84那他们的秩就是1、2、3、4。另一个数据集上四个模型准确率是0.70、0.65、0.63、0.60秩依然是1、2、3、4。虽然两个数据集分数范围完全不同但在秩的尺度上完全可比这正好解决了我前面说的“跨数据集加总失真”问题。Friedman检验就是在这样一张“秩次矩阵”上做文章。它的原假设是k个模型在n个数据集上的平均秩没有显著差异也就是说所有模型表现相同各个位次的优势只是随机波动。2. Friedman检验的核心思想与手算全过程4模型×8数据集实例2.1 原假设与统计思想在正式进入公式前先建立一个直观认知。假设模型根本没有差异那么每个数据集上的排名就纯粹是随机的每个模型的平均秩都应该接近(k1)/2。比如4个模型时这个基准值就是(41)/22.5。如果模型A平均秩是1.3模型D是3.7那显然偏离基准很多说明模型之间很可能存在真实差异。Friedman检验做的事就是计算观察到的平均秩与基准值的偏离程度如果偏离超过某个阈值就拒绝原假设认为至少有一个模型的性能分布与其他模型不同。2.2 一个可复现的手算案例我构造一个能完整演示过程的案例4个模型A、B、C、D8个公开数据集风格的数据点用准确率作为性能指标。数据如下数据集模型A模型B模型C模型DD10.960.910.870.84D20.940.880.890.82D30.920.950.860.83D40.930.890.830.85D50.950.900.870.83D60.930.890.900.83D70.900.930.820.85D80.950.910.870.84注意这里的指标是越大越好所以每个数据集上准确率最高的模型秩为1。先把这个矩阵转成秩次矩阵。比如D1这一行A0.96最高秩1B0.91次之秩2C0.87排第三秩3D0.84排第四秩4。每行都这么处理数据集A秩B秩C秩D秩D11234D21324D32134D41243D51234D61324D72143D81234然后对每一列求和得到每个模型的秩和。A1121112110B2312231216C3234324324D4443443430。除以数据集数8就得到平均秩A1.25B2.0C3.0D3.75。一眼看过去A的表现明显更好D则垫底整个排序的确定性似乎也蛮高。但“似乎”不行我们要用统计量来验证。2.3 χ²_F统计量计算Friedman检验的统计量公式是χ²_F [12 / (n × k × (k1))] × ΣR_j² - 3 × n × (k1)其中n是数据集数量8k是模型数量4R_j是第j个模型的秩和。先把各模型的秩和平方加起来R_A² R_B² R_C² R_D² 10² 16² 24² 30² 100 256 576 900 1832。代入公式χ²_F [12 / (8 × 4 × 5)] × 1832 - 3 × 8 × 5 (12 / 160) × 1832 - 120 0.075 × 1832 - 120 137.4 - 120 17.4自由度是k-1 3。查卡方分布表自由度为3时0.001显著性水平的临界值约16.27而我们算出的17.4比它更大所以p值小于0.001。在常用α0.05的显著性水平下可以非常自信地拒绝原假设四个模型的性能存在显著差异。2.4 Iman-Davenport修正与Kendall W效应量看到这里你可能以为万事大吉了但我必须提醒一个细节上面的χ²_F统计量在大样本下近似卡方分布但当n比较小时这种近似会偏乐观即容易“假阳性”。统计学家Iman和Davenport在1980年提出了一个更稳妥的修正统计量用F分布来近似F_F (n-1) × χ²_F / (n × (k-1) - χ²_F)它的自由度是df1 k-1df2 (k-1) × (n-1)。代入本例F_F 7 × 17.4 / (8 × 3 - 17.4) 121.8 / 6.6 ≈ 18.45。自由度是3和21查F分布表p值仍然远小于0.001。这说明结论很稳健。另外报告结果时最好同时给一个效应量推荐Kendall协调系数WW χ²_F / (n × (k-1))代入计算W 17.4 / 24 0.725。经验上W0.1算小效应0.3算中等0.6以上就算很大的效应。0.725说明4个模型在8个数据集上的排名一致性非常高这个结果不只是“统计显著”而且实际排序也非常稳定。3. Nemenyi后续检验Friedman显著之后到底谁和谁真的有差异3.1 Friedman显著只是开始不是结束Friedman检验显著只说明“这几个模型整体上存在差异”但不会告诉你差异具体发生在哪些模型之间。可能是A显著强于D也可能A、B、C、D之间都有差异。想要定位到具体哪一对就需要后续检验也叫事后检验或post-hoc检验。最直观的做法是把所有模型两两组合分别跑配对比较。但这就绕回了多重比较问题四模型有6对组合每一对都有5%的假阳性风险全部跑下来整体出错的概率远不止5%。Nemenyi检验正是为这个问题设计的它用studentized range分布来校正临界值让所有两两比较合在一起的总体错误率控制在α以内。3.2 临界差CD怎么算、怎么查表Nemenyi检验的核心值是临界差Critical Difference缩写CD。只要两个模型平均秩的差的绝对值超过CD就认为两者差异显著反过来如果平均秩差不到CD那就算一个是1.25、一个是2.0也只能说“排序上有差距但统计上分不开”。CD的计算公式是CD q_α × sqrt(k × (k1) / (6 × n))其中q_α来自studentized range分布查表值和模型数k、显著性水平α有关。这里给出α0.05时常用的几个q值来自Demšar 2006年的经典论文中的表kq_0.0521.96032.34342.56952.72862.850我们例子中k4所以q2.569。代入公式CD 2.569 × sqrt(4 × 5 / (6 × 8)) 2.569 × sqrt(20/48) 2.569 × 0.6455 ≈ 1.6583.3 本例的两两比较结果现在把四个模型的平均秩两两相减对比平均秩差是否大于CD1.658结论A vs B0.75否不显著A vs C1.75是显著A vs D2.50是显著B vs C1.00否不显著B vs D1.75是显著C vs D0.75否不显著这个结果非常典型Friedman整体检验是显著的但Nemenyi事后检验没有给出“全明星互撕”的结论。A显著优于C和DB显著优于D但A和B之间、B和C之间、C和D之间都还不能下结论。你看这个信息量比单纯一句“四个模型有显著差异”丰富得多。如果有人说“我的模型排名第一显著强于所有对手”那你就可以用CD图去验证如果他和第二名的平均秩差不到一个CD这个说法就站不住脚。3.4 读懂CD图和热力图Nemenyi检验最常见的可视化叫CD图或者ranking图。横轴是平均秩通常从左到右表示从优到劣但也有一些图是反过来的所以看的时候先确认坐标方向。每个模型在横轴上占一个位置如果两个模型之间画了一根粗线段说明它们差异不显著归在同一档如果没有线段连着说明它们之间存在显著差异。我们例子中A和B的平均秩是1.25和2.0差0.75小于CD所以A和B之间会有一条粗线。A和C平均秩1.25和3.0差1.75大于CD没有连线说明A和C不在一档。这样的图最直观的价值是一眼扫过去能看到模型被分成了几档你的方法和最强对手是否真的拉开了距离。另外Python库scikit-posthocs还提供另一种形式的热力图横纵轴都是模型格子颜色深浅代表两两p值p0.05的格子通常标深色。这种图适合放在附录里正文里大家还是习惯看CD图。3.5 Nemenyi、Bonferroni-Dunn、Holm怎么选Nemenyi不是唯一的后续检验方案实际使用里要按场景选。如果你关心的是“所有模型之间两两比较”比如比较6个完整的独立算法用Nemenyi检验是最常见的也是相对最保守的不容易出戏审稿人挑不出大毛病。如果你有一个明确要主推的基准模型其他模型只是来衬托它那适合用Bonferroni-Dunn检验。它只比较“基准模型vs其他每个模型”而不是所有两两对。临界差公式相同但q值用的是标准正态校正过的值通常比Nemenyi检验的q值略小所以检验力更高一些更容易检测出差异。如果你希望在两两比较场景下获得比Nemenyi更高的统计功效可以试试Holm逐步校正法。它在控制家族错误率的前提下比Nemenyi更灵敏。代价是实现稍微复杂一点用scikit-posthocs库也可以直接跑。我的建议是投稿论文时如果方法不复杂优先Nemenyi因为它最经典、评委最熟悉如果评审质疑你“检验过度保守导致不显著”再换Holm试试并说明你做了多重比较校正的敏感性分析。4. Python实战从DataFrame到显著性结论和CD图4.1 数据准备就是一张性能矩阵进入代码环节。先确认数据格式一个pandas DataFrame行是数据集列是模型值是性能指标。下面代码直接使用前面手算例子里的数据你可以替换成自己的实验结果。import pandas as pd df pd.DataFrame( { A: [0.96, 0.94, 0.92, 0.93, 0.95, 0.93, 0.90, 0.95], B: [0.91, 0.88, 0.95, 0.89, 0.90, 0.89, 0.93, 0.91], C: [0.87, 0.89, 0.86, 0.83, 0.87, 0.90, 0.82, 0.87], D: [0.84, 0.82, 0.83, 0.85, 0.83, 0.83, 0.85, 0.84], } )这里每个模型在8个数据集上各有一个准确率形成了8行4列的完整矩阵。如果你的性能指标是AUC、F1、NDCG之类也没关系只要同一个数据集内各模型的指标是可比较的数值即可。4.2 手写一个Friedman检验函数Friedman检验的原理并不复杂完全可以自己写便于加深理解也方便你在没有现成库的环境下应急使用。import numpy as np from scipy.stats import rankdata, chi2, f def friedman_test(df, descendingTrue): # n: 数据集数, k: 模型数 n, k df.shape # 对每一行(每个数据集)做降序秩次排列 if descending: ranked np.array([rankdata(-row) for row in df.to_numpy()]) else: ranked np.array([rankdata(row) for row in df.to_numpy()]) R ranked.sum(axis0).astype(float) # Friedman chi-square statistic chi2_stat (12.0 * np.sum(R ** 2)) / (n * k * (k 1)) - 3.0 * n * (k 1) p_chi2 1.0 - chi2.cdf(chi2_stat, k - 1) # Iman-Davenport F statistic f_stat (n - 1) * chi2_stat / (n * (k - 1) - chi2_stat) df1, df2 k - 1, (k - 1) * (n - 1) p_f 1.0 - f.cdf(f_stat, df1, df2) # Kendall W effect size w chi2_stat / (n * (k - 1)) return { rank_matrix: ranked, rank_sum: R, avg_rank: R / n, chi2: chi2_stat, chi2_p: p_chi2, f_stat: f_stat, f_p: p_f, kendall_w: w, } res friedman_test(df) print(res)默认传入的是越大越好的指标所以对每行取负再排序准确率场景没问题。如果你用的是越小越好的指标比如错误率或RMSE就把参数改成descendingFalse。这里再提醒一句rankdata对并列值会取平均秩这个细节很关键后面避坑部分会展开。运行这段代码会得到和手算一致的统计量χ²_F17.4F_F≈18.45Kendall W0.725。4.3 scipy一行验证再配合scikit-posthocs做Nemenyi如果用现成库Friedman检验一行就能跑from scipy.stats import friedmanchisquare stat, p friedmanchisquare( df[A], df[B], df[C], df[D] ) print(ffriedman chi2 {stat:.3f}, p {p:.4f})注意scipy的friedmanchisquare要求把每个模型的观测序列作为独立参数传入。它内部实现就是基于秩和检验结果和我前面手算的χ²_F相同。但scipy不会直接给你Iman-Davenport的F统计量和Kendall W所以如果需要报告这些值还是建议用手写版本。接下来做Nemenyi后续检验需要安装scikit-posthocspip install scikit-posthocs然后运行import scikit_posthocs as sp # 默认输入也是 rows datasets, columns models p_matrix sp.posthoc_nemenyi_friedman(df) print(p_matrix)输出是一个4×4的p值矩阵ABCDA1.0000000.38...0.03...0.00...B0.38...1.0000000.51...0.04...C0.03...0.51...1.0000000.72...D0.00...0.04...0.72...1.000000只要某个格子里的p值小于0.05就说明对应两个模型差异显著。你可以用一行代码把显著点位筛出来significant p_matrix 0.05 print(significant)结果会是一个布尔矩阵True的地方就是有显著差异的模型对。和我们手算CD的判断一致A与C、A与D、B与D这三对显著其余不显著。4.4 绘制CD图的两种姿势如果想快速出一张“p值热力图”直接使用scikit-posthocs自带的sign_plotimport matplotlib.pyplot as plt plt.figure(figsize(6, 5)) sp.sign_plot(p_matrix, labelsdf.columns) plt.tight_layout() plt.show()热力图的问题在于它直接展示p值矩阵不是排名位置的直观呈现很多做机器学习的人不太习惯。想要论文里那种经典的CD rank图scikit-posthocs目前并不直接提供需要自己画或者借助其他工具。这里给出一个我常用的简化版CD图函数核心思路是在一条横向排名轴上标出各模型位置然后用粗线连接“差异不显著的模型对”import matplotlib.pyplot as plt def plot_cd(avg_ranks, cd, axNone): items sorted(avg_ranks.items(), keylambda kv: kv[1]) names [it[0] for it in items] ranks [it[1] for it in items] if ax is None: _, ax plt.subplots(figsize(max(6, 0.9 * len(names)), 2.8)) x_min max(0, ranks[0] - cd * 0.2) x_max ranks[-1] cd * 0.2 # 主轴线 ax.axhline(0, colorblack, linewidth1.2) # 每个模型的位置和名称 for xi, name in zip(ranks, names): ax.plot([xi, xi], [0, 0.08], colorblack) ax.text(xi, 0.22, name, hacenter, vabottom, fontsize10) # 标注CD长度 ax.plot([ranks[0], ranks[0] cd], [0.7, 0.7], colorblack, linewidth1.5) ax.text(ranks[0] cd / 2, 0.85, fCD {cd:.2f}, hacenter, fontsize9) # 连接差异不显著的模型 for i in range(len(names)): for j in range(i 1, len(names)): if ranks[j] - ranks[i] cd: ax.plot([ranks[i], ranks[j]], [0.08, 0.08], colorblack, linewidth2.5) ax.spines[[left, right, top]].set_visible(False) ax.set_yticks([]) ax.set_ylim(-0.3, 1.6) ax.set_xlim(x_min, x_max) ax.set_xlabel(average rank) return ax avg_ranks {name: r for name, r in zip(df.columns, res[avg_rank])} cd 1.658 plot_cd(avg_ranks, cd) plt.show()这段代码会画出一条横轴A和B之间、B和C之间、C和D之间各有一根粗线连接表示它们之间的差异没有超过CD而A与C、A与D、B与D之间没有连线说明这些组合存在显著差异。如果你嫌这个图还不够“发表级”可以直接去用R的scmamp包里的plotCD函数输入平均秩和CD就能出标准图效果非常专业。4.5 典型输出怎么解读最后把整个流程串起来。我给一个典型的结果解读示范Friedman检验χ²_F(3)17.40p0.0006拒绝原假设说明四个模型在8个数据集上的性能存在显著差异。Iman-Davenport修正F_F(3,21)18.45p0.001结论一致且更稳健。Kendall W0.725说明模型排序一致性高。Nemenyi后续检验α0.05时临界差CD1.658模型A与C、A与D、B与D之间差异显著其余对比不显著。你在论文里遇到实验结果时不要只说“我的方法平均排名最高”而是要把这样的整套统计结果放上去审稿人才会相信你的排名不是碰运气碰出来的。5. 论文级报告与避坑经验这些细节我踩过5.1 样本量不足时的统计量选择Friedman的原生统计量χ²_F是渐近近似数据集数量n比较小的时候会偏乐观。我见过有人拿6个数据集、8个模型跑Friedman直接报告χ²_F和p值审稿人追问一句“数据集数只有6为什么不用Iman-Davenport修正”只能低头改。建议n小于10时一律用Iman-Davenport的F_F统计量作为主要报告结果它更保守也更容易站得住脚。如果n只有5或者更少老实说Friedman检验的可靠性已经不太够了最好换用其他方法或者明确承认统计功效有限。5.2 并列秩与缺失值容易翻车的两个点我在实际数据里经常遇到并列分数比如两个模型在同一个数据集上准确率恰好都是0.88。Friedman检验要求每个数据集内部有序并列时正确做法是取平均秩也就是scipy的rankdata默认行为。如果手写实现时偷懒直接按出现顺序编号会人为地制造差异结果不可信。所以上面代码里我特意用rankdata而不是np.argsort就是为了避免踩这个坑。另外一个常见问题是矩阵不完整。某个模型因为参数爆炸或者运行超时在某个数据集上没有结果这时候如果把缺失位置填0会严重扭曲秩次。最好要么剔除这个数据集要么补上重跑的分数绝不要留半口。还有一些项目里每个算法在每个数据集上跑多次取均值再进矩阵这是可以的但报告时要说明这一处理。5.3 p值之外还需要报告什么统计显著不等于实际显著。我见过很多新手拿到p0.05就很激动但其实两个模型的平均秩只差了0.15只是数据集多让它“显著”了。这个差异在工程上可能完全没有意义。所以我的习惯是报告p值的同时必须把平均秩和Kendall W一起放上最好再在正文里提一嘴“该差异在实际业务指标上的幅度约为XX”。审稿人看到你有意识地区分统计显著和实际显著印象分会高不少。另外要警惕模型间相关性的影响。如果你的两个模型来自同一套预训练权重、只是微调方式不同它们的结果高度相关这时Nemenyi检验往往偏保守。如果这类对比是你的主要卖点可以考虑在方法部分说明尽管存在相关性本实验仍采用最保守的Nemenyi检验以保证结论可靠。这个说法不仅能堵住质疑还能显得你很懂。5.4 论文里可以直接套用的段落模板我整理了一个中文报告的模板结构如下你在写论文时直接替换实验数据即可本文在8个数据集上比较了A、B、C、D四个模型的准确率。Friedman检验结果表明四个模型的性能存在显著差异χ²_F(3)17.40, p0.0006Iman-Davenport修正后的F(3,21)18.45, p0.001Kendall W0.725。进一步采用Nemenyi后续检验在α0.05水平下临界差CD1.658。两两比较结果表明模型A显著优于模型C与D模型B显著优于模型D而A与B、B与C、C与D之间的差异未达到统计显著水平。平均秩排序为A1.25 B2.00 C3.00 D3.75。如果投英文期刊对应写成The Friedman test revealed significant differences among the four models across the eight datasets (χ²_F(3)17.40, p0.0006; Iman-Davenport F(3,21)18.45, p0.001; Kendalls W0.725). A subsequent Nemenyi post-hoc test (α0.05) yielded a critical difference of CD1.658. Model A significantly outperformed Models C and D, and Model B significantly outperformed Model D, while no statistically significant differences were found between A and B, B and C, or C and D.这个模板我是真在论文里用过的改数字就能用。5.5 什么场景下这套方法不合适Friedman和你Nemenyi这套组合在“多模型、多数据集、单指标”的场景下是最舒服的。但如果你的实验设计复杂很多比如每个模型在每个数据集上不是单个数而是一整个分布的多次重复采样那更适合的做法是先把重复采样聚合成均值或中位数或者直接上混合效应模型。再比如你需要同时考虑准确率和耗时两个指标那属于多目标比较已经不是Friedman能覆盖的范围了。还有一种情况如果你的数据集数很少但模型很多Nemenyi检验的CD会非常大几乎什么都检验不出来。这时候可以考虑Holm逐步校正这类更高功效的方法或者老实承认当前证据不足增加数据集才是正路。我在实际项目中的习惯是把这整套流程封装成一个脚本输入一张性能矩阵自动输出χ²_F、F_F、p值、Kendall W、Nemenyi的p值矩阵和CD图。每次实验对比跑完直接出报告省得每次手动查表、算秩、画图。这套工具已经被我反复用了好几年做技术选型汇报、写论文实验章节都用得上。如果你经常需要做多模型对比建议也尽早把这套流程沉淀成自己的标准工具链。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →