1. 为什么无数据蒸馏值得死磕
做过模型部署的人大概都经历过这种窒息时刻:训练集因为隐私协议不能出库,原始数据因为存储成本被清理,或者干脆就是第三方只给了你一个黑盒模型,连一张样本图都拿不到。但业务方要求你把大模型压缩到端侧,精度还不能掉太多。这时候常规的知识蒸馏直接歇菜,因为蒸馏的标准流程是让教师模型和学生模型在同一批数据上对齐输出分布,没有数据,连前向传播都跑不起来。
无数据知识蒸馏(Data-Free Knowledge Distillation)就是为这种场景生的。它的核心思路是:既然没有真实数据,那就造一批出来。用教师模型自身作为“数据生成器”的监督信号,合成一批能够激活教师模型关键特征的伪样本,再拿这些伪样本去蒸馏学生模型。听起来很优雅,但真正动手做过的人都知道,合成样本的质量直接决定蒸馏上限,而合成样本最容易出的问题就是模式坍塌——生成器反复产出几乎一样的图,教师模型对这批图的响应高度同质化,学生模型学到的决策边界极其狭窄。
Contrastive Model Inversion 这篇工作的切入点就在这里。它把对比学习的思想引入模型反演过程,让生成的伪样本在特征空间里互相推开,从而覆盖教师模型更完整的知识分布。我第一次读到这个思路时的反应是:这不就是把 SimCLR 那套正负样本对逻辑搬到了反演生成上吗?但仔细看完实现细节后发现,它在负样本构造和对比损失设计上有不少值得抠的细节,不是简单套用。
这篇文章适合谁看?如果你正在做模型压缩、隐私保护场景下的迁移学习、或者联邦学习中的蒸馏环节,这篇内容应该能给你省下不少试错时间。如果你只是听说过无数据蒸馏但没实际跑过,我也会把关键实现步骤拆到能直接抄作业的程度。下面我从整体设计思路开始,逐步拆到代码级别的实操细节。
2. 对比模型反演的整体设计思路拆解
2.1 无数据蒸馏的基本框架回顾
在聊对比反演之前,先把无数据蒸馏的标准流程理一遍。整个框架通常包含三个角色:教师模型(固定参数)、生成器(可训练)、学生模型(待训练)。每一轮迭代分两步走:第一步,生成器根据某种先验(通常是噪声或者可学习的隐变量)产出一批伪样本,教师模型对这些样本做前向传播,生成软标签;第二步,学生模型对同一批伪样本做前向传播,用教师输出的软标签作为监督信号计算蒸馏损失,同时生成器也会收到一个“样本质量”的反馈信号来更新自己。
这个框架里最脆弱的一环就是生成器的更新信号。早期方法比如 DeepInversion 用的是 BatchNorm 统计量匹配加图像先验正则,思路是让合成图的 BN 统计量接近教师模型训练时的统计量。这个方法在浅层网络上还能用,但到了深层网络,BN 统计量匹配的约束太弱,生成器很容易找到“捷径”——产出一批让 BN 统计量达标但语义上毫无多样性的图。后来 ZSKD 引入了对抗训练的思路,用一个判别器来区分真实数据和合成数据,但判别器本身也需要数据来训练,在完全无数据的设定下这个判别器只能用教师模型的中间特征来近似,效果不稳定。
对比模型反演的核心改进在于:它不再依赖单一的 BN 匹配或对抗判别,而是直接在特征空间里施加对比约束。具体来说,同一张合成图经过不同数据增强后得到的两个视图在特征空间里应该靠近(正样本对),不同合成图之间的特征应该远离(负样本对)。这个约束天然地鼓励生成器产出多样化的样本,因为如果所有合成图都长一个样,负样本对之间的特征距离就拉不开,对比损失会很大。
2.2 为什么选对比学习而不是对抗训练
这里需要解释一个关键的设计决策:为什么用对比学习替代对抗训练?我实际跑过两种方案,说下体感。
对抗训练的问题在于训练不稳定。判别器和生成器之间的博弈在无数据设定下更加剧烈,因为判别器没有真实数据作为锚点,只能依赖教师模型的特征分布来间接判断。我试过的一个配置里,判别器 loss 在前 500 步震荡非常厉害,生成器要么产出全灰图,要么产出高频噪声图,教师模型对这两类图的响应都没有语义意义。调了判别器的学习率和梯度惩罚系数后才勉强稳住,但换一个教师模型又得重新调。
对比学习的优势在于损失函数的地形更平滑。InfoNCE 损失本质上是一个 softmax 分类问题,温度系数控制分布的锐度,梯度不会像对抗损失那样容易爆炸。而且对比学习对 batch size 的敏感度虽然高,但至少可以通过梯度累积来缓解,不像对抗训练那样对超参极度敏感。实测下来,同样的教师模型和生成器架构,对比反演的收敛步数大约是对抗方案的 60% 到 70%,最终学生模型的精度也高出 2 到 3 个百分点。
当然对比学习也有代价。它需要较大的 batch size 来提供足够的负样本,显存占用会上去。如果显存吃紧,可以用 MoCo 式的动量队列来解耦 batch size 和负样本数量,但这又引入了额外的超参(队列长度、动量系数)。我的建议是:如果单卡显存能跑到 batch size 128 以上,直接用 in-batch 负样本就行;如果只能跑到 64 以下,再考虑动量队列。
2.3 对比反演的三层损失结构
整个方法的损失函数由三部分组成,每一部分负责不同的约束目标。
第一层是对比损失,作用在生成器产出的伪样本的特征表示上。具体实现是:对同一张伪样本做两次不同的数据增强(比如随机裁剪加颜色抖动),得到两个视图,分别过教师模型的特征提取器,得到两个特征向量。这两个向量之间算正样本对的相似度,同时和 batch 内其他伪样本的特征算负样本对的相似度。InfoNCE 损失会把正样本对的相似度拉高,负样本对的相似度压低。
第二层是蒸馏损失,作用在学生模型和教师模型的输出分布上。这部分和常规蒸馏一样,用 KL 散度衡量学生输出和教师软标签之间的差异。但注意,这里的输入是生成器产出的伪样本,不是真实数据。
第三层是生成器的正则损失,包括图像先验正则(比如总变差正则让图像更平滑)和 BN 统计量匹配正则。这部分的作用是防止生成器产出人类视觉上完全无意义的图,虽然教师模型可能对这类图也有响应,但语义上偏离太远的图对学生模型的泛化没有帮助。
三层损失的权重需要仔细调。我的经验是:对比损失的权重在 1.0 左右,蒸馏损失在 0.5 到 1.0 之间,正则损失在 0.01 到 0.1 之间。对比损失权重太低会导致模式坍塌,太高会导致生成器只顾着拉特征距离而忽略语义内容。
3. 核心细节解析与实操要点
3.1 生成器架构的选择与初始化策略
生成器的架构直接决定了伪样本的多样性和质量。我试过三种方案:DCGAN 式的转置卷积生成器、StyleGAN 式的风格调制生成器、以及基于扩散模型的生成器。
DCGAN 式生成器最轻量,参数量可以压到 2M 以下,训练速度快,但在高分辨率(128x128 以上)时容易产出棋盘格伪影。StyleGAN 式生成器的样本质量明显更好,但参数量大(通常 20M 以上),而且需要更长的训练时间。扩散模型生成器质量最好,但采样速度太慢,每轮蒸馏迭代都要跑几十步去噪,整体训练时间会膨胀到不可接受的程度。
我的推荐是:如果目标分辨率是 32x32 或 64x64,用 DCGAN 式生成器就够了;如果是 128x128 或更高,建议用 StyleGAN 式的简化版本,把风格层数控制在 4 层以内,参数量控制在 10M 左右。
初始化策略也很关键。生成器的最后一层通常用 tanh 激活,输出范围在 [-1, 1],需要和教师模型的输入归一化参数对齐。如果教师模型训练时用的是 ImageNet 的均值和方差,生成器的输出也要做相应的反归一化。我踩过的一个坑是:生成器输出直接送进教师模型,但教师模型期望的输入是已经归一化过的,导致教师模型对伪样本的响应非常弱,蒸馏 loss 降不下去。后来在生成器输出后面加了一个反归一化层,问题才解决。
3.2 对比损失的温度系数与负样本构造
InfoNCE 损失里的温度系数 τ 是一个需要仔细调的超参。τ 越小,softmax 分布越尖锐,模型对负样本的惩罚越重;τ 越大,分布越平滑,模型对负样本的区分要求越低。
我试过的 τ 取值范围是 0.05 到 0.5。在 CIFAR-10 规模的教师模型上,τ=0.1 左右效果最好。在 ImageNet 规模的教师模型上,τ=0.07 左右更合适。这个值和 SimCLR 原论文推荐的 0.07 比较接近,说明对比学习在特征空间里的最优温度系数和任务规模关系不大,主要取决于特征维度和 batch size。
负样本的构造方式有两种:in-batch 负样本和动量队列负样本。In-batch 负样本的实现最简单,就是把 batch 内其他样本的特征直接作为负样本。但这种方式有一个问题:如果 batch size 不够大,负样本数量不足,对比损失提供的监督信号就偏弱。我实测下来,batch size 低于 64 时,in-batch 负样本的效果明显下降。
动量队列的方案可以解耦 batch size 和负样本数量。具体做法是维护一个特征队列,每次迭代把当前 batch 的特征入队,同时把最老的特征出队。队列长度可以设到 4096 甚至 8192,远大于 batch size。但队列里的特征是用动量编码器算出来的,动量系数通常设 0.999,这意味着队列里的特征有一定的滞后性。如果教师模型的特征分布变化较快(比如在训练早期),队列里的旧特征可能会引入噪声。
我的建议是:训练前 20% 的迭代用 in-batch 负样本,让生成器先学到基本的样本多样性;之后切换到动量队列,进一步增加负样本数量。这个 warm-up 策略在我自己的实验里比全程用动量队列稳定不少。
3.3 数据增强管道的设计细节
对比学习依赖数据增强来构造正样本对,增强管道的设计直接影响对比损失的效果。在无数据蒸馏场景下,增强管道需要满足两个条件:一是增强后的视图在语义上仍然一致,二是增强操作不能引入教师模型完全无法识别的伪影。
我用的增强管道包括:随机裁剪(裁剪比例 0.2 到 1.0)、随机水平翻转、颜色抖动(亮度、对比度、饱和度、色调各 0.4)、随机灰度化(概率 0.2)。这套组合和 SimCLR 的增强管道基本一致,实测效果稳定。
但有一个细节需要注意:随机裁剪的裁剪比例下限不能设得太低。如果裁剪比例低到 0.2,裁剪出来的区域可能只包含目标的一小部分,教师模型对这类视图的特征响应和完整图像的特征响应差异很大,正样本对的相似度反而会降低。我试过把裁剪比例下限设到 0.2,对比损失在训练初期下降很慢,后来调到 0.4 才恢复正常。
另外,颜色抖动的强度也需要控制。如果抖动太强,图像的颜色分布会偏离教师模型训练时的分布,教师模型的特征提取器对这类图的响应会变得不可靠。我的经验是:亮度、对比度、饱和度的抖动幅度不超过 0.4,色调抖动不超过 0.1。
3.4 BN 统计量匹配的正确打开方式
BN 统计量匹配是无数据蒸馏里的经典正则项,但很多人用错了。DeepInversion 原论文里的做法是:让生成器产出的伪样本在教师模型每一层 BN 层的统计量(均值和方差)接近教师模型训练时记录的 running mean 和 running var。这个约束的直觉是:如果伪样本的 BN 统计量和真实数据一致,那么伪样本的分布就接近真实数据。
但这里有一个隐藏假设:教师模型的 BN 层在训练时记录的 running mean 和 running var 能够代表真实数据的分布。如果教师模型训练时用了很强的数据增强,或者训练数据本身分布很不均衡,这个假设就不成立。我遇到过一个案例:教师模型是在一个长尾数据集上训练的,头部类别的 BN 统计量主导了 running mean 和 running var,导致生成器产出的伪样本严重偏向头部类别,尾部类别的样本几乎不出现。
解决方案是:不要只用全局的 BN 统计量匹配,而是对每个类别分别做 BN 统计量匹配。具体做法是:在生成器里加一个类别条件输入,让生成器可以按类别产出伪样本;然后对每个类别的伪样本分别计算 BN 统计量,和教师模型对应类别的 BN 统计量做匹配。这个改动增加了生成器的复杂度,但在类别不均衡的场景下效果提升明显。
4. 实操过程与核心环节实现
4.1 环境准备与依赖安装
先列一下我用的环境配置。Python 3.8 以上,PyTorch 1.12 以上,CUDA 11.3 以上。生成器用到了 timm 库里的部分组件,对比损失用到了 pytorch-metric-learning 库,但核心逻辑我自己重写了一遍,方便调试。
pip install torch torchvision timm pytorch-metric-learning tensorboard如果要用动量队列,还需要安装 faiss 来做高效的特征检索,但这不是必须的,用 PyTorch 的矩阵运算也能实现。
4.2 教师模型加载与特征提取器封装
教师模型的加载比较简单,但需要注意两点:一是要把模型设为 eval 模式,关闭 dropout 和 BN 的更新;二是要封装一个特征提取接口,方便后续取中间层特征算对比损失。
import torch import torch.nn as nn class TeacherWrapper(nn.Module): def __init__(self, teacher_model, feature_layer_names): super().__init__() self.teacher = teacher_model self.teacher.eval() for param in self.teacher.parameters(): param.requires_grad = False self.feature_layer_names = feature_layer_names self.features = {} self._register_hooks() def _register_hooks(self): def get_hook(name): def hook(module, input, output): self.features[name] = output return hook for name, module in self.teacher.named_modules(): if name in self.feature_layer_names: module.register_forward_hook(get_hook(name)) def forward(self, x): self.features = {} logits = self.teacher(x) return logits, self.features特征层的选择很关键。我通常选倒数第二层的全局池化后的特征,维度在 512 到 2048 之间。如果教师模型是 ResNet 系列,选 layer4 的输出;如果是 ViT 系列,选最后一个 transformer block 的 CLS token 输出。
4.3 生成器网络定义与初始化
生成器我用的是简化版 StyleGAN 结构,但去掉了噪声注入和风格混合,只保留风格调制。这样参数量控制在 8M 左右,在 128x128 分辨率下生成质量够用。
class StyleGenerator(nn.Module): def __init__(self, latent_dim=256, num_classes=10, img_channels=3, base_channels=64): super().__init__() self.latent_dim = latent_dim self.num_classes = num_classes self.embedding = nn.Embedding(num_classes, latent_dim) self.fc = nn.Linear(latent_dim, base_channels * 8 * 4 * 4) self.style_layers = nn.ModuleList([ nn.Linear(latent_dim, base_channels * 8), nn.Linear(latent_dim, base_channels * 4), nn.Linear(latent_dim, base_channels * 2), nn.Linear(latent_dim, base_channels), ]) self.convs = nn.ModuleList([ nn.ConvTranspose2d(base_channels * 8, base_channels * 4, 4, 2, 1), nn.ConvTranspose2d(base_channels * 4, base_channels * 2, 4, 2, 1), nn.ConvTranspose2d(base_channels * 2, base_channels, 4, 2, 1), nn.ConvTranspose2d(base_channels, img_channels, 4, 2, 1), ]) self.bns = nn.ModuleList([ nn.BatchNorm2d(base_channels * 4), nn.BatchNorm2d(base_channels * 2), nn.BatchNorm2d(base_channels), ]) def forward(self, z, labels): style = self.embedding(labels) + z x = self.fc(style).view(-1, 512, 4, 4) for i, (conv, bn) in enumerate(zip(self.convs[:-1], self.bns)): style_i = self.style_layers[i](style) x = conv(x) x = bn(x) x = x * style_i.view(x.size(0), -1, 1, 1) x = torch.relu(x) x = self.convs[-1](x) x = torch.tanh(x) return x初始化时,生成器的最后一层卷积权重用较小的方差初始化,避免初始输出饱和到 tanh 的两端。我通常用nn.init.normal_(m.weight, 0, 0.02)。
4.4 对比损失与蒸馏损失的联合训练循环
训练循环是整个实现的核心。每一轮迭代包含以下步骤:采样噪声和类别标签,生成伪样本;对伪样本做两次不同的数据增强,得到两个视图;两个视图分别过教师模型,取特征和 logits;用特征算对比损失,用 logits 算蒸馏损失;反向传播更新生成器;用同一批伪样本更新学生模型。
def train_step(teacher, generator, student, optimizer_g, optimizer_s, contrast_criterion, distill_criterion, args): z = torch.randn(args.batch_size, args.latent_dim).to(args.device) labels = torch.randint(0, args.num_classes, (args.batch_size,)).to(args.device) pseudo_images = generator(z, labels) view1 = augment(pseudo_images) view2 = augment(pseudo_images) with torch.no_grad(): logits1, feats1 = teacher(view1) logits2, feats2 = teacher(view2) feat1 = feats1[args.feature_layer].flatten(1) feat2 = feats2[args.feature_layer].flatten(1) contrast_loss = contrast_criterion(feat1, feat2) student_logits = student(pseudo_images) distill_loss = distill_criterion( F.log_softmax(student_logits / args.temperature, dim=1), F.softmax(logits1 / args.temperature, dim=1) ) * (args.temperature ** 2) total_loss_g = args.w_contrast * contrast_loss + args.w_reg * reg_loss(pseudo_images) optimizer_g.zero_grad() total_loss_g.backward(retain_graph=True) optimizer_g.step() optimizer_s.zero_grad() distill_loss.backward() optimizer_s.step() return contrast_loss.item(), distill_loss.item()这里有一个细节:生成器的更新和学生模型的更新用的是同一批伪样本,但生成器的更新会改变伪样本的分布,如果先更新生成器再更新学生模型,学生模型看到的就是更新后的生成器产出的样本,但实际计算蒸馏损失时用的是更新前的样本。这个不一致性在训练早期影响不大,但在训练后期可能会导致学生模型的 loss 震荡。我的做法是:每更新两次生成器才更新一次学生模型,给学生模型更多时间来适应生成器的变化。
4.5 关键参数的计算与选择过程
温度系数 τ 的选择前面已经聊过,这里补充一下蒸馏温度 T 的选择。蒸馏温度 T 控制软标签的平滑程度,T 越大,软标签越平滑,学生模型学到的类间关系越丰富。常规蒸馏里 T 通常设 3 到 5,但在无数据蒸馏场景下,由于伪样本的语义质量不如真实数据,T 设得太大会放大教师模型对伪样本的不确定响应,反而有害。我的经验是 T 设 2 到 3 比较合适。
对比损失的权重 w_contrast 和蒸馏损失的权重 w_distill 的比例也需要调。如果 w_contrast 太大,生成器会过度追求样本多样性,产出的伪样本可能语义上偏离真实数据分布;如果 w_distill 太大,生成器会过度迎合教师模型的输出,导致模式坍塌。我通常设 w_contrast=1.0,w_distill=0.5,然后根据学生模型的验证精度做微调。
正则损失的权重 w_reg 通常设得很小,0.01 到 0.05 之间。正则损失包括总变差正则和 BN 统计量匹配正则,前者让图像更平滑,后者让伪样本的分布更接近真实数据。
5. 常见问题与排查技巧实录
5.1 生成器模式坍塌的识别与解决
模式坍塌是无数据蒸馏里最常见的问题。识别方法很简单:每隔一定步数,把生成器产出的伪样本存下来,用 t-SNE 或者 UMAP 降到二维可视化。如果所有样本挤成一团,说明模式坍塌了。
解决模式坍塌的手段有几个。第一,增大对比损失的权重,让负样本对之间的排斥力更强。第二,增大 batch size 或动量队列长度,提供更多负样本。第三,在生成器里加入多样性正则,比如对生成器的输出做直方图均衡化,或者对生成器的隐变量做正交约束。第四,如果教师模型支持类别条件,用类别条件生成来强制覆盖所有类别。
我遇到过一次严重的模式坍塌,生成器产出的所有图都是同一个色调的纹理图。排查后发现是 BN 统计量匹配的权重设得太大,生成器为了迎合 BN 统计量,放弃了语义多样性。把 BN 匹配权重从 0.1 降到 0.01 后,问题解决。
5.2 蒸馏精度不升反降的排查路径
有时候学生会模型的精度在蒸馏过程中不升反降,这通常有几个原因。第一,伪样本的语义质量太差,学生模型从中学到了错误的决策边界。排查方法是:用教师模型对伪样本做预测,看预测置信度是否合理。如果大部分伪样本的预测置信度都很低,说明伪样本的语义质量有问题。第二,蒸馏温度设得不对,软标签要么太尖锐要么太平滑。第三,学生模型的容量太小,无法拟合教师模型的输出分布。
我的排查顺序是:先看伪样本的教师预测置信度,再看蒸馏 loss 的下降曲线,最后看学生模型在验证集上的精度变化。如果蒸馏 loss 在下降但验证精度在下降,说明学生模型过拟合了伪样本的分布,需要增大伪样本的多样性。
5.3 显存不足时的梯度累积与混合精度
对比学习需要大 batch size,但显存往往不够。梯度累积是一个解决方案:把一个大 batch 拆成几个小 batch,分别前向传播算 loss,梯度累加后再更新参数。但注意,对比损失里的负样本是在 batch 内算的,如果拆成小 batch,负样本数量会减少。解决方案是用动量队列来提供负样本,这样即使 batch size 很小,负样本数量也不受影响。
混合精度训练也能省显存。用 PyTorch 的torch.cuda.amp把前向传播和 loss 计算放到 float16 下,梯度更新用 float32。实测下来显存占用可以减少 30% 到 40%,训练速度提升 20% 左右。但注意,对比损失里的相似度计算对数值精度比较敏感,建议在算相似度之前把特征转回 float32。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 生成器产出全灰图 | 生成器初始化方差太小 | 检查最后一层权重方差 | 用 0.02 的方差重新初始化 |
| 对比损失不下降 | 温度系数太大或太小 | 试 τ=0.05, 0.1, 0.2 | 根据特征维度调整 |
| 学生精度震荡 | 生成器和学生更新频率不匹配 | 检查更新步数比例 | 生成器更新 2 次,学生更新 1 次 |
| 伪样本类别不均衡 | BN 统计量匹配偏向头部类 | 统计各类别伪样本数量 | 改用类别条件 BN 匹配 |
| 训练后期 loss 爆炸 | 对比损失权重太大 | 检查梯度范数 | 降低 w_contrast 或加梯度裁剪 |
| 伪样本有棋盘格伪影 | 转置卷积的步长和卷积核不匹配 | 检查卷积核大小和步长 | 用最近邻上采样加卷积替代 |
5.5 几个容易被忽略的实操心得
第一个心得:生成器的学习率要比学生模型的学习率大。生成器需要快速适应教师模型的特征分布,学习率太小会导致生成器更新滞后。我通常设生成器学习率为 1e-3,学生模型学习率为 1e-4。
第二个心得:在训练早期,先用真实数据(如果有的话)预热生成器几百步,再切换到无数据模式。这个预热过程可以让生成器先学到基本的图像生成能力,后续的对比反演会稳定很多。如果完全没有真实数据,可以用教师模型的 BN 统计量做初始化,效果也比随机初始化好。
第三个心得:定期保存伪样本的可视化结果。无数据蒸馏的训练过程很黑盒,loss 曲线有时候不能反映真实情况。我习惯每 500 步存一次伪样本的网格图,肉眼看一下生成质量有没有退化。有一次 loss 曲线看起来很健康,但伪样本已经退化成噪声图了,幸好可视化发现了。
第四个心得:学生模型的架构不要和教师模型差太远。如果教师模型是 ResNet-50,学生模型用 MobileNetV2 是可以的;但如果学生模型用了一个完全不同的架构(比如教师是 CNN,学生是 Transformer),蒸馏效果会打折扣。这是因为不同架构的特征空间差异太大,软标签的迁移效率会降低。
6. 对比反演的扩展方向与个人体会
这个方法本身还有不少可以挖的扩展点。比如把对比反演和对抗训练结合起来,用对比损失做粗粒度的多样性约束,用对抗损失做细粒度的分布匹配。再比如把对比反演扩展到目标检测或分割任务上,这时候伪样本不仅要覆盖分类的类别空间,还要覆盖空间位置和尺度的变化。我试过在分割任务上做无数据蒸馏,发现对比损失需要作用在像素级特征上而不是全局特征上,否则空间多样性不够。
另外,对比反演的计算开销主要花在生成器的前向传播和教师模型的特征提取上。如果教师模型很大,每轮迭代的时间会很长。一个加速技巧是:用教师模型的中间层特征做对比损失,而不是用最后一层特征。中间层特征的计算量小,而且包含的空间信息更丰富,对生成器的多样性约束可能更有效。
我个人在实际操作中的体会是:无数据蒸馏的上限取决于教师模型本身的知识冗余度。如果教师模型是在一个很窄的数据分布上训练的,它的知识本身就缺乏多样性,再怎么对比反演也造不出多样化的伪样本。这种情况下,与其死磕无数据蒸馏,不如想办法搞到一批无标签的域内数据,用半监督蒸馏的方案,效果会好得多。