ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

联邦学习实验项目拆解:从FedAvg到FedOur的对比与实现

联邦学习实验项目拆解:从FedAvg到FedOur的对比与实现 简介基于Python的联邦学习实验资源包聚焦三个典型对比场景在Cifar-10上比较FedAvg、FedPer、FedRep与自定义方法的准确率和目标损失利用MedMNIST数据集验证不同客户端数量下的效果使用Chest X-Ray Images数据集对比全局模型与本地训练的表现。资源面向计算机相关专业学生、老师及企业开发者适合作为毕设、课程设计或入门进阶的参考。包内共43个文件以14个Python源码、18个实验图表、5个XML配置及README文档为主整体仅631KB目录结构清晰便于快速定位训练、聚合、测试等模块。已有224人学习下载。压缩包提供完整训练与测试脚本、各实验结果图覆盖常用联邦学习算法对比代码均测试运行成功可在此基础修改扩展也可用于毕业设计、课程作业的初期演示。1. 一个联邦学习毕设项目的拆解三组实验到底在验证什么拿到federal-learning-experiment-master这个源码包时我第一反应是它的目录和普通分类项目很不一样既有标准 PyTorch 的dataset.py、options.py又多出FedOur_LocalUpdate.py、FedOur_Aggr.py、transfer.py这类明显带有联邦学习印记的文件。它的核心不是单个模型而是三组对比实验Cifar-10 上的算法横向比较MedMNIST 上的客户端数量扩展以及 Chest X-Ray 上的迁移效果验证。对做毕设或课程设计的人来说这个包真正有价值的地方在于它把 FedAvg、FedPer、FedRep 和自定义的 FedOur 放在同一套代码框架里让你能直接观察不同个性化策略在准确率和目标损失上的差异。本文不打算逐行解读全部源码而是按“代码链路 → 实验一 → 实验二 → 实验三 → 跑通技巧”的顺序把这条联邦学习实验脉络拆清楚。2. 代码核心链路FedOur的LocalUpdate与Aggr如何协同2.1 从dataset.py到options.py数据切分与训练参数准备联邦学习实验的第一步是把同一个数据集切分成多个客户端的数据。这个项目里dataset.py承担了这部分工作。常见的做法是为每个客户端分配一个索引数组训练时用torch.utils.data.Subset取出对应子集。如果模拟非独立同分布non-IID则需要先按标签排序再按分片划分给不同客户端def split_noniid(dataset, num_clients, shards_per_client2): labels torch.tensor([y for _, y in dataset]) sorted_indices torch.argsort(labels) shard_size len(dataset) // (num_clients * shards_per_client) shards [sorted_indices[i:i shard_size] for i in range(0, len(sorted_indices), shard_size)] client_indices [] for i in range(num_clients): indices [] for s in range(shards_per_client): shard_idx (i * shards_per_client s) % len(shards) indices.extend(shards[shard_idx].tolist()) client_indices.append(indices) return client_indices这段代码将整个数据集按标签排序后切成若干分片每个客户端拿两个分片从而保证客户端之间的标签分布有明显差异。参数num_clients直接控制客户端总数shards_per_client控制每个客户端持有多少个分片值越小数据倾斜越严重。实际使用时Cifar-10 实验通常设置 10 个客户端而 MedMNIST 实验会切换到 10、50、100 三档来观察规模影响。options.py是另一个入口它用argparse集中管理所有运行参数。我在本地跑时通常这样释放参数python FedOur.py --dataset cifar10 --algorithm FedAvg --num_clients 10 --local_ep 5 --rounds 100这里的--algorithm可以替换成FedPer、FedRep、FedOur等项目会在训练开始前动态决定使用哪个本地更新类和聚合逻辑。--local_ep控制每轮本地迭代轮数--rounds控制全局通信轮数这两者共同决定总训练量是调参时最先要确定的值。2.2 FedOur_LocalUpdate.py客户端本地训练怎么实现FedOur_LocalUpdate.py是每个客户端的训练入口。它接收全局模型参数、本地数据集和超参数在本地用 SGD 迭代若干轮后返回更新后的模型状态。一个经典 FedAvg 的本地更新函数大致长这样class LocalUpdate: def __init__(self, idx, dataset, global_model, args): self.idx idx self.dl DataLoader(dataset, batch_sizeargs.local_bs, shuffleTrue) self.model copy.deepcopy(global_model) self.opt torch.optim.SGD(self.model.parameters(), lrargs.lr, momentum0.9) self.args args def train(self): self.model.train() for epoch in range(self.args.local_ep): for data, target in self.dl: self.opt.zero_grad() output self.model(data) loss F.cross_entropy(output, target) loss.backward() self.opt.step() return self.model.state_dict(), len(self.dl.dataset)逻辑说明每次通信轮次开始时服务端把全局模型下发到参与客户端客户端用copy.deepcopy保留副本在本地数据上执行多个 epoch。返回的是模型state_dict而不是梯度这样可以减少传输数据量。参数local_ep和local_bs分别表示本地训练轮数和批次大小它们的取值直接影响模型在客户端之间的漂移程度。在 FedOur 的实现中本地更新通常会区分“全局基本层”和“个性化层”。我会在返回时把两部分分开处理基本层参与后续的跨客户端平均而个性化层保留在本地。这与 FedPer 的做法一致也是后面实验一对比的核心变量。2.3 FedOur_Aggr.py服务端聚合与全局模型更新服务端聚合在FedOur_Aggr.py中完成。经典 FedAvg 的聚合就是对所有客户端的参数做加权平均权重由本地样本占比决定def aggregate(global_model, client_weights, client_datasizes): total sum(client_datasizes) global_dict global_model.state_dict() for key in global_dict: global_dict[key] torch.zeros_like(global_dict[key]) for weight, size in zip(client_weights, client_datasizes): for key in weight: global_dict[key] weight[key] * (size / total) global_model.load_state_dict(global_dict) return global_modelclient_weights是每个客户端上传的模型参数字典client_datasizes是用于加权的数据量。这里要特别注意如果客户端参与训练的数据量不均衡加权平均可以避免大客户端被小客户端稀释。代码中size / total就是权重系数它必须在所有客户端的参数上保持一致否则会出现部分 key 更新不充分的问题。FedOur 的聚合逻辑与 FedAvg 的区别在于它会对基本层和个性化层分别处理基本层采用加权平均个性化层则只在对应客户端内部用不跨客户端合并。这样做的好处是服务端始终只维护一套基本表示各个客户端的分类头可以保留自身的分布偏好。2.4 我是怎么理解FedOur与FedAvg差异的从代码结构上看FedOur 不是彻底推翻 FedAvg而是在其骨架上增加了“分层处理”的机制。FedAvg 所有层都被平均FedPer 只保留本地分类头FedRep 则把低维表示和分类器分开训练。FedOur 在这条思路上多了一层它用全局模型提供基础特征又用本地偏置补偿客户端分布差异。所以你在运行时会看到FedOur_LocalUpdate.py里除了普通特征提取还会计算一个偏置项来修正本地输出。这种设计的代价是实现复杂度更高但在非独立同分布数据上它通常比 FedAvg 收敛更稳这也是实验一那张cifar-10-loss.png里 FedOur 曲线下降更快的原因。3. Cifar-10上的基准对比FedAvg、FedRep与FedOur的准确率与损失曲线怎么读3.1 五个对比项的设置FedAvg、FedPer(Classify)、FedPer(Classify1 Block)、FedRep、FedOur实验一的对比矩阵在test.py和FedOur.py中定义。五个方法的核心区别可以归纳为一张表方法全局共享层本地个性化层额外模块FedAvg全部层平均无无FedPer (Classify)特征提取层分类头无FedPer (Classify1 Block)部分特征层最后一个 Block 分类头无FedRep学习低维表示仅在本地训练分类器无FedOur全局基本层个性化层 本地偏置元迁移偏置这张表描述了每个方法的“共享-私有”边界。FedPer(Classify) 只保留分类头个性化而 FedPer(Classify1 Block) 则把最后一个残差块也划入个性化范围显然是给客户端更多表达空间。FedRep 的做法是服务端只负责表示部分本地随意迭代分类器。FedOur 则多加了一个偏置向量用来捕捉客户端数据分布的一阶矩差异。3.2 命令行怎么跑实验一python FedOur.py --dataset cifar10启动实验一不需要修改任何代码直接用FedOur.py传入算法名即可python FedOur.py --dataset cifar10 --algorithm FedAvg --rounds 200 --local_ep 1 python FedOur.py --dataset cifar10 --algorithm FedPer --rounds 200 --local_ep 1 python FedOur.py --dataset cifar10 --algorithm FedRep --rounds 200 --local_ep 1 python FedOur.py --dataset cifar10 --algorithm FedOur --rounds 200 --local_ep 1参数中的--rounds 200表示全局通信 200 轮--local_ep 1表示每轮本地只更新一个 epoch。这里用 1 是为了控制变量本地迭代越少全局模型对客户端分布的包容性越关键个性化方法的优势更容易被凸显。如果想加快收敛可以适当增大--lr我常用初始学习率 0.01配合 Momentum 0.9每 50 轮乘以 0.5 衰减。运行结束后项目会把每轮的测试准确率和损失值记录到日志并生成cifar-10-acc.png、cifar-10-loss.png、cifar-10-detail-acc.png等图片。这些图不是装饰而是用来判断哪个方法最早越过阈值、哪个方法在训练末期还在摆动。3.3 从cifar-10-loss.png和acc.png里能看出什么看这类曲线时我先看“横向可比性”同一数据集、同一客户端划分、同一轮数下不同算法的最终准确率阶梯。通常 FedAvg 在 10 个客户端、标签非独立同分布的情况下准确率会低于 FedPer 和 FedOur。原因在于 FedAvg 强制所有客户端共享同一个分类头而每个客户端本地标签分布明显不同梯度更新相互冲突。FedRep 会好一些因为它把表示学习与分类解耦。FedOur 如果最终 loss 最低、acc 最高说明它的偏置机制确实捕捉到了客户端之间的系统性偏移。还有一点值得看训练初期的 loss 曲线形状。FedOur 的第一轮 loss 往往会比 FedAvg 高一点因为额外偏置没有被充分训练需要一两轮适应。如果这个适应阶段没有出现反而可能意味着偏置初始化得太靠近零失去了修正作用。我在复现时遇到过一次 FedOur 和 FedAvg 曲线几乎重合的情况检查后发现是本地学习率太大偏置项在一轮内就被正则项拉回零附近。把--lr从 0.01 降到 0.003 后两条曲线才真正分开。4. MedMNIST客户端数量实验10、50、100个client的收敛差异4.1 MedMNIST数据集特点与实验目的实验二切到 MedMNIST具体涉及dermamnist和bloodmnist两个子集。前者是皮肤镜图像类别不多但类别间形态差距大后者是血细胞分类样本相对均匀。这个实验要回答的问题很实际当客户端数量从 10 涨到 50、100 时每个客户端本地数据变少通信轮次变多不同算法的表现如何变化。MedMNIST 数据源是medmnist这个 Python 包dataset.py里会判断数据集名并调用对应的DermaMNIST、BloodMNIST类。这些图像尺寸小28x28输入通道数和类别数都与 Cifar-10 不同因此在options.py里要单独维护输入维度和类别数映射DATASETS { cifar10: (3, 32, 10), dermamnist: (3, 28, 7), bloodmnist: (3, 28, 8), }4.2 客户端数量如何影响每轮本地迭代与通信轮次客户端数量变多以后最直接的变化是每个客户端分到的样本数变少。假设dermamnist训练集有 7000 张图10 个客户端时每个客户端 700 张100 个客户端时只剩 70 张。如果本地 epoch 数不变模型在客户端内只能看到非常少的样本过拟合风险显著增加。项目里对这种场景的处理一般是通过sampling.py控制每轮实际参与通信的客户端比例。举个例子python FedOur.py --dataset dermamnist --num_clients 100 --frac 0.1 --rounds 300这里的--frac 0.1表示每轮随机挑选 10 个客户端参与更新而不是让 100 个客户端全部参与。这样做有两个好处一是减少通信开销让单轮时间可控二是引入随机性变相增加了每轮更新的多样性。当num_clients从 10 变为 50 或 100 时建议同步调低--frac否则服务端每轮聚合的梯度来源太少模型容易在早期震荡。4.3 从dermamnist和bloodmnist的曲线看客户端规模效应运行多个客户端数量后dermamnist_10clients_acc.png、dermamnist_50clients_acc.png、dermamnist_100clients_acc.png这组图会呈现出明显的规律性。10 个客户端时模型一般在 50 轮内就能达到较高准确率曲线平稳50 个客户端时收敛轮数变长且偶尔出现准确率突然下降又恢复的尖峰到 100 个客户端时如果本地学习率没调loss 曲线甚至会短时冲高。原因在于客户端数量增加后全局模型每轮接收到的更新来自更分散的分布每个客户端的本地风险面差异变大。FedAvg 在这种情况下容易产生“灾难性遗忘”特征新一批客户端更新的参数会覆盖掉之前学到的全局知识。FedOur 因为有一层额外的偏置校正能在聚合后重新对齐全局表示所以 100 客户端场景下它的损失上升幅度比 FedAvg 小。我建议在看这组图时重点比较 100 客户端那两张图的收敛速度而不是只看最终准确率——最终准确率相差 2% 以内都正常但收敛速度决定了这个算法是否适合真实的大规模设备网络。5. Chest X-Ray上的局部微调全局基本层与Meta-Transform的迁移效果5.1 实验三的对比基线FedAvg的全局模型、Local训练、FedOurMeta-Transfer实验三使用 Chest X-Ray Images 数据集聚焦迁移场景。三个对比基线分别是由 FedAvg 训练得到的全局模型直接用于测试每个客户端只用自己的本地数据训练的模型FedOur 训练出的全局基本层再经过transfer.py中的元迁移模块做一次针对性迁移。这里的逻辑很清晰在医学影像上一个客户端很难积累足够大的标注集纯本地训练容易过拟合直接用全局模型又无法适应本地设备的图像采集差异。FedAvg 全局模型作为一种预训练初始化本质上是“参数起点”FedOur 的全局基本层则更聚焦于可迁移的特征因此transfer.py主要承担“把全局基本层适配到目标客户端”的角色。5.2 transfer.py与全局基本层参数冻结与迁移transfer.py的实现核心是参数复用。我通常的做法是先加载预训练好的全局基本层再在目标数据上微调最后的分类层同时允许少量全局层参数继续更新def transfer(global_model, local_train_loader, local_test_loader, args): model copy.deepcopy(global_model) for name, param in model.named_parameters(): if classifier not in name: param.requires_grad False optimizer torch.optim.Adam( [p for p in model.parameters() if p.requires_grad], lrargs.transfer_lr) model.train() for epoch in range(args.transfer_epochs): for data, target in local_train_loader: optimizer.zero_grad() loss F.cross_entropy(model(data), target) loss.backward() optimizer.step() return model参数冻结的逻辑是先把所有层参数设成不可训练再只对名字含classifier的参数开启requires_grad True。这样能保留全局基本层学到的通用结构只让分类头适应当前客户端的输出分布。--transfer_lr一般设得比训练阶段低比如 0.001防止大范围破坏全局基本层。--transfer_epochs控制在 5 到 10 之间太多会陷入本地数据过拟合太少又不足以校准偏置。5.3 fine-tune-test-acc.jpg 的结果怎么解读根据项目输出图fine-tune-test-acc.jpg和fine-tune-test-loss.jpg你可以设置下面这个测试方案来还原实验三python transfer.py --dataset chestxray --algorithm FedAvg --num_clients 10 --transfer_epochs 5 python transfer.py --dataset chestxray --algorithm FedOur --num_clients 10 --transfer_epochs 5如果 FedOur 的迁移准确率高于本地训练模型说明全局基本层捕获到的胸部 X 光特征比随机初始化更适合本地分类如果还普遍高于 FedAvg 迁移结果则说明元迁移模块在缩小源域和目标域差距上有效。一个容易被忽略的点是这里的“本地训练”并不完全等于随机初始化从头训练它也可能使用了一个较弱的预训练权重。因此比较时要注意--pretrained参数是否一致否则结果说服力会打折。6. 让项目跑起来环境配置、参数选项与常见陷阱6.1 requirements.txt与Python环境准备项目自带的requirements.txt中核心依赖是torch、torchvision、medmnist、numpy、pillow。我建议在干净环境里安装conda create -n fed python3.8 conda activate fed pip install -r requirements.txtPython 版本不要选太高3.8 或 3.9 比较稳。PyTorch 的版本要支持argsort、Subset等常用操作2.0 以上的 CPU 版也能跑通只是训练速度慢一些。如果下载 MedMNIST 和 Chest X-Ray 数据集时网络不稳定先手动下载后放到./data目录并确认dataset.py中的根路径指向正确。6.2 options.py里的关键参数怎么调options.py中的参数直接影响实验能否复现出论文中的曲线趋势。我把常用的几个整理成表格方便对照修改参数作用推荐值影响--dataset选择实验数据集cifar10 / dermamnist / bloodmnist / chestxray决定输入维度与类别数--algorithm选择联邦学习算法FedAvg / FedPer / FedRep / FedOur决定本地更新和聚合逻辑--num_clients客户端总数10 / 50 / 100决定数据切分颗粒度--frac每轮参与客户端的比例0.1 ~ 0.3值越小通信开销越低但收敛越慢--local_ep本地训练轮数1 ~ 5影响本地过拟合程度--lr本地学习率0.003 ~ 0.01过大导致震荡过小导致收敛慢--rounds全局通信轮数100 ~ 300控制总训练预算调参时先固定--num_clients和--frac只改--lr与--local_ep。如果发现准确率曲线呈锯齿状优先降低--lr如果曲线一直平坦无上升则增大--local_ep或--rounds。6.3 三个易踩的坑第一个坑是数据集划分不一致。多次运行实验时如果没有固定随机种子dataset.py每次生成的客户端子集不同导致不同算法之间的对比失去公平性。我一般会在options.py里加一个--seed 42并在初始化所有随机操作前固定它。第二个坑是模型定义与数据集类别数不匹配。models/Resnet18.py和Resnet34.py预设的分类头和DATASETS字典必须同步修改。比如dermamnist有 7 类如果沿用 Cifar-10 的 10 类分类器测试时会出现输出维度错误报错信息不会直接指出原因排查起来很费时间。第三个坑是聚合时没错但测试时忘切换模型模式。训练阶段用model.train()测试阶段要调用model.eval()并且让BatchNorm层使用全局统计量。我在跑 Chest X-Ray 时遇到过一次本地测试准确率异常低就是因为客户端返回的模型还处于训练模式导致 BatchNorm 在单样本预测时计算了错误的归一化量。解决方法是测试前显式加一行model.eval()并把torch.no_grad()包在推理循环外侧。本文还有配套的精品资源点击获取
返回列表