拓冰建站拓冰建站
首页 / 资讯中心 / 正文

联邦学习在高校成绩预测中的算法选型与工程实践

简介一套基于联邦学习的高校学生成绩预测项目适合计算机、人工智能等专业的学生用于毕设或课设也适合想学习联邦学习落地实践的开发者。项目基于Python实现通过Streamlit搭建了可视化平台可直接查看预测结果与模型表现。算法层面覆盖FedRep、FedProx、Ditto、APFL、Scaffold、L2GD等多种联邦学习策略并配有Nets、Update、train_utils等模块便于理解模型聚合与本地训练流程。压缩包共55个文件含18个Python源码、7份CSV数据集及实验记录、1张混淆矩阵图、1份README说明整体仅2.25MB轻量易部署目录划分清晰目前已有218人学习下载。代码均经过运行验证能提供完整的数据处理、模型训练、评估对比与可视化框架可在其基础上修改网络结构或数据集快速扩展新功能。1. 为什么高校成绩预测需要联邦学习一个毕设源码的真实切片高校学生成绩预测这个题目很多人第一反应是堆特征、上深度学习模型但换到真实校园场景就会撞墙成绩数据分散在各学院、各教学班甚至各教务系统里出于隐私和行政壁垒根本汇不到一个中心服务器。这个项目把联邦学习(Federated Learning)和成绩预测放到了一起让我眼前一亮的是它不止给了 FedAvg而是把 FedRep、Ditto、SCAFFOLD、L2GD、APFL 等算法以独立入口形式放在 main_local.py、main_ditto.py、main_fedrep.py 这些文件里并用 Streamlit 做结果展示。对想快速跑通联邦成绩预测的开发者来说这既是调参台也是一份可以对照源码学习各算法差异的活教材。2. 成绩预测中的联邦算法选型FedRep、Ditto、SCAFFOLD 与 L2GD 的取舍联邦学习项目最先要回答的问题不是“模型多深”而是“客户端之间数据不一致时全局模型还适不适用”。成绩预测的数据分布天然异构选错算法会让本地准确率远低于服务端报告的平均值。2.1 横向联邦的全局更新与本地个性化横向联邦学习假设每个参与方拥有同特征空间、不同样本。服务端维护一份全局模型每轮挑选若干客户端把当前模型下发客户端用本地数据跑几步梯度下降后再把参数或梯度传回服务端加权平均。到这里读者可能会觉得这与分布式训练没什么区别但核心差异在于客户端之间的数据分布不独立同分布(non-IID)。成绩预测正是典型的 non-IID 场景不同学院对给分松紧、课程难度、学生基础都不一样同一个全局模型很难同时让所有客户端都满意。针对这个问题学术界给出了多条路线。FedRep 把模型拆成共享表征层和本地分类头服务端只聚合表征层分类头留在本地继续训练。Ditto 则同时维护全局模型和本地模型在本地目标函数中加一项与全局模型的近端距离约束本地模型不要偏离太远。SCAFFOLD 用控制变量来修正客户端更新方向减少因为轮流训练产生的客户端漂移。L2GD 的思路更偏向优化和通信用本地梯度下降与全局梯度下降交替更新降低通信轮次如果再叠加偏置压缩本地更新在传输前只保留部分关键张量通信开销还能进一步下降。灾难性遗忘在联邦数据异构时很容易出现Ditto 和 FedRep 能在不同程度上缓解。2.2 从 main_*.py 理解项目支持的算法簇项目根目录的文件命名直接把实验入口暴露了出来。main_local.py 是纯本地训练基线main_scaffold.py、main_ditto.py、main_l2gd.py、main_fedrep.py、main_mtl.py、main_apfl.py 分别对应不同联邦算法。根目录还有 FedProx.py它提供 FedProx 的近端项实现可以嵌入其他入口。这种组织方式比把算法堆在同一个 train.py 里清晰实验时切换算法只需要换启动脚本。入口文件核心机制典型适用场景对成绩预测的切合点main_fedrep.py共享表征 本地分类头客户端特征相似但头部不同各学院共享基础知识本地适配评分尺度main_ditto.py全局/本地双模型 近端约束客户端数据异构明显防止本地微调后遗忘全局规律main_scaffold.py控制变量修正局部更新客户端轮流参与、数据漂移降低不同教学班带来的分布偏移main_l2gd.py本地与全局梯度下降交替通信受限需减少同步减少教务节点间的同步流量main_mtl.py多任务学习共享/私有参数多任务相关但不同将每个学院视为一个任务main_apfl.py模型插值学习用户偏好强调个性化按学生群体生成个性化预测头这张表的关键不是记住算法缩写而是知道它们在平衡什么全局模型的泛化能力与本地模型的个性化能力之间永远存在矛盾。成绩预测里没有哪个算法绝对最优只有“在这个数据切片下谁更稳定”。2.3 如何切换算法入口注册表与启动脚本多入口项目最常见的坑是不同脚本维护了不同超参最后实验结果对不齐。我一般会在项目外部包一层调度用算法名映射入口脚本保证命令行参数统一。import subprocess ALGO_SCRIPTS { local: main_local.py, scaffold: main_scaffold.py, ditto: main_ditto.py, l2gd: main_l2gd.py, fedrep: main_fedrep.py, mtl: main_mtl.py, apfl: main_apfl.py, } def run_algo(name: str, extra_args: str ) - None: if name not in ALGO_SCRIPTS: raise ValueError(f未知算法: {name}可选 {list(ALGO_SCRIPTS)}) script ALGO_SCRIPTS[name] cmd fpython {script} {extra_args} subprocess.run(cmd, shellTrue, checkTrue)这段代码把算法名映射到具体的入口文件并用subprocess拉起训练进程。extra_args用来追加统一参数例如--num_users 5 --frac 0.6 --seed 42。这样写的好处是批量跑算法对比时只需要在循环里改变name不会因为进错目录或拼错文件名而中断实验。回到成绩预测场景如果各学院在培养方案上高度一致只是期末评分尺度不同我优先试 FedRep因为它把共享知识固定在表征层每个学院只需要学习自己的线性头如果连课程内容都有差异Ditto 通过 λ 参数控制本地更新幅度λ 设太大会退化成纯本地训练设太小则接近 FedAvg。仓库里出现的accs_fedrep_mnist*.csv和losses_fedrep_mnist*.csv说明作者用 MNIST 做过算法验证换成成绩数据后不能直接照搬这些曲线但可以沿用同样的结果保存结构。3. 成绩数据预处理与模型构建从 CSV 到可联邦训练的样本划分跑通联邦训练之前数据划分是最容易翻车的环节。很多人把成绩表读进来就喂给模型结果本地验证集分数很高一到新学院就崩问题往往出在数据泄漏或 non-IID 模拟方式不对。3.1 数据文件的字段与初步检查data 目录下有两个数据文件>import pandas as pd df pd.read_csv(data/data-JSJfb1.csv) print(shape:, df.shape) print(df.dtypes) print(缺失值:\n, df.isnull().sum()) print(描述统计:\n, df.describe(includeall))通过dtypes确认哪些列是数值、哪些是类别describe能发现量纲差异和异常值。比如成绩 0 分和缺考 NaN 要区分有些系统会把缺考记为 0直接填充会污染分布。3.2 填充、标准化与训练验证泄漏问题成绩字段经常有缺失常见做法是用中位数填充。先切分再标准化这是防止数据泄漏的基本要求。from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split num_cols [attendance, assignment, midterm] df[num_cols] df[num_cols].fillna(df[num_cols].median()) X df[num_cols].values y df[final_grade].values # 二分类: 1 及格, 0 不及格 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) scaler StandardScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test)我见过很多初写联邦学习的代码在全局数据上做fit_transform后再按客户端切分这会让每个客户端都看到全局均值和方差等于提前泄漏了全局统计量。正确顺序是先把训练集按客户端切好再对每个客户端用各自的统计量做标准化或者服务端只下发一次 scaler 参数。上面代码用stratify保证正负样本比例在全量和测试集中一致但这只解决了单机验证问题联邦场景还要继续按客户端切分。3.3 按客户端划分数据并模拟 non-IID联邦场景下数据切分单元不是样本而是客户端。每个客户端可以是一个学院、一个自然班或一位老师手里的成绩数据。如果直接全部打乱再平均分给客户端会造成每个客户端都服从全局分布这不是联邦要解决的问题。为了模拟 non-IID我一般会在每个类别内用 Dirichlet 分布控制分配给各客户端的比例。import numpy as np def noniid_split(labels, num_clients, alpha0.5, seed42): 按类别比例把样本分给多个客户端alpha 越小客户端数据越异构。 rng np.random.default_rng(seed) all_idx np.arange(len(labels)) clients [[] for _ in range(num_clients)] for cls in np.unique(labels): cls_idx all_idx[labels cls] rng.shuffle(cls_idx) props rng.dirichlet(np.repeat(alpha, num_clients)) counts rng.multinomial(len(cls_idx), props) pos 0 for i, cnt in enumerate(counts): clients[i].extend(cls_idx[pos:pos cnt]) pos cnt return [np.array(c, dtypeint) for c in clients] # 假定 labels 为 0/1 的及格标签5 个客户端alpha0.7 client_indices noniid_split(y_train, num_clients5, alpha0.7) print([len(c) for c in client_indices])rng.dirichlet生成一个和为 1 的概率向量rng.multinomial根据这个概率把当前类别的样本分给客户端。当 alpha 越小概率向量越极端容易出现某些客户端完全拿不到某个类别。例如 alpha0.1某个客户端可能只拿到不及格样本alpha100 时每个客户端各类别比例接近全局退化为 IID。训练时每个客户端用自己的索引构造 PyTorch 的 DataLoader再交给sampling.py选择本轮参与训练的客户端。3.4 模型结构从 MLP 到序列输入的扩展models 目录下的 Nets.py 是模型定义集中地。对于表格成绩数据一个两到三层的 MLP 已经够用如果数据包含学生课程的历史顺序则需要 LSTM 或者 Transformer。import torch import torch.nn as nn class GradeMLP(nn.Module): def __init__(self, input_dim, hidden_dim64, num_classes2): 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, num_classes), ) def forward(self, x): return self.net(x)input_dim必须与预处理后的特征数量一致num_classes2用于及格/不及格如果是回归成绩分数可以把输出维度改成 1 并去掉最后的 softmax。Dropout 对本地数据少的客户端很重要能降低过拟合。联邦训练时每个客户端加载同样的模型结构但参数分别从全局初始化。数据类型模型选择输入形状出勤/作业/期中期末MLP(batch, input_dim)历史课程成绩序列LSTM/GRU(batch, seq_len, input_dim)课程依赖关系GNN(batch, num_nodes, feats)这个项目在 MNIST 上验证过算法所以 Nets.py 里也可能保留着 MNIST 全连接或 CNN 结构切换到成绩数据时要检查第一层 Linear 的输入维度否则一运行就会报维度不匹配。4. 复现联邦训练options.py、sampling.py 与 train_utils.py 的执行链路当我看一个联邦学习源码包时习惯从三个文件反推整个执行链路options.py 决定超参怎么进sampling.py 决定每轮谁参与train_utils.py 决定模型怎么更新和聚合。把这三个文件读通基本就能把项目跑起来。4.1 options.py 的超参数说明options.py 通常负责解析命令行超参。我在跑这个项目时会先过一遍参数默认值确认num_users、frac、local_ep这些关键项。下面是常见参数表。参数示例值控制内容--num_users10参与联邦训练的客户端总数--frac0.2每轮实际参与更新的客户端比例--local_ep5客户端本地训练轮数--local_bs16本地 batch size--lr0.01本地学习率--epochs100服务端全局通信轮数--seed42随机种子保证实验可复现import argparse def parse_args(): parser argparse.ArgumentParser(descriptionFederated Grade Prediction) parser.add_argument(--num_users, typeint, default10) parser.add_argument(--frac, typefloat, default0.2) parser.add_argument(--local_ep, typeint, default5) parser.add_argument(--local_bs, typeint, default16) parser.add_argument(--lr, typefloat, default0.01) parser.add_argument(--epochs, typeint, default50) parser.add_argument(--seed, typeint, default42) return parser.parse_args() if __name__ __main__: args parse_args() print(f参与客户端: {args.num_users}, 每轮采样比例: {args.frac})frac0.2在 10 个客户端下每轮采样 2 个通信量小但收敛慢frac调大能加快收敛但增加网络开销。local_ep太大会导致本地过拟合和全局偏移一般从 3 到 5 起步。4.2 sampling.py 的客户端采样sampling.py 承担客户端采样。公共做法是先按总数和比例计算 k再用random.sample从客户端列表中采样确保同一客户端不会重复。import random def sample_clients(num_users: int, frac: float, seed: int 42) - list[int]: random.seed(seed) k max(1, int(frac * num_users)) return random.sample(range(num_users), k) # 10 个客户端每轮选 20% print(sample_clients(10, 0.2))每轮调用会重新洗牌如果希望实验完全复现需要在外部固定 seed。采样后train_utils 只对这 k 个客户端调用本地训练未参与的客户端保持上一轮的模型参数不变。4.3 train_utils.py 中的联邦聚合train_utils.py 是核心流程文件。顺序一般为服务端把全局模型 state_dict 发给客户端客户端在本地数据训练local_ep轮后返回参数服务端加权平均。按样本数加权是 FedAvg 的标准做法。def fed_avg(local_weights, local_sizes): total sum(local_sizes) avg_state {} for key in local_weights[0].keys(): avg_state[key] sum( state[key] * size for state, size in zip(local_weights, local_sizes) ) / total return avg_state关键点在于FedRep 不会聚合所有键它只聚合共享表征层的参数本地分类头保留在客户端。SCAFFOLD 则需要额外维护控制变量Ditto 要保存全局和本地两组参数。所以不要看到一个fed_avg就以为所有算法都是同一个聚合函数必须回到各 main 文件查看。4.4 通信开销与结果落盘仓库里出现了 comm_helpers.py这类 helper 通常处理客户端与服务端之间的消息打包、张量压缩和会话管理。通信是联邦学习的性能瓶颈尤其是成绩数据分布在多个校区时网络可能窄且不稳定。常见做法是只传输更新值参数差而不是全量参数再用量化或偏置压缩减少字节数。所谓偏置压缩是指压缩后保持期望无偏从而不破坏收敛性在 L2GD 这类梯度更新方法上效果更直接。# 以 FedRep 入口启动5 个参与方每轮选 60%本地训练 5 轮 python main_fedrep.py --num_users 5 --frac 0.6 \ --local_ep 5 --local_bs 16 --lr 0.01 --epochs 50 --seed 42运行后观察 save 目录正常情况下会生成accs_fedrep_mnist2.csv、losses_fedrep_mnist3.csv这类结果文件。注意文件名里的 mnist 是算法验证时的数据集换成成绩数据后应修改输出命名避免把不同实验混在一起。启动前还要确认 Nets.py 里的输入维度已经改成成绩预处理后的特征数。5. Streamlit 展示实验结果的三个落地技巧曲线、混淆矩阵与单条预测Streamlit 搭建可视化平台的价值在于训练结果不再是控制台里滚动的数字而是可以打开网页交互查看的图表。这里分享三个我从这个项目里提炼出的落地技巧。5.1 用 st.line_chart 读取训练曲线训练产生的 CSV 可以直接用 st.line_chart 展示但要注意索引列。import pandas as pd import streamlit as st acc pd.read_csv(save/accs_fedrep_mnist2.csv) st.line_chart(acc.set_index(acc.columns[0]))第一列通常是轮次或通信轮数不设为索引会导致 x 轴变成默认序号曲线看不出来趋势。如果同时保存了多个算法的结果可以横向拼接后一起画对比更直观。5.2 把混淆矩阵图片与轮次联动训练脚本最好按轮次命名矩阵图这样 Streamlit 只需要用 f-string 拼路径不需要写一堆文件判断逻辑。import streamlit as st from PIL import Image epoch st.selectbox(选择轮次, [2, 3, 5]) st.image(Image.open(fsave/confusion_matrix_epoch{epoch}.png))st.selectbox会重建页面但图片文件不大影响可以忽略。如果图片很多可以把轮次列表改为从目录中动态扫描得出。5.3 加载模型对单条成绩预测模型训练完成后可以加载 checkpoint 做单条预测。这里最容易踩坑的是输入特征没有做和训练时相同的标准化。import torch import numpy as np import streamlit as st model GradeMLP(input_dim10) model.load_state_dict(torch.load(save/model.pt, map_locationcpu)) model.eval() with st.form(grade_form): attendance st.number_input(出勤率, 0.0, 1.0, 0.8) assignment st.number_input(作业均分, 0.0, 100.0, 80.0) midterm st.number_input(期中成绩, 0.0, 100.0, 75.0) submitted st.form_submit_button(预测) if submitted: # 注意必须使用训练时保存的 scaler 做转换 x scaler.transform(np.array([[attendance, assignment, midterm]])) prob torch.softmax(model(torch.tensor(x, dtypetorch.float32)), dim1) st.metric(及格概率, f{prob[0][1].item():.2%})st.form将多个输入组件包在一起避免每次修改数值都触发重跑。scaler.transform而不是fit_transform因为训练时已经 fit 好了。如果模型是回归输出最后一行改成st.metric(预测分数, f{model(...).item():.1f})即可。这样页面既能看训练曲线又能实时预测相当于把实验平台和演示平台合并了。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门