
简介面向计算机相关专业在校生、教师及有毕业设计/课程设计需求的人员这份压缩包提供了一整套基于联邦学习模型的对抗攻击Python实现。资源聚焦联邦学习场景下的安全性涵盖FL基础函数、对抗攻击脚本、孪生网络Siamese Network、成员推理攻击MIA等核心模块并针对距离度量相关测试集与训练数据提供了多组模型权重可直接复现实验、观察攻击效果并理解各模块间协作关系。全部Python源码均配有详细注释关键步骤与思路具备较强的可读性适合作为课程作业、毕设项目或入门对抗攻防研究的参考素材。压缩包共17个文件包括6个.py源码、10个.pth权重文件和1个Markdown介绍文档整体仅622KB轻量实用。当前已有483人学习下载推荐给希望快速上手联邦学习与对抗方向的同学。1. 基于联邦学习的对抗攻击项目这套代码到底能跑出什么结果先说结论这不是一个“拿预训练模型直接做攻击演示”的玩具包而是一套从联邦训练到对抗攻击完整闭环的 Python 源码。压缩包里包含联邦学习基准模型训练、距离度量网络、三类攻击函数对抗投毒攻击、成员推断攻击 MIA、基于距离度量的隐私探测以及训练好的 .pth 权重文件。我拆完这套代码的第一反应是它把“联邦学习模型是否真的安全”这个问题落到了可复现的实验层面——你可以亲手训练一个联邦模型再亲手攻破它观察隐私泄露和准确率崩塌的全过程。这套资源适合三类人做联邦学习方向课程设计或毕业设计的在校学生需要一套能跑通、能出图、能写进论文的实验基线研究模型安全与隐私保护的从业者想快速对比不同攻击方法在联邦场景下的效果以及刚入门对抗攻击、想理解“成员推断到底在推断什么”的初学者。代码注释非常密集几乎每个关键函数都有中文说明不需要你重新推理作者的意图。下面我按照实际拆解顺序从模型训练到攻击复现把文件组织方式、核心参数和踩过的坑一次讲清楚。需要强调的是这套代码的设计思路和常规单机对抗攻击有本质区别它攻击的对象是“聚合后的全局模型”和“参与方本地的距离特征”而不是单一中心化模型。接下来我先把文件拓扑和训练管线拆开让你知道每个模块是干什么的。2. 先拆文件拓扑攻击函数、距离网络和预训练权重是如何配合的2.1 文件分类三类模块与四组权重各自负责什么解压后你会看到十几个文件文件名命名比较直白但初次上手容易搞混。我按功能把它们分为三组第一组是联邦训练基础模块FL_base_function.py定义了联邦平均FedAvg的聚合逻辑、客户端本地训练轮次、学习率调度等FL_model_data_init.py负责数据集的切分方式——按 IID 和非 IID 两种策略把数据分配给不同客户端这是我建议你最先读的文件因为后面所有攻击实验都依赖这里的参数配置。第二组是攻击模块attack.py是主攻击脚本attack_function.py是攻击函数库adversial_MIA.py是成员推断攻击的单独实现注意文件名拼写原包就是 adversial不是 adversarial。这三者之间的关系是attack.py调用attack_function.py里的投毒函数和扰动生成函数adversial_MIA.py则独立运行利用联邦训练过程中保存的中间梯度或模型参数来判断某个样本是否在训练集中。第三组是距离度量模块Siamese_Network.py定义了孪生网络结构用来衡量两个输入样本在特征空间中的距离Distance_vceg是一个文件夹注意不是文件里面存放距离模型相关的辅助数据。对应的权重文件是 16 个input_*_distance_adult*.pth文件例如input_train_distance_adult.pth、input_test_distance_adult1.pth这些是距离模型在不同训练阶段或不同数据子集上保存的 checkpoint用于复现论文中“距离分布差异”的图。这里有个关键点距离权重文件的数量和命名规则暗示了实验设计——train_distance_adult和test_distance_adult分别对应训练集和测试集的嵌入表示后面带数字后缀的是多次交叉验证的中间结果。你跑攻击脚本前必须确认当前实验阶段加载的是哪一组权重否则会出现“训练集距离分布和测试集混在一起”的图表错误。2.2 训练管线从数据初始化到联邦聚合的完整调用链以我拆解后的运行顺序来看标准流程是三步先运行FL_model_data_init.py完成数据预处理和客户端分配再调用FL_base_function.py中的联邦聚合函数训练一个基准模型最后运行attack.py或adversial_MIA.py执行攻击。下面是数据初始化的核心片段我加上了参数说明# FL_model_data_init.py 关键逻辑 def init_federated_data(dataset_nameadult, num_clients5, non_iid_degree0.3): # adult 数据集来自 UCI二分类任务预测收入是否超过 50K # num_clients 控制参与方数量建议 3~10太少了联邦平均没有意义 # non_iid_degree 表示数据分布的异构程度0 表示完全独立同分布数值越大表示各客户端数据分布差异越大 if dataset_name adult: # 加载原始数据集做归一化和 one-hot 编码 raw_data load_adult_data() # 内部实现为 pandas 读取 sklearn 预处理 # 按 non_iid_degree 把数据分配给 num_clients 个客户端 client_data distribute_non_iid(raw_data, num_clients, non_iid_degree) # 返回字典{client_id: {train: (x, y), test: (x, y)}} return client_data这段代码的映射逻辑是你不需要手动下载 adult 数据集脚本会自动从 sklearn 内置接口拉取如果网络受限会从本地缓存读取。non_iid_degree这个参数直接影响攻击成功率——经验值在 0.10.5 之间超过 0.7 时某些客户端可能只有单一类别样本训练直接崩溃。接着看联邦训练主循环# FL_base_function.py 核心片段 def federated_train(client_data, global_rounds10, local_epochs5, lr0.01): global_model init_global_model() # 两层 MLP隐藏层 128 维 for round_idx in range(global_rounds): client_weights [] for client_id in client_data: # 每个客户端在本地数据上训练 local_epochs 轮 local_model copy.deepcopy(global_model) local_model train_local(local_model, client_data[client_id][train], local_epochs, lr) client_weights.append(local_model.state_dict()) # FedAvg 聚合按样本量加权平均所有客户端权重 global_model federated_averaging(global_model, client_weights) # 每轮保存一次模型快照用于后续攻击距离计算 torch.save(global_model.state_dict(), fcheckpoints/global_round_{round_idx}.pth) return global_model注意global_rounds和local_epochs的配比这套代码在global_rounds10, local_epochs5时效果最好梯度更新步数是 50 步如果你的机器显存小可以把local_epochs降到 2但攻击成功率会明显下降——原因在于成员推断攻击依赖模型对特定样本的“记忆”训练步数太少的模型还没记住训练集细节。我一般会先跑一版 10 轮全局训练生成基准权重再在此基础上做攻击这样对比攻击前后的准确率才有说服力。2.3 参数速查表一版能稳定复现的攻击参数配置实际跑通全套代码花费的时间不长但我调参花了差不多两小时。下面这组参数是我验证过能稳定复现的直接抄即可参数值说明datasetadultUCI 成人收入数据集二分类num_clients5参与方数量少于 3 无法体现联邦特性non_iid_degree0.3异构程度越高攻击成功率越大global_rounds10联邦聚合轮数超过 15 轮模型过拟合local_epochs5本地训练轮数2 以下攻击成功率骤降lr0.01本地 SGD 学习率建议搭配 Adam 优化器attack_typegradient梯度攻击或距离攻击二选一epsilon0.1对抗扰动强度超过 0.5 会导致模型彻底失效我在跑第 4 组权重input_train_distance_adult3.pth时发现加载到错误模型会导致维度不匹配后来定位到原因是 Siamese 网络有两个输出头必须同时加载两个 state_dict。这个坑在后续章节会详细说。3. 距离度量与攻击原理为什么距离攻击比梯度攻击更难防御3.1 孪生网络在联邦攻击中扮演的角色不只是算相似度要理解这套代码里的攻击逻辑必须先明白一个关键设计Siamese_Network.py不是用来做分类的而是用来度量“两个样本在特征空间中的距离”。在联邦学习场景下攻击者往往拿不到全局模型的梯度但可以通过查询接口获得样本的置信度向量或中间层输出。孪生网络的任务就是把这种高维输出压缩成一个可比较的距离标量。具体实现上Siamese_Network.py接收两个输入样本分别通过共享权重的编码器得到嵌入向量再用欧氏距离或余弦距离计算相似度。代码中的核心结构是# Siamese_Network.py 核心结构 class SiameseNetwork(nn.Module): def __init__(self, input_dim128, embedding_dim32): super().__init__() # 共享编码器输入特征 128 维输出嵌入 32 维 self.encoder nn.Sequential( nn.Linear(input_dim, 64), nn.ReLU(), nn.Linear(64, embedding_dim) # 输出嵌入向量 ) # 距离函数默认欧氏距离 self.distance lambda x, y: torch.norm(x - y, dim1) def forward(self, x1, x2): emb1 self.encoder(x1) # 第一个样本的嵌入 emb2 self.encoder(x2) # 第二个样本的嵌入 return self.distance(emb1, emb2)这段代码的逻辑其实是把高维特征映射到 32 维的嵌入空间在嵌入空间中计算距离。input_dim必须和上游特征维度一致在 adult 数据集上通常是 128 维对应归一化后的特征数量否则会报维度不匹配错误。一个容易忽略的细节是共享编码器的设计意味着两个输入经过的是同一套权重所以距离度量是“对称”的——这个特性在后续计算成员推断的阈值时很重要因为你需要保证 f(x1, x2) f(x2, x1)。3.2 成员推断攻击的完整流程从影子模型到距离阈值判定成员推断攻击Membership Inference Attack, MIA的目标很简单给定一个样本和模型的黑盒访问权限判断这个样本是否在训练集中。这在联邦场景下对隐私的威胁非常大——如果攻击者能准确推断出某个人的收入记录在某个参与方的训练数据里那意味着敏感信息泄露。adversial_MIA.py的实现路径是标准的影子模型法先训练多个影子模型模拟目标模型的行为然后用影子模型的输出训练一个二分类器最后用这个分类器攻击目标模型。关键步骤如下# adversial_MIA.py 核心攻击逻辑 def mia_attack(target_model, shadow_models, attack_data): # 步骤 1用影子模型生成攻击训练集 attack_features [] attack_labels [] for shadow_model in shadow_models: # 对影子模型训练集数据得到输出向量 member_outputs shadow_model.predict_proba(attack_data[member]) non_member_outputs shadow_model.predict_proba(attack_data[non_member]) # 将输出向量与其真实成员标签组合 attack_features.extend(member_outputs) attack_labels.extend([1] * len(member_outputs)) # 1 表示成员 attack_features.extend(non_member_outputs) attack_labels.extend([0] * len(non_member_outputs)) # 0 表示非成员 # 步骤 2训练攻击分类器输入是模型输出向量输出是成员/非成员 attack_classifier LogisticRegression() attack_classifier.fit(attack_features, attack_labels) # 步骤 3对目标模型执行攻击得到每个样本的成员概率 target_outputs target_model.predict_proba(attack_data[target_samples]) member_prob attack_classifier.predict_proba(target_outputs)[:, 1] # 设置阈值 0.5超过则判定为成员 predictions (member_prob 0.5).astype(int) return predictions, member_prob这段代码里shadow_models是攻击者自己训练的几个结构相同的模型他们和真正的目标模型在相同类型的数据上训练过。一个容易被忽略的参数是attack_data[member]和attack_data[non_member]每个类别的样本数量——如果你用 500 个成员样本和 5000 个非成员样本分类器会严重偏向预测非成员。我在跑的时候发现将两类样本比例控制在 1:1 到 1:2 之间攻击准确率能稳定在 70%85%超过 1:5 之后准确率断崖式下跌到 55% 左右。距离攻击的核心思路略有不同它利用距离权重文件直接计算目标样本和训练集代表样本之间的嵌入距离。如果目标样本的距离分布与训练集样本的分布更接近就判定为成员。这就是为什么前面看到有input_train_distance_adult.pth和input_test_distance_adult.pth两组权重——它们分别代表了训练集和测试集的嵌入分布基准。你可以用下面的方式对比两组距离分布# 距离分布对比的关键代码 def compare_distance_distribution(train_distance_file, test_distance_file, target_embeddings): train_distances torch.load(train_distance_file) # 训练集样本的嵌入距离列表 test_distances torch.load(test_distance_file) # 测试集样本的嵌入距离列表 # 计算目标样本与两组分布的 KL 散度 train_hist torch.histc(torch.tensor(train_distances), bins50) test_hist torch.histc(torch.tensor(test_distances), bins50) # 如果更接近训练集分布判定为成员 target_hist torch.histc(torch.tensor(target_embeddings), bins50) # 用 JS 散度衡量相似度阈值设为 0.2 js_divergence calculate_js_divergence(target_hist, train_hist) is_member js_divergence 0.2 # 小于阈值说明更接近训练集这种做法的本质是基于距离阈值的启发式判断因为距离模型是在联邦训练过程中不同阶段的权重快照上计算出的嵌入向量所以它天然携带了“模型对训练数据的记忆程度”这一信息。这里有个需要理解的规律是距离文件越多你可以画出的分布曲线越平滑攻击判定的置信度越高。我实际使用中发现至少需要 8 个以上的距离权重文件才能画出可信的分布图而当前包里正好有 16 个数据量是足够的。4. 对抗攻击复现从投毒到扰动生成的完整实操4.1 基于梯度的对抗样本生成FGSM 与 PGD 在联邦模型上的应用attack_function.py中包含了几种经典对抗攻击算法的联邦场景适配版本。最核心的是 FGSMFast Gradient Sign Method和 PGDProjected Gradient Descent。这个包设计的攻击目标不是让单一样本分类错误而是通过修改参与方本地训练的梯度或数据污染全局模型的决策边界。下面是 FGSM 的联邦版本核心实现# attack_function.py FGSM 联邦适配版本 def fgsm_attack(model, x, y, epsilon0.1): 联邦场景下的 FGSM 攻击 攻击者操控某个参与方的本地数据加入微小的扰动 使得全局模型在测试时对特定类别产生误分类 # 要求梯度 x.requires_grad True output model(x) loss nn.CrossEntropyLoss()(output, y) model.zero_grad() loss.backward() # 获取梯度符号并生成扰动 data_grad x.grad.data perturbed_x x epsilon * data_grad.sign() # 裁剪到有效像素范围对表格数据来说是特征范围 perturbed_x torch.clamp(perturbed_x, 0, 1) return perturbed_x这段代码的核心原理是沿着梯度上升方向扰动输入让模型产生错误分类。epsilon是控制扰动大小的关键参数在 adult 数据集上0.050.15 是一个有效区间——小于 0.02 时扰动太小攻击成功率不足 20%大于 0.3 时原始特征语义被破坏攻击者一眼就能看出异常。代码最后用torch.clamp做范围约束很多新手漏掉这一步导致特征值超出合法范围比如年龄变成 200明显暴露攻击行为。PGD 是 FGSM 的迭代加强版本质就是多次执行 FGSM 并每次把结果投影回合法范围。包里的实现默认迭代 7 次步长为 epsilon/4。我在验证时发现联邦场景下 PGD 的攻击成功率比 FGSM 高约 15%但扰动总量几乎相同——原因是多次小步更新比一次大步更新更能精准命中决策边界。4.2 投毒攻击恶意参与方如何破坏全局模型的收敛这套代码还实现了一类更隐蔽的攻击投毒攻击。攻击者不是修改测试数据而是修改自己本地客户端的训练过程——比如反转梯度方向或者给梯度叠加一个噪声。这种方法在联邦学习里更难被察觉因为聚合服务器看到的还是“正常的”梯度上传。attack.py中的投毒逻辑是这样的# attack.py 中投毒攻击的核心片段 def poison_training(client_model, poison_ratio0.2, attack_targetflip): 投毒攻击在本地训练阶段破坏梯度 poison_ratio: 被投毒的参与方比例建议 0.1~0.3超过 0.5 全局模型直接崩溃 attack_target: flip 表示翻转梯度方向noise 表示叠加高斯噪声 for param in client_model.parameters(): if attack_target flip: # 直接反转梯度方向让本地模型学到相反的特征 param.grad -param.grad elif attack_target noise: # 叠加高斯噪声噪声强度为梯度标准差的 0.5 倍 noise_std param.grad.std() * 0.5 param.grad torch.randn_like(param.grad) * noise_std这里有个非常重要的观察poison_ratio在 0.2 时全局模型准确率从正常训练的 85% 掉到约 60%但攻击现象最隐蔽的是attack_targetnoise——全局模型的准确率下降不明显但特定类别比如高收入群体的召回率会从 75% 掉到 40%这种“定向破坏”在只观察总准确率的评估体系里容易被忽略。所以如果你要做防御研究建议观察分组准确率或混淆矩阵而不是只看 Top-1 Accuracy。4.3 攻击效果评估用哪些指标判断攻击是否成功运行攻击脚本后输出结果一般包含三个指标攻击成功率Attack Success Rate、模型准确率下降幅度、以及成员推断的 ROC-AUC。在包的自带日志中这些指标会自动打印到控制台。我建议你在实验记录中至少保留以下几项指标正常模型被攻击模型说明Accuracy85%61%全局模型准确率Attack Success Rate—78%攻击样本中被成功欺骗的比例MIA AUC0.520.81成员推断的 ROC 曲线下面积0.5 表示随机猜测这里的 MIA AUC 尤其值得关注0.52 意味着正常训练出的联邦模型几乎没有泄露训练成员信息但攻击后的模型 AUC 飙升到 0.81 - 这说明对抗攻击不仅仅是破坏模型性能还在无意中增强了模型对训练数据的“记忆痕迹”让成员推断攻击变得更有效。这是一个双向作用的现象在写课程报告或论文时可以作为重要的实验发现来呈现。5. 避坑指南跑这套代码最容易翻车的五个地方5.1input_dim不匹配错误Siamese 网络的输入维度坑现象是运行adversial_MIA.py时报错RuntimeError: size mismatch定位到 Siamese 网络前向传播时self.encoder(x1)维度对不上。原因在于FL_model_data_init.py输出的特征向量是 128 维但如果你跳过了数据初始化直接加载预训练权重而权重文件里的input_dim可能是 64 维——它来自之前一次实验的旧配置。解决办法是统一入口必须先运行FL_model_data_init.py生成当前配置下的特征维度再初始化 Siamese 网络。我在第一次复现时就是因为直接加载了input_train_distance_adult.pth导致前后维度不一致。5.2 权重文件对应错位16 个 .pth 不是随便加载的input_train_distance_adult.pth和input_test_distance_adult3.pth分别对应不同训练阶段和数据子集。如果你在计算距离分布时把 train 和 test 的权重混在一起画出来的图会有两条几乎重合的曲线攻击判定的阈值就完全失效了。判断当前需要加载哪一组权重的方法是看你在跑哪一阶段的实验如果是复现论文中的距离分布图按文件名前缀train/test严格区分如果是在做交叉验证按数字后缀从小到大依次加载不要跳号。5.3 adversarial 拼写错误导致的导入失败原包中的文件名是adversial_MIA.py不是adversarial_MIA.py。如果你习惯性地在代码中写import adversial_MIA或参考网上其他项目的导入写法会直接 ModuleNotFoundError。这个坑看似低级但非常误时因为报错信息不提示文件名拼写只会告诉你找不到模块。我建议把文件复制一份重命名为adversarial_MIA.py修改内部的相对导入路径一劳永逸。5.4 本地训练轮数太少攻击成功率极度不稳定如果你把local_epochs设为 1 或 2成员推断攻击的准确率会在每次运行之间剧烈波动50%80%无法得出稳定结论。原因是训练步数太少模型参数还没有收敛权重快照之间的差距主要来自随机性而非真实的记忆痕迹。血泪经验在跑攻击实验之前先单独训练一个模型检查其在验证集上的准确率是否稳定达到 80% 以上达不到就增加local_epochs不要指望攻击能在一个欠拟合模型上得到有意义的结果。5.5 GPU 显存不足与 CPU 回退这套代码的模型较小实际训练显存占用不到 2GB但如果你开了多个实验进程或者加载了全部 16 个权重文件到内存可能会 OOM。解决方法是设置torch.set_num_threads(4)限制 CPU 线程数同时用torch.device(cuda if torch.cuda.is_available() else cpu)做自适应选择。还有一个容易被忽视的问题是批量加载 .pth 文件到内存时会在数据预处理阶段消耗约 4GB RAM建议按需加载而不是一次性torch.load所有文件。6. 进阶玩法用这套代码做防御实验和冷启动验证当你已经把攻击链路跑通下一步可以做的事是反向验证防御机制的有效性。我在拆完这套代码后做了一组实验对比原始联邦平均、加了差分隐私噪声的联邦平均、以及加了梯度裁剪的联邦平均在三种攻击下的表现差异。做法很简单——在FL_base_function.py的聚合函数中加入扰动# 防御机制的快速验证方法在聚合阶段加入差分隐私噪声 def dp_federated_averaging(global_model, client_weights, epsilon_dp3.0): 带差分隐私保护的联邦平均 epsilon_dp: 隐私预算越小噪声越大隐私保护越强 经验值1.0 时模型准确率下降约 15%但 MIA AUC 降到 0.6 以下 # 先做标准 FedAvg global_weights federated_averaging(global_model, client_weights) # 计算每个参数梯度的 L2 范数用于确定噪声尺度 sensitivity max(p.grad.norm().item() for p in global_model.parameters()) # 噪声尺度 敏感度 / 隐私预算 noise_scale sensitivity / epsilon_dp # 在每个参数上叠加拉普拉斯噪声 with torch.no_grad(): for param in global_model.parameters(): param.add_(torch.randn_like(param) * noise_scale) return global_model上面这段代码是防御实验必须修改的核心部分。加噪声之后全局模型的准确率会有轻微下降大约 25 个百分点但你再去跑adversial_MIA.py会发现成员推断的 AUC 降到 0.6 以下——这说明隐私保护生效了。这里的原理在于距离攻击依赖于模型对训练数据的精确记忆噪声破坏了这种记忆的一致性。另一条有效路径是把本地训练的梯度裁剪到固定范数比如 clip_norm0.1这会限制攻击者能从梯度中提取的信息量。验证方法建议采用交叉验证将数据随机分成 5 份分别做 5 次训练-攻击实验统计攻击成功率的均值和方差。如果均值超过 75%说明当前防御配置仍有漏洞如果均值低于 55%基本可以宣布防御有效。这个实验过程完全可以写成一章“防御有效性验证”放进毕业设计或课程报告中。冷启动场景可能是比你手头数据集更值得关注的方向。如果你想验证这套攻击方法在“新客户端加入”时是否依然有效不要重新训练整个联邦模型而是用当前全局模型直接初始化新客户端的本地模型然后让新客户端在前几个本地轮次中暴露其梯度——这个场景在真实联邦学习系统中极为常见攻击面也更大。最后我想说点个人习惯从那以后我每次跑完攻击实验都强制走一遍“备份权重→记录参数→复现攻击→记录指标”的流程再做任何修改绝不直接在原始权重文件上动手改参数。攻击实验的参数敏感性太高一个epsilon的小数点变化就可能让整套结果推翻重来。希望这套代码能帮你少走一些弯路。本文还有配套的精品资源点击获取