SimBA算法实现过程

文章目录

添加噪声

操作:将频率扰动通过trans( )转为像素域扰动加到原始图像上(trans返回频率域转换为像素域的结果)

python 复制代码
expanded = (
    images_batch[remaining_indices] +  # 原始图像(剩余样本)
    trans(                            # 频率扰动转换为像素扰动 trans(IDCT)
        self.expand_vector(            # 扩展频率扰动向量
            x[remaining_indices],      # 当前扰动向量(剩余样本)
            expand_dims                # 扩展尺寸
        )
    )
).clamp(0, 1)                         # 限制像素范围

衡量扰动

python 复制代码
l2_norms = torch.zeros(batch_size, max_iters, device=self.device)  # L2扰动范数

linf_norms = torch.zeros(batch_size, max_iters, device=self.device)  # L∞扰动范数

典型应用场景
​​无限制攻击​​:只关注攻击成功率,不限制范数
​​L2约束攻击​​:要求总扰动能量小于阈值
​​L∞约束攻击​​:要求每个像素变化小于阈值(更常见)

  • ​​L2范数(欧几里得范数)​​:

    计算所有像素扰动值的平方和的平方根

    公式:∥δ∥₂ = √(Σ(δ_i)²)

    特点:衡量扰动的整体能量(幅度)

  • L∞范数(无穷范数)​​:

    取所有像素扰动值的绝对值的最大值

    公式:∥δ∥_∞ = max(|δ_i|)

    特点:衡量单个像素的最大变化量,对局部大扰动敏感

  • 在对抗攻击中的重要性

    ​​L2范数小​​:

    表示扰动分散在整个图像

    人眼不易察觉(类似高斯噪声)

    示例:轻微改变所有像素

    ​​L∞范数小​​:

    表示没有单个像素被大幅修改

    人眼对局部突变更敏感

    示例:所有像素最多只改变0.01

  • 评估攻击隐蔽性​​:

    L2范数小 → 扰动不易察觉

    L∞范数小 → 没有明显突变点

示例数值

假设一个3×3图像的扰动:

δ = [[0.1, 0.2, 0.1],

0.3, 0.0, 0.1\], \[0.1, 0.1, 0.2\]

复制代码
L2范数 = √(0.1²+0.2²+0.1²+0.3²+0.0²+0.1²+0.1²+0.1²+0.2²) ≈ 0.55
L∞范数 = max(0.1,0.2,0.1,0.3,0.0,0.1,0.1,0.1,0.2) = 0.3

总结

这两种范数提供了互补的视角:

复制代码
L2:关注全局扰动能量
L∞:关注局部最大变化

高级索引

高级索引

  • 代码中用到了布尔索引
    用布尔张量筛选元素
python 复制代码
x = torch.tensor([1, 2, 3, 4, 5])
mask = x > 3 #大于三才会是true
print(mask) # tensor([False, False, False, True, True])
print(x[mask]) # tensor([4, 5])

可用于筛选满足某种条件的元素,非常直观且强大。

python 复制代码
left_indices = remaining_indices[improved]#用高级索引来筛选
remaining_indices = [0, 1, 2, 3]  # 剩余样本索引
improved = [True, False, True, False]  # 改善状态
left_indices = [0, 2]  # 改善样本的索引

变量名

python 复制代码
batch_size = x.size(0)#这一批次图片的数量
------------------------------------
remaining  #ne结果为1(true,未攻击成功),为0(false,攻击成功)
#在用到remaining时,只有1才会被计入,如remaining.sum()。
-----------------------------------------------------
#这个一维矩阵里只放0(攻击成功),1(攻击不成功),# 更新剩余样本索引
remaining_indices = torch.arange(0, batch_size, device=self.device).long()
--------------------------------------------
#对抗样本集adv
--------------------------------------------------------
#x用来放(样本,该样本各个维度的像素值(通道*长*宽)),一行就是一个样本
x = torch.zeros(batch_size, n_dims, device=self.device)
---------------------------------------------------------
# 创建日志记录张量,张量就是矩阵
probs = torch.zeros(batch_size, max_iters, device=self.device)  # 目标标签概率
succs = torch.zeros(batch_size, max_iters, device=self.device)  # 成功标志
queries = torch.zeros(batch_size, max_iters, device=self.device)  # 查询次数
l2_norms = torch.zeros(batch_size, max_iters, device=self.device)  # L2扰动范数
linf_norms = torch.zeros(batch_size, max_iters, device=self.device)  # L∞扰动范数
# all_probs[[[]]]每个样本在每个迭代步骤中,模型对所有 10 个类别的预测概率,用的是CIFAR-10所以10个类别
all_probs = torch.zeros(batch_size, max_iters, 10, device=self.device)  # 所有类别概率
------------------------------------------------------

#可能的随机序列:(通道索引, 行索引, 列索引) [ (0,0,0), (2,2,2), (1,1,0), (0,1,2), ... ]
indices = torch.randperm(3 * freq_dims * freq_dims, device=self.device)[:max_iters]
#indices里放的是同一批次中每张图片增加扰动的维度顺序

for k in range(max_iters):
    dim = indices[k]  # indices扰动位置的索引张量
#dim是特定次批次要更改的维度数

diff[:, dim] = epsilon  #一行是一个样本(图片),所有行的第dim维度(列)赋值为epsilon
# 攻击不成功的那些样本里都要减去diff扰动向量
# 生成两个方向的扰动向量
left_vec = x[remaining_indices] - diff  # 负方向扰动过的样本集
right_vec = x[remaining_indices] + diff  # 正方向扰动过的样本集

------------------------------------------
# 设置攻击参数
max_iters = 200  # 最大迭代次数1000,应小于3*freq_dims*freq_dims
freq_dims = 10  # 频率维度32
stride = 7  # 步长
epsilon = 0.2  # 扰动大小
targeted = False  # 非目标攻击
pixel_attack = False  # 像素攻击=false,使用DCT攻击

代码

python 复制代码
# 只加载第一个批次的图像和标签
images, labels = next(iter(testloader))
images = images.to(device)
labels = labels.to(device)

总体代码

python 复制代码
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader, Subset
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
import numpy as np
import os
import time
import utils  # 确保有utils模块


# 定义与训练代码相同的网络结构
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 64, 3, padding=1)
        self.conv2 = nn.Conv2d(64, 64, 3, padding=1)
        self.pool1 = nn.MaxPool2d(2, 2)
        self.bn1 = nn.BatchNorm2d(64)#64是输入的通道数
        self.relu1 = nn.ReLU()

        self.conv3 = nn.Conv2d(64, 128, 3, padding=1)
        self.conv4 = nn.Conv2d(128, 128, 3, padding=1)
        self.pool2 = nn.MaxPool2d(2, 2, padding=1)
        self.bn2 = nn.BatchNorm2d(128)
        self.relu2 = nn.ReLU()

        self.conv5 = nn.Conv2d(128, 128, 3, padding=1)
        self.conv6 = nn.Conv2d(128, 128, 3, padding=1)
        self.conv7 = nn.Conv2d(128, 128, 1, padding=1)
        self.pool3 = nn.MaxPool2d(2, 2, padding=1)
        self.bn3 = nn.BatchNorm2d(128)
        self.relu3 = nn.ReLU()

        self.conv8 = nn.Conv2d(128, 256, 3, padding=1)
        self.conv9 = nn.Conv2d(256, 256, 3, padding=1)
        self.conv10 = nn.Conv2d(256, 256, 1, padding=1)
        self.pool4 = nn.MaxPool2d(2, 2, padding=1)
        self.bn4 = nn.BatchNorm2d(256)
        self.relu4 = nn.ReLU()

        self.conv11 = nn.Conv2d(256, 512, 3, padding=1)
        self.conv12 = nn.Conv2d(512, 512, 3, padding=1)
        self.conv13 = nn.Conv2d(512, 512, 1, padding=1)
        self.pool5 = nn.MaxPool2d(2, 2, padding=1)
        self.bn5 = nn.BatchNorm2d(512)
        self.relu5 = nn.ReLU()

        self.fc14 = nn.Linear(512 * 4 * 4, 1024)
        self.drop1 = nn.Dropout2d()
        self.fc15 = nn.Linear(1024, 1024)
        self.drop2 = nn.Dropout2d()
        self.fc16 = nn.Linear(1024, 10)

    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = self.pool1(x)
        x = self.bn1(x)
        x = self.relu1(x)#对张量矩阵中的每个数值依次relu1()

        x = self.conv3(x)
        x = self.conv4(x)
        x = self.pool2(x)
        x = self.bn2(x)
        x = self.relu2(x)

        x = self.conv5(x)
        x = self.conv6(x)
        x = self.conv7(x)
        x = self.pool3(x)
        x = self.bn3(x)
        x = self.relu3(x)

        x = self.conv8(x)
        x = self.conv9(x)
        x = self.conv10(x)
        x = self.pool4(x)
        x = self.bn4(x)
        x = self.relu4(x)

        x = self.conv11(x)
        x = self.conv12(x)
        x = self.conv13(x)
        x = self.pool5(x)
        x = self.bn5(x)
        x = self.relu5(x)
        x = x.view(-1, 512 * 4 * 4)#扁平化
        x = F.relu(self.fc14(x))
        x = self.drop1(x)
        x = F.relu(self.fc15(x))
        x = self.drop2(x)
        x = self.fc16(x)###不归一化???
        return x


class SimBA:
    def __init__(self, model, dataset, image_size):
        self.model = model
        self.dataset = dataset
        self.image_size = image_size
        self.model.eval()
        self.device = next(model.parameters()).device  # 获取模型所在的设备

    #expand_vector有两种用法:像素攻击与DCT频率域攻击(DCT频率域攻击这个输入的x应该提前DCT化了)
    #x扰动过的样本集,size要修改的低频维度数
    #注意 必须 size <= self.image_size
    def expand_vector(self, x, size):#用于锁定低频部分进行扰动
        batch_size = x.size(0)
        x = x.view(-1, 3, size, size)#从一维向量 (batch_size, 3*size*size) 变为四维张量 (batch_size, 3, size, size)
        z = torch.zeros(batch_size, 3, self.image_size, self.image_size, device=self.device)#补size与image_size差的0
        z[:, :, :size, :size] = x
        return z

    def normalize(self, x):
        return utils.apply_normalization(x, self.dataset)

    def get_probs(self, x, y):#返回特定标签预测概率
        # 确保输入在正确设备上
        x = x.to(self.device)
        y = y.to(self.device)

        output = self.model(self.normalize(x))
        probs = F.softmax(output, dim=-1)
        # 选择对应标签的概率
        probs_selected = probs[torch.arange(probs.size(0)), y]
        return probs_selected

    def get_all_probs(self, x):#返回全部标签预测概率
        x = x.to(self.device)
        output = self.model(self.normalize(x))
        probs = F.softmax(output, dim=-1)
        return probs

    def get_preds(self, x):#返回标签预测概率最大的那个的标签(序号)
        x = x.to(self.device)
        output = self.model(self.normalize(x))#把图形归一化后送入模型中得到预测概率
        _, preds = output.data.max(1)
        return preds

    #攻击函数定向攻击与否,取决于labels_batch是原标签(非定向),还是目标标签(定向)
    def simba_batch(self, images_batch, labels_batch, max_iters, freq_dims, stride, epsilon, linf_bound=0.0,
                    order='rand', targeted=False, pixel_attack=False, log_every=1):

    #ef simba_batch(self, images_batch, labels_batch, max_iters 最大尝试扰动次数,
    #               freq_dims, stride strided找扰动顺序时跳动的步长, epsilon(扰动的幅度大小), linf_bound=0.0 L2范数,
    #               order='rand''rand': 随机顺序(只在低频部分进行扰动)
    #               , targeted=False非定向攻击,
    #               pixel_attack=False ;True在像素空间进行攻击;False在频率域(DCT域)进行攻击
    #               , log_every=1 每隔多少次迭代打印一次日志):

        # 将 labels_batch,images_batch 放在self.device上,确保输入在正确设备上
        #labels_batch(标签),images_batch(样本数,通道,长,宽)是张量,
        images_batch = images_batch.to(self.device)
        labels_batch = labels_batch.to(self.device)#模型和数据都要在GPU同一个设备上

        batch_size = images_batch.size(0)
        image_size = images_batch.size(2)
        assert self.image_size == image_size

        if order == 'rand':##扰动dct系数矩阵的顺序
            indices = torch.randperm(3 * freq_dims * freq_dims, device=self.device)[:max_iters]
        elif order == 'diag':
            indices = utils.diagonal_order(image_size, 3)[:max_iters].to(self.device)
        elif order == 'strided':
            indices = utils.block_order(image_size, 3, initial_size=freq_dims, stride=stride)[:max_iters].to(
                self.device)
        else:
            indices = utils.block_order(image_size, 3)[:max_iters].to(self.device)

        if order == 'rand':##低频扰动只扰freq_dims*freq_dims
            expand_dims = freq_dims
        else:#其它从全局中选点扰动
            expand_dims = image_size

        n_dims = 3 * expand_dims * expand_dims
        #x用来放(样本,该样本各个维度的像素值(通道*长*宽)),一行就是一个样本
        x = torch.zeros(batch_size, n_dims, device=self.device)

        # 创建日志记录张量,张量就是矩阵
        probs = torch.zeros(batch_size, max_iters, device=self.device)  # 目标标签概率
        succs = torch.zeros(batch_size, max_iters, device=self.device)  # 成功标志
        queries = torch.zeros(batch_size, max_iters, device=self.device)  # 查询次数
        l2_norms = torch.zeros(batch_size, max_iters, device=self.device)  # L2扰动范数
        linf_norms = torch.zeros(batch_size, max_iters, device=self.device)  # L∞扰动范数
        #all_probs[[[]]]每个样本在每个迭代步骤中,模型对所有 10 个类别的预测概率,用的是CIFAR-10所以10个类别
        all_probs = torch.zeros(batch_size, max_iters, 10, device=self.device)  # 所有类别概率

        # 获取初始概率和预测
        prev_probs = self.get_probs(images_batch, labels_batch)#返回图像集对应真实类别的概率矩阵
        preds = self.get_preds(images_batch)#get_preds()求一个标签,对一个批次用就返回**标签预测矩阵**

        if pixel_attack:
            trans = lambda z: z
        #逆DCT变换,将频率域转化为像素域
        #z 要变换的左上角低频部分的张量大小
        #block_size是进行DCT变化是的基本块大小(基函数波面的数目为block_size*block_size)
        #linf_bound为L∞的阈值
        else:
            trans = lambda z: utils.block_idct(z, block_size=image_size, linf_bound=linf_bound)

        #创建一个从0到batch_size-1的整数序列,用于后面标记各个序号的图片攻击成功了么
        #remaining_indices后面表示未被攻击成功的图片。
        #这个一维矩阵里只放0(攻击成功),1(攻击不成功)
        remaining_indices = torch.arange(0, batch_size, device=self.device).long()


        #攻击核心步骤,添加扰动,对一张图片有max_iters次扰动
        for k in range(max_iters):
            dim = indices[k]#indices扰动位置的索引张量
            #操作:将像素域扰动加到原始图像上(trans返回频率域转换为像素域的结果)
            #expanded包含所有尚未成功攻击的样本
            expanded = (images_batch[remaining_indices] + trans(
                self.expand_vector(x[remaining_indices], expand_dims))).clamp(0, 1)#将像素值限制在[0,1]范围内
            # 计算整个批次的频率扰动张量(多维矩阵)
            perturbation = trans(self.expand_vector(x, expand_dims))
            l2_norms[:, k] = perturbation.view(batch_size, -1).norm(2, 1)#L2
            #view(batch_size, -1) 形状变化:(batch_size, 3, H, W) → (batch_size, 3*H*W)
            #norm(2, dim=1):沿维度1(行方向)计算L2范数(norm是范数的意思)
            linf_norms[:, k] = perturbation.view(batch_size, -1).abs().max(1)[0]#L∞
            #max(1):沿维度1(行方向)求最大值  [0]:取元组的第一个元素(最大值)

            # expanded(剩余未攻击成功图像)是已经扰动过的图像集合,输入到预测中,得到对抗样本的最大预测概率的标签的一维矩阵
            preds_next = self.get_preds(expanded)#的到矩阵
            preds[remaining_indices] = preds_next## 更新未被攻击成功图片的预测结果

            if targeted:#定向攻击,preds是标签预测矩阵,labels_batch是定向攻击的标签集
                #ne结果为1(true,未攻击成功),为0(false,攻击成功)
                remaining = preds.ne(labels_batch)#逐元素比较 preds 和 labels_batch
            else:#非定向攻击,labels_batch是原真实的标签集
                remaining = preds.eq(labels_batch)#逐元素比较 preds 和 labels_batch,同为ture
            ##为了统一定向与不定向的返回0,1的意义。返回1 (True): 攻击尚未成功,0 (False): 攻击已成功,
            #expanded扰动过的之前未攻击成功的图像集
            current_all_probs = self.get_all_probs(expanded)
            #remaining_indices指明是第几个样本,k是迭代轮次,赋上10个类别的预测概率
            all_probs[remaining_indices, k] = current_all_probs

            #如果全部都生成了对抗样本,收尾
            if remaining.sum() == 0:
                #获取最终对抗样本集adv
                adv = (images_batch + trans(self.expand_vector(x, expand_dims))).clamp(0, 1)
                #计算对抗样本adv对应标签的最终预测概率
                #对于非定向攻击:原始标签的概率(目标是降低),对于定向攻击:目标标签的概率(目标是提高)
                probs_k = self.get_probs(adv, labels_batch)
                probs[:, k:] = probs_k.unsqueeze(1).repeat(1, max_iters - k)
                #第k次成功,则之后迭代都标为1
                succs[:, k:] = torch.ones(batch_size, max_iters - k, device=self.device)
                #这里只填充从k到max_iters位,后面求查询总次数时要把queries相加
                queries[:, k:] = torch.zeros(batch_size, max_iters - k, device=self.device)
                #填充剩余迭代的所有概率
                for j in range(k, max_iters):
                    all_probs[:, j] = current_all_probs
                break# 提前结束循环

            #如果没有全部都生成了对抗样本,继续
            remaining_indices = torch.arange(0, batch_size, device=self.device)[remaining].long()
            if k > 0:
                succs[:, k] = ~remaining#succs中1成功,0失败;而remaining 1(true,未攻击成功),为0(false,攻击成功)正好相反,所以要取反

            ## 创建扰动向量
            #扰动向量(二维)(剩余未成功样本,该样本各个维度的像素值(通道*长*宽))
            diff = torch.zeros(remaining.sum(), n_dims, device=self.device)
            diff[:, dim] = epsilon  #一行是一个样本(图片),所有行的第dim维度(列)赋值为epsilon
            # 攻击不成功的那些样本里都要减去diff扰动向量
            # 生成两个方向的扰动向量
            left_vec = x[remaining_indices] - diff  # 负方向扰动过的样本集
            right_vec = x[remaining_indices] + diff  # 正方向扰动过的样本集

            # 尝试负方向扰动
            #对抗样本集adv,remaining_indices未攻击成功的样本,expand_dims要修改的低频维度数
            #像素域,频率域攻击都从这进去
            adv = (images_batch[remaining_indices] + trans(self.expand_vector(left_vec, expand_dims))).clamp(0, 1)
            #负方向扰动后的预测概率集,一维数组
            left_probs = self.get_probs(adv, labels_batch[remaining_indices])
            #初始化查询计数
            queries_k = torch.zeros(batch_size, device=self.device)#一维数组,每个元素对应一个样本的查询计数
            queries_k[remaining_indices] += 1#remaining_indices=1:当前尚未攻击成功的样本索引,对这些索引位置的计数器加1,通过累加来得到最终查询次数
            if targeted:#定向攻击所以要提高预测概率,向目标标签集靠近
                # left_probs攻击后的每个样本的预测概率一维数组,prev_probs[remaining_indices]还未攻击成功的原预测概率
                improved = left_probs.gt(prev_probs[remaining_indices])#improve是一个仅有0,1的一维数组
            else:#不定向攻击所以要降低预测概率,远离原标签集
                improved = left_probs.lt(prev_probs[remaining_indices])
            #如果有样本未改进,需要额外查询
            if improved.sum() < remaining_indices.size(0):#.size(0)返回remaining_indices的的第一维度元素数,即长度(元素个数)
                queries_k[remaining_indices[~improved]] += 1

            # 尝试正方向扰动,与上面一样
            adv = (images_batch[remaining_indices] + trans(self.expand_vector(right_vec, expand_dims))).clamp(0, 1)
            right_probs = self.get_probs(adv, labels_batch[remaining_indices])

            if targeted:
                right_improved = right_probs.gt(torch.max(prev_probs[remaining_indices], left_probs))
            else:
                right_improved = right_probs.lt(torch.min(prev_probs[remaining_indices], left_probs))

            probs_k = prev_probs.clone()
            # 更新负方向改进的样本
            if improved.sum() > 0:# 如果有至少一个样本在负方向扰动中得到了改善
                left_indices = remaining_indices[improved]# 获取改善样本的索引,也就是improved为1的那部分
                left_mask_remaining = improved.unsqueeze(1).repeat(1, n_dims)#n_dims要扰动的低频部分
                x[left_indices] = left_vec[left_mask_remaining].view(-1, n_dims)## left_vec负方向扰动过的样本集,把x更新为负方向扰动过的
                probs_k[left_indices] = left_probs[improved]#更新改善样本的预测概率
            # 更新正方向改进的样本
            if right_improved.sum() > 0:
                right_indices = remaining_indices[right_improved]
                right_mask_remaining = right_improved.unsqueeze(1).repeat(1, n_dims)
                x[right_indices] = right_vec[right_mask_remaining].view(-1, n_dims)
                probs_k[right_indices] = right_probs[right_improved]

            probs[:, k] = probs_k# 记录当前第k次迭代预测概率
            queries[:, k] = queries_k# 记录当前第k次迭代查询次数
            prev_probs = probs[:, k]# 更新图像集对应真实类别的概率矩阵
            # # 定期打印日志,eg:Iteration 50: queries = 99.8000, prob = 0.5678, remaining = 0.6000
            if (k + 1) % log_every == 0 or k == max_iters - 1:
                print('Iteration %d: queries = %.4f, prob = %.4f, remaining = %.4f' % (
                    k + 1, queries.sum(1).mean().item(), probs[:, k].mean().item(), remaining.float().mean().item()))
            # for k in range(max_iters): for循环结束
        ## 计算最终对抗样本
        expanded = (images_batch + trans(self.expand_vector(x, expand_dims))).clamp(0, 1)
        preds = self.get_preds(expanded)# # 获取最终预测

        if targeted:
            remaining = preds.ne(labels_batch)
        else:
            remaining = preds.eq(labels_batch)

        succs[:, max_iters - 1] = ~remaining
        return expanded, probs, succs, queries, l2_norms, linf_norms, all_probs, preds



#得到/simba_attack_results/data文件夹
def save_results(images, adversarial_images, labels, adversarial_preds, classes,
                 probs, all_probs, save_path="results"):
    # def save_results(
    #         images,  # 原始图像张量 (形状: [batch, 3, H, W])
    #         adversarial_images,  # 对抗样本张量 (同原始图像形状)
    #         labels,  # 原始标签 (形状: [batch])
    #         adversarial_preds,  # 对抗样本的预测标签 (形状: [batch])
    #         classes,  # 类别名称列表 (如: ['plane', 'car', ...])
    #         probs,  # 原始标签的概率变化 (形状: [batch, max_iters])
    #         all_probs,  # 所有类别的概率 (形状: [batch, max_iters, num_classes])
    #         save_path="results"  # 保存路径,默认为"results"
    # ):
    # 反归一化图像,将图像从 [-1, 1] 范围恢复到 [0, 1] 范围
    def denormalize(tensor):
        tensor = tensor.clone()
        for t, m, s in zip(tensor, [0.5, 0.5, 0.5], [0.5, 0.5, 0.5]):
            t.mul_(s).add_(m)
        return tensor
    # 创建目录结构
    os.makedirs(os.path.join(save_path, "images"), exist_ok=True)# 保存图像文件
    os.makedirs(os.path.join(save_path, "probabilities"), exist_ok=True)# 保存概率数据

    # 保存原始和对抗样本图像
    for i in range(len(images)):#遍历批次中的所有图像
        # 原始图像
        plt.imshow(denormalize(images[i]).permute(1, 2, 0).cpu().numpy())
        plt.title(f"原始: {classes[labels[i]]}")
        plt.savefig(os.path.join(save_path, "images", f"original_{i}.png"))
        plt.close()

        # 对抗样本
        plt.imshow(denormalize(adversarial_images[i]).permute(1, 2, 0).cpu().numpy())
        plt.title(f"对抗样本: {classes[adversarial_preds[i]]}")
        plt.savefig(os.path.join(save_path, "images", f"adversarial_{i}.png"))
        plt.close()

    # 保存概率数据
    torch.save({
        'probs': probs,#目标标签的概率变化
        'all_probs': all_probs#所有类别的概率
    }, os.path.join(save_path, "probabilities", "probability_data.pt"))

def load_model(device):
    """加载预训练模型"""
    net = Net().to(device)
    checkpoint_path = 'C:\\python\\project\\Resnet50\\SimBA\\weights.tar'# 设置预训练权重文件路径
    if os.path.exists(checkpoint_path):# 检查权重文件是否存在
        checkpoint = torch.load(checkpoint_path, map_location=device)
        net.load_state_dict(checkpoint['model_state_dict'])
        print("成功加载预训练模型权重")
    else:
        print(f"警告: 未找到权重文件 {checkpoint_path}")
        print("将使用随机初始化的权重进行预测")
    net.eval()  # 设置模型为评估模式,不再改变权重
    return net # 返回加载好的模型


def load_cifar10_data(batch_size=10, num_samples=10):#当调用者不提供该参数时,使用默认值
    """加载CIFAR-10测试数据子集"""
    # 1. 创建数据处理流水线
    transform = transforms.Compose([
        transforms.ToTensor(),# 将图像转换为Tensor格式
        transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))# 数据归一化处理
    ])
    # 2. 加载完整的CIFAR-10测试集
    testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
    # 3. 创建数据子集(取testset前num_samples个样本)
    subset_indices = list(range(num_samples))
    subset = Subset(testset, subset_indices)#取testset的前num_samples个作为测试集
    # 4. 创建DataLoader用于批量加载数据
    testloader = DataLoader(
        subset,  # 要加载的数据子集
        batch_size=batch_size,  # 每个批次的样本图片数量
        shuffle=False  # 不随机打乱数据顺序
    )
    return testloader, subset_indices#返回DataLoader,数据子集(取testset前num_samples个样本)


# 修复后的可视化函数,生成simba_attack_results/visualizations,原始图像 对抗样本 扰动可视化(3倍增强)
def visualize_attack(original_images, adversarial_images, labels, adversarial_preds, classes, idx,
                     save_path="attack_results", perturbation_factor=3):
    """可视化攻击结果"""

    def denormalize(tensor):
        tensor = tensor.clone()
        for t, m, s in zip(tensor, [0.5, 0.5, 0.5], [0.5, 0.5, 0.5]):
            t.mul_(s).add_(m)
        return tensor

    # 获取当前样本
    original_img = original_images[idx]
    adversarial_img = adversarial_images[idx]
    label = labels[idx]
    pred = adversarial_preds[idx]

    # 确保在CPU上转换
    original = denormalize(original_img).cpu().permute(1, 2, 0).numpy()
    adversarial = denormalize(adversarial_img).cpu().permute(1, 2, 0).numpy()
    perturbation = (adversarial - original) * perturbation_factor + 0.5

    fig, axes = plt.subplots(1, 3, figsize=(18, 6))

    axes[0].imshow(original)
    axes[0].set_title(f"原始图像\n真实类别: {classes[label]}")
    axes[0].axis('off')

    pred_label = classes[pred.item()]
    axes[1].imshow(adversarial)
    axes[1].set_title(f"对抗样本\n预测类别: {pred_label}")
    axes[1].axis('off')

    axes[2].imshow(perturbation)
    axes[2].set_title(f"扰动可视化(x{perturbation_factor})")
    axes[2].axis('off')

    os.makedirs(save_path, exist_ok=True)
    plt.savefig(os.path.join(save_path, f"attack_visualization_{idx}.png"))
    plt.close()


# 修改后的分析函数,输出simba_attack_results/analysis,各样本概率变化图像
def analyze_results(probs, succs, queries, all_probs, classes, save_path="analysis_results"):
    """分析攻击结果"""
    # 统一移动到CPU并分离计算图
    probs = probs.cpu().detach()
    succs = succs.cpu().detach()
    queries = queries.cpu().detach()
    all_probs = all_probs.cpu().detach()

    os.makedirs(save_path, exist_ok=True)

    # 计算攻击成功率
    success_rate = succs[:, -1].float().mean().item() * 100
    print(f"\n最终攻击成功率: {success_rate:.2f}%")

    # 平均查询次数
    avg_queries = queries.sum(dim=1).float().mean().item()
    print(f"平均查询次数: {avg_queries:.2f}")

    # 1. 综合概率变化图
    plt.figure(figsize=(15, 10))
    num_samples = all_probs.size(0)
    for i in range(num_samples):
        plt.subplot((num_samples + 1) // 2, 2, i + 1)
        probs_data = all_probs[i].cpu().numpy()  # 确保在CPU上
        for cls_idx, cls_name in enumerate(classes):
            plt.plot(probs_data[:, cls_idx], label=cls_name)
        plt.title(f"样本 {i + 1} 概率变化")
        plt.xlabel("迭代次数")
        plt.ylabel("概率")
        plt.legend()
    plt.tight_layout()
    plt.savefig(os.path.join(save_path, "combined_prob_changes.png"))
    plt.close()

def main():
    # 设置设备
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    print(f"使用设备: {device}")

    # 加载模型
    model = load_model(device)

    # 加载数据,一次多少个样本
    batch_size = 5  # 较小的批量大小以便快速演示,加载几个批次由后面决定
    num_samples = 100  # #取testset的前num_samples个作为测试集
    testloader, _ = load_cifar10_data(batch_size, num_samples)


    # 只加载第一个批次的图像和标签
    images, labels = next(iter(testloader))
    images = images.to(device)
    labels = labels.to(device)
    # 处理每个批次
    # for batch_idx, (images, labels) in enumerate(testloader):
    #     # 处理每个批次
    #     print(f"处理批次 {batch_idx + 1}/{len(testloader)}")
    #     process_batch(images, labels)

    # CIFAR-10类别的名称
    classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

    # 初始化SimBA攻击器
    simba = SimBA(model, dataset='cifar10', image_size=32)
    # model:要攻击的目标模型
    # dataset='cifar10':指定数据集类型(用于标准化)
    # image_size=32:图像尺寸(CIFAR-10 为 32x32)

    # 设置攻击参数
    max_iters = 1000  # 最大迭代次数
    freq_dims = 32  # 频率维度
    stride = 7  # 步长
    epsilon = 0.2  # 扰动大小
    targeted = False  # 非目标攻击
    pixel_attack = False  # 像素攻击=false,使用DCT攻击

    print("\n开始黑盒攻击...")
    start_time = time.time()

    # 新增:用户可自定义的保存路径
    result_dir = "simba_attack_results"
    os.makedirs(result_dir, exist_ok=True)

    # 注意:simba_batch现在返回8个值,包括preds
    adversarial_images, probs, succs, queries, l2_norms, linf_norms, all_probs, preds = simba.simba_batch(
        images_batch=images,
        labels_batch=labels,
        max_iters=max_iters,
        freq_dims=freq_dims,
        stride=stride,
        epsilon=epsilon,
        targeted=targeted,
        pixel_attack=pixel_attack,
        log_every=50
    )

    # 分析结果(保存到指定路径)
    analyze_results(probs, succs, queries, all_probs, classes,
                    save_path=os.path.join(result_dir, "analysis"))

    # 可视化攻击结果
    #生成simba_attack_results/visualizations,原始图像 对抗样本 扰动可视化(3倍增强)
    for i in range(min(3, batch_size)):
        visualize_attack(images, adversarial_images, labels, preds, classes, i,
                         save_path=os.path.join(result_dir, "visualizations"))

    # 保存完整结果
    save_results(images, adversarial_images, labels, preds, classes,
                 probs, all_probs, save_path=os.path.join(result_dir, "data"))

    # 保存对抗样本数据
    torch.save({
        'original_images': images.cpu(),
        'adversarial_images': adversarial_images.cpu(),
        'labels': labels.cpu(),
        'adversarial_preds': preds.cpu(),
        'probs': probs.cpu(),
        'all_probs': all_probs.cpu(),
        'queries': queries.cpu(),
        'l2_norms': l2_norms.cpu(),
        'linf_norms': linf_norms.cpu()
    }, os.path.join(result_dir, 'adversarial_results.pth'))

    print(f"所有结果已保存到 {result_dir} 目录")


if __name__ == '__main__':
    main()
相关推荐
账户不存在1 小时前
《Learning To Count Everything》论文阅读
论文阅读·pytorch·深度学习·无监督训练
夜斗小神社2 小时前
【LeetCode 热题 100】(六)矩阵
算法·leetcode·矩阵
七77.2 小时前
Track Any Anomalous Object: A Granular Video Anomaly Detection Pipeline
深度学习·计算机视觉·异常检测
2501_924730613 小时前
智慧城管复杂人流场景下识别准确率↑32%:陌讯多模态感知引擎实战解析
大数据·人工智能·算法·计算机视觉·目标跟踪·视觉检测·边缘计算
weixin_307779133 小时前
C++实现MATLAB矩阵计算程序
开发语言·c++·算法·matlab·矩阵
学不动CV了3 小时前
FreeRTOS入门知识(初识RTOS任务调度)(三)
c语言·arm开发·stm32·单片机·物联网·算法·51单片机
Kingfar_13 小时前
智能移动终端导航APP用户体验研究案例分享
人工智能·算法·人机交互·ux·用户界面·用户体验
dlraba8024 小时前
机器学习-----SVM(支持向量机)算法简介
算法·机器学习·支持向量机
_poplar_4 小时前
09 【C++ 初阶】C/C++内存管理
c语言·开发语言·数据结构·c++·git·算法·stl