尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

联邦学习攻击与防御复现实战:从Label-Flipping到Krum闭环验证

联邦学习攻击与防御复现实战:从Label-Flipping到Krum闭环验证 简介本资源是一份面向计算机及相关专业本科生的联邦学习安全方向毕业设计实践包聚焦于论文级攻击防御方案的代码复现与工程落地适用于毕设、课程设计、科研入门及AI安全方向自学。压缩包共184个文件含109个Python源码涵盖FL训练框架、后门攻击注入、鲁棒聚合算法等核心模块、14个YAML配置文件定义数据集划分、模型结构与攻击参数、12个Shell脚本支持一键环境部署与实验启动以及5份Markdown文档含详细运行说明、答辩要点与扩展建议整体体积仅391KB轻量易部署。已有171人下载学习资源经作者实测全部可运行答辩平均分96分附带清晰目录结构与README引导支持小白快速上手也便于进阶者基于现有代码修改适配新攻击场景或防御策略。1. 联邦学习攻击预防不是加个“安全层”就完事毕业设计里真正要复现的是攻击者怎么绕过聚合、客户端如何被毒化、以及防御代码跑起来后指标为什么反而掉——这三件事必须闭环验证你手里的毕业设计标题写着“联邦学习攻击预防与论文代码复现”但实际打开仓库发现README里只有pip install -r requirements.txt和一句“运行main.py即可”训练日志里acc从92%掉到63%却没报错测试集准确率波动像心电图而导师问“你复现的是哪篇论文的哪个攻击防御模块插在Aggregator还是Client侧消融实验对比了baseline吗”——你卡住了。这不是Python环境配不配得上的问题而是联邦学习的攻击与防御天然嵌套在系统级交互中模型更新被篡改、梯度被投毒、客户端被伪造、聚合规则被利用……这些动作不发生在单机训练循环里而藏在client上传→server校验→aggregation→下发的四步链路上。本篇不讲“联邦学习是什么”只聚焦你正在跑、跑不通、跑出来结果不对的那个复现任务用Python复现经典攻击如label-flipping、model poisoning与对应防御如Krum、RFA、Norm Clipping并确保每一步都能观测、可调试、能解释下降原因。适合已跑通FedAvg baseline、正卡在“防御后精度崩塌”或“攻击没生效”的本科生与硕士生。2. 复现前必须厘清的三道生死线攻击类型、防御位置、评估协议——选错任意一项代码再全也是无效劳动联邦学习攻击预防的复现本质是在特定威胁模型下验证某防御机制对某类攻击的有效性。跳过威胁建模直接写代码等于在没画靶心的情况下开枪。下面三条线是你启动复现前必须亲手划清的边界。2.1 攻击类型决定代码结构Label-Flipping和Model Poisoning根本不是同一层的事Label-Flipping标签翻转发生在数据层客户端本地训练时把猫的图片标成狗再上传被污染的梯度。它不修改模型参数只污染训练信号。复现时需在client端train()函数中插入标签扰动逻辑# client.py 中 train() 函数片段 def train(self, model, dataloader, epochs1): model.train() optimizer torch.optim.SGD(model.parameters(), lr0.01) for epoch in range(epochs): for x, y in dataloader: # 【关键插入点】仅对恶意客户端执行标签翻转 if self.is_malicious: # 需提前设置 malicious_client_ids y (y 1) % 10 # 假设10分类将标签循环1 x, y x.to(self.device), y.to(self.device) optimizer.zero_grad() loss F.cross_entropy(model(x), y) loss.backward() optimizer.step() return model.state_dict() # 返回污染后的state_dict注意这段代码必须绑定到具体客户端ID不能全局生效self.is_malicious需在初始化client时传入而非运行时随机判定——否则无法复现论文中“20%客户端被攻陷”的设定。Model Poisoning模型投毒则发生在参数层客户端上传前直接篡改state_dict()例如注入后门触发器或放大特定层梯度。复现时需在upload()环节拦截# client.py 中 upload() 函数 def upload(self, model_state_dict): if self.is_malicious: # 【典型投毒操作】放大最后一层bias制造类别偏移 if fc2.bias in model_state_dict: model_state_dict[fc2.bias] * 5.0 # 放大5倍 # 或注入高斯噪声破坏收敛性 for k, v in model_state_dict.items(): if weight in k: noise torch.randn_like(v) * 0.1 model_state_dict[k] v noise return model_state_dict逻辑说明Label-Flipping影响的是梯度方向Model Poisoning直接影响聚合输入值。前者需配合数据集如CIFAR-10的label映射表操作后者直接操作tensor——二者调试方式、观测指标loss曲线 vs 参数范数分布完全不同。你复现的论文若未明确攻击类型立刻查原文Methodology章节的Threat Model小节别猜。2.2 防御位置决定代码挂载点Server端聚合防御和Client端鲁棒训练不可混用几乎所有毕业设计代码库都把防御写在server.py里这是对的——因为Krum、RFA、Bulyan等主流防御都在server端对收到的N个client update做筛选或加权。但必须确认你复现的论文是否真在server端防御。例如Krum计算每个client update与其他所有update的欧氏距离平方和选距离和最小的那个update参与聚合RFARobust Federated Averaging对每个参数维度取N个client该维度值的几何中位数geometric medianNorm Clipping对每个client上传的梯度向量做L2范数裁剪超阈值则缩放。它们的共同点是输入是N个state_dict输出是1个聚合后state_dict。代码必须放在server端aggregate()函数内# server.py 中 aggregate() 函数以Krum为例 def aggregate(self, client_updates): # client_updates: List[Dict[str, torch.Tensor]], 长度为N n len(client_updates) scores torch.zeros(n) # 计算每个client update的score距离和 for i in range(n): dist_sum 0 for j in range(n): if i ! j: # 对每个参数key计算欧氏距离平方 dist_sq 0 for k in client_updates[i].keys(): diff client_updates[i][k] - client_updates[j][k] dist_sq torch.sum(diff ** 2).item() dist_sum dist_sq scores[i] dist_sum # 选score最小的client update作为聚合结果 best_idx torch.argmin(scores).item() return copy.deepcopy(client_updates[best_idx])参数说明scores[i]越小代表第i个client的update越“中心”Krum假设恶意client的update会偏离正常分布。阈值不需设——它天然选1个不是过滤。若你看到代码里有if score threshold:那大概率是作者自己魔改的非标准Krum与原论文不符。而Client端防御如FedProx、SCAFFOLD需修改client本地训练目标函数与server端防御完全隔离。若你的论文同时用了两者如“Server用RFA Client用FedProx”必须拆成两个独立分支复现禁止在一个main.py里硬塞两种逻辑——否则无法归因精度变化是哪部分起效。2.3 评估协议是精度数字的唯一判据没有global test set重测一切acc都是幻觉最致命的复现错误用client本地test set算accuracy当global性能。联邦学习的global accuracy必须在server持有的、独立于所有client的global test set上计算。这个set不能是任何client的test data切分出来的——它必须是全新采集/划分的数据。常见做法是使用CIFAR-10时将原始10000张test image按类别均匀切分为global_test5000张 reserved_for_poisoning5000张reserved部分用于构造后门攻击的trigger样本如贴小方块global_test纯用于最终评估所有client的train/test split仅用于本地训练绝不参与global指标计算。验证代码必须显式加载global test set# evaluate.py def global_evaluate(model, global_test_loader): model.eval() correct, total 0, 0 with torch.no_grad(): for x, y in global_test_loader: # 注意这里用的是global_test_loader不是client的loader x, y x.to(device), y.to(device) pred model(x).argmax(dim1) correct pred.eq(y).sum().item() total len(y) return 100. * correct / total # 在main.py训练循环末尾调用 global_acc global_evaluate(server_model, global_test_loader) print(fGlobal Test Accuracy: {global_acc:.2f}%)血泪经验曾见三个毕业设计代码库global_test_loader实际加载的是某个client的test data导致防御后acc虚高15%——因为那个client恰好数据干净。务必检查global_test_loader.dataset的len()和class distribution确认它独立于所有client dataset。3. 用PyTorchFlower复现FedAvg baseline50行核心代码跑通但必须亲手改这3处才能进攻击防御阶段很多同学卡在第一步连基础FedAvg都跑不起来更别说加攻击和防御。问题不在代码量而在三个隐藏依赖必须手动补全。以下用PyTorch Flower框架轻量、易调试、社区活跃给出最小可行复现路径所有代码均可直接粘贴运行。3.1 安装与环境Flower 1.3 PyTorch 2.0 Python 3.8——版本错一个client注册就失败# 创建干净虚拟环境强烈推荐 python3.8 -m venv fed_env source fed_env/bin/activate # Linux/MacWindows用 fed_env\Scripts\activate.bat # 安装指定版本Flower 1.4对PyTorch 2.1支持有bug锁定1.3.1 pip install torch2.0.1 torchvision0.15.2 pip install flwr1.3.1 # 关键不要pip install flwr最新版 pip install numpy scikit-learn tqdm提示Flower 1.3.1是最后一个稳定支持PyTorch 2.0的版本。若用Python 3.9需降级到PyTorch 1.13否则flwr.client.NumPyClient序列化失败。环境变量PYTHONPATH无需设置Flower自动处理。3.2 Server端50行代码启动但必须重写fit_config和evaluate_fn# server.py import flwr as fl import torch import numpy as np from collections import OrderedDict # 1. 定义全局模型此处用简单CNN与client一致 class Net(torch.nn.Module): def __init__(self): super().__init__() self.conv1 torch.nn.Conv2d(3, 6, 5) self.pool torch.nn.MaxPool2d(2, 2) self.conv2 torch.nn.Conv2d(6, 16, 5) self.fc1 torch.nn.Linear(16 * 5 * 5, 120) self.fc2 torch.nn.Linear(120, 84) self.fc3 torch.nn.Linear(84, 10) def forward(self, x): x self.pool(torch.nn.functional.relu(self.conv1(x))) x self.pool(torch.nn.functional.relu(self.conv2(x))) x torch.flatten(x, 1) x torch.nn.functional.relu(self.fc1(x)) x torch.nn.functional.relu(self.fc2(x)) x self.fc3(x) return x # 2. 初始化全局模型 net Net().to(cpu) # server通常不用GPU params [val.cpu().numpy() for _, val in net.state_dict().items()] # 3. 定义聚合策略FedAvg strategy fl.server.strategy.FedAvg( fraction_fit1.0, # 所有client参与训练 fraction_evaluate0.0, # 不在server端评估我们自己做 min_available_clients2, # 【关键修改1】必须重写fit_config让client知道训练轮次和batch_size on_fit_config_fnlambda server_round: { server_round: server_round, local_epochs: 1, # 每轮client只训1 epoch避免过拟合 batch_size: 32, }, # 【关键修改2】重写evaluate_fn在server端用global test set评估 evaluate_fnglobal_evaluate_fn, # 下方定义 ) # 4. global_evaluate_fn必须加载global test set def global_evaluate_fn(server_round, parameters, config): # 将NumPy参数转回PyTorch state_dict params_dict zip(net.state_dict().keys(), parameters) state_dict OrderedDict({k: torch.tensor(v) for k, v in params_dict}) net.load_state_dict(state_dict, strictTrue) # 加载global test set此处简化实际需从文件读取 from torchvision import datasets, transforms transform transforms.Compose([transforms.ToTensor()]) global_testset datasets.CIFAR10( root./data, trainFalse, downloadTrue, transformtransform ) global_testloader torch.utils.data.DataLoader( global_testset, batch_size100, shuffleFalse ) # 计算global accuracy net.eval() correct, total 0, 0 with torch.no_grad(): for x, y in global_testloader: pred net(x).argmax(dim1) correct pred.eq(y).sum().item() total len(y) accuracy correct / total print(fRound {server_round} Global Accuracy: {accuracy:.4f}) return float(accuracy), {accuracy: accuracy} # 5. 启动server fl.server.start_server( server_address0.0.0.0:8080, configfl.server.ServerConfig(num_rounds10), strategystrategy, )逻辑说明on_fit_config_fn返回的字典会通过gRPC传给每个clientclient据此设置local_epochs和batch_sizeevaluate_fn在每轮结束后被调用它必须重新加载global test set不能复用client的loader且返回(loss, metrics)元组——loss可设为0metrics必须含accuracy键供Flower记录。3.3 Client端继承NumPyClient但必须重写get_parameters/set_parameters# client.py import flwr as fl import torch import numpy as np from torch import nn, optim from torchvision import datasets, transforms class CIFARClient(fl.client.NumPyClient): def __init__(self, cid, is_maliciousFalse): self.cid cid self.is_malicious is_malicious # 【关键修改3】client必须自己加载数据且train/test split独立 transform transforms.Compose([transforms.ToTensor()]) trainset datasets.CIFAR10( root./data, trainTrue, downloadTrue, transformtransform ) # 按cid划分数据假设10个client每个拿10%数据 indices list(range(len(trainset))) np.random.seed(int(cid)) # 确保划分可复现 np.random.shuffle(indices) client_size len(trainset) // 10 client_indices indices[int(cid)*client_size:(int(cid)1)*client_size] self.trainloader torch.utils.data.DataLoader( torch.utils.data.Subset(trainset, client_indices), batch_size32, shuffleTrue ) # test set也独立划分不用于global评估 testset datasets.CIFAR10( root./data, trainFalse, downloadTrue, transformtransform ) self.testloader torch.utils.data.DataLoader( testset, batch_size100, shuffleFalse ) def get_parameters(self, config): # 返回当前模型参数NumPy格式 return [val.cpu().numpy() for _, val in net.state_dict().items()] def fit(self, parameters, config): # 加载server下发的参数 params_dict zip(net.state_dict().keys(), parameters) state_dict OrderedDict({k: torch.tensor(v) for k, v in params_dict}) net.load_state_dict(state_dict, strictTrue) # 本地训练此处插入攻击逻辑 if self.is_malicious: self.poison_labels() # 或 self.poison_model() # 标准训练 net.train() optimizer optim.SGD(net.parameters(), lr0.01) for _ in range(config[local_epochs]): for x, y in self.trainloader: optimizer.zero_grad() loss nn.CrossEntropyLoss()(net(x), y) loss.backward() optimizer.step() # 返回更新后的参数 return self.get_parameters({}), len(self.trainloader.dataset), {} def poison_labels(self): # 示例对trainloader中的batch做label翻转 pass # 实现见2.1节 def poison_model(self): # 示例对state_dict做投毒 pass # 实现见2.1节 def evaluate(self, parameters, config): # client本地评估仅用于debug不计入global指标 params_dict zip(net.state_dict().keys(), parameters) state_dict OrderedDict({k: torch.tensor(v) for k, v in params_dict}) net.load_state_dict(state_dict, strictTrue) net.eval() correct, total 0, 0 with torch.no_grad(): for x, y in self.testloader: pred net(x).argmax(dim1) correct pred.eq(y).sum().item() total len(y) return float(0), len(self.testloader.dataset), {accuracy: correct/total} # 启动client在不同终端运行 fl.client.start_numpy_client( server_addresslocalhost:8080, clientCIFARClient(cid0, is_maliciousFalse), )参数说明cid必须是字符串Flower要求is_malicious控制是否启用攻击poison_labels()和poison_model()留空待填——这就是你接续2.1节的位置。注意fit()返回的第三个值是metrics字典此处为空因global评估由server端evaluate_fn完成。4. 攻击与防御代码落地Krum防御Label-Flipping攻击的完整闭环附3个必调参数与效果验证方法现在你已跑通FedAvg baseline下一步是注入攻击并部署防御。本节以Krum防御对抗Label-Flipping攻击为例给出可直接运行的完整闭环代码并指出三个决定效果的参数——它们不是随便写的而是根据论文公式推导出的实操值。4.1 Label-Flipping攻击实现在client.fit()中精准翻转且只翻训练集标签# client.py 中补充 poison_labels() 方法 def poison_labels(self): # 获取trainloader的dataset直接修改其targetsCIFAR-10 targets是list if hasattr(self.trainloader.dataset, dataset): # Subset情况 base_dataset self.trainloader.dataset.dataset else: base_dataset self.trainloader.dataset # 确认是CIFAR-1010类否则报错 if not hasattr(base_dataset, targets) or len(base_dataset.targets) 0: raise ValueError(Dataset must have targets attribute) # 只翻转指定比例的样本论文常用20% num_samples len(base_dataset.targets) num_flip int(0.2 * num_samples) # 20%翻转率 # 随机选索引固定seed保证可复现 flip_indices np.random.RandomState(42).choice( num_samples, sizenum_flip, replaceFalse ) # 翻转将标签y改为(y1)%10避免翻到自身 for idx in flip_indices: original_label base_dataset.targets[idx] base_dataset.targets[idx] (original_label 1) % 10 print(f[Client {self.cid}] Poisoned {num_flip}/{num_samples} labels)逻辑说明此方法直接修改dataset.targets比在dataloader迭代时动态翻转更可靠——因为后者可能被shuffle打乱导致翻转比例失控。RandomState(42)确保每次运行翻转相同样本方便对比实验。4.2 Krum防御实现server端aggregate()替换为Krum逻辑注意距离计算维度# server.py 中替换 aggregate() 函数替代FedAvg def krum_aggregate(client_updates): n len(client_updates) f 1 # 假设最多1个恶意client论文默认f1 m n - f - 2 # Krum选m个最近的update # 将所有state_dict转为向量便于距离计算 update_vectors [] for update in client_updates: vec [] for k in sorted(update.keys()): vec.append(update[k].flatten()) update_vectors.append(torch.cat(vec)) # 计算每对update的欧氏距离平方 scores torch.zeros(n) for i in range(n): distances [] for j in range(n): if i ! j: dist_sq torch.sum((update_vectors[i] - update_vectors[j]) ** 2) distances.append(dist_sq.item()) # 取最小的m个距离之和 distances.sort() scores[i] sum(distances[:m]) # 选score最小的update best_idx torch.argmin(scores).item() return copy.deepcopy(client_updates[best_idx]) # 在strategy中使用替换FedAvg strategy fl.server.strategy.Strategy( # ... 其他参数同前 # 替换aggregate函数 aggregate_fitlambda server_round, results, failures: ( krum_aggregate([r[1] for r in results]), # results是(client, parameters)元组列表 {} ), )参数说明f是预估恶意client数量必须≤⌊(n-1)/2⌋否则Krum失效mn-f-2是Krum论文公式不可随意修改距离计算用torch.sum((vec_i - vec_j)**2)而非torch.norm()因后者在GPU上可能有精度误差。4.3 效果验证三板斧global acc、client divergence、gradient norm heatmap光看global accuracy数字不够必须交叉验证。以下是三个必做的验证步骤验证项操作方法正常现象异常信号Global Accuracy趋势运行10轮记录每轮global_evaluate_fn返回的accuracyFedAvg baseline92%→88%缓慢下降KrumAttack75%→82%先跌后升Attack未生效Krum曲线与baseline几乎重合Defense失效Krum曲线持续低于baselineClient Update Divergence每轮记录所有client update的L2范数画箱线图正常恶意client范数显著高于良性client投毒放大梯度所有client范数分布重叠 → 攻击未成功注入或防御过度平滑Gradient Norm Heatmap取最后一层fc.weight计算每个client上传的梯度矩阵的Frobenius范数热力图可视化正常恶意client如cid0对应格子颜色最深全图颜色均匀 → 投毒强度不足或位置错误生成heatmap的代码示例# 在server端aggregate前添加 def plot_gradient_heatmap(client_updates, round_num): norms [] for update in client_updates: # 提取fc.weight的梯度范数假设key为fc3.weight if fc3.weight in update: norm torch.norm(update[fc3.weight]).item() else: norm 0 norms.append(norm) plt.figure(figsize(8, 2)) plt.imshow([norms], cmapReds, aspectauto) plt.colorbar() plt.title(fRound {round_num} fc3.weight Gradient Norm) plt.xlabel(Client ID) plt.yticks([]) plt.savefig(fheatmap_round_{round_num}.png) plt.close()提示heatmap比数字更早暴露问题。若攻击后heatmap无变化立刻检查poison_labels()是否真修改了dataset.targets——打印base_dataset.targets[:10]前后对比。5. 避坑指南毕业设计复现中最常踩的5个坑每个都导致答辩被问住复现联邦学习攻击防御90%的问题不是代码写错而是对联邦学习系统行为的误解。以下5个坑是我带过17届毕设学生后总结的高频翻车点每个都附真实现象、根因和解法。5.1 坑1client注册失败报错“Connection reset by peer”——其实是server端口被占不是网络问题现象运行fl.client.start_numpy_client()后client日志卡在Connecting to 0.0.0.0:8080...server端无client连接记录几秒后报ConnectionResetError。原因端口8080已被其他进程占用如Jupyter Lab、旧的Flower server、Docker容器。Flower默认不检测端口占用直接尝试连接失败后抛出底层socket错误。解决Linux/Mac执行lsof -i :8080Windows执行netstat -ano | findstr :8080杀掉对应PID进程或改server端口为8081client同步改server_addresslocalhost:8081。5.2 坑2global accuracy始终为0.00%——client上传的参数根本没被server加载现象server日志显示INFO flower 10 clients connected但global_evaluate_fn中net.load_state_dict(state_dict)后模型预测全错accuracy恒为0。原因client端get_parameters()返回的参数顺序与server端net.state_dict().keys()不一致。PyTorch 2.0中state_dict().keys()顺序受模型定义顺序影响若client和server用不同脚本定义Netkeys顺序可能不同导致zip(keys, parameters)错位。解决在server和client中强制统一keys顺序# server.py 和 client.py 中都加 ordered_keys sorted(net.state_dict().keys()) # 排序确保一致 def get_parameters(self, config): params [val.cpu().numpy() for k, val in net.state_dict().items() if k in ordered_keys] return params # load时也按ordered_keys params_dict zip(ordered_keys, parameters)5.3 坑3防御后accuracy比baseline还低——Krum误杀了良性client现象开启Krum后global accuracy从88%掉到72%且server日志显示每轮都选中同一个client如cid3。原因Krum的mn-f-2参数设置错误。当n5个clientf1时m5-1-22但若恶意client恰好位于数据分布边缘如label翻转后梯度方向异常Krum可能连续选中它——因为它的“距离和”意外最小。解决降低f值如设f0或增加client数量n≥10使统计更鲁棒或改用RFAgeometric median对异常值更鲁棒。切勿强行调高f。5.4 坑4attack生效但defense无反应——防御代码根本没被执行现象Label-Flipping后global accuracy掉到65%但启用Krum后仍是65%曲线完全重合。原因Flower的Strategy类中aggregate_fit函数未被正确覆盖。常见错误是复制了FedAvg源码但忘了删掉super().aggregate_fit()调用导致实际执行的是父类FedAvg而非你的Krum。解决在自定义strategy中彻底重写aggregate_fit不要继承FedAvg直接继承fl.server.strategy.Strategy基类或确认super().aggregate_fit()被注释掉。5.5 坑5复现论文结果差10%——忽略了论文的client heterogeneity设置现象论文报告Krum在20%攻击下acc85%你复现只有75%。原因论文使用Non-IID数据划分如Dirichlet分布α0.1而你用IID随机均分。Non-IID下恶意client更容易被识别IID下所有client相似Krum区分度下降。解决用torch.utils.data.random_split无法模拟Non-IID必须用sklearn.model_selection.train_test_split按类别分层抽样或使用torchvision.datasets的Subset配合np.random.dirichlet生成client数据比例。示例# 按Dirichlet分布划分CIFAR-10 from sklearn.model_selection import train_test_split alpha 0.1 n_clients 10 class_counts [1000] * 10 # CIFAR-10每类1000训练样本 dirichlet_dist np.random.dirichlet([alpha] * n_clients, size10) # 10类×10client6. 毕业设计答辩前的终极验证用3个命令生成可展示的证据链让导师一眼信服你真复现了答辩时导师最想看的不是代码而是证据链攻击确实发生了、防御确实起效了、结果确实可复现。以下三个命令生成三份材料构成闭环证据。我带的学生用这套方法100%通过预答辩。6.1 命令1生成攻击生效证据——client本地accuracy与global accuracy的剪刀差图# 运行attack-only实验不启用Krum python server.py --strategy fedavg --attack label_flip --malicious_ratio 0.2 # 日志中提取每轮数据存为attack_log.csv # 用以下脚本生成对比图 import pandas as pd import matplotlib.pyplot as plt log pd.read_csv(attack_log.csv) # 列round, client_acc_cid0, client_acc_cid1, ..., global_acc plt.figure(figsize(10,6)) for cid in range(10): plt.plot(log[round], log[fclient_acc_cid{cid}], alpha0.6, labelfClient {cid}) plt.plot(log[round], log[global_acc], k-, linewidth2, labelGlobal Accuracy) plt.xlabel(Round) plt.ylabel(Accuracy (%)) plt.title(Label-Flipping Attack: Local vs Global Accuracy Divergence) plt.legend() plt.grid(True) plt.savefig(attack_divergence.png)为什么有效图中若出现“多条client曲线分散global曲线居中下移”证明攻击成功制造了client间差异——这是防御的前提。若所有client曲线重合说明攻击未生效。6.2 命令2生成防御生效证据——Krum选中的client ID历史记录表# 修改server.py在krum_aggregate()末尾添加日志 best_idx torch.argmin(scores).item() print(fRound {server_round}: Krum selected client {best_idx} (score{scores[best_idx]:.2f})) # 运行defense实验日志存为krum_log.txt # 提取并生成表格 import re with open(krum_log.txt) as f: lines f.readlines() rounds [] selected [] for line in lines: m re.search(rRound (\d): Krum selected client (\d), line) if m: rounds.append(int(m.group(1))) selected.append(int(m.group(2))) df pd.DataFrame({Round: rounds, Selected_Client_ID: selected}) df.to_csv(krum_selection.csv, indexFalse)RoundSelected_Client_IDScore13124.562798.2133110.03.........为什么有效表格证明Krum不是固定选某个client而是动态响应——若连续10轮都选cid0说明它可能是恶意client但防御机制仍在工作选它是因为它最“中心”。导师扫一眼就知道你真跑了Krum。6.3 命令3生成可复现性证据——requirements.txt seed设置 hash校验# 1. 锁定环境 pip freeze requirements.txt # 2. 在所有random操作前加seedclient.py, server.py torch.manual_seed(42) np.random.seed(42) # 3. 对关键输出生成hash import hashlib with open(global_accuracy_history.txt, rb) as f: hash_val hashlib.md5(f.read()).hexdigest() print(fResult Hash: {hash_val}) # 例如a1b2c3d4e5f6...为什么有效答辩时你只需说“导师这是我的requirements.txt、所有seed设为42、最终accuracy文件的MD5是a1b2c3d4…您在任意机器上pip install -r后运行结果完全一致。”——这比解释100行代码更有说服力。最后说句实在的联邦学习攻击防御的毕业设计本文还有配套的精品资源点击获取
返回列表