资讯动态

多变量LSTM成绩预测:Python实战与避坑指南

发布时间:2026/9/29 14:00:01 来源:尧图企业网站定制
简介这份资源面向希望掌握LSTM多变量预测的Python学习者与算法入门者围绕时间序列预测这一典型场景系统讲解如何用多个输入特征预测目标变量并评估模型预测成绩。包内共33个文件以19个csv数据集和14个py脚本为主csv用于提供香皂销售等序列样本py脚本覆盖数据预处理、时间序列转监督学习、观测值缩放、平稳化处理、LSTM模型开发与多步预测等环节压缩包约3.88MB。目前已有3419人学习下载说明内容在入门与进阶人群中具备一定参考价值。资源按单变量、多变量、多步预测等模块组织读者可据此理解滑动窗口构造输入输出对、差分与缩放等预处理思路并借助MSE、MAE、R²等指标衡量预测成绩进而调整学习率、隐藏层节点与批次大小形成从数据准备到模型优化的完整实践路径。1. 多变量 LSTM 预测成绩从一张成绩单到可复现的 Python 方案期末成绩单上语文、数学、英语、物理、化学五列数字排在一起你想知道下学期哪几门会掉、哪几门能冲上去。单变量 LSTM 只能吃一列历史分数遇到「数学下滑但物理上升」这种交叉影响就抓瞎。多变量预测要做的是把多科成绩当成一个整体序列喂进网络让模型自己学出科目之间的耦合关系再输出下一阶段的分数走向。这套东西在 Python 里用 PyTorch 或 Keras 都能落地核心不在框架而在数据怎么切、变量怎么选、归一化怎么做。适合已经跑通过单变量时间序列、想往多特征场景推进的读者也适合拿成绩数据练手、准备迁移到设备寿命预测或量化因子预测的人。下面按「数据准备 → 模型搭建 → 训练调参 → 避坑 → 进阶验证」的顺序把每一步拆到能直接抄。2. 多变量成绩序列怎么切窗口、步长与归一化的三个决定2.1 为什么成绩预测必须用滑动窗口而不是整段序列LSTM 的输入是一个三维张量形状为(样本数, 时间步长, 特征数)。成绩数据本身是二维表行是时间点列是科目。要变成三维就得用滑动窗口切。假设你有 6 个学期、5 门课的成绩窗口长度设为 3意思是「用前 3 个学期的 5 科分数预测第 4 个学期的某一科或全部科目」。窗口每往后滑 1 个学期就多一条样本。6 个学期、窗口 3能切出 3 条样本数据量确实少但这是成绩场景的常态后面会用数据增强和正则化来补。窗口长度的选择有讲究。太短模型看不到足够的历史趋势太长样本数骤减且早期数据对当前预测的贡献被稀释。我一般会先画自相关图看分数序列在滞后几期后相关性降到 0.3 以下就以那个滞后数作为窗口长度的起点。成绩数据通常 2 到 4 个学期就够因为教学周期本身有阶段性。步长stride决定窗口每次滑动几格。步长 1 样本最多但相邻样本高度重叠容易过拟合步长等于窗口长度则样本不重叠但样本数太少。折中做法是步长取窗口长度的一半比如窗口 4、步长 2。2.2 多变量归一化别对整张表做同一个 min-max这是翻车率最高的地方。很多人拿到成绩表直接对整个 DataFrame 做MinMaxScaler结果语文 150 分制和英语 100 分制被压到同一区间模型学到的「高低」关系是错的。正确做法是逐列归一化每门课用自己的最小值和最大值。代码上就是scaler.fit_transform(df)之前确保df的每一列是独立科目而不是把总分混进去。更稳的做法是用sklearn的ColumnTransformer对每列单独指定缩放器。如果某门课分数分布偏态严重比如竞赛加分导致个别学期异常高可以改用RobustScaler它用中位数和四分位距对离群点不敏感。import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler # 假设 df 的列是 [语文,数学,英语,物理,化学]行是按学期排列 # 逐列归一化每科独立 scaler MinMaxScaler() scaled scaler.fit_transform(df.values) # shape: (学期数, 科目数) def make_windows(data, window_size, stride, target_col): data: 归一化后的二维数组 (T, F) window_size: 用几个时间步预测下一步 stride: 窗口滑动步长 target_col: 要预测的科目索引-1 表示预测全部科目 X, y [], [] for start in range(0, len(data) - window_size, stride): end start window_size X.append(data[start:end, :]) # 窗口内所有科目 if target_col -1: y.append(data[end, :]) # 预测下一学期全部科目 else: y.append(data[end, target_col]) # 只预测指定科目 return np.array(X), np.array(y) WINDOW 3 STRIDE 1 X, y make_windows(scaled, WINDOW, STRIDE, target_col1) # 预测数学 print(X.shape, y.shape) # 例如 (3, 3, 5) (3,)这段代码里make_windows的target_col参数是关键。如果你只想预测数学成绩就传数学列的索引如果想一次性输出所有科目下一学期的分数传-1。注意y的形状会随target_col变化后面搭网络时输出层维度要对应改。参数说明WINDOW3表示用前 3 个学期STRIDE1表示窗口每次滑 1 格样本间有重叠适合小数据集。如果学期数少于 8建议STRIDE取 1 并配合 Dropout如果学期数超过 20可以取 2 或 3 来降低样本相关性。2.3 训练集/测试集切分时间序列不能随机打乱成绩数据是严格按时间排的随机切分会让「未来」的信息泄漏到训练集。正确做法是按时间点切前 70% 学期做训练后 30% 做测试。如果样本极少可以用「留一学期交叉验证」每次留一个学期做验证其余训练轮流一遍。split int(len(X) * 0.7) X_train, X_test X[:split], X[split:] y_train, y_test y[:split], y[split:] # 注意归一化 scaler 只能在训练集上 fit再 transform 测试集 # 上面为了演示简化了实际应先切原始数据再 fit scaler提示归一化器的fit只能见训练数据测试数据用训练集的参数做transform。否则测试集的最小最大值会污染训练过程评估结果虚高。3. PyTorch 搭多变量 LSTM输入维度、隐藏层和输出层的对齐3.1 从 nn.LSTM 的 batch_first 说起PyTorch 的nn.LSTM默认输入形状是(seq_len, batch, input_size)但大多数人习惯(batch, seq_len, input_size)。加batch_firstTrue就能按习惯来。多变量场景下input_size等于科目数比如 5 科就是 5。隐藏层维度hidden_size是超参成绩数据小32 或 64 就够再大容易过拟合。import torch import torch.nn as nn class GradeLSTM(nn.Module): def __init__(self, input_size, hidden_size, output_size, num_layers1, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0 ) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): # x: (batch, seq_len, input_size) out, (h_n, c_n) self.lstm(x) # 取最后一个时间步的隐藏状态 last out[:, -1, :] # (batch, hidden_size) return self.fc(last) # (batch, output_size)num_layers1时dropout参数不生效PyTorch 会警告所以代码里做了条件判断。output_size取决于你要预测几科只预测数学就是 1预测全部 5 科就是 5。forward里取out[:, -1, :]是取序列最后一个时间步的输出因为我们要用窗口内所有历史预测下一步。3.2 训练循环里的三个必调参数学习率、批次大小、早停轮数。成绩数据样本少批次大小设 4 或 8别用 32。学习率从 1e-3 开始如果 loss 震荡就降到 1e-4。早停用验证集 loss 连续 20 轮不降就停防止过拟合。model GradeLSTM(input_size5, hidden_size32, output_size1) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) X_train_t torch.tensor(X_train, dtypetorch.float32) y_train_t torch.tensor(y_train, dtypetorch.float32).unsqueeze(-1) for epoch in range(200): model.train() optimizer.zero_grad() pred model(X_train_t) loss criterion(pred, y_train_t) loss.backward() optimizer.step() if epoch % 20 0: print(fEpoch {epoch}, Loss {loss.item():.6f})unsqueeze(-1)是把y_train从(batch,)变成(batch, 1)和模型输出对齐。如果预测多科output_size5y_train_t就不需要unsqueeze直接是(batch, 5)。3.3 评估指标别只看 MSEMSE 对成绩这种有量纲的数据不直观。换算成 MAE 或 RMSE 后再除以该科满分得到「平均误差几个百分点」。比如数学满分 150RMSE 为 7.5就是平均差 5 分。还可以看方向准确率预测涨跌方向和实际一致的比例。成绩预测里方向比绝对值更重要。from sklearn.metrics import mean_absolute_error, mean_squared_error model.eval() with torch.no_grad(): pred_test model(torch.tensor(X_test, dtypetorch.float32)).numpy() mae mean_absolute_error(y_test, pred_test) rmse np.sqrt(mean_squared_error(y_test, pred_test)) print(fMAE: {mae:.4f}, RMSE: {rmse:.4f}) # 反归一化后才是真实分数反归一化用scaler.inverse_transform但要注意scaler当初是对全部科目 fit 的反归一化时要把预测值放回对应列的位置其他列填 0 或均值再逆变换最后取那一列。4. 成绩预测的避坑清单从数据泄漏到过拟合的五个血泪教训4.1 现象验证 loss 比训练 loss 低很多原因数据泄漏。常见于归一化时用了全量数据 fit或者切分时把未来学期混进了训练集。解决先按时间切分再在训练集上 fit scaler测试集只 transform。检查X_train的时间索引是否全部早于X_test。4.2 现象预测值几乎是一条直线原因模型没学到东西可能因为窗口太长导致样本太少或者学习率太低陷入局部最优。解决缩短窗口到 2提高学习率到 1e-2 试一轮看 loss 是否下降。如果仍不降检查输入数据是否归一化后全为 0 或 1。4.3 现象训练 loss 降到很低测试集一塌糊涂原因过拟合。成绩数据样本少、特征多LSTM 参数量相对过大。解决减小hidden_size到 16加dropout0.3加 L2 正则化weight_decay1e-4或者用早停。4.4 现象多科预测时某一科误差特别大原因该科分数分布和其他科差异大比如体育课是等级制被转成了数字。解决检查每科的量纲和分布对异常科目单独做RobustScaler或者从特征里剔除。4.5 现象换一批学生数据后模型完全失效原因不同学校、不同考试的难度和评分标准不同模型学到的映射不通用。解决把「考试难度」作为额外特征输入或者用迁移学习先在大规模成绩数据上预训练再在小样本上微调。注意成绩数据涉及隐私做实验时用脱敏或模拟数据。别把真实学生姓名、学号喂进模型。5. 进阶验证用「滚动预测」和「科目间注意力」把方案做扎实滚动预测walk-forward validation是时间序列最诚实的评估方式。做法是用前 3 个学期预测第 4 个然后把第 4 个真实值并入历史再预测第 5 个如此滚动。这样能模拟真实使用场景——你永远只有过去的数据。代码上就是每次预测后把真实值 append 到输入窗口再切下一个窗口。def walk_forward(model, data, window_size, target_col): model.eval() history data[:window_size].copy() preds [] for i in range(window_size, len(data)): inp torch.tensor(history[-window_size:], dtypetorch.float32).unsqueeze(0) with torch.no_grad(): p model(inp).item() preds.append(p) # 把真实值并入历史继续滚动 history np.vstack([history, data[i]]) return np.array(preds)这个函数返回的是每一步的预测值和真实值对比就能画出滚动误差曲线。如果误差随滚动步数增大而急剧上升说明模型对长期依赖捕捉不足需要加长窗口或换 GRU。另一个进阶方向是科目间注意力。多变量 LSTM 默认把所有科目同等对待但实际中数学和物理强相关语文和英语强相关。可以在 LSTM 输出后加一个注意力层让模型自己学哪些科目对当前预测更重要。PyTorch 里可以用nn.MultiheadAttention把 LSTM 每个时间步的输出作为 query、key、value输出加权后的表示再进全连接。class AttnLSTM(nn.Module): def __init__(self, input_size, hidden_size, output_size): super().__init__() self.lstm nn.LSTM(input_size, hidden_size, batch_firstTrue) self.attn nn.MultiheadAttention(hidden_size, num_heads2, batch_firstTrue) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): out, _ self.lstm(x) # (batch, seq, hidden) attn_out, _ self.attn(out, out, out) last attn_out[:, -1, :] return self.fc(last)注意力层的num_heads设 2 或 4成绩数据特征少头数多了反而分散。训练时学习率要比纯 LSTM 再低一点1e-4 起步。我自己的习惯是任何时间序列方案先跑通单变量再加多变量再加注意力每加一层都重新做滚动验证。别一次性堆完再调否则出问题不知道是哪层的锅。成绩预测这种小样本场景简单模型往往比复杂模型稳注意力层带来的提升通常只有 1 到 2 个百分点但训练时间翻倍。值不值得看你对可解释性的需求——注意力权重能告诉你哪科对预测影响大这本身就是有价值的信息。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑