关键词

术语英文核心概念
对抗样本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, perturbation

FGSM 的特点

  • 计算效率高:只需一次前向和反向传播
  • 单步攻击:相比迭代方法更快
  • 可解释性强:扰动方向由损失函数的梯度决定
  • 局限性:对于使用防御技术(如对抗训练)的模型效果有限

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 images

5. 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, delta

6. 黑盒攻击与白盒攻击:攻击者需要知道什么?

6.1 白盒攻击:知己知彼

白盒攻击就像是一场信息不对称的战争——攻击者对目标模型了如指掌,包括:

  • 模型的架构(多少层、什么类型)
  • 模型的参数(权重矩阵的具体数值)
  • 模型的梯度(损失函数如何变化)

这听起来很苛刻,但在很多场景下这确实是可能的。比如:

  • 公司内部人员攻击自己公司的AI系统
  • 开源模型的部署版本被攻击者下载分析
  • 攻击者通过API逆向工程猜出模型结构

白盒攻击的优势是强大、精准,缺点是需要更多信息。

6.2 黑盒攻击:盲人摸象

黑盒攻击则相反,攻击者几乎什么都不知道,只能:

  • 输入图片
  • 看到输出结果(分类结果、置信度等)

这听起来像是一个不可能完成的任务,但实际上研究者发现了几个”作弊”的方法:

方法一:查询攻击

攻击者像在测试API一样,反复向目标系统发送图片,根据返回的预测结果推断模型的弱点。比如,如果把图片某个区域变黑,置信度从99%掉到了50%,说明这个区域对分类很重要。

方法二:迁移攻击

这是最实用的黑盒攻击方法。攻击者先训练一个自己的模型(可以是同类型的,也可以是不同类型的),然后在这个模型上生成对抗样本。由于对抗样本的迁移性,这些样本往往也能欺骗目标模型。

6.3 攻击方法对比

攻击类型所需信息攻击强度实际可行性
白盒攻击模型结构+参数+梯度最强中等(需要获取模型)
黑盒查询攻击输入输出中等较高(API可访问时)
黑盒迁移攻击无需目标模型较强高(最常用)

7. 动手实验:用自己的图片生成对抗样本

7.1 环境准备

pip install torch torchvision numpy matplotlib

7.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)。它的核心思想是:

  1. 给输入加很多随机噪声
  2. 对加噪后的输入进行分类
  3. 如果某个类别在统计上显著多于其他类别,就认为原始输入在该扰动范围内是鲁棒的
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, upper

9.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_images

12. 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, perturbed

16. 对抗补丁(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_images

17.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_output

18. 对抗样本检测

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 mask

20. 对抗攻击的评估与基准

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安全领域的核心研究方向之一。

这段旅程告诉我们:

  1. AI系统比我们想象的更脆弱:人眼几乎无法察觉的扰动就能让最先进的模型完全失效
  2. 安全是一个持续的过程:攻防双方在不断进化,没有一劳永逸的解决方案
  3. 理论理解和实践应用同样重要:只有深入理解对抗样本的本质,才能设计出真正有效的防御

未来的AI安全研究还有很长的路要走。我们需要:

  • 更强的理论工具来理解神经网络的泛化能力和鲁棒性
  • 更实用的防御方法来保护真实部署的系统
  • 更完善的评估基准来公平比较不同的攻防方法
  • 更负责任的研究实践来平衡创新和风险

希望这篇指南能帮助你建立起对对抗样本的直观理解和动手能力。如果你对这个领域感兴趣,不妨从实验代码开始,自己动手生成一些对抗样本,感受一下它们的”威力”。

毕竟,了解敌人是战胜敌人的第一步


参考文献

  1. Szegedy, C., et al. (2013). “Intriguing properties of neural networks.” arXiv:1312.6199.
  2. Goodfellow, I. J., et al. (2015). “Explaining and Harnessing Adversarial Examples.” ICLR.
  3. Madry, A., et al. (2017). “Towards Deep Learning Models Resistant to Adversarial Attacks.” ICLR.
  4. Carlini, N., & Wagner, D. (2017). “Towards Evaluating the Robustness of Neural Networks.” IEEE S&P.
  5. Kurakin, A., et al. (2016). “Adversarial examples in the physical world.” ICLR Workshop.
  6. Brown, T. B., et al. (2017). “Adversarial Patch.” arXiv:1712.09665.
  7. Moosavi-Dezfooli, S. M., et al. (2016). “DeepFool: A Universal and Approximate Method.” CVPR.
  8. Athalye, A., et al. (2018). “Obfuscated Gradients Give a False Sense of Security.” ICML.
  9. Ilyas, A., et al. (2019). “Adversarial Examples Are Not Bugs, They Are Features.” NeurIPS.
  10. Zhang, H., et al. (2019). “Theoretically Principled Trade-off between Robustness and Accuracy.” ICML.

相关文档