
文章目录
添加噪声
操作:将频率扰动通过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()