时钟图片分类器诊断报告

一个几乎完美读取合成钟面的EfficientNet-B3在照片上出错的原因——以及我测试的四个假设来找出原因。

时钟图片分类器诊断报告
梯形图转SCL | AI模型价格对比 | AI工具导航 | ONNX模型库 | Vibe Coding教程 | PLC在线仿真器 | Tripo 3D | Meshy AI | ElevenLabs | KlingAI | ArtSpace | Phot.AI | InVideo

我训练了一个分类器来读取模拟时钟。在基准测试中,它的得分是99.82%。在真实时钟的照片上,它的得分是2.2%,而随机概率是0.7%。

这篇博文是诊断报告。它围绕四个差距假设组织——旋转、校准、增强真实性和训练数据——其中三个被我证伪了。在这个过程中,基准数字本身被证明比看起来更"软":一个标签排序陷阱默默损失了33个百分点,一个调整大小-滤波器依赖制造了虚假的失败模式并逆转了哪个检查点获胜,以及数据集中的两个渲染缺陷。

下面的每个数字都是测量出来的。当结果在统计上较弱时,我会用检验统计量说明,包括那个有效的假设。

研究发现的摘要:

None
None

1. 任务、数据、模型、指标

1.1 标签空间

一个12小时表盘量化为5分钟增量,给出144个类别。类别i是5·i分钟过12点,所以标签空间是一个离散圆:类别143(11:55)和类别0(12:00)是相邻的,但平面softmax不知道。这种不匹配在后面反复出现。

数据集是gpiosenka/time-image-datasetclassification(CC0):14,400个合成224×224 RGB渲染,每类恰好100个,按80/10/10分割为11,520 / 1,440 / 1,440。它捆绑了一个预训练的EfficientNet-B3检查点time-99.68.h5

1.2 模型

本帖中每次运行都相同:

EfficientNetB3 (ImageNet初始化,完全解冻)
  → GlobalMaxPooling2D
  → BatchNormalization
  → Dense(256, relu)
  → Dropout(0.3)
  → Dense(144, softmax)

11,220,159个参数。优化器Adamax @ 1e-3,损失为分类交叉熵,批大小32。ReduceLROnPlateau(factor=0.5, patience=2)监控val_lossEarlyStopping(monitor="val_accuracy", patience=12, restore_best_weights=True)在120个epoch预算内运行。运行在第32到42个epoch之间提前停止。

两个非显而易见的实现细节,都浪费了我的时间:

输入是150×150,不是224×224。 图像以224存储,每个都在加载时下采样。第2b节完全是关于这个看似无害的步骤做了什么。

预处理在模型内部。 Rescaling(1/255)Normalization是层,所以模型接受原始浮点数[0, 255]。习惯性地自己除以255会喂给它[0, 0.004],准确率会默默崩溃。

增强是keras.Sequential应用于原始值训练流:

augment = keras.Sequential([
    keras.layers.RandomRotation(rotation_factor, fill_mode="nearest"),
    keras.layers.RandomZoom(0.05, fill_mode="nearest"),
    keras.layers.RandomTranslation(0.05, 0.05, fill_mode="nearest"),
], name="augment")

rotation_factor是2π的分数,所以0.15 = ±54°。这是第4节中检查点变化的唯一旋钮。

硬件:每次运行一个A100-SXM4–40GB或H100(Slurm,dgxa100分区,10个CPU,64 GB)。完整训练运行约15分钟;四模型旋转扫描是1小时11分钟,真实数据实验是2小时16分钟。

1.3 指标

Top-1 / top-5是通常的指标。但144路argmax隐藏了错误的结构,因为差一个类别和差六个小时都是"错误的"。所以主要的错误指标是循环分钟差:将每个类别映射到720分钟表盘上的分钟数,取较短的弧。

def label_to_minutes(label):          # "10-35" -> 635
    h, m = label.split("-")
    return (int(h) % 12) * 60 + int(m)

def circular_error(a, b, period=720):
    d = abs(a - b) % period
    return min(d, period - d)         # 较短的路径绕表盘

对于校准,我使用15个等宽箱的最大softmax置信度上的ECE

ECE = Σ_b (n_b / N) · |acc(b) − conf(b)|,

其中MCE是该差距的最大值,加上NLL和Brier。温度缩放通过最小化验证集上的NLL来拟合单个标量Tsoftmax(log p / T),重新归一化),选择性预测报告准确率与覆盖率随置信度阈值上升的变化。

对于注意力,我在top_activation处使用Grad-CAM,并通过焦点总结每个热图:最热10%像素中CAM质量的分数。集中的热图意味着模型锁定在一个区域;分散的热图意味着它从未找到指针。

2. 失败分析,第一部分:基准比看起来更"软"

在诊断领域差距之前,值得确认源域数字的含义。三件事表明相反。

2.1 标签排序陷阱(33分的静默bug)

Keras分类器的输出单元i意味着训练期间索引i处的类别。这个数据集提供了两种构建该列表的合理方式,它们不一致:

# (A) clocks.csv中下划线标签的字母顺序——训练方式
order_A = [l.replace("_", "-") for l in sorted(clocks["labels"].unique())]
#   ['10-00', '10-05', ..., '1-00', ..., '9-55']

# (B) 连字符目录名的字母顺序——直观选择
order_B = sorted(d.name for d in (DATA_DIR / "train").iterdir() if d.is_dir())
#   ['1-00', '1-05', ..., '10-00', ..., '9-55']

原因是ASCII:-是45,_是95,所以连字符排在数字之前,下划线排在数字之后。整小时偏移。

None

没有崩溃。image_dataset_from_directory接受B没有抱怨,训练收敛,损失曲线看起来健康。你因小时偏移失去了三分之一的准确率,唯一的线索是可疑的"每个错误都是60分钟的精确倍数"模式——这正是为什么循环误差分箱,而不是top-1,是捕捉它的指标。

CSV的class index列看起来像权威的第三个选项。它按完全相同的目录顺序分配,所以它是同一陷阱的不同帽子。

修复: 显式传递class_names=,从labels列派生。

2.2 调整大小滤波器逆转了模型排名

这是我没预料到的。

224 → 150的下采样发生在每个图像上,管道中没有任何东西固定插值方法。我将双线性换成最近邻——相同的权重、相同的文件、相同的标签——并重新运行完整的1,440图像测试集。

因为两个滤波器都在相同图像上评估,正确的测试是对不一致对的McNemar精确检验,而不是双样本比例检验:

None

仔细阅读发布模型的行:0 / 7。最近邻严格更好——它获得双线性获得的每个图像,加上另外七个。其中六个是数据集自身文档描述为模型标志弱点的11-10 → 7:55簇(p ≈ 0.9)。在最近邻下,该失败模式不存在。它从来不是权重的属性。

原因是平凡的。发布的检查点是用Keras 2.8的ImageDataGenerator训练的,其flow_from_dataframe默认为interpolation="nearest"。我用image_dataset_from_directory评估,它默认为双线性。我一直在它从未训练过的分布上评分,并将差异归因于模型。时钟指针是一像素宽、高频、接近混叠的结构——正是重采样内核破坏的东西。

而且它是对称的:我自己通过双线性路径训练的模型,在最近邻下退化1.7个百分点。每个模型在它训练时使用的滤波器下获胜,它们之间的排名在我们都没写下的选择上翻转。

这排除了什么,没有排除什么。 它没有推翻我自己的双线性管道内的检查点比较——该比较内部一致。它确实意味着任何跨管道比较部分是管道的测量,并且我一直在引用的0.9938-vs-0.9979差距被夸大了:在其自己的预处理下,发布的模型是0.9986。

2.3 数据集中的两个渲染缺陷

Grad-CAM通常是确认模型看着正确东西的地方。我期望在错误上看到分散的注意力,结果相反:在模型据称失败的图像上,看到紧凑、自信的热图。

所以我重新计算了有符号偏移,而不是循环距离。在我自己的检查点上,所有13个非空白的全局错误都落在恰好±195分钟——3小时15分钟,没有模糊,没有接近。11:10 → 7:55恰好是11:10 − 3:15

一个精确、重复、自信的偏移是去看的信号,而不是结论——去看发现了两个不同的原因:

  • 偏移的指针。 valid/8-50/36.jpg标记为8:50,其指针绘制在~12:05,恰好是8:50 + 3:15。模型正确读取图像;图像与自己的文件夹名称矛盾。
  • 空白表盘。 没有渲染指针——注意力确实变得分散且置信度降至~0.07的情况。

两者都聚集在特定的文件索引036385172)上,跨不相关类别。给定索引的144个文件中只有1-4个受影响,但在那些中,100%是精确的±195分钟偏移,0%是任何其他错误类型。模型混淆会在幅度上分散;渲染器bug会这样做。

None

扣除两个缺陷后,所有14,400个图像上的真实准确率约为99.97%,而不是99.82%。阅读失败单独使数字上升

None

3. Sim2Real评估

到目前为止的每个数字都是在合成渲染上:直立、居中、光线均匀、正面,并设置为精确的5分钟刻度。目标分布同时违反了所有五个。

协议。 92张来自kongaskristjan/real-clocks的CC0照片,在文件名中标记到分钟。因为真实时间不在5分钟网格上,真值通过两种方式评分:真实时间四舍五入到最近的类别用于精确匹配准确率,未四舍五入的时间用于循环误差,所以四舍五入从不美化模型。

def nearest_5min_label(hour, minute):
    r = int(round(minute / 5.0)) * 5     # 将舍入进位到小时
    hour = (hour + r // 60) % 12
    return f"{hour if hour else 12}-{r % 60:02d}"

结果。

None

144类的随机概率是0.7%。2/92的95% Wilson区间是[0.6%, 7.6%]——所以诚实的说法是"介于随机和略高于随机之间"。这不是退化;这是一个不同的领域。

None

排除单一系统性错误。 诱人的假设是一个干净的偏移——指针交换,或±195分钟缺陷重复。循环误差分箱说没有:

None

错误分散在每个幅度上。~180°"表盘对面"带存在但只有~15%的案例——远非主导。那是分布外混淆,不是可修复的bug。

从质量上看,六个最佳预测是干净、正面、光线充足的面孔——最接近合成流形的照片。六个最差的是装饰表盘、奇怪构图和反光玻璃。

4. 假设1——是旋转。(证伪)

直接的故事:真实照片是轴外拍摄的,合成渲染是完美直立的,所以模型对旋转脆弱。扩大增强并缩小差距。

设置。 四个检查点,除了--rotation-factor ∈ {0.03, 0.06, 0.10, 0.15}(±10.8° … ±54°)外都相同,都是种子0。每个在144个测试图像(每类一个)上从−90°到+90°以15°步长扫描测试时旋转,使用fill_mode="nearest"匹配训练而不是引入黑色角落混淆测量。

结果——源域部分工作得很好。

None

鲁棒平台的边缘跟踪字面训练RandomRotation范围。扩大它是免费的:±54°同时是最旋转鲁棒最准确的直立检查点。没有权衡,所以它成为默认。

None

悬崖有尖锐的结构。在平台外,发布的模型(无旋转增强)在恰好−90°、0°、+90°得分0.993,在每个角度之间得分0.000——它学会了表盘的90°对称性,仅此而已。该死区的置信度保持在0.44–0.88,Grad-CAM保持紧密锁定在旋转的指针上。模型没有丢失指针;它找到它们并自信地误读它们。定位完整,解码不完整。

None

结果——目标域部分是完全无效的。

None

Fisher精确检验5/92 vs 2/92:p = 0.44。旋转鲁棒性五倍扩大没有买到可测量的东西,点估计朝错误方向移动了。

这排除了什么。 旋转作为差距的主导轴。它没有排除旋转作为贡献因素——n = 92时,80%功效下可检测的最小改进约为+10分,所以任何更小的在这里不可见。正确的说法是"没有大效应",而不是"没有效应"。

5. 假设2——路由自信案例,推迟其余。(证伪)

如果你不能让模型准确,让它知道何时出错:一个选择性预测系统,在置信度阈值以上回答,其余推迟给人类。该计划需要在转移下保持校准。

设置。 可靠性图和ECE(15个箱),在合成valid上拟合温度缩放,以及准确率与覆盖率的扫描,在合成测试和真实照片上运行。

None

分布内校准是教科书式的:ECE 0.002,拟合温度1.07,基本不需要校正。

分布外它反转。仅合成模型在真实照片上的p ≥ 0.8预测准确率为0%。不仅仅是校准不良——在你会实际门控的区域反相关。在合成valid上拟合的温度缩放没有帮助,因为这是领域转移,而不是缩放错误:一个标量无法修复已经反转的排名。

有趣的是,在真实数据上微调(第7节)修复了排名而没有修复缩放:该模型在绝对意义上极度过度自信(ECE 0.48),但其置信度前~12%是86%正确的。对于选择性预测,排名是你需要的——但注意57张照片的12%约为7张图像,所以将该单元视为方向性的。

None
这排除了什么。 部署基于该模型置信度的人类回退系统。你会门控的信号在你会最信任它的地方指向错误方向。

6. 假设3——让合成数据看起来真实。(证伪)

设置。 在旋转增强之上叠加真实性管道——RandomTranslation(0.12)RandomZoom(-0.2..+0.3)RandomShear(0.15)RandomPerspective(0.35)RandomBrightness(0.3)RandomContrast(0.3)、高斯模糊。这是诱人的举动:无需数据收集,均匀扩展到所有144类。

结果。 真实top-1 7.0% → 8.8%(4/57 → 5/57;Fisher p = 1.00),代价是合成下降~0.8个百分点(0.9972 → 0.9889)。

一张图像改变了。照片真实感不是仿射和光度抖动的合成——增强的渲染仍然是线图,差距是生成性的(表盘艺术、材料、3D透视、运动模糊、镜面眩光),而不是仿射的。

7. 假设4——将真实照片放入训练集。(弱支持)

设置。 第二个标记源将真实池增加到195张照片,按小时桶分层分割138训练/57测试,种子0——每类分层在每类约1.4张照片时是不可能的。57张测试照片从未被训练过;清单构建器和混合代码都过滤split == "train"

真实照片被连接到合成流中,过采样25×,然后从强合成检查点微调:

train_stream = cm.make_dataset("train", batch_size, shuffle=True).unbatch()
if args.real_mix:
    real_stream = (cm.make_real_dataset("train", batch_size, shuffle=True)
                   .unbatch().repeat(args.real_oversample))     # 25x
    train_stream = train_stream.concatenate(real_stream)
train_ds = (train_stream.shuffle(4096, reshuffle_each_iteration=True)
            .batch(batch_size).prefetch(2))

结果。

None

真实top-1几乎翻了三倍,中位循环误差减半,合成准确率保持在0.35个百分点内,从合成检查点初始化优于从ImageNet训练。Grad-CAM定性一致:热图焦点从0.262 ± 0.10上升到0.357 ± 0.14,在72%的照片上更紧密——从在表圈和背景上游荡到锁定在轮毂上。

现在是大多数分析会省略的部分。 这些是相同57张图像上的配对预测,所以McNemar精确检验适用:

baseline 4/57 = 0.070      real-mix 11/57 = 0.193
discordant: baseline-only-right 2, real-mix-only-right 9
McNemar exact p = 0.065

p = 0.065。 标题"3×改进"未达到α = 0.05。方向在四个独立信号上一致——top-1、中位误差、注意力焦点、置信度排名——9对2的不一致对具有启发性。但在57张图像的测试集上,它未确立,我不会写得好像它确立了。

注意与第2b节的对比:我偶然发现的调整大小-滤波器发现,统计上是可靠的(p = 0.016和4 × 10⁻⁷)。我最希望为真的发现是最弱的。这个顺序不是巧合——当效应量大但n是57时就会发生这种情况。

8. 评估集与训练集有相同的疾病

在使用自信的模型-标签不一致来发现别人数据集中的缺陷(2c)后,我对自己的数据集运行了相同的程序。

92张kongaskristjan照片,在文件名中标记,检查通过——那里的标记图像是真实困难照片上的真实模型错误。

来自第二个源的103张照片不是。它的标签来自按行索引的label.csv,抽查标记图像发现了明显的错误:一个标记为12:03的显示~6:05,一个标记为2:20的显示~4:00。错误不是一致的偏移,所以没有行错位可撤销——只是噪声。

我的57张照片测试集约58%来自该源。所以19.3%被一个我无法确定的方向污染:测试集中的标签噪声通常降低测量准确率,但如果微调从训练半部分学到了相同的噪声,它同样可能被美化。

结合p = 0.065,第7节的诚实总结是:方向上支持,定量上未确立。 干净的版本是在92张可信照片上重新运行,它是列表上的下一件事。

9. 数字能支持什么,不能支持什么

小评估集是第4-7节所有内容的约束条件,所以值得明确说明分辨率限制。在80%功效和α = 0.05下:

None

关键结果的Wilson 95%区间:

None

这些区间宽7-20个百分点。任何价值低于~10个百分点的干预在这个样本量下不可见——这意味着我的两个"证伪"假设恰恰是作为大效应被证伪的,而支持的假设正好处于可检测性的边缘。源域结果(n = 1,440和n = 14,400)没有这个问题,这正是为什么调整大小-滤波器发现是无懈可击的,而sim2real发现不是。

我在两个方向上都受数据约束:没有足够的真实照片来训练,也没有足够的来测量。

10. 要点

99.8%从来不是谎言;它是一个狭窄的声明。 基准说"这个模型读取这些渲染。"我听到"这个模型读取时钟。"每个后续惊讶都来自那个替换。

在排名检查点之前固定你的预处理。 一个未记录的调整大小滤波器在一个模型上价值0.5个百分点,在另一个模型上价值1.7个百分点,方向相反,它制造了一个网络中任何权重都不负责的标志性失败模式。将评估预处理与训练预处理匹配,并在README中说明。

错误分布中的结构优于聚合准确率。 "每个错误都是60分钟的精确倍数"识别了标签排序bug;"每个错误恰好是±195分钟"识别了渲染器bug。两者在top-1数字中都不可见,第二个在混淆矩阵中也不可见——它需要有符号循环偏移。

Grad-CAM区分失败模式,而不仅仅是正确性。 紧凑注意力+错误答案=模型读取了与你的标签矛盾的真实结构。分散注意力+低置信度=那里什么都没有。这两个需要相反的响应,热图将它们区分开。

校准也是领域偏移的。 分布内ECE 0.002和分布外p ≥ 0.8桶0%准确率是同一个模型。温度缩放无法修复反转的排名,基于分布内校准构建的选择性预测系统在它旨在捕捉的输入上失败最严重。

在你最喜欢的结果上报告检验统计量,而不仅仅是最不喜欢的。 对一个你正在证伪的假设要求严格,对一个有效的假设给予通过,这很容易。我最好的结果是p = 0.065,在部分标记错误的57张图像分割上。说出来没有任何代价,这是发现与声明之间的区别。

当你受数据约束时,说明哪个方向。 195张照片跨144类中的99类,一个源有噪声标签,以及一个无法解析低于15个百分点的评估集。没有架构搜索能修复这个。下一个杠杆是更多照片,干净标记——或者一个渲染器足够好,输出停止看起来像图表。

我墙上的时钟仍然赢。但差距现在被分解了,四个明显解释中的三个被带有数字的证据消除了,剩下的一个有价格标签:标记的照片,数百张,不是几十张。


原文链接:From 99.8% to 2.2%: Diagnosing the Sim2Real Gap in a 144-Class Clock Classifier

汇智网翻译整理,转载请标明出处