基于遗传算法与梯度估计的黑盒决策攻击方法
简介基于决策的黑盒对抗网络攻击是当前对抗样本研究的重要方向。这份Python项目源码与文档资料面向人工智能安全、网络攻防相关专业的学生、研究者和工程师帮助读者在无法获取模型内部参数的黑盒条件下实现并理解基于决策的对抗攻击算法。资源共42个文件包含Python源码、实验效果图、数据集CSV、预训练模型权重PTH及说明文档整体大小约10.33MB。源码中不仅有主程序和核心攻击模块还提供多种数据集的加载与模型定义、梯度策略、遗传算法优化、可视化展示等功能目录划分清晰便于直接运行和二次开发。随附的README文档介绍了环境配置与使用方式适合课程设计、毕业设计或项目初期验证已有61人学习参考。1. 黑盒攻击不止有梯度决策信号也能撬动深度学习模型很多人第一次听到「黑盒对抗攻击」时脑子里默认是那种只能拿到模型输出概率的软标签攻击比如迁移攻击或基于得分的零阶优化。但现实中更严苛的场景是你连概率分数都看不到模型只给你一个最终的类别判定比如人脸识别告诉你通过/拒绝内容审核告诉你正常/违规。本文要拆的这个项目就是在这种只有决策标签的极低信息条件下对图像分类模型发起有效对抗攻击。它的核心是用遗传算法加方向性梯度估计在模型决策边界上做高效游走最终用极少的查询次数生成高置信度的对抗样本。代码层面项目以like_attack.py为主入口配合genetic_algorithm.py、gradient_strategy.py、attack_setting.py等模块支持 MNIST、CIFAR-10、CIFAR-100 以及 ImageNet 数据集的攻击实验。适合正在研究对抗鲁棒性、AI 安全测评或者在准备相关课程设计/毕业设计的人直接跑起来能看到攻击效果改改参数就能迁移到自己的模型上。2. 项目结构、环境依赖与数据准备2.1 从压缩包到可运行状态先理清模块职责解压后你会看到一组层次分明的文件。我建议先不要急着运行main.py而是按「数据 → 模型 → 攻击 → 可视化」这条链路梳理一下目录结构这对后面改代码非常关键。核心文件如下文件/目录职责like_attack.py攻击主流程编排初始化、迭代、保存样本genetic_algorithm.py基于遗传算法的候选扰动搜索gradient_strategy/决策梯度估计策略用于构造更新方向attack_setting.py攻击参数配置如查询预算、扰动幅度utils/construct_model_data.py加载模型与数据集的统一入口models/预训练模型定义VGG、ResNet、DenseNetcheckpoints/已训练权重如vgg_mnist.pthimagenet.csvImageNet 数据集索引文件show_or_save.py结果可视化与图片保存其中utils/load_data.py和utils/generate_model.py是辅助函数gradient_strategy目录下面应该是多种策略的接口实现比如基于随机采样的方向估计和基于历史梯度的累积优化。理解了这个分层你就能明白这个项目不是把攻击写成一坨而是把「数据加载」「模型管理」「攻击算法」解耦了替换数据集或模型时不用动攻击核心。2.2 环境配置与依赖安装项目基于 PyTorch 实现因为模型定义和检查点加载都依赖torch。我建议使用 Python 3.8 或 3.9太高的 Python 版本在某些老版本 PyTorch 下会踩编译器的坑。安装依赖时直接使用pippip install torch torchvision pandas numpy matplotlib pillow tqdm注意imagenet.csv只是索引文件图片需要你自己准备 ImageNet 的验证集子集。如果只想快速验证攻击流程可以先跑 MNIST 或 CIFAR-10这两个数据集加载时如果没有本地缓存torchvision会自动下载。这里有一个常见的坑如果你在公司内网torchvision下载数据集会超时我一般先手动下载数据集压缩包放到~/.cache/torchvision/datasets/下再把data/dataset里的加载逻辑改成本地路径。在config/config.py里你会看到类似这样的配置块DATASET mnist # mnist / cifar10 / cifar100 / imagenet MODEL_NAME vgg_mnist # 对应 checkpoints 下的权重文件名 QUERY_BUDGET 10000 # 最大查询次数 ATTACK_TYPE decision # 仅使用决策标签 TARGETED False # 非定向攻击 EPSILON 0.3 # 扰动预算常用于 MNIST BATCH_SIZE 1 # 决策攻击按样本串行更稳定这里的EPSILON是 L_inf 球半径。对于 MNIST0.3 的扰动在人眼看来依然明显但可以接受对于 CIFAR一般用 8/255 ≈ 0.031ImageNet 上更严格常用 4/255 或 8/255。不要直接拿 MNIST 的配置跑 ImageNet否则攻击耗时和成功率都会非常不理想。2.3 下载后的第一件事验证模型检查点项目自带matplt.py和test.pytest.py可以先用来验证模型权重是否与代码匹配。运行前确认checkpoints/vgg_mnist.pth存在于正确路径python test.py --model vgg_mnist --dataset mnist如果输出类似Test accuracy: 99.2%说明模型加载正常。如果出现size mismatch错误大概率是generate_model.py里的网络定义与训练时的网络结构不一致比如VGG的num_classes默认为 10而 CIFAR-100 需要改成 100。这种问题在多个模型之间切换时非常常见建议在这步花两分钟确认后面攻击流程才不会在莫名其妙的维度错误上卡住。3. 核心攻击机制决策梯度估计与遗传搜索的配合3.1 为什么决策攻击要分「方向探索」和「距离收缩」两个阶段基于决策的黑盒攻击也叫 Decision-based Attack最早由 Boundary Attack 提出后续有 HSJA、OPT、SurFree 等演进。它们的共同思路是先找到一张已经属于目标类或对抗类的初始样本然后沿着决策边界逐步靠近原始样本。这个项目沿用类似框架但将边界上的游走策略换成了「遗传算法 梯度策略」的组合。整个迭代循环在like_attack.py中大概是这样的伪代码逻辑# 初始化从随机噪声或均匀分布采样中找对抗样本 adv initialize_adversarial(x_orig, target_label) for step in range(max_steps): # 1. 在决策边界上搜索下降方向梯度估计 direction estimate_gradient(adv, model, query_budget_per_step) # 2. 沿方向推进尝试减小与原始样本的距离 new_adv adv step_size * direction # 3. 如果新样本仍是对抗样本则接受并调整步长 if model.predict(new_adv) target_label: adv new_adv step_size step_size * 1.2 else: step_size step_size * 0.8这里的关键在于estimate_gradient。由于我们只能拿到二值化决策无法直接计算梯度所以要用零阶优化来估计。常见做法是在当前对抗样本附近随机采样若干方向然后看往哪个方向微调后样本仍然保持对抗性再往哪个方向微调会导致误分类——两个方向之差就是决策边界的法线方向。3.2 遗传算法在方向采样中的应用项目的genetic_algorithm.py并不像进化算法论文里那样复杂它更像是用遗传算子在候选方向池里做搜索。每次迭代维护一个候选方向种群每个个体是单位方向向量。适应度定义为「沿该方向移动后对抗样本到原始样本距离的减少量」加上「保持对抗性的成功率」。下面是我基于项目结构还原的简化版核心代码帮你理解输入输出格式import numpy as np def fitness(direction, adv, x_orig, model, target_label): # 沿 direction 移动一个较小步长 candidate adv 0.05 * direction # 计算与原始样本的 L2 距离 dist np.linalg.norm(candidate - x_orig) # 如果移动后不再是目标类给一个高惩罚 if model.predict(candidate) ! target_label: dist 1e6 return -dist # 距离越小适应度越高 def genetic_search(parent_pop, adv, x_orig, model, target_label, generations20): pop parent_pop.copy() for _ in range(generations): scores np.array([fitness(ind, adv, x_orig, model, target_label) for ind in pop]) idx np.argsort(scores)[-len(pop)//2:] # 保留前一半 elites pop[idx] # 变异向精英方向加噪声 new_pop [] for e in elites: for _ in range(2): mutant e np.random.randn(*e.shape) * 0.1 mutant / np.linalg.norm(mutant) new_pop.append(mutant) pop np.vstack([elites, np.array(new_pop[:len(pop)-len(elites)])]) best_idx np.argmax([fitness(ind, adv, x_orig, model, target_label) for ind in pop]) return pop[best_idx]fitness函数里我故意用了很大的数值惩罚是为了防止样本掉出对抗区域。实际项目中你还会看到该函数结合gradient_strategy返回的方向——先由梯度策略给出一个粗糙方向再用遗传算法在粗糙方向邻域内精细化搜索。这就像先用指南针找大方向再用探针在草丛里探路。参数调整时注意generations不要太大否则单次迭代查询次数翻倍总体预算会被快速消耗。3.3 距离度量与步长衰减策略决策攻击不仅要保证对抗性还要让扰动尽可能小。项目里主要用 L2 距离衡量优化目标因此每个方向上的步长大小直接影响收敛速度。我习惯把步长初始化为原始图片 L2 范数的 10%然后根据每次更新是否成功进行自适应调整成功则增大步长失败则缩小。在attack_setting.py中有类似参数INIT_STEP_SIZE 0.01 STEP_SIZE_DECAY 0.5 STEP_SIZE_GROWTH 1.5 MIN_STEP_SIZE 1e-6注意STEP_SIZE_DECAY如果设置成 0.9 更平滑但早期收敛慢0.5 更激进适合预算有限时快速逼近边界。我测试下来CIFAR-10 上用 0.7 左右表现最稳定MNIST 上 0.5 反而能更快把扰动压缩到 0.3 以下。4. 从 MNIST 到 ImageNet模型替换、参数适配与常见失败模式4.1 切换数据集时你需要同时改六个地方项目支持四类数据集但很多人只改了config.py中的DATASET就运行结果报错或攻击效果很差。因为load_data.py、generate_model.py、construct_model_data.py以及imagenet.csv都需要协同调整。下面是我整理的一个检查清单组件MNIST → CIFAR-10CIFAR-10 → ImageNetload_data.py图片通道数从 1 改 3需要imagenet.csv和本地图片根目录generate_model.py修改输入尺寸 32x32修改输入尺寸 224x224construct_model_data.py标签映射表使用 ImageNet 1000 类扰动预算0.3L_inf0.03L_inf步长初始化0.050.005查询预算1000050000图片大收敛慢如果imagenet.csv里的图片路径是相对路径建议把所有图片放在项目根目录下的imagenet_images/文件夹并保持 csv 中的文件名一致。我更推荐先跑通 CIFAR-10 再上 ImageNet因为 ImageNet 的高分辨率会显著放大遗传算法的计算开销单张图可能会跑几分钟。4.2 梯度策略模块不只是随机方向还有历史平滑在gradient_strategy目录下可能有不止一种策略实现例如random_gradient.py和momentum_gradient.py。决策边界附近的梯度估计噪声很大单次采样的方向往往不准确。项目中的策略思路类似于 NES自然进化策略用多次采样取期望def estimate_gradient_nes(adv, model, target_label, samples50, sigma0.01): dim adv.shape g np.zeros_like(adv) for _ in range(samples): u np.random.randn(*dim) u u / np.linalg.norm(u) # 在两侧采样利用决策结果 p1 model.predict(adv sigma * u) p2 model.predict(adv - sigma * u) if p1 target_label and p2 ! target_label: g u elif p2 target_label and p1 ! target_label: g - u return g / samples注意这里通过「交叉验证」来避免单次噪声的误导只有一侧保持对抗性而另一侧失去对抗性时才能判断u是靠近边界内侧还是外侧。如果两侧都保持对抗性说明方向平行于边界不应计入梯度。这个策略实际效果比直接比较距离更稳定。我在自己的项目里复现时把这个samples设置为 100查询预算消耗虽大但每轮方向质量高总步数减少最终总查询次数反而更少。4.3 查询预算与成功率权衡决策攻击的评估指标通常有三个平均查询次数、扰动大小L2 或 L_inf、成功率。项目README.md里应该记录了在vgg_mnist.pth模型上的 benchmark。我自己跑下来发现非定向攻击的成功率非常高接近 100%但定向攻击就难很多尤其是目标类别与真实类别相近时比如 CIFAR-10 里把「猫」攻击成「狗」边界是犬科 vs 猫科决策边界较复杂需要更多查询。建议做消融实验时固定攻击算法调整QUERY_BUDGET从 1000 到 10000记录扰动曲线的下降斜率。如果 3000 次查询后扰动还停在 0.5 以上大概率是步长衰减太快或者遗传算法早熟而不是预算不够。这个时候优先调大STEP_SIZE_GROWTH而不是盲目加预算。5. 攻击效果验证与可视化一张图确认对抗样本的「杀伤力」最后一个实战技巧是关于验证的。show_or_save.py和matplt.py是现成的可视化工具但直接跑main.py只会把对抗样本存到文件夹里没法直观看出攻击成功与否。我通常会在攻击循环里保存三张图原始图片、对抗图片、噪声差。并计算confidence——不过决策攻击没有置信度只能用「模型分类结果 与真实标签的差距」来侧面评估。下面这段代码可以放在main.py的每次迭代结束后用于实时打印攻击进度# 第5章示例每500次查询输出当前状态 if total_queries % 500 0: l2_dist np.linalg.norm(adv - x_orig) linf_dist np.max(np.abs(adv - x_orig)) current_pred model.predict(adv) is_success (current_pred target_label) print(fQueries: {total_queries}, L2: {l2_dist:.4f}, fLinf: {linf_dist:.4f}, success: {is_success}) # 保存当前对抗样本 save_image(adv, fstep_{total_queries}.png)注意这里的save_image需要将经过归一化的张量转换回 0-255 的整数格式否则保存的图片会是黑的。常见做法是def save_image(tensor, path): img tensor.squeeze().cpu().numpy().transpose(1, 2, 0) img (img - img.min()) / (img.max() - img.min() 1e-8) plt.imsave(path, img)这种归一化会改变图片的真实像素范围只适合可视化不适合作为攻击输出。如果想把对抗样本真正用于评测需要保存原始像素值并记录扰动范围。当你把第 500 步、第 1000 步、第 3000 步的图片放在一起看会明显看到噪声从「雪花点」逐渐变成「紧贴物体边缘的结构化纹理」。这个现象说明决策边界被精确定位了。另一个验证技巧是把model.predict换成模型最后一层 softmax 之前的 logits 输出自己计算置信度你会发现即便攻击成功置信度也往往在 0.5~0.7 之间摇摆不如基于迁移的对抗样本那样高置信。这正好说明决策攻击本质上是在「蹭边界」而不是把样本推入目标类的引力中心。最后建议你在攻击结束后调用generate_video.py把中间保存的图片合成视频。这样当你向导师或评审展示时可以从视频中直观看到扰动逐步收敛的过程比单纯贴一张前后对比图更有说服力。视频中每一帧代表 500 次查询的状态帧率设为 10 帧/秒一段 10000 次查询的攻击过程刚好 20 秒左右节奏合适。本文还有配套的精品资源点击获取