这个差异能复现吗 — 如何测量准确率损失
目标
在同一批测试样本上运行 FP32 原始模型、上一版部署模型、整数候选三个版本,留下预测和输出值,并编写工具 acccmp.py,用来判断准确率的差异是不是可复现的差异。分别测出区间、配对比较、翻转样本和输出差异,把基线设为两个,给出判定。
为什么重要
“准确率下降了 0.3%”本身不是判断依据。样本 400 个时,一个准确率的 95% 区间宽度接近 8 个百分点。把两个模型分别测量再相减,得到的值说不出比这更小的差异。 能让我们说话的是同一批样本。如果两个模型看到的是同样的输入,那么两者都答对的样本和两者都答错的样本不能用于判定,只有只答对一边的样本才是信息。这些分歧样本的个数,才是实际的分母。 基线也不止一个。FP32 原始模型回答量化损失了多少,上一版部署模型回答会不会比现在在线运行的更差。两个答案的方向可能不同,所以两者都要写下来。 而且输出差异和任务指标不同。最大绝对误差很大,准确率也可能不变,反过来也一样。因为离决策边界远的样本,即使输出波动,答案也不会变。 评分器不会相信你写下的数字。它每次都会用不同的随机种子和样本数摆出自己的预测表,真正运行你的工具,并把同样的计算重做一遍来核对。第 1 步会用你留下的随机种子重新生成测试输入,并亲自运行你的模型。
步骤
- 创建并运行 /root/acc/gen_eval.py,生成三个模型以及 /root/acc/preds.json、/root/acc/eval_seed.json。
- 在 /root/acc/acccmp.py 中实现
summary,输出样本数和各次运行的准确率。 - 加入
interval,以 Wilson 得分区间给出一个准确率的 95% 区间。 - 加入
paired,在同一批样本上给出 2x2 分歧个数和配对比较统计量。 - 加入
flips,只按方向统计被翻转的预测。 - 加入
outputs,给出输出值差异(最大绝对误差、平均绝对误差、余弦)。 - 加入
baseline,分别与两个基线比较,并把你这份数据的判定写入 /root/acc/verdict.json。 - 把 /root/acc/accuracy_report.md 写成四节。
参考
- Python 解释器是
/opt/onnx-lab/bin/python。系统自带的python3中既没有 numpy 也没有 onnx。 - 运行约定:
/opt/onnx-lab/bin/python /root/acc/acccmp.py <명령> <preds.json> [인자...](占位符依次为命令名与其余参数)。成功时退出码为 0,文件不存在时为 3,用法错误时为 2。答案以一个 JSON 对象输出到标准输出。 preds.json的形式:{"labels": [정수...], "runs": {"fp32": {"pred": [정수...], "logits": [[실수...], ...]}, "prev": {...}, "int8": {...}}}。logits 不要四舍五入,原样写入。eval_seed.json的形式:{"seed": 정수, "n": 400, "features": 16, "classes": 10, "label_sigma": 1.0}。- 测试输入是
numpy.random.default_rng(seed).standard_normal((n, features)).astype(numpy.float32)。正确答案表是在 FP32 输出(升为 float64 的值)上加numpy.random.default_rng(seed + 1).standard_normal(모양) * label_sigma(占位符为数组形状)后取 argmax 得到的。评分器会按这个规则重新生成,并亲自运行你的模型。 - 模型的输入名称为
x,输入形状为[None, 16],输出类别共 10 个。prev.onnx用quantize_dynamic生成,int8.onnx用quantize_static(quant_format=QDQ)生成。三个文件名为/root/acc/fp32.onnx、/root/acc/prev.onnx、/root/acc/int8.onnx。 summary的响应:{"n": 정수, "runs": {이름: {"correct": 정수, "accuracy": 실수}}}。interval的响应:{"run", "n", "correct", "accuracy", "center", "low", "high", "half_width"}。以 z = 1.96 计算 Wilson 得分区间。中心不是 p,而是(p + z^2/(2n)) / (1 + z^2/n),半宽是z/(1 + z^2/n) * sqrt(p(1-p)/n + z^2/(4n^2))。paired <a> <b>的响应:{"a","b","n","both_correct","only_a","only_b","both_wrong","accuracy_a","accuracy_b","delta","discordant","statistic","significant"}。delta 是 b 减去 a 所得的值。discordant 是 only_a 与 only_b 之和。statistic 是这个实验固定的尺度(|only_a - only_b| - 1)^2 / discordant,discordant 为 0 时为 0.0。significant 表示 statistic 是否大于 3.841459。flips <a> <b>的响应:{"a","b","changed","to_wrong","to_right","changed_both_wrong","top_flip"}。changed 是预测不同的样本数,to_wrong 是 a 答对而 b 答错的个数,to_right 是相反的个数,changed_both_wrong 是两者都答错但答案变了的个数。top_flip 是翻转最多的[a 예측, b 예측, 건수](占位符依次为 a 的预测、b 的预测与个数),并列时取 a 预测、b 预测较小的一项,没有翻转时为 null。outputs <a> <b>的响应:{"a","b","max_abs_error","mean_abs_error","cos_min","cos_mean","argmax_changed"}。平均绝对误差是所有元素的平均值,余弦对每个样本分别求出,给出最小值和平均值。分母为 0 时,该样本的余弦取 1.0。baseline <run>的响应:{"run","baselines","vs","worst"}。baselines 是除 run 之外的其余名称排序后的结果,vs 的每一项是{"delta","only_a","only_b","discordant","statistic","significant"},把该基线当作 a、把 run 当作 b 所得的值。worst 是 delta 最小的基线的{"baseline","delta"}。verdict.json的形式:{"candidate","n","accuracy","delta_vs_fp32","delta_vs_prev","half_width","discordant_vs_fp32","statistic_vs_fp32","significant_vs_fp32","decision","reason"}。half_width 是候选的 Wilson 半宽。decision 遵循这个实验固定的规则:significant_vs_fp32 为真且 delta_vs_fp32 小于 0 时为hold,否则为ship。accuracy_report.md是## 무엇을 견주었나、## 같은 표본에서 본 차이、## 이 차이가 재현되는가、## 판정과 한계四节(四个标题为韩文,依次意为“比较了什么”“在同一批样本上看到的差异”“这个差异能否复现”“判定与局限”)。- 官方文档:ONNX Runtime 量化 · ONNX Concepts · QuantizeLinear
- 常见错误:把分别测得的两个准确率相减后报告;因为区间重叠就说没有差异;用最大绝对误差代替准确率;基线只设 FP32 一个。
- 这个实验不测时间和吞吐量。这台 Mac 是模拟环境,延迟会抖动到两倍。
在同一批样本上运行三个版本并留下预测
创建并运行 /root/acc/gen_eval.py,在 /root/acc 下生成 fp32.onnx、prev.onnx、int8.onnx 以及 /root/acc/preds.json、/root/acc/eval_seed.json。测试输入和正确答案表完全遵循参考部分的随机种子规则。
模型用 onnx.helper 把 MatMul + Add + Relu 连起来搭成。prev.onnx 用 quantize_dynamic,int8.onnx 给 quantize_static 传入 CalibrationDataReader 来生成。正确答案表由 FP32 输出生成,但要加上噪声,是因为准确率如果是 1.0,就看不出任何差异。logits 用 float() 原样写入,不要四舍五入。
把样本数和准确率汇总到一处
在 /root/acc/acccmp.py 中实现 summary <preds.json>,以 JSON 输出样本数以及各次运行的答对个数和准确率。
不要一上来就算总数,而是先为每个样本留下是否答对的真假列表。后面步骤中的配对比较和翻转统计,全都建立在这个列表之上。运行名称要排序后放入。
给一个准确率加上区间
加入 interval <preds.json> <run>(占位符依次为预测文件与运行名称),给该次运行的准确率加上 95% Wilson 得分区间并输出。包含 run、n、correct、accuracy、center、low、high、half_width。
z 固定为 1.96。Wilson 区间的中心不是样本比例 p,而是向 0.5 略微拉近的值。所以 low 和 high 不是以 p 为中点。即使准确率是 0 或 1,宽度也不会变成 0,这正是使用这个区间的原因。
在同一批样本上做配对比较
加入 paired <preds.json> <a> <b>(占位符依次为预测文件与两个运行名称),输出 2x2 表(both_correct、only_a、only_b、both_wrong)以及 delta、discordant、statistic、significant。
两者都答对的样本和两者都答错的样本,区分不了两个模型。判定的分母不是 n,而是分歧样本数。统计量直接使用参考部分的公式,分歧样本数为 0 时取 0.0。
只按方向统计被翻转的预测
加入 flips <preds.json> <a> <b>(占位符依次为预测文件与两个运行名称),输出 changed、to_wrong、to_right、changed_both_wrong、top_flip。
changed 是其余三项之和。如果这个恒等式不成立,就是某处数了两次或漏掉了。错误答案变成另一个错误答案的个数对准确率没有影响,但在用户眼里就是答案变了。
把输出差异和任务指标分开测量
加入 outputs <preds.json> <a> <b>(占位符依次为预测文件与两个运行名称),输出 max_abs_error、mean_abs_error、cos_min、cos_mean、argmax_changed。
输出动得多,并不意味着决策会变。离决策边界远的样本,即使输出波动,答案也不变。把这两个值放进同一个响应里并排看,这种关系就一目了然了。
设两个基线来判定
加入 baseline <preds.json> <run>(占位符依次为预测文件与运行名称),让其余各次运行分别作为基线来比较,并把你这份数据的判定按参考部分的键写入 /root/acc/verdict.json。
原始模型和上一版部署模型的答案方向可能不同。worst 是 delta 最小的基线,并列时取名称靠前的一项。判定规则在参考部分已经固定,原样照搬即可,reason 中用一行写下为什么会这样。
写到读的人能做出判断
把 /root/acc/accuracy_report.md 写成 ## 무엇을 견주었나、## 같은 표본에서 본 차이、## 이 차이가 재현되는가、## 판정과 한계 四节(四个标题为韩文,依次意为“比较了什么”“在同一批样本上看到的差异”“这个差异能否复现”“判定与局限”)。样本数、分歧样本数和判定,要以数字和字词写进正文。
写数字时,要同时写下这个数字是从哪里来的。局限一节要写这个判定没有说明的事——这个实验没有测时间,也没有测内存,而且只在一批测试样本上看过。