关键词

术语英文核心概念
对抗训练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 对抗训练的基本思路

对抗训练的核心思想用四个字概括就是”以毒攻毒”。具体来说:

  1. 先用攻击算法生成对抗样本
  2. 把对抗样本和原始样本混在一起训练
  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的核心思想特别简单:

  1. 计算损失关于输入的梯度
  2. 沿着梯度的符号方向走一大步
  3. 这一大步就足以让模型判断错误

用公式表示就是:

这个公式的意思是:我不是一点一点地试探,而是直接沿着损失增大的方向大步走。因为梯度指向的是损失增长最快的方向,所以只要一步,就能让损失变得很大。

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:选哪个?

特性FGSMPGD
迭代次数1步多步(通常7-10步)
训练速度快(~1x标准训练)慢(~7x标准训练)
防御强度较弱较强
超参数敏感度非常敏感相对不敏感
适用场景资源受限、大规模训练追求最强鲁棒性

4. PGD对抗训练详解:投影梯度下降法的防御机制

4.1 PGD为什么是”黄金标准”?

如果你看对抗训练的论文,PGD几乎无处不在。Madry的团队2017年在ICLR上发表的工作奠定了PGD对抗训练的地位,被认为是对抗训练的事实标准。

PGD相比FGSM的核心改进是:多走几步,边走边看

FGSM是直接大步冲过去,而PGD是:

  1. 先随机站在某个位置瞄一眼
  2. 沿着梯度方向走一小步
  3. 停下来看看走的位置对不对
  4. 继续走,继续看
  5. 重复直到满意为止

这种方法虽然慢,但能找到更强的对抗样本。

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 analysis

5. 对抗训练 vs 常规训练:为什么加了对抗样本反而更难训练?

5.1 训练曲线的差异

如果你实际跑过对抗训练,会发现几个有意思的现象:

  1. 训练损失下降更慢:因为任务变难了
  2. 最终准确率更低:干净准确率和鲁棒准确率存在权衡
  3. 验证曲线波动更大:对抗样本带来的噪声更多

打个比方,普通训练就像让学生做选择题,对抗训练就像让学生做证明题。题目难了,花的时间多了,考试分数可能还低了。

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 tradeoffs

5.3 如何缓解这个权衡?

虽然没有完美的解决方案,但有一些技巧可以缓解这个权衡:

  1. TRADES方法:用KL散度正则化,鼓励模型在干净样本和对抗样本上表现一致
  2. MART方法:关注那些容易被对抗样本欺骗的”困难样本”
  3. 标签平滑:减少对标签的过度自信,提高泛化能力
  4. 更大的模型容量:更复杂的模型可以同时兼顾两者

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 model

6.2 完整训练循环的Tips

实际应用中,有几个值得注意的点:

def training_tips():
    """
    对抗训练实战技巧
    """
    tips = {
        'random_initialization': """
            随机初始化非常重要!
            
            不随机初始化:
            - 所有样本的对抗扰动方向相同
            - 模型可能过拟合到特定的对抗模式
            
            随机初始化:
            - 每个样本从不同起点开始搜索
            - 最终找到的对抗样本更强
            - 训练出的模型更鲁棒
        """,
        'early_stopping': """
            早停要小心:
            
            普通训练的早停标准(验证集准确率)不适用于对抗训练
            因为验证集的对抗样本也需要重新生成
            
            建议:
            - 每隔几个epoch评估一次鲁棒准确率
            - 在验证集上评估时要生成对抗样本
        """,
        'mixed_precision': """
            混合精度训练可以加速:
            
            生成对抗样本时用float32保证精度
            模型前向传播时可以用float16加速
            注意:反向传播仍需要float32
        """,
        'batch_size': """
            批次大小的影响:
            
            更大的batch:
            - 梯度估计更稳定
            - 可以生成更多样的对抗样本
            - 但显存占用增加
            
            对抗训练通常用128或256
        """
    }
    
    return tips

7. 对抗训练的挑战:精度-鲁棒性权衡、训练代价

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 overheads

7.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 x

11.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, distance

13. 对抗训练的超参数:扰动大小ε、步数、攻击方法的经验

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 recommendations

13.2 epsilon选择指南

epsilon是对抗扰动的幅度上限,是最关键的超参数:

数据集常用epsilon说明
MNIST0.3像素值范围[0,1],0.3的扰动人眼可见
CIFAR-108/255 ≈ 0.031几乎不可见
ImageNet4/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 tips

14. 对抗训练在NLP中的应用:文本对抗攻击与防御

14.1 文本对抗的特殊挑战

图像对抗攻击可以在像素级别微调,但文本对抗面临独特挑战:

  1. 离散性:文本是离散的,不能直接梯度下降
  2. 语义一致性:扰动必须保持语义不变
  3. 语法正确性:扰动后的文本必须语法正确

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 analysis

15.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_safe

16. 对抗训练的前沿:自适应对抗训练、免费对抗训练

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 directions

17. 评估协议

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 evaluation

17.2 AutoAttack:标准化的鲁棒性评估

AutoAttack是一个标准化的攻击套件,由Liang等人2020年提出,包含:

  1. APGD-CE:自适应步长的PGD攻击,针对交叉熵损失
  2. APGD-DLR:APGD针对DLR损失
  3. FAB:点几何攻击
  4. 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. 学术引用与参考文献

  1. Madry, A., et al. (2017). “Towards Deep Learning Models Resistant to Adversarial Attacks.” ICLR.
  2. Goodfellow, I. J., et al. (2015). “Explaining and Harnessing Adversarial Examples.” ICLR.
  3. Wong, E., & Kolter, J. Z. (2018). “Provable Defenses against Adversarial Examples via the Convex Outer Adversarial Polytope.” ICML.
  4. Wong, E., et al. (2020). “Fast Is Better Than Free: Revisiting Adversarial Fine-Tuning.” ICLR.
  5. Shafahi, A., et al. (2019). “Adversarial Training for Free!” NeurIPS.
  6. Zhang, H., et al. (2019). “Theoretically Principled Trade-off between Robustness and Accuracy.” ICML.
  7. Wang, Y., et al. (2019). “Improving Adversarial Robustness via Misclassification Aware Adversarial Training.” NeurIPS.
  8. Cohen, J. M., et al. (2019). “Certified Adversarial Robustness via Randomized Smoothing.” ICML.
  9. Carlini, N., et al. (2019). “On Evaluating Adversarial Robustness.” arXiv.

20. 相关文档