LSTM多变量成绩预测的Python实战:从数据到模型

发布时间:2026/9/25 23:21:55
LSTM多变量成绩预测的Python实战:从数据到模型
简介这套LSTM系列教程资源面向Python机器学习初学者与时间序列预测实践者聚焦多变量预测、单变量预测及多步预测等典型任务覆盖股票、天气、销售等常见应用场景帮助用户从数据预处理到模型评估建立完整预测流程。资源共33个文件包括19个CSV数据集与14个Python脚本压缩包仅3.88MB便于直接下载使用。脚本内容涵盖时间序列转监督学习、观测值缩放、平稳化处理、模型构建与训练、误差评估与参数优化等关键环节并提供香皂销售预测、多步预测、更健壮LSTM案例等多个可运行示例方便对照学习不同场景下的建模思路。所有文件按单变量、多变量、多步预测等专题分目录组织结构清晰适合边学边练也可作为迁移到自有数据的参考模板。目前已有3419人学习下载是一份兼具系统性与实操性的LSTM预测入门及进阶资源。1. 这套 LSTM 多变量预测成绩的 Python 资源从数据到模型的完整闭环这套 LSTM 多变量预测成绩的 Python 资源不是把学生成绩单平铺成一列硬塞给神经网络而是把历次考试成绩、出勤率、作业提交率、在线学习时长按时间组成多变量序列让 LSTM 从“历史模式”里去推断下一次考试的分数。我拆过不少类似的学情分析项目最普遍的翻车点不是 LSTM 原理晦涩而是数据形状没对上模型收到的是二维表格LSTM 要的却是三维张量。这份资源包含数据样例、模型定义、训练脚本和预测脚本能在不折腾环境的前提下跑通完整流程。适合有 Python 和 PyTorch 基础、正在做成绩预测或时序预测作业的从业者和学生我建议你先把这篇笔记里的窗口切分逻辑看懂再动手改参数。2. 数据准备与滑动窗口特征、归一化、切分一个都不能错数据准备是所有 LSTM 项目里最容易被低估的一步但它决定了模型的上限。多数人把成绩单读进 DataFrame 后就直接进模型结果要么维度报错要么 Loss 乱跳。多变量预测要求你先把数据整理成“每个时间步是一组有序特征”的结构再切出样本。2.1 多变量特征先挑可对齐的时序指标历次分数、出勤、作业一个都不能少多变量预测里的“变量”不是指学生库里随便哪一列。LSTM 按时间步读取数据每个时间步上的特征必须在业务上能对齐到同一次考试周期。比如第 t 次考试的“出勤率”指的是该次考试前一个月的出勤统计而不是期末总出勤。选特征时我会遵循一条原则预测时刻之前已知且按考试轮次存在先后关系。以成绩预测为例一份可用的学生序列长这样特征列含义时间对齐方式exam_round考试轮次序号第1次到第N次score本次考试成绩历史成绩也可以作为目标列attendance本次考试前出勤率0到1之间homework作业平均分0到100之间study_time在线学习时长单位小时这里有一个容易踩的坑不要用“本次班级排名”做特征。排名是考试结束后才产生的预测时拿不到用了就等于作弊如果数据集中已经存在请直接删掉。我一般还会把学生 ID 和时间轮次单独保留用于后续分组和排序。缺失值处理按业务规则来考试成绩偶尔缺考常见做法是前向填充或把该轮次整条移除。不要用后一期的成绩去填充前一期的缺失那会让模型在训练时偷偷看到未来数据。如果某个学生的有效轮次太少比如只有 3 次滑动窗口会直接生成不了样本这时我会把窗口调小或干脆丢弃该学生。2.2 滑动窗口怎么滑用前 N 个时间步预测下一个时间步LSTM 不是表格模型它依赖“过去一段连续长度”的信息。滑动窗口就是从每个学生的历史序列上切出固定长度的片段。窗口大小 window_size5 表示用最近 5 次考试的各项特征预测第 6 次成绩。窗口越长模型能看到的历史模式越多但也会吃掉开头样本窗口太短比如 2模型很难学到成绩的上升趋势。写一个可以把 DataFrame 转成张量的函数import numpy as np import pandas as pd import torch def create_sequences(df, window_size5, pred_colscore): feature_names [c for c in df.columns if c not in (student_id, exam_round)] X, y [], [] for sid, g in df.groupby(student_id, sortFalse): g g.sort_values(exam_round).reset_index(dropTrue) values g[feature_names].values.astype(np.float32) score_idx feature_names.index(pred_col) for i in range(len(values) - window_size): X.append(values[i:i window_size]) y.append(values[i window_size, score_idx]) return torch.tensor(np.array(X)), torch.tensor(np.array(y))这个函数做了三件事按 student_id 分组保证每个学生的历史不被截断到另一个人头上按 exam_round 升序排序保证时间顺序不被打乱滑动生成输入窗口 X 和目标 y。X 的形状是(样本数, 5, 特征数)y 的形状是(样本数,)这种形状可以直接喂给设置了 batch_firstTrue 的 PyTorch LSTM。如果你还想同时预测出勤率或学习时长那就不是单输出回归而要改成多输出回归y 的形状会变成(样本数, 5, 其他特征数)之类的。但大多数成绩预测场景只需要预测分数保持单目标会让模型和调参都简单很多。2.3 归一化和数据划分最容易信息泄露的两步归一化对 LSTM 是必须的。成绩是 0-100学习时长可能是 0-1000作业分也是 0-100如果不缩放梯度会被数值大的特征主导。常见做法是 StandardScaler但绝大多数翻车点就藏在 fit 位置上。正确的归一化顺序是先划分训练集和验证集然后只对训练集做 fit再对验证集和测试集做 transform。from sklearn.preprocessing import StandardScaler feature_names [c for c in train_df.columns if c not in (student_id, exam_round)] scaler StandardScaler() # 只拿训练集的原始数值去fit train_values train_df[feature_names].values.astype(np.float32) scaler.fit(train_values) train_df.loc[:, feature_names] scaler.transform(train_values) # 验证集和测试集用同一个scaler禁止重新fit valid_df.loc[:, feature_names] scaler.transform(valid_df[feature_names].values.astype(np.float32))为什么不能对整个数据集 fit因为验证集中的“平均分数”和“分数分布”已经参与了训练前统计模型在训练时就能间接看到验证集的信息验证 Loss 会严重失真。实际落地时的表现会比验证差很多典型表现是线上预测一塌糊涂。数据划分也要按时间切。我见过有人把同一个学生的不同窗口随机放到训练集和验证集这样验证集几乎就是“复读”过的数据因为窗口之间存在重叠。你应该按学生 ID 划分比如取前 70% 的学生做训练后 30% 做验证或者按考试轮次划分前五轮做训练后两轮做验证。两种都可以但不要 random_state 洗牌后直接切。这里还要连着一个细节生成 X 和 y 之后数据集里相邻样本是同一个学生平移一个单位的窗口重叠很大。如果 shuffleTrue训练时模型很容易记住这个学生的模式但验证集如果来自同一学生评估依然会虚高。所以更严谨的做法是“按学生切分后再造窗口”。上面 create_sequences 函数也可以先传训练学生列表过滤数据再各自生成窗口。3. 用 PyTorch 从零搭 LSTM 回归模型输入格式和训练循环才是重点模型结构本身并不复杂复杂的是输入形状和训练循环里的细节。PyTorch 的 LSTM 对输入格式很挑剔一不留神就会在维度上卡半天。3.1 LSTM 单元的输入输出形状先搞懂三句话PyTorch 的 nn.LSTM 接受三维输入。默认顺序是(seq_len, batch, input_size)如果你平时习惯了表格思维会写成(batch, seq_len, input_size)。这个错误我在第一次写的时候也犯过解决办法是在构造器里加 batch_firstTrue然后统一用(batch, seq_len, input_size)思维。seq_len 就是滑动窗口的 window_sizeinput_size 是特征数batch 是训练时的样本数。LSTM 每个时间步会输出一个隐藏状态维度由 hidden_size 决定。整个序列过完后输出张量有两个部分output 是所有时间步的隐藏状态序列形状是(batch, seq_len, hidden_size)最后一个时间步的隐藏状态可以直接取 output[:, -1, :] 或从(hn, cn)里取。回归任务只需要最后一个时间步的向量然后接一个全连接层映射到 1 维分数。hidden_size 不是时间步数而是 LSTM 的记忆容量。hidden_size64 代表每个时间步用一个 64 维向量表示。hidden_size 越大能记住的模式越多但和数据量不匹配时更容易过拟合。num_layers 是堆叠层数2 层比 1 层能提取更抽象的特征但梯度传播路径更长需要配合 dropout 防止过拟合。3.2 模型定义与训练代码一份可复现的成绩预测骨架以下代码可以直接保存成 lstm_model.py 运行我这里用的是 PyTorch 2.x1.x 同样支持。模型结构就是 LSTM 加 Dropout 加全连接层import torch import torch.nn as nn from torch.nn.utils import clip_grad_norm_ class LSTMRegressor(nn.Module): def __init__(self, n_features, hidden_size64, n_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizen_features, hidden_sizehidden_size, num_layersn_layers, batch_firstTrue, dropoutdropout ) self.drop nn.Dropout(dropout) self.fc nn.Linear(hidden_size, 1) def forward(self, x): out, _ self.lstm(x) out self.drop(out[:, -1, :]) return self.fc(out)forward 里取 out[:, -1, :] 拿最后一个时间步的隐藏状态。Dropout 只在训练时生效在 eval 模式下被跳过。如果你的 n_layers1nn.LSTM 会忽略 dropout 参数所以 dropout 主要发生在层与层之间。训练循环是最容易写错的地方MSE 回归的 label 维度。model 输出形状是(batch, 1)而 y 可能是(batch,)直接用 MSELoss 计算时 PyTorch 会广播导致 loss 被多算或形状对不上。我一般会先 squeeze 再算损失from torch.utils.data import DataLoader, TensorDataset X_train torch.randn(200, 5, 4) # 示意200个样本窗口5特征4 y_train torch.randn(200) dataset TensorDataset(X_train, y_train) loader DataLoader(dataset, batch_size32, shuffleTrue) model LSTMRegressor(n_features4, hidden_size64, n_layers2, dropout0.2) mse nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) for epoch in range(100): model.train() total_loss 0.0 for xb, yb in loader: optimizer.zero_grad() pred model(xb) loss mse(pred.squeeze(1), yb) # 把(batch,1)压成(batch,) loss.backward() clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() if (epoch 1) % 10 0: print(fepoch {epoch 1}, loss {total_loss / len(loader):.4f})这里有几个参数可以照着调参数推荐值调参方向window_size5数据长度少就调小想做长周期预测才调大hidden_size64Loss 降不下去可以试 128但要加正则n_layers2数据量很小时用 1防止过拟合dropout0.2验证集比训练集差太多时调到 0.3lr0.001如果 Loss 震荡或上升降到 0.0001batch_size32样本只有一两百就调到 8 或 4epoch100配合早停不要求必须跑到 100我用 Adam 时会把 lr 初始值设为 0.001这是 PyTorch 教程里最常见的配置。若 loss 出现 NaN除了检查数据还要检查梯度clip_grad_norm_ 就是为了防梯度爆炸。如果加入梯度裁剪仍不稳定就降低学习率。3.3 预测成绩的完整流程还原归一化后输出真实分数训练完成后预测并不只在模型 forward 一次就结束。模型输出的值是归一化后的分数要还原回真实分数区间必须借助之前保存的 scaler。import joblib model.eval() with torch.no_grad(): y_pred model(X_valid) y_pred y_pred.cpu().numpy().reshape(-1, 1) # 先保存scaler训练结束后用于还原 joblib.dump(scaler, score_scaler.pkl) # 太容易出错inverse_transform需要原始特征列数 inverse_values scaler.inverse_transform( np.concatenate([y_pred, np.zeros((y_pred.shape[0], len(feature_names)-1))], axis1) ) pred_score inverse_values[:, feature_names.index(score)]这里要重点说明StandardScaler 在 fit 时是对所有特征列一起计算的inverse_transform 也必须传入同样列数的数组。如果你只传入一个 y_pred 单列会报维度错误如果你重新对 y_pred 列单独 fit 一个 scaler那还原后的数值只是“模型输出的统计还原”不是原始分数。正确做法是把成绩列放在原特征数据集中的位置其他列填 0 或原始均值然后取还原后的第一列。如果你不想做这种矩阵拼接可以在归一化前单独对成绩列做一个 scaler这样预测还原时干净很多。我在源码包里就是用后一种方式。如果验证集表现正常最后的预测结果也需要走同一套还原逻辑。到这里一个完整的成绩预测流程已经通了数据准备好了LSTM 模型训练好了预测分数也还原回 0-100 了。接下去真正影响落地成败的是那些看起来不起眼的坑。4. 避坑指南LSTM 成绩预测的五个必踩坑与排查方法这一章写给所有准备把代码往真实数据集上跑的人。我踩过的坑基本都在这里每一条都按“现象、原因、解决”的顺序说清楚。4.1 Loss 不降反升现象训练第 1 个 epoch 的 loss 可能在几十甚至上百第 2 个 epoch 不降反升到几百训练过程中还会出现 NaN。原因数据没做标准化学习率太大或者输入存在异常值。LSTM 对输入数值范围很敏感成绩是 0-100学习时长是几百小时两者加在一起梯度很容易被某个特征主导。学习率 0.001 对归一化后的数据是合理的但对原始量级就是放大噪声。另外如果目标列本身有 NaNLoss 计算会一路传导到梯度。解决先检查数据是否包含 NaN 或 Inf再训练前用 StandardScaler 或 MinMaxScaler 做一次归一化。如果归一化后 Loss 还往上走把 Adam 的学习率降到 0.0001同时加上梯度裁剪 clip_grad_norm_(max_norm1.0)。我排查时会打印每个 batch 的输入范围确认没有极端值后再动模型结构。4.2 预测结果是一条水平线现象预测分数几乎恒定在一个固定值附近画出来是一条直线和训练集均值差不多。原因模型没有学到时间步之间的映射关系退化成“输出训练集均值”。这种情况常见于目标列本身方差很小或者输入特征和目标之间没有时序关联。也可能是因为 LSTM 输出层的偏置初始值太大让全连接层一开始就偏向均值训练又没把它拉回来。解决先画出训练集 y 的分布如果成绩方差本来就只有两三分说明该数据集不适合用 LSTM。如果方差正常尝试把 window_size 从 5 减到 3去掉学习时长这类噪声特征让模型更容易聚焦到成绩序列的波动。另外检查一下损失函数回归问题一定要用 MSELoss 或 MAELoss不要误用了 CrossEntropyLoss。4.3 训练集很好、验证集崩盘现象训练 Loss 降到 0.01验证 Loss 却差一个量级甚至预测值全部偏向低分。原因数据划分或者归一化出了问题。最常见的是对全量数据做了一次 StandardScaler fit验证集的统计信息被模型提前看到。另一种隐蔽情况是同一学生的不同窗口被随机分到了训练集和验证集由于窗口之间高度重叠验证集几乎是从训练集复制的模型在验证集上盲目自信但遇到真正没见过的学生就失效。解决划分时按 student_id 分组先把学生列表拆成 train_ids 和 valid_ids再分别过滤数据生成窗口。归一化只对训练集 fit验证集和测试集用同一个 scaler transform。如果这样做了之后验证 Loss 还是不理想那说明模型本身过拟合再考虑加 Dropout 或减小 hidden_size。4.4 归一化后预测值还原不回来现象预测分数还原后出现负数或超过 150明显超出成绩合理区间。原因scaler 的维度没对齐。训练时对 4 列特征一起 fit预测时直接对 1 列模型输出做 inverse_transform会报维度错误就算重新 fit 一个单列 scaler得到的结果也只是把输出分布映射到任意区间根本不是原始分数。这是我在第一版代码里最惨痛的教训。解决单独为成绩列建一个 scaler训练和预测都用它。或者用第 3.3 节里的补列方案把 y_pred 放到成绩列位置其他列填 0构造出和训练时相同列数的数组再 inverse_transform。别忘了把 scaler 和模型一起保存预测时重新加载不能每次重新 fit。4.5 随机种子一变结果就变现象同一份代码、同一份数据跑两次预测结果能差 10 分以上。原因LSTM 权重随机初始化、Dropout 随机性、DataLoader 的数据打乱顺序都会影响结果。小样本场景下模型对初始值特别敏感固定一个随机种子能保证实验可复现但是不能保证模型稳定。解决在代码开头固定 torch.manual_seed(0)、np.random.seed(0)同时给 DataLoader 设置 generatorg torch.Generator() g.manual_seed(0) loader DataLoader(dataset, batch_size32, shuffleTrue, generatorg)固定随机种子只能让你有后悔药但模型稳定性不能只靠种子。更可靠的做法是跑 5 次取均值或者用时间序列交叉验证看结果范围。如果各次结果标准差超过 5 分说明模型本身对初始化太敏感应该先缩小模型规模或加正则。5. 进阶技巧滚动多步预测与模型稳定性验证实际业务不会只问“下一次考试多少分”更多是问“接下来三次分别多少分”。LSTM 单步预测只能输出一步多步预测要用递归策略把当前预测值拼到窗口末尾再作为下一次输入。如果窗口内只有成绩一个目标变量递归预测很好写但我们的窗口里还有出勤、作业等其他特征它们未来值不一定已知。我常用的折中方法是出勤和作业按教学计划填预估计划值成绩用模型预测值学习时长用最近一周均值顶替。如果这些未来特征完全没有计划值那就退化成单变量 LSTM 预测只保留成绩列避免引入噪声。下面这段代码展示了递归预测的基本思路def recursive_forecast(model, feature_names, window, steps2): model.eval() preds [] cur window.clone() # 形状(1, window_size, n_features) with torch.no_grad(): for _ in range(steps): p model(cur).item() preds.append(p) # 下一个窗口成绩用预测值其他特征用计划值或当前最后值 next_vec torch.zeros(1, 1, cur.shape[-1]) score_idx feature_names.index(score) for j in range(cur.shape[-1]): if j score_idx: next_vec[0, 0, j] p else: next_vec[0, 0, j] cur[:, -1, j] cur torch.cat([cur[:, 1:, :], next_vec], dim1) return preds这里假设除成绩外的其他特征在下一时刻保持不变这不一定是业务事实但至少给你一个可运行的基线。真正的多变量多步预测需要另外建模型同时预测所有特征复杂度立刻上去了这个资源里也不建议你一上来就做。验证多步预测质量不能只看单步 MAE。我会把验证集最后 steps 个真实值拿出来用上面的 recursive_forecast 做一次滚动预测然后计算每一步的 MAE 和累计偏差。若第 1 步误差小、第 3 步误差翻倍说明误差在累积这是正常现象如果第 1 步就很大先回头查窗口和归一化。验证代码很简单from sklearn.metrics import mean_absolute_error mae mean_absolute_error(y_true[-steps:], preds)我从那次没保存 scaler、把预测成绩还原成负数之后每次做 LSTM 成绩预测都强制把模型和 scaler 一起打包并且先画真实值对预测值的曲线确认没有水平线再谈优化。这个习惯帮我挡掉了至少两次无效调参。希望帮到你。本文还有配套的精品资源点击获取