关键词
| 术语 | 英文 | 核心概念 |
|---|---|---|
| 对抗样本 | Adversarial Example | 精心设计的导致模型误分类的扰动输入 |
| 快速梯度符号法 | FGSM | 基于梯度的单步攻击方法 |
| 投影梯度下降 | PGD | 迭代式攻击的代表性方法 |
| Carlini-Wagner攻击 | C&W Attack | 优化的 L2 攻击方法 |
| 扰动 | Perturbation | 添加到原始输入的微小噪声 |
| 白盒攻击 | White-box Attack | 攻击者完全了解模型参数的攻击 |
| 黑盒攻击 | Black-box Attack | 攻击者只能访问模型输入输出的攻击 |
| 对抗补丁 | Adversarial Patch | 物理世界中可打印的对抗攻击 |
| 欺骗攻击 | Evasion Attack | 在推理阶段规避检测的攻击 |
| 毒性攻击 | Poisoning Attack | 在训练阶段注入恶意样本的攻击 |
| 对抗训练 | Adversarial Training | 使用对抗样本增强模型鲁棒性 |
| 蒸馏防御 | Defensive Distillation | 通过知识蒸馏提高模型平滑性 |
| 输入变换 | Input Transformation | 对输入进行预处理防御 |
| 可证明鲁棒性 | Certified Robustness | 可证明防御边界的理论保证 |
0. 开场故事:一只熊猫的奇幻漂流
2015年的一天,Google Brain的研究员Ian Goodfellow正和同事们讨论一个有趣的现象。他们给一个训练好的深度神经网络看一张图片——一只憨态可掬的熊猫站在竹林里悠闲地啃着竹子。模型信心满满地给出了答案:“panda(熊猫),置信度57.7%”。
然后,研究员往这张图片里加了点”调料”——准确地说,是一组精心计算的、看似随机分布的噪声。如果你眯着眼睛看,加了”调料”后的图片和原图几乎一模一样,一只正常的眼睛根本分辨不出区别。
但当模型再看这张”新”图片时,它眨了眨眼,思考了一秒,然后斩钉截铁地说:“gibbon(长臂猿),置信度99.3%”。
一只熊猫,就这么被”魔法”变成了长臂猿。
这个场景听起来像是科幻小说,但实际上它每天都在AI实验室里发生。这就是我们今天要聊的主题——对抗样本(Adversarial Examples)。
这个故事的意义
对抗样本的存在告诉我们一个重要的事实:现代AI系统并没有我们想象的那么可靠。一个对人类来说几乎无法察觉的小变化,就能让最先进的图像识别系统彻底”眼瞎”。
1. 对抗样本入门:一张”干净”的图片如何骗过AI?
1.1 什么是对抗样本?
想象你有一双魔法眼睛,能看到三维人眼看不到的高维空间。在那个空间里,每一张图片都是一个点,而训练好的神经网络会在这个空间中画出一些”分界线”——我们叫它决策边界。
正常情况下,熊猫这个类别的所有图片点都会聚集在一个区域里,而长臂猿的图片点在另一个区域。只要你待在熊猫的区域里,模型就能认出你是一只熊猫。
但是,问题来了。
在高维空间里,决策边界往往不是平滑的曲线,而是像锯齿一样参差不齐。有时候,熊猫区域的边缘会突然伸出一个”尖刺”,尖刺的尽头离长臂猿区域只有一步之遥。
对抗样本攻击做的事情,就是在图片点上加一个极小的扰动,把这个点往那个尖刺的方向推那么一小步。这小小的一步,刚好能让图片点”越界”,掉进长臂猿的区域。对于人眼来说,这张图片还是一只熊猫;但对于模型来说,它已经变成了一只长臂猿。
这就是为什么我们说对抗样本是”人类不可察觉”的——因为这个扰动确实太小了,可能只是把某个像素的值从254改成255,肉眼根本看不到任何变化。但问题在于,这个扰动的方向是精心计算的,专门找准了决策边界的薄弱点。
1.2 为什么对抗样本值得关注?
你可能会说:“这不就是给图片加点噪声嘛,有什么大不了的?”
这话对了一半。普通的随机噪声确实不会对模型造成太大影响——模型会把那些噪声当作无关信息过滤掉。但对抗样本不一样。
关键区别在于:对抗扰动不是随机的,而是”有目的”的。
你可以把对抗样本想象成一封精心设计的伪装信。普通的噪声就像是信纸上的褶皱和墨迹,邮递员会忽略它们;但对抗扰动就像是在信中藏了一段只有特定收件人才能解读的密语,而这段密语会让收件人做出完全不同的反应。
这就很可怕了。
因为在现实世界中,AI系统正在承担越来越多的关键任务:
- 自动驾驶汽车:靠视觉识别路况、交通标志、行人
- 医疗诊断系统:靠图像识别肿瘤、病灶
- 安防监控系统:靠人脸识别确认身份
- 金融风控系统:靠模型检测欺诈交易
如果这些系统都能被一张”加了料”的图片轻易欺骗,那后果可能就不只是”把熊猫认成长臂猿”这么简单了。
1.3 对抗样本的几个特点
经过这么多年的研究,科学家们发现对抗样本有以下几个有趣的”性格特点”:
特点一:无处不在
几乎所有现代神经网络都存在对抗样本。不管你用的是ResNet、VGG还是最新的Vision Transformer,不管网络有多深、参数有多少,只要你训练的是一个用于分类的神经网络,对抗样本就能被构造出来。这不是某个特定模型的bug,而是深度学习模型的”基因缺陷”。
特点二:可以迁移
这是最让人头疼的特性之一。假设你在自己训练的模型A上生成了一个对抗样本,这个样本往往也能成功欺骗另一个完全不同的模型B。这种”迁移性”意味着,即使攻击者不知道你用的是哪个模型,他也可以用自己训练的模型来生成对抗样本,然后再去攻击你的系统。
特点三:物理世界也有效
早期人们以为对抗样本只能在数字世界逞威风,毕竟谁能在打印出来的图片上精确控制像素值呢?但后来的研究证明,通过打印-拍照这个过程,对抗样本依然能保持攻击效果。这意味着贴在路牌上的贴纸、戴在脸上的眼镜框,都可能成为攻击AI的武器。
2. 对抗样本的数学定义
2.1 形式化定义
说了这么多故事,我们来点硬核的数学。
给定一个分类器 和原始输入 ,对抗样本 满足以下条件:
其中 表示 范数, 是人类感知阈值。
对于定向攻击,目标类别为 ,则需要满足:
2.2 对抗样本的存在性解释
从决策边界角度,对抗样本的存在可以用高维空间的线性近似来解释。对于一个权重向量 和输入 ,模型输出为:
如果 与 符号相同且幅度足够大,即使 极小,也能改变分类结果。
import numpy as np
import torch
import torch.nn as nn
def explain_adversarial_examples(model, x, epsilon=0.1):
"""
解释对抗样本存在性的数值示例
在高维空间中,即使扰动很小,
累积效应也足以改变分类结果
"""
# 获取模型预测
model.eval()
with torch.no_grad():
original_pred = model(x.unsqueeze(0)).argmax(dim=1).item()
# 获取输入梯度(扰动方向)
x.requires_grad = True
output = model(x.unsqueeze(0))
loss = output[0, original_pred]
loss.backward()
gradient = x.grad.data
# 计算梯度方向上的扰动
perturbation = epsilon * torch.sign(gradient)
adversarial_x = x + perturbation
with torch.no_grad():
adversarial_pred = model(adversarial_x.unsqueeze(0)).argmax(dim=1).item()
print(f"原始预测: {original_pred}, 对抗样本预测: {adversarial_pred}")
print(f"扰动范数 L∞: {torch.abs(perturbation).max().item():.4f}")
return adversarial_x, perturbation
# 高维空间线性效应的数学演示
def high_dimensional_linear_effect():
"""
在高维空间中:
- 随机方向扰动的期望范数增长为 O(√d)
- 但正交于特征方向的扰动可以被忽略
- 梯度的符号方向总是"对齐"的
这解释了为什么微小扰动能累积成大影响
"""
d = 10000 # 假设10000维空间
num_samples = 1000
# 随机扰动的平均绝对值
random_perturbations = np.random.randn(num_samples, d)
avg_magnitude = np.mean(np.abs(random_perturbations))
# 梯度方向扰动(所有维度对齐)
aligned_perturbations = np.ones((num_samples, d)) * 0.01
aligned_magnitude = np.mean(np.abs(aligned_perturbations))
print(f"随机扰动平均幅度: {avg_magnitude:.6f}")
print(f"对齐扰动平均幅度: {aligned_magnitude:.4f}")
print(f"比率: {aligned_magnitude / avg_magnitude:.2f}x")2.3 决策边界与对抗样本
对抗样本的存在与神经网络的决策边界结构密切相关。在高维空间中,决策边界往往呈现出复杂的几何结构,导致存在”尖锐”的角落:
class DecisionBoundaryAnalyzer:
"""
决策边界分析器
分析神经网络决策边界的几何特性
"""
def __init__(self, model):
self.model = model
self.model.eval()
def compute_curvature(self, x, epsilon=0.01, num_directions=100):
"""
计算决策边界的曲率
高曲率区域更可能产生对抗样本
"""
curvatures = []
for _ in range(num_directions):
# 随机方向
direction = torch.randn_like(x)
direction = direction / direction.norm()
# 计算沿方向的二阶导数
x_plus = (x + epsilon * direction).requires_grad_(True)
x_minus = (x - epsilon * direction).requires_grad_(True)
# 一阶导数
loss_plus = self.model(x_plus).max()
loss_minus = self.model(x_minus).max()
grad_plus = torch.autograd.grad(loss_plus, x_plus)[0]
grad_minus = torch.autograd.grad(loss_minus, x_minus)[0]
# 二阶差分近似曲率
curvature = torch.norm(grad_plus - grad_minus) / (2 * epsilon)
curvatures.append(curvature.item())
return np.mean(curvatures)
def find_adversarial_direction(self, x, target_class):
"""
找到指向目标类别的对抗方向
返回:
- 对抗方向的单位向量
- 到达决策边界的估计距离
"""
x.requires_grad = True
# 获取目标类别的logit
output = self.model(x.unsqueeze(0))
target_logit = output[0, target_class]
# 梯度指向增加目标logit的方向
grad = torch.autograd.grad(target_logit, x)[0]
return grad / (grad.norm() + 1e-8)3. FGSM攻击:快速梯度符号法,攻击只需一步
3.1 一个故事理解FGSM
FGSM,全称Fast Gradient Sign Method,中文叫”快速梯度符号法”。它的发明者Ian Goodfellow就是开头那个故事的当事人。
想象你要把一个人从”好人”那边推到”坏人”那边,但你只能用一种特殊的推法:你只能选择推还是不推,不能控制力度大小(只能选择正负符号)。
FGSM就是这么干的。它不是漫无目的地乱推,而是在模型的”损失函数地形图”上,找到了那个最陡的下山方向,然后沿着这个方向狠狠地推一步。
3.2 算法原理
FGSM的计算公式出奇的简单:
等等,让我解释一下每个符号的意思:
- 是我们生成的对抗样本
- 是原始图片
- 是扰动的步长(一般设成0.03左右,意味着每个像素最多移动0.03)
- 是损失函数对输入图片的梯度
- 是符号函数,把正数变成+1,负数变成-1
这个公式做的事情很简单:找到让损失增大的方向,然后朝那个方向迈一步。
3.3 FGSM 实现
import torch
import torch.nn.functional as F
def fgsm_attack(model, images, labels, epsilon=0.03):
"""
快速梯度符号法(FGSM)攻击
参数:
model: 目标分类器
images: 输入图像 (B, C, H, W)
labels: 真实标签 (B,)
epsilon: 扰动幅度
返回:
adversarial_images: 对抗样本
"""
# 保存原始图像用于恢复
images.requires_grad = True
# 前向传播
outputs = model(images)
# 计算损失
loss = F.cross_entropy(outputs, labels)
# 反向传播获取梯度
model.zero_grad()
loss.backward()
# 获取梯度并计算扰动
gradient = images.grad.data
perturbation = epsilon * torch.sign(gradient)
# 生成对抗样本
adversarial_images = images + perturbation
# 确保扰动后图像仍在有效范围内
adversarial_images = torch.clamp(adversarial_images, 0, 1)
return adversarial_images, perturbation
def fgsm_targeted_attack(model, images, target_labels, epsilon=0.03):
"""
定向 FGSM 攻击:使模型输出特定目标类别
"""
images.requires_grad = True
outputs = model(images)
# 定向攻击:最大化目标类别的损失
loss = -F.cross_entropy(outputs, target_labels)
model.zero_grad()
loss.backward()
gradient = images.grad.data
perturbation = epsilon * torch.sign(gradient)
adversarial_images = images + perturbation
adversarial_images = torch.clamp(adversarial_images, 0, 1)
return adversarial_images, perturbationFGSM 的特点
- 计算效率高:只需一次前向和反向传播
- 单步攻击:相比迭代方法更快
- 可解释性强:扰动方向由损失函数的梯度决定
- 局限性:对于使用防御技术(如对抗训练)的模型效果有限
4. PGD攻击:FGSM的迭代增强版
4.1 为什么需要PGD?
FGSM虽然快,但它的问题也很明显:只走一步,可能走不到最优点。
打个比方,你想从山脚爬到山顶,但FGSM就像只迈一步——如果这一步刚好迈到了一块大石头上,你就被卡住了,要么在原地踏步,要么只能绕路。
PGD(Projected Gradient Descent,投影梯度下降)则更像是真正的登山者。它会小步快跑,每一步都重新判断方向,然后继续往上爬。如果走到了不该走的地方(比如扰动超出了允许范围),它会用”投影”操作把脚步拉回到正确的范围内。
4.2 算法原理
PGD的迭代公式:
其中 是投影到允许扰动集合 的操作, 是步长。
4.3 PGD 实现
def pgd_attack(model, images, labels, epsilon=0.03, alpha=0.003, num_iter=10,
targeted=False, target_labels=None):
"""
投影梯度下降(PGD)攻击
参数:
model: 目标分类器
images: 原始图像
labels: 真实标签
epsilon: 最大扰动范数
alpha: 每次迭代的步长
num_iter: 迭代次数
targeted: 是否为定向攻击
target_labels: 定向攻击的目标标签
"""
# 保存原始图像
original_images = images.detach().clone()
# 初始化:随机起点(增加攻击成功率)
images = images.detach() + torch.zeros_like(images).uniform_(-epsilon, epsilon)
images = torch.clamp(images, 0, 1)
for i in range(num_iter):
images.requires_grad = True
outputs = model(images)
if targeted:
# 定向攻击:最大化目标类别概率
loss = -F.cross_entropy(outputs, target_labels)
else:
# 非定向攻击:最小化真实类别概率
loss = F.cross_entropy(outputs, labels)
model.zero_grad()
loss.backward()
# 更新扰动
gradient = images.grad.data
images = images.detach() + alpha * torch.sign(gradient)
# 投影到允许范围
images = torch.maximum(images, original_images - epsilon)
images = torch.minimum(images, original_images + epsilon)
images = torch.clamp(images, 0, 1)
return images
def pgd_attack_l2(model, images, labels, epsilon=1.0, alpha=0.1, num_iter=10,
targeted=False, target_labels=None):
"""
L2 范数约束的 PGD 攻击
"""
original_images = images.detach().clone()
# 随机初始化
delta = torch.zeros_like(images)
delta.normal_()
delta = delta / torch.sqrt(torch.sum(delta ** 2, dim=(1, 2, 3), keepdim=True))
delta = delta * torch.rand(images.size(0), 1, 1, 1).to(images.device) * epsilon
images = (original_images + delta).clamp(0, 1)
for i in range(num_iter):
images.requires_grad = True
outputs = model(images)
if targeted:
loss = -F.cross_entropy(outputs, target_labels)
else:
loss = F.cross_entropy(outputs, labels)
model.zero_grad()
loss.backward()
gradient = images.grad.data
# L2 归一化梯度方向
grad_norm = torch.sqrt(torch.sum(gradient ** 2, dim=(1, 2, 3), keepdim=True))
gradient = gradient / (grad_norm + 1e-10)
# 更新并投影到 L2 球
images = images.detach() + alpha * gradient
delta = images - original_images
delta_norm = torch.sqrt(torch.sum(delta ** 2, dim=(1, 2, 3), keepdim=True))
delta = delta / (delta_norm + 1e-10) * torch.minimum(delta_norm, torch.tensor(epsilon))
images = (original_images + delta).clamp(0, 1)
return images5. CW攻击:更隐蔽的对抗攻击
5.1 CW攻击的设计哲学
Carlini-Wagner攻击(简称C&W)代表了另一种攻击思路。如果说FGSM和PGD是”大力出奇迹”,那C&W就是”精准打击”。
C&W的核心目标是:在保证攻击成功的前提下,让扰动尽可能小。
这就像绑匪和人质谈判,绑匪(攻击者)希望既拿到赎金(攻击成功),又不被人质发现异常(扰动最小)。
5.2 算法原理
C&W的攻击目标是求解一个优化问题:
其中 是分类器输出转化为标量的辅助函数:
是分类器的 logits 输出, 是目标类别, 是置信度参数。
5.3 C&W 攻击实现
class CWL2Attack:
"""
Carlini-Wagner L2 攻击实现
使用变量替换和重参数化技巧将约束优化问题转化为无约束优化
"""
def __init__(self, model, kappa=0, max_iter=1000, learning_rate=0.01):
self.model = model
self.kappa = kappa # 置信度参数
self.max_iter = max_iter
self.lr = learning_rate
def attack(self, images, target_labels, targeted=True):
"""
执行 C&W L2 攻击
"""
batch_size = images.size(0)
device = images.device
# 初始化扰动变量(使用 tanh 变换确保有界)
w = torch.zeros_like(images)
w.requires_grad = True
optimizer = torch.optim.Adam([w], lr=self.lr)
for iteration in range(self.max_iter):
optimizer.zero_grad()
# 重参数化:w -> delta(-1 到 1)
delta = 0.5 * (torch.tanh(w) + 1) - images
# 计算 logits
adv_images = images + delta
logits = self.model(adv_images)
# 辅助函数 f
if targeted:
# 定向攻击:使目标类别 logit 最大
one_hot = F.one_hot(target_labels, num_classes=logits.size(-1)).float()
other_logits = ((1 - one_hot) * logits - one_hot * 1e9)
f = torch.max(other_logits, dim=-1)[0] - logits.gather(1, target_labels.unsqueeze(1)).squeeze()
else:
# 非定向攻击:使真实类别 logit 最小
real_logits = logits.gather(1, target_labels.unsqueeze(1)).squeeze()
other_logits = torch.where(
torch.arange(logits.size(-1), device=device).unsqueeze(0) == target_labels.unsqueeze(1),
torch.tensor(-1e9, device=device),
logits
)
f = real_logits - torch.max(other_logits, dim=-1)[0]
# 扰动幅度(使用变换后的 delta)
delta_reshaped = delta.view(batch_size, -1)
perturbation_norm = torch.sum(delta_reshaped ** 2, dim=-1)
# 损失函数
loss = perturbation_norm + 0.01 * f.sum()
loss.backward()
optimizer.step()
# 生成最终对抗样本
delta = 0.5 * (torch.tanh(w.detach()) + 1) - images
adversarial_images = (images + delta).clamp(0, 1)
return adversarial_images, delta6. 黑盒攻击与白盒攻击:攻击者需要知道什么?
6.1 白盒攻击:知己知彼
白盒攻击就像是一场信息不对称的战争——攻击者对目标模型了如指掌,包括:
- 模型的架构(多少层、什么类型)
- 模型的参数(权重矩阵的具体数值)
- 模型的梯度(损失函数如何变化)
这听起来很苛刻,但在很多场景下这确实是可能的。比如:
- 公司内部人员攻击自己公司的AI系统
- 开源模型的部署版本被攻击者下载分析
- 攻击者通过API逆向工程猜出模型结构
白盒攻击的优势是强大、精准,缺点是需要更多信息。
6.2 黑盒攻击:盲人摸象
黑盒攻击则相反,攻击者几乎什么都不知道,只能:
- 输入图片
- 看到输出结果(分类结果、置信度等)
这听起来像是一个不可能完成的任务,但实际上研究者发现了几个”作弊”的方法:
方法一:查询攻击
攻击者像在测试API一样,反复向目标系统发送图片,根据返回的预测结果推断模型的弱点。比如,如果把图片某个区域变黑,置信度从99%掉到了50%,说明这个区域对分类很重要。
方法二:迁移攻击
这是最实用的黑盒攻击方法。攻击者先训练一个自己的模型(可以是同类型的,也可以是不同类型的),然后在这个模型上生成对抗样本。由于对抗样本的迁移性,这些样本往往也能欺骗目标模型。
6.3 攻击方法对比
| 攻击类型 | 所需信息 | 攻击强度 | 实际可行性 |
|---|---|---|---|
| 白盒攻击 | 模型结构+参数+梯度 | 最强 | 中等(需要获取模型) |
| 黑盒查询攻击 | 输入输出 | 中等 | 较高(API可访问时) |
| 黑盒迁移攻击 | 无需目标模型 | 较强 | 高(最常用) |
7. 动手实验:用自己的图片生成对抗样本
7.1 环境准备
pip install torch torchvision numpy matplotlib7.2 完整实验代码
"""
对抗样本生成实战
用你自己的图片生成对抗样本,看看能不能骗过模型
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
# 加载预训练的ResNet模型
model = torchvision.models.resnet18(pretrained=True)
model.eval()
# 定义FGSM攻击
def fgsm_attack(image, epsilon, gradient):
# 获取梯度的符号
sign_gradient = gradient.sign()
# 生成对抗样本
perturbed_image = image + epsilon * sign_gradient
# 确保像素值在有效范围内
perturbed_image = torch.clamp(perturbed_image, 0, 1)
return perturbed_image
# 定义PGD攻击
def pgd_attack(model, image, label, epsilon, alpha, num_iter):
original_image = image.clone()
# 随机初始化
perturbed_image = image.clone() + torch.zeros_like(image).uniform_(-epsilon, epsilon)
perturbed_image = torch.clamp(perturbed_image, 0, 1)
for i in range(num_iter):
perturbed_image.requires_grad = True
# 前向传播
output = model(perturbed_image)
loss = F.cross_entropy(output, label)
# 反向传播
model.zero_grad()
loss.backward()
# 更新扰动
gradient = perturbed_image.grad.detach()
perturbed_image = perturbed_image.detach() + alpha * gradient.sign()
# 投影到epsilon球内
delta = torch.clamp(perturbed_image - original_image, min=-epsilon, max=epsilon)
perturbed_image = torch.clamp(original_image + delta, 0, 1)
return perturbed_image
# 可视化函数
def visualize_results(original, perturbed, original_pred, perturbed_pred, epsilon):
"""展示原始图片和对抗样本的对比"""
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# 原始图片
axes[0].imshow(original.squeeze().permute(1, 2, 0).numpy())
axes[0].set_title(f'原始图片\n预测: {original_pred}')
axes[0].axis('off')
# 扰动(放大显示)
perturbation = perturbed - original
axes[1].imshow(perturbation.squeeze().permute(1, 2, 0).numpy() * 10) # 放大10倍
axes[1].set_title('扰动(放大10倍)')
axes[1].axis('off')
# 对抗样本
axes[2].imshow(perturbed.squeeze().permute(1, 2, 0).numpy())
axes[2].set_title(f'对抗样本 (ε={epsilon})\n预测: {perturbed_pred}')
axes[2].axis('off')
plt.tight_layout()
plt.savefig('adversarial_example.png', dpi=150)
plt.show()
def main():
# 加载并预处理图片
# 这里使用ImageNet的标签
with open('imagenet_classes.txt') as f:
classes = [line.strip() for line in f.readlines()]
# 加载你自己的图片
image = Image.open('your_image.jpg') # 换成你的图片路径
transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
])
input_image = transform(image).unsqueeze(0)
# 获取原始预测
with torch.no_grad():
output = model(input_image)
probs = F.softmax(output, dim=1)
original_pred = classes[output.argmax().item()]
original_conf = probs.max().item()
print(f"原始预测: {original_pred}, 置信度: {original_conf:.2%}")
# 测试不同的epsilon值
epsilons = [0.01, 0.03, 0.1, 0.3]
for eps in epsilons:
print(f"\n--- epsilon = {eps} ---")
# FGSM攻击
input_image.requires_grad = True
output = model(input_image)
label = output.argmax(dim=1)
loss = F.cross_entropy(output, label)
model.zero_grad()
loss.backward()
gradient = input_image.grad
perturbed_image_fgsm = fgsm_attack(input_image.detach(), eps, gradient)
with torch.no_grad():
output_fgsm = model(perturbed_image_fgsm)
pred_fgsm = classes[output_fgsm.argmax().item()]
conf_fgsm = F.softmax(output_fgsm, dim=1).max().item()
print(f"FGSM - 预测: {pred_fgsm}, 置信度: {conf_fgsm:.2%}")
# PGD攻击
label = output.argmax(dim=1)
perturbed_image_pgd = pgd_attack(model, input_image.detach(), label, eps, eps/4, 10)
with torch.no_grad():
output_pgd = model(perturbed_image_pgd)
pred_pgd = classes[output_pgd.argmax().item()]
conf_pgd = F.softmax(output_pgd, dim=1).max().item()
print(f"PGD - 预测: {pred_pgd}, 置信度: {conf_pgd:.2%}")
# 可视化
if eps == 0.1:
visualize_results(
input_image.detach(),
perturbed_image_pgd,
f"{original_pred} ({original_conf:.1%})",
f"{pred_pgd} ({conf_pgd:.1%})",
eps
)
if __name__ == "__main__":
main()8. 对抗样本为什么存在?线性解释 vs 非线性解释
8.1 Goodfellow的线性解释
2014年,Ian Goodfellow提出了一个简洁的解释:对抗样本的存在是因为神经网络太”线性”了。
这个解释听起来有点反直觉——我们明明知道神经网络是非线性函数,怎么会说它太线性了呢?
关键在于权重的分布。
在高维空间中,神经网络的大多数区域确实表现得像线性函数。即使激活函数是非线性的,但如果权重集中在一个较小的范围内,整体行为也会接近线性。
线性解释的核心思想是:在一个很高的维度里,即使是微小的扰动,只要方向和权重的方向一致,就能累积成很大的影响。
用公式来表示,假设输入的扰动是 ,模型的权重是 ,那么:
只要 足够大,就能改变分类结果。而在高维空间中,只要 和 的方向足够一致,这个值可以很大。
8.2 非线性解释:决策边界的弯曲
后来的研究者提出了不同的观点。他们认为对抗样本的存在不是因为网络太线性,而是因为决策边界太”弯曲”。
想象一张纸上有两个点A和B,中间有一条弯弯曲曲的线把它们分开。从A到B的直线距离很短,但如果你必须待在纸的同一侧,就必须绕很长一段路才能到达B。
对抗样本就是找到了那个”近道”——通过在决策边界的尖锐角落处,轻轻一推就能跨越边界。
8.3 Ilyas等人的”特征”解释
2019年,MIT的Ilyas等人提出了一个更具争议性的解释:对抗样本之所以有效,是因为它们利用了”非鲁棒特征”。
这个观点认为,神经网络在训练过程中不仅学习了真正有意义的特征(人类能理解的特征),还学习了那些”捷径特征”——这些特征对分类有帮助,但对人类来说毫无意义,甚至可能是误导性的。
对抗扰动的作用,就是专门放大这些非鲁棒特征,让它们压过真正的鲁棒特征。
这个解释的可怕之处在于:也许我们训练的模型本身就是建立在”错误的基础”之上的。
9. 对抗防御方法对比:三大流派
9.1 对抗训练:以毒攻毒
对抗训练是最直接的防御方法——既然对抗样本能骗过模型,那就让模型在训练的时候就见见世面,多看看对抗样本长什么样。
类比一下就是:与其给士兵看敌人的照片(干净样本),不如让他们在演习中直接面对敌人的攻击(对抗样本),这样在真正遇到敌人时就不容易慌了。
class AdversarialTraining:
"""
对抗训练
在训练过程中包含对抗样本
提高模型对对抗攻击的鲁棒性
"""
def __init__(self, model, epsilon=0.03, alpha=0.003, num_iter=7):
self.model = model
self.epsilon = epsilon
self.alpha = alpha
self.num_iter = num_iter
def train_step(self, images, labels, optimizer):
"""
对抗训练步骤
策略:使用PGD攻击生成对抗样本进行训练
"""
# 生成对抗样本
adversarial_images = pgd_attack(
self.model, images, labels,
epsilon=self.epsilon,
alpha=self.alpha,
num_iter=self.num_iter
)
# 联合训练:真实样本 + 对抗样本
optimizer.zero_grad()
# 真实样本损失
real_outputs = self.model(images)
real_loss = F.cross_entropy(real_outputs, labels)
# 对抗样本损失
adv_outputs = self.model(adversarial_images)
adv_loss = F.cross_entropy(adv_outputs, labels)
# 总损失
total_loss = real_loss + adv_loss
total_loss.backward()
optimizer.step()
return total_loss.item()9.2 输入变换:见招拆招
输入变换防御的思路是:既然对抗扰动是精心设计的微小变化,那我就通过一些”粗糙”的预处理来消除它们。
常见的方法包括:
- 随机裁剪:随机裁剪图片的不同区域,然后缩放到原尺寸
- 随机缩放:对图片进行小幅度的缩放
- 位深度压缩:把像素值从256级降到16级
- JPEG压缩:利用JPEG的有损压缩特性平滑掉微小扰动
这些方法的共同特点是:它们对正常的图片影响很小,但能有效地破坏对抗扰动的结构。
class InputTransformationDefense:
"""
输入变换防御
通过对输入进行预处理来抵御对抗攻击
"""
def __init__(self):
self.transforms = []
def add_transform(self, transform_fn):
"""添加变换"""
self.transforms.append(transform_fn)
def apply_randomization(self, x, num_samples=5):
"""
输入随机化
对输入应用随机变换,平均多次预测
"""
predictions = []
for _ in range(num_samples):
x_transformed = x.clone()
# 随机裁剪
if torch.rand(1).item() > 0.5:
x_transformed = self.random_crop(x_transformed)
# 随机翻转
if torch.rand(1).item() > 0.5:
x_transformed = torch.flip(x_transformed, dims=[3])
# 随机缩放
if torch.rand(1).item() > 0.5:
x_transformed = self.random_scale(x_transformed)
predictions.append(x_transformed)
# 返回原始尺度(假设平均处理)
return x_transformed # 简化:实际需要逆变换
def random_crop(self, x, crop_size=None):
"""随机裁剪"""
if crop_size is None:
crop_size = int(x.size(-1) * 0.9)
h, w = x.size(-2), x.size(-1)
top = torch.randint(0, h - crop_size + 1, (1,)).item()
left = torch.randint(0, w - crop_size + 1, (1,)).item()
return F.interpolate(
x[:, :, top:top+crop_size, left:left+crop_size],
size=(h, w),
mode='bilinear',
align_corners=False
)9.3 认证防御:给防御加个”保险”
前面两种方法都是”经验性”的防御——在实践中可能有效,但我们不知道它们的防御边界在哪里。
认证防御则是提供理论保证:如果一个样本通过了认证,那在某个扰动范围内,模型的行为是可预测的。
最著名的认证防御方法是Cohen等人提出的随机平滑(Randomized Smoothing)。它的核心思想是:
- 给输入加很多随机噪声
- 对加噪后的输入进行分类
- 如果某个类别在统计上显著多于其他类别,就认为原始输入在该扰动范围内是鲁棒的
class CertifiedDefense:
"""
可证明鲁棒性防御
提供可证明的防御边界
"""
def __init__(self, model, epsilon=0.1):
self.model = model
self.epsilon = epsilon
def verify_sample(self, x, label, timeout=60):
"""
验证样本的鲁棒性
使用区间分支边界(IBP)方法
返回:是否鲁棒 + 证明边界
"""
device = next(self.model.parameters()).device
# 初始化上界和下界
lower = x - self.epsilon
upper = x + self.epsilon
lower = torch.clamp(lower, 0, 1)
upper = torch.clamp(upper, 0, 1)
# 迭代优化边界
for iteration in range(10):
# 计算中间点
mid = (lower + upper) / 2
# 计算在mid点的输出
mid_out = self.model(mid)
mid_pred = mid_out.argmax(dim=1)
# 检查是否改变预测
if mid_pred != label:
# 预测改变了,缩小上界
upper = mid
else:
# 预测未变,增大下界
lower = mid
# 返回验证结果
is_robust = (lower.max() >= upper.min())
return is_robust, lower, upper9.4 三大防御流派对比
| 防御类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 对抗训练 | 效果好,能显著提升鲁棒性 | 训练时间长,准确率可能下降 | 高安全要求的系统 |
| 输入变换 | 实现简单,计算开销小 | 防御强度有限,效果不稳定 | 资源受限的部署环境 |
| 认证防御 | 有理论保证,结果可信 | 计算开销大,认证边界可能较宽松 | 安全关键系统 |
10. 对抗训练实战:如何在PyTorch中实现PGD对抗训练
10.1 完整的对抗训练流程
"""
PGD对抗训练完整实现
包含课程学习策略和TRADES损失
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
import torchvision.models as models
def pgd_attack(model, images, labels, epsilon, alpha, num_iter, targeted=False, target_labels=None):
"""PGD攻击生成器"""
original_images = images.detach().clone()
# 随机初始化
images = images.detach() + torch.zeros_like(images).uniform_(-epsilon, epsilon)
images = torch.clamp(images, 0, 1)
for i in range(num_iter):
images.requires_grad = True
outputs = model(images)
if targeted:
loss = -F.cross_entropy(outputs, target_labels)
else:
loss = F.cross_entropy(outputs, labels)
model.zero_grad()
loss.backward()
gradient = images.grad.data
images = images.detach() + alpha * torch.sign(gradient)
# 投影
delta = torch.clamp(images - original_images, min=-epsilon, max=epsilon)
images = torch.clamp(original_images + delta, 0, 1)
return images
class PGDAdversarialTrainer:
"""
PGD对抗训练器
支持:
- 标准对抗训练
- 课程对抗训练(逐渐增加攻击强度)
- TRADES损失
"""
def __init__(self, model, epsilon=0.03, alpha=0.003, num_iter=7):
self.model = model
self.epsilon = epsilon
self.alpha = alpha
self.num_iter = num_iter
def train_epoch(self, dataloader, optimizer, epoch, total_epochs, use_curriculum=True):
"""训练一个epoch"""
self.model.train()
total_loss = 0
correct = 0
total = 0
for batch_idx, (images, labels) in enumerate(dataloader):
optimizer.zero_grad()
# 课程学习:逐渐增加epsilon
if use_curriculum:
progress = epoch / total_epochs
current_epsilon = self.epsilon * (0.5 + 0.5 * progress)
current_alpha = self.alpha * (0.5 + 0.5 * progress)
current_iter = max(3, int(self.num_iter * (0.5 + 0.5 * progress)))
else:
current_epsilon = self.epsilon
current_alpha = self.alpha
current_iter = self.num_iter
# 生成对抗样本
adv_images = pgd_attack(
self.model, images, labels,
epsilon=current_epsilon,
alpha=current_alpha,
num_iter=current_iter
)
# 标准对抗训练损失
outputs_clean = self.model(images)
outputs_adv = self.model(adv_images)
# 干净样本损失
loss_clean = F.cross_entropy(outputs_clean, labels)
# 对抗样本损失
loss_adv = F.cross_entropy(outputs_adv, labels)
# 总损失
loss = 0.5 * loss_clean + 0.5 * loss_adv
loss.backward()
optimizer.step()
total_loss += loss.item()
# 计算准确率(使用干净样本和对抗样本的平均)
with torch.no_grad():
pred_clean = outputs_clean.argmax(dim=1)
pred_adv = outputs_adv.argmax(dim=1)
correct += ((pred_clean == labels).sum().item() +
(pred_adv == labels).sum().item())
total += 2 * labels.size(0)
avg_loss = total_loss / len(dataloader)
accuracy = 100. * correct / total
return avg_loss, accuracy
class TRADESLoss:
"""
TRADES (TRADE-off between robustness and accuracy)
对抗训练的另一种损失函数
同时优化干净样本准确率和鲁棒性
"""
def __init__(self, model, beta=1.0):
self.model = model
self.beta = beta
def compute_loss(self, images, labels, epsilon=0.03):
"""
TRADES 损失
Loss = CE(model(x), y) + β * KL(model(x)||model(x+ε))
"""
# 干净样本的预测
outputs = self.model(images)
# 生成对抗样本
adversarial_images = pgd_attack(
self.model, images, labels,
epsilon=epsilon, num_iter=7
)
# 对抗样本的预测
outputs_adv = self.model(adversarial_images)
# 交叉熵损失
ce_loss = F.cross_entropy(outputs, labels)
# KL散度损失
kl_loss = F.kl_div(
F.log_softmax(outputs, dim=1),
F.log_softmax(outputs_adv, dim=1),
reduction='batchmean'
)
return ce_loss + self.beta * kl_loss
def main():
# 加载模型和数据
model = models.resnet18(pretrained=False, num_classes=10)
# 使用CIFAR-10数据集作为示例
transform = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
])
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform)
trainloader = DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2)
# 初始化训练器
trainer = PGDAdversarialTrainer(model, epsilon=8/255, alpha=2/255, num_iter=7)
optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4)
scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=[100, 150], gamma=0.1)
# 训练
num_epochs = 200
for epoch in range(num_epochs):
loss, accuracy = trainer.train_epoch(
trainloader, optimizer, epoch, num_epochs, use_curriculum=True
)
if (epoch + 1) % 10 == 0:
print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {loss:.4f}, Accuracy: {accuracy:.2f}%")
scheduler.step()
# 保存模型
torch.save(model.state_dict(), 'robust_model.pth')
print("对抗训练完成!模型已保存。")
if __name__ == "__main__":
main()11. 对抗样本的物理世界攻击:贴在路牌上的贴纸能骗过自动驾驶
11.1 从数字到现实
早期很多人认为对抗样本只是”实验室里的玩具”,毕竟在实际场景中,谁能精确控制打印出来的图片的像素值呢?
但2016年的一篇论文彻底打破了这个幻想。研究者们证明了对抗样本可以通过打印-拍照这个过程依然保持攻击效果。这意味着,只要你能在物理世界中制造出对抗扰动,就能欺骗真实的AI系统。
11.2 著名的物理攻击案例
案例一:Stop标志上的贴纸
2017年,OpenAI的研究者设计了一种特殊的贴纸,贴在Stop停车标志上后,自动驾驶的视觉系统会把它误识别为”限速45英里”的标志。如果这个攻击发生在真实的道路上,后果不堪设想。
这种攻击的厉害之处在于:贴纸看起来就像是一堆抽象的涂鸦,完全不会引起路人的警觉,但实际上它包含了精心设计的对抗扰动。
案例二:对抗眼镜
研究者还设计了一种特殊的眼镜框图案,佩戴这种眼镜的人可以让面部识别系统误认为他们是另一个人。这种攻击的可怕之处在于,它针对的是人脸识别系统,可以用于绕过安检或者冒充他人身份。
案例三:对抗补丁
Brown等人提出了”对抗补丁”的概念,这种补丁可以打印出来贴在任意位置,就能让图像分类器输出攻击者想要的任意类别。就像是给AI系统施了一个”障眼法”,让它看到什么都会认成别的东西。
11.3 EOT攻击:让对抗样本更”抗造”
为了让对抗样本在物理世界中也有效,研究者提出了EOT(Expectation Over Transformation)攻击。
物理世界中的图片会经历各种变换:不同角度的光照、手机摄像头造成的畸变、打印机的色彩失真等等。EOT攻击的核心思想是:在生成对抗扰动的时候,就已经考虑了这些可能的变换,确保在经过这些变换后,攻击效果依然存在。
class EOTAttack:
"""
EOT(期望过转换)攻击
核心思想:
在多种随机变换下优化对抗扰动
使得变换后的对抗样本仍然具有攻击性
适用于:
- 物理世界攻击
- 对抗补丁
- 相机传感器攻击
"""
def __init__(self, model, transformations, epsilon=0.1, num_iter=100):
self.model = model
self.transformations = transformations
self.epsilon = epsilon
self.num_iter = num_iter
def apply_random_transform(self, images):
"""应用随机变换"""
transformed = []
for img in images:
for transform in self.transformations:
t_img = transform(img)
transformed.append(t_img)
return torch.stack(transformed)
def compute_eot_gradient(self, images, labels, target_labels=None):
"""
计算 EOT 梯度
对所有可能的变换计算期望梯度
"""
total_grad = torch.zeros_like(images)
for _ in range(10): # 采样次数
# 应用随机变换
transformed_images = self.apply_random_transform(images)
# 计算梯度
transformed_images.requires_grad = True
outputs = self.model(transformed_images)
if target_labels is not None:
loss = -F.cross_entropy(outputs, target_labels.repeat(len(self.transformations)))
else:
loss = F.cross_entropy(outputs, labels.repeat(len(self.transformations)))
loss.backward()
total_grad += transformed_images.grad.data
return total_grad / 10
def attack(self, images, labels, target_labels=None):
"""执行 EOT 攻击"""
adversarial_images = images.clone()
for iteration in range(self.num_iter):
# 计算 EOT 梯度
grad = self.compute_eot_gradient(adversarial_images, labels, target_labels)
# 更新扰动
perturbation = self.epsilon * torch.sign(grad.mean(dim=0, keepdim=True))
adversarial_images = adversarial_images + perturbation
# 投影
adversarial_images = torch.clamp(adversarial_images, 0, 1)
return adversarial_images12. AI安全全景:对抗样本只是AI安全的一个方面
12.1 AI安全的完整图景
对抗样本是AI安全领域最著名的研究方向之一,但它只是冰山一角。AI系统面临的安全威胁还有很多:
数据安全问题
- 数据投毒:在训练数据中混入恶意样本,让模型学会错误的行为
- 数据窃取:通过查询API推断出训练数据的某些特征
- 隐私泄露:从模型中提取出训练数据的敏感信息
模型安全问题
- 对抗样本:我们今天讨论的主题
- 模型逆向:通过输入输出推断模型的内部结构
- 模型盗取:复制一个功能相似的模型
部署安全问题
- 边缘设备攻击:针对移动端、IoT设备的攻击
- 对抗样本检测规避:让恶意样本绕过安全检测系统
- 后门攻击:在模型中植入隐藏的”后门”
12.2 为什么AI安全值得关注?
想象一下,如果AI系统被广泛应用于医疗诊断、金融风控、自动驾驶等领域,那么AI系统的安全性就直接关系到:
- 病人的生命安全
- 金融系统的稳定
- 道路交通的安全
- 社会的公平正义
这不仅仅是技术问题,更是社会问题和伦理问题。
13. 对抗样本在AI伦理中的作用:为什么需要关注AI安全?
13.1 AI系统的公平性问题
很多AI系统存在偏见和歧视问题。比如,面部识别系统对不同种族、性别的人识别准确率不同。对抗样本的研究揭示了一个更深层的问题:我们甚至不知道模型到底在依赖什么特征来做决策。
如果一个面部识别系统能被人眼看不出的扰动轻易欺骗,那它可能根本没有学到”人脸”的真正含义,而是学到了一些”投机取巧”的特征。
13.2 负责任的AI研究
对抗样本研究教会我们一个重要的教训:科学研究的成果可能被滥用。
一个有效的对抗攻击方法,如果落入坏人手中,可能造成严重的危害。因此,研究者在发表对抗样本相关的研究时,需要考虑:
- 是否应该先通知受影响的厂商?
- 是否应该在论文中隐藏关键的技术细节?
- 是否应该设定一个”负责任披露”的期限?
13.3 构建更安全的AI系统
对抗样本研究的最终目标,不是让AI变得更脆弱,而是让AI变得更强大。
通过了解攻击的原理,我们可以设计出更好的防御方法。通过理解模型的弱点,我们可以训练出更鲁棒的模型。通过承认AI系统的局限性,我们可以更谨慎地在关键场景中部署AI。
14. 实际系统中的AI安全检测
14.1 为什么要检测对抗样本?
在很多实际应用中,我们可能需要判断一个输入是否是恶意的。比如:
- 一个内容审核系统,需要判断上传的图片是否是对抗样本
- 一个入侵检测系统,需要判断网络流量是否是攻击
- 一个金融风控系统,需要判断交易是否是被操纵的
14.2 检测方法一览
class AdversarialDetector:
"""
对抗样本检测器
多种检测方法的集合
"""
def __init__(self, model):
self.model = model
self.model.eval()
def confidence_based_detection(self, x, threshold=0.9):
"""
基于置信度的检测
正常样本通常有较高的分类置信度
对抗样本可能有异常的置信度分布
"""
with torch.no_grad():
outputs = self.model(x)
probs = F.softmax(outputs, dim=1)
max_probs = probs.max(dim=1)[0]
# 低置信度可能表明对抗样本
is_adversarial = max_probs < threshold
return is_adversarial, max_probs
def lid_detection(self, x, batch, k=20):
"""
局部内在维度(LID)检测
对抗样本通常具有异常的高LID值
"""
# 获取特征
with torch.no_grad():
features = self.model.extract_features(x)
batch_features = self.model.extract_features(batch)
# 计算每个样本的LID
n_samples = x.size(0)
lids = []
for i in range(n_samples):
# 计算到其他样本的距离
distances = torch.norm(batch_features - features[i:i+1], dim=1)
distances = distances.sort()[0][1:k+1] # k近邻
# LID估计
lid = -k / torch.log(distances / (distances.max() + 1e-10))
lids.append(lid.mean().item())
return torch.tensor(lids)14.3 实际部署的注意事项
在实际系统中部署对抗样本检测,需要考虑:
- 实时性要求:检测不能太慢,否则会影响用户体验
- 误报率控制:太高的误报率会导致正常用户被拒绝
- 更新迭代:攻击者在进化,检测系统也需要不断更新
15. DeepFool 攻击
15.1 算法原理
DeepFool 由 Seyed-Mohsen Moosavi-Dezfooli 等人在 2016 年提出,是一种迭代攻击方法,通过在决策边界之间逐步扰动来找到最小扰动:
class DeepFool:
"""
DeepFool 攻击
原理:
1. 找到当前点所属决策区域
2. 计算到最近决策边界的距离
3. 沿法向量方向进行最小步长移动
4. 重复直到分类改变
"""
def __init__(self, model, num_classes=10, overshoot=0.02, max_iter=50):
self.model = model
self.num_classes = num_classes
self.overshoot = overshoot
self.max_iter = max_iter
def attack(self, images, labels):
"""
执行 DeepFool 攻击
"""
batch_size = images.size(0)
device = images.device
adversarial_images = images.clone().detach()
perturbed = torch.zeros_like(images)
for idx in range(batch_size):
x = images[idx:idx+1].clone().detach().requires_grad_(True)
original_label = labels[idx].item()
current_label = original_label
iteration = 0
while current_label == original_label and iteration < self.max_iter:
iteration += 1
# 获取模型输出
output = self.model(x)
# 计算梯度
model.zero_grad()
output[0, original_label].backward(retain_graph=True)
grad_original = x.grad.data.clone()
# 寻找最小扰动方向
min_dist = float('inf')
min_perturbation = None
min_class = None
for class_idx in range(self.num_classes):
if class_idx == original_label:
continue
# 计算到其他类别边界的距离
model.zero_grad()
output[0, class_idx].backward(retain_graph=True)
grad_target = x.grad.data.clone()
# 扰动方向
perturbation = grad_original - grad_target
# 距离计算
dist = torch.abs(output[0, original_label] - output[0, class_idx]) / (
torch.norm(perturbation) + 1e-10
)
if dist < min_dist:
min_dist = dist
min_perturbation = perturbation
min_class = class_idx
# 应用扰动
if min_perturbation is not None:
r = (min_dist + 1e-4) * (min_perturbation / torch.norm(min_perturbation))
x = (x + (1 + self.overshoot) * r).detach().requires_grad_(True)
# 检查是否改变分类
with torch.no_grad():
current_label = self.model(x).argmax(dim=1).item()
# 保存对抗样本
adversarial_images[idx] = x.squeeze()
perturbed[idx] = x.squeeze() - images[idx]
return adversarial_images, perturbed16. 对抗补丁(Adversarial Patch)
16.1 补丁攻击的原理
对抗补丁是一种可以在物理世界中打印和使用的对抗攻击,通过在图像任意位置放置一个局部补丁来欺骗分类器。
class AdversarialPatchAttack:
"""
对抗补丁攻击
核心思想:
- 生成一个局部补丁(可以是任意形状)
- 补丁位置可以是随机的
- 优化补丁图案使其具有最大的"欺骗能力"
"""
def __init__(self, model, patch_size=50, num_classes=1000):
self.model = model
self.patch_size = patch_size
self.num_classes = num_classes
def create_adversarial_patch(self, target_class, iterations=1000):
"""
生成对抗补丁
参数:
target_class: 目标攻击类别
iterations: 优化迭代次数
"""
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 随机初始化补丁
patch = torch.rand(3, self.patch_size, self.patch_size).to(device)
patch.requires_grad = True
optimizer = torch.optim.Adam([patch], lr=0.1)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=iterations)
for i in range(iterations):
optimizer.zero_grad()
# 生成随机背景图像
background = torch.rand(1, 3, 224, 224).to(device)
# 随机放置补丁位置
h, w = self.patch_size, self.patch_size
top = torch.randint(0, 224 - h, (1,)).item()
left = torch.randint(0, 224 - w, (1,)).item()
# 应用补丁
patched_images = background.clone()
patched_images[:, :, top:top+h, left:left+w] = torch.sigmoid(patch)
# 前向传播
outputs = self.model(patched_images)
# 定向损失:最大化目标类别概率
loss = -F.cross_entropy(outputs, torch.tensor([target_class]).to(device))
loss.backward()
optimizer.step()
scheduler.step()
# 裁剪补丁值到有效范围
with torch.no_grad():
patch.clamp_(0, 1)
if (i + 1) % 100 == 0:
prob = F.softmax(outputs, dim=-1)[0, target_class].item()
print(f"Iter {i+1}, Target prob: {prob:.4f}")
return patch.detach()物理攻击的威胁等级
对抗补丁攻击已在多项研究中得到验证:
- 通过贴在 Stop 标志上的补丁使自动驾驶系统无法识别停车标志
- 通过佩戴特制眼镜使面部识别系统误识别为特定目标
- 通过打印的补丁使图像分类器输出错误类别
17. 黑盒攻击技术
17.1 基于查询的黑盒攻击
class QueryBasedBlackBoxAttack:
"""
基于查询的黑盒攻击
假设攻击者只能:
- 输入图像获取模型输出
- 不知道模型内部结构
"""
def __init__(self, target_model, epsilon=0.03, delta=1e-4):
self.model = target_model
self.epsilon = epsilon
self.delta = delta
def estimate_gradient(self, x, labels, num_queries=100):
"""
用有限差分估计梯度
对每个维度添加小扰动,估算梯度
"""
batch_size = x.size(0)
dim = x.view(batch_size, -1).size(1)
gradient = torch.zeros_like(x)
x_flat = x.view(batch_size, -1)
grad_flat = gradient.view(batch_size, -1)
# 随机选择维度进行估计(加速)
num_estimate = min(num_queries, dim)
selected_dims = torch.randperm(dim)[:num_estimate]
for dim_idx in selected_dims:
# 正向扰动
x_plus = x_flat.clone()
x_plus[:, dim_idx] += self.delta
x_plus = x_plus.view_as(x)
with torch.no_grad():
output_plus = self.model(x_plus)
pred_plus = output_plus.argmax(dim=1)
# 计算梯度估计
for i in range(batch_size):
if pred_plus[i] != labels[i]:
grad_flat[i, dim_idx] = 1
else:
grad_flat[i, dim_idx] = -1
return gradient
def attack(self, images, labels, num_iterations=10):
"""黑盒攻击"""
adversarial_images = images.clone()
for iteration in range(num_iterations):
# 估计梯度
grad = self.estimate_gradient(adversarial_images, labels)
# 更新
adversarial_images = adversarial_images + self.epsilon * torch.sign(grad)
adversarial_images = torch.clamp(adversarial_images, 0, 1)
return adversarial_images17.2 迁移攻击
class TransferAttack:
"""
迁移攻击
利用模型之间的迁移性:
1. 训练一个替代模型
2. 在替代模型上生成对抗样本
3. 将对抗样本迁移到目标模型
"""
def __init__(self, substitute_model, target_model):
self.substitute = substitute_model
self.target = target_model
def train_substitute(self, training_images, training_labels, epochs=10):
"""
训练替代模型
使用雅可比矩阵增强训练数据
"""
substitute = copy.deepcopy(self.substitute)
optimizer = torch.optim.Adam(substitute.parameters(), lr=0.001)
for epoch in range(epochs):
for images, labels in DataLoader(zip(training_images, training_labels)):
optimizer.zero_grad()
outputs = substitute(images)
loss = F.cross_entropy(outputs, labels)
loss.backward()
optimizer.step()
self.substitute = substitute
return substitute
def generate_adversarial(self, images, epsilon=0.03, method='fgsm'):
"""
在替代模型上生成对抗样本
"""
if method == 'fgsm':
return fgsm_attack(self.substitute, images, labels=None, epsilon=epsilon)
elif method == 'pgd':
return pgd_attack(self.substitute, images, labels=None, epsilon=epsilon)
def transfer(self, images, labels):
"""执行迁移攻击"""
# 在替代模型上生成
adv_images = self.generate_adversarial(images)
# 测试在目标模型上的效果
with torch.no_grad():
target_output = self.target(adv_images)
return adv_images, target_output18. 对抗样本检测
18.1 基于置信度的检测
class FeatureSqueezeDetector:
"""
特征压缩检测
通过压缩输入并比较输出来检测对抗样本
"""
def __init__(self, model):
self.model = model
def squeeze(self, x, method='bit_depth', bit_depth=4):
"""压缩输入"""
if method == 'bit_depth':
# 位深度压缩
levels = 2 ** bit_depth
return torch.round(x * levels) / levels
elif method == 'jpeg':
# JPEG压缩(简化实现)
return x # 实际需要图像处理库
elif method == 'median_filter':
# 中值滤波
return F.median_pool2d(x, kernel_size=3)
def detect(self, x, threshold=0.1):
"""检测对抗样本"""
# 原始预测
with torch.no_grad():
original_pred = self.model(x).argmax(dim=1)
# 压缩后预测
squeezed_x = self.squeeze(x)
with torch.no_grad():
squeezed_pred = self.model(squeezed_x).argmax(dim=1)
# 预测不一致可能表明对抗样本
is_adversarial = (original_pred != squeezed_pred)
return is_adversarial, (original_pred != squeezed_pred).float()19. 对抗攻击的实际应用场景
19.1 自动驾驶系统攻击
class AutonomousVehicleAttack:
"""
自动驾驶系统对抗攻击
目标:欺骗感知系统使车辆做出错误决策
"""
def __init__(self, perception_model):
self.perception = perception_model
def traffic_sign_patch_attack(self, sign_image, target_class):
"""
交通标志补丁攻击
生成贴在交通标志上的对抗补丁
使感知系统误识别标志
"""
patch_size = (50, 50)
# 初始化补丁
patch = torch.rand(3, *patch_size, requires_grad=True)
optimizer = torch.optim.Adam([patch], lr=0.1)
for iteration in range(500):
optimizer.zero_grad()
# 将补丁应用到标志图像
patched_sign = sign_image.clone()
patched_sign[:, :, :patch_size[0], :patch_size[1]] = torch.sigmoid(patch)
# 前向传播
output = self.perception(patched_sign)
# 定向损失
loss = -F.cross_entropy(output, torch.tensor([target_class]))
loss.backward()
optimizer.step()
return patch.detach()19.2 面部识别系统攻击
class FaceRecognitionAttack:
"""
面部识别系统攻击
"""
def __init__(self, recognition_model):
self.model = recognition_model
def adversarial_glasses_attack(self, face_image, target_identity):
"""
对抗眼镜攻击
生成佩戴后能使系统误识别为特定身份的眼镜图案
"""
# 眼镜区域掩码
mask = self.get_eyewear_mask(face_image)
# 初始化眼镜图案
glasses = torch.rand(1, 3, 50, 150, requires_grad=True)
optimizer = torch.optim.Adam([glasses], lr=0.1)
for iteration in range(300):
optimizer.zero_grad()
# 应用眼镜
patched_face = face_image.clone()
patched_face = patched_face + mask * torch.tanh(glasses)
# 前向传播获取身份嵌入
embedding = self.model.extract_embedding(patched_face)
target_embedding = self.model.get_identity_embedding(target_identity)
# 损失:最小化与目标身份的嵌入距离
loss = 1 - F.cosine_similarity(embedding, target_embedding).mean()
loss.backward()
optimizer.step()
return torch.tanh(glasses.detach())
def get_eyewear_mask(self, face_image):
"""获取眼镜区域的掩码"""
_, _, h, w = face_image.shape
mask = torch.zeros_like(face_image)
# 假设眼镜在眼睛位置
mask[:, :, int(h*0.35):int(h*0.45), int(w*0.2):int(w*0.8)] = 1.0
return mask20. 对抗攻击的评估与基准
20.1 攻击成功率度量
class AttackEvaluator:
"""
攻击评估器
评估对抗攻击的效果
"""
def __init__(self, model):
self.model = model
def evaluate_attack(self, original_images, labels, adversarial_images):
"""
评估攻击效果
返回:
- 攻击成功率
- 平均扰动幅度
- 置信度变化
"""
with torch.no_grad():
# 原始预测
original_output = self.model(original_images)
original_pred = original_output.argmax(dim=1)
original_conf = F.softmax(original_output, dim=1).max(dim=1)[0]
# 对抗预测
adversarial_output = self.model(adversarial_images)
adversarial_pred = adversarial_output.argmax(dim=1)
adversarial_conf = F.softmax(adversarial_output, dim=1).max(dim=1)[0]
# 攻击成功:预测改变
attack_success = (original_pred != adversarial_pred).float()
# 计算指标
success_rate = attack_success.mean().item()
# 扰动幅度
perturbation = adversarial_images - original_images
perturbation_linf = perturbation.view(perturbation.size(0), -1).abs().max(dim=1)[0].mean().item()
perturbation_l2 = perturbation.view(perturbation.size(0), -1).norm(dim=1).mean().item()
# 置信度变化
conf_change = (adversarial_conf - original_conf).mean().item()
return {
'success_rate': success_rate,
'perturbation_linf': perturbation_linf,
'perturbation_l2': perturbation_l2,
'confidence_change': conf_change,
'original_confidence': original_conf.mean().item(),
'adversarial_confidence': adversarial_conf.mean().item()
}21. 总结与展望
对抗样本研究走过了将近二十年的历程,从最初被认为是”神经网络的怪癖”,到现在已经成为AI安全领域的核心研究方向之一。
这段旅程告诉我们:
- AI系统比我们想象的更脆弱:人眼几乎无法察觉的扰动就能让最先进的模型完全失效
- 安全是一个持续的过程:攻防双方在不断进化,没有一劳永逸的解决方案
- 理论理解和实践应用同样重要:只有深入理解对抗样本的本质,才能设计出真正有效的防御
未来的AI安全研究还有很长的路要走。我们需要:
- 更强的理论工具来理解神经网络的泛化能力和鲁棒性
- 更实用的防御方法来保护真实部署的系统
- 更完善的评估基准来公平比较不同的攻防方法
- 更负责任的研究实践来平衡创新和风险
希望这篇指南能帮助你建立起对对抗样本的直观理解和动手能力。如果你对这个领域感兴趣,不妨从实验代码开始,自己动手生成一些对抗样本,感受一下它们的”威力”。
毕竟,了解敌人是战胜敌人的第一步。
参考文献
- Szegedy, C., et al. (2013). “Intriguing properties of neural networks.” arXiv:1312.6199.
- Goodfellow, I. J., et al. (2015). “Explaining and Harnessing Adversarial Examples.” ICLR.
- Madry, A., et al. (2017). “Towards Deep Learning Models Resistant to Adversarial Attacks.” ICLR.
- Carlini, N., & Wagner, D. (2017). “Towards Evaluating the Robustness of Neural Networks.” IEEE S&P.
- Kurakin, A., et al. (2016). “Adversarial examples in the physical world.” ICLR Workshop.
- Brown, T. B., et al. (2017). “Adversarial Patch.” arXiv:1712.09665.
- Moosavi-Dezfooli, S. M., et al. (2016). “DeepFool: A Universal and Approximate Method.” CVPR.
- Athalye, A., et al. (2018). “Obfuscated Gradients Give a False Sense of Security.” ICML.
- Ilyas, A., et al. (2019). “Adversarial Examples Are Not Bugs, They Are Features.” NeurIPS.
- Zhang, H., et al. (2019). “Theoretically Principled Trade-off between Robustness and Accuracy.” ICML.