基于决策的黑盒攻击:仅凭类别标签也能构造对抗样本
简介一套基于决策的黑盒对抗网络攻击Python项目含可运行源码与说明文档面向人工智能安全、对抗样本方向的在校学生、教师及企业开发者。项目围绕黑盒攻击下的决策策略覆盖数据加载、模型构建、遗传算法攻击、结果可视化等模块适用于课程设计、毕业设计与科研预研。压缩包共四十二个文件以二十八个Python脚本为主另含十一张攻击效果对比图、一个ImageNet标签表、一个预训练模型权重及README说明整体约十点三三MB。代码按utils、dataset、models、checkpoints等模块组织便于替换网络与扩展算法模型覆盖MNIST、CIFAR-10/100等常见数据集上的VGG、ResNet、DenseNet结构可直接用于跨场景攻击实验。已有六十一人学习下载。所有代码均通过运行测试可直接复现也可二次开发以对比不同网络的防御效果。遇到环境配置或执行问题时可联系作者获得远程教学支持。1. 基于决策的黑盒攻击只靠“对/错”也能打穿模型只告诉攻击者模型把这张图片分成了哪个类别连置信度、概率向量、梯度一概不给——基于决策的黑盒对抗攻击decision-based attack就在这种最苛刻的信息条件下工作。它属于黑盒对抗攻击的一种与常被混淆的“基于分数的黑盒攻击”关键区别是没有连续分值可供参考攻击者手里只有一串离散的标签。这种设定看似不可能却在近几年的研究中被反复证明可行并且直接对应现实中大量只暴露最终结论的线上服务。做 AI 安全评估、模型鲁棒性测试或对抗样本方向的工程人员都会在某个环节遇到这个问题如何用有限的查询次数在决策边界上构造出最小扰动。这也是这类“源码文档说明”项目最常见的动机。2. 基于决策攻击与“分数黑盒”的本质区别2.1 三种信息级别决定了攻击问题的难度对抗攻击按攻击者可获得的信息量大致分成三个层级。这个分层不是学术上的清谈它直接决定算法设计里你能用什么样的优化工具。攻击类型攻击者掌握的信息典型算法优化手段白盒攻击模型结构、参数、梯度FGSM、PGD反向传播求精确梯度基于分数的黑盒攻击输入到输出的连续概率向量Square Attack用概率值构造替代梯度基于决策的黑盒攻击仅限最终的 top-1 类别标签Boundary Attack、HSJA、SurFree布尔信号上的几何搜索最后一行是本文的主角。“决策”二字在这里指的是模型输出的离散类别标签和强化学习里基于策略的“决策”没有关系。这个区分很重要很多初学者拿着代码跑了一遍却说不清算法为什么能在没有梯度的条件下工作。基于决策设定的难点在于你每一次查询拿到的反馈信息量极小。一个对抗样本查询一次结果只有“成功了”或“没成功”连“接近成功了多少”都不知道。因此这类攻击算法的核心能力是从布尔反馈中提取出足够多的几何信息逐步逼近真实决策边界。2.2 形式化定义与优化目标给定一个分类器 f输入 x0真实类别 y。基于决策的攻击目标是寻找一个最小扰动 δ使得 f(x0 δ) ≠ y即让模型把一个给定的输入错分到任意其他类别。用于定向攻击时要求 f(x0 δ) y_target。用优化语言描述就是最小化扰动范数minimize ||δ||subject to f(x0 δ) ≠ y问题在于这个约束条件 f(x0 δ) ≠ y 是一个不可微的硬约束。标准梯度下降无法直接使用因为你连 f 的输出值都拿不到。这就是为什么需要设计专门的方法而不是简单套一个优化器。2.3 决策边界上的二分查找是核心原语在只有类别标签的情况下一个最基本的可执行操作是在已知的“正常样本 x0”和“已对抗样本 x_adv”之间做二分查找找到决策边界的大致位置。因为 f 的输出是离散的沿着从 x0 到 x_adv 的连线上一定存在一个点从该点起再往前一步分类结果就变了。二分查找在这条线段上反复取中点用模型预测类别来判断中点落在哪一侧逐步收缩边界点的范围。这个操作一次调用模型多次但每次只问“这是哪个类别”完全符合基于决策的信息限制。可以说二分查找是这类攻击算法里最基础的原子操作后面所有更复杂的算法都在它的基础上叠加几何策略。2.4 从随机扰动到边界投影攻击的初始阶段和迭代阶段策略不同。初始阶段攻击者通常会从一个随机的大噪声扰动开始不断增大噪声强度直到模型对 x0 noise 的预测类别不再是 y。此时 x0 与 x_adv 之间的连线上一定存在一个边界点。迭代阶段的通用思路是在边界点附近施加一个微小随机扰动让样本越过边界进入对抗区域然后在越过边界的候选点和 x0 之间重新做二分查找得到一个新的、更靠近 x0 的边界点。重复这个过程边界点会沿着决策边界逐步滑动。传统边界攻击甚至不依赖梯度估计纯粹靠随机游走加上二分查找完成这在信息受限的设定下依然有效缺点是查询次数消耗非常大。3. 基于决策攻击的最小可运行实现PyTorch3.1 目标模型一个本地能跑通的 CNN实现基于决策攻击前需要先准备一个目标分类器。这里用 PyTorch 在 MNIST 上训练一个结构非常简单的 CNN目的是让后面演示攻击时目标模型的参数和梯度信息在攻击阶段完全不可用。import torch import torch.nn as nn from torchvision import datasets, transforms class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.conv nn.Sequential( nn.Conv2d(1, 16, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(16, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), ) self.fc nn.Sequential( nn.Flatten(), nn.Linear(32 * 7 * 7, 128), nn.ReLU(), nn.Linear(128, 10), ) def forward(self, x): return self.fc(self.conv(x)) transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_set datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) train_loader torch.utils.data.DataLoader(train_set, batch_size128, shuffleTrue) model SimpleCNN() optimizer torch.optim.Adam(model.parameters(), lr1e-3) loss_fn nn.CrossEntropyLoss() model.train() for epoch in range(3): for x, y in train_loader: optimizer.zero_grad() loss loss_fn(model(x), y) loss.backward() optimizer.step() print(fepoch {epoch 1} done, loss{loss.item():.4f})逻辑说明这个 CNN 由两层卷积和两层全连接组成MNIST 单通道输入输出是 10 个类别的 logits。训练 3 个 epoch 就能到 98% 左右准确率足够用于攻击演示。跑这里的示例不需要额外安装攻击库只要 python 环境里有 torch 和 torchvision2.x 版本均可就行。如果训练阶段报缺失依赖的错先pip install torch torchvision确认基础环境没问题。3.2 边界攻击核心实现下面这段代码实现了完整的 Decision-Based Attack。先找到一个初始对抗样本再通过迭代二分查找向决策边界靠近。def predict_label(model, x): return model(x).argmax(dim1) def project_to_boundary(model, x0, x_adv, steps10): 在 x0(正常) 与 x_adv(对抗) 之间二分找边界点 lo, hi 0.0, 1.0 original_label predict_label(model, x0) for _ in range(steps): mid (lo hi) / 2 cand (1 - mid) * x0 mid * x_adv if predict_label(model, cand) ! original_label: hi mid else: lo mid return (1 - hi) * x0 hi * x_adv def boundary_attack(model, x0, max_queries5000, step_size0.1): model.eval() original_label predict_label(model, x0) # 阶段1随机噪声初始化直到产生对抗样本 x_adv x0 0.5 * torch.randn_like(x0) queries 0 while predict_label(model, x_adv) original_label: x_adv x0 1.0 * torch.randn_like(x0) queries 1 if queries 100: raise RuntimeError(无法找到初始对抗样本请增大噪声强度) boundary project_to_boundary(model, x0, x_adv) # 阶段2沿边界迭代滑动 queries 10 # 二分查找消耗的查询次数 while queries max_queries: noise step_size * torch.randn_like(boundary) candidate boundary noise if predict_label(model, candidate) ! original_label: boundary project_to_boundary(model, x0, candidate) queries 10 else: queries 1 # 每 100 次查询打印一次扰动大小 if queries % 100 0: l2 (boundary - x0).norm().item() print(fqueries{queries}, l2{l2:.4f}) return boundary逻辑说明第一阶段的循环在随机噪声中寻找一个类别不同的样本这里刻意不把噪声裁剪到像素有效区间内因为二分会自动收缩到有效区间附近。第二阶段的核心是随机游走加二分投影每次在边界点周围加一个随机扰动如果扰动后样本依然是对抗样本就把它投影回边界否则直接丢弃。整个过程不需要模型梯度也不访问模型内部的任何数值完全符合基于决策的信息约束。参数说明step_size控制每步随机扰动的幅度太大则边界点抖动明显太小则收敛缓慢。max_queries是总查询次数上限实际部署中要依据线上服务的速率限制来设定。project_to_boundary里的steps参数控制二分精度每增加 1查询次数大约翻倍但边界定位更准。3.3 查询次数与收敛指标的解读运行上面的攻击函数会看到类似queries1200, l20.3240的输出。这里的 l2 是边界点与原始图像的欧氏距离代表攻击者实际施加的扰动强度。观察这个值的变化趋势比单看“是否攻击成功”更有意义。攻击成功只说明模型给出的类别变了l2 小才说明扰动接近不可见。很多线上黑盒攻击测试里攻击者会把 l2 控制在 0.3 以下对应的像素差异人眼几乎不可察觉。如果算法跑到 3000 次查询后 l2 依然在 0.6 以上通常不是算法不收敛而是步长选择偏大导致边界点在滑动时来回震荡建议把step_size降到 0.05 再跑一遍。4. 改进版 HSJA 与三个必调参数4.1 HSJA 的梯度估计思想边界攻击的效果不错但查询效率偏低。HopSkipJumpAttackHSJA在此基础上引入了一个关键改进用蒙特卡洛采样去估计决策边界的法向梯度再把该梯度用于更积极的几何推进而不是纯粹依赖随机游走。它的核心操作可以理解为在边界点附近采一批随机方向向量 u_k逐个观察沿该方向移动微小距离后分类结果是否发生变化。统计所有改变分类的方向合成的向量就是边界法向的近似梯度估计。def estimate_gradient(model, x_boundary, x0, num_samples100, gamma0.01): 在边界点处用随机采样估计梯度方向 grad torch.zeros_like(x_boundary) original_label predict_label(model, x0) for _ in range(num_samples): u torch.randn_like(x_boundary) u u / u.norm() # 沿 u 方向越过边界一步 candidate x_boundary gamma * u is_adv predict_label(model, candidate) ! original_label grad u if is_adv else -u return grad / num_samples逻辑说明gamma必须取足够小确保候选点只在边界附近活动否则采样点会远离边界估计出来的梯度方向失去意义。num_samples越大梯度估计越稳定但查询开销线性上升。得到梯度方向后HSJA 沿着该方向平移边界点再通过二分投影回边界最终实现比随机游走更快的收敛。4.2 三个典型参数的取值范围HSJA 类攻击有几个参数在实践中最影响成败这里列成一张参考表。参数含义推荐范围参数调大/调小的影响num_samples梯度估计的采样方向数100 ~ 1000增大更稳定查询翻倍减小抖动大binary_search_steps二分查找迭代次数5 ~ 10增大边界定位更精确开销指数上升gamma梯度估计的扰动半径0.01 ~ 0.1过大脱离边界过小梯度弱到不可用除此之外HSJA 的实现里还有step_size这类几何参数但它的作用效果依赖上述三者的配合。常见做法是先固定binary_search_steps7再调num_samples最后才动gamma。4.3 参数设置与监控方法调参不能靠感觉要结合运行日志判断。推荐在攻击循环里记录三类指标当前查询次数、边界点与原始输入的 l2 距离、以及最近 100 次查询中攻击成功率的滑动均值。观察这三类指标有一个经验法则如果 l2 距离在前 500 次查询内下降很快但之后陷入平台期优先减少gamma此时边界点正在一个粗糙的梯度估计方向附近震荡。如果 l2 从一开始就下降得慢把num_samples加倍梯度估计的方向会更准确。另外不要无脑增加binary_search_steps超过 10 之后边界定位精度的提升已经有限但查询开销会大到难以承受。5. 验证攻击有效性的两个技巧5.1 同时上报扰动范数和成功率在项目文档里写“攻击成功率 100%”是很多演示项目的通病。基于决策的攻击天然允许较大的扰动把噪声调到肉眼可见的程度成功率当然高。所以在自己的文档说明里一定要同时列出平均 L2 距离和成功率最好能附带一张“成功率随查询次数变化”的表格这样项目结论才可信。def verify_attack(model, x0, x_adv): l2 (x_adv - x0).norm().item() max_diff (x_adv - x0).abs().max().item() is_success predict_label(model, x_adv) ! predict_label(model, x0) print(fL2{l2:.4f}, max_pixel_diff{max_diff:.4f}, success{is_success}) # 可视化扰动验证人眼不可见性 diff (x_adv - x0).squeeze().cpu().numpy() import matplotlib.pyplot as plt plt.imshow(diff, cmapgray, vmin-0.5, vmax0.5) plt.title(perturbation map) plt.show()画扰动差分图比直接看对抗样本更直观。正常样本的扰动图应当没有明显的结构如果差分图里出现了清晰的数字轮廓说明攻击产生了结构化的伪影这种样本即使通过了分类器也会被人眼判定为异常。5.2 查询预算耗尽时的排错顺序如果 max_queries 用完了还没有收敛排查顺序是先检查project_to_boundary的循环次数是否小到无法把边界点定位到半像素级别再确认step_size是否过大导致边界点绕圈最后才是怀疑模型太强。这个顺序对应三种症状边界点抖动无法下降、l2 呈现锯齿状波动、l2 稳定但攻击不成功。把“二分精度不够”这个误判排除掉再去看梯度估计的方差通常就能在几轮调整内把查询次数压下来。本文还有配套的精品资源点击获取