用模型给数据挑错:从图像分类到目标检测的一次实验
先交代一下背景。我手上有一批约12000张的鸟类图像数据,此前是外包标注的,抽查的时候发现标注质量相当不稳定,同一物种能出现五六种不同写法,位置框更是随心所欲,有的框只圈住鸟头,有的连半棵树都框进去了。数据不干净,后面无论是训练分类模型还是做检测模型,都会被这些噪音标签牵着鼻子走。与其一怒之下全量重标(成本实在太高),不如换个思路——既然靠人检查又慢又容易遗漏,那就先让模型给我挑一遍错,我再针对性地人工复核。
这就是这次实验的初衷:用现成的模型给数据挑错,从图像分类模型粗筛类别错标,再到目标检测模型细抠边界框问题,最后用多模态模型来兜底处理边界情况。整个过程跑下来,总共挑出了1400多张标注有问题的图片,最终确认的错标率在11%以上。这篇文章就是完整复盘,包含每一步的思路、可复现的代码做法、参数选择和踩过的坑,希望能给同样被脏数据折磨的同行一点参考。
1. 整体方案设计:为什么用模型来挑错
先说一个朴素的观点:数据清洗本身就是一个模型推理问题。类别标错了,本质上是图像内容与标签语义不匹配;框标错了,本质上是定位与目标不匹配。那么找一个已经具备语义理解或定位能力的模型来做这个匹配判断,是完全成立的路线。
我设计的三阶段方案是这样的:
- 图像分类模型粗筛:先解决“图上是什么”的问题。用预训练ResNet50提取特征,再通过小样本最近邻找出与标签语义不符的图片,一次性捞出大概率标错类别的样本。
- 目标检测模型细筛:再解决“目标在哪”的问题。用YOLOv5s做检测,对比人工标注框与模型预测框的重合度(IoU),把那些框错位、漏检、多框误检的样本找出来。
- 多模态模型兜底:最后解决“边界情况”的问题。把前两轮筛选出的边界样本切成局部图,用图文匹配模型判断框内内容与文字描述是否一致,揪出分类和检测都查不出来的隐性错标。
为什么是这三层而不是直接上一步到位的工具?因为不同错误的“粒度”不同。类别错误是语义级的,检测模型不一定感知得到——模型可能确实检测到了鸟,但标签上的物种名写错了;框错误是位置级的,分类模型根本无从判断——分类器只看全局特征,不会告诉你框有没有歪。所以必须分阶段,各管一摊。
另外一个很现实的原因是效率。多模态模型最准但最慢,全量跑12000张图可能要好几天;检测模型中等速度,也得按小时计。而ResNet50特征提取快得多。所以整体策略是“先用快的模型排出大部分错,再用慢的模型处理剩余边界”,算力花在刀刃上。
我建议任何拿到脏数据的人都先按这个思路设计自己的流程,不要一上来就想着全量人工复核,也不要迷信单一模型能包打天下——不同模型的能力边界不同,组合使用才能覆盖更多错误类型。
2. 第一阶段:用ResNet50小样本分类挑出类别错标
2.1 为什么选ResNet50而不是更大更新的模型
先说选型问题。这个阶段的任务是提取图像特征做相似度判断,需要的是稳定、轻量、特征表达能力足够强的骨干网络。我实测过ViT和ResNet50,最终选定了ResNet50,原因是:
- 小样本场景下CNN比Transformer更稳。ViT的特征维度高(768或1024维),在数据量不足时特征分布稀疏,算出来的相似度噪声很大。ResNet50的2048维特征相对更平滑,用余弦相似度做检索时区分度更好。
- 推理速度差距明显。同样跑完12000张图,ResNet50在单卡V100上大约十来分钟,ViT要慢一倍以上。纯清洗任务完全不值得上大模型。
- 不需要调整分类头。这里有一个新手容易踩的坑:直接用预训练好的ResNet50做特征提取时,不要去动最后一层分类头。我们要的是倒数第二层那个2048维的特征向量,而不是1000类的分类输出。如果自作聪明把分类头改了,反而破坏了预训练学到的语义空间。
有人可能问:“那用热词里提到的ViT评估时要调整分类头吗?”我的结论是:如果你的任务是微调训练,那分类头必须替换成自己的类别数;可如果只是拿预训练模型当特征提取器用,分类头碰都不要碰。我第一版实验就是踩了这个坑,把ViT的分类头改了之后提取特征,效果反而更差,后来才发现问题出在特征空间被污染了。
2.2 小样本(1-shot/5-shot)判错的完整步骤
这个阶段的设定其实是一个标准的小样本图像分类问题。我手里有12,410张图,对应约30种鸟类,每个类别有少量确认过标签正确的“种子图”——这些种子图是我从原始数据里人工挑出来的,保证干净。流程分四步:
# 伪代码:ResNet50特征提取 import torch import torchvision.models as models from torchvision import transforms model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) model.fc = torch.nn.Identity() # 去掉分类头,直接输出2048维特征 model.eval().cuda() # 预处理 transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # 对所有图片提取特征并保存为npy文件 # 然后对每类种子特征取平均作为类中心,计算待检图片与类中心的余弦相似度具体步骤如下:
- 准备种子集:每个类别选取1张(1-shot)或5张(5-shot)人工确认无错的图片,提取特征并计算该类别的特征中心(5-shot时取平均更稳)。
- 提取全量特征:对全部12,410张图片提取2048维特征向量,按图片ID保存到一个npy矩阵里,方便后续批量运算。
- 计算相似度:对每张待检图片,计算它与所有类别中心的余弦相似度,取最高分数及其对应类别作为“模型认为的正确标签”。
- 判定错标:若模型预测标签与原始标注标签不一致,或最高相似度低于阈值(我实验中取0.75),则标记为“疑似错标”,进入人工复核队列。
我试过1-shot和5-shot两种设定,差异很明显:1-shot时类别中心由单张图决定,遇到拍摄角度刁钻或者遮挡严重的样本,相似度波动很大;5-shot让类中心更稳,误判率明显下降,最终确定5-shot方案作为正式流程。这也符合小样本学习的直觉——支持集越大,类别分布估计越可靠。
2.3 滑窗处理长图与遮挡图
实验过程中我发现了一个分类模型的结构性盲区:很多鸟类摄影图是长条形的,主体占比很小,直接Resize到224x224后鸟可能只占了画面的一小块区域,特征被大量背景稀释了。还有一部分图片主体被树枝遮挡,全局特征看不出鸟的完整形态。
解决办法是对长图做滑窗处理,这也是热词里提到的“滑动窗口滤波”思路在图像分类场景下的变体。具体做法是:
- 把224x224的窗口按50%重叠率(步长112)在图上滑动,每个窗口都提取特征并单独判断类别;
- 汇总所有窗口的预测结果,取置信度最高的窗口作为最终判断依据;
- 如果最高分的窗口类别与整图标注的类别一致,则判定通过;否则再结合整图特征综合判定。
注意不要忽略去重问题。滑窗会产生大量重叠窗口,同一只鸟可能同时出现在多个窗口中。我一开始没做去重,导致同一张图被标记了三四次“疑似错标”,人工复核的工作量翻倍。后来加了一个简单的逻辑:同一张图只保留最高置信度的那次判定,其余窗口结果忽略,只作为投票参考。
这一步实际帮我捞出了不少问题图片。很多长图边缘标的是远景树丛里的鸟,全局特征根本看不出来,但滑窗切到鸟所在区域后,模型就能给出比较确定的类别判断了。
2.4 分类筛选结果与阈值分析
第一轮跑完,共标记了527张疑似错标图片。我抽了其中120张做人工复核,确认有96张确实是类别标错了,粗略估了一下准确率在80%左右。于是我把全部527张都提交复核,最终确认452张存在类别错误,另外75张是“可争议边界”——比如亚种分类,人和模型都很难达成一致的那种。
这里有个值得说的经验:不要只把相似度低于阈值的图片设为疑似。我最初设了相似度阈值0.75,跑完之后发现有些图相似度挺高(比如0.88),但最高相似度的类别和原标签不同,这类图其实是“置信的错误”而不是“模糊的错误”,更值得关注。所以最终判定条件有两类:一是最高相似度对应的类别与原标签不一致;二是最高相似度低于0.75且原标签类别不是最高相似类别。两条都满足才标记为疑似,逻辑上更严谨。
3. 第二阶段:用YOLOv5s检测模型挑出框错误
3.1 从分类到检测能解决什么问题
分类阶段解决的是“整张图内容与标签不符”的问题,但坐标系里还有另一种错误分类模型根本管不了:检测框错位。一个框应该刚好包住整只鸟,结果标注员可能只框了鸟头,或者把树枝一起包了进去。这种错在分类模型眼里完全无感,因为图片内容没变,标签也没变,变的只是“定位的范围”。
目标检测模型天然适合做这件事,因为检测的产出本身就是“框+类别+置信度”。拿模型输出和人工标注做几何对比,就能精准定位问题所在。
3.2 数据准备与YOLOv5s微调
检测阶段的模型选型我用了YOLOv5s,没上更大的YOLOv5l或YOLOv8系列。原因有三个:一是v5s参数量小,推理速度快,能在较低的GPU配置上批量跑;二是yolov5s在中小目标上的表现均衡,而鸟类数据很多是小目标;三是轻量化版本处理大规模批数据时对显存压力小,我可以一次放更大的batch size。
微调用的数据不能太脏,否则模型会学错。我从前两轮已经确认干净的图片里挑出800张做微调训练集,标注是人工重新校准过的。训练参数可以参考以下配置:
- 输入尺寸:640x640
- batch size:16
- epoch:80轮(早停在30轮左右)
- 学习率:0.01(SGD,配合余弦退火衰减)
- label smoothing:默认0.1,但我实际调成了0.05,后面细说
训练大约花了2小时,收敛后的mAP@0.5在96%以上,足够用来做错标筛查了。
3.3 三类检测错误的判定逻辑
拿到模型预测框后,我按以下三类问题分别筛查:
第一类:框错位(IoU过低)。将预测框与对应的人工标注框做IoU计算,低于0.3的判定为疑似错位。注意:同一个GT框可能与多个预测框重叠,这里要取最大IoU,不要取平均值——取平均值会把一个完全重合的框和一个完全不重合的框平均成一个“看起来还行”的数字,掩盖真实问题。
第二类:漏检(有人工标注但在预测中缺失)。一张图中如果人工标注框的数量大于预测框命中数(预测框与GT框IoU>0.5的才算命中),说明模型没有找到这个目标。这种情况大概率是因为目标太小、被遮挡,或是最初标注框就画在了错误的位置上——模型没在那里找到鸟,这本身就是标注问题的信号。
第三类:误检(模型预测中有但人工标注缺失)。反向逻辑,模型自信地检测出一个目标但人工没有标注。这种情况常见于标注员漏标,但也可能是模型把背景误判为鸟。不管哪种,都需要人工看一遍确认。
实际执行时我写了一个脚本,输出CSV表格,每列对应图片ID、GT框坐标、预测框坐标、类别、最大IoU、判定类型、置信度,方便后续按条件筛选和排序。
3.4 检测筛选结果与IoU阈值讨论
第一轮检测筛选共标记了1,143张疑似有框问题的图片,其中框错位占大头(约73%),漏检约18%,误检约9%。人工复核后确认879张确实存在框问题,准确率约77%。这个命中率比分类阶段略低,但考虑到检测问题的主观性更强(框往左挪两个像素算不算错,人跟人判断都不一样),这个命中率完全可以接受。
关于IoU阈值设0.3合不合适,我的实验结论是这样:阈值越高,挑出的图片越多,但误报也越多;阈值越低,漏检率越高。0.3附近是一个不错的平衡点。如果你手里的数据标注质量特别差,可以先0.2跑一遍看看输出规模,再决定要不要收紧。另外对于小目标,IoU天然偏低,要单独设置一个宽松阈值,否则会把大量小目标图片误伤进复核队列。
4. 第三阶段:多模态AI兜底边界情况
4.1 为什么还需要多模态模型
前两轮跑完后我以为工作接近尾声了,但复核过程中发现还有一类错误卡在中间地带:类别对但语义怪。比如标签写的是“翠鸟”,图里确实也有翠鸟,但翠鸟只占了画面边缘一小块;或者标签写“两只鸟在打架”,框里其实是一只在巢里的鸟——这种错误分类和检测模型都很难发现,因为分类模型觉得“类别没错”,检测模型觉得“框里有目标”,但整体语义就是不对劲。
这时候需要引入多模态AI来做语义层面的判断。思路其实很简单:把图片(或裁剪后的检测框区域)送入图文匹配模型,同时输入一段文本描述,模型输出图文匹配度分数。分数低,就说明图与文字描述不符,值得人工看看。
4.2 实现方式与成本控制
具体实现上,我用的方案是“YOLO目标检测 + 多模态AI分析”的组合流程——这也是当前比较流行的工程范式。步骤是:
- 取第二轮的疑似图片,用YOLOv5s裁出每个目标框区域;
- 构造文本描述模板:“一张{类别名}的照片”,也可以更细化为“一只{类别名}站在树枝上”、“一只{类别名}在飞行中”等变体;
- 使用中文多模态模型(也可以用CLIP类模型,取决于你的数据语言环境)分别计算图像区域与文本描述的匹配分数;
- 匹配分数低于设定阈值(如0.25)的,标记为“语义不符”,进入人工复核队列。
这里要强调成本控制。多模态推理速度慢,全量跑12,410张图完全不现实。我的做法是只对前两轮中被判为“边界”的图片做多模态复核,也就是分类阶段相似度在0.75到0.85之间、检测阶段IoU在0.3到0.5之间的图片,总量约2000张。这样多模态模型只承载了约六分之一的负载,整体耗时控制在可接受范围。
4.3 多模态兜底发现的问题
这一轮又捞出了120张疑似边界问题图片,人工复核确认87张确实存在标注问题。主要类型包括:鸟类部分被遮挡但标注框中包含了过多背景;一张图里有多只鸟但只标了一只;以及一张图里标注了不存在的物种(物体无中生有)——这种情况通常是因为标注员混淆了参考图。
这些问题的共同点是:分类模型看类别没错,检测模型看框里有东西,只有多模态模型把“图”和“文”结合起来比对时才能发现语义裂缝。所以我的经验是,如果数据是给人看的、人用语言描述去标注的,那么清洗时也要用能理解语言的模型去筛,逻辑是对称的。
5. 全流程复盘:踩过的坑与避坑指南
5.1 新手最容易踩的五个坑
整个实验过程中我踩了不少坑,挑五个最有代表性的记录一下:
坑一:直接用脏数据微调检测模型。我第一次微调YOLO时偷懒,直接拿原始标注(未清洗)当训练集,结果模型mAP只有78%左右,检测框普遍偏大——因为原始标注框就偏大,模型反而学到了这个错误习惯。后来换成清洗后的数据重新训练,mAP直接跳到96%。结论:先清洗再训练,不要让模型学坏。
坑二:ViT评估时乱改分类头。前面已经提过了,用预训练模型做特征提取时不要动分类头。我一开始以为把分类头改成自己的类别数可以让模型“更懂”我的数据,结果特征空间被污染,相似度判断反而不准。核心原则是:提取特征用原装分类头,微调训练才换自定义分类头。
坑三:滑窗不去重。前面也说过了,不做去重会导致大量重复标记,人工复核工作量膨胀。建议所有滑窗结果按图片ID聚合,只保留最高置信度的一次。
坑四:IoU計算邏輯問題。计算单个GT框与预测框的IoU时,如果图里有多个目标,必须先做匈牙利匹配之类的指派逻辑,让每个GT框只对应一个预测框,再算IoU。否则同一张图里的重复检测会让统计失真。我一开始没做指派,结果框错位的误报率特别高。
坑五:label smoothing过强。YOLOv5默认的label smoothing是0.1,对正常训练没问题,但在清洗任务中会压低模型的整体置信度分布——很多正确样本的置信度从0.9掉到0.7,导致阈值筛选时被误判为“低置信度疑似错误”。我把label smoothing调到0.05后,置信度分布明显更清晰,筛查准确率也上来了。
5.2 全流程时间成本核算
很多同行关心这套流程的实际开销,我整理了一个明细表:
| 阶段 | 处理图片量 | 耗时 | 硬件 |
|---|---|---|---|
| ResNet50特征提取 | 12,410张 | 约15分钟 | 单卡V100 |
| 相似度计算与筛选 | 12,410张 | 约5分钟 | CPU |
| YOLOv5s推理 | 4,000张(重点筛选) | 约40分钟 | 单卡RTX 2080Ti |
| 多模态兜底 | 2,000张 | 约90分钟 | 单卡V100 |
| 人工复核 | 1,790张 | 约1人天 | - |
总计算时间不到3小时,人工复核一个工作日。相比全量12,410张图从头人工复查(估计要五到七个人天),省下来的时间非常可观。这也验证了我一开始的判断:模型挑错+人工复核的模式,在标注质量筛查场景下是性价比极高的方案。
5.3 阈值到底怎么定才靠谱
阈值是这类清洗任务里最需要反复调的东西。我的经验是:别拍脑袋定,一定要先抽样看分布。具体做法是:随机抽200张图,手工标注“错/对”,然后跑一遍模型得分数,画ROC曲线,选一个“综合漏检率和误报率最优”的切分点。以我的数据来说,相似度0.75和IoU 0.3都是这样试出来的。
另外,每个阈值都会有一个置信区间,要结合你的下游任务容忍度来定。如果清洗后的数据要用于训练严肃模型(比如医疗影像),阈值可以定严一点,宁可多花时间复核;如果只是做统计分析,阈值可以宽松一点,减少无效工作。
6. 写在最后的实操心得
这次实验让我对“模型给数据挑错”这件事有了更深的理解。以前总觉得模型是消费者,向数据索要知识;现在换个角度,模型完全可以是数据的质检员,用它们在预训练阶段学到的通用先验,反过来监督人类标注的质量。分类模型查语义错,检测模型查位置错,多模态模型查图文错——三层各管一摊,组合起来基本可以覆盖绝大部分标注质量问题。
最后再分享一个实操细节:清洗结果别只存一个“错/对”标记,我建议把每一轮的模型输出、判定条件、复核结果全部保存成单独字段。这轮清洗完不代表数据就一劳永逸了,后续随着更好的模型出现,你还可以用同样的流程再筛一遍,而历史记录能帮你衡量“新模型比旧模型多发现了什么”。我当时就是留了这些底,后来换了一版更强的检测模型重跑,又额外捞回了两百多张有问题的图。
数据清洗不是一次性工程,而是一个可以持续迭代的过程。模型越强,清洗能力越强;数据越干净,训练出的模型越强。这个正向循环一旦跑起来,后面的所有工作都会省力很多。