ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

联邦学习对抗攻击实战:用Python实现PGD投毒与防御

联邦学习对抗攻击实战:用Python实现PGD投毒与防御 简介面向人工智能、计算机等相关专业学生与科研人员聚焦联邦学习场景下的模型安全隐患提供一套可运行的对抗攻击完整实现。资源包共18个文件包括10个pth模型权重、6个Python核心脚本、1个说明文档及1个许可证压缩后约623KB其中Python脚本是主要实现逻辑pth为预训练参数目录层级清晰便于按需定位。代码覆盖联邦基础函数、距离攻击、对抗样本生成与成员推断等核心环节每个函数均附详细注释可直接基于已训练好的pth权重运行验证攻击效果省去重复训练时间借助注释还能逐步理解从本地训练、全局聚合再到发起攻击的完整链路。该资源适合有一定Python和深度学习基础、正在完成课程大作业或毕业设计的学生参考也适合人工智能安全方向初学者对照实践。已有58人学习。1. 课程大作业里的联邦学习对抗攻击到底是什么为什么你的模型最容易被这一招打穿联邦学习一直打的招牌是“数据不出本地隐私更安全”但你如果在课程大作业里真去复现一遍会发现一个反直觉的事实服务器只收梯度、不看数据这个设计恰恰给对抗攻击留了一扇最大的后门。恶意客户端不需要碰任何人的原始数据只要在本地把自己那一份梯度或模型参数改一改就能让全局模型在特定样本上定向犯错甚至把后门埋进聚合后的模型里。这篇笔记要解决的就是怎么用 Python 写出一套带详细注释的联邦学习对抗攻击源码把数据切分、客户端训练、服务端聚合、恶意注入这一整条链路跑通并讲清楚参数怎么调、坑在哪。适合正在做联邦学习课程设计、毕业设计或者想从代码层面理解“投毒攻击到底是怎么生效”的从业者。读完你至少能交出一份能跑的作业并能在答辩时把攻击原理讲明白。2. 把联邦学习和对抗攻击放进同一个小实验架构设计与最小数据集选型2.1 联邦学习的“恶意客户端”视角攻击为什么藏在聚合环节要理解联邦学习里的对抗攻击先得把联邦学习分类搞清楚。常见做法是按聚合方式分FedAvg 是最主流的客户端本地训练若干轮后上传模型参数服务端按数据量加权平均FedSGD 则是上传梯度而不是参数通信量更大但聚合粒度更细FedProx 在 FedAvg 基础上加了近端项用来压制 Non-IID 数据下的模型漂移。课程作业里 90% 的情况选 FedAvg 就够了因为它代码量最小、收敛可视化最直观老师也最容易看懂。那攻击为什么藏在聚合环节因为 FedAvg 的聚合操作本质上是“加权平均”它对异常值的容忍度非常低。一个恶意客户端只要把上传的梯度方向翻转或者干脆在本地数据上做对抗训练再返回被污染的参数服务端没有任何手段验证这些参数是否来自真实数据分布。FedSGD 稍微好一点因为梯度范数异常更容易被发现但攻击者同样可以伪造梯度统计特征。这就是为什么几乎所有联邦学习投毒攻击的研究论文都假设“服务端是 honest-but-curious客户端可能有一小部分是恶意”的威胁模型。从做作业的角度你想在有限时间内复现一套攻击最省力的方案是正常客户端做普通本地训练恶意客户端做基于 PGD 的对抗训练或标签翻转服务端把两边参数一平均。不需要改联邦学习框架本身攻击效果就出来了。这也是这个标题值得做的原因——代码量不大但能撑起一篇有深度的课程报告。2.2 用 MNIST 或 CIFAR-10 搭起本地联邦环境数据切分与客户端模拟选数据集这件事直接影响你作业的完成速度和攻击效果的可见度。MNIST 是 28x28 单通道手写数字在 CPU 上训练一个两层全连接网络一个客户端本地跑 5 个 epoch 只需要十几秒非常适合跑通流程。CIFAR-10 是 32x32 三通道彩色图攻击效果更接近真实场景但你需要 ResNet 之类的卷积网络才能体现攻击的破坏力训练时间会拉长到小时级。我一般建议第一版用 MNIST 跑通全流程交作业前再换 CIFAR-10 补一个实验对比。数据切分是联邦学习入门里最容易被糊弄过去的一步。很多人直接把数据集随机打乱平均分给客户端这是 IID 分布在真实场景中几乎不存在。真实联邦场景里每个客户端的数据分布高度偏斜比如某台手机上的照片全是室内场景另一台全是户外。这种 Non-IID 分布会放大恶意客户端的影响力因为服务端很难通过对比客户端梯度来识别异常。下面这段代码实现了最简单的 Non-IID 切分按标签聚拢数据再以一定比例分配给客户端。import numpy as np from torch.utils.data import Subset def partition_data(dataset, num_clients10, non_iid_ratio0.5): idxs np.arange(len(dataset)) np.random.shuffle(idxs) labels np.array(dataset.targets) if hasattr(dataset, targets) \ else np.array([dataset[i][1] for i in range(len(dataset))]) client_indices {} per_client len(dataset) // num_clients for c in range(num_clients): if np.random.rand() non_iid_ratio: # Non-IID 分支优先取某个单一标签的样本 label c % len(dataset.classes) candidate idxs[labels[idxs] label][:per_client] # 如果该标签样本不够用随机样本补齐 if len(candidate) per_client: extra idxs[~np.isin(idxs, candidate)][:per_client - len(candidate)] candidate np.concatenate([candidate, extra]) client_indices[c] candidate else: # IID 分支顺序切片即可 start c * per_client client_indices[c] idxs[start:start per_client] np.random.shuffle(client_indices[c]) return {c: Subset(dataset, client_indices[c]) for c in range(num_clients)}逻辑说明non_iid_ratio 控制每个客户端拿到 Non-IID 分布的概率取 0.5 表示约一半客户端会拿单标签数据。单标签分支里先按标签筛选候选索引取前 per_client 个如果样本不够从剩余索引里补足。这样每个客户端的数据量保持一致避免因为数据量不均衡影响 FedAvg 的权重计算。参数说明num_clients 是模拟的客户端数量课程作业建议 10太少聚合效果不稳定太多训练时间拉长。non_iid_ratio 建议从 0.3 试到 0.7你会发现非独立同分布程度越高全局模型收敛越慢但恶意客户端的攻击也会更容易被淹没在梯度噪声里。这里用 Subset 包装而不是直接复制数据是为了省内存——MNIST 全量也就几万张图但 Subset 在后续做 DataLoader 时更灵活。2.3 攻击面定位白盒、黑盒与投毒课程作业常选哪条路很多人一听到对抗攻击第一反应是 FGSM 或 PGD 那种对输入加噪声、让单一模型误分类的做法。但在联邦学习里攻击面完全不一样——攻击者不是向模型输入加扰动而是直接控制参与训练的客户端。按攻击者掌握的信息量分常见的联邦学习攻击有三类白盒攻击攻击者知道全局模型结构和聚合算法可以精确计算梯度方向在本地制造一个“最毒”的更新上传。课程作业最常选这条因为实现直观、效果可解释。黑盒攻击攻击者只知道最终模型通过查询接口构造对抗样本再把这些样本注入本地训练集。相当于把输入扰动攻击嫁接到投毒上实现复杂度偏高但更贴近真实场景。数据投毒或标签翻转最简单粗暴。把恶意客户端本地数据集里的标签全部翻转比如 1 改成 7再正常训练上传。聚合后模型会对真实标签为 1 的样本产生混淆。从做作业性价比来看我推荐组合路线恶意客户端在本地做 PGD 对抗训练等价于你给了攻击者白盒权限同时把目标标签的样本标签翻转叠加两种攻击手段让防御更难一次拦住。这样代码里就同时覆盖了输入扰动和标签投毒两条技术线报告里也有得写。3. 用 Python PyTorch 跑通一套最小联邦对抗攻击源码3.1 客户端与服务端的骨架代码通信轮次、本地训练与 FedAvg 聚合这一节直接给出最小可运行的程序骨架。整体结构分三块客户端类负责本地训练服务端类负责聚合主循环负责通信调度。注意这里的代码做了刻意简化——所有客户端共享同一个模型结构本地训练用的是 SGD聚合方式是最基础的按样本量加权平均但它足以让你观察攻击前后的行为差异。import copy import torch from torch import nn from torch.utils.data import DataLoader class FedClient: def __init__(self, cid, model_fn, dataloader, lr0.01, epochs1, maliciousFalse): self.cid cid self.model model_fn() self.dataloader dataloader self.lr lr self.epochs epochs self.malicious malicious self.device torch.device(cuda if torch.cuda.is_available() else cpu) self.model.to(self.device) def local_train(self, global_params): # 用全局参数覆盖本地模型保证每轮起点一致 self.model.load_state_dict(global_params) optimizer torch.optim.SGD(self.model.parameters(), lrself.lr) loss_fn nn.CrossEntropyLoss() self.model.train() for epoch in range(self.epochs): for data, target in self.dataloader: data, target data.to(self.device), target.to(self.device) if self.malicious: # 恶意客户端先做 PGD 攻击增强再正常训练 data pgd_attack(self.model, data, target, epsilon0.1, alpha0.02, iterations5) optimizer.zero_grad() output self.model(data) loss loss_fn(output, target) loss.backward() optimizer.step() return {k: v.cpu().detach().clone() for k, v in self.model.state_dict().items()}逻辑说明local_train 接收服务端下发的全局参数用 load_state_dict 覆盖本地模型再进行若干轮本地训练。恶意客户端的差异只在训练前多调了一次 pgd_attack对输入样本加对抗扰动后继续正常训练相当于让模型学会对扰动后的样本仍然输出正确标签——但由于扰动方向是让 loss 最大化模型实际上在“努力适应被污染的特征分布”。参数说明lr 取 0.01 时 MNIST 收敛稳定换 CIFAR-10 建议降到 0.005。epochs 是本地训练轮数正常客户端设 1恶意客户端我会设 3 以上——后面 4.3 会解释为什么要这样放大恶意更新的影响力。返回的是 state_dict 而不是梯度这是 FedAvg 和 FedSGD 最直观的区别。服务端聚合代码class FedServer: def __init__(self, model_fn, clients, sample_ratio0.5): self.global_model model_fn() self.clients clients self.sample_ratio sample_ratio self.device torch.device(cuda if torch.cuda.is_available() else cpu) def aggregate(self, selected_clients, weights_dict): # 按样本量加权平均 FedAvg total_samples sum(len(c.dataloader.dataset) for c in selected_clients) new_state {k: torch.zeros_like(v) for k, v in self.global_model.state_dict().items()} for client, w in zip(selected_clients, weights_dict): n len(client.dataloader.dataset) for k in new_state.keys(): new_state[k] w[k] * (n / total_samples) self.global_model.load_state_dict(new_state) return copy.deepcopy(self.global_model.state_dict()) def round(self, round_idx): # 每轮随机抽一部分客户端参与 n max(1, int(len(self.clients) * self.sample_ratio)) selected np.random.choice(self.clients, n, replaceFalse) global_params copy.deepcopy(self.global_model.state_dict()) weights [c.local_train(global_params) for c in selected] return self.aggregate(selected, weights)逻辑说明aggregate 函数按每个客户端数据集的样本数占总样本数的比例做加权平均这是 FedAvg 论文里的标准做法。恶意客户端如果数据量恰好又比别人大它的模型参数在聚合时占的权重会更高攻击效果翻倍。参数说明sample_ratio 是每轮参与聚合的客户端比例。联邦学习里并不是每轮所有客户端都上线取 0.5 表示 10 个客户端每轮随机抽 5 个。这个参数直接影响攻击的稳定性——抽中恶意客户端的轮次越多攻击见效越快但模型波动也更大。3.2 恶意客户端注入对抗梯度基于 PGD 的本地对抗训练源码PGDProjected Gradient Descent投影梯度下降是 FGSM 的迭代强化版本。它在输入上做多次小步扰动每次把扰动投影回 epsilon 球内最终得到让模型 loss 最大化的对抗样本。为什么选 PGD 而不是 FGSM因为 FGSM 一步到位扰动方向容易过拟合到当前模型而 PGD 迭代出来的是更“毒”的样本对聚合后的全局模型有更强的迁移效果。def pgd_attack(model, data, target, epsilon0.1, alpha0.02, iterations5): PGD 白盒攻击在 epsilon 球内迭代寻找让 loss 最大的输入扰动 model.eval() # 保留原始数据用于裁剪 ori_data data.clone().detach() # 扰动从零开始迭代更新 delta torch.zeros_like(data, requires_gradTrue) loss_fn nn.CrossEntropyLoss() for _ in range(iterations): output model(ori_data delta) loss loss_fn(output, target) loss.backward() # 沿梯度上升方向扰动 delta.data delta.data alpha * delta.grad.detach().sign() # 裁剪到 epsilon 球内 delta.data torch.clamp(delta.data, -epsilon, epsilon) delta.data torch.clamp(ori_data delta.data, 0, 1) - ori_data delta.grad.zero_() model.train() return ori_data delta.detach()逻辑说明pgd_attack 的输入是模型、原始图像和标签。每次迭代先用当前扰动后的输入过模型计算 loss反向传播得到梯度然后沿梯度上升方向走一个 alpha 步长最后把 delta 裁剪回 [-epsilon, epsilon] 区间并保证原图加扰动后仍在 [0, 1] 像素范围内。迭代 5 次后的对抗样本会在本地训练中参与前向传播和反向传播让模型朝错误的方向更新参数。参数说明epsilon 是扰动预算MNIST 上取 0.1 已经能明显影响模型CIFAR-10 建议取 0.03 到 0.05因为像素取值范围相同但模型更复杂扰动太大反而会让对抗样本失去隐蔽性。alpha 是每步步长经验值是 epsilon 的五分之一到十分之一。iterations 取 5 到 10超过 10 边际收益递减而且恶意客户端本地训练时间会翻倍。这里还有一条易踩的坑pgd_attack 内部把模型切到了 eval 模式但调用前客户端可能已经在 train 模式。我的习惯是在函数结束前恢复 model.train()否则后面本地训练的 BatchNorm 统计量会出错——尤其换 ResNet 时BatchNorm 在 eval 模式下累积的 running stats 会让 dropout 失效训练曲线会异常。3.3 服务端防御基线按梯度范数裁剪后的模型鲁棒性变化课程作业如果只做攻击不做防御答辩时容易被问住。但完整的差分隐私联邦学习实现起来太重我推荐在服务端加一个简单的梯度范数裁剪作为防御基线对比。名字可以叫“基于范数裁剪的异常更新抑制”写上报告就能加分。def aggregate_with_clip(self, selected_clients, weights_dict, clip_norm5.0): 带范数裁剪的 FedAvg 聚合 先把每个客户端的更新裁剪到 clip_norm 内再做加权平均 # 计算每个客户端本次更新的总范数 global_params self.global_model.state_dict() deltas [] for w in weights_dict: delta {k: w[k] - global_params[k] for k in global_params.keys()} total_norm torch.sqrt(sum(torch.sum(d[k] ** 2) for d in [delta] for k in d)) # 范数超过阈值则缩放 if total_norm clip_norm: scale clip_norm / total_norm delta {k: v * scale for k, v in delta.items()} deltas.append(delta) # 平均 delta 并叠加到全局参数上 avg_delta {k: torch.mean(torch.stack([d[k] for d in deltas]), dim0) for k in deltas[0].keys()} new_state {k: global_params[k] avg_delta[k] for k in global_params.keys()} self.global_model.load_state_dict(new_state)逻辑说明每个客户端上传的不是直接参数而是参数与全局参数之差即 delta。计算每个 delta 的总范数超过 clip_norm 就整体等比缩小让恶意客户端的大范数更新不会一次性把全局模型拉飞。之后再对 delta 求平均叠加回全局参数。参数说明clip_norm 需要根据模型规模调整两层全连接网络取 5.0 即可ResNet 这类大模型可能要取 20 到 50。设太大会让裁剪失效设太小则正常客户端的更新也被严重压缩模型收敛变慢。做实验时建议固定其他变量只调 clip_norm你会看到攻击成功率随裁剪强度单调下降——这就是一个非常漂亮的实验曲线。4. 三个必调参数让你的攻击从“把准确率打低”变成“让模型定向犯错”4.1 攻击轮次与投毒客户端比例联邦聚合里的小样本效应很多新手做投毒攻击一上来就把一半客户端设为恶意。结果不出所料——聚合后的模型要么完全崩溃不收敛要么准确率掉几个点之后恢复正常。这其实是对抗攻击目标设错了。课程作业里你要展示的不应该是“把模型打崩”崩掉的模型任何人都能看出异常没有攻击隐蔽性。真正有价值的攻击是全局模型在普通测试集上准确率只下降 2% 到 3%但在某个特定类别或特定触发模式下错误率飙升到 80% 以上。要达成这个目标投毒客户端占比是关键。我做过一组对照实验10 个客户端里恶意客户端分别占 1、2、3、4 个。结果是1 个恶意客户端时攻击效果不明显epsilon 调到 0.2 都只能在目标类别上产生几个百分点的偏移2 到 3 个时效果最好既能明显观察到目标类别的准确率塌陷又不至于让全局模型发散4 个以上时模型开始剧烈震荡防御方一眼就能看出有客户端在捣鬼。原因在于 FedAvg 的加权平均本质上是多数表决。恶意客户端太少它们的毒更新会被正常客户端的梯度淹没太多则直接主导了聚合方向失去隐蔽性。这里的“小样本效应”指的是聚合轮次和恶意客户端出现频率的交互如果你的 sample_ratio 是 0.5每轮只有 5 个客户端参与那么 10 个客户端里有 2 个恶意客户端时每轮抽中至少一个恶意客户端的概率是 1 - C(8,5)/C(10,5) ≈ 78%。也就是说大多数轮次攻击都在生效但仍有一部分轮次是干净的——这种间歇性投毒比持续性投毒更难被检测算法发现。4.2 扰动大小 epsilon 的双刃剑逃过检测与攻击效果的平衡epsilon 是最直观的参数但也是最容易被误调的。常见误用是把它设成 0.3 甚至 0.5然后惊奇地发现攻击成功率反而下降了。原因很简单epsilon 越大本地训练时模型见到的样本与原始数据分布偏离越远模型在对抗样本上学到的特征偏离真实分布聚合后全局模型的整体性能大幅下降。但服务端如果做了梯度范数裁剪这种大扰动更新会被判定为异常直接压缩到接近零。一个更隐蔽的问题epsilon 过大会导致恶意客户端本地训练无法收敛。因为 PGD 每次迭代都在往 loss 增大的方向推如果扰动空间太大模型在 5 个迭代内就会被推到某个极值附近后续正常训练的梯度方向会变得极不稳定——有时候这轮攻击效果好下轮又跌没了。这是联邦学习里最典型的“攻击效果波动”原因。按我的经验MNIST 上 epsilon 从 0.05 起步每次翻倍直到 0.2。观察两个指标一是全局模型在干净测试集上的准确率二是目标类别上的攻击成功率。epsilon 0.1 时通常能同时满足“主任务准确率下降不超过 3 个百分点”和“目标类别准确率下降 30 个百分点以上”。这个区间就是你课程报告里要展示的实验数据。CIFAR-10 上对应区间是 0.01 到 0.05数值完全不同务必重新扫参数不要直接搬 MNIST 的经验值。4.3 恶意客户端本地 epoch 的放大效应为什么它比全局轮次更敏感聚合轮次全局通信轮次对攻击效果的影响反而是最弱的。20 轮的实验里攻击效果从第 5 轮开始显现之后基本趋于稳定。真正的放大器是恶意客户端的本地 epoch 数。攻击者可以在本地多训练几轮让自己的模型在主任务上做到很高的本地性能再上传一个更极端的模型参数。为什么本地 epoch 有放大效应因为 FedAvg 的聚合是对模型参数的加权平均而不是对训练过程的平均。一个恶意客户端本地训练 10 个 epoch 后的模型参数已经离全局模型很远即使它只占 1/10 的权重也会把平均结果拉向它的方向。相比之下全局轮次只是决定这个拉偏过程重复多少次一旦方向确定后面的轮次不过是按比例积累。从防御角度看这种放大效应其实是梯度裁剪的克星。只裁剪单轮更新范数但恶意客户端本地训练 10 轮后它的更新方向已经被“打磨”得很干净——范数不大但方向非常明确地指向攻击目标。这就是为什么只靠范数裁剪很难防住这类攻击你需要在聚合前加上方向异常检测比如 cosine similarity 过滤而不是只看大小。5. 对抗攻击联邦学习常见坑从“不掉点”到“掉点不收敛”的排查清单5.1 现象恶意客户端参与后全局准确率不降反升这是最容易让人自我怀疑的结果。明明加了 PGD 对抗训练全局模型在主任务上的表现却比基线还好。原因有两个第一epsilon 设得太小PGD 生成的对抗样本和原图差异极小相当于给本地训练加了一点数据增强意外提高了泛化能力第二恶意客户端的本地 epoch 太少扰动带来的影响在聚合时被平均掉了。解决方法是先确认恶意客户端本地模型在它自己的对抗样本上确实有下降趋势。可以单独打印恶意客户端本地训练后的测试 loss如果本地 loss 还在降说明攻击强度不够加大 epsilon 或本地轮数再试。另一个排查点是确认 pgd_attack 真的在更新 delta——一个常见翻车是 requires_grad 没设好导致 delta 恒为 0。5.2 现象攻击成功但验证集误分类集中在一类有时你看到全局模型在某些类别上准确率掉了但细盘一下发现掉的恰好是恶意客户端本地标签翻转的目标类。这种情况不是攻击失败而是攻击目标太单一了——模型只是把“数字 1”混淆成“数字 7”其他类别完全正常。这在做演示时反而不够漂亮老师一眼就看穿你的攻击只影响了一个类。更隐蔽的做法是同时翻转多个标签对1-7、3-8或者在多个客户端里分别使用不同的目标标签。另外还要警惕如果按 2.2 的 Non-IID 切分有些客户端本来就只有一两个标签的数据它的本地模型天然对某些类别敏感。此时全局模型的局部掉点可能只是灾难性遗忘的表现而不是攻击生效。区分方法很简单换一个 IID 切分跑同样参数如果掉点现象消失说明是数据分布导致的不是攻击结果。5.3 现象本地训练稳定聚合后模型直接崩掉这是新手最容易遇到的情况恶意客户端单独跑没有任何问题但每轮聚合完下轮所有客户端都像疯了一样loss 直接变成 NaN 或者冲到几千。原因是恶意客户端上传的更新范数太大直接把聚合后的全局参数推到了一个 loss 地形极差的区域。此时正常客户端下一轮从很差的起点开始训练又会回传更大的梯度形成正反馈恶性循环。解决方法是三层一是在服务端做梯度裁剪按 3.3 的方式clip_norm 先调到很小的值二是降低全局学习率FedAvg 聚合后往往还要乘以一个 0.1 到 1.0 的缩放系数三是检查恶意客户端是否在本地训练时把模型权重吹得过大——可以加一项权重衰减weight_decay来抑制。还有一个排查思路容易被忽略检查 DataLoader 是否设置了 shuffleTrue。在联邦环境中如果每个客户端的数据分片没有提前打乱顺序某些客户端可能同一轮次连续读到相同标签的样本导致本地模型向单一类别过拟合聚合时形成虚假的“攻击效果”。把 shuffle 关掉再试一次如果攻击效果消失说明你的实验设置里存在混淆变量。5.4 现象修复一个坑后另一个坑出现——参数联动效应联邦学习的参数不像单模型训练那样独立可调。epsilon、投毒比例、客户端本地 epoch、参与率、裁剪阈值这五个参数互相牵制。最常见的联动是你调小了 epsilon 想让攻击更隐蔽结果发现攻击效果消失你把投毒比例调大补偿又触发了 5.3 的发散问题你加上裁剪阈值压住发散又发现裁剪把恶意更新一起干掉了。我自己踩过这个连环坑后养成了一个习惯固定其他四个参数每次只动一个并且记录实验矩阵。下面是我常用来记录参数组合的表头参数名基线值最小值最大值步长观察指标epsilon0.10.020.30.05主任务准确率、攻击成功率恶意客户端数量2141攻击成功率、模型是否崩溃本地 epoch31101全局 loss 是否震荡clip_norm5.01.020.02.0聚合后准确率、更新范数每个参数单独跑一组实验记录准确率曲线和目标类别准确率。这样你交出去的实验报告天然有说服力也方便在答辩时回答“你怎么确定攻击确实起效了”这种问题。6. 验证攻击是否有效的三个步骤准确率曲线、去中心化评估与迁移视角先定义两个容易混淆的指标。全局准确率ACC是模型在完整测试集上的分类准确率它衡量的是攻击的隐蔽性攻击成功率ASR是针对特定目标攻击的指标比如“把标签为 1 的样本预测为 7 的比例”它衡量的是攻击的有效性。课程报告里这两个指标必须同时出现只报 ACC 下降会被老师追问“是不是模型本身收敛不好”。验证攻击效果的第一步是画一条全局准确率随通信轮次变化的曲线。一个正常联邦训练的过程应该是平滑下降损失或上升准确率而加入了恶意客户端后曲线会出现两类特征阶梯式突降通常对应恶意客户端被抽中的轮次或者持续下滑但斜率变缓说明投毒更新在持续累积。我一般会把干净基线和攻击实验跑相同的种子两条曲线画在一起做对比一眼就能看出攻击的起始轮次和强度。第二步是把聚合后的模型部署到未参与联邦训练的独立数据集上做黑盒评估。这是检验攻击泛化能力的关键——一个真正的投毒攻击不只是在参与客户端的数据上有效在没有见过的东西上也应该让模型定向犯错。做法很简单从 MNIST 测试集中挖出一部分样本只保留标签 1 和 7统计模型把 1 预测成 7 的比例。如果 ASR 高于随机水平说明攻击不是恶意客户端本地记忆的结果而是真正污染了全局模型的决策边界。最后一步是迁移视角这也是课程作业里最容易被忽视的加分点。把攻击后得到的全局模型暴露在 FGSM 和 PGD 生成的标准对抗样本下看它的鲁棒性是否比干净基线的模型更差。如果答案是肯定的就能说明投毒攻击不仅定向破坏了目标类别还削弱了模型整体的对抗鲁棒性——这个结论可以自然延伸到防御研究怎么在不损失主任务性能的前提下同时抵御投毒攻击和输入扰动攻击。你甚至可以在此基础上试一下 Krum 聚合或余弦相似度过滤看看异常检测能在多大程度上恢复模型性能。我自己做完这套实验的一个习惯是在代码里把随机种子固定到 np.random.seed(42) 和 torch.manual_seed(42)把每一轮每个客户端的本地 loss 都落盘成 CSV。联邦学习攻击的复现性本来就差稍有不慎实验结果就变玄学了。固定种子后拿故意设置坏的参数跑一次你就能看到那条让人头大的准确率曲线到底是什么导致的——多数情况下是参数联动而非代码 bug。希望这个排查习惯能帮到你。本文还有配套的精品资源点击获取
返回列表