4、防御蒸馏技术:蒸馏的基本原理、防御蒸馏的实现步骤、防御蒸馏的优缺点分析

4.1 蒸馏的基本原理

说到蒸馏,很多人第一反应是「这不是模型压缩用的吗?」没错,传统蒸馏确实是为了把大模型变小。但防御蒸馏,说白了就是利用蒸馏过程,把模型决策边界给「磨平」了。

我刚开始接触这个思路时,也觉得挺反直觉的。你想想看,对抗攻击之所以能成功,是因为攻击者找到了决策边界附近那些「敏感区域」。模型在这些区域里,稍微改一个像素,输出就翻天了。

蒸馏的核心思想,就是让模型学会「软标签」。什么意思呢?

  • 硬标签:猫就是猫,狗就是狗,概率直接给1或0
  • 软标签:猫的概率0.8,狗的概率0.15,狐狸的概率0.05

软标签里藏着教师模型的「知识」。比如一张图看起来像猫又有点像狐狸,教师模型会给出一个平滑的概率分布。学生模型学这个分布,决策边界自然就平滑了。

关键点:防御蒸馏不是让学生模型学「正确答案」,而是学「教师模型的思考过程」。这个思考过程里包含了对抗样本的「免疫信息」。

我在项目中遇到过一件事。有个团队直接拿硬标签训练防御模型,结果对抗攻击一来,准确率直接掉到30%。后来改用软标签蒸馏,同样的攻击,准确率还能维持在85%以上。差距就这么大。

4.2 防御蒸馏的实现步骤

防御蒸馏的实现,其实就三步。嗯,听起来简单,但每一步都有坑。

步骤一:训练教师模型

先训练一个高精度的教师模型。这个模型可以很大,也可以很复杂。我建议用ResNet-152或者更深的网络,因为教师模型越强,蒸馏出来的学生模型防御效果越好。

# 教师模型训练示例
import torch
import torch.nn as nn
import torch.optim as optim

teacher_model = ResNet152(pretrained=True)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(teacher_model.parameters(), lr=0.001)

for epoch in range(100):
    for inputs, labels in dataloader:
        outputs = teacher_model(inputs)
        loss = criterion(outputs, labels)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

我的经验:教师模型不要用对抗训练过的。为什么?因为对抗训练会让模型决策边界变得「扭曲」,蒸馏出来的学生模型反而学不到平滑的边界。我踩过这个坑,后来改用普通训练的教师模型,效果反而更好。

步骤二:生成软标签

这一步是核心。用教师模型对训练数据做推理,但不要取argmax,而是取softmax后的概率分布。这里有个超参数叫「温度T」。

def generate_soft_labels(teacher_model, dataloader, temperature=5.0):
    soft_labels = []
    teacher_model.eval()
    
    with torch.no_grad():
        for inputs, _ in dataloader:
            outputs = teacher_model(inputs)
            # 温度缩放后的softmax
            soft_outputs = torch.softmax(outputs / temperature, dim=1)
            soft_labels.append(soft_outputs)
    
    return torch.cat(soft_labels, dim=0)

温度T越大,软标签越平滑。T=1时就是普通softmax,T=10时几乎变成均匀分布。我一般取T=5,这个值在大多数任务上表现都不错。

注意:温度太高,软标签就失去了信息量,学生模型学不到东西。温度太低,软标签又接近硬标签,防御效果大打折扣。我曾经试过T=20,结果学生模型准确率直接掉到50%以下。

步骤三:训练学生模型

学生模型用软标签来训练。损失函数不再是交叉熵,而是KL散度。KL散度衡量的是两个概率分布之间的差异。

def distillation_loss(student_outputs, teacher_soft_labels, temperature):
    # 学生输出也要做温度缩放
    student_soft = torch.softmax(student_outputs / temperature, dim=1)
    # KL散度损失
    loss = nn.KLDivLoss(reduction='batchmean')(
        torch.log(student_soft), teacher_soft_labels
    )
    return loss * (temperature ** 2)

为什么要乘以温度平方?这是为了平衡梯度的大小。如果不乘,温度越高梯度越小,模型根本学不动。

学生模型的结构可以比教师模型小很多。我习惯用MobileNet或者ShuffleNet,推理速度快,部署也方便。

4.3 防御蒸馏的优缺点分析

维度 优点 缺点
防御效果 对白盒攻击有较好防御 对黑盒攻击防御有限
计算成本 只需训练一次教师模型 需要额外蒸馏步骤
模型大小 学生模型可以很小 教师模型需要较大
部署难度 推理时无额外开销 需要维护两个模型
通用性 适用于多种模型架构 对强攻击效果下降明显

优点详解

第一,防御效果好。特别是对基于梯度的白盒攻击,比如FGSM、PGD。因为学生模型的决策边界平滑,攻击者很难找到有效的梯度方向。

第二,推理速度快。学生模型可以比教师模型小10倍甚至100倍。我在一个边缘设备项目里,用蒸馏后的MobileNet替换了ResNet-50,推理速度提升了8倍,防御效果只下降了3%。

第三,无需对抗样本。不像对抗训练需要不断生成对抗样本,蒸馏只需要一次教师模型推理。训练流程简单很多。

缺点详解

第一,对黑盒攻击防御有限。黑盒攻击不依赖梯度,而是靠查询或者迁移。蒸馏平滑了决策边界,但无法改变模型的「本质弱点」。我曾经测试过,蒸馏模型对基于迁移的黑盒攻击,防御效果只比普通模型好10%左右。

第二,温度调参麻烦。温度T对效果影响很大,而且不同数据集、不同模型的最佳温度都不一样。我建议用验证集做网格搜索,T从1到10每隔0.5试一次。

第三,容易被自适应攻击绕过。如果攻击者知道你在用蒸馏防御,他可以针对蒸馏模型设计攻击。比如先训练一个替代模型,再用替代模型的梯度来攻击。这种自适应攻击下,蒸馏的防御效果会大幅下降。

我的建议:防御蒸馏不要单独使用。我一般把它和对抗训练结合起来:先用对抗训练训练教师模型,再用蒸馏训练学生模型。这样既能享受蒸馏的平滑边界,又能获得对抗训练的鲁棒性。当然,训练时间会翻倍,但效果确实好。

避坑指南

我曾经犯过一个低级错误。蒸馏时把教师模型和学生模型用同一个数据集训练,结果学生模型过拟合了教师模型的「错误」。后来我改用不同的数据增强策略,教师模型用强增强,学生模型用弱增强,效果好了很多。

还有一个坑:蒸馏后的模型对「干净样本」的准确率可能会下降。我遇到过下降5%的情况。解决办法是在损失函数里加入一小部分硬标签损失,让学生模型既学软标签,也学硬标签。

def combined_loss(student_outputs, teacher_soft_labels, hard_labels, temperature, alpha=0.7):
    # 蒸馏损失
    distill_loss = distillation_loss(student_outputs, teacher_soft_labels, temperature)
    # 硬标签损失
    hard_loss = nn.CrossEntropyLoss()(student_outputs, hard_labels)
    # 加权组合
    return alpha * distill_loss + (1 - alpha) * hard_loss

alpha取0.7到0.9之间比较合适。alpha太大,硬标签学不好;alpha太小,防御效果差。

嗯,防御蒸馏就讲到这里。说白了,它不是一个「万能药」,但在很多场景下确实好用。特别是当你需要部署一个小模型,又希望它有一定的抗攻击能力时,蒸馏是个不错的选择。