尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
联邦学习MNIST实战:Dirichlet划分与FedAvg排错指南
简介这份压缩包面向联邦学习、分布式训练与隐私保护方向的初学者和项目实践者围绕MNIST手写数字识别给出可运行的联邦学习与差分隐私实验材料适合用于课程设计、入门复现和算法对比。包内共17个文件约56.29MB主要包括Python训练与测试脚本、pth与pt模型权重、MNIST原始图像与标签文件及npz数据并配有说明文本和压缩数据覆盖服务端聚合、客户端本地训练、LeNet模型定义与测试评估等环节。已有669人学习下载说明其在入门实践中具有一定参考价值。通过梳理联邦平均聚合、数据分片、差分隐私加噪和模型迭代流程读者可理解原始数据不出本地、仅上传模型更新的协作训练机制也可借助预训练权重复现实验结果、观察不同轮次的性能变化为后续扩展到医疗、金融等敏感场景打下基础。1. 联邦学习跑 MNIST一个压缩包能省掉的三天搭环境时间第一次把联邦学习跑起来卡住大多数人的不是 FedAvg 那几行加权平均而是数据60 万张 MNIST 要切成几十份互不重叠、标签分布还各不相同的子集切完还得保证每轮聚合时索引对得上、样本数能当权重用。这份「联邦学习分布式训练 MNIST 数据集」压缩包解决的正是这段最枯燥的准备工作——数据集、划分脚本、客户端/服务端训练骨架打包在一起跳过造轮子直接进到调参和排错。适合两类人要复现 Non-IID 场景下 FedAvg 表现的学生和准备把自己业务数据套进联邦流程、先拿 MNIST 验证链路通不通的工程师。下面按拆包、切分、训练、通信、排错的顺序走一遍。2. 拆包与数据切分Non-IID 的 Dirichlet 划分怎么落地联邦学习分布式训练里数据集这一层的坑比模型层多得多。集中式训练只要一个DataLoader就完事联邦要的是「N 个客户端各自的 DataLoader」而且这 N 份数据得满足三个条件互不重叠、并集等于全集、标签分布可控。这一章把压缩包里的数据侧内容拆开讲透。2.1 包内结构与运行环境确认解压之后文件命名各家不同但按作用找一般是这四类东西原始 MNIST或已经转好的 idx/npz、划分脚本、客户端与服务端的训练脚本、以及一份配置。我拿到任何训练包的第一件事都不是急着跑而是先看清楚它把「数据划分」放在哪一步——是预处理阶段生成固定分区还是每次启动时现切。这两种做法的差别很大前者可复现、能缓存后者省磁盘但每次随机种子一变分区就变了实验结果没法比。位置典型内容上手必须先确认的点data/MNIST 原始文件或已打包的 npz是原始 28×28 灰度图还是已经做过归一化partition*.py划分脚本IID / Dirichlet / Shard是否固定 seed是否落盘client*.py本地训练循环本地 epoch 数、优化器、是否复制全局模型server*.py聚合与轮次控制加权方式是按样本数还是按客户端等权configs/超参文件客户端数、参与率、通信轮次先把环境跑通别在这一步省事# 看包里到底有什么先看清单再解压避免覆盖同名目录 unzip -l federated_mnist.zip | head -40 # 解压到独立目录 unzip -q federated_mnist.zip -d fl_mnist cd fl_mnist # 确认关键依赖MNIST 规模下 CPU 也能跑有卡更快 python -c import torch, torchvision, numpy; \ print(torch, torch.__version__); \ print(cuda, torch.cuda.is_available()) # 固定随机性联邦实验最怕每轮分区都不一样 export PYTHONHASHSEED0 export CUBLAS_WORKSPACE_CONFIG:4096:8unzip -l先看清单是为了确认压缩包是不是「平铺」结构——很多包解压出来直接是十几个 py 文件铺在当前目录和已有工程混在一起后面清理很麻烦。PYTHONHASHSEED管的是 Python 层的哈希顺序影响set、dict遍历CUBLAS_WORKSPACE_CONFIG是开启torch.use_deterministic_algorithms(True)时 cuBLAS 需要的环境变量不设会在第一次矩阵乘时报错。MNIST 上这两项不是必须但联邦实验动辄要跑五六组对照结果不能复现的话后面所有对比都是白做。还有一点如果包里的数据是.pt或.npz打包好的先用 numpy 看一眼形状和取值范围。import numpy as np d np.load(data/mnist_partitioned.npz) print(list(d.keys())[:5]) # 期望形如 c0, c1, ... 或 x_train / y_train x d[list(d.keys())[0]] print(x.shape, x.dtype, x.min(), x.max()) # 期望 (N, 28, 28) uint8 且 0~255若是 (N, 784) float 且 0~1说明已经归一化过这一步看的是两件事形状和值域。如果拿到的已经是float32且落在 0~1训练脚本里再来一次Normalize((0.1307,), (0.3081,))就会把数据压到接近全零模型直接学不动而且 loss 曲线看起来还挺「平稳」非常容易误判成收敛。归一化只能做一次这是我在联邦场景里见过最多的静默失败。2.2 从 IID 到 Dirichlet客户端数据划分的实现为什么要专门讲划分因为联邦学习分布式训练的价值全在 Non-IID 上。如果每个客户端的数据都是均匀的十类分布那 FedAvg 的收敛曲线和集中式训练几乎重合你根本验证不出任何东西。真实的端侧数据天然带偏MNIST 上模拟这种偏态最常用的两种做法是 Shard每个客户端只分到固定几类和 Dirichlet按浓度参数抽比例。Shard 简单粗暴把训练集按标签排序切成2 × num_clients个 shard每个客户端拿 2 个。缺点是偏得太极端客户端本地只有两个类模型的输出层会严重偏向这两类聚合后全局模型在别的类上掉点非常明显容易让人误以为算法有问题。Dirichlet 更接近真实每个客户端都持有全部十类但比例各不相同。核心是按类独立做一次 Dirichlet 抽样得到这一类的样本在各客户端上的分配比例。落地代码如下可以直接抄import numpy as np from torchvision import datasets def dirichlet_partition(targets, num_clients, alpha, seed0, min_size10): 按类做 Dirichlet 抽样返回每个客户端的样本索引列表 rng np.random.default_rng(seed) num_classes int(targets.max()) 1 # 先按类别把索引分组并打乱避免同类样本扎堆 idx_by_class [np.where(targets c)[0] for c in range(num_classes)] for c in range(num_classes): rng.shuffle(idx_by_class[c]) client_idx [[] for _ in range(num_clients)] for c in range(num_classes): # alpha 越小比例越极端alpha 越大越接近均匀 p rng.dirichlet(np.repeat(alpha, num_clients)) cuts (np.cumsum(p) * len(idx_by_class[c])).astype(int)[:-1] for cid, part in enumerate(np.split(idx_by_class[c], cuts)): client_idx[cid].extend(part.tolist()) # 兜底alpha 很小时会出现空客户端从最大的客户端借样本 for cid in range(num_clients): if len(client_idx[cid]) min_size: donor max(range(num_clients), keylambda i: len(client_idx[i])) need min_size - len(client_idx[cid]) client_idx[cid].extend(client_idx[donor][:need]) client_idx[donor] client_idx[donor][need:] return client_idx train datasets.MNIST(root./data, trainTrue, downloadTrue) parts dirichlet_partition(train.targets.numpy(), num_clients50, alpha0.5, seed42) sizes [len(p) for p in parts] print(客户端数, len(parts), 最少, min(sizes), 最多, max(sizes), 总计, sum(sizes))逻辑上有三个点值得说。第一rng.shuffle在分类之后做是为了让同一类里的样本随机落到不同客户端否则 MNIST 原始顺序本身带一点聚集性。第二np.split用cuts切分cumsum保证比例严格按 Dirichlet 抽出来的p[:-1]去掉末尾的 1.0否则会多切出一个空段。第三兜底循环不能省alpha0.1时几乎必然出现某个客户端只分到个位数样本甚至 0 个这类客户端参与聚合时权重极小却会贡献一个偏差很大的梯度方向我在早期实验里被它坑过一次。参数怎么选参数含义建议取值影响num_clients客户端总数20 / 50 / 100数量越多单客户端样本越少本地 epoch 要相应调小alphaDirichlet 浓度0.1 / 0.5 / 1.00.1 极端偏态100 近似 IID做对照实验用 0.5 起步seed分区随机种子固定值换了它等于换了整个实验报告里必须写清min_size客户端最小样本数10~50低于这个数本地 BatchNorm 和 loss 都会抖想要一组有说服力的对照就用同一个seed跑alpha ∈ {0.1, 0.5, 1.0}三条曲线。注意alpha1.0也只是「接近」IID真 IID 要单独用均匀切分别拿它冒充。2.3 分区落盘与加载别每轮重新切划分这步的计算量不大但如果你每次启动实验都重切一遍一是慢二是只要 seed 忘了固定前后两次实验的分区就不一样结果完全不可比。所以我一般的做法是把分区结果落成一份索引文件训练脚本只读不切。import numpy as np # 只存索引不存图像本体几十 MB 变几百 KB np.savez_compressed( data/partitions_a0.5_c50_s42.npz, **{fc{i}: np.asarray(v, dtypenp.int64) for i, v in enumerate(parts)} ) print(已保存, len(parts), 个客户端分区)存索引不存图像是很实际的一条经验。MNIST 训练集 6 万张50 个客户端如果各自存一份图像副本压缩包体积会膨胀十几倍而索引文件只有几百 KB。文件名里把alpha、客户端数、seed 都写进去跑十组实验也不会搞混这比在代码里维护一堆常量靠谱得多。读取侧用Subset包一层就够了别自己写 Datasetimport torch from torch.utils.data import DataLoader, Subset from torchvision import datasets, transforms tf transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)), # 只做一次数据侧不要再归一化 ]) base datasets.MNIST(root./data, trainTrue, downloadTrue, transformtf) parts np.load(data/partitions_a0.5_c50_s42.npz) def client_loader(cid, batch_size32, num_workers2): idx parts[fc{cid}] ds Subset(base, idx.tolist()) return DataLoader(ds, batch_sizebatch_size, shuffleTrue, num_workersnum_workers, pin_memoryTrue, drop_lastFalse) test_set datasets.MNIST(root./data, trainFalse, downloadTrue, transformtf) test_loader DataLoader(test_set, batch_size512, shuffleFalse)Subset共享同一个base的图像缓存50 个客户端不会各存一份原始数据内存占用和客户端数基本无关这是它比「每个客户端单独建 Dataset」强的地方。drop_lastFalse是刻意的联邦里客户端样本数直接当聚合权重用丢掉最后不满一个 batch 的样本会让权重算错尤其是小客户端上误差能到 20% 以上。num_workers在单机多进程模拟时要注意每个客户端进程再开 2 个 worker50 个客户端就是 100 个进程机器资源很快就撑不住。最后一件必须做的事把全局测试集和各客户端的本地测试集分清楚。上面这份test_loader是全局测试集用来评估聚合后的全局模型如果你想看「个性化」效果才需要给每个客户端再切一份本地测试集。这两者混用是最常见的指标虚高来源第 5 章会细说。3. 本地训练与参数聚合FedAvg 一轮完整流程的手写实现数据准备好之后联邦学习分布式训练的主干其实很短服务端下发全局模型 → 各客户端本地跑若干 epoch → 回传参数 → 服务端加权平均 → 进入下一轮。真正花时间的是每一段的参数怎么定、状态怎么对齐。这一章把一轮完整的 FedAvg 手写一遍包括那些文档里通常不写的对齐细节。3.1 客户端本地训练epoch、batch 与优化器的一致性本地训练这段代码看起来和普通训练没区别但有三处必须和集中式训练区别对待优化器每轮都要重建、本地 epoch 不能太大、学习率要跟着本地步数调。import copy, torch import torch.nn.functional as F from torch import nn class SmallCNN(nn.Module): def __init__(self, num_classes10): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 28 - 14 nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 14 - 7 ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64 * 7 * 7, 128), nn.ReLU(), nn.Linear(128, num_classes) ) def forward(self, x): return self.classifier(self.features(x)) def local_train(global_state, loader, local_epochs1, lr0.01, devicecpu): # 关键深拷贝绝不能让客户端直接改全局 state_dict model SmallCNN().to(device) model.load_state_dict(copy.deepcopy(global_state)) # 优化器必须每轮新建动量和权重衰减状态不能跨轮继承 opt torch.optim.SGD(model.parameters(), lrlr, momentum0.9, weight_decay5e-4) model.train() for _ in range(local_epochs): for x, y in loader: x, y x.to(device, non_blockingTrue), y.to(device, non_blockingTrue) opt.zero_grad(set_to_noneTrue) loss F.cross_entropy(model(x), y) loss.backward() opt.step() return copy.deepcopy(model.state_dict()), len(loader.dataset)copy.deepcopy(global_state)这一行是整段代码里最容易出事的地方。load_state_dict默认做的是param.data.copy_(input)本身是拷贝语义所以单次调用没问题但如果多个客户端共享同一个模型对象比如[global_model] * N这种列表乘法它们改的是同一份权重聚合结果等于最后一个客户端的结果曲线会呈现出一种诡异的「一直在学但学不好」的状态。深拷贝是最省心的做法内存代价在一个 42 万参数的小网络上可以忽略。opt SGD(...)放在函数内部而不是外面对应的是 FedAvg 的标准定义每轮客户端从全局模型重新出发优化器状态清零。如果跨轮复用优化器动量项会把上一轮的方向带进来相当于在客户端之间引入了隐式的历史依赖聚合出来的模型和不复用的情况差别不小。想做 FedOpt 那一类服务端自适应优化是另一回事那是服务端的优化器别和客户端混了。参数上MNIST 这种量级本地 epoch 取 1 到 3 就够。取到 10 以上客户端会严重过拟合本地分布Non-IID 下全局模型反而掉点——这不是玄学本地步数越多客户端漂移越远加权平均出来的方向越偏。学习率从 0.01 起步配合 momentum 0.9alpha0.1这种极端偏态下可以降到 0.005。3.2 服务端聚合加权平均与 state_dict 对齐聚合这三行代码谁都会写难的是「哪些 key 能平均、哪些不能」。MNIST 上如果用带 BatchNorm 的网络num_batches_tracked是 int64直接乘浮点权重会报 dtype 错running_mean和running_var也不能跨客户端平均语义上讲不通遍历不同分布得到的统计量。def fedavg(global_state, client_states, client_sizes): 按样本数加权平均只对浮点张量做整数 buffer 保留全局值 total float(sum(client_sizes)) weights [n / total for n in client_sizes] new_state {} for k, v in global_state.items(): if not torch.is_floating_point(v): new_state[k] v.clone() # num_batches_tracked 这类直接沿用 continue acc torch.zeros_like(v, dtypetorch.float32) for ws, w in zip(client_states, weights): acc ws[k].float() * w new_state[k] acc.to(v.dtype) return new_state先说权重。client_sizes用的是各客户端的样本数不是等权。这两者在 IID 下几乎没差别但在alpha0.1这种偏态下等权会让小客户端可能只有几十个样本的梯度方向和有大几千样本的客户端一样重全局模型会被拉偏。这个差别我在 50 客户端、alpha0.1的设置下实测过最终准确率能差到 3 到 5 个百分点所以别图省事写1/len(states)。再说acc用float32累加。模型参数本来就是 float32但如果你开了amp或者用了 bfloat16累加过程中精度损失会累积尤其是 100 个客户端依次相加的时候。先.float()累加最后再.to(v.dtype)回转是一个几乎零成本的习惯。至于整数 buffer直接保留全局值最省事如果一定要处理 BatchNorm 的统计量正确做法是让客户端在本地重新统计一遍再聚合而不是对running_var求平均。聚合完还有一步经常被漏掉把新 state_dict 推回全局模型。global_model SmallCNN() global_state copy.deepcopy(global_model.state_dict()) for rnd in range(1, 101): client_states, client_sizes [], [] for cid in sampled_clients: state, n local_train(global_state, client_loader(cid), local_epochs1, lr0.01) client_states.append(state) client_sizes.append(n) global_state fedavg(global_state, client_states, client_sizes) global_model.load_state_dict(global_state) # 漏了这行评估的还是初始权重global_model.load_state_dict(global_state)这行漏掉的后果是训练照跑、loss 照降、评估指标一动不动。因为评估用的是global_model而它从头到尾没被更新过。这个 bug 我在别人代码里见过不止一次排查时先看这行在不在。3.3 轮次、参与率与收敛口径超参里最需要想清楚的是三个总轮次R、每轮参与率C、本地 epochE。这三个量共同决定「总共算了多少本地步」和「通信了多少次」而通信次数才是联邦场景下真正的成本。参数典型值调大的后果调小的后果R总轮次50~200线性增加通信开销收益递减欠拟合曲线还没平就停了C参与率0.1~1.0每轮更稳定但单轮耗时上升梯度噪声大曲线抖E本地 epoch1~5通信轮次可减少但客户端漂移加剧通信轮次需要成倍增加Bbatch size32~128本地步数减少收敛变慢小客户端一个 epoch 只有几步观察收敛曲线有个经验口径前 20 轮掉点、甚至比初始模型还差是正常的。Non-IID 下各客户端的初始方向互相冲突前几轮平均出来的模型往往不如随机初始化第 30 轮之后才开始稳定爬升。如果 100 轮之后准确率还在 10% 附近打转MNIST 十类的随机水平基本可以断定是聚合那一步出了问题而不是「联邦学不动」。评估频率也别每轮都做。MNIST 测试集 1 万张每轮评估一次在小网络上要多花几秒100 轮下来就是几分钟的纯开销。我一般每 5 轮评一次同时记录参与客户端的平均本地准确率两条曲线一起看如果全局准确率涨但平均本地准确率不涨说明聚合在做「平均化」各客户端的个性化能力被抹平了这时候该考虑 FedProx 或者个性化层。4. 分布式通信层单机多进程与多机拓扑的取舍标题里带「分布式训练」但压缩包里的实现大概率是单机多进程模拟这一点要提前认清否则上真机时会发现处处不对劲。这一章讲通信层的选型、开销估算以及掉线这类真分布式才会遇到的问题。4.1 三种跑法的边界多进程、Ray、gRPC同样是「多个客户端并行训练」实现方式差别很大成本和调试难度也不一样。方案适用规模优点代价multiprocessing/concurrent.futures单机 8~50 客户端零依赖改几行就能并行只能单机进程间靠 pickle 传参Ray Actor单机到小集群天然支持异构资源和调度多一层框架调试栈变深gRPC / HTTP 自建跨机最接近真实部署形态要自己处理序列化、超时、重连我一般的顺序是先用多进程把算法逻辑跑通、曲线调对再换 Ray 验证并行调度最后才考虑真跨机。反过来做的话你会同时面对算法 bug 和网络 bug排查成本翻倍。压缩包里的多进程版本价值就在这——它把「算法正确性」和「通信实现」解耦了你可以先只关心前者。用多进程时有个参数必须显式设import torch.multiprocessing as mp if __name__ __main__: # 每个进程开 OMP 线程会互相抢核必须限制 torch.set_num_threads(1) ctx mp.get_context(spawn) # fork 在多线程 CUDA 下容易死锁 with ctx.Pool(processes8) as pool: results pool.starmap(run_client, [(cid, global_state) for cid in sampled])spawn而不是默认的fork是因为fork出来的子进程会继承父进程已经初始化的线程池和 CUDA 上下文在多线程环境下偶发死锁而且不报错、只是卡住非常难查。torch.set_num_threads(1)是因为 8 个客户端进程各自默认开满 OMP 线程的话48 核机器也会被打爆反而比单进程还慢。这两个坑都属于「不设也能跑但跑得莫名其妙」。4.2 通信开销先算清楚再优化联邦学习论文里翻来覆去讲通信效率但很多人没算过自己这个模型传一次到底多大。一个两层卷积的 MNIST 小网络大约 42 万参数float32 下约 1.7 MB一轮 5 个客户端参与就是上传 8.5 MB、下发 8.5 MB下发如果做广播可以省100 轮总计 1.7 GB 左右。这个量级在单机上是噪音但在真实端侧就是实打实的成本。def state_bytes(state): total 0 for v in state.values(): total v.numel() * v.element_size() return total n state_bytes(global_state) print(f单份模型 {n/1024/1024:.2f} MB) print(f100 轮 x 每轮 5 客户端 上行合计 {n*5*100/1024/1024/1024:.2f} GB)先算清楚再决定要不要上压缩。常见的三种做法量化float32 转 int8体积降到 1/4MNIST 上精度损失很小、稀疏化只传绝对值大的 top-k 梯度需要配合残差累积、以及减少通信轮次增大本地 epoch。前两种要改序列化层第三种只改超参所以如果你只是想让实验跑得快一点先调E和R别一上来就写压缩算法。序列化本身也有讲究。多进程之间传state_dict走的是 pickle一个 1.7 MB 的 dict 序列化加反序列化大概在毫秒级50 个客户端就有几十毫秒的纯开销跑到几百轮就很可观。能传张量列表就别传带嵌套结构的 dict能用torch.save的二进制格式就别用 json。4.3 掉线与慢客户端真分布式才有的问题单机多进程不会有掉线但一旦上真机客户端中途退出就是常态而不是异常。处理好这件事联邦流程才算完整。import random def sample_clients(available, frac, seed): rng random.Random(seed) k max(1, int(len(available) * frac)) return rng.sample(available, k) # 无放回避免同一客户端被抽两次 def run_round(global_state, clients, frac, round_seed, timeout_s120): picked sample_clients(list(clients.keys()), frac, round_seed) states, sizes [], [] for cid in picked: try: # 真实场景这里是 RPC 调用超时抛异常 state, n clients[cid].train(global_state, timeouttimeout_s) except TimeoutError: print(fclient {cid} 超时本轮跳过) continue states.append(state); sizes.append(n) if not states: # 全部失败时回退别让训练中断 return global_state, 0 return fedavg(global_state, states, sizes), len(states)三个细节。random.sample是无放回抽样用rng.choice(..., replaceTrue)会抽到重复客户端聚合时同一个客户端的权重被算两次等价于给它加权曲线会莫名偏好某些客户端的数据分布。timeout必须显式给没有超时的 RPC 在客户端掉线时会一直挂着整个训练卡死。最后那个if not states的回退分支是为了防止某一轮所有客户端都失败时直接抛异常中断——真机上这种情况会发生回退到上一轮的全局模型下一轮重新采样就行。还有一个「慢客户端」问题某个客户端每个 batch 要跑 10 秒其他只要 0.1 秒同步聚合下整轮就被它拖住。常见做法是设一个单轮时间上限超时的客户端本轮结果丢弃但要记账——如果某个客户端连续 5 轮超时说明它的算力或网络确实不行应该从候选池里降权而不是每轮都白等。5. 避坑排查联邦训练里最容易被忽略的五个翻车点这一章全是踩过的坑。每条按「现象 → 原因 → 解决」写碰到对应的现象可以直接对号入座。5.1 数据侧的两个静默失败坑一客户端数据不重不漏但准确率就是上不去。现象训练能跑完loss 正常下降全局准确率停在 60% 左右怎么调学习率都没用。原因分区时用了带放回的抽样或者多个客户端的索引区间有重叠。重复样本在聚合时被反复加权等价于给这部分数据放大了权重模型在这部分上过拟合在没被覆盖的样本上表现差。这种问题不会报错只能靠检查。解决划分完成后立刻做一次并集和交集检查并写进流程里import numpy as np all_idx np.concatenate([np.asarray(v) for v in parts.values()]) assert len(all_idx) len(np.unique(all_idx)), 存在重复样本 assert len(all_idx) 60000, f样本数不对: {len(all_idx)} print(分区校验通过无重复无遗漏)这两行断言建议每个新分区都跑一遍成本几毫秒能省掉半天的困惑。坑二全局准确率和本地准确率对不上差得离谱。现象全局模型在全局测试集上 92%但把模型发到某个客户端上测只有 40%。原因多半是客户端本地测试集和全局测试集混用了或者本地测试集本身就来自这个客户端的训练分区数据泄漏。Non-IID 下客户端本地分布和全局分布本来就不同40% 不一定是 bug但如果本地测试集是从训练索引里切的那就是泄漏数字完全没有参考价值。解决训练集、全局测试集、各客户端本地测试集三份索引必须分开生成、分开落盘、命名带前缀。评估时明确标注用的是哪一份。要看个性化效果本地测试集必须从该客户端「没参与训练」的样本里切——而 MNIST 上这往往意味着要从全局测试集里按标签分布再切一份别偷懒从训练分区里拿。5.2 训练侧的两个隐蔽偏差坑三聚合后模型性能还不如单个客户端单独训练。现象某一轮聚合完成全局模型准确率突然掉一大截下一轮又恢复来回震荡。原因客户端漂移。本地 epoch 太多、学习率太大各客户端在自己那份偏态数据上跑得太远回传的参数方向互相冲突加权平均的结果落在一个谁都不满意的位置。alpha越小、E越大这个现象越明显。解决先把E降到 1把学习率减半看震荡是否消失。如果消失说明确实是漂移后续可以用 FedProx 加近端项约束第 6 章讲或者用学习率衰减让后期的本地更新更保守。别一上来就怀疑聚合代码写错了先排除超参。坑四换了个随机种子结果差 5 个点。现象同一份代码、同一组超参只改了 seed最终准确率从 91% 掉到 86%。原因联邦实验的方差本来就比集中式大——客户端采样、数据划分、本地 shuffle、权重初始化四个随机源叠在一起。只跑一次就下结论等于在噪声里找信号。解决固定三个种子分区 seed、客户端采样 seed、torch 全局 seed报告结果时至少跑 3 个种子取均值和标准差。如果两组配置的差距小于标准差那这个差距不值得写进结论。这一条听起来像废话但我在对照实验里见过太多次「A 比 B 好 2 个点」其实是种子噪声。5.3 聚合与通信的一个致命细节坑五聚合代码没错但全局模型像是没更新过。现象训练日志正常打印每轮 loss评估指标从头到尾一条直线。原因三个可能按概率排序——评估用的模型对象没有被load_state_dict更新聚合结果赋给了局部变量没传出去多进程里子进程改了副本父进程的global_state还是旧的。解决在聚合后加一行断言直接比对参数是否真的变了before global_state[classifier.3.weight].clone() global_state fedavg(global_state, client_states, client_sizes) after global_state[classifier.3.weight] delta (after - before).abs().mean().item() assert delta 0, 聚合后参数没有变化检查 state_dict 传递链路 print(f参数平均变化量 {delta:.6f})这个delta还能当健康指标用。正常情况下它随轮次递减如果某几轮突然变大说明有客户端贡献了异常大的更新往回去查那个客户端的数据量是不是太小、学习率是不是太高。把调试信息变成断言留在代码里比每次出问题再手写 print 强得多。6. 对齐集中式上界给联邦结果找一个可比的标尺联邦实验最容易自欺欺人的地方是只跟自己比。准确率从 85% 涨到 90% 听起来不错但集中式训练在同样数据上能到 99%那这 90% 到底说明算法有效还是流程有瓶颈我的习惯是每次都跑一条集中式基线当上界用完全相同的网络结构、优化器和总本地步数只是把数据合在一起训练。# 集中式基线同样的模型、同样的总 epoch 预算数据不切分 base_model SmallCNN() opt torch.optim.SGD(base_model.parameters(), lr0.01, momentum0.9, weight_decay5e-4) full_loader DataLoader(Subset(base, np.arange(60000)), batch_size64, shuffleTrue) for epoch in range(5): # 5 x 60000/64 ≈ 4680 步约等于联邦 100 轮 x 5 客户端 x 9 步 base_model.train() for x, y in full_loader: opt.zero_grad(); F.cross_entropy(base_model(x), y).backward(); opt.step()对齐的关键是「总本地步数」而不是「轮数」。联邦 100 轮、每轮 5 个客户端、每客户端 1 个 epoch 大约 9 步合计约 4500 步集中式基线就按 4500 步来配这样两条曲线才有可比性。如果集中式基线只跑 1 个 epoch 就来对比那联邦赢了也没意义。表里是我在alpha0.5、50 客户端、C0.2这组设置下常见的结果形态可以作为你自查的参照配置预期测试准确率说明集中式基线4500 步98.5%~99.2%上界用来判断流程有没有问题FedAvgalpha1.097%~98.5%接近 IID损失很小FedAvgalpha0.595%~97.5%常规 Non-IID主要看这个区间FedAvgalpha0.188%~94%方差大必须多种子取均值单客户端本地训练60%~85%只见过自己那部分数据泛化差如果alpha1.0都跑不到 97%先别怀疑算法回去查数据归一化是不是做了两次、聚合权重是不是等权、评估模型有没有被更新。这三个查完绝大多数「联邦学不动」的问题就解决了。再往上走一步是 FedProx在客户端本地 loss 里加一个近端项(mu/2) * ||w - w_global||^2把本地更新拉向全局模型抑制漂移。mu从 0.001 试起alpha越小越需要它。加了之后alpha0.1那组通常能提 2 到 4 个点代价是多一个要调的超参。从那以后我每次开新的联邦实验都强制先跑一遍分区校验和集中式基线两条都过了才动算法。这个习惯救过我很多次——有回怎么调都不收敛最后发现是分区脚本里alpha被写死成 100等于在跑 IID自然什么都验证不出来。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

iOS 手搓轻量物理引擎:浮动气泡圆碰撞检测与网格分桶实战

iOS 手搓轻量物理引擎:浮动气泡圆碰撞检测与网格分桶实战

简介:这是一份面向iOS开发者的浮动泡泡功能示例工程,围绕自定义视图绘制、动画驱动与碰撞反弹展开,适合已掌握基础UIKit、希望练习Core Graphics绘图和基础物理检测的中级学习者参考。压缩包共24个文件、约7.34MB,含m与h源码、sto…

📅 2026/11/5 4:06:14
ASP.NET餐饮管理系统源码实战:部署、排错与二次开发

ASP.NET餐饮管理系统源码实战:部署、排错与二次开发

简介:面向餐饮行业信息化开发学习者与中小餐厅管理人员的 ASP.NET 餐饮管理系统完整项目包,以 Web 形式实现菜单、订单、库存、会员、报表与后台权限等常见业务,可作为课程设计、毕业设计或企业原型参考。包内共约 2000 个文件、整体 46.58MB…

📅 2026/11/5 4:06:14
secureCRT-7.0.0实战:SSH2公钥、端口转发与日志脚本配置

secureCRT-7.0.0实战:SSH2公钥、端口转发与日志脚本配置

简介:SecureCRT 7.0.0 是面向 IT 运维、网络管理员及开发测试人员的 64 位终端仿真与文件传输工具包,主要用于 shell 远程登录、服务器维护及多协议连接管理。压缩包共93个文件,约15.86MB,含26个dll运行库、14个vbs与6个py脚本、5…

📅 2026/11/5 4:06:14
MORE NEWS

更多资讯

📰

PySceneDetect终极性能对比分析:为什么它是最佳场景检测工具选择

PySceneDetect终极性能对比分析:为什么它是最佳场景检测工具选择 【免费下载链接】PySceneDetect :movie_camera: Python and OpenCV-based scene cut/transition detection program & library. 项目地址: https://gitcode.com/GitHub_Trending/py/PySceneDet…

📰

PySceneDetect入门指南:5分钟学会视频场景自动检测

PySceneDetect入门指南:5分钟学会视频场景自动检测 【免费下载链接】PySceneDetect :movie_camera: Python and OpenCV-based scene cut/transition detection program & library. 项目地址: https://gitcode.com/GitHub_Trending/py/PySceneDetect PySc…

📰

PySceneDetect核心功能详解:10种场景检测算法全解析

PySceneDetect核心功能详解:10种场景检测算法全解析 【免费下载链接】PySceneDetect :movie_camera: Python and OpenCV-based scene cut/transition detection program & library. 项目地址: https://gitcode.com/GitHub_Trending/py/PySceneDetect PyS…

📰

ai-goofish-monitor 闲鱼商品监控完整指南:5 分钟部署到 AI 自动捡漏

ai-goofish-monitor 闲鱼商品监控完整指南:5 分钟部署到 AI 自动捡漏 【免费下载链接】ai-goofish-monitor 基于 Playwright 和AI实现的闲鱼多任务实时/定时监控与智能分析系统,配备了功能完善的后台管理UI。帮助用户从闲鱼海量商品中,找到心…

📰

ElixirOffice架构解读:6层架构如何基于ONLYOFFICE引擎打造开源办公套件

【免费下载链接】ElixirOffice Open Source Office suite for users used to MS Office - multi-platform, tuned for performance 项目地址: https://gitcode.com/gh_mirrors/el/ElixirOffice 点击查看 免费下载 ElixirOffice 是一套面向 Microsoft Office 用户的…

📰

ADS122U04+PIC18F96J94:24位ADC工业测温方案解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬