做深度学习项目的人,十有八九被脏标签折磨过。众包标注、自动爬取、规则生成,都会往训练集里掺进各种各样的错误标签。你在跑分类任务时可能遇到过这种情况:验证集明明在涨,可线上效果一塌糊涂;或者训练损失死活降不下去,一看样本,发现有些图压根贴错了标签。这时候大多数人第一个念头是“清洗数据”,把可疑样本筛出来删掉或人工复核。可“清洗”这两个字说起来容易,落地却很尴尬——删掉的样本里可能带着宝贵特征,人工复核的成本又高得离谱。
DIVIDEMIX这个思路,拍案叫绝的地方就在于它换了个视角:既然标签可能是错的,那干脆别把标签当“真值”用。把带标签的数据集动态划分成“干净子集”和“噪声子集”,干净子集正常喂给监督分支,噪声子集呢,不删除、不重标注,而是降级成“无标签数据”,走半监督学习那套流程,用低置信度伪标签 + 一致性正则来训练。一句话总结:噪声标签学习 = 半监督学习的一部分。
这篇文章就把DIVIDEMIX的前因后果、技术细节、复现要点和实践经验掰开揉碎讲清楚。适合正在做数据清洗的工程同学、研究鲁棒训练方法的算法工程师,以及想理解“为什么坏标签也可以不扔掉”的进阶读者。
1. 把噪声标签当成半监督学习,思路从哪来
1.1 噪声标签为什么会成为项目的拦路虎
先定义一下“噪声标签”是什么。它指训练集中与真实类别不一致的标签,来源非常普遍:
- 众包标注平台,标注员对模糊图片的理解不一致,产生分歧
- 自动标注流程,比如用上一代模型生成伪标签,错误会传播
- 爬虫抓取数据,网页标题、alt文本与图片内容不匹配
- 用户反馈数据,用户点的“不感兴趣”标签可能不是错,但“感兴趣”往往极不准确
最麻烦的是,噪声标签不会均匀分布。有的类别错得多,有的类别错得少;有的样本错得天翻地覆,有的只是“边缘模糊”。如果模型把噪声标签当成硬真值去拟合,轻则学不到正确的决策边界,重则直接记住错误样本——这就是所谓“确认偏差”:模型越是相信脏标签,脏标签带来的危害越大,双方互相强化。小学习率能延缓这个过程,但不能根除。
1.2 直接清洗标签为什么很难做
最常见的清洗方案有几种:
- 人工抽检复核:准确,但成本太高,对于几十万上百万级别的样本不现实
- 用低置信度过滤:拿一个先验模型预测,丢low confidence样本。但先验模型本身可能被噪声影响,而且过滤后会损失大量有效信息
- 聚类/最近邻匹配:依赖特征质量,遇到视觉相似但类别不同的样本时,很容易把干净样本误判为噪声
这些方案的共同问题,是它们把“噪声样本”当作“垃圾样本”处理。但一个样本即便标签错了,它的视觉特征、语义结构仍然是真实的。比如一张标注成“猫”的狗图片,它的特征和“狗”类样本高度一致;你把它删掉,实际上是浪费了一条可以帮助模型理解“狗”类别的数据。反过来,如果你把它当作无标签样本,让模型从特征空间去学习它属于“狗”,那它反而变成了有用的辅助信号。
1.3 换一个视角:丢弃标签不等于丢弃样本
这就是DIVIDEMIX最核心的哲学转变:一个不可靠的标签,最合理的用法不是去纠正它,而是忽略它。模型总需要学到“这个特征对应什么类别”,这可以来自两个渠道:
- 监督信号:干净的、可信的样本
- 无监督/半监督信号:大量样本本身的特征分布、样本间关系、增强后的一致性
噪声样本恰恰可以充当第二种信号。把带噪样本当作无标签数据后,我们不需要知道它的真实类别,只需要让模型在增强前后对它的预测保持一致,并让它在特征空间里与相似样本靠拢。用这种方式,坏标签没有变成正确的标签,但数据本身的价值被保留了。
这个视角很像日常生活中的判断:当一个人提供的地址不可靠时,你不会完全不信他这个人,而是忽略地址信息,从其他行为线索判断。DIVIDEMIX正是把“不可靠的标签”降级为“不提供明确方向的信息源”。
2. DivideMix如何把“清洗+半监督”变成一套训练流程
2.1 小学习率拟合:让网络先“记住”干净的样本
DIVIDEMIX的第一步,是先对模型做一段“热身”训练。这段训练直接用原始带噪标签,但刻意使用很小的学习率。
为什么一定要小学习率?因为噪声样本通常带来异常大的梯度。用大学习率时,几个噪声样本就能把模型推向错误的方向,模型很快就开始记住脏标签。用小学习率时,模型更新幅度小、步长短,更倾向于拟合那些在多个样本间一致的、稳定的模式——也就是干净样本赋予的模式。换句话说,小学习率天然是一个“干净样本优先”的筛选器。
在我实际实验里,这个热身阶段的长度非常关键。太短,模型还没学到足够的特征,损失分布拉不开;太长,模型又会开始适应噪声。比较稳妥的做法是先用总训练轮数的十分之一左右热身,观察验证集曲线不再上升时立刻停住。如果观察到干净样本和噪声样本的损失值开始没有明显分区,就把热身轮数再缩短一截。
2.2 损失分布划分:用GMM而不是一个死板阈值
热身训练结束后,模型会对每个训练样本产生一个损失值。直觉上,干净样本的损失低,噪声样本的损失高。但难点是:这个损失分布的形态不固定,噪声比例高时,两条分布会重叠,你很难画一条线把两类彻底分开。
DIVIDEMIX的处理方式是用高斯混合模型GMM去拟合损失分布。把损失值当作一维特征,假设它由两个高斯分布组成:一个对应干净样本(均值低),一个对应噪声样本(均值高)。用EM算法求出每个样本属于“干净分量”的后验概率,再根据这个概率来划分。这比手动设定固定阈值鲁棒得多,因为不同数据集、不同网络结构、不同噪声率下,“低损失”和“高损失”的绝对数值完全不同。我试过用固定阈值去替代GMM,低噪声率场景还行,一旦噪声率达到0.4以上,阈值怎么调都很难平衡两个子集的纯度。
值得留意的是,在双网络版本的DIVIDEMIX中,两个网络是分别做划分的。每个网络都有自己的GMM,对同一个样本的归属判断可能不同。此时可以使用“重叠损失”或者说“两个网络共识”的思路:对于两个网络一致认为是噪声的样本,伪标签的置信度会很低;两个网络判断不一致的样本,则取一个中间权重。这背后的逻辑是,两个初始化和训练轨迹不同的网络,不会同时犯同一个错误;它们的一致性,比单一网络更有参考价值。
2.3 双网络分歧:防止“确认偏差”越滚越大
只用一个网络做划分有一个天然缺陷:模型在认脏标签的过程中,会逐渐对某些错误模式习以为常,产生“确认偏差”——它认为干净的样本可能越来越不干净,划分结果越来越差。DIVIDEMIX使用两个网络并行训练,让两个网络在初始化、数据增强、参数更新上刻意保持差异。由于两个网络独立学习,它们的错误不会完全同步。当两个网络对同一个样本的预测出现分歧时,这个样本很可能处于边界区域,它的伪标签需要被压低权重;当两个网络达成共识时,这个样本的信号才值得被采纳。
这种双网络机制,本质上是一种“双保险”式的交叉验证。在复现时需要注意:两个网络的每一次迭代必须使用不同的小批样本顺序和不同的增强结果,否则它们很快会趋同,双网络就失去了意义。
2.4 噪声子集的无监督损失怎么算
划分完成后,干净子集直接做标准交叉熵训练。噪声子集则进入半监督分支,这一步用的是MixMatch思想,但做了一些针对性的改良:
- 对噪声样本做两次增强,一次弱增强、一次强增强。弱增强版本用于生成伪标签,强增强版本用于计算一致性损失。
- 计算弱增强的预测,并用一个温度参数做“锐化”处理,让预测分布更“自信”。
- 对干净集样本和噪声集样本,在嵌入空间做MixUp混合。混合不仅发生在输入层面,也发生在标签层面。
关键是,伪标签置信度并非“一视同仁”。DIVIDEMIX用两个网络的损失信息构造了一个样本级别的权重。如果一个样本在划分阶段被判为噪声,但两个网络对它的预测非常一致且置信度高,那它就能获得较高的无监督损失权重;如果两个网络本身就分歧很大,就调低这个样本对总损失的贡献。这个动态权重的意义是:伪标签的质量参差不齐,质量控制必须细化到样本级别。
3. 实操:复现DivideMix的关键配置与训练细节
3.1 数据与验证集怎么准备
开始复现前,必须先想好验证集怎么来。很多带噪声标签的数据集只有训练集和测试集,训练集里既包含了干净样本也包含了噪声样本,没有额外的干净验证集。此时有两个常用做法:
- 从划分出的“干净子集”里,固定抽出一小部分作为验证集,不参与训练
- 或者从原始训练集随机抽取一小部分(比如5%),人工确保这些样本足够干净,作为验证集
第二种方式更稳妥,因为验证的目的是看模型真实泛化能力,如果验证集本身也有噪声,早停就不准了。我一般习惯保留一个干净的小验证集,不管训练数据噪声多高,验证集绝对保持干净。这好比你在嘈杂的厨房里做饭,还得留一个安静的角落放味觉试菜。
3.2 网络、优化器与训练周期
DIVIDEMIX本身不绑定具体网络结构,我通常用两个结构相同但初始化不同的网络。以图像分类为例,常见选择是两层相似卷积网络或残差网络。优化器用带动量的随机梯度下降,初始学习率设为一个较小值,比如0.02。训练周期一般较长,尤其在噪声比例高时,需要更多轮次来稳定划分。
训练周期内,每一轮的流程是:先用GMM对全量训练样本算损失并划分,然后在同一轮内更新两次模型——第一次用干净子集监督损失,第二次用噪声子集无监督损失。两个网络的损失会沿用到下一轮划分中,也就是说,划分结果和训练过程是交替进行的。这个“每轮动态重划”很重要,因为随着训练推进,模型的判别能力在增强,原本被判为噪声的样本可能逐渐变得清晰,之后可以重新归入干净集。
超参数方面,常用配置如下:
| 超参数 | 推荐值区间 | 说明 |
|---|---|---|
| 热身轮数 | 总轮数的5%~15% | 以验证集为准提前停止 |
| 初始学习率 | 0.01~0.05 | 越小,干净样本优先性越强 |
| 批量大小 | 64~128 | 大批量有利于GMM稳定 |
| 无监督损失权重 | 从0线性升到1 | 避免一开始就让噪声主导 |
| 锐化温度 | 0.5~1.0 | 温度越低,伪标签越尖锐 |
| MixUp比例 | 0.2~0.8 | 控制混合强度 |
3.3 核心损失函数与伪标签的加权
整个训练过程的总损失可以写成下面这个形式:
L_total = L_supervised(干净子集) + lambda_t * L_unsupervised(噪声子集)
L_supervised就是标准交叉熵。L_unsupervised则是在MixUp之后,对伪标签计算一致性损失。lambda_t是一个随时步变化的系数,前期很小,后期逐渐增大。为什么要这样设计?因为训练早期模型的预测本身不靠谱,如果给无监督分支过大的权重,模型会被自己的错误预测带偏;随着训练进行,模型逐渐学到结构,伪标签质量提高,此时才应该逐步提高无监督分支的贡献。这就像先让你读带标准答案的例题,再让你做没答案的拓展题,而不是一上来就做无答案题。
伪标签的生成,我推荐这么操作:对噪声样本取一次弱增强得到预测p1,取一次强增强得到预测p2,两者取平均后做锐化。这里锐化的作用是让预测分布更接近one-hot,从而避免模型对伪标签过于含糊。但要注意,锐化温度不宜过低,否则伪标签过于“自信”,对错误预测也毫不怀疑,反而有害。
最后,把两个网络算出的损失信息转化成权重w_i。一个简单可复现的做法是:先对两个网络的损失各自做标准化(归一到0~1),再算它们的乘积,再映射到0~1,作为该样本在无监督损失中的加权系数。两个网络都给出高损失的样本,w接近0;两个网络都给出低损失的样本,w接近1;一高一低时取中间值。这样,伪标签的质量控制就真正落到了每一个样本上。
3.4 训练循环伪代码与执行顺序
下面是整个训练流程的Python风格伪代码,每一步都对应上面说过的机制:
# 伪代码:DivideMix 训练主循环 def train_divide_mix(): # 两个网络,同样的结构,不同的初始化 net_a = init_network() net_b = init_network() optimizer_a = SGD(net_a.parameters(), lr=0.02, momentum=0.9) optimizer_b = SGD(net_b.parameters(), lr=0.02, momentum=0.9) # 热身阶段:小学习率,直接用原始标签训练 for epoch in range(warmup_epochs): for images, labels in train_loader: loss_a = ce(net_a(images), labels) loss_b = ce(net_b(images), labels) update(net_a, optimizer_a, loss_a, lr=0.02) update(net_b, optimizer_b, loss_b, lr=0.02) # 正式动态划分 + 半监督训练 for epoch in range(total_epochs): # 1. 用当前网络计算全体训练样本的损失 losses_a = evaluate_loss(net_a, train_set) losses_b = evaluate_loss(net_b, train_set) # 2. 对损失分别做GMM拟合,得到干净/噪声的概率 clean_mask_a = gmm_divide(losses_a) clean_mask_b = gmm_divide(losses_b) # 3. 根据两个网络的划分,得到最终干净集与噪声集 clean_set, noisy_set = merge_divide_with_overlap( clean_mask_a, clean_mask_b ) # 4. 干净集上做标准监督训练 for images_clean, labels_clean in clean_loader: loss = ce_a + ce_b # 5. 噪声集上做MixMatch风格半监督训练 for images_noisy in noisy_loader: # 生成锐化伪标签,计算无监督一致性损失 pseudo_a = sharpen(net_a(weak_aug(images_noisy))) pseudo_b = sharpen(net_b(weak_aug(images_noisy))) pseudo = average(pseudo_a, pseudo_b) # 根据两个网络的损失信息,计算样本权重 w = overlap_weight(losses_a, losses_b) loss_u = consistency_loss( net(strong_aug(images_noisy)), pseudo ) loss_u = mean(w * loss_u) # 6. 更新总损失并反向传播 loss = supervised_loss + lambda_t * loss_u update(net_a, optimizer_a, loss) update(net_b, optimizer_b, loss)这个伪代码可以直接套进常见的深度学习框架,只需把GMM部分换成现成的混合模型工具库,把一致性损失换成交叉熵或均方误差即可。我第一次复现时,最需要警惕的地方就是“噪声集”和“干净集”的batch要分开处理,不要把两个分支的样本混在同一个batch里,否则增强和MixUp的边界会变得混乱。
4. 评测结果与调参避坑经验
4.1 不同噪声率下的“预期表现”
DIVIDEMIX在几类标准基准集上的提升是有明显规律的。噪声率低(10%~20%)时,它和普通监督学习相比优势不算夸张,因为此时干净样本占比高,正常训练已经能学到大部分信息。噪声率升高到40%~50%时,普通训练几乎崩掉,但DIVIDEMIX仍然能保持相对平滑的下降曲线。这说明“划分+半监督”的设计,在极端噪声下格外有价值:当大部分标签都不可信时,从监督信号里继续榨取价值已经没有意义,反而是无监督分支提供的特征一致性约束在拖住模型。
在真实噪声场景下,比如某个从爬虫数据构造的数据集,噪声分布并不对称,某些类别的错误率远高于其他类别。这种情况下,DIVIDEMIX的GMM划分依然能工作,因为划分是逐样本进行的,不看类别整体错误率,只看个体损失。我自己的经验是,真实噪声下把GMM换成更稳健的鲁棒损失拟合(比如对损失做分位数转化)会有额外提升,但整体框架不需要改动。
4.2 训练中最常踩到的几个坑
第一个坑:GMM拟合分不开。表现为干净样本和噪声样本的损失分布几乎重叠,GMM无法有效划分。原因通常是热身轮数太少,模型还没学会特征,所有样本的损失都在高位。解决办法很简单:适当增加热身轮数,或者手动把训练集的损失做个log变换,放大尾部分离。
第二个坑:两个网络逐渐趋同。如果两个网络每次迭代看到的batch顺序、增强方式一模一样,它们会很快收敛到同一个状态,双网络的共识机制就失效了。解决方法是保证两个网络使用不同的数据增强路径和不同的数据顺序,也可以在反向传播时给两个网络加不同的随机丢弃(dropout)模式。
第三个坑:无监督损失权重拉升太快。训练早期伪标签质量很低,如果lambda_t没做ramp-up,直接从0跳到满值,模型很容易被自己误导,出现“越训越差”的情况。建议从0开始,线性增长,到总训练轮数的后半程再让它达到最大。
第四个坑:显存和训练时间爆表。双网络加两次增强加MixUp,相当于每个step同时跑了好几份前向和反向。我通常用混合精度训练,同时把“整轮计算loss用于划分”这一步,放到不计算梯度的推理模式下,显存消耗会明显下降。还有一个技巧是:不需要每轮都重新划分,可以每5轮或每10轮重划一次,能省不少时间、效果也不差。
4.3 调参顺序建议
有很多人一开始就调锐化温度、MixUp参数、GMM分量数,结果调了半天训练都跑不稳。我的建议是,按下面顺序排查:
- 先保证热身阶段正常:验证集能持续下降,损失分布初步分离。
- 再固定无监督损失权重为0,只跑监督分支,确认干净子集训练不崩。
- 再逐步打开无监督分支,看验证集曲线是否比纯监督更好。
- 最后再调锐化温度、MixUp比例、GMM分位数这些精细项。
这个顺序的逻辑是:先确认划分机制可靠,再确认监督分支稳定,最后才是半监督的增益。任何一步往前跳,出了bug都很难定位。
5. 延伸:从分类任务到更泛的监督问题
5.1 可以把这套思路复用到哪些场景
DIVIDEMIX虽然一般以分类问题为例,但它的思想完全能迁移到其他监督信号不可信的场景:
- 目标检测:边界框回归的标签也可以看作一种噪声标签,可以用划分逻辑把低质量框降级为无监督候选框
- 语义分割:标注边界模糊、类别混淆严重,可以按像素级别的损失做划分
- 多标签分类:某些标签漏标、错标,可以把样本划分为“全部可信”“部分可信”两类,只对可信部分做监督
- 医疗影像辅助标注:专家之间标注不一致时,把分歧样本当作无标签数据来训练模型
这里面更通用的原则是:不要因为一个监督信号不可靠,就丢掉对应的数据,而是把不可靠信号降级为无监督信号,让模型从多个角度去利用它。
5.2 和其他技术的组合
我试过在DIVIDEMIX基础上叠加一些常见技巧,效果比较好:
- 在清洗后的干净子集上使用样本重加权,比如按照GMM概率软加权,而不是硬划分到0/1
- 把伪标签的锐化机制换成更精细的置信度惩罚,对低置信度样本直接置零损失
- 与对比学习结合,把噪声样本做更多正样本增强,用对比损失进一步增强特征稳定性
这些组合的共同特点,是让“可信度”这个信息更加连续化:不是非黑即白地划分为干净或噪声,而是在两者之间保留平滑过渡。DIVIDEMIX的框架天然适合这种扩展,因为GMM给出的后验概率本身就是一个0到1之间的连续值。
5.3 简单可行的起步方案
如果你不想一上来就实现完整双网络版本,可以先做一个简化版:
- 单网络,加上GMM损失划分
- 固定用每10轮重划一次
- 只用同一个网络对噪声样本做伪标签
- 无监督损失用最朴素的交叉熵一致性
这个简化版在噪声率不高于0.3时已经能比普通训练好不少,而且代码量极少。跑通之后再加双网络、再加MixUp、再加锐化调节,逐步升级成完整版。对我个人来说,这个“从简化到完整”的路径比一次性照搬论文要轻松得多,也更容易理解每一步到底给模型带来了什么。
最后再分享一个小技巧:训练过程中把每个样本的GMM干净概率可视化出来,我习惯每隔几十个epoch把概率按从低到高排个序,抽查低概率样本。你会发现模型判为噪声的样本里,有一部分确实是标签错误,但也有一部分其实是边界样本、模糊样本。理解到这一点,你就真正明白了为什么“丢标签不丢样本”是一个合理的策略——错误标签和模糊样本是两回事,前者需要忽略,后者恰恰是需要更多无监督信号的宝藏。