
简介面向联邦学习入门与毕设场景的Python实现方案围绕“简单完全去中心化联邦学习”给出了可运行源码与配套说明。代码覆盖数据加载与预处理、客户端/服务端模型定义、横向与纵向训练流程以及结果图保存等模块配置由YAML统一管理适合计算机、人工智能、通信工程等专业学生用于课程设计、项目演示或二次开发。压缩包共46个文件以20个Python脚本为主体另含15个pyc字节码、7张流程/结果图、1份README说明及YAML、CSV等辅助文件整体仅514KB结构轻量、目录清晰便于按模块查阅。已有144人浏览学习。包内自带README与示例数据训练结果图统一存放于res目录若运行遇到环境或依赖问题可通过私信获得远程指导。零基础读者可对照源码逐步理解去中心化联邦学习基本流程高年级学生也能以其为毕设初期基线。1. 去中心化联邦学习这份Python源码到底解决了什么联邦学习这几年被提得越来越多但多数开源代码是中心化联邦一个中心服务器收集客户端梯度再聚合服务器一挂整个训练就瘫。这份基于Python的完全去中心化联邦学习源码核心思路是把中心聚合节点去掉让客户端之间直接交换模型参数并完成聚合。项目同时提供横向联邦样本不同、特征相同和纵向联邦样本重叠、特征互补两条训练流程数据用波士顿房价集带配置文件、数据预处理模块和训练结果图。适合谁一类是做联邦学习课设、毕设的学生需要一份能跑通、能讲清原理的参考实现另一类是刚接触联邦学习的Python工程师想用最小代价看出两种联邦范式在代码层面的差异。它不是贴公式的demo是真能运行、能出图的工程结构。国内对联邦学习的系统化认知和杨强团队早期的梳理关系很大但真正动手时你会发现论文里的框架图落到代码上处处是细节。2. 架构与目录读源码前先弄清去中心化的两条实现路径2.1 中心化与去中心化的本质区别聚合点在哪联邦学习的概念框架是“数据不动模型动”这句话听起来简单但数据不动不代表训练流程一样。中心化联邦学习FedAvg 是典型代表有一个明确的服务器节点各客户端把本地训练得到的梯度或模型参数上传服务器做加权平均后再下发。这种模式实现最简单几乎所有联邦学习框架的第一版 demo 都是这个结构但它有两个绕不开的问题单点故障和服务器信任。服务器一旦宕机整轮训练停止服务器被攻破它返回的聚合参数可以悄无声息地毒化所有客户端的模型。去中心化联邦学习把“聚合”这个动作从中心节点分散到了每个参与节点。常见做法是 gossip 协议那一套每个节点训练完本地模型把参数广播给邻居收到多个邻居的参数后按规则融合再进入下一轮。这样做的优势是拓扑上不存在瓶颈增加节点不需要扩容服务器代价是收敛速度变慢而且节点间需要额外的校验机制来确认收到的参数没被篡改。这份源码的 train 目录下同时放了 train_horizontal.py 和 train_vertical.py分别对应横向和纵向两种数据划分方式。横向联邦假设参与方的特征维度一致、样本不同比如几家医院都有同一套检查指标但各自病例不同纵向联邦假设样本重叠、特征互补比如一家有用户基本信息另一家有消费记录服务的是同一批用户。很多读者把这两个概念搞混记住一句口诀横向切样本纵向切特征。这里要强调一个容易混淆的点去中心化不等于没有协调逻辑。即使没有中心服务器节点之间仍需约定参数交换的方式、聚合的时机和权重。源码里 utils 目录下有 file_utls 和 hash_utilsfile_utls 负责训练过程的文件读写hash_utils 负责参数哈希校验——节点收到邻居参数后先算哈希确认完整性和一致性再进入聚合。这个设计虽然简单但把去中心化场景里最关键的信任问题落到了代码层面这也是我推荐读者先读 utils 再读 train 的原因。2.2 目录结构逐层拆解train、model、configuration、utils 各管一段拿到 decentralized_federated_learning-master.zip 解压后第一件事不是急着跑而是把目录结构过一遍。这份源码的目录划分很干净适合直接当工程模板用目录/文件职责关键内容train/训练入口train_horizontal.py、train_vertical.pyres/ 存放训练结果图configuration/配置管理configuration.yml训练超参与路径集中管理data/数据加载与预处理preprocess.py、dataloader.py、boston 数据集model/模型定义client 相关模型结构每个参与方持有一份utils/工具函数file_utls.py 文件读写、hash_utils.py 参数哈希校验img/文档配图README 需要的架构图与界面演示图这个结构里最值得借鉴的习惯是把所有可调参数收敛到 configuration.yml而不是散落在训练脚本里。我拆过不少毕设源码最常见的问题就是超参数硬编码在训练函数里改一个学习率要翻三个文件、全局搜索三遍。这份源码把数据集路径、参与节点数、训练轮数、学习率、批次大小都收进一个 yml 文件训练脚本只负责读配置和执行复现成本低很多。train/res 目录单独拆出来放结果图所有模型训练完的 loss 曲线、精度对比都会落到这里方便横向比较。model 目录下是 client 模型相关代码说明模型是按“每个参与方各持有一份”的思路组织的。这是联邦学习的核心约束本地保留数据、本地训练模型只交换参数。data 目录里的 boston 是内置数据集preprocess.py 负责特征标准化和训练测试划分dataloader.py 把数据包装成可迭代的批次。整个链路从数据加载、模型训练到结果落盘是完整的工程闭环不是单文件实验脚本。README.md 放在根目录下载后建议先打开看一遍里面通常有运行顺序和结果图的说明能省掉不少猜谜时间。2.3 配置驱动训练configuration.yml 里的关键参数configuration.yml 是复现这份源码的入口我每次都是先读它再跑代码。横向联邦的配置项大致长这样# configuration/configuration.yml dataset: name: boston test_size: 0.2 random_seed: 42 federated: mode: horizontal # horizontal 或 vertical num_clients: 4 # 参与联邦训练的客户端数量 rounds: 50 # 联邦聚合轮数 local_epochs: 5 # 每个客户端本地训练轮数 batch_size: 32 optimizer: lr: 0.01 momentum: 0.9需要说明的是上面这段是我根据这份源码常见组织方式补的结构示例实际内容以你解压出的 configuration.yml 为准但参数项大体就是这些。mode 字段决定跑横向还是纵向是最先要看的值num_clients 决定把数据切分成几份rounds 是联邦聚合的总轮数local_epochs 是每轮聚合前客户端在本地迭代的次数。这组参数里最容易调错的是 local_epochs 和 rounds 的比例。local_epochs 太大客户端容易在本地数据上过拟合聚合后模型反而变差这是联邦学习里和灾难性遗忘相关的一个典型现象local_epochs 太小每轮通信的收益低总轮数就得翻倍。入门阶段用默认值跑通记录一轮 loss 曲线再单独调 local_epochs 看变化比一上来就大改参数靠谱得多。去中心化配置里通常还会有一个邻居相关参数比如每个节点每轮与几个邻居交换参数。这个参数在中心化联邦里不存在在去中心化场景下直接决定通信开销和收敛速度邻居太少参数传播慢收敛要更多轮邻居太多单轮通信成本上涨网络拥塞时反而拖慢训练。单机模拟阶段这个参数影响不明显但如果你把代码改造成多进程甚至多机部署它是第一个需要重新调的参数。提示配置文件里任何路径都建议以项目根目录为基准写相对路径。Windows 上反斜杠路径在 yml 里解析容易出错统一用正斜杠或相对路径最省事。3. 横向联邦跑通 train_horizontal.py看懂参数交换与聚合细节3.1 环境准备requirements.txt、Python 版本与虚拟环境先看项目根目录的 requirements.txt。这份源码的依赖不重主要是 numpy、pandas、scikit-learn、pyyaml如果模型部分用了 PyTorch会多一个 torch 依赖。如果你刚接触 Python 安装这一步我建议严格按下面的顺序走能省掉后面一半的报错# 创建虚拟环境避免污染系统 Python python -m venv fl_env # 激活环境 # Windows: fl_env\Scripts\activate # Linux/macOS: source fl_env/bin/activate # 安装依赖 pip install -r requirements.txt很多读者卡在第一步不是代码有问题而是依赖装到了系统环境里和其他项目冲突后整个环境不可用。虚拟环境是这类 Python 项目的后悔药装坏了直接删掉 fl_env 重建不影响系统 Python。注意 requirements.txt 如果没锁版本安装时大概率会拉到最新的 torch 或 sklearn这时要警惕接口兼容性——新版库可能已经移除了代码里调用的旧接口运行时报 AttributeError 或 ImportError看到这类错误先别怀疑源码先检查依赖版本。装完依赖后做一次冒烟验证确认核心库都能正常导入python -c import numpy, sklearn, yaml; print(numpy.__version__, sklearn.__version__)能打印出版本号说明依赖基本就位。这一步不是玄学是避免你把调试时间浪费在环境问题上。如果源码用了 torch把 torch 也加进验证语句顺带确认一下能不能调用 GPU——去中心化模拟的节点数一多CPU 训练会慢到让你怀疑人生。3.2 横向联邦的核心流程数据分片、本地 SGD、参数聚合跑通之前先理解 train_horizontal.py 在做什么。横向联邦的基本假设是每个客户端持有一部分样本特征维度相同。代码流程大体分四步读 configuration.yml确定客户端数量和数据切分方式。把 Boston 数据集按客户端数切成互不相交的样本分片。每轮聚合前各客户端在本地分片上做若干轮 SGD得到本地模型参数。客户端之间交换参数按聚合规则更新全局模型进入下一轮。聚合逻辑在代码里的核心部分长这样伪代码示意实际以源码为准# train_horizontal.py 聚合核心逻辑示意 def aggregate_parameters(models): # models: 各客户端返回的模型参数字典列表 # 每个参数字典形如 {weight: tensor, bias: tensor} aggregated {} num_clients len(models) for key in models[0].keys(): # 对每个参数张量做逐元素平均这是 FedAvg 最朴素的写法 aggregated[key] sum(m[key] for m in models) / num_clients return aggregatedaggregate_parameters 是横向联邦最基础的一轮操作取所有客户端模型的同名参数张量逐元素求平均。注意这里是参数级平均不是梯度级平均——参数级平均在通信轮次较少时更稳定梯度级平均则要求所有客户端从同一初始点出发否则梯度含义不对。num_clients 这个数不能乱改它必须和 configuration.yml 里的值一致否则数据切分份数和模型列表长度对不上聚合时要么下标越界要么平均值分母错误。完整的训练循环大概是这样的结构for r in range(config[federated][rounds]): client_models [] for client_id in range(config[federated][num_clients]): model train_local(client_id, global_params) client_models.append(model) global_params aggregate_parameters(client_models)每次循环开始前所有客户端从 global_params 初始化本地模型保证每轮聚合的起点一致。这是联邦学习收敛的隐含前提——如果每个客户端独立随机初始化聚合出来的模型等于在平均一堆毫不相关的参数训练直接震荡甚至不收敛这是新手最容易踩的坑。我在第一次跑这份源码时就是没注意这点改了 model 目录里的初始化逻辑结果十轮之后 loss 还在原地跳。3.3 跑训练命令与结果图解读环境就绪、流程清楚后直接跑横向训练# 从项目根目录执行 python train/train_horizontal.py跑完后去 train/res 目录看结果。源码里预置了参考结果图比如 configuration.png 和 image-20230806184146406.png你自己跑出来的图和它对比即可。正常的训练曲线应该是前几轮 loss 快速下降中段震荡收窄后期趋于平稳。如果 loss 曲线一路飙升或者锯齿大得看不清趋势优先检查两处一是 configuration.yml 里的 lr 是否偏大二是 data/preprocess.py 里的特征标准化是否正确。波士顿数据集如果不做标准化特征量纲差异会直接放大梯度训练发散得很快。标准化的标准写法是# data/preprocess.py 特征标准化示意 from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test)注意测试集只用 transform、不用 fit_transform这是防止测试集信息泄露的规范动作。很多毕设代码在这一步偷懒对全量数据统一 fit导致评测结果虚高答辩时被问到数据泄露会很难收场。这份源码把 preprocess 独立成模块就是为了让这条链路可检查、可复现。你在复现时也可以打印标准化前后的数据均值方差确认 scaler 真的生效了而不是在走一个空流程。提示train/res 里的结果图就是你判断训练是否正常的依据。建议每调一次参数就重命名或另存一份结果图方便对比学习率、轮数变化带来的影响。4. 纵向联邦train_vertical.py 的复现与特征切分预处理4.1 纵向联邦的适用场景特征分属不同参与方横向和纵向的区别一句话概括横向着眼于样本不共享、特征共享纵向着眼于样本重叠、特征互补。举例银行 A 有用户收入、年龄、职业特征电商 B 有同一批用户的消费频次、浏览行为两家服务的是同一批用户但各自只掌握部分特征标签比如信用风险只在银行 A 手里。纵向联邦要做的就是在不交换原始特征的前提下联合两边的特征训练一个更好的风控模型。这是纵向联邦最有价值的落地场景也是它比横向更难实现的原因。train_vertical.py 的实现思路和横向有本质区别横向联邦聚合的是完整模型的参数纵向联邦因为每个参与方只有部分特征没法独立训练完整模型。常见做法是各参与方各自计算中间结果比如特征嵌入或梯度的一部分再按规定协议交换协作更新模型。在完全去中心化场景下参与方通常轮流更新自己那部分参数把中间状态传给下一个参与方形成一条训练环路。这也解释了为什么纵向联邦代码比横向复杂不仅要管模型还要管特征对齐和中间结果的传递顺序。新手很容易把纵向联邦理解成“把数据拼起来训练一个模型”那就完全违背了联邦学习的隐私前提。纵向联邦的价值恰恰在于数据始终留在各自手里交换的只是中间计算结果。你在读 train_vertical.py 时重点看它交换了什么、没交换什么——凡是出现原始特征或标签直接传递的地方都要打个问号那是信息泄露的高危点。4.2 数据预处理链路preprocess.py、dataloader.py 与 Boston 数据跑纵向训练前先确认数据怎么准备。Boston 数据集在这个项目里同时承担横向和纵向的输入纵向模式下数据按特征列切分——比如前 6 列分给参与方 A后 7 列分给参与方 B标签单独存放。dataloader.py 负责把切分后的数据包装成批次# data/dataloader.py 纵向数据切分示意实际实现以源码为准 import torch from torch.utils.data import TensorDataset, DataLoader def load_vertical_data(X, y, split_index6): # split_index: 特征在第几列切开 X_a X[:, :split_index] X_b X[:, split_index:] dataset_a TensorDataset( torch.tensor(X_a, dtypetorch.float32), torch.tensor(y, dtypetorch.float32) ) dataset_b TensorDataset( torch.tensor(X_b, dtypetorch.float32), torch.tensor(y, dtypetorch.float32) ) return DataLoader(dataset_a, batch_size32), DataLoader(dataset_b, batch_size32)split_index 是纵向联邦里最关键的参数决定特征在哪个位置切开。切得太偏某一方的特征信息量不足模型效果明显下降切得太平均也不一定最优因为 Boston 数据集的 13 个特征对房价预测的贡献差异很大有些特征和房价的相关性接近零切给谁都会拉低那一方的信息质量。我一般会先算一下各特征与目标变量的相关系数再决定切分位置而不是盲目从中间一刀切。这一步用 pandas 的 corr 函数就能做成本极低。预处理链路里还有一个隐藏环节特征对齐。纵向联邦要求两边的样本是同一批人所以数据加载时通常先按样本 ID 对齐再把对齐后的特征矩阵切分。源码里 dataloader.py 的职责就是保证参与方 A 和 B 拿到的样本 index 一致。这个环节出错纵向训练会出现标签错配模型收敛到错误目标而且很难从曲线图上发现因为 loss 照样在下降。复现时我建议打印两个 DataLoader 的样本数确认一致再往下跑。4.3 训练结果验证与横向对比纵向联邦训练命令和横向类似python train/train_vertical.py跑完同样去 train/res 看结果。建议把横向和纵向的结果图放在一起对比重点看三点收敛速度、最终 loss、曲线平滑度。纵向联邦因为每个参与方只能看到部分特征理论上需要更多轮数才能达到横向的精度这是正常现象不代表代码有 bug。如果纵向结果和横向差不多甚至更好反而要警惕检查是否发生了信息泄露比如标签被同时喂给了两个参与方或者切分前做了全局归一化导致特征之间引入了本不该存在的关联。一个值得做的实验是把 configuration.yml 中的 mode 从 vertical 改成 horizontal其他参数不变跑同一个数据集记录两种模式的收敛轮次和最终指标。这个对照实验做完你对联邦学习两种范式的理解会比看十篇论文都深。数据预处理部分也可以对比横向模式下数据切分的是行纵向模式下切分的是列两种切法对应完全不同的通信协议这是这份源码最值得讲清楚的地方也是答辩时评委最爱追问的切入点。5. 避坑指南去中心化联邦学习五个常见问题与排查记录5.1 坑一hash 校验失败参数交换被拒绝现象跑 train_horizontal.py 时日志输出参数哈希校验失败训练直接中断看起来像网络问题但实际是本地校验报错。原因utils/hash_utils.py 负责对传输的参数做一致性校验。常见原因有两个一是各客户端模型初始化时引入了随机性导致两端参数哈希对不上二是浮点精度在不同操作系统上的截断位不同同样一次平均运算Windows 和 Linux 上算出的最后几位不一样精确哈希自然不一致。解决先在 model 初始化处固定随机种子import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)在训练脚本入口调用 set_seed()确保所有客户端从同一初始模型出发。哈希比较也建议从精确相等改成阈值比较比如用 np.allclose(a, b, atol1e-6)而不是直接比较字节。这属于联邦学习里经典的浮点一致性问题中心化联邦也存在但去中心化因为每对节点都在做校验暴露概率更高值得第一时间排查。5.2 坑二相对路径导致的 ModuleNotFoundError 与 FileNotFoundError 交替出现现象从项目根目录运行正常换个目录启动就报找不到 configuration.yml或者 import train 时报 ModuleNotFoundError。原因训练脚本里用了相对路径启动位置不同路径解析结果就不同。这是毕设源码最常见的通病作者在自己机器上的 IDE 里配好了工作目录换一台机器直接命令行启动就翻车。解决统一从项目根目录启动并在 train 脚本开头固定工作目录import os import sys PROJECT_ROOT os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, PROJECT_ROOT) os.chdir(PROJECT_ROOT)这段代码把脚本所在目录的上一级设为项目根加入模块搜索路径并把工作目录切过去之后所有相对路径都从根目录解析不会再受启动位置影响。我拆源码的习惯是拿到压缩包后先改这一个地方再跑任何脚本都不会因为路径报错。5.3 坑三requirements.txt 没锁版本新版本库接口不兼容现象依赖装完import 时报某个函数或属性不存在比如 sklearn 的 train_test_split 参数名变了torch 的某个接口被移除。原因requirements.txt 只写了包名没锁版本pip 默认装最新版而源码是按写代码时的旧版 API 写的。这类报错在 python 教程里很少被提到但实操中发生率极高尤其是 torch 这种迭代快的库。解决先看 README.md 有没有注明测试时的依赖版本没有的话根据代码里的 import 语句反查兼容版本。安装时锁定版本pip install numpy2 scikit-learn1.2.2 pyyaml装完重新跑冒烟验证。如果源码用了 torch还要确认版本和本机 CUDA 匹配这一步直接决定训练能不能用 GPU。我的经验是毕设类源码的 requirements.txt 大概率是写代码当时的版本快照别迷信最新版。5.4 坑四训练不收敛loss 在某个值附近震荡现象loss 曲线不下降或者降到一定值后出现剧烈锯齿每轮聚合后模型忽好忽坏。原因三个方向逐一排查。第一学习率偏大聚合后的模型在最优解附近来回跳跃第二Boston 数据未做标准化特征量纲差异放大梯度第三local_epochs 和 rounds 比例失衡本地过拟合导致每轮聚合都在抵消前一轮的成果这是联邦学习里灾难性遗忘的典型表现参与方多了之后会更明显。解决回到 configuration.yml把 lr 从 0.01 降到 0.001 试一轮同时确认 preprocess.py 里 StandardScaler 对训练集和测试集的处理正确再检查 local_epochs如果超过 10 而数据集又小果断降到 3 到 5。调参的顺序也重要先修标准化再降学习率最后动 local_epochs一次只改一个变量否则出了问题根本不知道是哪个参数导致的。5.5 坑五随机种子不固定两次跑出来的结果差异大现象同样的配置、同样的代码连续跑两次结果图和最终 loss 差别明显甚至出现一次收敛一次不收敛。原因数据切分、模型初始化、batch 采样每一步都带随机性。去中心化场景下节点间参数交换顺序还受进程调度影响随机性被进一步放大。解决把 set_seed() 固化成项目级习惯在 configuration.yml 里用 random_seed 字段统一管理。要注意这个字段要同时作用于数据切分、模型初始化两个环节缺一个结果都不可复现。复现性在联邦学习里比单机训练更重要因为多节点环境下你根本没法判断结果差异是算法问题还是随机抖动。我后来写所有实验脚本第一行永远是 set_seed。6. 进阶把单机模拟改成多进程节点外加一条验证联邦效果的笨办法单机跑通只是第一步。这份源码在单机上循环模拟了多个客户端但真实去中心化场景是每个节点独立存在。最小改动方案是用 Python 的 multiprocessing 把每个客户端放进独立进程用 Manager 共享参数模拟节点间交换from multiprocessing import Process, Manager def client_worker(node_id, shared_params, rounds): for r in range(rounds): # 本地训练返回本地模型参数 local_model train_local(node_id) # 写入共享参数池模拟去中心化节点发布参数 shared_params[node_id] local_model manager Manager() shared_params manager.dict() processes [ Process(targetclient_worker, args(i, shared_params, 50)) for i in range(4) ] for p in processes: p.start() for p in processes: p.join()这段代码把原来的客户端循环拆成了独立进程共享参数池模拟去中心化环境下的参数交换——每个进程只写自己的参数槽位聚合时统一读取。改成多进程后你会发现 hash_utils 的作用真正体现出来了进程之间的参数读取不再有单机模拟的确定性校验逻辑变成必需项而不是可有可无的装饰。验证联邦效果我有一个笨办法但非常有效先跑一个集中式 baseline把所有数据合在一起训练同一个模型记录指标再跑横向联邦同样记录两个数字的差距就是联邦化带来的精度损失。这个损失超过 10%说明聚合策略或本地训练轮数需要调在 3% 以内说明这套去中心化方案是健康的。做毕设答辩时这个对照实验比任何原理图都有说服力因为评审老师问的第一个问题永远是“联邦到底损失了多少精度”。从那以后我每次拆联邦学习源码都强制自己先跑 baseline 再跑联邦配置文件读取和随机种子设置两步从不开玩笑。数据切分、哈希校验、路径解析——这些坑踩过一次就能记住但不如一开始就用固定工作流把它们挡在门外。希望帮到你。本文还有配套的精品资源点击获取