尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
用Python实现本地模拟横向联邦学习:从FedAvg到单机可复现实验
简介面向机器学习初学者与联邦学习研究者的横向联邦学习本地模拟资源使用Python构建以公开数据集搭建可复现的单机模拟环境让没有分布式集群的读者也能体验多客户端协作建模的完整过程。资源从数据加载、模型定义、客户端本地训练到服务端参数聚合均有独立脚本并含有配置文件与数据集分片覆盖模型定义、通信接口、服务器聚合等联邦学习核心环节可直观理解多客户端协同训练、参数上传与全局模型广播的完整闭环。包内共25个文件以源码为核心附编译后字节码、工程配置、数据分片等文件类型压缩包约302MB目录划分清晰便于按模块对照学习。目前已有723人学习下载适合作为横向联邦学习入门实践的参考读者可在此基础上修改客户端数量、数据划分方式观察不同设置对全局模型收敛的影响也为后续研究安全聚合、通信优化等方向提供了可扩展的实验基底。1. 用Python实现本地模拟横向联邦学习为什么在单机上跑通框架比你想的更重要横向联邦学习是多方协作训练一种常见设定参与各方的数据特征维度一致但样本各自私有模型在“数据不出本地”的约束下被反复训练与聚合。很多开发者一开始就奔着分布式框架去装集群、配通信结果在环境上花了大量时间迟迟没有验证核心算法。实际上联邦学习的关键机制——数据切分、本地训练、参数聚合——完全可以在单个 Python 进程里建模。在一台普通开发机上实现本地模拟横向联邦学习用几百行代码就能跑出一个行为可信的最小闭环用来验证 FedAvg 的收敛性、测试 non-IID 数据的影响、甚至对比不同聚合策略。作为研究者它是你快速验证假设的试验台作为工程师它是你理解横向联邦学习内部机制的低成本起点。我在多轮项目里靠这套模拟器排掉了大量逻辑错误这篇文章把完整做法展开讲清楚。2. 横向联邦学习的最小闭环FedAvg 的数学直觉与代码骨架2.1 FedAvg 到底在聚合什么参数平均和梯度平均的差别横向联邦学习里最常见的聚合算法是 FedAvg联邦平均。它做的事情一句话能说清每个客户端用本地数据把模型训练若干轮local epochs把训练后的模型参数交给服务器服务器按每个客户端样本数量的占比做加权平均得到新一轮全局模型再广播回去。整个横向联邦学习 round 就这么循环下去。容易被忽略的一个区别是FedAvg 平均的是模型参数不是梯度。分布式训练常用的 AllReduce 每次同步平均梯度FedAvg 则允许客户端本地跑完多个 epoch 才上传模型参数。这带来两个直接的好处通信次数急剧下降因为一次通信携带的是本地训练了 E 轮之后的完整模型状态而不是每个小 step 的梯度同时样本更多的客户端在聚合时的权重更大因为它对全局模型的贡献理应更显著。# FedAvg 聚合伪代码 # 第 t1 轮全局模型 Σ (n_k / N) * w_k # n_k : 客户端 k 的本地样本数 # N : 本轮参与客户端样本总数 # w_k : 客户端 k 本地训练 E 轮后的模型参数 def fedavg_aggregate(local_weights, local_sizes): total_size sum(local_sizes) global_weight {} for key in local_weights[0].keys(): global_weight[key] sum( w[key] * (s / total_size) for w, s in zip(local_weights, local_sizes) ) return global_weight代码逻辑local_weights是各客户端上传的参数 dict 列表local_sizes是对应的样本数列表。聚合时对每一层参数做加权求和权重是样本数占比返回的global_weight就是下一轮全局模型。参数说明此处的权重是“样本占比”不是参与客户端数量当各客户端数据非均匀分布时这个细节直接决定模型偏向谁如果你的客户端数据量完全均等那么样本占比与数量占比等价但为了通用性仍然建议显式传local_sizes。提示聚合时使用float类型累加注意样本数差距过大时大样本客户端的参数会主导全局模型这既可能符合直觉也可能掩盖 non-IID 带来的模型退化后面会专门讨论。这里要再强调一点FedAvg 假设各客户端本地数据是同分布的即 IID。真实场景往往不是这样但如果连模拟器里的基础逻辑都不正确后面做 non-IID 分析就会分不清是聚合算法问题还是数据切分问题。2.2 单机模拟的核心抽象数据分片、客户端训练、服务端聚合三层拆分真实横向联邦系统的结构是客户端进程或设备通过网络与服务器通信服务器负责调度、聚合和广播。单机模拟不需要模拟网络——它需要的是把“多个客户端”这一个概念翻译成进程内对象。我习惯按三个层次去建模这也是代码结构的基本原则。第一层是数据切分器DataPartitioner。它接收完整数据集和客户端数量 K返回一个字典键是客户端 ID值是该客户端持有的数据索引。第二层是客户端训练器ClientTrainer。它是一个函数或者类方法输入是全局模型参数、本地数据索引、超参数学习率、本地轮次、batch size输出是一份训练后的本地模型参数。第三层是服务端聚合器ServerAggregator。它维护全局模型参数调度每个 round 参与的客户端收集参数并聚合。我一般用 Python 的state_dict字典结构表示模型参数让每个客户端训练时由模型工厂创建全新的模型实例再加载全局权重这样避免客户端之间参数相互污染。在单机模拟里所有客户端训练一般串行执行避免同时开多个进程带来的资源竞争。如果你用的是 PyTorch最好把每一层参数从state_dict中取出来单独存成 NumPy 数组或直接以张量形式交换这样可以避免后续聚合时出现设备不匹配CPU/GPU 张量混用的问题。下面是一个最小骨架示例class ClientTrainer: 客户端本地训练器输入全局权重 - 输出本地权重 def __init__(self, model_fn, devicecpu): self.model_fn model_fn self.device device def train(self, global_weight, data_loader, lr0.01, local_epochs1): model self.model_fn().to(self.device) model.load_state_dict(global_weight) # 以全局模型为起点 optimizer torch.optim.SGD(model.parameters(), lrlr) # 具体训练循环见 3.2 节 return model.state_dict()代码逻辑train方法是整个模拟器的核心入口每次从全局权重出发创建一个新模型用本地数据训练若干轮后返回更新的权重这样客户端之间互不污染。参数说明model_fn是一个返回全新模型的工厂函数保证每个客户端拿到的是独立模型实例device参数让代码能在 CPU 与 GPU 之间切换模拟真实客户端可能使用不同算力的场景。注意这里没有在__init__里创建模型原因是为了避免多个客户端共享同一个模型对象的坑。注意不要在构造函数里创建模型后又在train里 load_state_dict 复用同一个实例容易踩“权重原地修改”的坑导致某个客户端训练完其他客户端的模型也被污染。这一层抽象几乎就是横向联邦学习模拟器的全部骨架。剩下的工作就是把骨架填实数据怎么切、训练循环怎么写、聚合循环怎么调度。下面从代码层面逐块展开。3. 从零搭建模拟器数据切分、客户端训练与服务端聚合3.1 数据切分模拟 IID 与 non-IID 数据分布的两种做法数据切分是本地模拟横向联邦学习里最需要注意的一步。IID 场景指的是每个客户端的数据分布与总体分布一致non-IID 则是每个客户端只拥有部分类别或分布偏移的数据。真实世界中non-IID 几乎是一种常态因为每个参与方采到的数据天然有偏。在模拟器里这一步直接决定后续所有实验结论的适用范围。先看 IID 切分。做法非常简单把样本索引打乱后平均切给 K 个客户端即可。下面这个函数返回一个字典键是客户端编号值是该客户端持有的样本索引列表import numpy as np def iid_split(labels, num_clients, seed42): IID 切分随机打乱索引后均分给各客户端 labels: 一维数组每行样本的类别 num_clients: 客户端数量 seed: 随机种子固定后实验结果可复现 rng np.random.RandomState(seed) indices rng.permutation(len(labels)) # 打乱全局索引 # np.array_split 会把 indices 均匀分成 num_clients 份 client_indices np.array_split(indices, num_clients) return {k: idx.tolist() for k, idx in enumerate(client_indices)}代码逻辑rng.permutation生成一个确定性的乱序索引np.array_split把索引均匀切成 K 份返回一个键为客户端编号、值为样本索引列表的字典。参数说明seed必须固定否则每次运行切分结果不同后续做对比实验时无法判断准确率变化来自算法还是数据分布labels在这里实际只用于确定样本数IID 并不要求依据类别信息。non-IID 切分最经典的做法是按标签排序后切块。每个客户端拿到的样本类别高度集中甚至完全属于某一个类别。最简单的实现def noniid_split_by_label(labels, num_clients, shard_size2, seed42): 按标签排序的非 IID 切分每个客户端拿到 shard_size 个类别的样本 labels: 一维类别数组 num_clients: 客户端数量 shard_size: 每个客户端占有的类别数越小 non-IID 程度越强 rng np.random.RandomState(seed) labels np.array(labels) sorted_idx np.argsort(labels) # 按类别排序 shards np.array_split(sorted_idx, num_clients) client_indices {} for k, shard in enumerate(shards): # 每个 shard 内部再随机打乱避免同一客户端的样本永远按原始顺序排列 client_indices[k] rng.permutation(shard).tolist() return client_indices代码逻辑先按标签排序再平分给客户端使同类别的样本集中在少数客户端手里模拟真实世界的偏斜分布。参数说明shard_size在这里只是一个命名提示实际用于控制每个客户端持有样本的类别集中度——shard_size 越小客户端类别越单一non-IID 程度越高。如果你需要更精细的非 IID 控制常见方案是用狄利克雷分布为每个客户端采样类别比例alpha越小分布越偏斜。提示判断你的切分是否有意义可以打印每个客户端各类别样本占比矩阵一眼就能看出是不是“看似切了、实则还是均匀”的假 non-IID。拿到client_indices之后用一行torch.utils.data.Subset(dataset, indices)就能构建出每个客户端各自的 DataLoader后续所有客户端本地训练都基于这个子集进行。3.2 客户端本地训练用 PyTorch 写一个可复用的本地更新函数接下来是客户端本地训练。我以 PyTorch 为例但逻辑同样适用于 TensorFlow 或纯 NumPy 实现。关键在于训练函数只依赖「输入全局权重 本地数据加载器」不感知其他客户端的存在。这样无论你后面想串行跑还是并行跑这个函数都可以原样复用。import torch def client_local_train(model, global_weight, train_loader, lr0.01, local_epochs1, devicecpu): 客户端本地训练函数 model: 模型实例每个客户端调用前由外部创建 global_weight: 服务端下发的全局模型权重OrderedDict train_loader: 该客户端本地数据的 DataLoader lr: 本地学习率 local_epochs: 本地训练轮数 device: 运行设备cpu/cuda model model.to(device) model.load_state_dict(global_weight) # 必须加载全局权重不能从头初始化 model.train() optimizer torch.optim.SGD(model.parameters(), lrlr) criterion torch.nn.CrossEntropyLoss() for epoch in range(local_epochs): for inputs, targets in train_loader: inputs, targets inputs.to(device), targets.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, targets) loss.backward() optimizer.step() return model.state_dict()这段代码的逻辑比表面看上去要重要。load_state_dict这一步决定了每个客户端是从全局模型开始继续训练而不是从随机初始化出发——后者等于每个客户端训练了一个完全独立的模型聚合没有任何意义。训练循环本身与普通监督训练没有区别唯一的差异是训练数据是本地的子集。参数说明local_epochs1时每个客户端只遍历本地数据一遍通信频率最高lr在联邦场景通常要比集中式训练小一些因为多客户端平均会在迭代过程中引入更大的隐式批量尺度。注意如果不显式调用model.train()PyTorch 在加载权重后可能默认保持 eval 模式BatchNorm/Dropout 行为异常聚合后的模型表现会出现令人困惑的波动。3.3 服务端聚合与完整模拟循环FedAvg 的加权平均实现与调度逻辑服务端聚合函数在 2.1 节已经给出这里把它放进完整的轮次循环里同时加上「每轮只采样部分客户端参与」的逻辑这是横向联邦学习模拟中最重要的调度参数之一。真实系统里不是所有设备每轮都在线采样率就是用来模拟这种不确定性的。def run_federated_rounds(models_fn, client_data, num_clients, global_lr1.0, client_lr0.01, local_epochs1, sample_ratio1.0, num_rounds50, devicecpu, seed42): 横向联邦学习主循环 models_fn: 模型工厂函数返回未初始化的模型实例 client_data: 字典客户端 ID - DataLoader sample_ratio: 每轮实际参与的客户端比例1.0 表示全部参与 num_rounds: 通信轮次 rng np.random.RandomState(seed) global_model models_fn().to(device) global_weight global_model.state_dict() history [] for round_idx in range(num_rounds): # 1. 采样本轮参与客户端 num_participants max(1, int(sample_ratio * num_clients)) participants rng.choice(num_clients, sizenum_participants, replaceFalse) # 2. 各客户端本地训练 local_weights, local_sizes [], [] for client_id in participants: local_loader client_data[client_id] local_weight client_local_train( models_fn(), global_weight, local_loader, lrclient_lr, local_epochslocal_epochs, devicedevice ) local_weights.append(local_weight) local_sizes.append(len(local_loader.dataset)) # 3. 聚合 global_weight fedavg_aggregate(local_weights, local_sizes) # 4. 在全局测试集上评估略 history.append(evaluate(global_model, global_weight, valid_loader)) return global_weight, history代码逻辑每一轮先按sample_ratio采样参与客户端模拟真实系统中不是所有设备每轮都在线再逐客户端执行本地训练最后用 FedAvg 聚合之后在验证集上评估全局模型。参数说明global_lr是额外加入的一个缩放系数当你把聚合结果乘上一个小于 1 的系数时全局更新变得更保守这在客户端数据高度异构时能稳定训练sample_ratio直接影响每轮信息量值越小每轮计算开销越低但需要更多通信轮次才能收敛。值得注意的是models_fn()在循环里被反复调用每次创建独立的模型实例——这正是 2.2 节强调的抽象它避免了多个客户端共享模型实例带来的参数污染。这样三步走完一个本地模拟横向联邦学习的最小闭环就完整跑通了。接下来考虑里面最重要的超参数怎么设。4. 参数怎么设客户端数量、本地轮次、学习率与批量大小的联动关系4.1 客户端数量与采样率训练开销与收敛速度的权衡客户端数量在横向联邦学习里是很微妙的参数。总数据量不变切分的客户端越多每个客户端的数据就越少本地训练的统计波动就越大。反而是客户端数量较少时每个客户端的数据更充足本地模型质量更高但 non-IID 程度通常更严重。我常见的一组参考配置表格如下参数典型值对结果的影响客户端数量 K5~20模拟、50~100大规模K 越大单客户端数据越少本地模型越不稳定采样率 sample_ratio0.1~1.0越低通信开销越小但收敛可能需要更多轮次本地轮次 local_epochs1~5越大通信越少但容易 client drift客户端学习率0.001~0.05需要随 local_epochs 增大而相应降低批量大小16~64与 local_epochs 联动影响本地更新步数这个表可以帮助你做初选。K 值特别极端比如 K100时有的客户端可能只有几个样本本地训练几乎等于噪声更新一定要配合采样率避免每轮都让全体参与。采样率的设计逻辑在于模拟真实系统里的设备可用性。真实系统中每轮只有一部分设备处于空闲、电量充足、网络联通的可用状态。在模拟器里sample_ratio0.5表示每轮随机挑一半客户端参与这既能节省算力也提高了每轮之间的随机性间接相当于一种正则化。如果你发现训练曲线抖动剧烈可以试着调低采样率并增大轮数看看是否因为高采样率下的过拟合了某些客户端的数据。4.2 本地迭代轮次与学习率local epoch 不是越大越好这是新手最容易翻车的地方。直觉上本地训练越多客户端模型应该越好但联邦学习的聚合机制决定了本地训练步数过多会导致“客户端偏移”client drift——每个客户端模型都朝着自己的数据分布过度优化聚合后全局模型反而被拉偏。我自己的经验是local_epochs 在 1 到 5 之间比较常见具体选多少取决于数据分布。IID 场景下可以取较大的 local_epochs比如 5减少通信轮次non-IID 场景下建议从 1 开始优先保证聚合的稳定性再逐步增加观察测试曲线变化。关键联动关系是学习率。本地训练步数与批量大小和 local_epochs 的关系可以用公式粗略表示# 本地更新步数近似 # num_local_steps ≈ (样本数 / batch_size) * local_epochs # 等效全局批量 ≈ num_local_steps * batch_size * num_clients_per_round这个公式的意义在于联邦学习的每一步全局更新实际上用了非常多“本地累积”的信息。等效全局批量远大于普通集中训练所以学习率不能直接沿用集中式的经验值比如 0.1通常要下调一个数量级到 0.01 甚至 0.001。如果你观察到训练曲线发散或者先降后爆先检查一下是不是 learning rate 没有针对本地迭代步数做调整。4.3 批量大小与本地更新步数的联动batch size 对联邦训练的非线性影响在很多人的直觉里批量大小的变化不会带来本质差别但在横向联邦学习中它是与 local_epochs 强相关的超参数。本地迭代步数等于数据量除以批大小再乘以本地轮数批大小减半等于本地步数翻倍相当于隐式地把 local_epochs 加倍。这带来一个比较隐蔽的坑如果你在实验 A 中设 batch size 为 64、local_epochs 为 1在实验 B 中设 batch size 为 32、local_epochs 仍为 1实际上训练量并不一样B 的每个客户端本地更新步数翻倍了。比较实验时要把总本地训练量对齐或者明确记录「每轮的等效步数」否则实验结果被混淆。此外批大小影响优化噪声水平。小批量带来更大的梯度噪声在 non-IID 场景下会让 client drift 更严重。我一般的做法在 non-IID 模拟中优先选择较大的 batch size比如 64减少本地梯度方向的偏移在 IID 场景下可以放小到 32 或 16增加随机性有时反而带来更好的泛化。如果你发现训练曲线震荡剧烈并且最终准确率低先尝试增大 batch size 看看是否缓解。注意在跨客户端聚合时sample_ratio 和 batch size 共同决定每轮全局模型的“平均梯度”质量。两者不宜同时取极值——比如采样率 0.1 加 batch size 16会使得单轮的信息量极低模型基本在噪声中徘徊。5. 本地模拟避坑盘点5 个让新手翻车的场景与排查思路5.1 全局模型不收敛学习率与本地轮次的组合失控现象训练轮数增加全局测试准确率没有明显上升甚至下降loss 曲线震荡剧烈。原因最常踩的是两个参数同时失控学习率过大和 local_epochs 过大。在 non-IID 数据下每个客户端的本地优化方向已经偏离全局最优local_epochs 越大偏离越远服务端聚合得到的模型像在多个方向之间来回“拉锯”。此时如果学习率再按集中式训练的习惯设 0.1全局模型被反复拉扯后偏移会更严重。解决把客户端的本地学习率降到 0.01 或更低同时把 local_epochs 降到 1先让系统稳定收敛再逐步放大参数观察边界。如果仍然震荡可以给聚合函数加一个全局学习率系数比如乘以 0.5让每轮更新更平滑。5.2 聚合后模型崩溃权重归一化与客户端权重设置现象聚合后的全局模型在验证集上准确率接近随机猜测甚至输出 NaNloss 显示 inf。原因最常见的是聚合时没有按样本数归一化。如果直接对参数做简单平均而非加权数据量最小的客户端与最大的客户端对全局模型的贡献一样模型会被极小样本训练出的噪声参数带崩。NaN 则通常来自学习率过大导致的梯度爆炸或不稳定的批量归一化统计量。解决严格按样本占比加权聚合并且校验total_size 0。在聚合函数里加一个小 epsilon 防止除零。另外如果模型里有 BatchNorm 层在本地训练后要把 running_mean/running_var 也一并上传和聚合或者改用不依赖全局统计量的层如 GroupNorm。5.3 IID 切分太均匀模拟结果失去参考意义现象模拟中 IID 场景的准确率很高与真实生产环境表现差距很大导致你高估了算法能力。原因你在模拟器里切分的 IID 实际上把数据均匀地打乱分给了每个客户端每个客户端的本地数据分布与全局完全一致这是最理想的场景。真实世界的用户数据按行为聚类天然是 non-IID。解决至少跑一组 non-IID 作对照。最简单的是按标签排序切分要更精细就使用狄利克雷分布控制每个客户端各类别的比例alpha取 0.1~10能覆盖从极端 non-IID 到接近 IID 的连续谱系记录下alpha同收敛性的变化实验结论的可信度会明显提升。5.4 内存暴涨多个客户端模型副本的显存管理现象客户端数量调大后模拟器内存或显存使用直线上升甚至直接崩溃。原因每个客户端的本地训练都会创建一个新的模型实例如果这些实例同时保持在 GPU 显存或内存中K 个客户端就相当于 K 份模型参数。在 PyTorch 里如果训练函数返回值是state_dict()而你在循环里又保留了模型的引用模型不会被垃圾回收显存就一直增长。解决串行训练每一轮循环内部用完的模型显式删除或把返回值限制为纯参数张量的 dict 而不是模型对象。如果数据本身太大可以只把 batch 加载到 GPU模型保持在 CPU 上训练完成后再转回 CPU 保存权重这样模型本身就是 CPU 存储不影响后续 CPU 聚合。注意使用 PyTorch 时训练完一个客户端后最好调用torch.cuda.empty_cache()如果用了 GPU并尽量让client_local_train内部创建模型和优化器避免在函数外部持有引用。5.5 随机种子没固定实验重复性无从谈起现象同一个配置重复跑两次结果相差很大甚至收敛趋势都不同。原因横向联邦学习模拟中随机来源很多——数据切分的索引、模型初始化的权重、每轮参与客户的采样、DataLoader 的 shuffle 顺序。任何一个没有固定实验结果就不可复现。解决把所有随机源都固定下来。在代码入口设置torch.manual_seed(seed)、np.random.seed(seed)在切分数据和每轮客户端采样时都使用RandomState(seed)或generator传入而不是全局随机源。推荐的做法是把 seed 作为模拟器的唯一配置项记录在日志里实验对比时只改 seed、其他全不变再取多次运行的均值和方差。6. 从模拟到可信实验验证收敛、多种子运行与参数记录6.1 收敛性验证的三个关键信号模拟器跑通之后你需要回答一个问题这个实验结果是可信的吗我用三个信号来判断。第一训练集上的全局 loss 是否在整体下降。在每一轮聚合后把数据混合起来在全局训练集上算一次 loss如果这个值不下降说明聚合出了问题——这时再去看单个客户端本地 loss 其实没什么意义因为本地每个客户端都可以正常过拟合。第二测试集准确率曲线的形态。如果在几十个 round 内仍然处于上升期说明还没收敛需要增大通信轮次如果先升后降考虑是不是学习率过高导致的过拟合或模型震荡。还有个容易忽略的信号验证曲线波动幅度。如果相邻轮次的准确率抖动超过 5%说明客户端数据分布差异太大或者采样率过低结果不稳定。第三需要与集中式训练进行对比。集中式训练是最强的 baseline可以引入同一个模型在所有数据上集中训练的测试准确率。联邦学习的性能通常会略低一点点但如果相差过多考虑是不是 non-IID 程度过于极端。这个对比也能帮你判断 non-IID 模拟的合理性。6.2 实验配置记录与管理可复现的模拟器配置为了让实验真正可信我习惯把所有参数集中到一个配置对象中并在每次运行前把所有配置打印或写入 JSON。这个习惯能避免和朋友、同事来回确认参数时出现各种误解。def run_experiment(config): 运行一次完整实验并返回历史记录 config: 字典包含种子、客户端数、采样率、本地轮次、学习率等全部超参数 print([config], config) torch.manual_seed(config[seed]) np.random.seed(config[seed]) # 数据切分 client_indices ( iid_split(config[num_clients], config[seed]) if config[data_dist] iid else noniid_split_by_label(config[num_clients], config[seed]) ) # 构建 DataLoader、运行主循环……略这个函数的关键点是一张配置表中包含数据分布的类型、non-IID 的 alpha 参数或 cut 的类别个数、参与客户端数量、采样率、本地轮数、客户端学习率、全局学习率、批量大小、优化器类型、训练轮数、随机种子。这样每一次运行都是自包含的可以在同一代码库中重复执行多次实验并对结果取均值。提示在一次实验中我通常跑 3~5 个不同种子取测试准确率的均值和标准差。这样得到的结果才谈得上可复现写报告时更有说服力。最后一个习惯是不要追求复杂先简单。我是从固定一个 seed、K5、sample_ratio1.0、local_epochs1、lr0.01 开始跑通后再逐个放开限制。这个顺序让我在每一步都能清楚定位问题出现在哪里是数据切分的坑还是本地训练的坑还是聚合的坑。如果你第一次模拟横向联邦学习就同时上 non-IID、大量客户端、低采样率和高 local_epochs任何指标异常都很难解释。我的习惯是不做玄学调参把每次改动限制在一个参数以内。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

Java+SSM+Django仓库管理系统:从业务建模到并发控制实践

Java+SSM+Django仓库管理系统:从业务建模到并发控制实践

基于JavaSSMDjango的仓库管理系统,放在毕业设计和公司信息化项目里都是老牌选题了。但网上能找到的同类源码,大多数只是把增删改查套上一个库存表的壳子,真正能对接“明嘉新材料公司”这类生产制造场景的并不多。这篇内容不是列功能清单&…

📅 2026/10/10 16:07:57
YOLO实时物体检测实战:从齿条螺栓螺母裂纹数据集到TensorRT部署

YOLO实时物体检测实战:从齿条螺栓螺母裂纹数据集到TensorRT部署

简介:面向工业质检与计算机视觉开发者的YOLO实时物体检测工程包,聚焦齿条、螺栓、螺母及裂缝等目标的识别与定位,适合有深度学习基础的开发者进行算法研究或项目移植;YOLO本身将检测任务转化为单个回归问题,通过网格与…

📅 2026/10/10 16:02:55
用C#与easyHook实现Win32 API Hook:程序行为监控与远程注入实战

用C#与easyHook实现Win32 API Hook:程序行为监控与远程注入实战

简介:这是一份C# EasyHook库的完整使用示例工程,面向需要在运行时实现跨进程函数拦截与注入的.NET开发者,适合对Windows钩子机制有一定了解、希望快速上手EasyHook的读者。包内包含WinForms测试窗口、类库工程与可运行Demo,覆盖了…

📅 2026/10/10 16:02:55
MORE NEWS

更多资讯

📰

高光谱遥感影像分类实战:Python实现(2D)²PCA降维与双通道CNN-SVM融合

简介:本资源面向高校学生与开发者,提供一套基于Python的高光谱遥感影像识别与分类完整项目,适用于毕业设计、课程设计及项目开发等场景。项目围绕高光谱影像分类中的特征冗余与泛化能力不足等问题展开,涵盖基于波段组合(2D)PCA的降…

📰

ASP+Access校园新闻发布系统源码解析:从结构到部署避坑指南

简介:基于ASPAccess的校园新闻发布管理系统,是一套采用B/S架构的完整项目源码与配套文档,面向高校学生、ASP初学者以及有课程设计、毕业设计参考需求的开发者。系统涵盖前台新闻展示、后台分类管理、信息发布、图片上传等核心功能&#xff0c…

📰

基于用电时序数据的家庭占用检测:特征工程与分类建模实践

简介:这是一份数据科学方向硕士毕业设计项目代码,聚焦通过智能电表电力消耗数据检测家庭人员占用状态,面向从事机器学习、特征工程研究的开发者与相关领域学习者。项目基于ECO开源数据集,验证了功耗数据作为家庭占用预测指标的可行…

📰

海面舰船红外与可见光图像配准:从选型到避坑的工程实践

简介:这份PDF文献聚焦海面舰船红外与可见光图像配准这一计算机视觉与图像处理领域的技术难点,面向从事目标检测、图像配准与目标识别研究的科研人员、研究生及毕业设计学生。资源包内仅含1个PDF文件,约792KB,完整收录了发表于《红…

📰

微信小程序在线课堂毕设全解析:SSM框架实战与避坑指南

简介:这份资源是面向毕业设计场景的在线课堂微信小程序完整项目,基于微信小程序前端与SSM(SpringSpringMVCMyBatis)后台框架、MySQL数据库开发,适合需要完成类似课题的计算机相关专业学生参考。项目覆盖管理员、教师、…

📰

用 Solidity 写一个待办事项合约:从需求到代码的完整思考过程

上一篇我留了一道自测题:写一个管理"待办事项列表"的合约,支持添加、完成、删除、查询,每个待办有创建时间戳和完成状态,只有创建者能操作自己的待办。这一篇就是这道题的完整解答。但我不想只给你一份能跑的代码——我…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

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

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

📞 💬