☰
联邦学习对抗攻击实战:从FGSM到后门注入与防御
2026/10/5 16:08:29 网站建设 项目流程

简介:面向联邦学习安全研究的课程大作业实现,聚焦联邦场景下模型易受恶意攻击、隐私数据可能被推断等关键问题,提供完整Python源码、详细注释及预训练模型,适合计算机、人工智能、通信工程、自动化等专业学生作为课程设计、毕业设计或入门进阶素材,也可供教师与企业研发人员参考。资源共17个文件,压缩包仅622KB,包含10个pth模型权重(面向Adult数据集的训练/测试阶段),6个py核心脚本(覆盖攻击函数、联邦学习基础功能、连体网络、对抗成员推断攻击、模型数据初始化等),1个md介绍文档。代码从模型初始化到联邦学习流程再到对抗攻击方法形成完整链路,关键步骤均有详细注释,便于理解与调试。目前已有483人学习下载,可直接运行验证,也可在此基础上扩展实现更多攻击策略或防御机制,满足实验演示与科研探索需求。

1. 课程大作业里的联邦学习对抗攻击:它到底在验证什么

做过单机深度学习对抗攻击的人,第一次把 FGSM 或 PGD 挪到联邦学习环境里多半会翻车:本地模型上攻击成功率能到 90%,换到聚合后的全局模型上直接掉到 30%。这不是代码写错了,而是联邦学习这套“多客户端本地训练 + 服务端聚合”的流程,把攻击面和梯度分布都改掉了。你在课程作业里看到的这份“基于联邦学习模型的对抗攻击 python 源码+详细注释+模型.zip”,本质上就是让你把两件事串起来:一是理解联邦训练中模型参数的流动方式,二是把对抗攻击从集中式训练迁移到分布式训练里,并给出可量化的实验结果。适合正在做课程设计、准备毕设,或者想评估联邦模型鲁棒性的从业者——它能帮你少走半个月弯路,直接把一个能跑、能出图、能写进报告的攻击实验平台搭起来。

2. 联邦学习的攻击面:为什么本地模型攻击经验在这里会失效

2.1 三类高频攻击路径:模型投毒、后门注入与梯度逆向

联邦学习最核心的训练范式是 FedAvg:每个客户端用自己的本地数据训练若干轮,把模型参数或梯度发给服务端,服务端按样本量加权平均后更新全局模型。这个流程天然暴露了三个攻击入口。

第一是模型投毒(Model Poisoning),恶意客户端直接修改自己上传的梯度或模型权重,让全局模型朝攻击者想要的方向偏移。最常见的手法是梯度放大——把自己算出来的梯度乘以一个大于 1 的系数再上传,相当于在投票时给自己加权。对应到热搜词里的“模型中毒攻击”,指的就是这一类。第二是后门注入(Backdoor Attack),攻击者在本地训练时往训练集里混入带有特定触发图案的样本,让模型学会“看见这个图案就输出攻击者指定的标签”。后门攻击的隐蔽性比梯度投毒高,因为正常样本上的精度几乎不掉,只有带 trigger 的输入才会触发异常。第三是梯度逆向(Gradient Inversion),恶意服务端或监听者根据客户端上传的梯度反推训练数据,这在医疗、金融等隐私敏感场景里是致命的。

这三类攻击和集中式对抗攻击最大的区别在于:集中式对抗攻击是修改输入(加扰动),联邦攻击更多是修改训练过程(改梯度或改数据分布)。标题里提到的“基于联邦学习模型的对抗攻击”如果只做 FGSM 对全局模型的推理期攻击,其实只覆盖了对抗攻击的一小块;完整的课程大作业通常会把训练期投毒也纳入对比,因为联邦学习的主要风险恰恰在训练期。你拿到的源码包里,大概率两类攻击都有,只是入口不同——推理期攻击入口是测试集图片,训练期攻击入口是客户端本地数据或上传梯度。

2.2 在参数服务器上复现最小联邦环境:两个客户端也能跑通攻击

很多课程作业给的源码是在单机上模拟联邦环境,不涉及真实的多机通信。我一般建议:先用两个客户端把攻击跑通,再扩展到多客户端。这样调试梯度问题时,你能直接看到每个客户端上传了什么,避免被“平均效应”绕晕。下面是一个极简的 FedAvg 聚合核心,正常客户端上传梯度,恶意客户端上传放大后的梯度:

import copy import torch def fedavg_aggregate(client_weights, client_sizes, malicious_index=None, attack_scale=1.0): """ client_weights: list of state_dict, 每个客户端上传的模型参数 client_sizes: list of int, 每个客户端的本地样本量 malicious_index: 恶意客户端下标, None 表示无攻击 attack_scale: 梯度放大倍数, 仅对恶意客户端生效 """ total_size = sum(client_sizes) # 先深拷贝一份全局模型, 避免原地修改污染后续比较 global_weights = copy.deepcopy(client_weights[0]) for key in global_weights.keys(): # 加权平均: 全局参数 = sum(客户端参数 * 客户端样本占比) global_weights[key] = torch.zeros_like(global_weights[key]) for i in range(len(client_weights)): weight = client_weights[i][key].float() if i == malicious_index: weight = weight * attack_scale # 放大恶意梯度 global_weights[key] += weight * (client_sizes[i] / total_size) return global_weights

这段代码的逻辑很直接:服务端拿到所有客户端参数后,按样本量占比做加权平均。恶意客户端的参数先乘以attack_scale,再参与聚合。整体参数更新量放大后,全局模型就会向恶意客户端的方向偏移。这里建议先把attack_scale设为 1.0 跑一遍无攻击基线,记录全局模型在测试集上的精度;再设为 5.0 或 10.0 跑一遍攻击场景,对比精度变化。

参数说明里最值得关注的是malicious_index和attack_scale的配合:恶意客户端越多或放大倍数越大,攻击效果越强,但被发现的风险也越高。常见做法是先固定一个恶意客户端,逐步调大attack_scale,观察攻击成功率曲线何时开始攀升。如果调大到 50 以上全局模型还不受影响,说明你的聚合逻辑里可能加了梯度裁剪,或者本地训练轮数太多导致梯度被稀释,这一步排查方法在后面的避坑章节单独展开。

3. 把对抗攻击写进联邦训练流程:从 non-IID 数据到 FGSM/PGD

3.1 用 Dirichlet 分布做 non-IID 数据划分,模拟真实客户端

真实的联邦学习场景里,每个客户端的本地数据分布几乎不可能一致。比如手机输入法客户端,有的用户喜欢打英文,有的用户喜欢打中文,本地数据天然就是 non-IID 的。如果课程作业直接随机打散 MNIST 分给各客户端,攻击实验做出来的结论在真实场景里是不成立的——因为 IID 数据下客户端之间的梯度差异小,恶意攻击很容易被平均掉。

常见做法是用 Dirichlet 分布控制数据划分的均匀程度,代码我一般这样写:

import numpy as np def split_non_iid(labels, num_clients, alpha=0.5, seed=42): """ labels: 所有训练样本的标签数组, shape=(N,) num_clients: 客户端数量 alpha: Dirichlet 分布浓度参数, 越小数据分布越不均衡 """ rng = np.random.default_rng(seed) num_samples = len(labels) client_data_idx = [[] for _ in range(num_clients)] # 对每个类别单独分配样本 for class_id in np.unique(labels): class_idx = np.where(labels == class_id)[0] # 用 Dirichlet 分布生成每个客户端在该类上的样本比例 proportions = rng.dirichlet(np.ones(num_clients) * alpha) proportions = (proportions * len(class_idx)).astype(int) # 修正舍入误差, 确保样本总数对齐 proportions[-1] = len(class_idx) - proportions[:-1].sum() cursor = 0 for client_id, count in enumerate(proportions): client_data_idx[client_id].extend(class_idx[cursor:cursor + count]) cursor += count return client_data_idx

这个函数的核心是alpha参数:alpha越大,Dirichlet 分布越均匀,各客户端数据分布越接近 IID;alpha越小,每个客户端越可能只拿到少数几个类别的样本。课程实验里我建议alpha=0.1模拟极端 non-IID,alpha=1.0模拟中等偏均匀场景,各跑一组对照。真实场景里你甚至可以给不同客户端设置不同的alpha,模拟“有的客户端数据偏,有的客户端数据匀”的混合状态。

代码后面的注意点在proportions[-1]这行的修正:因为astype(int)会截断小数,直接累加会导致样本总和对不上。如果不做修正,后面做数据加载时就会报 index 越界,这是非常容易忽视的坑。

3.2 推理阶段对全局模型做 FGSM/PGD:攻击成功率如何计算

数据划分完成后,先按正常 FedAvg 流程训练一个全局模型,保存权重到model.zip对应的文件位置。接下来对全局模型做推理期对抗攻击,最常用的是 FGSM 和 PGD。FGSM 是一步攻击,速度快但效果有限;PGD 是多步迭代攻击,每步先算梯度再做投影,效果更稳。

下面这段是 FGSM 攻击的核心实现,可以直接放进训练脚本里调用:

def fgsm_attack(model, images, labels, epsilon=0.3): """ model: 已经加载好权重的全局模型 images: 输入图像批次, 范围 0~1 labels: 真实标签 epsilon: 最大扰动幅度 """ model.eval() images.requires_grad = True outputs = model(images) loss = torch.nn.functional.cross_entropy(outputs, labels) # 关键一步: 反向传播得到梯度, 只保留梯度方向 model.zero_grad() loss.backward() grad = images.grad.data # 在梯度方向上添加扰动, 并裁剪回合法范围 adv_images = images + epsilon * grad.sign() adv_images = torch.clamp(adv_images, 0, 1) return adv_images

FGSM 的关键点在grad.sign():它只取梯度的符号,也就是“往哪个方向扰动让 loss 变大”,而不关心梯度的大小。epsilon是扰动幅度上限,在 MNIST 上通常取 0.1 到 0.3,在 CIFAR-10 上取 0.01 到 0.05 就足够明显。如果epsilon太大,攻击成功率虽然高,但生成图像肉眼看得出明显噪点,容易被防御手段识别,攻击的隐蔽性就没了。

PGD 则是在 FGSM 的基础上迭代多步,每步走一个小步长alpha,总共迭代num_steps次,每步结束后把扰动投影回以原图为中心、半径epsilon的球内:

def pgd_attack(model, images, labels, epsilon=0.1, alpha=0.01, num_steps=40): """ 多步迭代攻击, 比 FGSM 更稳定, 攻击成功率更高 """ model.eval() adv_images = images.clone().detach().requires_grad_(True) for _ in range(num_steps): outputs = model(adv_images) loss = torch.nn.functional.cross_entropy(outputs, labels) model.zero_grad() loss.backward() grad = adv_images.grad.data # 先走一小步, 再把总扰动投影回 epsilon 球内 adv_images = adv_images + alpha * grad.sign() delta = adv_images - images delta = torch.clamp(delta, -epsilon, epsilon) adv_images = torch.clamp(images + delta, 0, 1) adv_images = adv_images.detach().requires_grad_(True) return adv_images

PGD 的参数选择比 FGSM 讲究:alpha一般取epsilon / num_steps的 3 到 5 倍,比如epsilon=0.1、num_steps=40时alpha=0.01比较合适。如果alpha太大,攻击会振荡不收敛;太小则迭代步数要跟着增加,实验时间成倍拉长。攻击成功率的计算方式是:对测试集逐批次生成对抗样本,统计模型预测结果与真实标签不一致的比例,除以总样本数。

这里有一个课程作业里常见的误区:攻击成功率算的是“模型把对抗样本分错的比例”,不是“对抗样本与原始样本的差异大小”。很多人把 PSNR(峰值信噪比)当攻击指标,写进报告会被老师追问——PSNR 衡量的是扰动可见性,攻击成功率衡量的是有效性,两者一个都不能少,但作用完全不同。

3.3 训练阶段注入后门触发器:毒化样本的构造与位置

推理期攻击和训练期后门攻击在源码里通常是两个模块。后门攻击的做法是在客户端的本地训练数据里混入一部分带有触发器的样本,这些样本的标签被改成攻击者指定的目标标签。核心问题有三个:触发器长什么样、混入比例是多少、如何保证正常任务精度不崩。

我见过最简单的触发器写法是在图像右下角贴一个固定的小方块,比如 5x5 的白色像素块,然后把这个图和目标标签一起放进某个客户端的训练集。以下是一个构造后门样本的代码片段,用在了源码包的backdoor.py里——注意,这里不能只改训练集,还得改对应标签,否则模型学不到 trigger 和 label 的关联:

def add_backdoor_pattern(images, pattern_size=5, target_label=0): """ images: 原始图像批次, 假设尺寸为 (B, C, H, W), 值范围 0~1 pattern_size: 触发图案的边长 target_label: 攻击目标标签, 这些样本都会被改成该标签 """ b, c, h, w = images.shape adv_images = images.clone() # 在右下角画一个方形触发器, 颜色为白色 adv_images[:, :, h-pattern_size:h, w-pattern_size:w] = 1.0 # 标签统一改成目标标签 labels = torch.full((b,), target_label, dtype=torch.long) return adv_images, labels

后门攻击的参数重点是pattern_size和注入比例。pattern_size太大容易被检测到——用肉眼扫一眼就能看出图片被改过;太小模型又学不到。MNIST 上 5x5 合适,CIFAR-10 上 8x8 比较合理。注入比例建议控制在 5% 到 15% 之间:低于 2% 模型根本学不到 trigger 和 label 的关联,高于 20% 主任务精度会明显下降。源码注释里要标清楚,这里修改的是“恶意客户端”的数据,不能在所有客户端上都混入触发器——如果全部客户端数据都种后门,训练结束后的模型对所有输入都带偏,攻击痕迹太明显。

还有一个容易被忽略的点:后门攻击的效果评估不能只看测试集整体精度,必须单独构造一个“带 trigger 的测试子集”,统计模型在这个子集上的分类成功率(后门攻击成功率)。如果这个成功率高但正常测试集精度低,说明攻击把模型搞坏了;最理想的状态是两边都高——正常样本上模型表现如常,只有带 trigger 的样本被“定向引爆”。

4. 联邦对抗攻击的 5 个常见坑:模型不收敛、攻击失效、梯度被裁剪

4.1 攻击无效与模型不收敛的排查记录(现象 → 原因 → 解决)

坑一:FGSM 攻击成功率在全局模型上远低于本地模型。

现象:在单个客户端本地模型上跑 FGSM,攻击成功率 80% 以上;换成服务端聚合后的全局模型,同样的epsilon降到 20%。

原因:FedAvg 聚合后的模型参数是多个客户端模型的加权平均,决策边界被“平滑”了。原本单个模型在某张图上有很尖锐的梯度方向,平均后梯度方向被多个模型的梯度抵消,一步 FGSM 的攻击效果自然变差。

解决:先用 PGD 替代 FGSM,多步迭代能累积扰动;如果仍不够,把epsilon上调 30% 到 50% 再试。注意这只是实验层面的调参,不代表真实攻击有同样的效果。

坑二:梯度放大后全局模型 loss 直接炸掉,不收敛。

现象:把恶意客户端梯度放大 20 倍后,全局模型在测试集上的 loss 变成 NaN。

原因:放大倍数过大,聚合后的参数更新一步跨出太远,超出了模型参数空间的稳定区域。这在优化里等价于学习率突然放大 20 倍,不炸才怪。

解决:不要一次到位,从 2 倍、5 倍、10 倍逐步试。每个倍数下训练完整轮次,记录全局模型 loss。也可以配合梯度裁剪做 Clipping,但注意防御场景下裁剪会削弱攻击效果,实验设计时要分别跑。

坑三:non-IID 数据下客户端本地模型灾难性遗忘。

现象:客户端本地训练 5 个 epoch 后,服务端聚合得到的全局模型在某个类别上的准确率特别低。这对应热搜词里“灾难性遗忘 联邦学习”的场景。

原因:极端 non-IID 下,每个客户端的本地数据只有少数几个类别,本地训练几个 epoch 后模型朝自己的数据分布偏移,把之前学到的其他类别知识覆盖掉了。聚合时“少数类别”的客户端更新量小,在全局模型里被多数客户端淹没,形成对某些类的系统性遗忘。

解决:调低本地训练轮数(从 5 降到 2 或 1),调低本地学习率(0.01降到0.001),或者增大 Dirichlet 的alpha让数据分布不那么极端。课程实验里换数据分布重跑是最省事的做法。

4.2 后门攻击的隐蔽性指标:精度崩了等于暴露攻击

坑四:后门攻击“成功”但主任务精度骤降,一看就是被攻击过的。

现象:注入比例 30%,带 trigger 的测试子集分类成功率 99%,但干净测试集精度从 95% 掉到 70%。

原因:注入比例太高,模型把大量权重用于拟合 trigger 特征,正常特征被挤占。后门攻击的隐蔽性就在于“干净样本上不乱来”,主任务精度骤降等于告诉防御方有异常。

解决:把注入比例降到 10% 以内,并检查 trigger 的图案是否太明显——白色方块在灰度图上很显眼,可以考虑用亮度很低的小像素块或者细线状图案,让肉眼不容易察觉。

坑五:恶意客户端的攻击梯度被其他客户端“平均”掉了。

现象:只有一个恶意客户端,attack_scale设为 5,攻击结果和没有攻击时几乎一样。

原因:如果正常客户端数量多且本地数据量大,恶意客户端的梯度在 FedAvg 加权平均中占比很小,放大 5 倍根本不够改变聚合结果。

解决:要么增加攻击者数量(常见做法是模拟 10 个客户端中 2 到 3 个恶意),要么提高attack_scale。但注意:数量和放大倍数同时上调很容易触发坑二,所以推荐“少量多次”策略——恶意客户端多轮持续上传放大梯度,而不是一轮上传极大梯度。“多轮累积攻击”比“单轮暴力攻击”效果更稳,也更难被检测。

5. 从攻击到防御:差分隐私、梯度裁剪与鲁棒聚合的验证方法

5.1 三种防御手段的原理与推荐参数

攻击实验做完,课程大作业里通常还要补一个防御对比表,用它来说明“这个攻击在防御手段面前还奏不奏效”。联邦学习里最常见的三种防御手段各有利弊。

梯度裁剪是最简单的防御:服务端收到客户端上传的梯度或参数后,先计算它的 L2 范数,如果超过阈值就按比例缩放回去。这样恶意客户端就算把梯度放大到 100 倍,裁剪后也会被拉回正常量级。常见的做法是把裁剪阈值设为正常客户端梯度范数的中位数或 90 分位数。MNIST 上我一般取 1.0 到 5.0,CIFAR-10 上取 0.5 到 2.0。需要注意裁剪会拖慢正常训练收敛速度,因为正常的大梯度也被限了幅。

差分隐私(DP)在联邦里最常用的实现是梯度加噪:每个客户端在本地梯度上加上满足差分隐私的高斯噪声,再上传。噪声的尺度由隐私预算epsilon和敏感度决定,epsilon越小噪声越大,隐私保护越强,模型精度掉得越厉害。课程作业里epsilon取 4 到 10 比较平衡,既能挡住一定程度的梯度逆向攻击,又不会让模型精度崩到没法交差。

鲁棒聚合是对 FedAvg 的直接改造。FedAvg 用的是加权平均,鲁棒聚合改用中位数、截尾平均或 Krum 算法,把“离群”的客户端参数直接剔除或降权。Krum 的实现逻辑是:对每个客户端,计算它和其他所有客户端的参数距离平方和,选距离和最小的那个客户端作为全局模型的来源。这个算法能直接干掉梯度放大攻击,但只保留一个客户端意味着信息利用率低,模型收敛会变慢。做实验的时候,我会把 Krum 的“保留客户端数量”设为全部客户端数量的 30% 以内,效果比较明显。

5.2 用防御后的模型跑一套攻击结果对比表

实验的核心是对比“无防御/单防御/组合防御”三组配置下的攻击效果,每组都要记录两个维度:攻击成功率(越高说明攻击越有效)和主任务精度(越低说明防御对正常性能的损失越大)。下面是 MNIST 上典型的实验结果结构:

防御配置主任务精度FGSM 攻击成功率后门攻击成功率
无防御98.5%42.0%99.2%
梯度裁剪(阈值 2.0)97.8%38.5%97.5%
差分隐私(ε=5)96.1%31.2%89.0%
Krum 鲁棒聚合95.6%29.8%12.3%

看这张表的口径:FGSM 攻击成功率是本实验自己手写的对抗样本测出来的,不是某个现成工具自动生成的;后门攻击成功率是单独构造带 trigger 测试子集算出来的。Krum 对后门攻击的抑制效果通常最明显,因为它直接不允许“像恶意客户端”的参数进入聚合;但它的主任务精度下跌也最严重,因为只有一个客户端被保留,信息多样性打折扣。

做防御实验时的验证顺序建议是:先无防御跑出基线,再加单一防御,最后叠加防御。叠加时注意参数之间的耦合——比如梯度裁剪的阈值设得很小,再叠差分隐私加噪,模型精度可能会掉到 90% 以下,这不是因为某一种防御不好,而是组合防御的噪声预算叠加导致过拟合压力过大。遇到这种情况,把差分隐私的epsilon调大(比如从 4 调到 10)再观察,往往能找回部分精度。

6. 把源码包变成自己的实验平台:一个最小替换攻击的完整闭环

源码包拿到手,核心价值不是跑通一遍,而是能把它改造成自己的实验平台。我建议的第一步永远是“替换攻击对象”:把里面的受害者模型从 MNIST 换到你自己任务的小模型上。具体做法是先加载model.zip里的全局模型,用 FGSM 跑一遍原始数据得到基线攻击成功率;然后把聚合循环里的模型结构替换成你自己的网络(比如一个两层卷积),保持数据和攻击代码不动,重跑一遍。如果攻击成功率变化太大,不要急着调epsilon——先检查替换后的模型是否正常收敛,很多人换模型后连干净测试集精度都没验证就直接攻击,得出来的低攻击成功率其实是模型没训练好的结果。

跑通之后,用热词里提到的“灾难性遗忘”场景做一轮压力测试:把alpha从 1.0 调到 0.1,看后门攻击成功率是否会因为数据分布极端而飙升。我个人的习惯是把每次实验的epsilon、alpha、客户端数量、恶意客户端占比、攻击成功率、主任务精度全部落成一行 CSV 记录,而不是只记最后一张图。这样报告里写“为什么攻击成功率在不同参数下波动”,你随时有数据能查,不用返工重跑。经历过几次重跑十几个小时实验的教训后,我深刻体会到:攻击实验的结果极其依赖随机种子和数据划分,同样的参数,换一个seed攻击成功率能差 10 个百分点,不落日志等于没做实验。

代码注释是这份作业的核心交付物之一。写注释时不要抄公式原文,而是写“为什么这里要 sign、为什么 PGD 每步要 clamp、为什么后门样本不能所有客户端都注入”,这样你答辩被问到的时候能直接讲出设计动机。把整个实验流程做成一个 shell 脚本串起来:数据划分 → 攻击注入 → 聚合训练 → 对抗样本生成 → 指标计算 → 绘图,每一步的输入输出都是文件,中间任意一步失败都能从断点续跑。

这个方向值不值得继续投入,我的看法是:联邦学习对抗攻击的产出不仅是攻击成功率数字,更是你对分布式训练中“信任边界”的理解。攻击者能做什么、防御者怎么识别、参数怎么选会改变平衡——这套思维在真实系统里照样适用。很多人的第一个攻击实验跑完就删了,我更建议把它整理成你的鲁棒性测试工具,后续跑任何联邦模型都先拿它过一遍。希望这些踩坑记录能帮你少走点弯路,把这份大作业做成真正属于自己的实验平台。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询