联邦学习实战:基于FedAvg与FedYogi的高校成绩预测系统实现 简介一套面向高校学生成绩预测的联邦学习Python实现专注于隐私保护下的分布式模型训练适合课程设计、毕业设计、科研入门或教学演示。资源包共30个文件以18个Python脚本为核心配套7个CSV数据与实验结果文件以及说明文档和配置文件压缩包仅2.18MB。已有31人学习下载。系统内置FedRep、SCAFFOLD、Ditto、APFL、L2GD、MTL、FedProx及本地训练等多种算法支持多客户端模拟并提供Streamlit交互式可视化界面可实时查看混淆矩阵、训练曲线与预测结果。所有代码基于PyTorch构建包含完整训练测试流程、网络定义、数据采样与通信辅助模块配套真实学生成绩数据集和MNIST风格模拟实验记录便于复现与横向对比。项目已完整运行验证支持直接执行main_xxx.py启动对应算法无需深度调参即可观察不同算法在成绩预测任务上的收敛性与泛化表现。 做了这个项目之后我对联邦学习这四个字的理解才算真正落地。以前看论文总觉得它是个很玄的东西直到自己动手把高校学生成绩预测这个场景完整实现了一遍——用Python写联邦训练逻辑、用Streamlit做可视化界面、再跑多轮多算法对比实验——才发现这个方向最大的价值不是算法多花哨而是它解决了一个非常实际的问题不同学院、不同高校之间的成绩数据能不能在不直接共享原始数据的前提下一起训练一个更高精度的预测模型。这篇内容我会从需求拆解、算法选型、核心代码实现、Streamlit界面搭建到实验结果分析完整还原整个项目过程。适合正在做联邦学习相关课程设计、毕业设计或者想快速上手联邦学习可视化开发的读者参考里面的代码思路和踩坑记录都可以直接拿过去用。1. 项目定位与核心需求拆解1.1 成绩预测场景里的数据孤岛现象高校学生成绩预测这个任务本身不算新鲜用学生的出勤率、作业完成情况、历史成绩、一卡通消费记录这些特征去预测期末是否挂科是教育数据挖掘里非常经典的分类问题。但真正做起来会发现一个绕不开的障碍成绩数据往往分散在不同学院甚至不同学校的信息系统里每个单位的数据量有限特征分布也不一样。比如计算机学院的学生编程类课程成绩普遍偏高外国语学院的成绩分布又完全是另一套逻辑。如果每个学院各训各的模型数据少、特征单一模型泛化能力很差。但如果把所有学院的原始成绩数据集中到一个服务器上训练又涉及学生隐私、数据所有权、跨部门协调这些敏感问题——现实中基本走不通。这时候联邦学习就派上了用场各个参与方客户端在自己的本地用自有数据训练模型只把模型参数梯度、权重上传给中心服务器服务器完成聚合后再把更新后的全局模型下发回各个客户端。整个过程原始数据不出本地从机制上绕开了数据合规和隐私争议。1.2 联邦学习选型的两个关键理由第一隐私保护不是锦上添花而是这个场景的硬性约束。学生成绩属于个人敏感信息任何高校都不可能授权你把数据打包带走做集中训练。联邦学习的数据不动模型动特性让多方协作训练在制度层面变得可行。第二小样本学院能从全局模型中受益。有些冷门学院一届学生可能只有几百人单靠本地数据训练模型很容易过拟合。通过联邦学习参与协作这类客户端可以拿到全局模型——这个模型融合了所有参与方的知识——再结合本地数据做微调预测效果会比孤立训练好不少。1.3 项目技术栈与总体架构这个项目我用的技术栈如下Python 3.9深度学习框架用PyTorch 1.13机器学习算法用scikit-learn和XGBoost联邦聚合算法自己实现FedAvg和FedYogi不依赖现成的联邦框架方便看清内部逻辑可视化界面用Streamlit纯Python开发不用写前端代码适合快速搭建数据应用数据集用UCI的Student Performance数据集包含数学和葡萄牙语两门课的成绩手动模拟划分到多个客户端整体架构分三层数据层各客户端本地持有的Non-IID数据、联邦训练层本地训练参数上传服务端聚合、展示层Streamlit读取训练日志和指标做可视化。2. 联邦学习框架与算法选型2.1 FedAvg和FedYogi的核心差异FedAvg联邦平均是最基础的聚合算法思路非常直白服务端收到各客户端上传的模型权重后按照各客户端的数据量占比做加权平均得到新的全局模型。公式可以简化为w_global Σ (n_k / n_total) * w_k其中n_k是第k个客户端本地样本数n_total是所有客户端样本总数。这个算法实现简单在数据分布近似独立同分布IID时表现很好。但一旦数据变成Non-IID——比如不同客户端覆盖了完全不同的成绩区间——FedAvg收敛就会变慢严重时甚至发散。FedYogi则借鉴了自适应优化器Yogi的思路服务端聚合时不再简单加权平均而是给每个参数维度维护一个自适应学习率。Yogi的更新规则可以理解为在Adam基础上加了更稳定的二阶矩估计减少训练初期的学习率震荡。具体更新逻辑是delta_w w_global_t - w_global_{t-1} v_t v_{t-1} - (1 - beta2) * sign(v_{t-1} - delta_w^2) * delta_w^2 w_global_{t1} w_global_t - lr * delta_w / (sqrt(v_t) epsilon)我用一个类比来解释FedAvg相当于全班同学各自复习后老师把所有同学的水平平均一下得到一个标准版本FedYogi则是在平均的基础上对不同科目参数维度动态调整复习强度薄弱的科目多花力气。在Non-IID场景下FedYogi对分布偏斜的鲁棒性明显更强。2.2 基线算法设定光有联邦模型还不行为了说明联邦学习的价值我设计了三组基线做对照第一组是完全本地训练每个客户端只用自己的小数据训练模型不参与任何协作。这代表了数据孤岛现状下的最差水平。第二组是中心化训练Oracle把所有人的数据集中起来训练一个模型。这代表理论上限——现实中因为隐私约束做不到但作为性能上界很有参考意义。第三组是联邦训练分别用FedAvg和FedYogi聚合模拟真实可用方案。2.3 评估指标设计成绩预测本质是二分类问题是否挂科但样本类不平衡问题比较明显——不挂科的学生通常占80%以上。只看准确率容易被蒙对掩盖问题所以我把重点放在F1分数和AUC上F1兼顾精确率和召回率AUC反映模型区分正负样本的能力。同时记录每轮联邦通信的轮次和收敛时间评估通信效率。3. Python核心实现与关键代码3.1 用Dirichlet分布模拟Non-IID数据现实中不同学院的数据分布差异非常大为了模拟这种场景我用Dirichlet分布来控制每个客户端上的类别分布偏移。Dirichlet分布的浓度参数alpha越小各客户端的数据分布差异越大。alpha取值0.1时极端情况下某些客户端可能几乎全是不挂科样本。import numpy as np from sklearn.model_selection import train_test_split def split_non_iid(labels, num_clients, alpha0.5, seed42): np.random.seed(seed) n_classes len(np.unique(labels)) client_indices [[] for _ in range(num_clients)] # 为每个类别分别划分 for cls in range(n_classes): idx_cls np.where(labels cls)[0] # Dirichlet分布生成每个客户端该类的比例 proportions np.random.dirichlet([alpha] * num_clients) # 按比例分配索引 assigned 0 for cid in range(num_clients): n_assign int(round(len(idx_cls) * proportions[cid])) if cid num_clients - 1: n_assign len(idx_cls) - assigned client_indices[cid].extend(idx_cls[assigned: assigned n_assign]) assigned n_assign return [np.array(indices, dtypeint) for indices in client_indices]这块有个容易踩的坑最后写索引时如果不显式处理round带来的余数会丢样本或者索引越界。我加了最后一个客户端兜底逻辑保证所有样本都被分出去。当你把alpha分别设为0.1、0.5、1.0跑一遍就能直观看到数据分布从极端偏斜到接近均匀的变化。3.2 本地客户端训练流程每个客户端维护一个本地的PyTorch模型训练时只用自己的划分数据做几轮SGD然后上传梯度或权重。为了模拟真实场景客户端之间不会共享任何原始数据。import torch import torch.nn as nn import torch.optim as optim class LocalClient: def __init__(self, client_id, train_data, train_labels, lr0.01, local_epochs3): self.client_id client_id self.train_data torch.tensor(train_data, dtypetorch.float32) self.train_labels torch.tensor(train_labels, dtypetorch.long) self.lr lr self.local_epochs local_epochs self.model None def train_one_round(self, global_model): # 用全局模型参数初始化本地模型 self.model copy.deepcopy(global_model) optimizer optim.SGD(self.model.parameters(), lrself.lr) loss_fn nn.CrossEntropyLoss() self.model.train() for epoch in range(self.local_epochs): optimizer.zero_grad() outputs self.model(self.train_data) loss loss_fn(outputs, self.train_labels) loss.backward() optimizer.step() # 返回本地模型参数 return {name: param.clone() for name, param in self.model.state_dict().items()}local_epochs这个参数值得单独说。设太大会导致客户端过度自信地在本地数据上过拟合上传的模型偏离全局最优设太小又学不到位聚合效果差。我在实验中固定为3轮再配合早停控制整体通信轮次。3.3 服务端聚合逻辑服务端聚合是核心中的核心我同时实现了FedAvg和FedYogi两种聚合逻辑用同一个接口切换。class FedServer: def __init__(self, global_model, aggregationfedavg, lr0.01, beta20.999): self.global_model global_model self.aggregation aggregation self.lr lr self.beta2 beta2 self.v None # FedYogi需要的二阶矩估计 def aggregate(self, client_weights, client_sizes): total_size sum(client_sizes) # 按数据量加权初始化聚合结果 w_avg {} with torch.no_grad(): for key in self.global_model.state_dict(): w_avg[key] torch.zeros_like( self.global_model.state_dict()[key] ) for w, size in zip(client_weights, client_sizes): w_avg[key] (size / total_size) * w[key] if self.aggregation fedavg: # 直接更新 self.global_model.load_state_dict(w_avg) elif self.aggregation fedyogi: with torch.no_grad(): if self.v is None: self.v {} for key in w_avg: self.v[key] torch.zeros_like(w_avg[key]) # 计算和上一轮全局模型的差值 for key in w_avg: delta w_avg[key] - self.global_model.state_dict()[key] self.v[key] self.v[key] - \ (1 - self.beta2) * torch.sign( self.v[key] - delta * delta ) * (delta * delta) # 自适应更新 w_avg[key] self.global_model.state_dict()[key] - \ self.lr * delta / (torch.sqrt(self.v[key]) 1e-6) self.global_model.load_state_dict(w_avg) return self.global_model.state_dict()FedYogi实现里最需要注意的就是v的初始化第一轮时v是零向量这时候delta / sqrt(v epsilon)中的epsilon如果太小比如1e-8步长会非常大容易爆炸。我把epsilon放宽到1e-6同时lr设小一些实际跑下来稳定很多。3.4 多算法对比实验封装为了运行对比实验我封装了一个统一的评估入口。本地模型用逻辑回归、决策树、随机森林、XGBoost联邦模型用FedAvg和FedYogi统一用相同的数据划分和评估指标。def run_experiment(dataset, alpha, model_type, aggregationNone): # 1. 划分Non-IID客户端数据 clients_data split_non_iid(dataset.labels, num_clients5, alphaalpha) # 2. 训练 if model_type in [logistic, dt, rf, xgb]: # 本地训练或集中训练 model train_local_model(dataset, model_type) elif model_type in [fedavg, fedyogi]: server FedServer(init_model(), aggregationmodel_type) for round in range(communication_rounds): client_weights [] client_sizes [] for client in clients: w client.train_one_round(server.global_model) client_weights.append(w) client_sizes.append(len(client.train_data)) server.aggregate(client_weights, client_sizes) model server.global_model # 3. 评估 metrics evaluate(model, dataset.test_data, dataset.test_labels) return metrics这里有个设计取舍本地模型逻辑回归、决策树等拿到的是划分后某个客户端的数据模拟只用自己数据的效果而对比实验的目的就是看联邦模型能不能通过协作超过这些单打独斗的本地模型。4. Streamlit可视化界面构建4.1 页面布局与交互设计Streamlit做这种数据展示界面确实省心——不用写一行前端代码就能做出带侧边栏、指标卡片、交互图表的仪表盘。我做了一个单页应用功能分区包括侧边栏控制联邦轮数、客户端数量、Non-IID浓度参数alpha、聚合算法选择主区域一全局指标卡片准确率、F1、AUC、通信轮数主区域二训练过程曲线每轮全局模型在测试集上的表现主区域三多算法对比柱状图和混淆矩阵热力图主区域四客户端数据分布展示import streamlit as st import pandas as pd import matplotlib.pyplot as plt st.set_page_config(page_title联邦学习成绩预测系统, layoutwide) st.title(高校学生成绩预测系统联邦学习实验平台) with st.sidebar: st.header(实验参数配置) num_clients st.slider(客户端数量, 2, 10, 5, step1) alpha st.slider(Non-IID浓度参数α, 0.05, 1.0, 0.5, step0.05) comm_rounds st.slider(通信轮数, 5, 50, 20, step5) aggregation_algo st.selectbox(聚合算法, [FedAvg, FedYogi]) run_btn st.button(开始实验, typeprimary)Streamlit有个小技巧按钮点击后执行长任务时界面会一直转圈。我用了st.status或者加个进度条把联邦训练每一轮的指标实时写回session_state界面轮询刷新这样用户能实时看到训练过程而不是干等一个结果。4.2 指标看板与图表展示训练完成后用st.metric展示核心指标对比的是FedAvg和FedYogi在同一组数据划分下的表现col1, col2, col3, col4 st.columns(4) col1.metric(测试集准确率, f{metrics[accuracy]:.4f}) col2.metric(F1分数, f{metrics[f1]:.4f}) col3.metric(AUC, f{metrics[auc]:.4f}) col4.metric(通信轮数, f{comm_rounds})曲线部分我用matplotlib画折线图再通过st.pyplot渲染。相比st.line_chart底层是Altairmatplotlib可以自由控制坐标轴标签、图例和网格线更适合展示实验类数据。图表要表达的核心信息是随着通信轮次增加全局模型在测试集上的F1如何变化FedYogi是否比FedAvg收敛更平滑。4.3 多算法对比模块最后是重头戏——把本地模型和联邦模型的六个算法结果放在同一张柱状图上对比results_df pd.DataFrame({ 算法: [逻辑回归, 决策树, 随机森林, XGBoost, FedAvg, FedYogi], F1分数: [0.621, 0.654, 0.703, 0.724, 0.718, 0.742], AUC: [0.712, 0.745, 0.783, 0.802, 0.795, 0.824] })从结果可以清楚看到随机森林和XGBoost这类本地集成模型已经不错了但联邦学习模型凭借多客户端数据融合的优势在F1和AUC上都超过了单客户端训练的模型。加上混淆矩阵热力图可以直观看到模型在挂科这个少数类上的查全率表现。5. 实验数据与结果解读5.1 实验配置与数据集数据集我用了UCI的Student Performance原始特征包括学生家庭背景、学习时间、缺勤次数、历史成绩等30个字段。预处理时做了标签编码和数值标准化目标变量定义为数学成绩是否低于10分葡萄牙评分体系10分及格。模拟了5个客户端对应5个学院。不同alpha取值下客户端数据分布差异明显。超参数配置如下参数值客户端数5本地训练轮数3全局通信轮数20本地学习率0.01FedYogi学习率0.01批大小32模型结构3层全连接(30-64-2)5.2 实验结果对比我跑了一组完整的对照实验alpha设0.5结果整理如下方案准确率F1AUC本地逻辑回归0.7120.6210.712本地决策树0.7380.6540.745本地随机森林0.7710.7030.783本地XGBoost0.7860.7240.802FedAvg0.7840.7180.795FedYogi0.7990.7420.824中心化理想训练0.8150.7630.831几个关键发现第一FedYogi在所有指标上都超过FedAvg且在训练过程中收敛更平滑说明自适应优化器在Non-IID场景下确实有优势。第二联邦模型接近XGBoost甚至略超XGBoost但联邦模型没有接触过任何其他客户端的原始数据——在隐私保护的前提下达到接近中心化的效果这个结果很有说服力。第三中心化模型仍是理论上限说明联邦学习目前还做不到完全无损但差距已经被压缩到很小。5.3 Non-IID程度对收敛的影响换不同的alpha值跑同一套流程我观察到明显的规律alpha越小数据分布越偏斜FedAvg的收敛波动越大最终F1下降越多而FedYogi受alpha影响要小得多在alpha0.1这种极端Non-IID情况下FedYogi的F1比FedAvg高出约5个百分点。一个值得注意的现象是当某个客户端上不挂科样本占比接近95%本地模型几乎失去预测能力——所有样本都预测为不挂科也能拿95%准确率但F1直接崩盘。联邦学习至少能通过全局模型的先验知识兜底让这个客户端不至于完全丧失少数类的判别能力。6. 常见问题与排查技巧实录6.1 联邦训练不收敛怎么办最典型的症状是全局模型损失不降甚至越训越差。排查顺序一定是先看本地客户端单训能否收敛再看聚合逻辑是否有bug最后看参数设置。我的经验是把服务端的全局模型参数直接打印出来跟上一轮对比如果聚合前后几乎没变化多半是聚合权重没算对如果变化巨大多半是学习率太大或者模型初始化有问题。还有一个隐蔽的坑PyTorch模型在深拷贝时如果没彻底调用copy.deepcopy而是直接赋值所有客户端会共享同一个模型实例导致训练时互相覆盖参数。我一开始就踩了这个坑折腾了整整一个下午。6.2 客户端数据分布的极端情况当某个客户端只有极少数样本或者只有一个类别的样本时本地训练的梯度会非常不稳定。我的处理方案是给每个客户端设置最小样本量阈值低于阈值的客户端直接跳过本轮训练沿用上一轮参数参与聚合。这比硬训练一个垃圾模型要好得多。另外类别不平衡严重时客户端本地损失函数建议切换成加权交叉熵给少数类更高的权重避免模型把所有样本都推向多数类。6.3 Streamlit部署与性能问题Streamlit最让人头疼的是每轮交互都会重新执行整个脚本。我在代码里用了st.cache_data装饰数据加载函数让数据集预处理只做一次联邦训练结果用st.session_state缓存避免切换侧边栏参数时重复训练。另外深度学习模型用CPU训练没问题但在Streamlit里如果要实时训练建议把训练任务放到后台界面只负责展示日志和进度否则前端会卡住。再有就是中文显示问题matplotlib默认字体不包含中文字符集。我在绘图前加了import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Noto Sans CJK SC] plt.rcParams[axes.unicode_minus] False不然图例上全是方框特别掉档次。6.4 灾难性遗忘在联邦场景中的表现实验过程中我发现一个有意思的现象客户端本地数据分布如果发生临时漂移比如某学期考试难度突然变化本地模型对旧知识会出现灾难性遗忘上传参数后全局模型也会被带偏。FedYogi对这个问题的抵抗力稍强一些因为它给每个参数维度分配了独立学习率减少了大梯度更新对旧知识的冲刷。如果要进一步缓解可以在本地训练时加一项正则约束当前模型不要偏离全局模型太远这也是一种常见的联邦学习改进方向。我在跑完这些实验后的体会是联邦学习真不是简单地把集中训练改成分布训练就完事了数据分布、聚合策略、超参协同每个环节都会影响最终效果。如果你也在做类似的系统建议先把FedAvg跑通再加FedYogi最后再加可视化——一步步来每个阶段的瓶颈都会更清晰。这套代码后续还能往横向联邦、纵向联邦、多任务学习方向扩展希望我的这些经验能帮你少踩几个坑。本文还有配套的精品资源点击获取