
简介这是一套基于Python和Streamlit实现的联邦学习高校学生成绩预测研究项目面向具备一定Python基础的机器学习学习者、科研人员及毕业设计开发者。项目以多客户端本地训练加中央聚合的联邦机制预测学生成绩并内置FedRep、FedProx、APFL等多种聚合算法便于对比实验同时利用Streamlit搭建可视化界面可直接运行并观察训练过程与评价指标。压缩包共55个文件主要由18个Python源码及28个编译后pyc文件、7个CSV数据集、1个混淆矩阵PNG和1份Markdown说明文档构成整体仅2.1MB轻量易部署数据与模型均内含无需额外下载外部资源。目前已有160人学习浏览特别适合需要快速上手联邦学习应用、开展成绩预测仿真或进行毕业论文课题复现的读者便于在本地快速验证效果并扩展自定义算法。1. 为什么“成绩预测”需要联邦学习而不是直接训练一个模型把“高校学生成绩预测”和“联邦学习”放在一起第一眼看上去像是一篇学术论文的题目但拆开看这其实是一个很典型的隐私计算落地场景每所高校都握有自己的学生成绩、出勤、选课和日常行为数据但没有任何一方愿意把原始数据直接交给另一方——尤其是跨校联合建模时涉及学生隐私和教务数据归属权数据根本出不了校门。传统的做法是拉一份全校数据到一台服务器上训练LightGBM或XGBoost模型这在单校内没问题一旦跨校就卡死在数据共享上。联邦学习的思路刚好相反模型参数在各校本地训练只把更新后的权重上传到聚合端原始数据和特征工程全程留在校内。而Streamlit在这个方案里的角色是把“训练脚本”和“预测工具”包装成一个能点、能看、能调的Web界面让教务老师和课题组成员不必碰命令行就能完成试验和结果查看。本文要讲的就是这套方案怎么从零搭起来联邦训练框架怎么选、本地模型怎么写、Streamlit界面如何展示训练状态和预测结果以及最容易踩的坑在哪里。2. 联邦学习在成绩预测场景下的模型选型与通信机制联邦学习不是一种具体的模型结构而是一种分布式训练范式。它解决的是“数据不动、模型动”的问题参与方本地保存数据训练出的模型参数以加密或明文形式上传到服务端服务端聚合后再下发。高校学生成绩预测这个场景数据天然分布在多个院系或不同院校的教务系统中特征空间相似课程成绩、学分绩点、出勤率、课堂表现但样本ID几乎没有重叠这正好对应横向联邦学习Horizontal Federated Learning的标准设定。2.1 为什么横向联邦适合成绩预测而不是纵向联邦横向联邦要求各参与方的特征维度一致、样本不同纵向联邦则相反特征是分部在多个参与方的。成绩预测里各校的成绩表字段基本是同一个模板学号、课程编号、平时成绩、期末成绩、总评等级、出勤次数、作业提交率、参与课堂互动的频次等。这些字段在不同学校之间高度对齐只是具体数值分布有差异——有的学校给分偏高有的平时分占比不同这就是典型的横向联邦。纵向联邦在成绩预测中极少用到的原因也很直接它要求参与方之间有样本对齐的机制比如通过加密ID求交集而两所不同高校几乎没有重叠的学生样本对齐无从谈起。设计这套方案时应该先确认各参与方拿到的数据表字段是否一致字段含义是否统一这就是联邦学习里常说的“特征对齐”。字段不一致时不要急着上联邦框架先做字段映射和归一化否则后端的梯度聚合毫无意义。2.2 FedAvg聚合算法的具体实现与参数含义联邦学习最常用的基线算法是FedAvgFederated Averaging。它做的事情很简单每一轮训练中服务端把当前全局模型参数下发给K个客户端各客户端用自己的本地数据训练几个epoch然后把模型梯度或模型权重上传服务端按样本占比加权平均出一组新的全局参数。整个过程反复迭代直到全局模型的损失收敛或达到预设轮数。在实现上FedAvg的聚合公式可以用伪代码表达如下。# FedAvg 服务端聚合逻辑伪Python实现 def fed_avg(global_model, client_updates, client_sizes): # global_model: 当前全局模型含state_dict # client_updates: 各客户端本地训练后的state_dict列表 # client_sizes: 各客户端本地训练样本数列表 total_samples sum(client_sizes) aggregated {} # 按每层的参数名逐一加权平均 for layer_name in global_model.state_dict().keys(): weighted_sum None for update, size in zip(client_updates, client_sizes): weight size / total_samples if weighted_sum is None: weighted_sum update[layer_name] * weight else: weighted_sum update[layer_name] * weight aggregated[layer_name] weighted_sum return aggregated这段代码里最关键的是weight size / total_samples它表示样本量大的客户端对全局模型的影响更大。如果不按样本量加权而是简单求平均小样本的学校会和大样本的学校拥有同等话语权模型容易被数据量少的参与方带偏。另一个细节是这里聚合的是模型权重而非梯度FedAvg原始论文推荐的是聚合权重因为它在非独立同分布数据上的稳定性更好实现也更简单。2.3 成绩预测本地模型的选型对比本地模型的选择直接影响联邦通信的效率和最终精度。在学生成绩预测中常见的选择有三类逻辑回归、XGBoost/LightGBM等树模型、PyTorch实现的多层感知机MLP。三者在联邦学习场景下的表现差异明显。模型类型联邦适配难度通信开销精度表现推荐程度逻辑回归低参数为向量极小中等适合做基线XGBoost/LightGBM高树结构合并复杂极大高不推荐首版MLP多层感知机低原生支持较小高特征工程做好时推荐树模型在单机训练中往往精度最高但在联邦场景中树模型的聚合远没有神经网络方便。XGBoost的联邦版本需要传递直方图和分裂点不同客户端的特征分箱可能不一致实现复杂度会显著上升通信量也大。因此首版方案推荐直接用PyTorch或TensorFlow的MLP模型。成绩预测的特征基本是数值型的成绩、出勤率、提交次数MLP完全能拟合这种规模的表格数据。需要补充一句如果后续想要更高精度可以用联邦学习框架Flower配合LightGBM的分裂点聚合方案但那是进阶玩法第一版先跑通MLP FedAvg的流水线是更务实的路径。3. 搭建本地成绩预测模型与联邦训练服务端确认了横向联邦 FedAvg MLP的组合后下一步是搭建代码骨架。这里推荐用PyTorch作为深度学习框架配合Flower联邦学习库pip安装flwr来简化通信层的实现。如果你不想引入额外依赖也可以自己写socket通信但Flower已经把客户端注册、服务端启动、参数序列化这些问题处理好了用它能把精力集中在模型和数据处理上。3.1 成绩数据的预处理与特征工程最小实现在写任何模型之前数据预处理是决定成绩预测上限的环节。学生成绩预测常见的原始字段包括学号、课程编号、平时成绩、实验成绩、期中成绩、期末成绩、出勤次数、总课时、作业平均分。原始数据往往有缺失值比如某学生缺考、异常值成绩超过100分、不一致的单位。以下是最小可用的预处理代码。import pandas as pd import numpy as np def clean_score_data(df): # 删除学号和姓名等标识列避免模型学习到学生ID的规律 df df.drop(columns[学号, 姓名], errorsignore) # 成绩字段统一clip到[0, 100]超出范围视为录入错误 score_cols [平时成绩, 实验成绩, 期中成绩, 期末成绩] for col in score_cols: if col in df.columns: df[col] pd.to_numeric(df[col], errorscoerce) df[col] df[col].clip(0, 100) # 缺失值处理成绩缺失的学生直接用该课程的均值填充 for col in score_cols: if col in df.columns: mean_val df[col].mean() df[col] df[col].fillna(mean_val) # 构造两个派生特征总评倾向 平时*0.3 期中*0.3 期末*0.4 if all(c in df.columns for c in [平时成绩, 期中成绩, 期末成绩]): df[综合得分倾向] df[平时成绩] * 0.3 df[期中成绩] * 0.3 df[期末成绩] * 0.4 # 出勤率出勤次数 / 总课时 if 出勤次数 in df.columns and 总课时 in df.columns: df[出勤率] df[出勤次数] / df[总课时].replace(0, np.nan) df[出勤率] df[出勤率].fillna(0) return df预处理的核心逻辑有三点。第一直接删除学号、姓名这类标识列防止模型把特定学生ID当作有效特征也避免联邦场景下跨校ID关联导致的隐私问题。第二成绩字段统一clip到0到100之间这是最容易忽略的步骤因为不同学校的录入习惯不同有的会录成“优秀/良好”有的是百分制必须在进入模型前统一格式。第三构造“综合得分倾向”和“出勤率”两个派生特征它们比原始字段有更强的预测意义。3.2 用PyTorch实现一个可联邦化的MLP成绩预测模型MLP的结构不需要太深成绩预测的特征数量通常在10到20之间两层隐藏层已经足够。关键在于模型的定义要和Flower的联邦聚合逻辑兼容也就是模型的所有参数都必须存储在state_dict中不能有不可序列化的部分。下面是模型定义和本地训练函数。import torch import torch.nn as nn import torch.optim as optim class ScorePredictor(nn.Module): def __init__(self, input_dim, hidden_dim64): super().__init__() self.net nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Dropout(0.3), nn.Linear(hidden_dim, hidden_dim // 2), nn.ReLU(), nn.Linear(hidden_dim // 2, 1) # 输出连续值即预测的期末成绩 ) def forward(self, x): return self.net(x).squeeze(-1) def train_local_model(model, train_loader, epochs5, lr0.001): device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.MSELoss() optimizer optim.Adam(model.parameters(), lrlr) model.train() for epoch in range(epochs): total_loss 0.0 for features, labels in train_loader: features features.to(device) labels labels.to(device) optimizer.zero_grad() outputs model(features) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * features.size(0) epoch_loss total_loss / len(train_loader.dataset) print(f本地训练 Epoch {epoch1}/{epochs}, Loss: {epoch_loss:.4f}) return model.state_dict()这里的Dropout层在训练时生效在联邦聚合时不影响参数数量state_dict的key和value仍然是完整的Flower可以正常序列化传输。MSELoss用于回归任务成绩预测的目标值是0到100的连续分数如果想要预测“是否挂科”这种二分类目标把最后一层的输出维度改为2并换成CrossEntropyLoss即可。联邦学习场景下有一个和普通训练不同的点本地训练的epoch数epochs不宜设置过大。每轮联邦通信中客户端训练太多轮会导致各客户端的模型参数发散严重聚合后反而更难收敛。常见的做法是设置3到10个本地epoch配合较小的学习率0.001左右。3.3 在Flower中注册客户端并启动联邦训练Flower的核心概念是Client类和start_client函数。每个客户端需要实现三个方法get_parameters返回当前本地模型的参数fit执行本地训练并返回更新后的参数evaluate在本地验证集上评估模型。import flwr as fl import collections class ScoreClient(fl.client.NumPyClient): def __init__(self, model, train_loader, val_loader): self.model model self.train_loader train_loader self.val_loader val_loader def get_parameters(self, config): # 把模型参数转为NumPy数组列表 return [val.cpu().numpy() for val in self.model.state_dict().values()] def fit(self, parameters, config): # 把服务端下发的参数加载到本地模型 params_dict zip(self.model.state_dict().keys(), parameters) state_dict collections.OrderedDict( {k: torch.tensor(v) for k, v in params_dict} ) self.model.load_state_dict(state_dict, strictTrue) # 本地训练epochs由服务端config控制 epochs config.get(local_epochs, 5) train_local_model(self.model, self.train_loader, epochsepochs) # 返回更新后的参数和本地样本量 updated_params [val.cpu().numpy() for val in self.model.state_dict().values()] num_samples len(self.train_loader.dataset) return updated_params, num_samples, {} def evaluate(self, parameters, config): params_dict zip(self.model.state_dict().keys(), parameters) state_dict collections.OrderedDict( {k: torch.tensor(v) for k, v in params_dict} ) self.model.load_state_dict(state_dict, strictTrue) # 在本地验证集上计算RMSE self.model.eval() criterion nn.MSELoss() total_loss 0.0 with torch.no_grad(): for features, labels in self.val_loader: outputs self.model(features) total_loss criterion(outputs, labels).item() * features.size(0) avg_loss total_loss / len(self.val_loader.dataset) rmse avg_loss ** 0.5 return rmse, len(self.val_loader.dataset), {rmse: rmse}这段代码中fit方法的返回值有三个关键内容更新后的参数列表、本地样本数量num_samples、以及空的字典可扩展来传自定义指标。num_samples非常重要因为服务端的FedAvg聚合会用到它来计算加权权重。如果返回的样本数是错的整个聚合权重就会失真模型收敛方向也会出错。服务端启动联邦训练的代码更简洁只需要定义聚合策略。# 启动联邦训练服务端监听端口8080 python -m flwr.server --server_address 0.0.0.0:8080 --num_rounds 10 --strategy fedavg服务端启动后参与训练的客户端分别执行fl.client.start_client(server_address服务器IP:8080, clientScoreClient(...))就能加入联邦训练群组。这里有一个实际部署时很常见的坑Flower的客户端数量是动态的服务端会等待满足min_fit_clients数量的客户端参与当前轮次。如果配置的参与方是3个就启动3个客户端进程并保持在线不要中途断开否则该轮训练会一直等待直到超时。4. 用Streamlit搭建成绩预测的Web交互界面模型训练在终端里跑通了但对于高校课题组的场景使用者往往是教务老师他们需要的是一个能上传数据、点击按钮就能出预测结果的界面。Streamlit在这里的优势是纯Python实现、无需写前端HTML和JavaScript把训练好的模型加载进来配合st.file_uploader和st.dataframe就能做出一个完整的数据应用。4.1 Streamlit最小骨架上传数据并展示预测结果先做一个能用的单页应用上传成绩数据点击“预测”按钮显示预测成绩和真实成绩的对比图。import streamlit as st import pandas as pd import torch import matplotlib.pyplot as plt # 页面标题与说明 st.set_page_config(page_title学生成绩预测系统, layoutwide) st.title(基于联邦学习的高校学生成绩预测) # 上传数据文件 uploaded_file st.file_uploader(上传学生成绩CSV文件, type[csv]) if uploaded_file is not None: df pd.read_csv(uploaded_file) st.subheader(原始数据预览前10行) st.dataframe(df.head(10)) # 加载训练好的模型input_dim根据特征数量自动设置 input_dim len(df.columns) - 2 # 减去学号和真实成绩列如果有 model ScorePredictor(input_diminput_dim, hidden_dim64) model.load_state_dict(torch.load(global_model.pth)) model.eval() # 特征列选择与预测 feature_cols [c for c in df.columns if c not in [学号, 期末成绩]] X df[feature_cols].values.astype(float) X_tensor torch.tensor(X, dtypetorch.float32) with torch.no_grad(): preds model(X_tensor).numpy() df[预测成绩] preds.round(1) # 如果数据里有真实成绩展示对比散点图 if 期末成绩 in df.columns: st.subheader(预测值与真实值对比) fig, ax plt.subplots(figsize(8, 6)) ax.scatter(df[期末成绩], df[预测成绩], alpha0.6) ax.plot([0, 100], [0, 100], r--, label理想线 ax.set_xlabel(真实期末成绩) ax.set_ylabel(预测期末成绩) ax.legend() st.pyplot(fig)这段代码里的关键点是input_dim len(df.columns) - 2的假设数据中包含学号和真实期末成绩两列被排除在特征之外。实际使用时每个人的特征列不同应该用一个显式的特征列表来指定哪些列参与预测否则特征顺序一变化模型就完全失效。这个细节在Streamlit应用里尤其重要因为用户上传的Excel或CSV文件列顺序可能和训练时不一致。4.2 Streamlit展示联邦训练过程的实时指标除了做预测Streamlit还可以充当联邦训练的监控面板。在fit方法里返回的自定义指标比如每轮的RMSE可以以JSON格式写入一个文件Streamlit读取这个文件并刷新图表。import json import streamlit as st import pandas as pd # 读取训练日志文件 log_path training_log.json try: with open(log_path, r) as f: log_data json.load(f) except FileNotFoundError: st.warning(训练日志文件不存在请先启动联邦训练服务端) log_data {rounds: [], rmse: []} if log_data[rounds]: st.subheader(联邦训练过程中的RMSE变化) metrics_df pd.DataFrame({ round: log_data[rounds], rmse: log_data[rmse] }) st.line_chart(metrics_df.set_index(round)) else: st.info(暂无训练数据)这套做法的优势在于训练进程和Web进程完全解耦联邦训练可以跑在服务器上Streamlit跑在同一台或另一台机器上通过JSON文件或SQLite数据库交换状态。相比直接把Streamlit嵌入训练脚本这个方案的响应更快不会因为界面重绘而阻塞训练进程。4.3 联邦学习三个最需要调整的参数本地模型和Streamlit界面都就绪后真正需要花时间调的参数有三个每轮通信中本地训练的epoch数、参与联邦训练的最小客户端数、全局聚合轮数。这三个参数直接决定通信开销和模型质量。参数推荐初始值取值范围调整逻辑本地epoch数31–10太大导致本地过拟合、模型漂移太小则收敛慢min_fit_clients32–全部参与方值过大会使服务端长时间等待掉线客户端全局轮数2010–100数据分布差异大时需更多轮次收敛经验法则是先固定本地epoch为3全局轮数为20跑一遍基线记录RMSE然后逐步增加本地epoch为5、8对比每轮的收敛曲线。如果增加本地epoch后首轮RMSE反而大幅上升说明模型已经发生了“客户端漂移”应该回退转而增加全局轮数。注意联邦学习里最容易被忽略的参数是min_fit_clients和min_evaluate_clients它们不是同一个值。如果min_fit_clients设置为3但实际只有2个客户端在线服务端会一直等待直到报错或超时。在部署前应该用脚本模拟客户端掉线场景验证服务端的等待策略是否合理。5. 联邦训练结果的验证方法与实践建议模型跑完后不能只看训练损失要验证联邦训练是否真正有效。最直接的三步验证方式是第一记录全局模型在每轮结束后的RMSE确认单调下降趋势第二把联邦训练的最终模型与单校本地训练的模型在同一测试集上对比差距在10%以内即为可接受第三用Streamlit上传一份全新数据做预测检查输出分布是否合理。# 在服务端测试最终模型的预测误差 python -m flwr.client --server_address 服务器IP:8080 --cid 0验证时有一个实用技巧保存每轮聚合后的全局模型快照命名格式为global_model_round_{n}.pth。这样如果第15轮的模型比第20轮更好出现过拟合或数据漂移可以方便地回滚。回滚在联邦场景中比在单机训练中更重要因为各客户端的本地数据分布在训练过程中不会变化但模型可能在某些轮次被异常客户端上传的更新干扰。关于实践落地的三个建议。第一先做小规模试点两所学校各出2000条脱敏数据跑通全流程再扩展到更多参与方小规模试点时可以把num_rounds调大观察收敛趋势。第二把特征字典以JSON格式随模型一起下发和上传避免各参与方在特征顺序上各搞一套这是联邦系统最常见的隐性Bug。第三Streamlit界面里的预测结果至少要保留“概率值”或“误差区间”而不仅是一个点预测因为教务使用方需要知道哪些预测结果置信度低不能盲目用于学业预警。学生成绩预测这个课题本身并不新但“联邦学习 Streamlit”这个组合真正解决了跨校建模时的数据合规问题同时让非技术背景的使用者也能参与进来。如果你的课题正处于选题阶段这套方案在验证隐私计算能力和工程落地能力上都有足够的支撑。本文还有配套的精品资源点击获取