简介面向计算机相关专业在校学生与科研人员的联邦学习模型对抗攻击Python源码资源集完整代码、逐行中文注释与训练完成的模型权重于一体既适合课程大作业、毕业设计也可作为联邦学习隐私安全方向的教学案例或论文实验基础。压缩包内共17个文件包含6个Python脚本、10个模型权重文件pth格式和1份Markdown介绍文档整体大小仅622KB轻量易部署方便在本地快速复现实验并对照学习。目前已有483人学习下载代码经测试可顺利运行覆盖联邦学习基础函数、攻击函数、对抗成员推断MIA等关键模块注释详细到可直接理解每一步的设计意图能够帮助读者厘清联邦场景下的隐私攻击链路与防御思路。借助压缩包内预训练权重读者无需从零训练即可直接验证攻击效果也可基于源码进行二次开发适配课程报告演示、毕业设计功能扩展或后续科研对比实验。1. 课程大作业里的联邦学习对抗攻击到底要你交出什么拿到这个压缩包的场景通常不是自己在 GitHub 上淘开源项目而是课程大作业的题目直接给到了“基于联邦学习模型的对抗攻击”。答辩现场老师大概率会问两个问题你攻击的是什么阶段的模型以及你的攻击为什么有效。这份 zip 里的 Python 源码、详细注释和模型文件就是把“联邦训练 对抗攻击”这条链路按课程作业的粒度打包好——从数据切分、本地训练、参数聚合到构造对抗样本、评估攻击效果每一段都有代码和注释对应。要读明白它最好先建立两个共识联邦学习解决的是“数据不出本地怎么协同训练”的问题对抗攻击解决的是“模型在恶意输入面前有多脆弱”的问题。两者叠加后攻击面比传统集中式训练更大——既能攻击推理阶段的全局模型也能在训练阶段污染某个客户端的更新。这篇笔记按“先立原理、再拆源码、最后排错”的顺序写照着走一遍你能复现攻击前后的准确率对比也清楚参数怎么调、坑在哪儿。2. 联邦学习与对抗攻击的原理四个攻击面与算法选型2.1 联邦学习的核心机制参数聚合、数据不出本地与三种分类联邦学习的核心诉求是“数据不动模型动”。杨强在《联邦学习》里给出的定义大致可以概括为多个参与方在不共享原始数据的前提下协同训练一个全局模型。服务端负责聚合各客户端上传的模型更新客户端负责用自己的本地数据训练。整个过程中原始数据始终留在产生它的设备或机构里出去的是参数而不是样本。课程作业里最常见的实现是横向联邦学习——各客户端的数据特征维度相同但样本来自不同用户或设备。联邦学习按数据切分方式可以分成三类横向联邦学习、纵向联邦学习和联邦迁移学习。横向联邦适合各客户端样本重叠少、特征空间一致的场景比如不同医院的同类检查数据纵向联邦适合样本重叠多、特征互补的场景比如一家银行和一家电商对同一批用户建模联邦迁移学习则用于前两种都不成立的情况。对课程作业来说绝大多数选题都落在横向联邦上因为 MNIST、CIFAR-10 这类公开数据集天然可以按用户或者按样本随机切给多个客户端。聚合算法里最常用的是 FedAvg。它的思想很简单每个客户端先在本地上跑若干个 epoch然后把更新后的模型参数发给服务端服务端按各客户端样本量占总样本量的比例做加权平均得到新的全局模型。公式可以写作w_global Σ (n_k / n) * w_k其中n_k是第 k 个客户端的样本数n是所有参与客户端的总样本数。加权是为了防止样本量小的客户端对全局模型产生过大影响。下面是最小可用的 FedAvg 聚合代码不依赖任何联邦学习框架只靠 PyTorch 的 state_dict 就能跑通import copy import torch def fed_avg(global_model, client_models, client_sizes): # client_models: 每个客户端本地训练后的模型列表 # client_sizes: 每个客户端本地的样本数量用于加权 global_params global_model.state_dict() total_size sum(client_sizes) for key in global_params: # 按样本量加权求和等价于 FedAvg 的参数聚合公式 global_params[key] sum( client_models[i].state_dict()[key] * client_sizes[i] / total_size for i in range(len(client_models)) ) global_model.load_state_dict(global_params) return global_model这段代码里最关键的是state_dict()的逐层加权操作。PyTorch 的 state_dict 是一个保存了所有参数张量的字典这里对每一层参数都做一次加权求和。注意client_models[i].state_dict()[key]返回的是张量乘一个浮点数权重是合法的但要求所有客户端的模型结构完全一致否则 key 对不上。课程作业里如果直接用torch.load取出的是优化器状态或者包含额外字段的字典也会在这里报错。对入门阶段来说建议先不要引入 Flower、FedML 这类联邦学习框架直接用copy.deepcopy复制全局模型分发给客户端训练完再收回来聚合。这样整个流程完全透明出问题能一眼定位在哪里。等把 FedAvg 跑通了再换框架做更复杂的实验也不迟。2.2 联邦学习里对抗攻击的四个攻击面对抗攻击的本质是在输入上加一个肉眼几乎不可见的扰动 δ让模型以高置信度输出错误结果。白盒攻击要求攻击者能拿到模型参数和梯度黑盒攻击只能通过查询接口反复试探。经典方法里FGSM 是白盒攻击的起点公式写成x_adv x epsilon * sign(grad_x Loss)。联邦学习的出现把“攻击点”从一个变成了多个。按攻击发生的位置和作用对象可以分成四个攻击面攻击面攻击者掌握的资源常见实现方式作业里的上手难度数据投毒客户端本地数据篡改训练样本标签或注入触发器低对抗训练污染客户端本地训练过程用对抗样本替换干净样本做本地训练低模型投毒 / 后门客户端上传的模型更新构造恶意梯度或直接篡改权重再上传中推理期攻击全局模型白盒或黑盒接口FGSM / PGD 生成对抗样本测试鲁棒性低数据投毒和对抗训练污染都发生在客户端本地攻击者只需要控制一个或几个客户端的数据生成过程。模型投毒则更加隐蔽——恶意客户端上传的不是自己真实训练的结果而是精心构造的更新方向这个方向会让全局模型在特定输入上表现异常但在正常输入上几乎不降精度。推理期攻击是最直观的它不碰训练过程只在拿到全局模型后对测试输入加扰动观察模型是否翻车。课程作业里最容易出效果的是推理期攻击和数据投毒因为不需要设计复杂的恶意梯度。联邦学习还有一个特性会让攻击问题更复杂客户端漂移。每个客户端本地数据分布不同多轮本地训练后局部模型会偏离全局最优这种现象在 Non-IID 数据下尤其明显和灾难性遗忘的表现类似——客户端在本地数据上学习新知识时会遗忘掉全局模型已有的泛化能力。这一点在调试攻击时会经常出现比如攻击效果忽好忽坏不一定是攻击代码的问题而是全局模型本身就没收敛稳定。2.3 算法选型为什么课程作业普遍从 FGSM 和 PGD 起步攻击算法有很多但课程作业的时间通常不允许你从头实现 CW 这种需要二次梯度求解的方法。FGSM 只需要一次前向、一次反向代码量不到十行PGD 是 FGSM 的迭代版本每步走一个小步长再加投影裁剪效果通常更好但计算量也成倍增加。两者都只需要模型输出的梯度属于最容易复现的白盒攻击。算法是否迭代扰动上限控制计算成本攻击强度FGSM单步epsilon 直接控制极低弱PGD多步迭代每步投影回 epsilon 球中强CW优化目标损失函数约束高很强FGSM 的价值不在于强度而在于把“梯度方向就是让损失上升最快的方向”这件事讲清楚了。PGD 的价值在于用迭代逼近真正的最大损失点对抗性更强。课程作业里我一般建议先跑通 FGSM确认全链路没有梯度断链的问题再换成 PGD 调参。如果老师要求对比实验把 FGSM、PGD 在不同 epsilon 下的攻击准确率画一条曲线就足够撑起一个章节的论述了。选择 FGSM 和 PGD 还有一层现实考虑PyTorch 对自动求导的支持让这两类攻击的实现非常自然不需要像 CW 那样手动控制优化器的学习率和迭代终止条件。后者一旦参数没调好攻击成功率可能还不如 FGSM而且排查起来很费时间。课程作业追求的是稳定复现不是炫技。3. 压缩包源码怎么读目录结构、模型定义与训练主循环3.1 解压后的典型文件结构先看 README 和 requirements.txt拿到 zip 后不要急着双击跑train.py第一步是解压后先看目录结构。课程大作业的代码包通常不会太臃肿但一定会包含训练入口、攻击入口、模型保存文件和说明文档。常见的目录结构长这样project/ ├── README.md # 作业说明与运行步骤 ├── requirements.txt # python 依赖清单 ├── model.py # 模型结构定义 ├── data.py # 数据集加载与 Non-IID 切分 ├── federated.py # 联邦训练主循环 ├── attack.py # FGSM / PGD 攻击实现 ├── eval.py # 评估脚本输出准确率与攻击指标 └── checkpoint/ └── global_model.pth # 预训练好的全局模型权重你拿到的包文件名可能有差异但关键内容跑不出这几类模型定义文件、数据处理文件、联邦训练文件、攻击文件、评估文件以及一个 .pth 或 .pt 结尾的权重文件。README 里通常会写运行顺序比如先python federated.py训练再python attack.py攻击最后python eval.py出指标。如果 README 写得太简略按文件名也能推断出流程。环境配置在这类课程作业里经常卡住。我一般建议用 conda 新建一个 Python 3.9 左右的干净环境按 requirements.txt 里列的依赖安装。如果里面写的是torch和torchvision直接pip install torch torchvision装 CPU 版本就够跑 MNIST 和 CIFAR-10 了不需要配 CUDA。用 VSCode 打开项目后记得把解释器切到刚才建好的 conda 环境否则运行脚本时会出现明明装了 numpy 却 import 不到的诡异问题。提示如果环境中 torch 版本和压缩包里的权重文件保存时的版本相差过大后面 load_state_dict 时可能报 key 不匹配或参数形状错误。先看 requirements.txt没有就装 torch 2.x 最新稳定版大多数课程作业的模型都不会依赖特定小版本。3.2 模型定义与数据集切分用 MNIST 还是 CIFAR-10模型文件是整个代码包的地基。课程作业里的联邦学习场景模型通常不会太大两层卷积加全连接就能满足需求。过大的模型在本地训练时会拖慢每一轮的迭代速度而且在小规模数据集上容易过拟合。下面是一个典型的课程作业模型定义import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes10): super().__init__() # 输入 MNIST 单通道 28x28 图像 self.features nn.Sequential( nn.Conv2d(1, 16, kernel_size5, padding2), # 输出 16x28x28 nn.ReLU(), nn.MaxPool2d(2), # 输出 16x14x14 nn.Conv2d(16, 32, kernel_size5, padding2), # 输出 32x14x14 nn.ReLU(), nn.MaxPool2d(2), # 输出 32x7x7 ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(32 * 7 * 7, 128), nn.ReLU(), nn.Linear(128, num_classes), ) def forward(self, x): return self.classifier(self.features(x))这个模型的输入只有单通道直接对应 MNIST。如果压缩包里给的模型是这种结构那你需要把数据集也切到 MNIST 上否则输入形状对不上。CIFAR-10 是三通道 32x32 图像用这个模型会直接报维度错误。换数据集时把第一层卷积的输入通道从 1 改成 3并在forward之前确认图像已经归一化到[0,1]范围。联邦学习的数据集切分是个容易被忽略但影响很大的环节。课程作业常用的切分方式有两种IID把数据随机打乱后平均分给各客户端Non-IID让每个客户端只持有部分类别的样本。后者更贴近真实场景也是联邦学习论文里重点研究的情况。用 Dirichlet 分布可以很方便地生成 Non-IID 分布alpha 控制偏斜程度import numpy as np from torch.utils.data import Subset def split_non_iid(dataset, num_clients, alpha0.5): # 用 Dirichlet 分布把样本按类别比例分给各客户端 # alpha 越小每个客户端持有的类别分布越偏斜 labels np.array([dataset[i][1] for i in range(len(dataset))]) client_indices [[] for _ in range(num_clients)] for class_id in np.unique(labels): idx np.where(labels class_id)[0] np.random.shuffle(idx) # 为该类生成 num_clients 个比例总和为 1 proportions np.random.dirichlet([alpha] * num_clients) assigned (len(idx) * proportions).astype(int) # 修正最后一个客户端保证该类样本全部分完 assigned[-1] len(idx) - assigned[:-1].sum() cursor 0 for c in range(num_clients): client_indices[c].extend(idx[cursor:cursor assigned[c]]) cursor assigned[c] return [Subset(dataset, indices) for indices in client_indices]这段代码按类别逐个分配样本alpha0.5时每个客户端会同时持有大部分类别的少量样本alpha0.01时每个客户端几乎只持有某一类样本。课程作业里如果只想快速跑通用alpha1.0或直接随机切分就行。想对比 IID 和 Non-IID 下的攻击效果差异就把这份代码多跑几组不同 alpha 的值。3.3 联邦训练主循环客户端采样、本地更新与聚合编排federated.py 是整个训练的核心。课程作业里最常见的做法是每轮从所有客户端里随机抽一部分参与训练抽中的客户端在本地训练若干 epoch然后上传模型更新。下面是完整的最小主循环import copy import numpy as np import torch def train_local(model, dataset, local_epochs2, batch_size32, lr0.01): loader torch.utils.data.DataLoader(dataset, batch_sizebatch_size, shuffleTrue) optimizer torch.optim.SGD(model.parameters(), lrlr) model.train() for _ in range(local_epochs): for x, y in loader: optimizer.zero_grad() loss torch.nn.functional.cross_entropy(model(x), y) loss.backward() optimizer.step() return model def federated_train(global_model, client_datasets, rounds10, client_fraction0.5, local_epochs2, batch_size32, lr0.01): for r in range(rounds): # 每轮随机选一批客户端参与训练 num_selected max(1, int(client_fraction * len(client_datasets))) selected np.random.choice(len(client_datasets), num_selected, replaceFalse) updates [] sizes [] for cid in selected: local_model copy.deepcopy(global_model) train_local(local_model, client_datasets[cid], local_epochs, batch_size, lr) updates.append(local_model) sizes.append(len(client_datasets[cid])) global_model fed_avg(global_model, updates, sizes) print(fround {r1} done, selected clients: {selected.tolist()}) return global_model这里有一个容易踩的细节copy.deepcopy(global_model)必须在本地训练前执行否则每个客户端拿到的都是同一个对象引用改一个模型等于同时改所有模型。另一个细节是fed_avg内部的加权求和不能原地修改全局模型的参数要先把所有客户端的贡献算完再load_state_dict。参数的选择直接影响训练是否收敛。rounds是联邦通信轮数课程作业里 10 到 20 轮足够看到效果client_fraction是每轮参与训练的客户端比例0.5 表示一半客户端在每一轮被抽中local_epochs是本地训练轮数不要设太大一般 2 到 5 轮太多会导致客户端漂移加重学习率用 SGD 的 0.01 起步Adam 可以试试 0.001。训练结束后把全局模型保存下来zip 里的模型文件就是这么来的。torch.save(global_model.state_dict(), checkpoint/global_model.pth)加载对应文件的代码是global_model.load_state_dict(torch.load(checkpoint/global_model.pth))。如果报missing keys或unexpected keys多半不是模型结构不匹配就是权重是在 DataParallel 模式下保存的这个在第 5 章坑四里会给出具体解法。4. 对抗攻击代码逐行拆解FGSM、PGD 与三个插入点4.1 FGSM用 sign() 把梯度变成扰动方向FGSM 的整个思路只有一句话让损失上升最快的方向就是损失对输入梯度的符号方向。对图像来说梯度符号矩阵和原图尺寸完全一致乘以一个很小的epsilon再加回原图就得到对抗样本。代码实现非常短import torch import torch.nn as nn def fgsm_attack(model, x, y, eps0.1): # 输入 x 是需要攻击的原始图像y 是真实标签 # 返回与 x 同形状的对抗样本 x.requires_grad True loss nn.CrossEntropyLoss()(model(x), y) model.zero_grad() loss.backward() # grad.sign() 只保留方向每个元素是 1 或 -1 perturbation eps * x.grad.sign() # 扰动加回原图并裁剪到合法像素范围 adv_x torch.clamp(x perturbation, 0.0, 1.0) return adv_x.detach()这里有三处关键点。第一x.requires_grad True必须在计算损失之前设置否则x.grad会是 None后面调用.sign()直接报错。第二model(x)时模型必须在能计算梯度的模式下如果外层包了torch.no_grad()梯度同样拿不到。第三x.grad.sign()取的是梯度符号而不是梯度本身符号矩阵的 L∞ 范数为 1所以乘上eps后扰动范数恰好被限制在eps内。eps的取值和数据集密切相关。MNIST 图像本身就落在[0,1]eps0.1已经能产生肉眼可见但不算突兀的噪声eps0.3时噪声明显但攻击成功率会大幅提升。CIFAR-10 上因为图像内容更复杂同样eps0.1的效果会比 MNIST 弱不少课程作业里可以按0.05、0.1、0.2、0.3拉一组曲线看准确率的下降趋势。4.2 PGD在 epsilon 球内迭代投影是关键PGD 是 FGSM 的迭代版本每一步沿梯度符号方向前进一小步alpha并且每一步结束后把结果投影回以原始样本为中心、半径为eps的邻域内。这个投影操作是 PGD 和 FGSM 最本质的区别没有投影的迭代会飞出边界得到的“对抗样本”已经不属于 epsilon 约束下的攻击范畴。def pgd_attack(model, x, y, eps0.1, alpha0.01, iters20): # 保留原始样本用于投影约束 orig_x x.clone().detach() # 对抗样本初始化为原始样本并开启梯度追踪 adv_x x.clone().detach().requires_grad_(True) for _ in range(iters): loss nn.CrossEntropyLoss()(model(adv_x), y) model.zero_grad() loss.backward() # 沿梯度上升方向走 alpha 步长 adv_x adv_x alpha * adv_x.grad.sign() # 第一步投影把扰动限制在 eps 邻域内 adv_x torch.clamp(adv_x, orig_x - eps, orig_x eps) # 第二步投影保证像素值仍落在 [0,1] 合法范围 adv_x torch.clamp(adv_x, 0.0, 1.0) # 每次更新后都需要重新开启梯度 adv_x adv_x.detach().requires_grad_(True) return adv_x.detach()alpha和iters需要配合调整。常见做法是让alpha远小于eps比如eps0.1时用alpha0.01、iters20这样能保证每次更新都落在合理范围内。如果alpha太大迭代一次就飞出邻域再被投影拉回相当于做了无用的震荡如果iters太少可能还没逼近最大损失点攻击就结束了。经验上alphaeps/iters*2是一个不错的起点比如eps0.1、iters10时取alpha0.02。需要注意每次循环末尾的.detach().requires_grad_(True)。PyTorch 中通过张量运算得到的新adv_x在下一轮反向传播时如果沿用旧的梯度图会累积计算图内存不断上涨几十轮后会卡死甚至显存溢出。每次迭代后把旧的图断开重新开启梯度是写攻击循环的标准习惯。4.3 把攻击代码插进联邦学习流程三个可行的插入点攻击代码本身不复杂难的是把它放在联邦学习流程的正确位置。课程作业里可以选三个插入点效果和说理难度都不一样。第一个插入点是推理期攻击。全局模型训练完成后把测试集输入用 FGSM 或 PGD 攻击再计算攻击后的模型准确率对比干净准确率即可。这个插入点最容易出结果也最好向老师解释。评估代码可以写成一个通用的函数def evaluate_attack(model, loader, attack_fn, **attack_kwargs): model.eval() orig_correct 0 adv_correct 0 total 0 for x, y in loader: # 先对当前批次生成对抗样本内部会做梯度计算 x_adv attack_fn(model, x, y, **attack_kwargs) with torch.no_grad(): total y.size(0) # 干净样本的正确数 orig_correct (model(x).argmax(1) y).sum().item() # 对抗样本的正确数 adv_correct (model(x_adv).argmax(1) y).sum().item() return orig_correct / total, adv_correct / total注意这里attack_fn(model, x, y, **attack_kwargs)接收的是model、x、y三个位置参数调用时写evaluate_attack(model, loader, fgsm_attack, eps0.1)即可。**attack_kwargs会把eps传递进去这样换攻击方法时不需要改这个评估函数。model(x)的计算放在torch.no_grad()里避免生成对抗样本时累积的计算图影响后续前向推理。第二个插入点是数据投毒修改客户端本地训练过程。具体做法是在train_local的训练循环里每批次构造一部分对抗样本混入干净样本再喂给模型。这样训练出的模型学到的决策边界会被对抗样本带偏最终聚合后的全局模型对同类扰动也变得更敏感。实现上只需把本地训练函数里的model(x)改成先调用攻击函数代码如下def train_local_with_poison(model, dataset, poison_ratio0.3, eps0.1): loader torch.utils.data.DataLoader(dataset, batch_size32, shuffleTrue) optimizer torch.optim.SGD(model.parameters(), lr0.01) for x, y in loader: # 取批次前 30% 的样本替换为对抗样本 n_poison int(len(x) * poison_ratio) if n_poison 0: x[:n_poison] fgsm_attack(model, x[:n_poison], y[:n_poison], epseps) optimizer.zero_grad() loss torch.nn.functional.cross_entropy(model(x), y) loss.backward() optimizer.step()第三个插入点是模型投毒恶意客户端在本地训练后不上传真实权重而是直接篡改参数再上传。比数据投毒更隐蔽因为服务端看不到客户端本地数据只能检查上传的参数。最简单的模型投毒是把本地更新放大后叠加到全局模型上课程作业里可以这样演示def poison_update(global_model, local_model, scale10.0): # 计算本地模型相对全局模型的更新方向 # 放大后写入 local_model模拟恶意客户端上传被篡改的更新 gd global_model.state_dict() ld local_model.state_dict() for key in gd: delta ld[key] - gd[key] ld[key] gd[key] scale * delta local_model.load_state_dict(ld) return local_model模型投毒的攻击效果在课程作业里不容易一次性做出来因为恶意更新的方向可能被其他正常客户端的更新平均掉。想做对比实验可以把恶意客户端占比调高比如 5 个客户端里 2 个恶意或者直接让所有客户端都变成恶意客户端演示聚合算法在没有防御机制时的脆弱性。这种写法并不复杂但能体现对联邦学习安全威胁的理解深度。5. 从报错到“攻击无效”课程作业里最常踩的 5 个坑5.1 坑一生成的扰动全是 0对抗样本和原始样本一模一样现象打印对抗样本和原始样本的最大差异结果是 0.0模型准确率纹丝不动。原因最常见的是model处于torch.no_grad()上下文或eval()模式时输入张量的梯度没有被正确追踪。另一个隐蔽原因是攻击函数内部先对x做了detach()导致梯度信息被切断。还有的同学在loss model(x)前把x传入x.requires_grad_()后又用x.data构造了新张量梯度同样被截断。解决在攻击函数开头打印一句话print(x.requires_grad, x.grad is None)当面确认确保调用攻击函数时没有with torch.no_grad()包在外面不要对x做任何.detach()操作后再传进攻击函数。FGSM 里x.requires_grad True放在最前面并且后续所有操作都用x本身而不是x.data。5.2 坑二攻击后准确率几乎不掉演示效果翻车现象eps0.1跑完MNIST 测试集准确率从 99% 只降到 97%海报上画不出明显对比曲线。原因eps对 MNIST 而言可能偏小另一种更隐蔽的情况是图像已被归一化到[-1,1]范围但代码里torch.clamp(x, 0, 1)把对抗样本强行压回了[0,1]等于把一半的扰动截断了攻击效果无形中损失一大半。解决先确认数据集归一化范围。如果是[-1,1]把 clamp 范围改成[-1,1]。然后从eps0.3开始调确认准确率显著下降后再往回拉。调试阶段可以打印扰动张量的均值如果扰动均值小于eps/10基本可以断定归一化范围处理错了。5.3 坑三Non-IID 数据分布下攻击效果一会有、一会无现象同样一套代码跑第一次攻击成功率 60%跑第二次变成 92%再跑一次又跌回 55%。不只攻击效果如此干净模型准确率也在波动。原因Non-IID 切分导致各客户端本地数据分布差异大FedAvg 聚合后的全局模型在部分类别上欠拟合攻击者碰巧选到了薄弱类别时效果就强。另外本地训练轮数设太多会加剧客户端漂移模型本身就没收敛攻击效果自然不稳定。解决固定随机种子numpy.random.seed(0)、torch.manual_seed(0)都加上保证每次实验可复现。把local_epochs降到 2rounds加到 20 以上让全局模型先充分收敛再攻击。评估时对三个不同随机种子各跑一遍取平均值至少做三次看着才不算玄学。5.4 坑四模型权重加载报错size mismatch 或 key 对不上现象load_state_dict报size mismatch或者missing keys、unexpected keys模型结构明明没改就是加载不进去。原因一种情况是压缩包里的模型是DataParallel模式下保存的参数字典里的 key 都带module.前缀而当前代码用的是裸模型key 对不上另一种情况是训练时用不同 torch 版本保存参数形状有细微变化。解决加载时先打印state_dict的 key看到module.前缀就手动去掉再 loadingraw torch.load(checkpoint/global_model.pth) if any(k.startswith(module.) for k in raw): raw {k.replace(module., ): v for k, v in raw.items()} global_model.load_state_dict(raw, strictFalse)strictFalse可以绕过部分 key 不匹配但如果模型结构本身不一致仍然会报错。保险的办法是自己先训练 1 轮覆盖默认初始化把随机权重保存一遍再对比 key 的差异。5.5 坑五恶意客户端一直没被选中攻击没机会上场现象训练跑完了但攻击准确率和干净准确率完全一样因为后台检查活动日志发现恶意客户端一次都没被抽中。原因数据投毒和模型投毒都要求恶意客户端实际参与训练轮次。client_fraction0.2、总共 10 个客户端时每轮只抽 2 个恶意客户端连续 10 轮不被选中的概率并不低。解决最简单的做法是在federated_train里改成每轮强制包含一个恶意客户端或者直接把client_fraction调到 1.0让所有客户端每轮都参与。课程作业里不需要模拟真实系统的随机性优先保证攻击效果能展示出来。真要做随机也要在实验配置里写明恶意客户端参与频率否则答辩被问“为什么你的后门没生效”会很尴尬。6. 攻击效果评估三个指标和一张答辩表格课程作业的交付物除了源码更重要的是结果能不能被量化。答辩老师不关心你写了几百行代码关心的是“攻击有没有效果、效果有多强”。评估攻击可以用三个指标串起来。第一个指标是干净准确率与攻击后准确率的差值习惯上叫“掉点”。干净准确率 99%攻击后 30%掉点 69 个百分点这个数字是最直观的展示。第二个指标是扰动幅度用 L2 范数或 L∞ 范数计算作用是证明“我确实只加了很小的噪声”。第三个指标是攻击成功率对非目标攻击来说就是对抗样本中被预测为任意错误标签的样本占比对后门攻击来说是包含触发器样本被预测为目标标签的比例。课程作业做到前两个指标就基本合格第三个指标用于展示对目标攻击的理解。下面代码可以同时输出这三个指标def report_attack(model, loader, attack_fn, eps0.1): model.eval() orig_correct, adv_correct, total 0, 0, 0 perturb_norm 0.0 for x, y in loader: x_adv attack_fn(model, x, y, epseps) # 计算扰动 L2 范数除以样本数得到平均扰动幅度 perturb_norm (x_adv - x).pow(2).sum(dim(1, 2, 3)).sqrt().sum().item() with torch.no_grad(): total y.size(0) orig_correct (model(x).argmax(1) y).sum().item() adv_correct (model(x_adv).argmax(1) y).sum().item() clean_acc orig_correct / total adv_acc adv_correct / total asr 1.0 - adv_acc avg_l2 perturb_norm / total return {clean_acc: clean_acc, adv_acc: adv_acc, drop: clean_acc - adv_acc, asr: asr, avg_l2: avg_l2}展示结果时推荐做一张不同eps下的对比表格横向是攻击方法纵向是eps0.05、0.1、0.2、0.3单元格里填攻击后准确率或掉点幅度。答辩时按表格讲述再附上一行对抗样本的可视化对比图就足够说明问题了。如果你想做进一步延伸可以对比同一个攻击在 IID 和 Non-IID 两种联邦场景下的效果差异或者对比联邦模型和集中训练模型的鲁棒性这两组对比在论文里都是加分项。我自己的习惯是每次改完参数先跑一组eps0.1的基线记录在一个固定的输出文件里再改代码。这样无论怎么调参最后都能回看攻击效果是变好还是变差。课程作业最怕的不是实现不了而是实验记录丢了答辩时说不清楚某张图是怎么出来的。希望这个工作流对你也有用祝你的大作业一次跑通。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
苏宁广告实战:从零搭建完整示例,解决搭项目难题 苏宁广告实战:从零搭建完整示例,解决搭项目难题 刚学完语法,对着空白的 IDE 发呆?别慌,这比写代码本身更让人头疼。很多新手卡在“知道怎么写”到“能跑起来”之间的鸿沟,缺的不是知识点,而是一个能照着做的完整示例。今天我们就拿“苏宁广告”这… · 2026/9/23 12:40:21
遥感目标检测实战:YOLOv5小目标识别全流程调参与部署避坑 简介:面向计算机视觉与遥感应用方向的YOLOv5目标识别项目资源包,适合高校学生、教师及企业研发人员用于毕业设计、课程实践或初期项目展示。资源完整收录基于YOLOv5的遥感影像目标检测代码、模型权重、配置文件及说明文档,代码均通过运行验证… · 2026/9/23 12:40:15
基于网易新闻评论的舆情热点分析平台:从爬虫到可视化完整实践 简介:一份基于网易新闻与评论的舆情热点分析平台完整工程包,面向Python课程设计、毕业设计及数据科学初学者,旨在解决从舆论数据采集、清洗、情感判断到热点趋势可视化的全流程实践问题。包体共1403个文件,以JS、CSS、HTML等前端资… · 2026/9/23 12:40:14
C++ MFC五子棋人机对战:从课程设计到可运行桌面程序 简介:这是一份面向高校C课程学习者与Windows桌面开发入门者的期末大作业参考方案,围绕MFC框架实现人机对战五子棋,帮助读者理解面向对象设计、界面开发与博弈算法的结合方式。压缩包共36个文件,约160KB,以cpp与h源码为… · 2026/9/23 14:07:18
上市公司新闻文本分类:从数据清洗到TF-IDF模型实战 简介:这份源码面向具备一定Python基础的金融数据分析学习者与量化研究者,提供一套完整的上市公司新闻文本分析与分类预测方案,解决财经新闻自动抓取、特征提取与模型分类的实践问题。资源包共21个文件,以17个Python源代码文件为核… · 2026/9/23 14:07:18
爆客商圈源码解析:微信私域运营后台技术实现指南 简介:本资源为基于HTML5技术开发的商业社交类轻应用「爆客商圈」v1.1.24完整源码包,面向前端开发者、H5跨平台项目实践者及中小商家数字化工具学习者,适用于快速搭建本地化商圈服务平台或二次开发定制化营销功能。压缩包共51个文件࿰… · 2026/9/23 14:07:12
奶牛新手避坑指南:版本升级API全变后的生存法则 奶牛新手避坑指南:版本升级API全变后的生存法则 版本升级后 API 全变了,代码跑不通,文档对不上,这才是开发最崩溃的时刻。这份奶牛新手避坑指南,专门拆解升级后的核心陷阱。别急着骂娘,看完这篇,你的报错能少一半。… · 2026/9/23 14:07:05
租房如何提取公积金全流程解析:3步避坑指南 租房如何提取公积金全流程解析:3步避坑指南 官方文档那几万字看得人头皮发麻,关键条款还藏在附录里,新手根本抓不住重点。别慌,这篇避坑指南直接给你划重点,把租房提取公积金的底层逻辑和实操细节拆解得明明白白。很多人卡在材料不全或流程走错上,白白… · 2026/9/23 14:07:05
VS2019 C# 串口助手开发:从能收到字节到稳定运行 简介:这份资源面向具备一定C#基础、希望入门串口通信开发的程序员与嵌入式爱好者,提供一套基于VS2019的串口助手完整工程源码。内容围绕System.IO.Ports命名空间下的SerialPort类展开,涵盖串口打开关闭、波特率与校验位设置、DataReceived事件… · 2026/9/23 14:06:59
3招搞定手机怎么下载微信面试难题实战项目解析 3招搞定手机怎么下载微信面试难题实战项目解析 面试被问“手机怎么下载微信”背后的原理,90%的人答不上来。别笑,这看似弱智的问题,实则是考察你对移动应用分发机制、安全校验及网络协议理解的试金石。我带过不少校招新人,他们背了八股文,却连一个A… · 2026/9/23 0:00:03
你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 面试被问“高并发下如何保证消息不丢失”,你张口就是“用Redis”,结果面试官追问“如果Redis宕机了怎么办”,你瞬间卡壳。这种场景太常见了,很多新手在背八股文时,只记住了技术名词… · 2026/9/23 0:00:29