018、困难样本挖掘策略训练中自动发现易错样本定向补充标注去年秋天我在调试一个工业质检项目模型在产线上跑了一周漏检率始终卡在0.3%下不去。翻看日志发现那些漏掉的缺陷样本几乎全是同一个类型——边缘模糊的划痕标注框里只有几个像素宽。我盯着检测结果看了半小时突然意识到一个残酷的事实训练集里这类样本太少了模型根本没见过足够多的“难例”。这就是困难样本挖掘Hard Example Mining要解决的问题。不是所有样本对模型提升都有同等价值那些让模型“犹豫不决”的样本才是真正能推动性能边界的关键。从损失函数里“抓”出困难样本困难样本挖掘的核心思路很简单在训练过程中让模型自己告诉我们哪些样本它学得不好。最直接的做法就是看损失值——损失大的样本就是模型当前阶段搞不定的。我在YOLOv8里实现过一个粗糙版本代码大概长这样# 别这样写这只是演示思路defhard_example_mining(batch_losses,ratio0.3):# 按损失降序排列取前30%sorted_indicestorch.argsort(batch_losses,descendingTrue)hard_indicessorted_indices[:int(len(batch_losses)*ratio)]returnhard_indices这里踩过坑直接按损失排序取top-k会导致训练初期大量背景样本被选进来。因为模型刚开始啥都认不出来背景区域的损失反而最大。正确的做法是只对正样本包含目标的区域计算损失排序或者至少给正负样本分别设置不同的采样比例。OHEM在线困难样本挖掘的经典实现OHEMOnline Hard Example Mining是目标检测领域的老牌方法核心思想是让模型先跑一次前向传播找出损失最大的那些样本然后只在这些样本上做反向传播。在YOLOv6里集成OHEM时我踩过一个坑OHEM要求两次前向传播第一次只做推理不更新梯度第二次才在选出的困难样本上训练。这会带来显存翻倍的问题。我的解决方案是共享特征图——第一次前向传播时把中间特征缓存下来第二次直接复用。# 伪代码实际实现要处理batch维度defohem_forward(model,images,targets):# 第一次前向只算损失不更新梯度withtorch.no_grad():featuresmodel.extract_features(images)lossesmodel.compute_loss(features,targets)# 按损失排序选出困难样本索引_,hard_indicestorch.topk(losses,kint(len(losses)*0.3))# 第二次前向只在困难样本上训练hard_imagesimages[hard_indices]hard_targets[targets[i]foriinhard_indices]hard_featuresmodel.extract_features(hard_images)lossmodel.compute_loss(hard_features,hard_targets)loss.backward()这里有个细节容易被忽略OHEM的采样比例不是固定的。我试过0.1到0.5之间的各种比例发现0.25左右效果最好。比例太小模型学不到足够多的难例比例太大又退化成全量训练。Focal Loss让模型自己“关注”困难样本OHEM需要显式的采样操作而Focal Loss是一种更优雅的隐式方案。它通过修改损失函数让模型自动给困难样本分配更大的梯度权重。Focal Loss的公式看起来简单但调参是个技术活deffocal_loss(pred,target,gamma2.0,alpha0.25):# gamma控制困难样本的关注程度# alpha平衡正负样本ce_lossF.binary_cross_entropy_with_logits(pred,target,reductionnone)pttorch.exp(-ce_loss)focal_weight(1-pt)**gammaifalphaisnotNone:alpha_weighttarget*alpha(1-target)*(1-alpha)focal_weightfocal_weight*alpha_weightreturn(focal_weight*ce_loss).mean()我在YOLOv11上试过Focal Loss发现gamma2.0对大多数场景都够用但有个例外当你的数据集里困难样本占比特别高比如超过40%gamma反而要调低到1.5左右。因为gamma太大模型会过度关注那些极难样本反而忽略了中等难度的样本——这些样本才是提升泛化能力的关键。动态阈值策略让挖掘过程自适应固定比例的困难样本挖掘有个问题训练初期和后期模型的“困难”标准完全不同。初期可能所有样本都难后期可能只有极少数样本难。固定比例会导致后期选进来的样本其实并不难。我后来在YOLOv8里实现了一个动态阈值策略效果比固定比例好不少classAdaptiveHardMining:def__init__(self,initial_ratio0.3,decay_factor0.95):self.ratioinitial_ratio self.decay_factordecay_factor self.loss_history[]defupdate_ratio(self,current_losses):# 记录最近N个batch的平均损失self.loss_history.append(current_losses.mean().item())iflen(self.loss_history)100:self.loss_history.pop(0)# 如果平均损失持续下降说明模型在变好可以降低采样比例iflen(self.loss_history)10:recent_avgnp.mean(self.loss_history[-10:])old_avgnp.mean(self.loss_history[-20:-10])ifrecent_avgold_avg*0.9:self.ratiomax(0.1,self.ratio*self.decay_factor)# 根据当前比例计算阈值thresholdnp.percentile(current_losses.cpu().numpy(),(1-self.ratio)*100)returnthreshold这个策略的核心逻辑是模型学得越好需要挖掘的困难样本就越少。但要注意decay_factor不能设得太小否则采样比例下降太快模型会错过一些潜在的难例。数据层面的补充标注策略困难样本挖掘不只是训练时的技巧它还能指导数据标注。我在项目中做过一个“主动学习”流程用当前模型对未标注数据做推理选出置信度在0.3-0.7之间的样本这些是模型最不确定的把这些样本交给标注员补充标注用新标注的数据继续训练这个流程跑三轮标注效率提升至少3倍。因为标注员不再需要标注那些模型已经能搞定的简单样本只聚焦在模型搞不定的难例上。个人经验总结写了这么多说点实在的。困难样本挖掘不是银弹它解决的是“样本分布不均衡”的问题。如果你的数据集本身质量很高、分布均匀强行上困难样本挖掘反而可能破坏训练稳定性。我的建议是先用全量数据训练一个baseline然后分析错误样本的类型。如果错误集中在某几类样本上再针对性地做困难样本挖掘。别一上来就上OHEM或Focal Loss先搞清楚问题出在哪。另外困难样本挖掘和模型结构是耦合的。YOLOv6的RepVGG结构对OHEM比较友好因为它的梯度传播更稳定而YOLOv11的C2f结构配合Focal Loss效果更好。这个没有标准答案得自己试。最后提醒一句困难样本挖掘会增加训练时间OHEM大概增加30%-50%Focal Loss基本不增加。如果你的项目对训练速度敏感优先考虑Focal Loss。
018、困难样本挖掘策略:训练中自动发现易错样本,定向补充标注
发布时间:2026/5/29 2:51:09
018、困难样本挖掘策略训练中自动发现易错样本定向补充标注去年秋天我在调试一个工业质检项目模型在产线上跑了一周漏检率始终卡在0.3%下不去。翻看日志发现那些漏掉的缺陷样本几乎全是同一个类型——边缘模糊的划痕标注框里只有几个像素宽。我盯着检测结果看了半小时突然意识到一个残酷的事实训练集里这类样本太少了模型根本没见过足够多的“难例”。这就是困难样本挖掘Hard Example Mining要解决的问题。不是所有样本对模型提升都有同等价值那些让模型“犹豫不决”的样本才是真正能推动性能边界的关键。从损失函数里“抓”出困难样本困难样本挖掘的核心思路很简单在训练过程中让模型自己告诉我们哪些样本它学得不好。最直接的做法就是看损失值——损失大的样本就是模型当前阶段搞不定的。我在YOLOv8里实现过一个粗糙版本代码大概长这样# 别这样写这只是演示思路defhard_example_mining(batch_losses,ratio0.3):# 按损失降序排列取前30%sorted_indicestorch.argsort(batch_losses,descendingTrue)hard_indicessorted_indices[:int(len(batch_losses)*ratio)]returnhard_indices这里踩过坑直接按损失排序取top-k会导致训练初期大量背景样本被选进来。因为模型刚开始啥都认不出来背景区域的损失反而最大。正确的做法是只对正样本包含目标的区域计算损失排序或者至少给正负样本分别设置不同的采样比例。OHEM在线困难样本挖掘的经典实现OHEMOnline Hard Example Mining是目标检测领域的老牌方法核心思想是让模型先跑一次前向传播找出损失最大的那些样本然后只在这些样本上做反向传播。在YOLOv6里集成OHEM时我踩过一个坑OHEM要求两次前向传播第一次只做推理不更新梯度第二次才在选出的困难样本上训练。这会带来显存翻倍的问题。我的解决方案是共享特征图——第一次前向传播时把中间特征缓存下来第二次直接复用。# 伪代码实际实现要处理batch维度defohem_forward(model,images,targets):# 第一次前向只算损失不更新梯度withtorch.no_grad():featuresmodel.extract_features(images)lossesmodel.compute_loss(features,targets)# 按损失排序选出困难样本索引_,hard_indicestorch.topk(losses,kint(len(losses)*0.3))# 第二次前向只在困难样本上训练hard_imagesimages[hard_indices]hard_targets[targets[i]foriinhard_indices]hard_featuresmodel.extract_features(hard_images)lossmodel.compute_loss(hard_features,hard_targets)loss.backward()这里有个细节容易被忽略OHEM的采样比例不是固定的。我试过0.1到0.5之间的各种比例发现0.25左右效果最好。比例太小模型学不到足够多的难例比例太大又退化成全量训练。Focal Loss让模型自己“关注”困难样本OHEM需要显式的采样操作而Focal Loss是一种更优雅的隐式方案。它通过修改损失函数让模型自动给困难样本分配更大的梯度权重。Focal Loss的公式看起来简单但调参是个技术活deffocal_loss(pred,target,gamma2.0,alpha0.25):# gamma控制困难样本的关注程度# alpha平衡正负样本ce_lossF.binary_cross_entropy_with_logits(pred,target,reductionnone)pttorch.exp(-ce_loss)focal_weight(1-pt)**gammaifalphaisnotNone:alpha_weighttarget*alpha(1-target)*(1-alpha)focal_weightfocal_weight*alpha_weightreturn(focal_weight*ce_loss).mean()我在YOLOv11上试过Focal Loss发现gamma2.0对大多数场景都够用但有个例外当你的数据集里困难样本占比特别高比如超过40%gamma反而要调低到1.5左右。因为gamma太大模型会过度关注那些极难样本反而忽略了中等难度的样本——这些样本才是提升泛化能力的关键。动态阈值策略让挖掘过程自适应固定比例的困难样本挖掘有个问题训练初期和后期模型的“困难”标准完全不同。初期可能所有样本都难后期可能只有极少数样本难。固定比例会导致后期选进来的样本其实并不难。我后来在YOLOv8里实现了一个动态阈值策略效果比固定比例好不少classAdaptiveHardMining:def__init__(self,initial_ratio0.3,decay_factor0.95):self.ratioinitial_ratio self.decay_factordecay_factor self.loss_history[]defupdate_ratio(self,current_losses):# 记录最近N个batch的平均损失self.loss_history.append(current_losses.mean().item())iflen(self.loss_history)100:self.loss_history.pop(0)# 如果平均损失持续下降说明模型在变好可以降低采样比例iflen(self.loss_history)10:recent_avgnp.mean(self.loss_history[-10:])old_avgnp.mean(self.loss_history[-20:-10])ifrecent_avgold_avg*0.9:self.ratiomax(0.1,self.ratio*self.decay_factor)# 根据当前比例计算阈值thresholdnp.percentile(current_losses.cpu().numpy(),(1-self.ratio)*100)returnthreshold这个策略的核心逻辑是模型学得越好需要挖掘的困难样本就越少。但要注意decay_factor不能设得太小否则采样比例下降太快模型会错过一些潜在的难例。数据层面的补充标注策略困难样本挖掘不只是训练时的技巧它还能指导数据标注。我在项目中做过一个“主动学习”流程用当前模型对未标注数据做推理选出置信度在0.3-0.7之间的样本这些是模型最不确定的把这些样本交给标注员补充标注用新标注的数据继续训练这个流程跑三轮标注效率提升至少3倍。因为标注员不再需要标注那些模型已经能搞定的简单样本只聚焦在模型搞不定的难例上。个人经验总结写了这么多说点实在的。困难样本挖掘不是银弹它解决的是“样本分布不均衡”的问题。如果你的数据集本身质量很高、分布均匀强行上困难样本挖掘反而可能破坏训练稳定性。我的建议是先用全量数据训练一个baseline然后分析错误样本的类型。如果错误集中在某几类样本上再针对性地做困难样本挖掘。别一上来就上OHEM或Focal Loss先搞清楚问题出在哪。另外困难样本挖掘和模型结构是耦合的。YOLOv6的RepVGG结构对OHEM比较友好因为它的梯度传播更稳定而YOLOv11的C2f结构配合Focal Loss效果更好。这个没有标准答案得自己试。最后提醒一句困难样本挖掘会增加训练时间OHEM大概增加30%-50%Focal Loss基本不增加。如果你的项目对训练速度敏感优先考虑Focal Loss。