简介:面向计算机相关专业在校生与企业开发者的联邦学习攻击预防论文复现项目,基于Python实现,配套完整源码、文档说明与运行方法,可支撑毕业设计、课程设计或项目初期演示。压缩包共184个文件,含109个Python脚本、14个YAML配置、12个XML描述、7个Shell脚本及若干Markdown说明文档,整体仅391KB,轻量但结构完整。代码均经测试通过并成功运行,在答辩评审中取得较好成绩,适合作为学习联邦学习安全方向的入门实例,也可在此基础进行二次扩展。已有171人学习浏览,内容涉及模型训练、攻击检测与防御等模块,配置与脚本分类清晰,便于快速定位关键逻辑。对于基础薄弱的使用者,压缩包内的README及文本说明可帮助理解运行流程,提升上手效率。
1. 联邦学习攻击预防的论文复现,实际是在复现什么
拿到“联邦学习攻击预防”方向的毕业设计,很多人第一反应是:源码都有了,跑通不就行了吗?真正上手会发现,这类项目的代码复现难度不在 Python 语法,而在“攻击”和“防御”两套逻辑互相咬合。攻击要制造模型精度下降或后门行为,防御要压制攻击而不伤正常精度,两份 Python 源码能不能形成有效对照,才是论文复现的核心。
这类毕业设计通常沿着一条主线展开:在一个联邦学习框架中,先指定若干恶意客户端执行投毒,再用某种防御策略做鲁棒聚合,最后比对防御前后的模型精度、攻击成功率、通信轮次等指标。你需要复现的不是某个库的 API 调用,而是论文实验里“基线、攻击、防御、评估”四段式流程。
下面内容按“攻击面梳理 → 源码结构 → 参数联动 → 验证技巧”的顺序展开。新手能照着跑通最小例子,熟手可以借这套思路快速定位复现结果对不上的根因。
2. 先分清攻击面:联邦学习里的投毒、后门与梯度窃取
2.1 三类常见攻击对训练流程的影响
联邦学习攻击预防相关的论文里,攻击手段几乎都落在三个方向。第一是数据投毒,恶意客户端修改本地标签或样本,最常见的是标签翻转,把类别 1 的图片全标成类别 7。第二是模型投毒或梯度投毒,恶意客户端直接修改本地更新,让全局模型朝攻击目标偏移,典型实现是“梯度上升”或“放大倍数”。第三是梯度窃取,服务器或恶意参与方从上传梯度反推训练样本,严格说它不是破坏模型,而是泄露隐私。
三种攻击对训练流程的表现完全不同,防御策略也不一样。
| 攻击类型 | 作用位置 | 主要表现 | 常见防御思路 |
|---|---|---|---|
| 标签翻转 | 本地训练数据 | 主任务精度下降,类别混淆 | 客户端筛选、数据统计过滤 |
| 模型/梯度投毒 | 本地更新 | 模型方向被带偏,后门触发 | 鲁棒聚合、范数裁剪 |
| 梯度窃取 | 上传梯度 | 训练数据被重构 | 梯度扰动、差分隐私 |
先判断论文实验里是“有目标攻击”还是“无目标攻击”。如果是无目标攻击,评价指标通常看主任务精度;如果是有目标后门攻击,要同时看主任务精度和后门成功率。复现代码时,攻击注入的位置决定你要改哪个文件。
2.2 从论文实验设置反推需要复现的代码模块
一篇联邦学习攻击防御论文的实验设置,通常包含数据集划分方式、客户端总数、每轮参与客户端数、攻击比例、防御方法、聚合规则、通信轮次。这些信息会直接映射到源码里的五个模块:数据划分、客户端本地训练、服务器聚合、攻击注入、防御过滤。
常见做法是先用一个配置文件把实验参数集中起来,这样复现不同论文设置时不需要改业务代码。下面是一个很典型的配置骨架:
# config.py from dataclasses import dataclass @dataclass class FLConfig: dataset: str = "mnist" num_clients: int = 10 # 参与方总数 clients_per_round: int = 5 # 每轮被选中客户端数 num_rounds: int = 30 malicious_ratio: float = 0.4 # 恶意客户端比例 attack_type: str = "label_flip" # label_flip / model_poison defense: str = "trimmed_mean" # none / krum / trimmed_mean / dp seed: int = 42参数说明:malicious_ratio是攻击强度,论文里往往画一条“防御精度随恶意比例变化”的曲线;clients_per_round对防御效果影响很大,恶意比例太高且每轮选中客户端太少时,任何鲁棒聚合都很难救回来。复现时优先跑论文的主表,再跑消融实验,不要一上来就按自己的直觉调参。
2.3 防御机制的核心位置:聚合规则与客户端筛选
防御代码的正确位置在服务器聚合阶段。无论论文用的是 Krum、Median、Trimmed Mean 还是差分隐私,本质上都是在“如何把多个客户端上传的更新合并成全局模型”这一步做文章。因此,复现的代码里聚合函数必须是独立接口,不能把防御逻辑写死在训练循环里。
# aggregation.py import torch def trimmed_mean(updates, trim_ratio=0.2): """ 对每个参数维度做截断均值聚合。 先去掉最大和最小的一部分,再取均值,能削弱极端恶意更新。 updates: list of torch.Tensor,每个元素是某个客户端的梯度或模型参数差 """ stacked = torch.stack(updates, dim=0) # (客户端数, 参数维度) trim_num = int(updates[0].numel() * trim_ratio) if trim_num == 0: return stacked.mean(dim=0) # 按绝对值排序,删除两端的异常参数 flattened = stacked.view(stacked.size(0), -1) sorted_vals, _ = torch.sort(flattened, dim=0) trimmed = sorted_vals[trim_num: stacked.size(0) - trim_num] result = trimmed.mean(dim=0) return result.view_as(updates[0])代码里的trim_ratio是防御强度参数。设成 0 就是普通平均聚合,和论文里的“无防御基线”完全对应。你不需要在第一版复现里实现论文的完整证明,只要保证“攻击存在时,防御后的精度明显高于无防御”,就说明核心逻辑已经跑通。
3. 本地搭建复现环境:Python 源码结构、依赖与运行方法
3.1 最小依赖清单和目录划分
很多存量源码来自课程作业或各处的开源示例,结构上常常把攻击、防御、训练全塞进一个文件。这种代码能跑,但没法做参数对照。我一般会先按实验语义拆目录,再开始装依赖。
| 目录/文件 | 作用 |
|---|---|
data/ | 数据下载、IID/非IID划分 |
clients/ | 客户端本地训练与攻击注入 |
server/ | 全局聚合、防御策略 |
evaluate/ | 主任务精度、后门成功率计算 |
configs/ | 不同论文实验对应的配置 |
main.py | 训练主入口 |
Python 版本建议 3.8 到 3.10,深度学习框架用 PyTorch 还是 TensorFlow 取决于源码。如果只是复现 MNIST 或 CIFAR-10 级别的实验,CPU 也能跑,只是要把客户端数和轮次调小。依赖安装不要直接全部用最新版,PyTorch 和 numpy 版本不匹配是这类源码最常见的启动失败原因。
# 创建虚拟环境并安装最小依赖 python -m venv .venv source .venv/bin/activate # 这里按源码实际依赖调整;pytorch 官方安装命令会自动匹配 cuda 版本 pip install torch torchvision pip install numpy scikit-learn matplotlib安装完成后,先跑一次python main.py --help,确认入口脚本能正常加载配置。如果这一步就报错,优先检查 Python 版本和依赖冲突,不要急着改业务代码。
3.2 按论文复现需要的三个核心文件
论文复现最重要的三个文件是数据划分、本地训练和服务器聚合。数据划分决定攻击的传播范围,非独立同分布(Non-IID)数据会让恶意更新更难被识别;本地训练放攻击注入;服务器聚合放防御。
# main.py from config import FLConfig from data import partition_data from server import aggregate def run_experiment(cfg: FLConfig): client_data = partition_data(cfg) # 每轮选出参与客户端 for rnd in range(cfg.num_rounds): selected = select_clients(client_data, cfg.clients_per_round) updates = [] for cid in selected: local_update = client_train(cid) if is_malicious(cid, cfg): local_update = inject_attack(local_update, cfg.attack_type) updates.append(local_update) global_update = aggregate(updates, defense=cfg.defense) apply_global_update(global_update) eval_and_log(rnd)参数说明:client_train返回的是模型参数增量,不是模型本身,这样的接口设计能让你自由组合不同攻击和防御。如果你的源码把“参数更新”和“完整模型”混在一起,后面对比 Krum 和 Trimmed Mean 时会非常痛苦。
如果论文额外做了通信压缩,比如偏置压缩,通常是把压缩放在本地更新上传前,它属于通信优化层,和防御聚合不在同一位置,不要混在一起。
3.3 跑通最小数据集的完整运行命令
复现的第一步不是跑全量实验,而是把“1 个恶意客户端 + 默认防御”的最小流程跑通。不要一上来就复现论文图表,先确认 loss 能降、攻击能升、防御能压。
# 最小命令:10 个客户端,每轮 5 个,只跑 5 轮 python main.py \ --num_clients 10 \ --clients_per_round 5 \ --num_rounds 5 \ --malicious_ratio 0.2 \ --defense trimmed_mean \ --seed 42如果输出日志里能看到每一轮的 loss 和精度,说明代码链路已经完整。这一步不要纠结指标好不好看,先保证循环结构没有崩。之后再按论文默认参数跑 30 到 50 轮,生成第一版曲线。
4. 参数怎么设:攻击比例、恶意客户端数量与防御强度联动
4.1 攻击参数与评估指标对齐
复现结果对不上论文,八成是攻击参数没和评价指标对齐。标签翻转攻击要记录“攻击成功率”,模型投毒要记录“后门成功率”,梯度窃取要记录“重构误差”,这三者在同一份代码里不能共用一套指标函数。
| 攻击类型 | 攻击参数 | 核心指标 | 防御见效的表现 |
|---|---|---|---|
| 标签翻转 | flip_target、flip_ratio | 防御后主任务精度回升 | 精度从崩溃恢复到接近 80% |
| 后门投毒 | backdoor_label、trigger_pattern | 后门成功率下降 | 后门成功率从 95% 降到 10% |
| 梯度投毒 | poison_scale、adv_epochs | 与良性更新方向差异 | 聚合结果接近良性基线 |
不要只调malicious_ratio。恶意客户端数量和每轮参与客户端数的关系更重要,比如 20 个客户端里有 40% 恶意,但每轮只有 5 个参与,很可能这轮被选中的恶意客户端超过半数,防御失效。论文里的横坐标通常写恶意客户端的绝对数量或比例,复现时要看清它用的是“总客户端中的比例”还是“每轮参与客户端中的比例”。
4.2 防御参数的三组必调项
鲁棒聚合类防御,第一组必调参数是“截断/剪枝比例”,Trimmed Mean 的trim_ratio、Krum 的剔除数量,都跟恶意客户端比例直接相关。第二组是更新值的缩放范围,也叫范数裁剪边界。第三组是差分隐私的噪声系数,它不直接防御恶意更新,但能掩盖异常梯度模式。
# sweep.py:快速扫描攻击比例 x 防御强度 import itertools from config import FLConfig from main import run_experiment for attack_ratio, trim_ratio in itertools.product( [0.2, 0.4, 0.6], # 攻击比例 [0.0, 0.2, 0.4] # 防御强度,0 表示无防御 ): cfg = FLConfig( malicious_ratio=attack_ratio, trim_ratio=trim_ratio, defense="trimmed_mean" ) result = run_experiment(cfg) print(f"attack={attack_ratio}, trim={trim_ratio}, " f"acc={result['acc']:.2f}, backdoor={result['backdoor']:.2f}")这份代码的作用是生成一张二维对照表。如果你发现trim_ratio=0.4时正常精度也开始掉,说明防御强度已经压过正常数据,这个点就是论文里“防御过度”的位置。复现报告里能写出这个现象,比只贴最终结果更有说服力。
4.3 复现结果对不上论文时的排查路径
最常见的现象是:没有防御时攻击指标上去了,加上防御后也有效果,但数值就是和论文对不上。这时候不要怀疑自己的模型结构,先检查四个方面。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 无防御的基线精度就不对 | 数据划分方式不同 | 检查是否复现了 Non-IID 的狄利克雷分布 |
| 防御后精度几乎不变 | 防御参数没生效 | 确认聚合函数里传入了defense参数 |
| 后门成功率一直很高 | 后门触发条件错误 | 检查 trigger 的像素位置和样本宽高 |
| 多次运行结果差异大 | 随机种子没有全局设置 | 固定 Python、numpy、PyTorch、数据划分的随机种子 |
随机种子是重点。PyTorch 的 DataLoader 也要设置worker_init_fn,否则每次运行的打乱顺序不一样。还有一点容易被忽略:Non-IID 数据分布下的灾难性遗忘会放大攻击影响,客户端本地训练轮次太长,正常客户端也会产生偏向自己数据分布的更新,防御机制会把它们误判成异常。如果发现防御把正常客户端也过滤掉了,优先降低本地训练local_epochs,不要一上来就改防御阈值。
5. 用聚合层单元测试验证防御逻辑:一个可复现的基线技巧
复现联邦学习攻击防御实验,最怕的是“整个流程跑完但不知道结果对不对”。与其等 50 轮训练结束再看曲线,不如把聚合层单独拿出来做单元测试。这个技巧能让你在 5 秒内确认防御代码有没有生效,也能帮你向答辩老师说明代码的正确性。
做法是构造一组人工梯度,其中包含明显异常值,然后分别用普通平均聚合和鲁棒聚合处理,看结果差异。
# test_aggregation.py import torch from aggregation import trimmed_mean # 构造 5 个客户端的参数更新 benign_updates = [ torch.tensor([1.0, 1.0, 1.0]), torch.tensor([1.1, 0.9, 1.0]), torch.tensor([0.9, 1.1, 1.0]), ] # 恶意更新:方向完全相反,数值也放大 malicious_updates = [ torch.tensor([-10.0, -10.0, -10.0]), torch.tensor([100.0, -50.0, 20.0]), ] updates = benign_updates + malicious_updates mean_result = torch.stack(updates).mean(dim=0) trim_result = trimmed_mean(updates, trim_ratio=0.2) print("均值聚合:", mean_result.tolist()) print("截断均值:", trim_result.tolist())代码逻辑说明:恶意更新的数值和方向都被刻意放大,普通平均聚合会把全局更新拉向异常方向;trim_ratio=0.2意味着每个维度只保留中间 60% 的值,再来均值。运行后你会看到均值聚合的结果被推向负数或很大值,而截断均值依然保持在 1.0 附近。这就是防御机制“压住异常值”的直接证据。
进一步验证时,把这个测试扩展成“不同防御方法对同一组恶意更新的输出对比”,把 Krum、Median、Trimmed Mean 都跑一遍,记录各自与良性聚合结果的距离。这段输出可以直接放进毕业设计文档,比一张训练曲线更容易解释。注意测试用的张量维度要和真实模型参数维度一致,如果真实网络有偏置项,就把测试数据改成包含[weight, bias]的结构,否则你验证的只是聚合函数本身,没验证真实调用方式。
还有一件值得做的事:把防御前的指标和防御后的指标画在同一张图里,横轴用通信轮次,纵轴同时画主任务精度和后门成功率。曲线形状的重要性高于数值,论文里常见的“防御后精度波动变大但整体不崩”这个现象,只有在你保留每轮日志时才能看到。如果只记录最终结果,答辩时遇到“中间过程为什么波动”这类问题就很难回答。
把这个聚合层测试跑通后,再去碰训练循环和调参,你就会对“攻击预防”这套代码有信心。后续换数据集时,只要保证数据划分接口和聚合接口兼容,复现其他论文也只是改配置的事。
本文还有配套的精品资源,点击获取