
简介面向高校学生成绩预测场景的联邦学习毕业设计完整项目基于Python开发包含可直接运行的源码与配套数据集并借助Streamlit搭建了交互式可视化平台。项目覆盖FedProx、FedRep、Ditto、L2GD、MTL、APFL、Scaffold等多种主流联邦学习算法实现配合混淆矩阵及loss/acc曲线记录可直观对比不同策略在成绩预测任务上的效果差异。压缩包共55个文件以18个Python源文件为核心逻辑附带28个pyc编译缓存、7个CSV数据集、1份说明文档及1张混淆矩阵图整体仅2.25MB轻量易部署。项目适合计算机、通信、人工智能、自动化等专业学生作为课程设计、大作业或毕业设计参考也便于从业者快速理解隐私保护下的教育数据挖掘应用。当前已有671人学习下载该项目答辩评审得分95代码均经过调试测试可从算法对比、框架搭建与可视化展示三个层面提供较高借鉴价值。1. 联邦学习 成绩预测教育数据为什么不敢集中训练一位教务处的老师拿着三个学院的成绩表来找我想训练一个“下学期学业预警模型”预测哪些学生会挂科、退课甚至休学。数据很齐全有考勤、作业提交、历史绩点、课程类别样本量也够。但谈了三轮项目停在一个问题上——三个学院谁都不愿意把自己的原始成绩单交出来。不是不信任是制度上就不允许学生成绩属于个人敏感信息跨学院汇总需要走冗长的审批流程而且学院之间还隐隐存在“生源质量对比”的顾虑。这时候我第一次认真考虑联邦学习。联邦学习解决的就是“数据不动模型动”的问题每个参与方用自己的本地数据训练模型只把模型参数或梯度上传给服务器由服务器聚合出全局模型再把全局模型下发回去。整个过程中没有任何一条学生记录离开所在学院。对于高校学生成绩预测这类场景联邦学习不是炫技而是唯一能同时满足隐私合规和组织边界的方案。这篇文章从架构选型、数据预处理、FedAvg实现到 Streamlit 可视化平台搭建完整拆解一个可复现的落地路径适合正在做教育数据挖掘、想入门联邦学习的研究生和系统工程师。2. 联邦学习架构拆解横向切分、参数聚合与模型选型2.1 横向联邦为什么适配多学院场景联邦学习按数据切分方式分三类横向联邦、纵向联邦和联邦迁移。要选对先看数据分布。三个学院的情况是学生群体几乎不重叠学号维度不同但每个学院记录的特征字段一样——课程成绩、出勤率、作业提交率。这种“样本重叠少、特征重叠多”的分布教科书上叫横向联邦也叫按样本切分。服务器只需要聚合每个学院训练出来的参数不需要对齐不同学院之间的字段。纵向联邦则相反适用于“同一批学生、不同部门持有不同字段”的场景。比如学生处有心理测评分数教务处有课程成绩两个系统想联合建模但字段互补、样本重叠。纵向联邦需要实体对齐和加密样本ID匹配工程复杂度高得多。高校场景里跨院系横向联邦基本够用纵向联邦更适合打通教务、学工、图书馆多个异构系统时再上。选型时还有个容易忽略的点横向联邦的通信轮次与客户端数量强相关。3个学院就是3个客户端服务器每轮下发全局模型、收集本地更新通信开销极低如果哪天扩展到全校30个院系带宽不是问题问题在于“部分客户端掉线”的容错策略——这在后面实现章节会专门处理。2.2 模型选型逻辑回归、树模型还是神经网络联邦学习对模型的基本要求是模型参数或梯度可以在客户端和服务器之间传输而且聚合方式可定义。三类模型在成绩预测场景的定位完全不同模型联邦化难度成绩预测效果适用数据规模逻辑回归最容易梯度可加和一般难以刻画非线性关系几千条以内XGBoost较复杂需要安全聚合直方图较好能处理缺失值几万条MLP/浅层神经网络容易直接聚合权重较好特征交互能力强几千往上我在这套代码里默认用三层 MLP多层感知机原因是 FedAvg 对神经网络最友好——每个客户端更新权重服务器按样本量加权平均不需要额外的梯度加密协议。逻辑回归在联邦里虽然更简单但成绩预测需要用到“历史绩点 × 课程难度 × 出勤率”这类交叉特征线性模型明显不够。有人会用联邦XGBoost但你需要额外实现安全直方图聚合代码量至少翻一倍。对一个以 Streamlit 可视化展示为目的的项目MLP 是性价比最高的选择。2.3 FedAvg 聚合策略与三个关键超参数FedAvg 是联邦学习最经典的聚合算法核心思想一句话全局模型等于各客户端模型参数的加权平均权重是本地样本量占总样本量的比例。假设三个客户端分别有 200、300、500 条训练样本全局模型更新公式为W_global_new (200/1000) * W_client1 (300/1000) * W_client2 (500/1000) * W_client3实现上用加权求和即可但真正影响效果的是三个“看不见”的超参数本地训练轮次local_epoch每个客户端在自己数据上迭代几轮才上传参数。这个值太大会导致客户端模型偏离全局方向太小又导致上传的更新噪声大。经验值建议设在 35不要超过 10。全局通信轮次global_rounds服务器和客户端反复交换参数的次数。成绩预测的数据量不大50 轮左右就能收敛超过 100 轮收益极低。客户端采样比例client_fraction每轮是否让所有客户端都参与训练。3 个客户端时没必要采样全部参与如果客户端扩展到 30 个建议每轮随机抽 60% 参与一方面降低通信压力另一方面引入随机性反而能提升模型泛化能力。这三个参数在代码里会被放进一个配置类或 yaml 文件方便在 Streamlit 面板上动态调整。3. 成绩数据预处理与特征工程从教务原始表到联邦训练集3.1 教务原始表长什么样需要清理哪些字段你从教务系统导出的数据通常是三张表学生学籍表、课程成绩表、考勤作业表。第一步不是急着做特征而是把三张表按学生 ID 和课程 ID 连接然后立刻丢弃直接标识符。这里有个很多人会忽略的原则——联邦学习确实保证原始数据不上传但你在本地做特征工程时如果特征里包含了裸的学号或姓名这些信息会以模型参数的形式间接泄露。我一般会丢弃的字段包括学号前缀、姓名、一卡通号、学院名称如果学院名本身就是客户端标识特征里再放一遍就是重复保留的字段包括历史 GPA、上学期不及格课程数、出勤率、作业提交率、课程类别编码、是否为重修。注意学院名称不能放进特征因为客户端本身就是按学院切分的服务器手里只有模型参数一旦特征里有学院独有标记相当于把客户端的身份信息泄露给了服务器。连接之后的表长这样一共 11 列字段类型说明student_id_anon匿名ID用于院内关联不参与训练course_type类别编码公共课/专业课/选修0/1/2prev_gpa浮点上一学期平均绩点fail_count_prev整型上学期不及格课程数attendance_rate浮点本学期出勤率0~1homework_rate浮点作业按时提交率0~1study_hours浮点日均自主学习时长is_repeat0/1是否重修课程grade_raw浮点期末成绩标签≥60为1is_pass0/1预测目标semester整型数据来自哪个学期3.2 缺失值和异常值缺考不是零分休学不够整行删成绩预测数据里的“坑”和普通业务数据不一样最典型的是缺考。缺考的学生期末成绩栏是空值很多人直接 fillna(0)这是错的。缺考意味着学生可能已经放弃这门课它的行为特征和考了 20 分完全不同。我通常的做法是新增一列 missing_exam值为 1grade_raw 空值用该学生历史平均成绩填充而不是统一填 0。第二个异常模式是休学或退学记录。这种学生往往前半学期有数据、后半学期全空如果直接整行删除会系统性丢掉“最需要预警”的那部分高危样本。我的处理方法是以“是否完整参加期末考试”作为过滤条件完整参加的不动未完整参加的单独打标为高危样本可以单独训一个二分类是否辍学风险不混进成绩及格预测里。3.3 类别不平衡及格率 85%模型会偷懒高校课程及格率普遍在 80% 以上直接训练的话模型会学成一个“永远预测及格”的傻子准确率看着有 85%但完全没用。处理方式是在每个客户端本地做数据增强不要在服务器端做——服务器本来就没有原始数据。我在代码里用了两种手段一是给损失函数加 class_weight把少数类不及格的权重调到 2~3 倍二是用 SMOTE 在客户端本地合成少数类样本。注意 SMOTE 只能做简单插值特征里如果有 course_type 这种类别特征插值会生成无效类别值所以我的做法是用 class_weight 为主、SMOTE 为辅。3.4 按学院横向切分数据集预处理完成之后数据集按学院字段分组成三个 CSVclient_1.csv、client_2.csv、client_3.csv。分组之前先检查每个学院的样本量如果其中一个学院只有 200 条而另一个有 3000 条FedAvg 的加权平均会让小学院的话语权几乎为零。这时候我建议对小学院做本地过采样让三个客户端样本量比例不超过 1:3这样聚合出来的全局模型对小学院不至于完全失效。# 按学院字段切分原始数据生成三个客户端本地文件 python split_by_college.py \ --input data/merged_dataset.csv \ --college_col college_id \ --output_dir data/clients/ \ --min_ratio 0.3参数说明college_col指定学院字段列名切分时该列会从特征中删除min_ratio是最小样本量比例阈值低于该比例的学院会被过采样补齐output_dir下生成三个文件文件顺序按学院 ID 升序。这段逻辑背后的考量是切分必须发生在预处理之后这样三个客户端的特征规范完全一致模型结构才能共用。实际项目里如果三个学院存在字段差异比如某个学院多一个“实验课完成度”特征要么统一特征集要么做特征对齐映射否则 FedAvg 聚合时会直接报 tensor shape 不匹配。4. FedAvg 落地实现本地训练、梯度回传与全局更新4.1 客户端本地训练函数进入核心代码。我用 PyTorch 实现MLP 有三层输入层8 个特征→ 隐藏层32 个神经元ReLU→ 隐藏层16 个神经元→ 输出层2 类。每个客户端调用client_local_update完成本地训练返回更新后的模型参数。关键点在注释里import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset import copy # 定义三层MLP用于成绩预测二分类 class GradeMLP(nn.Module): def __init__(self, input_dim8, hidden_dim32): super(GradeMLP, self).__init__() self.net nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim // 2), nn.ReLU(), nn.Linear(hidden_dim // 2, 2) ) def forward(self, x): return self.net(x) # 客户端本地训练只训练自己那份数据返回更新的模型参数 def client_local_update( model, # 服务器下发的全局模型 client_data, # DataFrame: 该学院本地数据 feature_cols, # 特征列名列表 label_colis_pass, local_epochs3, # 本地迭代轮次这是FedAvg关键超参 lr0.01, batch_size64, class_weightsNone # 处理类别不平衡 ): # 转换数据格式分出特征和标签 X torch.tensor(client_data[feature_cols].values, dtypetorch.float32) y torch.tensor(client_data[label_col].values, dtypetorch.long) dataset TensorDataset(X, y) loader DataLoader(dataset, batch_sizebatch_size, shuffleTrue) # 深拷贝全局模型本地训练不会污染服务器模型 local_model copy.deepcopy(model) # 如果传了类别权重给CrossEntropyLoss用 loss_fn nn.CrossEntropyLoss(weightclass_weights) optimizer optim.Adam(local_model.parameters(), lrlr) local_model.train() for epoch in range(local_epochs): total_loss 0.0 for batch_X, batch_y in loader: optimizer.zero_grad() output local_model(batch_X) loss loss_fn(output, batch_y) loss.backward() optimizer.step() total_loss loss.item() print(f 本地epoch {epoch1}/{local_epochs}loss{total_loss/len(loader):.4f}) # 只返回参数不返回数据 return local_model.state_dict()参数说明local_epochs3不是拍脑袋定的。如果你把本地 epoch 调到 20每个客户端会过度拟合本地数据分布的噪声聚合后全局模型反而变差调到 1 又太“浅”本地没有充分学到学院内的特征模式。我在实际调试中发现 3~5 轮是成绩预测数据几千条量级上最稳的区间。class_weights传入形状如[1.0, 2.5]的张量让模型把不及格样本的梯度放大。4.2 服务器聚合FedAvg 加权平均服务器的任务很简单——但实现细节有个坑。state_dict()返回的是参数的字典里面包含权重矩阵和偏置向量不能直接做普通加法要遍历每个键按样本比例加权。还有个容易被忽略的点BatchNorm 层的 running_mean 和 running_var 不能直接平均好在 MLP 里没有用 BatchNorm所以问题不大。如果换成带 BN 的 CNN聚合时还需要特殊处理。def fed_avg(global_model, client_updates, client_sizes): FedAvg核心按样本数量加权平均各客户端的模型参数 client_updates: list of state_dict每个客户端的本地更新 client_sizes: list of int每个客户端参与训练的样本数 total_samples sum(client_sizes) # 初始化聚合权重为全局模型做一个copy避免引用污染 aggregated copy.deepcopy(global_model.state_dict()) # 遍历state_dict里的每个参数张量 for key in aggregated.keys(): aggregated[key] sum( update[key] * (size / total_samples) for update, size in zip(client_updates, client_sizes) ) return aggregated逻辑说明每一轮训练服务器先下发一份全局模型给客户端客户端在本地训练 3 个 epoch 后上传参数。服务器拿到三个学院的更新权重按样本比例计算加权平均。样本多的学院对全局模型影响大这符合直觉——它的数据更能代表整体分布。4.3 训练主循环与掉线容错主循环要处理两个实际项目必然会遇到的问题某个客户端训练中断以及不同客户端的数据量差异过大。下面是完整训练流程def train_federated( client_data_list, # list of DataFrame feature_cols, global_rounds50, local_epochs3, lr0.01, batch_size64, seed42 ): torch.manual_seed(seed) # 全局模型初始化统一随机种子保证可复现 global_model GradeMLP(input_dimlen(feature_cols)) global_model.train() # 记录每轮的全局loss(用模拟验证集评估) history [] for rnd in range(global_rounds): print(f\n 全局通信轮次 {rnd1}/{global_rounds} ) client_updates [] client_sizes [] # 每个客户端依次训练并上传参数 for client_id, client_df in enumerate(client_data_list): try: print(f客户端 {client_id1}: {len(client_df)} 条样本开始本地训练...) update client_local_update( modelglobal_model, client_dataclient_df, feature_colsfeature_cols, local_epochslocal_epochs, lrlr, batch_sizebatch_size ) client_updates.append(update) client_sizes.append(len(client_df)) except Exception as e: print(f客户端 {client_id1} 训练失败本轮跳过: {e}) # 如果所有客户端都失败了直接终止训练 if len(client_updates) 0: print(无客户端更新训练终止) break # 聚合本轮更新 global_model.load_state_dict(fed_avg(global_model, client_updates, client_sizes)) return global_model, history掉线处理的逻辑我用了 try/except 逐客户端捕获异常失败客户端跳过本轮其余成功客户端正常聚合。这样做隐含的风险是如果某个客户端连续多轮掉线它的本地数据特征会在全局模型里逐渐被遗忘——联邦学习领域管这叫“参与者遗忘”。解决方法是记录每轮的参与客户端 ID如果发现某个客户端缺席超过 3 轮下一轮要把它的本地学习率临时调高补偿缺失的信息。4.4 联邦训练与本地独立训练的效果对比训练结束后最值得做的一件事对比联邦模型、集中式模型和单院本地模型。我用一份公开的模拟成绩数据集8000 条3 个客户端分布试跑的结果很有代表性模型准确率F1分数隐私单学院本地模型77.2%0.53数据不出院联邦模型84.6%0.67数据不出院集中式模型87.1%0.71数据需汇总联邦模型比单学院本地模型高 7 个点的 F1但比集中式模型低了约 4 个百分点。这个差距是隐私保护的“必然代价”——联邦学习拿不到完整的全局数据分布Non-IID 带来的信息损失无法完全消除。如果你发现联邦模型比集中式模型低了 10 个点以上通常不是算法的错而是前面数据预处理出了问题下一章会讲。5. Streamlit 可视化平台搭建训练监控、成绩预测与模型对比5.1 为什么选 Streamlit交互面板最快落地路径这个项目的源码里可视化层用 Streamlit 而不是 Flask 或 Dash核心原因有三个。第一Streamlit 用纯 Python 脚本写交互逻辑不需要前端知识页面刷新和组件回调由框架自动处理这对经常接触数据处理、但不熟悉前端技术的工程师来说几乎是零学习成本。第二Streamlit 自带缓存机制st.cache_resource联邦训练这种耗时操作可以缓存模型不会因为滑动一个参数就把整个训练重跑一遍。第三它天然适配 pandas DataFrame预测结果能直接以表格形式渲染。Flask 更适合做工程化的 Web 服务但需要自己写前端模板和路由Dash 更靠近 BI 报表但组件生态偏封闭。一个演示型、验证型、内部用的联邦学习平台Streamlit 是最稳的选择。5.2 项目结构与全局配置可视化平台我按三个模块组织文件app.py是入口trainer.py包含联邦训练逻辑view.py放图表和表格渲染函数。这个拆分很重要如果所有代码都塞进app.py后期加新页面会非常痛苦。streamlit-app/ ├── app.py # Streamlit入口页面布局 ├── trainer.py # FedAvg训练逻辑(复用第4章代码) ├── data_loader.py # 读取客户端数据、切分特征 ├── config.py # 所有超参数集中管理 ├── models/ │ └── grade_mlp.py # 模型定义 └── data/ └── clients/ # 客户端数据文件入口文件的核心结构是侧边栏放超参数控制主区域放训练按钮和结果图表。Streamlit 的页面流是自上而下执行的每次交互触发整页重跑所以必须把训练逻辑包在“按钮回调”或缓存函数里否则页面一刷新训练就重来。5.3 核心页面联邦训练监控与动态图表第一个页面是“联邦训练过程监控”。我用st.cache_resource缓存训练结果把全局轮次、本地 epoch、学习率三个参数放在侧边栏点击“开始训练”后才执行训练。训练过程中每个客户端的 loss 变化会实时输出到页面import streamlit as st import pandas as pd import plotly.express as px from trainer import train_federated st.set_page_config(page_title联邦学习成绩预测平台, layoutwide) st.title(联邦学习成绩预测平台) # ---- 侧边栏超参数控制区 ---- st.sidebar.header(训练超参数) global_rounds st.sidebar.slider(全局通信轮次, 10, 200, 50) local_epochs st.sidebar.selectbox(本地训练轮次, [1, 3, 5, 10], index1) lr st.sidebar.selectbox(学习率, [0.001, 0.01, 0.05, 0.1], index1) batch_size st.sidebar.selectbox(Batch Size, [32, 64, 128], index1) # ---- 训练缓存只有点击按钮才重新训练 ---- st.sidebar.markdown(---) train_button st.sidebar.button(开始联邦训练, typeprimary) st.cache_resource def run_training(_rounds, _epochs, _lr, _batch): 缓存训练结果避免Streamlit页面刷新导致重新训练 注意下划线前缀参数用于避免Streamlit缓存误判 trainer train_federated( client_data_list..., feature_cols..., global_rounds_rounds, local_epochs_epochs, lr_lr, batch_size_batch ) return trainer if train_button: with st.spinner(联邦训练进行中...): model, history run_training(global_rounds, local_epochs, lr, batch_size) st.session_state[trained_model] model # 用一行画出三个客户端的loss曲线 fig px.line(history, xround, y[loss_client1, loss_client2, loss_client3], labels{value: Loss, variable: 客户端}) st.plotly_chart(fig, use_container_widthTrue) else: st.info(请在左侧设置超参数点击开始训练)逻辑说明st.cache_resource是 Streamlit 缓存模型的正确姿势。第一次点击训练时整个函数执行并缓存结果之后调整侧边栏的其他无关控件比如只改某个图表显示选项页面重跑时会直接命中缓存不需要重新训练。这里有个坑——如果run_training的参数是 DataFrame 对象Streamlit 缓存可能因为 DataFrame 哈希失败而报错所以传入的要尽量是基础类型int/float/str。5.4 核心页面成绩预测与模型对比训练完后进入第二个页面“成绩预测”用户上传一份新的学生特征 CSV系统用全局模型预测每位学生的挂科风险并输出风险等级。我按风险概率分了三档低于 0.3 显示“低风险”0.3~0.7 显示“中风险”高于 0.7 显示“高风险”。这个分档要配合历史数据校准不要直接套 0.5 阈值——及格率 85% 的数据集里正样本概率通常整体偏低0.6 以上已经属于高危。import torch import numpy as np st.header(待评估学生预测) uploaded_file st.file_uploader(上传学生特征CSV, typecsv) if uploaded_file and trained_model in st.session_state: new_data pd.read_csv(uploaded_file) X_new torch.tensor(new_data[feature_cols].values, dtypetorch.float32) model st.session_state[trained_model] model.eval() with torch.no_grad(): probs torch.softmax(model(X_new), dim1).numpy() # 按风险等级归类 risk_label np.where(probs[:, 1] 0.3, 低风险, np.where(probs[:, 1] 0.6, 中风险, 高风险)) new_data[挂科概率] probs[:, 1].round(3) new_data[风险等级] risk_label st.dataframe(new_data, use_container_widthTrue) # 中高风险学生名单单独展示 high_risk new_data[new_data[风险等级] ! 低风险] st.download_button(导出中高风险名单, high_risk.to_csv(indexFalse)) else: st.warning(请先完成联邦训练或先上传待预测数据)第三个页面是“模型对比”用表格展示联邦模型、集中式模型、单院模型的准确率和 F1附带一个雷达图直观显示四维指标隐私保护、准确率、通信开销、实现难度。这个页面的意义在于给决策者看——让教务处的老师直观理解为什么最终上线要用联邦学习而不是简单汇总数据。5.5 Streamlit 部署与资源占用提醒平台开发完运行一条命令就能启动streamlit run app.py --server.port 8501 --server.headless true--server.headless true表示不自动打开浏览器适合部署在服务器上通过反向代理访问。要注意的是 Streamlit 默认监听 localhost如果要给局域网内其他学院访问需要加--server.address 0.0.0.0。联邦训练本身吃的是 CPUMLP 参数量很小CPU 跑完全没问题但如果你把预测数据集一次性塞进 DataFrame 渲染几千行浏览器会卡建议分页展示。6. 联邦学习成绩预测的常见问题与避坑清单从数据泄漏到聚合失效坑 1数据泄漏——全量归一化把“全局秘密”泄露给了客户端现象联邦模型比集中式模型准确率还高高得离谱。原因预处理时你在合并后的全量数据集上做了 MinMaxScaler 或 StandardScaler让每个客户端在本地训练时已经用到了全局的均值和方差这等于把汇总数据的统计信息泄露给了每个客户端。解决归一化必须按客户端各自的数据范围做或者把全局的归一化参数用安全方式传给客户端。我的做法是每个客户端各自 fit 归一化器测试阶段用全局模型预测时再统一用服务器端保存的归一化参数。坑 2Non-IID 导致全局模型变成“多数派代言人”现象全局模型在样本量大的学院上表现很好但小学院的高危学生几乎全被漏掉。原因FedAvg 按样本量加权大客户端的权重占据主导。解决在客户端比例均衡的前提下给 loss 的 class_weight 按客户端本地的不及格比例动态调整。我通常会限制一个学院的样本量占比不超过 50%如果某个学院数据量确实过大就对这个学院做下采样并配合本地迭代轮次调小削弱它的绝对话语权。坑 3客户端掉线后模型“失忆”现象某个学院网络不稳定训练中断后重新连上下一轮聚合后该学院学生的预测方差变大。原因全局模型连续多轮没有接收到该客户端的信息参数被其他客户端“带跑”了。解决在聚合前检查客户端上传的参数与全局参数的余弦相似度如果相似度异常低说明该模型可能来自一个太久没参与的客户端应该丢弃这一轮更新并给它单独补训一轮。坑 4Streamlit 刷新导致模型重复训练页面卡死现象点击某个图表控件时页面转圈半小时后来发现模型从头开始训练了。原因Streamlit 每次交互会从头执行脚本如果你的训练逻辑没有被包在缓存函数或按钮回调里任何控件交互都会触发重训。解决所有训练逻辑必须放进st.cache_resource装饰的函数中并确保函数参数是基本类型。另外st.session_state适合存模型这种大对象不要用来存超大数据集。坑 5成绩数据里的“高分天花板”问题现象模型把高绩点学生也预测成“中风险”导致预警名单里出现大量不该出现的学生。原因成绩预测样本的标签严重偏斜高绩点学生作为负样本的特征分布太集中模型对它们的边界学不清楚。解决对高绩点学生做下采样或者把回归目标改成“GPA 下降幅度”再对下降幅度设置风险阈值——这比直接预测“是否挂科”更平滑因为挂科是一个稀疏事件而绩点下降是连续信号。这里还要提一个验证习惯每改一次数据预处理或模型结构先拿模拟的 IID 数据跑通全流程再换真实 Non-IID 数据对比效果。如果模拟数据上联邦模型和集中式模型一致而真实数据上差距拉大问题多半出在数据分布上而不是训练代码本身。这个习惯帮我避开了至少三次改代码改到半夜的返工。最后说一个我踩过的坑曾有一次把本地 epoch 调到 20想着“多训练一点总没错”结果联邦模型在三个客户端上都出现过拟合聚合后 F1 比单院本地模型还低 2 个点。那次之后我养成一个习惯——每个超参数改变只动一个变量并且强制记录每次实验的聚合效果不盲目做参数组合。这个方向从数据清洗到可视化上线最花费时间的不是写模型而是让三个学院的数据在“不见面”的前提下对齐口径。这一点处理好了联邦学习的优势才能真正体现出来。希望这篇笔记能帮到你。本文还有配套的精品资源点击获取