
简介围绕联邦学习中的对抗攻击与模型安全课程大作业完整交付了Python源码、详细注释与预训练模型切合计算机、人工智能、通信工程等专业学生的课程设计、毕业设计需求也适合相关方向的小白进阶学习。资源共17个文件其中6个Python脚本带有详细注释覆盖模型初始化、联邦学习基础训练、距离度量、对抗攻击与成员推理攻击等关键模块10个.pth文件为各阶段模型权重可直接加载复现实验结果1个Markdown文档提供整体说明便于快速上手。压缩包仅622KB轻量却结构完整目前已有483人学习下载。通过逐行注释可理清联邦学习与对抗攻击的联动逻辑还可在此基础上更换数据集、调整攻击策略快速支撑课设演示、毕设实验或进一步安全研究适合作为该方向的入门与开发基座。1. 联邦学习模型的对抗攻击课程作业这个 zip 里到底装了什么如果你正在做联邦学习相关课程设计恰好又拿到一个叫“基于联邦学习模型的对抗攻击”的源码包第一反应多半是联邦学习不是号称保护隐私吗怎么还能被攻击这套代码就是回答这个问题的——它用“成员推理攻击”的方式验证联邦学习模型确实会把训练数据的痕迹泄露出去。核心思路不复杂先让模型对训练集和测试集分别输出距离特征再用孪生网络学一个判别器判断某条样本到底在不在模型的训练集里。代码基于 PyTorchAdult 收入数据集distance_adult 系列权重文件对应的就是它从联邦学习数据初始化、距离特征提取到攻击判别全链路跑通。适合课设、毕设、或者想快速验证 FL 隐私风险的人拿到手先跑通再改比从零搭快得多。2. 把联邦学习环境搭起来从 FL_model_data_init.py 看参数怎么定2.1 联邦学习初始化脚本到底在做什么很多人拿到压缩包先找 attack.py觉得攻击脚本才是主角。但我建议你先打开 FL_model_data_init.py因为这套代码的攻击依赖一个前提你手上有一个“目标联邦学习模型”并且知道它大概的训练数据形态。这个文件干的就是这件事——把原始数据划分成多个客户端的本地数据集模拟联邦学习里的 non-IID非独立同分布场景同时为后续生成距离特征文件做准备。代码里的 distance_adult 系列 pth 文件不是模型权重而是“距离特征缓存”。每个 pth 里存的是一个矩阵每一行对应一条样本每一列是这条样本和一组锚点样本的距离。后面孪生网络吃的就是这种特征而不是原始表格数据。所以初始化脚本的核心产出是两类东西一是按客户端划分好的数据索引二是喂给攻击模块的距离特征文件。# FL_model_data_init.py 核心逻辑简化示意 import torch import numpy as np from sklearn.model_selection import train_test_split def split_client_data(X, y, num_clients5, non_iid_ratio0.3): 把数据集划分成 num_clients 个客户端 non_iid_ratio 控制 non-IID 程度越大各客户端数据分布差异越大 client_data [] indices np.arange(len(X)) # 先按标签排序方便做 non-IID 切分 sorted_idx np.argsort(y) sorted_indices indices[sorted_idx] # 按比例切成 num_clients 段每段内部再做一次随机打乱 chunk_size len(sorted_indices) // num_clients for i in range(num_clients): start i * chunk_size end (i 1) * chunk_size if i num_clients - 1 else len(sorted_indices) # 每个客户端的原始索引 client_idx sorted_indices[start:end] client_data.append(client_idx) return client_data # 生成距离特征缓存锚点集合可以固定取前 N 条训练样本 def build_distance_cache(model, dataloader, anchor_set, devicecpu): model.eval() dist_vectors [] with torch.no_grad(): for batch in dataloader: feat model.embedding(batch.to(device)) # 取倒数第二层输出 # 和锚点集合算欧氏距离拼成距离向量 dist torch.cdist(feat, anchor_set, p2) dist_vectors.append(dist.cpu()) return torch.cat(dist_vectors, dim0)逻辑说明split_client_data 先按标签排序再分段这样每个客户端拿到的数据天然带有标签偏好模拟真实联邦学习中“不同客户端数据分布不同”的情况。build_distance_cache 则是把模型对样本的 embedding 输出和固定锚点集合做 cdist 距离计算生成一条样本对应一组距离值。参数说明num_clients 一般取 5 或 10对应联邦学习里客户端数量non_iid_ratio 是控制数据偏斜程度的0 表示 IID1 表示完全按标签划分。距离特征的维度取决于锚点数量锚点取 100~200 条效果比较稳。2.2 数据划分的两种方式以及各自的坑这套代码里数据划分有两种常见思路你得先搞清楚你的目标是哪种。第一种是“影子模型”式把 Adult 数据集切一块出来训练一个本地模型用它模拟联邦学习里的全局模型。第二种是“直接复用”式直接加载现成的距离特征 pth跳过数据划分只做攻击部分。第一种方式适合你还没拿到目标模型权重的情况。你需要自己训练一个结构相近的模型当成“替身”然后在这替身上生成距离特征。第二种方式适合快速跑通——代码包里的 input_train_distance_adult.pth 和 input_test_distance_adult.pth 已经帮你生成好了直接 load 就能进攻击流程。# 两种加载方式的对比 # 方式一直接加载现成特征 train_dist torch.load(input_train_distance_adult.pth, map_locationcpu) test_dist torch.load(input_test_distance_adult.pth, map_locationcpu) print(train_dist.shape) # 预期输出: (训练样本数, 锚点数) # 方式二自行生成需要目标模型提取 embedding # train_dist build_distance_cache(target_model, train_loader, anchor_set) # test_dist build_distance_cache(target_model, test_loader, anchor_set)这里我踩过一个大坑Adult 数据集本身是表格数据没有“原始图片”这种天然 embedding。所以你如果自己重跑初始化脚本得先用一个 MLP 或简单网络把表格数据映射成向量这一步做不好后面的距离特征全是噪声。2.3 客户端数量与训练轮次的联动初始化脚本里还有一组参数决定联邦学习模拟的逼真程度客户端参与比例和本地训练轮次。这组参数直接影响模型权重的分布形态进而影响攻击难度。客户端参与比例越低全局模型见过不同客户端数据的次数越少成员推理的攻击准确率反而可能越高——因为没被参与训练的那部分客户端数据和参与过的差距更大。我一般会把 num_clients 设成 10、每轮参与比例 0.5本地 epoch 设 3。这样既不会让训练时间太长又能模拟出足够明显的“成员痕迹”。3. 攻击核心Siamese 网络与距离特征提取3.1 为什么选孪生网络做攻击判别器这是整套代码的灵魂问题。成员推理攻击最简单的做法是把样本挨个喂给模型看输出的置信度高置信度判定为“在训练集里”。但直接看 softmax 输出很容易翻车——模型对某些测试样本也可能给出很高的置信度而且置信度分布受类别不平衡影响很大。孪生网络Siamese Network的思路不一样它不直接看模型输出而是构造“样本对”学习两个样本之间的距离度量。同一类别、来自同一训练分布的样本距离应该更近分布不同的样本距离更远。这么做带来的好处是攻击判别器学到的是“相对差异”而不是“绝对分数”泛化能力好很多换数据集也能用。# Siamese_Network.py 核心结构简化示意 import torch.nn as nn class SiameseNetwork(nn.Module): def __init__(self, input_dim, hidden_dim128): super(SiameseNetwork, self).__init__() # 两个分支共享权重这是孪生网络的核心约束 self.fc nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 64), ) def forward_one(self, x): # 单个分支的前向输出一个 64 维的嵌入向量 return self.fc(x) def forward(self, x1, x2): # 两路输入经过同一个网络输出两个嵌入再算相似度 out1 self.forward_one(x1) out2 self.forward_one(x2) # 用欧氏距离衡量两路输出的差异 diff out1 - out2 return diff.pow(2).sum(dim1).sqrt()逻辑说明forward_one 把一条距离特征压成 64 维嵌入forward 同时接收两条样本先各自过共享权重网络再算欧氏距离。距离越小说明两条样本在模型眼中越“相似”。参数说明input_dim 是距离特征的维度必须和 2.1 里锚点数量一致hidden_dim 控制网络容量128 对 Adult 这种规模的数据集够用。训练时用 Contrastive Loss对比损失让同类样本对距离变小、异类样本对距离变大。3.2 距离特征 pth 文件的用途和对应关系zip 里那一堆 pth 文件容易让人发懵我逐个说下用途。input_train_distance_adult.pth 和 input_test_distance_adult.pth 是一对训练集距离特征 测试集距离特征。后面带数字后缀 1、2、3 的是不同实验轮次或不同客户端划分方案下的缓存版本。也就是说作者跑过多次实验把各轮的特征都存了一份。这些文件在攻击流程里的角色是“中间制品”不是最终模型。你可以把它理解成原始数据 → 模型 embedding → 距离特征pth 缓存→ 孪生网络 → 成员判定。所以如果你只关心攻击效果直接加载不带数字后缀的那对 pth 就行如果你要复现作者的对比实验再用带数字后缀的版本。3.3 attack_function.py 里封装了什么attack_function.py 是攻击模块的公共函数库主要封装三类东西距离特征生成、判别器训练、攻击指标计算。这个文件里可能有几个函数的命名不规范但作用要搞清楚不然改代码时会有点蒙。# attack_function.py 关键函数简化示意 import torch import torch.nn as nn from sklearn.metrics import roc_auc_score def compute_attack_score(siamese_net, sample_dist, ref_dist, devicecpu): 给定一条样本的距离特征和一组参考样本的距离特征 计算这条样本属于训练集的概率分数 siamese_net.eval() scores [] with torch.no_grad(): for ref in ref_dist: # 样本和每个参考样本算距离取平均作为攻击分数 d siamese_net(sample_dist.unsqueeze(0), ref.unsqueeze(0)) scores.append(d.item()) return sum(scores) / len(scores) def evaluate_attack(train_scores, test_scores): 成员推理评估训练集样本的分数 vs 测试集样本的分数 用 AUROC 衡量攻击能力1.0 表示完全可分0.5 表示瞎猜 y_true [1] * len(train_scores) [0] * len(test_scores) y_score train_scores test_scores return roc_auc_score(y_true, y_score)逻辑说明compute_attack_score 体现“参考样本法”——拿一条待判定样本和一批已知是训练集成员的参考样本逐一算距离距离越小说明越像“自己人”。evaluate_attack 把训练集样本和测试集样本的攻击分数混在一起用 AUROC 衡量可分性。参数说明ref_dist 的样本量决定攻击分数的稳定性一般取 20~50 条参考样本再多计算开销大、收益小。AUROC 小于 0.6 说明攻击基本失败0.7~0.8 是中等水平0.85 以上说明这个联邦学习模型有明显隐私泄露。4. 完整攻击链路attack.py 与 adversial_MIA.py 怎么配合4.1 attack.py 的主流程拆解拿到代码包别急着双击 run先理清 attack.py 编排了哪几步。这套攻击流程大致是加载目标模型 → 生成或加载距离特征 → 初始化孪生网络 → 训练判别器 → 在测试集上评估攻击效果。attack.py 是总指挥FL_base_function.py 提供联邦学习模型训练用的基础组件adversial_MIA.py 是攻击主逻辑。常见做法是把 attack.py 当成“实验入口”每次改动只调它的配置区。例如选择加载哪个 pth、判别器训练几个 epoch、负样本比例怎么设都在这个文件里改。我再强调一次不要改 FL_base_function.py 里的模型结构那是整套代码的地基改错一个维度后面全崩。# attack.py 主流程简化示意 import torch from Siamese_Network import SiameseNetwork from adversial_MIA import train_attack_model, mia_predict from attack_function import evaluate_attack def main(): # 1. 加载距离特征正样本训练集成员负样本测试集非成员 train_dist torch.load(input_train_distance_adult.pth, map_locationcpu) test_dist torch.load(input_test_distance_adult.pth, map_locationcpu) # 2. 初始化孪生网络input_dim 必须和距离特征维度对齐 siamese SiameseNetwork(input_dimtrain_dist.shape[1], hidden_dim128) # 3. 训练攻击判别器 # 这里把 train_dist 分成两部分一部分做参考样本一部分做查询样本 attack_model train_attack_model( siamesesiamese, train_membertrain_dist[:2000], train_nonmembertest_dist[:2000], ref_membertrain_dist[2000:2050], epochs30 ) # 4. 分别在训练集和测试集上计算攻击分数 train_scores mia_predict(attack_model, train_dist[2050:2550]) test_scores mia_predict(attack_model, test_dist[2050:2550]) # 5. 评估 AUROC auroc evaluate_attack(train_scores, test_scores) print(fAttack AUROC: {auroc:.4f}) if __name__ __main__: main()逻辑说明步骤 3 是核心——train_attack_model 内部会先固定孪生网络再用输出的嵌入向量训练一个二分类器通常是 MLP二分类器的标签是“成员/非成员”。步骤 4 用训练好的分类器去预测那些没参与训练的距离特征。参数说明2000 和 2050 这两个数字代表切片边界——前 2000 条做判别器训练2000~2050 这 50 条做参考样本2050~2550 这 500 条做攻击效果验证。实际数据量大时可以放大这些数字数据量小就要缩小否则判别器学不到东西。4.2 adversial_MIA.py成员推理攻击的判别器adversial_MIA.py 里实现的不是传统意义上的“生成对抗网络”那种对抗而是“攻击者 vs 防御者”的对抗。判别器吃的是孪生网络输出的嵌入向量输出的是二分类概率。这个判别器的结构深度不能太浅也不能太深两层全连接效果通常最好。# adversial_MIA.py 核心逻辑简化示意 import torch import torch.nn as nn import torch.optim as optim class MIADiscriminator(nn.Module): def __init__(self, embed_dim64): super(MIADiscriminator, self).__init__() self.net nn.Sequential( nn.Linear(embed_dim, 32), nn.ReLU(), nn.Linear(32, 1) ) def forward(self, x): return torch.sigmoid(self.net(x)).squeeze(-1) def train_attack_model(siamese, train_member, train_nonmember, ref_member, epochs30): # 先把所有样本过孪生网络得到嵌入向量 discriminator MIADiscriminator(embed_dim64) optimizer optim.Adam(discriminator.parameters(), lr1e-3) loss_fn nn.BCELoss() # 构造标签成员样本为 1非成员样本为 0 X [] y [] with torch.no_grad(): for m in train_member: # 与参考样本的距离每个 m 产生一个嵌入向量 dists [siamese(m.unsqueeze(0), r.unsqueeze(0)).item() for r in ref_member] X.append(dists) y.append(1) for nm in train_nonmember: dists [siamese(nm.unsqueeze(0), r.unsqueeze(0)).item() for r in ref_member] X.append(dists) y.append(0) X torch.tensor(X, dtypetorch.float32) y torch.tensor(y, dtypetorch.float32) for epoch in range(epochs): optimizer.zero_grad() pred discriminator(X) loss loss_fn(pred, y) loss.backward() optimizer.step() if epoch % 10 0: print(fEpoch {epoch}, Loss: {loss.item():.4f}) return discriminator逻辑说明这里的 MIA 判别器没直接吃原始距离特征而是吃“与参考样本的距离列表”。也就是说孪生网络先把原始特征映射成嵌入向量判别器再在这个嵌入空间里做成员/非成员分类。这个设计比直接对距离特征做分类要稳因为嵌入空间里同类样本会更聚拢判别器边界更好学。参数说明embed_dim64 要和孪生网络最后的输出维度一致改了一边另一边必须跟着改。BCELoss 配 sigmoid 输出是二分类的标准组合。epochs30 对这个数据规模足够再多就会过拟合到训练集的噪声上。实际跑的时候如果你发现训练 Loss 不断下降但 AUROC 只有 0.5 左右大概率是参考样本太少或者成员/非成员样本特征分布重叠太严重。4.3 模型权重文件怎么用、能不能迁移到别的数据集zip 里的多个 pth 文件在使用上要分清主次。核心是 input_train_distance_adult.pth 和 input_test_distance_adult.pth它们是对应 Adult 数据集的完整距离特征。带数字后缀的那些 pth 是多次实验的产物——作者可能调整过锚点数量、客户端划分比例或者模型初始化种子每次生成的特征单独保存了一份。这些 pth 能不能迁移到别的数据集不能直接用。因为距离特征的维度由锚点数量决定锚点又来自 Adult 数据集的样本。换数据集必须重新走一次 build_distance_cache 流程。但 Siamese 网络的结构和判别器的训练代码可以复用你只需把自己的数据集换成 Adult然后重新生成距离特征文件。如果你要换成 CIFAR-10 这种图片数据集需要额外加一个 CNN 特征提取器再把输出压成向量喂给孪生网络。# 在自定义数据集上复用攻击代码示例 # 假设你有自己的 embedding 提取器 my_encoder def prepare_custom_features(my_encoder, train_loader, anchors): my_encoder.eval() feature_list [] with torch.no_grad(): for x, _ in train_loader: emb my_encoder(x) # 自定义编码器输出特征 dist torch.cdist(emb, anchors, p2) feature_list.append(dist.cpu()) return torch.cat(feature_list, dim0)整体来看这套代码的复用思路是攻击管线孪生网络 MIA 判别器是通用的数据相关部分锚点、距离特征需要自己重新生成。很多做课程设计的同学栽在“想直接拿 pth 文件喂给别的模型”这一步记住一点pth 文件的本质是缓存不是模型一切以重新生成为准。5. 避坑指南pth 文件、路径与数据混淆的五个高频翻车点5.1 现象加载 pth 时直接报错RuntimeError: File is not a zip file原因PyTorch 的 torch.load 默认期望 pth 是 zip 格式但如果你在生成时用了老版本 PyTorch 或者保存方式不是 state_dict文件头会不一样。再者下载压缩包后直接解压到中文路径下文件路径编码也可能出问题。解决先用 torch.save 重存一遍或者加载时显式指定 map_location 和 weights_onlyFalse。代码包里这些 pth 文件是作者本机环境存的你本机的 PyTorch 版本如果差异大先单独写个小脚本测试能不能 load 通不要直接跑主流程否则报错之后你会怀疑是攻击代码的问题实际上是文件加载的问题。# 验证 pth 文件能正常加载的探针脚本 import torch try: data torch.load(input_train_distance_adult.pth, map_locationcpu) print(load ok, shape:, data.shape) except Exception as e: print(load failed:, e) # 尝试用非严格模式重新加载 data torch.load(input_train_distance_adult.pth, map_locationcpu, weights_onlyFalse)5.2 现象训练集和测试集距离文件混淆攻击效果暴跌原因input_train_distance_adult.pth 和 input_test_distance_adult.pth 文件名只差一个 train/test但数据内容完全不同。训练集特征来自参与训练的样本测试集特征来自没参与训练的样本。如果你把两个文件搞反相当于把正样本当负样本、负样本当正样本判别器学到的是完全错误的知识AUROC 可能低于 0.5。解决加载之后先打印 shape 和数值分布确认 train 和 test 分布有明显差异。正常情况是 train 距离整体比 test 距离小因为训练样本在模型上有更强的“适应痕迹”。如果 train 的均值反而更大检查是不是文件搞反了。5.3 现象攻击准确率虚高AUROC 接近 1.0不真实原因这是课程作业最喜欢出现的“假成功”——训练判别器时用的成员样本和非成员样本来自同一个 batch 切分存在数据泄漏。例如你拿 train_dist 的前 2000 条做训练又拿同一份文件的后 500 条做验证模型其实“记住”了文件顺序而不是学到了成员特征。解决严格把训练集样本和验证集样本分开中间加一个防火墙——参考样本也必须来自独立切片不能和查询样本有任何重叠。改造方法很简单把数据集先 shuffle 再切分成三个不相交的部分分别用于参考样本、判别器训练、攻击验证。5.4 现象batch size 变化导致攻击效果剧烈波动原因距离特征的分布和 batch 内的统计量有关。如果你在生成距离特征时用了带 BatchNorm 的模型又设了 model.train()那么 BN 层会使用 batch 统计量导致不同 batch 生成的特征分布不一致。你加载现成的 pth 文件没问题但如果你自己重新生成特征这一步非常容易踩坑。解决生成距离特征之前强制 model.eval()并且用 torch.no_grad() 包住前向过程。eval 模式会让 BN 层使用全局统计量保证每一条样本的距离特征独立于其它样本的 batch 搭配。这是一个典型的“玄学”问题——代码看起来没毛病结果就是反复震荡。5.5 现象PyTorch 版本差异导致 Siamese 网络加载失败原因如果你拿到的是老版本 PyTorch 保存的模型torch.load 时可能遇到 keys 名称不匹配尤其是经过 nn.DataParallel 包装过的模型keys 会多出 module. 前缀。解决加载时加 strip_prefix 逻辑把 module. 前缀去掉。另外代码包里给的是距离特征而不是模型权重所以这类问题更多出在你自己复现训练的时候。记住加载任何模型权重的第一件事是打印 state_dict 的 keys而不是直接塞进网络。# 兼容 DataParallel 保存的权重 state_dict torch.load(your_model.pth, map_locationcpu) if next(iter(state_dict.keys())).startswith(module.): state_dict {k.replace(module., ): v for k, v in state_dict.items()} # 再 load 进网络5.6 现象跑完主流程之后 AUROC 是 0.5攻击完全失败原因除了 5.3 的泄漏问题之外最常见的原因是参考样本选得太少或者选得不好。参考样本如果全部来自某一个类别比如全是高收入群体那么判别器学到的其实是“类别区分”而不是“成员区分”。解决参考样本要混合多个类别并且在采样时要打乱。代码里默认用 train_dist[2000:2050] 这种顺序切片这是稳妥但不高效的做法。更稳的方式是随机抽样得到 50 条参考样本并且保证正负样本均衡。我一般会用 random.sample 或者 torchtorch.randperm 生成一个固定随机种子让实验可复现。6. 进阶验证用 AUROC 和置信区间评估一次完整的攻击实验当 attack.py 跑通、AUROC 出来之后很多同学就停在这儿了。但如果你想在课设答辩里有底气、或者想在毕设里更进一步需要做一轮更扎实的验证。我推荐的做法是多次重复实验统计 AUROC 的均值和方差而不是只跑一次给个数字。# 多次实验验证攻击稳定性的模板 import numpy as np import torch from sklearn.metrics import roc_auc_score def run_attack_once(seed): torch.manual_seed(seed) # 加载数据 切分保证每次切分不同 train_dist torch.load(input_train_distance_adult.pth, map_locationcpu) test_dist torch.load(input_test_distance_adult.pth, map_locationcpu) # 你自己的攻击主流程 auroc attack_pipeline(train_dist, test_dist) return auroc # 跑 5 次输出均值和标准差 results [run_attack_once(seed) for seed in range(5)] print(fAUROC mean: {np.mean(results):.4f} ± {np.std(results):.4f})这一轮验证能回答“攻击是不是真的有效”这个问题。如果五次实验的 AUROC 均值在 0.8 以上、标准差小于 0.02说明攻击确实稳定捕捉到了成员痕迹如果标准差很大说明你的攻击流程里某个环节对随机性敏感需要回去检查参考样本抽样或判别器训练过程。再进一步你可以把攻击当“防御评估工具”用先加一个简单的差分隐私扰动比如在梯度上加高斯噪声再重新生成距离特征看 AUROC 有没有下降。下降越多说明防御效果越好。这也是很多学术论文里评估联邦学习隐私保护的标准套路。如果分数一直上不去检查顺序优先级是数据切分泄漏 参考样本选取 孪生网络容量 判别器过拟合。从那以后我每次做攻击实验都强制走一遍“先探针加载 pth、再打印数据分布、最后跑主流程”的流程宁可多花五分钟确认数据没问题也不愿意跑完才发现是文件路径引用错了。希望帮到你。本文还有配套的精品资源点击获取