关键词
| 术语 | 英文 | 核心概念 |
|---|---|---|
| 对抗训练 | Adversarial Training | 通过对抗样本增强模型鲁棒性 |
| min-max优化 | Min-Max Optimization | 对抗训练的数学框架 |
| PGD对抗训练 | PGD Adversarial Training | 投影梯度下降对抗训练 |
| 鲁棒性 | Robustness | 模型对扰动的不变性 |
| 认证防御 | Certified Defense | 有理论保证的防御方法 |
| 随机平滑 | Randomized Smoothing | 基于随机化的认证方法 |
| 防御蒸馏 | Defensive Distillation | 降低梯度敏感性的训练方法 |
| 认证边界 | Certified Bound | 鲁棒性的理论保证 |
| 对抗正则化 | Adversarial Regularization | 对抗样本作为正则化项 |
| 迁移攻击 | Transfer Attack | 利用替代模型生成对抗样本 |
1. 对抗训练入门:为什么要让模型学会”受攻击”?
1.1 从一个生活例子说起
你有没有遇到过这种情况:考试前背了很多题目,结果换了个问法就不会了?神经网络也有这个毛病,而且更严重——它可能被人精心设计的”陷阱题”骗得团团转。
对抗训练干的,就是让模型提前见识各种”陷阱题”,练出一副火眼金睛。
说白了,对抗训练就是给模型打预防针。想象一下:
- 普通训练:让模型做课本习题
- 对抗训练:让模型同时做课本习题和变态难题
经过这种魔鬼训练的模型,遇到真正的攻击时就不容易慌了。
1.2 对抗样本是什么”妖怪”?
对抗样本(Adversarial Example)是Ian Goodfellow在2014年提出的概念。简单说,就是对原始输入加上人类几乎看不出来的微小扰动,就能让模型做出完全错误的判断。
举个形象的比方:这就像是在一幅世界名画上滴了一滴几乎看不见的墨水,结果一个没见过的观众会把蒙娜丽莎认成一只狗。人类绝对不会认错,但神经网络偏偏就会。
这种脆弱性可不是闹着玩的。想象一下:
- 自动驾驶汽车:因为路上一个小小的贴纸就闯红灯
- 人脸识别系统:戴个特殊眼镜就能冒充别人
- 垃圾邮件检测:加几个不可见字符就能绕过检测
所以,研究人员才拼命想办法让模型变”皮实”一点,对抗训练就是目前最有效的那招。
1.3 对抗训练的基本思路
对抗训练的核心思想用四个字概括就是”以毒攻毒”。具体来说:
- 先用攻击算法生成对抗样本
- 把对抗样本和原始样本混在一起训练
- 让模型学会识别和抵御这些”陷阱”
这就好比练武之人要想防身,光练套路不行,还得有人陪练、有人攻击,才能练出真正的反应能力。
2. 对抗训练原理:min-max优化框架
2.1 从直觉到数学
对抗训练的核心是将鲁棒优化问题形式化为一个双人零和博弈的 min-max 优化:
其中:
- :模型参数
- :训练数据分布
- :损失函数(如交叉熵)
- :扰动预算
- :内层最大化问题(攻击者)
- :外层最小化问题(防御者)
这个公式看起来吓人,但其实很好理解:
- 内层最大化:攻击者想尽办法找到最强的对抗扰动,让模型损失最大
- 外层最小化:防御者(也就是模型)通过训练,尽量减小在最坏情况下的损失
你可以把这个过程想象成:
- 内层:攻击者在不断升级武器(找到更强的攻击方法)
- 外层:防御者在不断加固城墙(训练出更鲁棒的模型)
两者你来我往,攻防对抗,这就是”对抗”这个词的真正含义。
2.2 鲁棒 vs 准确:训练权衡
对抗训练面临一个重要的权衡问题:鲁棒性和标准准确率之间存在张力。
标准训练: 优化干净数据上的准确率
对抗训练: 优化对抗数据上的准确率
理论分析(Wong & Kolter, 2018):
- 存在数据集使得任何鲁棒模型的标准准确率必须低于某个界限
- 原因:对抗扰动可以改变数据的"语义"但保持人类感知不变
这个权衡背后有一个深刻的道理:模型需要在”记住正常样本”和”识别异常样本”之间找平衡。
打个比方,这就像考试的时候,老师希望你既能把标准答案背熟,又能在题目稍微变形时灵活应对。但人的精力是有限的,模型也一样——它在对抗样本上变”聪明”了,在正常样本上的表现可能就会稍微差一点。
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
class AdversarialTraining:
"""
对抗训练框架
min-max 优化:
- 内层(最大化):找到最有效的对抗扰动
- 外层(最小化):训练模型以最小化对抗损失
"""
def __init__(self, model, epsilon=0.03, alpha=0.01, num_iter=7):
self.model = model
self.epsilon = epsilon
self.alpha = alpha
self.num_iter = num_iter
def pgd_attack(self, images, labels, targeted=False, target_labels=None):
"""
PGD攻击:生成对抗样本
这是对抗训练中内层最大化问题的求解方法
"""
original_images = images.detach().clone()
# 随机初始化(在允许扰动范围内)
images = images.detach() + torch.zeros_like(images).uniform_(-self.epsilon, self.epsilon)
images = torch.clamp(images, 0, 1)
for i in range(self.num_iter):
images.requires_grad = True
outputs = self.model(images)
if targeted:
loss = -F.cross_entropy(outputs, target_labels)
else:
loss = F.cross_entropy(outputs, labels)
self.model.zero_grad()
loss.backward()
# 更新扰动
with torch.no_grad():
images = images + self.alpha * torch.sign(images.grad)
# 投影到允许范围
images = torch.maximum(images, original_images - self.epsilon)
images = torch.minimum(images, original_images + self.epsilon)
images = torch.clamp(images, 0, 1)
return images.detach()
def train_step(self, images, labels, optimizer):
"""
对抗训练单步
1. 生成对抗样本(内层最大化)
2. 用对抗样本训练模型(外层最小化)
"""
# 生成对抗样本
adversarial_images = self.pgd_attack(images, labels)
# 正常样本和对抗样本混合训练
optimizer.zero_grad()
# 正常样本损失
clean_outputs = self.model(images)
clean_loss = F.cross_entropy(clean_outputs, labels)
# 对抗样本损失
adv_outputs = self.model(adversarial_images)
adv_loss = F.cross_entropy(adv_outputs, labels)
# 总损失
total_loss = clean_loss + adv_loss
total_loss.backward()
optimizer.step()
return {
'clean_loss': clean_loss.item(),
'adv_loss': adv_loss.item(),
'total_loss': total_loss.item()
}3. FGSM对抗训练:快速梯度符号法的防御
3.1 FGSM是什么?
FGSM(Fast Gradient Sign Method)是Goodfellow等人2015年提出的攻击方法,也是对抗训练的老前辈。虽然现在PGD是主流,但理解FGSM对理解对抗训练很重要,因为后来的Fast AT就是基于它改进的。
FGSM的核心思想特别简单:
- 计算损失关于输入的梯度
- 沿着梯度的符号方向走一大步
- 这一大步就足以让模型判断错误
用公式表示就是:
这个公式的意思是:我不是一点一点地试探,而是直接沿着损失增大的方向大步走。因为梯度指向的是损失增长最快的方向,所以只要一步,就能让损失变得很大。
3.2 FGSM的物理直觉
你可以把FGSM想象成一个不聪明但很粗暴的攻击者:
- 它不会反复试探、慢慢逼近
- 它只看梯度指向哪个方向,然后直接冲过去
- 虽然方法简单,但往往很有效
这就像考试作弊,聪明人可能反复试探哪些题会被发现,但FGSM就是直接抄——虽然容易被发现(模型可能还是能识别),但至少成功率高(确实能骗过模型)。
3.3 FGSM在对抗训练中的应用
class FGSMAdversarialTraining:
"""
FGSM对抗训练
与PGD AT相比:
- 只需要一次前向+一次反向传播
- 训练速度快很多
- 但防御强度相对较弱
"""
def __init__(self, model, epsilon=8/255):
self.model = model
self.epsilon = epsilon
self.device = next(model.parameters()).device
def fgsm_attack(self, images, labels):
"""
FGSM攻击
关键点:需要随机初始化才能训练出好模型
不随机初始化的话,模型会过拟合到单步攻击上
"""
# 随机初始化是Fast AT成功的关键
delta = torch.zeros_like(images).uniform_(-self.epsilon, self.epsilon)
images_adv = images + delta
images_adv = torch.clamp(images_adv, 0, 1).detach()
images_adv.requires_grad = True
outputs = self.model(images_adv)
loss = F.cross_entropy(outputs, labels)
self.model.zero_grad()
loss.backward()
with torch.no_grad():
# 沿梯度符号方向走一大步
images_adv = images_adv + self.epsilon * torch.sign(images_adv.grad)
images_adv = torch.clamp(images_adv, 0, 1)
return images_adv.detach()
def train_step(self, images, labels, optimizer):
"""训练一步"""
# 生成FGSM对抗样本
adversarial_images = self.fgsm_attack(images, labels)
optimizer.zero_grad()
# 用对抗样本计算损失并更新
outputs = self.model(adversarial_images)
loss = F.cross_entropy(outputs, labels)
loss.backward()
optimizer.step()
return loss.item()3.4 FGSM vs PGD:选哪个?
| 特性 | FGSM | PGD |
|---|---|---|
| 迭代次数 | 1步 | 多步(通常7-10步) |
| 训练速度 | 快(~1x标准训练) | 慢(~7x标准训练) |
| 防御强度 | 较弱 | 较强 |
| 超参数敏感度 | 非常敏感 | 相对不敏感 |
| 适用场景 | 资源受限、大规模训练 | 追求最强鲁棒性 |
4. PGD对抗训练详解:投影梯度下降法的防御机制
4.1 PGD为什么是”黄金标准”?
如果你看对抗训练的论文,PGD几乎无处不在。Madry的团队2017年在ICLR上发表的工作奠定了PGD对抗训练的地位,被认为是对抗训练的事实标准。
PGD相比FGSM的核心改进是:多走几步,边走边看。
FGSM是直接大步冲过去,而PGD是:
- 先随机站在某个位置瞄一眼
- 沿着梯度方向走一小步
- 停下来看看走的位置对不对
- 继续走,继续看
- 重复直到满意为止
这种方法虽然慢,但能找到更强的对抗样本。
4.2 投影是什么意思?
“投影梯度下降”这个名字里的”投影”是关键。
想象你站在一个圆形的操场里,但规则规定你只能在操场范围内活动。FGSM就是不管三七二十一直接往某个方向跑,可能会跑到操场外面去。而PGD会在你每走一步之后,检查你是否出了边界,如果出去了就把你”投影”回边界上。
在对抗攻击中,这个”操场”就是以原始输入为中心、为半径的球。对抗扰动不能超过这个范围。
4.3 PGD-Training 完整实现
class PGDTraining:
"""
PGD对抗训练
步骤:
1. 使用PGD生成对抗样本
2. 用对抗样本计算梯度并更新模型
3. 重复直到收敛
"""
def __init__(self, model, epsilon=8/255, alpha=2/255, num_iter=7,
lr=0.01, weight_decay=5e-4):
self.model = model
self.epsilon = epsilon
self.alpha = alpha
self.num_iter = num_iter
self.device = next(model.parameters()).device
# 优化器
self.optimizer = torch.optim.SGD(
model.parameters(),
lr=lr,
momentum=0.9,
weight_decay=weight_decay
)
# 学习率调度
self.scheduler = torch.optim.lr_scheduler.MultiStepLR(
self.optimizer,
milestones=[100, 105],
gamma=0.1
)
def pgd_attack(self, images, labels):
"""PGD攻击"""
images = images.to(self.device)
labels = labels.to(self.device)
original_images = images.detach()
# 随机初始化
if True: # 使用随机初始化
images = images + torch.zeros_like(images).uniform_(
-self.epsilon, self.epsilon
)
images = torch.clamp(images, 0, 1)
for _ in range(self.num_iter):
images.requires_grad = True
outputs = self.model(images)
loss = F.cross_entropy(outputs, labels)
self.model.zero_grad()
loss.backward()
with torch.no_grad():
images = images + self.alpha * images.grad.sign()
# 投影到L无穷球
images = torch.maximum(images, original_images - self.epsilon)
images = torch.minimum(images, original_images + self.epsilon)
images = torch.clamp(images, 0, 1)
return images.detach()
def train_epoch(self, dataloader, epoch):
"""训练一个epoch"""
self.model.train()
total_loss = 0
correct_clean = 0
correct_adv = 0
total = 0
for batch_idx, (images, labels) in enumerate(dataloader):
images, labels = images.to(self.device), labels.to(self.device)
# 生成对抗样本
adversarial_images = self.pgd_attack(images, labels)
# 前向传播
clean_outputs = self.model(images)
adv_outputs = self.model(adversarial_images)
# 损失
clean_loss = F.cross_entropy(clean_outputs, labels)
adv_loss = F.cross_entropy(adv_outputs, labels)
loss = clean_loss + adv_loss
# 反向传播
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
# 统计
total_loss += loss.item()
_, clean_pred = clean_outputs.max(1)
_, adv_pred = adv_outputs.max(1)
correct_clean += clean_pred.eq(labels).sum().item()
correct_adv += adv_pred.eq(labels).sum().item()
total += labels.size(0)
self.scheduler.step()
return {
'loss': total_loss / len(dataloader),
'clean_acc': 100. * correct_clean / total,
'adv_acc': 100. * correct_adv / total,
'lr': self.optimizer.param_groups[0]['lr']
}4.4 PGD训练的理论分析
为什么PGD对抗训练能work?这里面有一些理论支撑:
def theoretical_analysis_pgd():
"""
PGD对抗训练的理论分析
关键结论:
1. 如果PGD能找到局部最大,则接近全局最大
2. 训练后的模型对任意L无穷攻击都有一定鲁棒性
3. 鲁棒性来源于决策边界的"平滑"
"""
analysis = {
'local_maxima': """
在L无穷约束下,损失函数的局部最大值
通常接近全局最大值。
原因:
- 高维空间中局部最大值和全局最大值差距不大
- PGD的随机初始化有助于发现强对抗样本
""",
'convergence': """
PGD在有限步内收敛:
- 步长 alpha >= epsilon / num_iter
- 通常 7-10 步足够
""",
'robustness_certification': """
认证边界:
- 如果模型对epsilon-PGD攻击鲁棒
- 则对任意L无穷范数<=epsilon的扰动鲁棒
- 条件:攻击必须是最优的
"""
}
return analysis5. 对抗训练 vs 常规训练:为什么加了对抗样本反而更难训练?
5.1 训练曲线的差异
如果你实际跑过对抗训练,会发现几个有意思的现象:
- 训练损失下降更慢:因为任务变难了
- 最终准确率更低:干净准确率和鲁棒准确率存在权衡
- 验证曲线波动更大:对抗样本带来的噪声更多
打个比方,普通训练就像让学生做选择题,对抗训练就像让学生做证明题。题目难了,花的时间多了,考试分数可能还低了。
5.2 为什么这种权衡是必然的?
理论上,Wong和Kolter证明了一个有点让人沮丧的结论:在某些数据集上,任何对对抗扰动鲁棒的模型,其标准准确率都必须下降。
这背后的直觉是:对抗扰动可以看作是在高维空间中对数据点的”语义保持”变形。也就是说,有些扰动虽然人眼看不出来,但确实改变了数据的本质特征。模型如果对这种扰动不敏感,可能确实需要在某些方面做出牺牲。
def analyze_robustness_accuracy_tradeoff():
"""
分析鲁棒性与标准准确率的权衡
理论背景:
- 对抗样本利用的是模型在高维空间的线性敏感性
- 鲁棒模型需要在决策边界附近"平滑"
- 这可能牺牲对干净数据的拟合能力
"""
# 实验观察(基于 CIFAR-10):
# 标准训练:Clean Acc ~ 95%, Adv Acc (PGD) ~ 0%
# 对抗训练:Clean Acc ~ 85%, Adv Acc (PGD) ~ 50%
tradeoffs = {
'standard_training': {
'clean_accuracy': 0.95,
'robust_accuracy_pgd20': 0.0,
'robust_accuracy_pgd100': 0.0
},
'adversarial_training': {
'clean_accuracy': 0.85,
'robust_accuracy_pgd20': 0.50,
'robust_accuracy_pgd100': 0.48
},
'TRADES_regularization': {
'clean_accuracy': 0.90,
'robust_accuracy_pgd20': 0.55,
'robust_accuracy_pgd100': 0.52
},
'MART_regularization': {
'clean_accuracy': 0.88,
'robust_accuracy_pgd20': 0.53,
'robust_accuracy_pgd100': 0.50
}
}
return tradeoffs5.3 如何缓解这个权衡?
虽然没有完美的解决方案,但有一些技巧可以缓解这个权衡:
- TRADES方法:用KL散度正则化,鼓励模型在干净样本和对抗样本上表现一致
- MART方法:关注那些容易被对抗样本欺骗的”困难样本”
- 标签平滑:减少对标签的过度自信,提高泛化能力
- 更大的模型容量:更复杂的模型可以同时兼顾两者
6. 对抗训练的代码实现:PyTorch逐行解析
6.1 最基础的版本
让我把对抗训练拆解成最核心的几个步骤:
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
def basic_adversarial_training(model, train_loader, epochs=10,
epsilon=8/255, alpha=2/255, num_iter=7):
"""
最基础的对抗训练流程
核心循环:
for epoch in epochs:
for batch in train_loader:
1. 生成对抗样本
2. 用对抗样本训练模型
"""
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
for epoch in range(epochs):
model.train()
total_loss = 0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
# ======== 核心1:生成对抗样本 ========
# 先克隆原始图像,保存下来用于投影
original_images = images.clone()
# 随机初始化:在epsilon范围内随机扰动
images = images + torch.zeros_like(images).uniform_(
-epsilon, epsilon
)
images = torch.clamp(images, 0, 1)
# PGD迭代
for _ in range(num_iter):
# 关键:需要梯度才能反向传播
images.requires_grad = True
# 前向传播
outputs = model(images)
loss = F.cross_entropy(outputs, labels)
# 反向传播,计算梯度
optimizer.zero_grad()
loss.backward()
# 更新扰动
with torch.no_grad():
images = images + alpha * images.grad.sign()
# 投影回允许范围
images = torch.maximum(images, original_images - epsilon)
images = torch.minimum(images, original_images + epsilon)
images = torch.clamp(images, 0, 1)
# ======== 核心2:用对抗样本训练 ========
optimizer.zero_grad()
outputs = model(images)
loss = F.cross_entropy(outputs, labels)
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f"Epoch {epoch+1}, Loss: {total_loss/len(train_loader):.4f}")
return model6.2 完整训练循环的Tips
实际应用中,有几个值得注意的点:
def training_tips():
"""
对抗训练实战技巧
"""
tips = {
'random_initialization': """
随机初始化非常重要!
不随机初始化:
- 所有样本的对抗扰动方向相同
- 模型可能过拟合到特定的对抗模式
随机初始化:
- 每个样本从不同起点开始搜索
- 最终找到的对抗样本更强
- 训练出的模型更鲁棒
""",
'early_stopping': """
早停要小心:
普通训练的早停标准(验证集准确率)不适用于对抗训练
因为验证集的对抗样本也需要重新生成
建议:
- 每隔几个epoch评估一次鲁棒准确率
- 在验证集上评估时要生成对抗样本
""",
'mixed_precision': """
混合精度训练可以加速:
生成对抗样本时用float32保证精度
模型前向传播时可以用float16加速
注意:反向传播仍需要float32
""",
'batch_size': """
批次大小的影响:
更大的batch:
- 梯度估计更稳定
- 可以生成更多样的对抗样本
- 但显存占用增加
对抗训练通常用128或256
"""
}
return tips7. 对抗训练的挑战:精度-鲁棒性权衡、训练代价
7.1 计算开销:为什么对抗训练这么贵?
对抗训练的主要瓶颈在于内层最大化问题的计算。对于每个训练样本,需要多次前向-反向传播来生成对抗样本:
标准训练: 1次前向 + 1次反向
对抗训练: (1 + num_iter)次前向 + num_iter次反向
计算开销比: (1 + K) / 1,其中K是PGD迭代次数
用CIFAR-10举例,标准训练需要几十秒一个epoch,但PGD对抗训练可能需要几分钟。训练一个完整的模型可能需要几天时间。
def compute_overhead_analysis():
"""
计算开销分析
"""
overheads = {
'FGSM_attack': {
'forward_passes': 2, # 干净样本 + 对抗样本
'backward_passes': 1,
'overhead_ratio': 2
},
'PGD_7': {
'forward_passes': 8, # 1初始化 + 7迭代 + 1最终
'backward_passes': 7,
'overhead_ratio': 8
},
'PGD_20': {
'forward_passes': 22,
'backward_passes': 20,
'overhead_ratio': 21
},
'PGD_100': {
'forward_passes': 102,
'backward_passes': 100,
'overhead_ratio': 101
}
}
return overheads7.2 其他实际挑战
除了计算开销,对抗训练还面临这些问题:
超参数敏感:epsilon、alpha、num_iter的选择都会显著影响结果。选大了模型可能欠拟合,选小了可能防御不够。
梯度消失/爆炸:对抗样本可能导致梯度异常,需要适当的梯度裁剪。
评估困难:怎么才算”真正鲁棒”?可能一个更强的攻击就能破解你的防御。
8. 对抗训练的效率问题与解决方案
8.1 Free AT:自由对抗训练
Free Adversarial Training (Free AT) 通过复用梯度来减少计算开销:
class FreeAdversarialTraining:
"""
Free Adversarial Training (Shafahi et al., 2019)
核心思想:
- 在每次参数更新中复用对抗扰动
- m次小批量更新后重新生成对抗扰动
原始方法:
- 训练时间:m倍于标准训练
- 对抗强度:与标准PGD训练相当
"""
def __init__(self, model, epsilon=8/255, alpha=2/255,
num_iter=2, num_replays=8):
self.model = model
self.epsilon = epsilon
self.alpha = alpha
self.num_iter = num_iter
self.num_replays = num_replays
# 累积的扰动
self.delta = None
def free_at_attack(self, images, labels, reverse=False):
"""
Free AT的扰动更新
在反向传播后立即更新扰动方向
"""
if self.delta is None or reverse:
self.delta = torch.zeros_like(images)
images_adv = images + self.delta
images_adv = torch.clamp(images_adv, 0, 1)
images_adv.requires_grad = True
outputs = self.model(images_adv)
loss = F.cross_entropy(outputs, labels)
self.model.zero_grad()
loss.backward()
with torch.no_grad():
# 更新扰动
self.delta = self.delta + self.alpha * images_adv.grad.sign()
# 投影
self.delta = torch.clamp(self.delta, -self.epsilon, self.epsilon)
return (images + self.delta).detach()
def train_step(self, images, labels, optimizer):
"""Free AT训练步骤"""
for replay in range(self.num_replays):
# 使用累积扰动生成对抗样本
adversarial_images = self.free_at_attack(images, labels)
# 计算损失并更新
optimizer.zero_grad()
outputs = self.model(adversarial_images)
loss = F.cross_entropy(outputs, labels)
loss.backward()
optimizer.step()
return loss.item()8.2 Fast AT:快速对抗训练
Fast Adversarial Training (Fast AT) 使用FGSM代替PGD来加速:
class FastAdversarialTraining:
"""
Fast Adversarial Training (Wong et al., 2020)
核心发现:
- 使用FGSM(单步攻击)可以训练出鲁棒模型
- 关键:使用随机初始化
- 训练时间与标准训练几乎相同
"""
def __init__(self, model, epsilon=8/255):
self.model = model
self.epsilon = epsilon
self.device = next(model.parameters()).device
def fast_at_attack(self, images, labels):
"""
Fast AT攻击
使用随机初始化的FGSM
"""
# 随机初始化
delta = torch.zeros_like(images).uniform_(-self.epsilon, self.epsilon)
images_adv = images + delta
images_adv = torch.clamp(images_adv, 0, 1).detach()
images_adv.requires_grad = True
outputs = self.model(images_adv)
loss = F.cross_entropy(outputs, labels)
self.model.zero_grad()
loss.backward()
with torch.no_grad():
# FGSM扰动
images_adv = images_adv + self.epsilon * torch.sign(images_adv.grad)
# 投影
images_adv = torch.clamp(images_adv, 0, 1)
return images_adv.detach()
def train_step(self, images, labels, optimizer):
"""
Fast AT训练步骤
与标准训练的计算开销几乎相同
"""
# 生成FGSM对抗样本
adversarial_images = self.fast_at_attack(images, labels)
optimizer.zero_grad()
# 使用对抗样本训练
outputs = self.model(adversarial_images)
loss = F.cross_entropy(outputs, labels)
loss.backward()
optimizer.step()
return loss.item()Fast AT vs PGD AT
- Fast AT:训练快(~1x时间),但需要精心调参
- PGD AT:训练慢(~7x时间),但更稳定可靠
- 两者在最终鲁棒性上可以相近,但Fast AT对超参数更敏感
8.3 SMART:自我对抗正则化
class SMARTTraining:
"""
SMART: Self-adversarial training with Margin Enhancement
(Jiang et al., 2020)
特点:
1. 使用动量FGSM提高对抗样本质量
2. 引入margin loss增强决策边界
3. 标签平滑减少过拟合
"""
def __init__(self, model, epsilon=8/255, alpha=1.25*8/255,
margin=0.2, label_smoothing=0.1):
self.model = model
self.epsilon = epsilon
self.alpha = alpha
self.margin = margin
self.label_smoothing = label_smoothing
self.device = next(model.parameters()).device
# 动量
self.momentum = torch.zeros_like(model.parameters())
def momentum_fgsm_attack(self, images, labels):
"""
动量FGSM攻击
累积梯度方向以稳定对抗扰动
"""
delta = torch.zeros_like(images).uniform_(-self.epsilon, self.epsilon).to(self.device)
for _ in range(2): # 2步迭代
delta.requires_grad = True
outputs = self.model(images + delta)
loss = F.cross_entropy(outputs, labels)
self.model.zero_grad()
loss.backward()
# 更新动量
self.momentum = 0.9 * self.momentum + delta.grad / (delta.grad.abs().mean() + 1e-10)
with torch.no_grad():
delta = delta + self.alpha * torch.sign(self.momentum)
delta = torch.clamp(delta, -self.epsilon, self.epsilon)
delta = torch.clamp(images + delta, 0, 1) - images
return (images + delta).detach()
def margin_loss(self, outputs, labels):
"""
Margin loss
鼓励模型对真实类别的置信度显著高于其他类别
"""
# 获取目标类别的logit
target_logits = outputs.gather(1, labels.unsqueeze(1)).squeeze()
# 获取次高logit
other_logits = outputs.clone()
other_logits.scatter_(1, labels.unsqueeze(1), float('-inf'))
second_logits = other_logits.max(dim=1)[0]
# Margin: 目标logit - 次高logit
margins = target_logits - second_logits
# 希望margin大于给定阈值
return F.relu(self.margin - margins).mean()
def train_step(self, images, labels, optimizer):
"""SMART训练步骤"""
# 生成对抗样本
adversarial_images = self.momentum_fgsm_attack(images, labels)
optimizer.zero_grad()
# 标准交叉熵
ce_loss = F.cross_entropy(
self.model(adversarial_images),
labels,
label_smoothing=self.label_smoothing
)
# Margin loss
outputs = self.model(adversarial_images)
margin_loss = self.margin_loss(outputs, labels)
# 总损失
loss = ce_loss + 0.5 * margin_loss
loss.backward()
optimizer.step()
return {
'ce_loss': ce_loss.item(),
'margin_loss': margin_loss.item(),
'total_loss': loss.item()
}9. 对抗正则化方法
9.1 TRADES:对抗正则化
TRADES (TRADE-off between robustness and accuracy) 通过KL散度正则化:
class TRADESTraining:
"""
TRADES: TRADE-off between robustness and accuracy
(Zhang et al., 2019)
损失函数:
L(x, y, θ) = L_ce(f_θ(x), y) + β * KL(f_θ(x) || f_θ(x'))
其中 x' 是对抗样本
"""
def __init__(self, model, epsilon=8/255, beta=6.0):
self.model = model
self.epsilon = epsilon
self.beta = beta
self.device = next(model.parameters()).device
def pgd_attack(self, images, labels):
"""生成对抗样本"""
original_images = images.detach()
images = images + torch.zeros_like(images).uniform_(-self.epsilon, self.epsilon)
images = torch.clamp(images, 0, 1)
for _ in range(10):
images.requires_grad = True
outputs = self.model(images)
loss = F.cross_entropy(outputs, labels)
self.model.zero_grad()
loss.backward()
with torch.no_grad():
images = images + 2/255 * torch.sign(images.grad)
images = torch.maximum(images, original_images - self.epsilon)
images = torch.minimum(images, original_images + self.epsilon)
images = torch.clamp(images, 0, 1)
return images.detach()
def train_step(self, images, labels, optimizer):
"""TRADES训练步骤"""
# 生成对抗样本
adversarial_images = self.pgd_attack(images, labels)
optimizer.zero_grad()
# 干净样本的交叉熵损失
clean_outputs = self.model(images)
ce_loss = F.cross_entropy(clean_outputs, labels)
# KL散度正则化
clean_probs = F.log_softmax(clean_outputs, dim=1)
adv_probs = F.softmax(self.model(adversarial_images), dim=1)
kl_loss = F.kl_div(clean_probs, adv_probs, reduction='batchmean')
# 总损失
loss = ce_loss + self.beta * kl_loss
loss.backward()
optimizer.step()
return {
'ce_loss': ce_loss.item(),
'kl_loss': kl_loss.item(),
'total_loss': loss.item()
}9.2 MART:基于鲁棒错误理论的对抗训练
class MARTTraining:
"""
MART: Misclassification Aware Adversarial Training
(Wang et al., 2019)
损失函数:
L = L_ce + λ * BCE
其中BCE是鲁棒错误分类损失
"""
def __init__(self, model, epsilon=8/255, beta=6.0):
self.model = model
self.epsilon = epsilon
self.beta = beta
def train_step(self, images, labels, optimizer):
"""MART训练步骤"""
# 生成对抗样本
adversarial_images = self.pgd_attack(images, labels)
optimizer.zero_grad()
# 干净样本和对抗样本的输出
clean_outputs = self.model(images)
adv_outputs = self.model(adversarial_images)
# 交叉熵损失
ce_loss = F.cross_entropy(clean_outputs, labels)
# 鲁棒错误分类损失
# 计算被错误分类的样本的对抗KL散度
clean_probs = F.softmax(clean_outputs, dim=1)
adv_probs = F.softmax(adv_outputs, dim=1)
# 找到当前预测类别
_, clean_pred = clean_outputs.max(1)
# BCE损失
bce_loss = clean_probs.gather(1, labels.unsqueeze(1)).squeeze().clamp(1e-8, 1)
bce_loss = -torch.log(bce_loss).mean()
# 总损失
loss = ce_loss + self.beta * bce_loss
loss.backward()
optimizer.step()
return loss.item()
def pgd_attack(self, images, labels):
"""PGD攻击"""
original_images = images.detach()
images = images + torch.zeros_like(images).uniform_(-self.epsilon, self.epsilon)
images = torch.clamp(images, 0, 1)
for _ in range(7):
images.requires_grad = True
outputs = self.model(images)
loss = F.cross_entropy(outputs, labels)
self.model.zero_grad()
loss.backward()
with torch.no_grad():
images = images + 2/255 * torch.sign(images.grad)
images = torch.maximum(images, original_images - self.epsilon)
images = torch.minimum(images, original_images + self.epsilon)
images = torch.clamp(images, 0, 1)
return images.detach()10. 认证防御:可证明的鲁棒性
10.1 为什么需要认证防御?
对抗训练虽然有效,但有一个根本问题:它只对训练时见过的攻击方法有效。
这就像考试前只准备了老师出过的题型,但真正考试可能出你没见过的题。
认证防御(Certified Defense)的目标更雄心勃勃:给出数学证明,保证模型在某个扰动范围内对任意攻击都鲁棒。
10.2 随机平滑:可扩展的认证方法
随机平滑是目前最实用的认证方法,由Cohen等人2019年提出:
class RandomizedSmoothing:
"""
随机平滑(Randomized Smoothing)
认证方法:
- 给输入添加高斯噪声
- 通过投票决定最终预测
- 提供认证半径保证
论文:Cohen et al., 2019
"""
def __init__(self, model, sigma=0.25, num_samples=1000):
self.model = model
self.sigma = sigma
self.num_samples = num_samples
self.device = next(model.parameters()).device
def certify(self, x, n0=100, alpha=0.05):
"""
认证函数
参数:
- x: 输入样本
- n0: 初始样本数(用于估计p_A)
- alpha: 置信度参数
返回:
- predicted_class: 预测类别
- certified_radius: 认证半径
"""
self.model.eval()
# 估计 top 类别的概率
with torch.no_grad():
# 采样 n0 次
counts = self._sample_predictions(x, n0)
top_class = counts.argmax().item()
p_lower = self._lower_confidence_bound(counts[top_class].item(), n0, alpha)
if p_lower < 0.5:
return top_class, 0.0
# 估计其他类别的上界
n = self.num_samples
with torch.no_grad():
all_counts = self._sample_predictions(x, n)
p_upper = self._upper_confidence_bound(
all_counts[top_class].item(), n, alpha
)
# 认证半径
radius = 0.5 * self.sigma * torch.distributions.Normal(0, 1).ppf(p_lower - 1 + alpha)
return top_class, radius.item()
def _sample_predictions(self, x, num_samples):
"""采样预测"""
# 重复输入
x_repeated = x.repeat(num_samples, 1, 1, 1)
# 添加噪声
noise = torch.randn_like(x_repeated) * self.sigma
x_noisy = torch.clamp(x_repeated + noise, 0, 1)
# 预测
with torch.no_grad():
outputs = self.model(x_noisy)
predictions = outputs.argmax(dim=1)
# 计数
num_classes = outputs.size(1)
counts = torch.zeros(num_classes)
for pred in predictions:
counts[pred] += 1
return counts
def _lower_confidence_bound(self, successes, trials, alpha):
"""Wilson score interval 下界"""
import math
z = 1.96 # 95% 置信度
n = trials
p_hat = successes / n
denominator = 1 + z**2 / n
center = (p_hat + z**2 / (2*n)) / denominator
margin = z * math.sqrt(p_hat * (1 - p_hat) / n + z**2 / (4*n**2)) / denominator
return max(0, center - margin)
def _upper_confidence_bound(self, successes, trials, alpha):
"""Wilson score interval 上界"""
import math
z = 1.96
n = trials
p_hat = successes / n
denominator = 1 + z**2 / n
center = (p_hat + z**2 / (2*n)) / denominator
margin = z * math.sqrt(p_hat * (1 - p_hat) / n + z**2 / (4*n**2)) / denominator
return min(1, center + margin)10.3 认证防御 vs 对抗训练
| 特性 | 对抗训练 | 认证防御 |
|---|---|---|
| 防御强度 | 经验性 | 理论保证 |
| 计算代价 | 较高 | 适中(随机平滑需要大量采样) |
| 适用范围 | 图像分类为主 | 通用 |
| 认证半径 | 无 | 有 |
| 当前实用性 | 更实用 | 仍在研究中 |
认证 vs 经验鲁棒性
- 认证半径提供了理论保证,即使攻击者知道模型参数也无法突破
- 但认证半径通常保守,可能低估实际鲁棒性
- 对抗训练可以提高经验鲁棒性,但不一定提高认证鲁棒性
11. 输入变换防御:去噪、JPEG压缩、随机化
11.1 预处理防御的思路
除了对抗训练,还有一类防御方法是在输入进入模型之前进行预处理。这类方法的核心思路是:如果对抗扰动在某些变换下会消失或变形,那模型就不会受到影响。
常见的输入变换包括:
- 去噪(denoising)
- JPEG压缩
- 随机化(randomization)
- 位深度缩减
11.2 去噪防御
class DenoisingDefense:
"""
去噪防御
假设:对抗扰动可以被当作噪声去除
方法:使用去噪网络或传统去噪方法
"""
def __init__(self, model, denoiser):
self.model = model
self.denoiser = denoiser
def predict(self, x):
"""先去噪,再预测"""
# 去噪
x_denoised = self.denoiser(x)
# 预测
with torch.no_grad():
outputs = self.model(x_denoised)
return outputs
def traditional_denoising(self, x, method='median', kernel_size=3):
"""
传统去噪方法
方法:
- 中值滤波:对每个像素,用邻域的中值替代
- 高斯模糊:加权平均
- 双边滤波:保边去噪
"""
import torch.nn.functional as F
if method == 'median':
# 中值滤波需要用unfold实现
pass
elif method == 'gaussian':
# 高斯模糊
return F.avg_pool2d(
F.pad(x, (kernel_size//2,)*4, mode='replicate'),
kernel_size, stride=1
)
return x11.3 JPEG压缩防御
class JPEGCompressionDefense:
"""
JPEG压缩防御
原理:JPEG压缩会消除高频成分,而对抗扰动通常是高频的
限制:JPEG压缩是离散变换,不容易微分,难以端到端训练
"""
def __init__(self, model, quality=75):
self.model = model
self.quality = quality
def compress_decompress(self, x):
"""
简化的JPEG压缩解压
实际实现需要用libjpeg或Pillow
这里只是概念演示
"""
# 量化过程(简化)
# 实际需要DCT变换、量化、反量化、反DCT
# 返回压缩后的图像
return x
def predict(self, x):
"""先JPEG压缩,再预测"""
x_compressed = self.compress_decompress(x)
return self.model(x_compressed)11.4 随机化防御
class RandomizationDefense:
"""
随机化防御
方法:
- 随机填充(Random Padding):在图像周围随机添加边
- 随机调整大小(Random Resizing):随机缩放后裁剪回原大小
原理:对抗扰动可能无法在各种变换下都保持有效
"""
def __init__(self, model, padding_size=4, resize_ratio=0.9):
self.model = model
self.padding_size = padding_size
self.resize_ratio = resize_ratio
def random_padding_resize(self, x):
"""
随机填充+调整大小
"""
batch_size, channels, height, width = x.shape
# 随机填充
pad_left = torch.randint(0, self.padding_size + 1, (batch_size,))
pad_right = torch.randint(0, self.padding_size + 1, (batch_size,))
pad_top = torch.randint(0, self.padding_size + 1, (batch_size,))
pad_bottom = torch.randint(0, self.padding_size + 1, (batch_size,))
# 填充
x_padded = F.pad(x, (pad_left.max().item(), pad_right.max().item(),
pad_top.max().item(), pad_bottom.max().item()),
mode='constant', value=0)
# 随机调整大小(缩放)
# 实际实现会更复杂
return x
def predict(self, x):
"""随机变换后预测"""
x_transformed = self.random_padding_resize(x)
return self.model(x_transformed)12. 对抗样本检测:不防御攻击,但检测攻击
12.1 检测 vs 防御
还有一类思路不走防御路线,而是走检测路线:如果能检测到输入是对抗样本,那我可以拒绝处理或者报警。
这在某些安全场景下很有用,比如:
- 银行拒绝可疑的交易请求
- 自动驾驶在检测到攻击时切换到安全模式
12.2 基于置信度的检测
class ConfidenceBasedDetection:
"""
基于置信度的对抗样本检测
原理:对抗样本通常在模型的预测置信度上有异常
方法:训练一个单独的检测器,或者用现有模型的特征
"""
def __init__(self, model, threshold=0.5):
self.model = model
self.threshold = threshold
def detect(self, x):
"""
检测是否为对抗样本
方法1:置信度低于阈值
方法2:预测的熵高于阈值
方法3:多个模型的预测不一致
"""
with torch.no_grad():
outputs = self.model(x)
probs = F.softmax(outputs, dim=1)
# 方法1:最大置信度
max_confidence = probs.max(dim=1)[0]
# 方法2:熵
entropy = -(probs * torch.log(probs + 1e-10)).sum(dim=1)
# 综合判断
is_adversarial = (max_confidence < self.threshold) | (entropy > 2.0)
return is_adversarial, {
'max_confidence': max_confidence,
'entropy': entropy
}12.3 基于特征距离的检测
class FeatureDistanceDetection:
"""
基于特征距离的检测
原理:干净样本和对抗样本在特征空间中的分布不同
方法:比较输入在干净模型和对抗训练模型上的特征
"""
def __init__(self, clean_model, robust_model):
self.clean_model = clean_model
self.robust_model = robust_model
def compute_feature_distance(self, x):
"""
计算两个模型的特征距离
"""
# 提取特征
with torch.no_grad():
clean_features = self.clean_model.extract_features(x)
robust_features = self.robust_model.extract_features(x)
# 计算距离
distance = torch.norm(clean_features - robust_features, dim=1)
return distance
def detect(self, x, threshold=None):
"""
检测对抗样本
"""
distance = self.compute_feature_distance(x)
if threshold is None:
# 自动阈值
threshold = distance.mean() + 2 * distance.std()
is_adversarial = distance > threshold
return is_adversarial, distance13. 对抗训练的超参数:扰动大小ε、步数、攻击方法的经验
13.1 超参数一览
对抗训练的效果很大程度上取决于超参数的选择:
def get_training_recommendations():
"""
对抗训练实用配置建议
"""
recommendations = {
'PGD_adversarial_training': {
'epsilon': '8/255 (CIFAR-10), 4/255 (ImageNet)',
'alpha': 'epsilon / 4',
'num_iter': '7-10',
'optimizer': 'SGD with momentum 0.9',
'learning_rate': '0.01-0.1',
'weight_decay': '5e-4',
'batch_size': '128-256',
'training_epochs': '110'
},
'fast_adversarial_training': {
'epsilon': '8/255',
'alpha': '1.25 * epsilon',
'random_init': 'True',
'optimizer': 'SGD with momentum 0.9',
'learning_rate': '0.1'
},
'TRADES': {
'epsilon': '8/255',
'beta': '6.0',
'optimizer': 'Adam or SGD',
'learning_rate': '0.01'
}
}
return recommendations13.2 epsilon选择指南
epsilon是对抗扰动的幅度上限,是最关键的超参数:
| 数据集 | 常用epsilon | 说明 |
|---|---|---|
| MNIST | 0.3 | 像素值范围[0,1],0.3的扰动人眼可见 |
| CIFAR-10 | 8/255 ≈ 0.031 | 几乎不可见 |
| ImageNet | 4/255 ≈ 0.016 | 需要仔细看才能发现 |
选择epsilon时需要考虑:
- 应用场景:安全要求高则选大值,用户体验重要则选小值
- 攻击者能力:假设攻击者能控制多大的扰动
- 数据集特性:CIFAR和ImageNet的像素值标准化后不同
13.3 训练技巧
def advanced_training_tips():
"""
对抗训练高级技巧
"""
tips = {
'warmup': """
学习率预热(Warmup):
对抗训练的初期,模型在对抗样本上的表现很差
直接用大学习率可能导致训练不稳定
建议先用小学习率预热几个epoch
""",
'scheduling': """
学习率调度:
- 多步衰减(MultiStep):在指定epoch降低学习率
- 余弦退火(Cosine):平滑下降
- 指数衰减(Exponential):持续缓慢下降
""",
'early_stopping': """
早停策略:
监控验证集上的鲁棒准确率(需要重新生成对抗样本)
如果连续几个epoch没有提升,可以提前停止
""",
'label_smoothing': """
标签平滑:
将硬标签(0或1)替换为软标签(0.9或0.1)
可以减少过拟合,提高泛化能力
推荐值:0.1
"""
}
return tips14. 对抗训练在NLP中的应用:文本对抗攻击与防御
14.1 文本对抗的特殊挑战
图像对抗攻击可以在像素级别微调,但文本对抗面临独特挑战:
- 离散性:文本是离散的,不能直接梯度下降
- 语义一致性:扰动必须保持语义不变
- 语法正确性:扰动后的文本必须语法正确
14.2 文本对抗方法
class TextAdversarialAttack:
"""
文本对抗攻击方法
"""
def character_level_attack(self, text, model):
"""
字符级攻击
方法:
- 替换:character -> charac+er
- 删除:character -> chacter
- 插入:character -> characther
"""
# 近似:使用困惑度选择要攻击的位置
pass
def word_level_attack(self, text, model, word_embeddings):
"""
词级攻击
方法:
- 同义词替换
- 词向量空间中的最近邻替换
"""
# 近似:使用词重要性排序选择要替换的词
pass
def sentence_level_attack(self, text, model):
"""
句子级攻击
方法:
- 添加对抗性句子
- 改写原句
"""
pass
class TextAdversarialTraining:
"""
文本对抗训练
"""
def __init__(self, model, epsilon=0.3):
self.model = model
self.epsilon = epsilon
def virtual_adversarial_training(self, embeddings, labels, attention_mask):
"""
虚拟对抗训练(VAT)
在词嵌入空间添加扰动
"""
# 初始化扰动
delta = torch.randn_like(embeddings) * self.epsilon
# 多次迭代找到有效扰动
for _ in range(5):
delta.requires_grad = True
outputs = self.model(inputs_embeds=embeddings + delta,
attention_mask=attention_mask)
loss = F.cross_entropy(outputs, labels)
loss.backward()
with torch.no_grad():
delta = delta + self.epsilon * delta.grad.sign()
delta = torch.clamp(delta, -self.epsilon, self.epsilon)
# 用扰动后的嵌入训练
optimizer.zero_grad()
outputs = self.model(inputs_embeds=embeddings + delta,
attention_mask=attention_mask)
loss = F.cross_entropy(outputs, labels)
loss.backward()
return loss.item()15. 大模型的对齐与鲁棒性:RLHF能否提升鲁棒性?
15.1 大模型的特殊问题
随着大语言模型(LLM)的兴起,对抗鲁棒性问题变得更加重要:
- LLM可能被精心设计的prompt诱导出有害内容
- 对抗后缀(adversarial suffix)可以让模型绕过安全检查
- 但同时也要避免过度保守导致正常请求被拒绝
15.2 RLHF与鲁棒性的关系
def rlhf_vs_adversarial_robustness():
"""
RLHF(人类反馈强化学习)与对抗鲁棒性的关系
"""
analysis = {
'what_rlhf_learns': """
RLHF学到了什么:
- 遵循指令的模式
- 避免生成有害内容
- 更自然的对话风格
但RLHF主要关注"语义"层面的安全
""",
'limitations': """
RLHF的局限性:
- 对抗样本可能保持语义安全但触发其他问题
- 对抗后缀不改变语义,但能绕过RLHF训练的"直觉"
- RLHF训练数据中很少有对抗样本
""",
'potential_combination': """
可能的结合方式:
- 在RLHF中加入对抗prompt的评估
- 用对抗样本来丰富reward model的训练数据
- 在PPO阶段加入对抗扰动的奖励惩罚
"""
}
return analysis15.3 对抗训练在LLM中的应用
class LLMAdversarialDefense:
"""
大语言模型的对抗防御
"""
def __init__(self, model, tokenizer):
self.model = model
self.tokenizer = tokenizer
def token_level_adversarial_training(self, prompts, targets, epsilon=0.1):
"""
Token级对抗训练
在embedding space添加扰动
"""
# 获取embeddings
inputs = self.tokenizer(prompts, return_tensors='pt', padding=True)
embeddings = self.model.get_input_embeddings()(inputs['input_ids'])
# 生成对抗扰动
delta = torch.zeros_like(embeddings)
delta.uniform_(-epsilon, epsilon)
delta.requires_grad = True
# 前向传播
outputs = self.model(inputs_embeds=embeddings + delta,
attention_mask=inputs['attention_mask'])
# 计算损失
loss = F.cross_entropy(outputs.logits, targets)
# 反向传播
loss.backward()
return delta.grad
def defense_in_llama_guard_style(self, prompt, response):
"""
类LlamaGuard的安全防御
训练一个独立的安全分类器
"""
# 结合prompt和response
combined = f"Prompt: {prompt}\nResponse: {response}"
inputs = self.tokenizer(combined, return_tensors='pt')
# 判断是否安全
with torch.no_grad():
outputs = self.model(**inputs)
is_safe = outputs.logits.argmax(dim=-1) == 0
return is_safe16. 对抗训练的前沿:自适应对抗训练、免费对抗训练
16.1 自适应对抗训练
传统对抗训练在训练过程中使用固定的攻击方法。自适应对抗训练会根据训练进度动态调整攻击策略:
class AdaptiveAdversarialTraining:
"""
自适应对抗训练
核心思想:
- 根据模型当前的弱点调整攻击
- 在训练早期使用较弱的攻击,让模型逐步适应
- 在训练后期使用更强的攻击,提高鲁棒性上限
"""
def __init__(self, model, epsilon_base=4/255, epsilon_max=8/255):
self.model = model
self.epsilon_base = epsilon_base
self.epsilon_max = epsilon_max
self.device = next(model.parameters()).device
def get_adaptive_epsilon(self, epoch, total_epochs):
"""
根据训练进度调整epsilon
线性增加:epsilon = epsilon_base + (epsilon_max - epsilon_base) * epoch/total_epochs
或者:余弦退火等
"""
progress = epoch / total_epochs
return self.epsilon_base + (self.epsilon_max - self.epsilon_base) * progress
def get_adaptive_num_iter(self, epoch, total_epochs):
"""
根据训练进度调整迭代次数
早期:少迭代,探索不同的对抗样本
后期:多迭代,找到更强的对抗样本
"""
progress = epoch / total_epochs
return int(7 + progress * 13) # 从7步增加到20步
def train_step(self, images, labels, optimizer, epoch, total_epochs):
"""自适应训练步骤"""
epsilon = self.get_adaptive_epsilon(epoch, total_epochs)
num_iter = self.get_adaptive_num_iter(epoch, total_epochs)
# 使用调整后的参数生成对抗样本
adversarial_images = self.pgd_attack(images, labels, epsilon, num_iter)
# 训练
optimizer.zero_grad()
outputs = self.model(adversarial_images)
loss = F.cross_entropy(outputs, labels)
loss.backward()
optimizer.step()
return loss.item()
def pgd_attack(self, images, labels, epsilon, num_iter):
"""PGD攻击"""
original_images = images.detach()
images = images + torch.zeros_like(images).uniform_(-epsilon, epsilon)
images = torch.clamp(images, 0, 1)
alpha = epsilon / 4
for _ in range(num_iter):
images.requires_grad = True
outputs = self.model(images)
loss = F.cross_entropy(outputs, labels)
self.model.zero_grad()
loss.backward()
with torch.no_grad():
images = images + alpha * images.grad.sign()
images = torch.maximum(images, original_images - epsilon)
images = torch.minimum(images, original_images + epsilon)
images = torch.clamp(images, 0, 1)
return images.detach()16.2 其他前沿方向
def frontier_research_directions():
"""
对抗训练前沿研究方向
"""
directions = {
'adversarial_training_plus': """
Adversarial Training Plus (AT+)
在标准对抗训练基础上增加:
- 更强的数据增强
- 课程学习策略
- 知识蒸馏
""",
'mixup_adversarial_training': """
Mixup对抗训练
将Mixup与对抗训练结合:
- 生成混合的对抗样本
- 提高泛化能力和鲁棒性的平衡
""",
'domain_adversarial_training': """
领域对抗训练
借鉴域适应的思想:
- 学习对领域无关扰动鲁棒的表示
- 提高跨域鲁棒性
""",
'test_time_adversarial_training': """
测试时对抗训练
在推理阶段也进行对抗训练:
- 动态调整模型以适应输入分布
- 代价是推理速度变慢
"""
}
return directions17. 评估协议
17.1 标准评估流程
def robust_evaluation_protocol():
"""
鲁棒性评估标准协议
"""
evaluation = {
'white_box_attacks': [
'PGD-20: PGD with 20 steps',
'PGD-100: PGD with 100 steps',
'AutoAttack: ensemble of multiple attacks'
],
'black_box_attacks': [
'Transfer attack from surrogate model',
'HopSkipJumpAttack',
'Square Attack'
],
'certified_robustness': [
'Randomized Smoothing',
'CROWN bounds',
'IBP (Interval Bound Propagation)'
],
'metrics': [
'Robust Accuracy: accuracy on adversarial examples',
'Clean Accuracy: accuracy on clean data',
'Certified Radius: guaranteed perturbation bound'
]
}
return evaluation17.2 AutoAttack:标准化的鲁棒性评估
AutoAttack是一个标准化的攻击套件,由Liang等人2020年提出,包含:
- APGD-CE:自适应步长的PGD攻击,针对交叉熵损失
- APGD-DLR:APGD针对DLR损失
- FAB:点几何攻击
- SQUARE:黑盒攻击
def autoattack_evaluation(model, test_loader, epsilon):
"""
AutoAttack评估
实际使用中可以直接调用advertorch或torchattacks库
"""
from torchattacks import AutoAttack
adversary = AutoAttack(model, eps=epsilon)
correct_clean = 0
correct_adv = 0
total = 0
for images, labels in test_loader:
# 干净准确率
with torch.no_grad():
outputs = model(images)
_, preds = outputs.max(1)
correct_clean += preds.eq(labels).sum().item()
# 对抗准确率
adv_images = adversary(images, labels)
with torch.no_grad():
outputs = model(adv_images)
_, preds = outputs.max(1)
correct_adv += preds.eq(labels).sum().item()
total += labels.size(0)
return {
'clean_accuracy': correct_clean / total,
'robust_accuracy': correct_adv / total
}18. 动手实验:用对抗训练提升ResNet的鲁棒性
18.1 完整训练脚本
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
from torchvision.models import resnet18
def train_robust_resnet(epochs=110, batch_size=128, epsilon=8/255):
"""
用对抗训练训练鲁棒的ResNet
数据集:CIFAR-10
模型:ResNet-18
"""
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 数据加载
transform_train = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
])
transform_test = transforms.Compose([
transforms.ToTensor(),
])
train_set = torchvision.datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform_train)
test_set = torchvision.datasets.CIFAR10(root='./data', train=False,
download=True, transform=transform_test)
train_loader = torch.utils.data.DataLoader(train_set, batch_size=batch_size,
shuffle=True, num_workers=4)
test_loader = torch.utils.data.DataLoader(test_set, batch_size=batch_size,
shuffle=False, num_workers=4)
# 模型
model = resnet18(num_classes=10).to(device)
# 对抗训练器
trainer = PGDTraining(
model=model,
epsilon=epsilon,
alpha=epsilon/4,
num_iter=7,
lr=0.1,
weight_decay=5e-4
)
# 训练循环
for epoch in range(epochs):
# 训练
train_stats = trainer.train_epoch(train_loader, epoch)
# 评估
if (epoch + 1) % 10 == 0:
test_stats = evaluate(model, test_loader, epsilon, device)
print(f"Epoch {epoch+1}/{epochs}")
print(f" Train - Loss: {train_stats['loss']:.4f}, "
f"Clean Acc: {train_stats['clean_acc']:.2f}%, "
f"Adv Acc: {train_stats['adv_acc']:.2f}%")
print(f" Test - Clean Acc: {test_stats['clean_acc']:.2f}%, "
f"Robust Acc: {test_stats['robust_acc']:.2f}%")
return model
def evaluate(model, test_loader, epsilon, device):
"""评估模型"""
model.eval()
correct_clean = 0
correct_adv = 0
total = 0
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
# 干净准确率
with torch.no_grad():
outputs = model(images)
_, preds = outputs.max(1)
correct_clean += preds.eq(labels).sum().item()
# 对抗准确率(用PGD-20评估)
adv_images = pgd_attack(model, images, labels, epsilon, num_iter=20)
with torch.no_grad():
outputs = model(adv_images)
_, preds = outputs.max(1)
correct_adv += preds.eq(labels).sum().item()
total += labels.size(0)
return {
'clean_acc': 100. * correct_clean / total,
'robust_acc': 100. * correct_adv / total
}
def pgd_attack(model, images, labels, epsilon, alpha=None, num_iter=20):
"""PGD攻击"""
if alpha is None:
alpha = epsilon / 4
original_images = images.detach()
images = images + torch.zeros_like(images).uniform_(-epsilon, epsilon)
images = torch.clamp(images, 0, 1)
for _ in range(num_iter):
images.requires_grad = True
outputs = model(images)
loss = F.cross_entropy(outputs, labels)
model.zero_grad()
loss.backward()
with torch.no_grad():
images = images + alpha * images.grad.sign()
images = torch.maximum(images, original_images - epsilon)
images = torch.minimum(images, original_images + epsilon)
images = torch.clamp(images, 0, 1)
return images.detach()
if __name__ == '__main__':
model = train_robust_resnet()
torch.save(model.state_dict(), 'robust_resnet_cifar10.pth')18.2 预期结果
使用上述脚本在CIFAR-10上训练,预期结果:
| 训练方法 | 干净准确率 | 鲁棒准确率(PGD-20) |
|---|---|---|
| 标准训练 | ~95% | ~0% |
| PGD对抗训练 | ~85% | ~50% |
| TRADES | ~88% | ~52% |
可以看到,对抗训练确实能显著提升鲁棒性,但代价是干净准确率下降约10个百分点。
19. 学术引用与参考文献
- Madry, A., et al. (2017). “Towards Deep Learning Models Resistant to Adversarial Attacks.” ICLR.
- Goodfellow, I. J., et al. (2015). “Explaining and Harnessing Adversarial Examples.” ICLR.
- Wong, E., & Kolter, J. Z. (2018). “Provable Defenses against Adversarial Examples via the Convex Outer Adversarial Polytope.” ICML.
- Wong, E., et al. (2020). “Fast Is Better Than Free: Revisiting Adversarial Fine-Tuning.” ICLR.
- Shafahi, A., et al. (2019). “Adversarial Training for Free!” NeurIPS.
- Zhang, H., et al. (2019). “Theoretically Principled Trade-off between Robustness and Accuracy.” ICML.
- Wang, Y., et al. (2019). “Improving Adversarial Robustness via Misclassification Aware Adversarial Training.” NeurIPS.
- Cohen, J. M., et al. (2019). “Certified Adversarial Robustness via Randomized Smoothing.” ICML.
- Carlini, N., et al. (2019). “On Evaluating Adversarial Robustness.” arXiv.