☰
DINO无标签蒸馏:从知识蒸馏到自监督学习的范式革新
2026/10/21 9:18:07 网站建设 项目流程

DINO,全称DIstillation with NO labels——蒸馏,但没有标签。我第一次看到这个名字的时候愣了一下:知识蒸馏不是必须有老师、学生、还有正确答案吗?DINO告诉你,老师可以有,学生可以学,唯独那个“正确答案”可以完全不要。它在自监督学习上加了一层蒸馏的壳,把传统教师-学生框架里的软标签,换成了教师网络对同一物体不同视角输出的特征分配。做自监督的同学绕不开它,做知识蒸馏的同学也绕不开它。这篇文章我会从蒸馏视角拆解DINO的核心机制、防崩塌设计、复现配置,以及我实际跑下来遇到的一堆坑,适合想真正把DINO跑起来、或者在无标签数据上做蒸馏研究的同学。

1. 从“考完试再教学生”到“两个盲人互相搀扶”:DINO重新定义了蒸馏

1.1 老规矩:知识蒸馏为什么天生绑定了标签

传统知识蒸馏的标准流程,我习惯类比成“老教师带实习生”。大模型老师在带标签的数据上训练得足够好,然后用它的softmax输出当软标签,小模型学生不仅要看硬标签(正确答案),还要模仿老师给出的概率分布。因为老师知道“这只猫更像狗而不是卡车”,这个信息比one-hot更丰富。所以传统KD本质上迁移的是老师已经形成的“判断习惯”,而判断习惯的形成必须建立在海量人工标注之上。

这个框架有三个默认前提:第一,老师要足够好,好老师才有值得学的判断习惯;第二,老师的输出必须对应真实类别,所以必须有标签空间来约束;第三,蒸馏完成后,学生继承的是老师的“结论”,而不是理解世界的方式。这三个前提放在今天的大模型场景里越来越尴尬——互联网上无标签数据无穷无尽,有标签数据却昂贵到劝退。如果蒸馏只能在有标签的“考试制度”下运行,那绝大部分数据就用不上。

1.2 把考场掀了:DINO对蒸馏做的三次反叛

DINO等于把考场直接掀了,它做了三件在传统KD看来“违背祖训”的事情。

第一,不训练老师。传统KD里老师是一个已经收敛好的模型,DINO里老师参数压根不是单独训出来的,而是学生网络自身参数的指数滑动平均(EMA)。换句话说,老师没有自己的“人生”,它是学生过去状态的慢速快照,它和学生一起成长。

第二,不要标签。传统KD里学生有两个监督信号,一个是硬标签的交叉熵,一个是软标签的熵匹配。DINO里两个都没有。学生要做的只有一件事:让“自己对这张图某个视角的输出”,去匹配“老师对这张图另一个视角的输出”。

第三,不蒸馏类别,蒸馏特征分配。传统KD里老师输出的softmax每个维度代表一个真实类别;DINO里输出的每个维度不代表任何真实类别,它更像是模型对这个样本“在语义空间中属于哪一块区域”的软性判断。所以更准确的说法是:DINO把知识蒸馏里的soft label换成了soft assignment,迁移的不是答案,而是视角一致性。

1.3 那么它到底解决了什么痛点

一句话总结DINO解决的核心矛盾:在没有人工标注的前提下,如何让一个视觉模型学会“理解画面里有什么、边界在哪、哪些区域属于同一物体”。

传统无监督方法要么靠重建(像素级任务,容易学到纹理但学不到语义),要么靠对比学习(需要精心设计正负样本,且负样本的规模直接影响效果)。DINO给出的方案是“无标签蒸馏”:让两个不同视角的模型互相校正语义理解,既不重建像素,也不显式对比负样本。结果反而在特征质量和可迁移性上超过了相当一部分有监督预训练,而且自监督训练出来的attention map天然带有语义分割能力。这种范式更新不仅对自监督有影响,对知识蒸馏本身也是个重要提示——蒸馏不一定需要“权威知识源”,两个不完美的视角也能蒸馏出有用的结构。

2. 拆解DINO防崩塌的四个核心机制

自监督模型最怕的事叫崩塌(collapse):网络学成一个恒等映射,任何图片进去都输出同一个向量,loss看着在降,表征一点没用。DINO能稳住,靠的是四个机制互相牵制。我把每个都拆开讲,其中最关键的是第二组。

2.1 慢半拍的老师:momentum teacher

DINO的老师不是独立训练的模型,而是学生的EMA。每一轮更新时,老师参数按这个公式滑动:

teacher_params = m * teacher_params + (1 - m) * student_params

其中m是动量系数,训练初期设为0.996,随训练进程按cosine schedule逐渐增大到1.0。m越接近1,老师更新越慢,目标函数越稳定。

为什么必须这么做?如果老师和学生都是正常梯度更新,两者会互相追逐,模型很容易走捷径——学生稍微给个错误信号,老师立刻跟着错,最后一起学到一个无信息的常数解。有一类自监督方法确实用“两个网络互相看”的思路,但都依赖stop-gradient或者额外的正则来防止奔跑式崩塌。momentum本质上是一种时序平滑:老师总是落后学生半个身位,学得慢但方向更稳定。这就像长跑时跟一个配速稳定的领跑员,你不会跑崩,但你能超过他。

实操上要注意:EMA动量不是固定的,官方设置是0.996到1.0的cosine上升。我试过全程固定0.99,小规模数据上看起来问题不大,但换到稍大的数据集,训练后期attention map稳定性明显不如cosine schedule版本。

2.2 一对互相拉扯的杠杆:centering和temperature

这是DINO精髓中的精髓。老师输出的logits在进softmax之前,要做两步处理:

teacher_logits = (teacher_logits - center) / teacher_temperature

第一步是减center。center是教师logits在batch维度上的指数滑动平均,动量通常取0.9。它的作用是把输出分布的均值拉回原点,防止某个维度一直占主导。如果没有center,softmax很容易被少数几个维度带跑,模型会倾向于把所有东西都判成同一个方向,这就是崩塌。

第二步是除temperature。老师温度设置得很小,默认0.04,学生温度是0.1。temperature越小,softmax分布越尖锐,模型越“敢下结论”。它的作用正好和center相反——center在把分布往均匀方向推,temperature在把分布往尖锐方向拉。

这两股力必须同时存在:只有center没有temperature,输出会被抹平成均匀分布,熵太大,学生学不到任何区分度;只有temperature没有center,输出会快速收敛成one-hot式的确定性,熵太小,模型开始走极端。两个杠杆一拉一推,把老师输出的熵稳定在“既不过于均匀、也不过于武断”的中间区间。实操中如果发现训练崩溃,第一件事就是检查center动量、teacher temperature和student temperature三者是否在一个合理组合里。

2.3 multi-crop:用大小视野制造“看到同一件事”的约束

DINO训练时的数据增强不是单纯的两张裁剪,而是multi-crop策略:取两个224分辨率的全局裁剪,同时取8个96分辨率的局部裁剪。老师网络只吃两个全局裁剪,学生网络把全局裁剪和局部裁剪全部吃进去。

损失计算时,学生每个局部裁剪的输出,都要去匹配老师对同一张图全局裁剪的输出。也就是说,模型被要求“只看一小块,也能说出和看全图一致的话”。这个约束非常强:局部没有全局的分辨率,但如果两者语义一致,网络就必须在特征层面编码“部分属于整体”的关系。局部裁剪负责带来细粒度的纹理和边界信息,全局裁剪负责把语义固定在上下文里。

multi-crop另一层作用是间接扩充了样本多样性。对比学习方法对batch size极其敏感,因为负样本池越大效果越好,而DINO的监督信号不依赖负样本,多裁剪等于在单张图上制造了多个角度的语义对应,所以它对batch size的宽容度比SimCLR那一类高很多。当然,也不能小到离谱。

2.4 为什么可以不要负样本

对比学习方法里,负样本是必不可少的“反面教材”。SimCLR、MoCo花了大量精力维护负样本队列或大batch,就是为了让模型知道“哪些东西不属于一起”。DINO没有显式负样本,但不代表没有做均匀性约束。

这里要理解一个关键点:DINO的teacher输出是softmax,softmax本身就自带竞争性。学生要在多个维度上分配概率,某个维度概率高了,其他维度自然会被压低。再加上centering在持续做“均值回拉”,模型整体输出分布不会坍缩到少数维度。当这两个机制配合起来,无负样本也能维持一个足够有区分度的表征。这跟BYOL的思路有相似之处:用不对称的更新机制+额外正则,替代显式负样本的排斥力。

我个人的理解是,DINO本质上把“均匀性”从对比学习的样本层搬到了聚类分布层。前者需要大量样本统计来保证均匀,后者只需要在特征分配上做正则。从这个角度看,DINO的设计比对比学习更优雅,但对超参数也更敏感,动一发牵全身。

3. 蒸馏的对象到底是什么:从soft label到soft assignment

3.1 和图像分类长得一样,但千个维度不代表千个类别

很多第一次看DINO代码的人会困惑:teacher输出不就是一个带softmax的向量吗?和图像分类网络最后一层有什么区别?

区别在于这个向量没有标签语义。图像分类网络softmax的第5维对应“猫”,第18维对应“狗”,这层对应关系是人工标注定义的。DINO的softmax维度是在训练中自动涌现的“隐式聚类”,每个维度更像是一个无名的语义槽位,模型自己决定哪些样本共享一个槽位。今天这1024个槽位可能恰好对应了某种动物、某种材质、某种场景结构,明天换个数据集,槽位含义就可能重新洗牌。

因此学生蒸馏的对象不是“老师认为这是什么”,而是“老师在表征空间里如何分配这一组语义槽”。这个分配受温度影响、受center影响、受视角影响,但它本质上是一个软性的、连续的语义描述。传统KD用KL散度或交叉熵让学生匹配老师的类别置信度,DINO用同样的交叉熵让学生匹配老师的特征分配。

3.2 蒸馏产出的“附带福利”:attention map变成语义分割图

DINO最出圈的成果不在精度数字,而是一张图:用ViT的[CLS] token对各个patch的attention可视化,模型竟然长出了清晰的语义边界。一个自监督模型,没有见过任何语义分割的标签,却能在一张图里把前景、背景、甚至同一类物体的各个实例都分开。

我当时第一次跑出这个可视化的时候确实被震住了。操作很简单:预训练完,取出最后一层(或倒数几层)的[CLS] token对所有patch token的attention权重,多个head取平均,reshape成特征图大小,再插值回原图。你会看到attention的热区几乎沿着物体边缘走,猫的轮廓、人的轮廓、远处的建筑都会被分得清清楚楚。

这和multi-crop强化局部一致性直接相关:局部裁剪看的是物体的一小块,它必须匹配全局语义分类,那么网络就只能把语义绑定到边界清晰的区域上,而不是糊成一团整体特征。如果你想快速验证一个自监督模型有没有学到结构,不要只看精度,直接抽attention map,这是最直观的体检。

3.3 怎么给蒸馏结果打分:kNN和linear probe

自监督模型的评估不靠loss,靠特征质量。最常用的两个手段:

kNN评估:在训练集上抽一批特征存成库,每个测试样本在库中找最近邻的K个样本,K个样本投票得到预测标签。它几乎不引入额外监督,适合复现阶段做“健康度体检”。如果kNN精度上去了,说明特征有真实的语义区分度。DINO 预训练的ViT-S/16在ImageNet上kNN精度大约在65%到68%区间,不同memory bank规模会有几个点浮动。

线性评估(linear probe):把backbone完全冻结,只训练一个全连接分类头。这时候模型只调整一层线性层,本质是在测试特征的可线性分离程度。DINO ViT-S/16的linear probe能到77%左右,作为对比,不少有监督预训练在同样规格下也就82%上下。两者差距已经很小,考虑到DINO完全没用标签,这个性价比相当高。

我建议复现时同时跟踪kNN和attention map,前者给量化指标,后者给定性判断。只盯loss非常容易产生“假收敛”的错觉。

4. 复现DINO:从配置到训练全程记录

4.1 环境准备与硬件底线

先讲环境。DINO是基于PyTorch的,依赖torchvision、CUDA等标准库,官方开源仓库用了一整套自带的Kevin Clark的数据增强流程,需要原样保留。重点提醒:数据增强里有不少是为了multi-crop量身订制的随机裁剪、颜色抖动、高斯模糊、多分辨率缩放,不要手滑简化掉。我见过有人为了省事把局部裁剪去掉,只留全局裁剪,结果训练出来的特征在线性评估上掉了十来个点。

硬件方面,官方300 epoch跑ImageNet通常是用8卡V100这个量级的配置。我自己复现时用的是4卡A100,跑ViT-S/16,batch size拉到256,在约150个epoch时已经能看到结构清晰的attention map。如果你只有单卡,也不是不能跑,但是把完整ImageNet跑完不现实。建议先拿一个几万规模的子集,把机制跑通,再决定是否上全量。

4.2 关键超参数配置表与选型理由

下面是我跑DINO时用的核心配置,基本对齐开源仓库默认值,不同torch版本之间可能有细微差异。

配置项推荐取值说明
backboneViT-S/16显存和效果平衡,ViT-B/16更强但要更多资源
batch size256不能太小,否则熵统计和EMA都不稳定
optimizerAdamW与ViT训练习惯一致
base learning rate0.0005 * batch/256官方按batch线性缩放
warmup epochs10必须保留,否则早期震荡严重
weight decay0.04ViT常用wd通常0.05,但DINO官方选择0.04
teacher temperature0.04越小输出越尖锐,不能随意调大
student temperature0.1学生端动态,但官方固定为0.1
center momentum0.9center更新用的EMA系数,需要明显大于teacher动量
teacher EMA momentum0.996 → 1.0按cosine schedule递增
global crops2个,224px老师和学生都吃
local crops8个,96px只给学生吃
head维度65536(或4096)输出特征维度,越大隐式聚类槽位越多

有一点特别容易被忽略:官方实现里会冻结backbone的最后一层参数(freeze_last_layer设为1)。原因是预训练阶段最后一层的表征还在震荡,如果让它跟着梯度更新,会对整个backbone产生不利扰动。复现时如果你发现早期训练loss抖动剧烈,先查这个开关有没有按默认开。

4.3 训练过程要盯的几个指标

DINO训练不是把训练脚本一挂了事,中途要做几个“体检”。我自己的习惯是每个epoch记录五个东西:训练loss、teacher输出熵、学生输出熵、kNN精度、以及几张固定图片的attention map。

teacher输出熵非常重要。如果熵一直很大、接近均匀分布,说明centering权重过大或者temperature太温和,模型还没开始形成聚类;如果熵掉得飞快、很快接近0,说明模型在走极端,下一步就是崩塌。健康的熵曲线是前20个epoch快速下降,之后缓慢走平,保持一个非零的稳定值。

核心训练循环的伪代码如下,方便理解整体流程:

# DINO核心训练循环(伪代码,参照官方实现思路) for images in dataloader: global_crops = images[0] # 2个224分辨率的全局裁剪 local_crops = images[1] # 8个96分辨率的局部裁剪 # 教师前向:不更新梯度 with torch.no_grad(): teacher_logits = teacher(global_crops) teacher_logits = teacher_logits / teacher_temp teacher_logits = teacher_logits - center teacher_probs = F.softmax(teacher_logits, dim=-1) # 学生前向:全局和局部都进 student_logits = student(torch.cat(global_crops + local_crops, dim=0)) student_probs = F.log_softmax(student_logits / student_temp, dim=-1) # 每个局部/全局裁剪,都要匹配教师对全局裁剪的输出 loss = 0.0 for i, g_idx in enumerate(global_crops_index): for l_idx in range(len(global_crops) + len(local_crops)): loss += -torch.sum( student_probs[l_idx] * teacher_probs[i] ) loss.backward() optimizer.step() # 更新教师EMA参数、更新center统计量 update_teacher(m) center = center * m_c + (1 - m_c) * teacher_logits.mean(dim=0)

注意这里的loss求和是把所有裁剪组成的batch和teacher的全局输出两两匹配,使用的形式等价于交叉熵。如果只看伪代码会觉得它很简单,难就难在它不是单纯让输出像,而是要在“像”的同时保持熵的平衡。

5. 我踩过的坑:DINO复现常见问题与排查清单

5.1 特征崩塌:loss正常但表征完全没用

最常见的失败模式是:训练过程中loss平稳下降,看起来非常健康,但kNN精度一直是个位数,attention map糊成一片。这种情况十有八九是模型悄悄塌缩,把所有图像映射到同一个方向。

我的排查顺序是:先看teacher熵,如果熵接近0,说明temperature太狠,尝试把teacher temperature调大到0.06到0.08;如果熵接近均匀分布,说明center的影响太大,把center的动量调低到0.8附近看看。然后看EMA momentum,m太快会让学生老师互相带偏,m太慢则学习目标更新过慢,卡在次优解。最后才动学习率,因为DINO对lr的敏感度低于对熵平衡的敏感度。

5.2 训练不稳定:loss陡增或warmup爆炸

loss陡增一般集中在训练前10个epoch。常见原因:warmup时间太短;base learning rate超过0.0005太多;或者freeze_last_layer没开。我把这三项全部检查一遍,大多数暴雷都在这。

另外,梯度裁剪不是DINO官方默认配置,但我在复现时发现,如果batch size比较小、训练偶尔出现尖峰loss,手动加一个max grad norm 3.0的梯度裁剪能有效抑制震荡,且基本不影响最终精度。

5.3 显存不够、训练时间太长怎么办

硬件不够时,第一反应往往是调小batch size,但在DINO里这是下策。batch太小,center统计和teacher的动量更新都变得极不稳定。更好的路线是:先减少local crop数量,比如从8个减到4个,这个操作对显存的释放最直接,对精度损伤相对可控;其次是混合精度训练,开启amp后显存能省接近一半;再不行才考虑降低全局裁剪分辨率,但伤害会大一些。

如果追求用更少的计算资源,可以把patch size从16改成更大的28,等于让模型看更粗粒度,ViT-S/28在同样epoch下显存和时长都大幅下降,表征质量会略低于patch16版本,但用来验证训练机制完全够。想跑通DINO又只有一块卡,我建议走这条路线而不是硬撑完整配置。

5.4 小数据或下游任务迁移时要注意什么

DINO非常依赖数据多样性,在几万样本的小数据集上直接端到端训练,效果会明显打折。我的经验是:小数据上宁可延长度量学习的时间,也不要在小数据集上从头预训练太久。如果只有一个小型业务数据集,优先考虑用ImageNet或更大规模直接预训练好的DINO权重,然后在自己的数据上做线性评估或者降学习率微调。

下游任务选型上,DINO的特征在分割和检测任务上尤其吃香,因为它强化了局部语义一致性。做图像分类时,线性probe的表现往往已经接近甚至超过全量微调,而且抗过拟合能力更好。做检测分割时,可以把它当作一个强大的backbone初始化,然后正常微调即可。

最后再分享一个个人体会:跑DINO最忌讳只盯着loss曲线判断训练好不好。我自己的项目里,loss在第30个epoch后几乎就是一条水平直线,看起来像“学完了”,但kNN精度和attention map在整个训练后半程都还在稳步改善。真正的健康度体检表,永远是特征本身——所以每次迭代都把kNN精度、teacher熵、attention map三件套打出来,比任何训练曲线都靠谱。这个习惯我一直留到现在,做其他自监督方法时同样管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询