
简介面向机器学习初学者的LSTM序列预测实战资源聚焦多变量预测与成绩预测等应用场景系统覆盖时间序列数据预处理、监督转换、模型定义、训练与评估的完整流程。压缩包共33个文件由19个CSV数据集和14个Python脚本组成脚本针对单变量、多变量、多步预测分别给出可运行实现CSV数据则配套各类实验场景方便对照学习与二次改造。资源包仅3.88MB轻量易下载已有3419人学习使用。内容由浅入深从基础的时间序列转监督学习数据开始逐步实现观测值缩放、稳定性处理、LSTM模型搭建与多步预测多变量部分重点演示如何利用多个输入特征共同预测目标适合希望通过Python快速掌握LSTM时序建模的读者。借助这套代码可以清晰理解滑窗构造样本、数据稳定化、多步预测等关键操作并在自带数据集上完成训练、预测与效果验证。1. LSTM多变量预测成绩为什么这个方向值得花一个周末复现想把一个学生的历次考试记录变成下一场考试的分数预测LSTM多变量预测是目前在 Python 生态里最容易落地、也最容易翻车的方案。直接丢给线性回归通常只能得到一条平均趋势线碰上考前突击和成绩起伏就彻底失效。LSTM 要解决的是这类序列问题把成绩、出勤率、作业完成率等多个特征按时间顺序喂进循环网络让模型自己记住低谷之后常有反弹这类模式。这篇笔记把一个可复现的 Python 方案完整讲清楚成绩数据怎么切片、LSTM 模型代码怎么写、训练时哪些坑会翻车以及预测结果到底能不能用到真实决策里。适合拿小数据集练手、想在一两天内跑通整个时间序列预测流程的从业者和学习者。2. 从成绩数据到LSTM输入滑窗、归一化与数据集划分2.1 成绩预测的本质是序列问题不是回归问题常见做法是把最近一次成绩出勤率作业完成率拼成一行扁平特征去做回归。这种做法丢失了最关键的信息时间顺序。连续两周下滑之后第三周反弹的概率和连续五周平稳后的概率完全不同但扁平特征向量里看不出这个顺序。LSTM 之所以适合成绩预测是因为它在每个时间步接收一组特征同时维护一个隐藏状态把前几步的信息带到当前步天然适合这类有前后关联的数据。以预测下次综合测验成绩为例单样本输入形状是 (seq_len, features)。seq_len 取 5代表过去 5 周features 取 5代表每周的数学成绩、英语成绩、出勤率、作业完成率、自习时长。输出是下一周综合测验的分数。这就是标准的多变量时间序列预测多个历史变量一个未来目标。和传统时间序列模型对比一下更能看清选型边界。ARIMA 这类模型要求数据平稳、需要手动确定阶数多变量支持也相对笨拙XGBoost 这类树模型能处理多特征但如果不人工构造滞后特征它看不到时间顺序。LSTM 的优点是多特征直接作为序列输入缺点是需要更多数据、训练不确定性大。成绩记录只有几十条时ARIMA 可能更稳但多变量场景下 LSTM 的扩展性最好这也是本篇选择它的核心理由。2.2 用滑窗把成绩历史转成监督学习样本假设你已经准备好一张表每行是一个学生某一周的多维记录列依次是数学成绩、英语成绩、出勤率、作业完成率、自习时长最后一列是当周综合测验成绩。LSTM 不能直接吃整张表得先把长序列切成 (seq_len, features) 的小窗口这一步叫滑窗。import numpy as np def make_windows(X, y, seq_len5): # X: 按时间排序的多维特征(n_samples, n_features) # y: 每个时间步对应的成绩标签(n_samples,) windows_x, windows_y [], [] for i in range(len(X) - seq_len): windows_x.append(X[i:i seq_len]) # 过去 seq_len 周的特征 windows_y.append(y[i seq_len]) # 下一周综合测验成绩 return np.array(windows_x), np.array(windows_y)逻辑上第 i 到 iseq_len-1 行的特征作为输入第 iseq_len 行的成绩作为目标。窗口之间允许重叠成绩表数据量小时靠重叠才能凑出足够样本。len(X) 个时间步能切出 len(X)-seq_len 个窗口如果某个学生只有 30 周记录seq_len 取 5 最后只有 25 条窗口所以历史记录不足时别急着把 seq_len 调大。这里特意没做随机打乱。一旦打乱时间顺序信息就废了模型在验证集上的表现会虚高因为同一条成绩曲线的多段窗口可能被拆进了两个集合。多变量预测的数据划分必须是先按时间切开再做窗口顺序反了后面所有指标都没有参考价值。2.3 归一化与数据集划分先切分再 fit 归一化器LSTM 对输入尺度很敏感。成绩是 0 到 100 的大数出勤率是 0 到 1 的小数自习时长是 0 到 300 的更大数直接混在一起梯度会被大数值特征主导。用 MinMaxScaler 把全部特征压到 [0,1] 区间输出也压到 [0,1]模型收敛会稳定很多。from sklearn.preprocessing import MinMaxScaler n_train int(len(X) * 0.7) X_train_raw, X_test_raw X[:n_train], X[n_train:] y_train_raw, y_test_raw y[:n_train], y[n_train:] scaler_x MinMaxScaler() scaler_y MinMaxScaler() X_train_norm scaler_x.fit_transform(X_train_raw) X_test_norm scaler_x.transform(X_test_raw) y_train_norm scaler_y.fit_transform(y_train_raw.reshape(-1, 1)).ravel() y_test_norm scaler_y.transform(y_test_raw.reshape(-1, 1)).ravel() # 划分完成后再构造窗口测试集样本不会被训练集窗口“看到” X_tr, y_tr make_windows(X_train_norm, y_train_norm, seq_len5) X_te, y_te make_windows(X_test_norm, y_test_norm, seq_len5)这里有一个一步都不能省的细节归一化统计量只从训练集 fit测试集调用 transform 而不是 fit_transform。如果先归一化再划分测试集的最大最小值会混进 scaler模型在训练时等于偷看了未来的数据范围预测分数会被人为抬高。这种错误在成绩预测项目里非常隐蔽因为损失曲线看起来一切正常真实原因只有换新数据时才会暴露。为什么用 MinMaxScaler 而不是 StandardScalerLSTM 内部使用 tanh 激活输入范围落在 [0,1] 能避开 tanh 的饱和区梯度传递更顺畅。类别型特征不要直接塞进 MinMaxScaler课程类型、班级编号这类离散列要么删掉要么做 one-hot 后单独处理。3. 用PyTorch搭多变量LSTM模型结构与三处关键配置3.1 为什么选 PyTorch 而不是 Keras成绩数据集通常很小几十到几百条几十个 epoch 也就几秒钟框架间的运算速度差距完全感觉不到。选 PyTorch 的真正理由是动态图和调试体验训练时可以在任意一行打断点看 hidden state 的形状方便定位输入输出维度在哪一步出错。后面要加注意力、改成多步预测时PyTorch 的代码改动量比 Keras 小得多。如果你更熟悉 Keras用 Sequential 堆 LSTM 层也能跑通但遇到预测值是一条直线这类问题时Keras 对中间层状态的黑匣子会让排查多绕一圈。PyTorch 模型代码直白调试时的血泪经验更少。另外成绩预测这种小数据任务用 CPU 训练完全够不必强求 CUDA环境配置成本也降低了。3.2 LSTM 模型代码与输入输出形状PyTorch 里 LSTM 层的输入形状是 (batch, seq_len, input_size)把 batch_firstTrue 打开后更直观。模型定义如下import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size5, hidden_size32, num_layers1, output_size1): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue ) 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_hidden h_n[-1] # 取最后一层最后一个时间步的隐状态 return self.fc(last_hidden) # (batch, output_size)input_size 必须等于特征数 5。hidden_size 取 32 是成绩这类小数据集的常见起点太小记不住趋势太大容易过拟合。num_layers 先用 1两层虽然理论上能建模更复杂的关系样本量小的时候反而会让模型更难收敛。forward 里为什么不直接用 out[:, -1, :]因为多层 LSTM 时 out 表示最后一层的所有时间步输出h_n[-1] 表示最后一层最后一个时间步的隐状态两者在取最后一时间步这件事上等价但 h_n 的语义更明确后面加层数不用改动。单层模型里怎么取都行按 h_n[-1] 写更稳妥。LSTM 对输入输出维度极其严格新手最容易在这里翻车。x 传入时必须是 float32如果原始数据是 inttorch.tensor 默认保留 int 类型进 LSTM 直接报 dtype 错误。构造输入时养成加 dtypetorch.float32 的习惯能省掉半个小时的排查时间。3.3 损失函数、优化器与学习率成绩预测是回归任务MSE 是默认选择。MSE 对大误差是平方级惩罚某次预测偏 20 分会比偏 5 分多出 16 倍的损失这符合成绩预测宁可保守也不允许离谱的实际需求。device torch.device(cuda if torch.cuda.is_available() else cpu) model LSTMPredictor(input_size5, hidden_size32, num_layers1).to(device) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, patience5, factor0.5 )Adam 配 lr0.001 是 LSTM 训练最稳的起点。成绩数据量小梯度本来就波动大把 lr 调到 0.01 很容易在几个 epoch 内把损失打成 NaN。scheduler 的作用是当验证损失连续 5 个 epoch 不再下降时学习率减半等于给训练留了后悔药。为了防止实验不可复现在模型定义前固定随机种子def set_seed(seed42): torch.manual_seed(seed) np.random.seed(seed) set_seed(42)不固定种子的话同一份代码跑三次会得到三个不同结果调参时根本无法判断某个改动是真实提升还是随机波动。成绩预测这类小数据集随机种子对结果的影响可能比 hidden_size 还大。4. 训练循环与成绩预测评估指标怎么设才算能用4.1 训练循环与早停小数据集上训练 LSTM最怕的是过拟合和反复震荡。固定 epoch 数不靠谱因为不同特征组合的收敛速度差异很大。写入早停机制后验证损失连续 patience 个 epoch 不降就停同时把出现过的最小验证损失对应的权重存下来。from torch.utils.data import DataLoader, TensorDataset train_ds TensorDataset(torch.tensor(X_tr, dtypetorch.float32), torch.tensor(y_tr, dtypetorch.float32)) val_ds TensorDataset(torch.tensor(X_te, dtypetorch.float32), torch.tensor(y_te, dtypetorch.float32)) train_loader DataLoader(train_ds, batch_size16, shuffleTrue) val_loader DataLoader(val_ds, batch_size16, shuffleFalse) def evaluate(model, loader): model.eval() total_loss 0.0 with torch.no_grad(): for xb, yb in loader: xb, yb xb.to(device), yb.to(device) pred model(xb).squeeze(-1) total_loss criterion(pred, yb).item() * xb.size(0) return total_loss / len(loader.dataset) def train_model(model, train_loader, val_loader, epochs80, patience10): best_val_loss float(inf) wait 0 for epoch in range(epochs): model.train() epoch_loss 0.0 for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() pred model(xb).squeeze(-1) loss criterion(pred, yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() epoch_loss loss.item() * xb.size(0) val_loss evaluate(model, val_loader) scheduler.step(val_loss) if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_lstm.pt) wait 0 else: wait 1 if wait patience: print(fepoch {epoch} 触发早停) breakDataLoader 里 shuffleTrue 会不会打乱时间顺序不会。窗口化之后每个样本内部已经保留了 seq_len 的时间结构打乱的是样本间的出场顺序反而能帮模型减少记忆偏置。测试集必须 shuffleFalse保证预测顺序与真实序列一致。clip_grad_norm_ 这行最容易被省略但小数据集上必须加。成绩序列偶尔会出现极端值反向传播梯度范数可能瞬间翻倍归一化到 [0,1] 缓不住这种尖峰clip 之后训练就稳了。max_norm1.0 是保守值如果发现训练欠拟合可以放宽到 5.0。早停 patience 取 10 意味着至少多等 10 个 epoch 才肯认输。如果 val_loss 前 20 个 epoch 一直在缓慢下降patience 太小会提前停这时把 patience 改成 15 再跑一遍。小数据集上这种反复试错很正常。4.2 评估指标MAE、RMSE 与 R²训练损失是归一化空间的数字不能直接拿去说损失 0.02 所以效果很好。预测值和真实值都要反归一化回原始分数再算有业务意义的指标from sklearn.metrics import mean_absolute_error, r2_score def evaluate_scores(y_true_norm, y_pred_norm, scaler_y): y_true scaler_y.inverse_transform(y_true_norm.reshape(-1, 1)).ravel() y_pred scaler_y.inverse_transform(y_pred_norm.reshape(-1, 1)).ravel() mae mean_absolute_error(y_true, y_pred) rmse float(np.sqrt(((y_true - y_pred) ** 2).mean())) r2 r2_score(y_true, y_pred) return mae, rmse, r2MAE 最容易向不懂模型的人解释平均差 3.8 分。RMSE 比 MAE 大多少可以判断误差分布是否有长尾——如果 RMSE 接近 MAE 的三倍说明有个别样本预测得离谱需要回去查那条样本。R² 在成绩预测里到 0.6 到 0.8 就算可用因为考试成绩本身带有随机因素想逼近 1.0 反而说明数据里有问题。提示反归一化时务必保证 y_pred 和 y_true 的顺序完全对齐不要在中间做任何排序或随机抽样后再算指标否则 MAE 会被严重低估。4.3 反归一化与预测曲线对照指标只能给结论看不到问题。把测试集预测结果画成曲线能一眼看出模型是不是在追着上一步走或者无脑输出平均分。成绩按周记录的话用折线图加散点标记就够。import matplotlib.pyplot as plt model.load_state_dict(torch.load(best_lstm.pt)) model.eval() y_pred_norm, y_true_norm [], [] with torch.no_grad(): for xb, yb in val_loader: xb xb.to(device) pred model(xb).squeeze(-1) y_pred_norm.append(pred.cpu().numpy()) y_true_norm.append(yb.numpy()) y_pred_norm np.concatenate(y_pred_norm) y_true_norm np.concatenate(y_true_norm) mae, rmse, r2 evaluate_scores(y_true_norm, y_pred_norm, scaler_y) print(fMAE{mae:.2f}, RMSE{rmse:.2f}, R2{r2:.2f}) plt.figure(figsize(10, 4)) plt.plot(y_true_norm, label真实成绩, markero, linewidth1.5) plt.plot(y_pred_norm, labelLSTM预测, markerx, linewidth1.5) plt.legend() plt.xlabel(测试样本编号按时间排序) plt.ylabel(成绩分) plt.title(LSTM多变量预测成绩对照) plt.show()对照图里出现两类问题需要警惕一是预测曲线的波峰全部被削平说明模型学到了均值回归没学到尖峰模式二是预测曲线相对真实值整体平移说明可能存在数据泄露或者特征里缺少当前状态信息。这两类问题单看 MAE 看不出来必须看图。5. LSTM多变量预测常见问题排查五个踩坑记录5.1 测试集指标很高但换到下学期就失效现象在历史成绩划分的测试集上 MAE 只有 3 分左右模型看起来已经能用了换到新学期的数据预测误差飙到 8 分以上。原因成绩数据里隐藏着时间结构。不同学期教师出题难度、班级整体水平都在变化模型很可能把第几周学期编号这类周期性信息当成了硬规则记住。更常见的元凶是归一化训练集和测试集混在一起 fit scaler或者未来数据被提前划进训练集属于典型的数据泄露。解决严格按时间顺序切分训练集只占前 70%scaler 只 fit 训练集删除周次学期号这类周期性列避免模型把时间索引当特征硬记。成绩预测项目里九成假高精度都是这个原因。5.2 预测值是一条几乎水平的直线现象测试集预测结果标准差极小曲线贴着均值走MAE 看着还行但完全没抓住成绩波动。原因LSTM 在小数据集上很容易学到输出平均成绩这种最优策略。平方误差下输出均值对大多数普通样本已经很安全只有少数尖峰样本被牺牲掉。hidden_size 过大、num_layers 过多会加剧模型把波动当成噪声滤掉了。解决先把 hidden_size 降到 16 或 8强制模型记忆更少的模式把 dropout 调低甚至去掉给训练更多自由度。如果确实需要更强的序列建模考虑加注意力而不是加深层数。成绩预测这里不是越深越好我在这上面翻过车。5.3 损失在前几个 epoch 变成 NaN现象训练到第 3 到第 5 个 epochloss 突然变成 nan后续无法恢复。原因学习率过大梯度在某个极端样本上爆炸或者原始成绩数据里存在缺失值NaN 经过归一化和窗口拼接后没有报错直到损失函数里被放大。解决先检查数据里有没有 np.isnan(X).any()有就先填充或删除再把 lr 从 0.001 降到 0.0003同时保留 clip_grad_norm_。如果还是 NaN看成绩列有没有无穷值MinMaxScaler 对无穷值不会报错但会把其他正常值压成 0。5.4 seq_len 到底取 3、5 还是 10现象seq_len 取 5 时 MAE 是 4.2取 10 时变成 4.8取 3 时变成 4.5看不出规律。原因seq_len 是模型对多长的历史记忆最敏感的超参数。取太短看不见连续下滑后的反弹模式取太长早期信息对下一周成绩的作用趋近于零反而引入噪声。成绩数据通常不存在长程依赖5 到 7 周覆盖一个月的学习节奏已经是极限。解决拿验证集做小网格搜索seq_len 在 [3, 5, 7, 10] 里各跑一遍每次固定 3 个随机种子取平均。这个超参数没有理论最优值数据决定一切靠玄学猜不如直接跑表。5.5 预测成绩跑出 120 分现象真实成绩都在 40 到 95 分之间预测值却出现 120 分甚至负数。原因输出层是线性激活LSTM 隐状态经过 fc 层后可以映射到任意值。归一化只约束训练目标测试时模型完全可能外推加上极端历史成绩的带动预测自然脱离合理区间。解决预测后做边界裁剪低于 0 按 0 算高于 100 按 100 算更稳的做法是把输出层改成 Sigmoid 再乘 100让模型结构上就不能越界。后一种会略微压缩中间区间如果 R² 因此下降超过 0.05 就退回线性输出加裁剪。6. 进阶给LSTM加注意力层专门改善尖峰学生的预测成绩预测里另一个痛点是尖峰连续低迷后突然考出高分以及稳定优秀生的偶发失误普通 LSTM 都容易把这类剧烈变化平滑掉。一个改动小、收益明显的方法是给 LSTM 输出加一层注意力机制让模型自动对不同时间步的历史状态加权。每个时间步的重要程度不再默认相等而是由一个小网络根据当前隐状态计算权重。class AttentionLSTM(nn.Module): def __init__(self, input_size5, hidden_size32, output_size1): super().__init__() self.lstm nn.LSTM(input_size, hidden_size, batch_firstTrue) self.attn nn.Linear(hidden_size, 1) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): out, _ self.lstm(x) # (batch, seq_len, hidden) weights torch.softmax(self.attn(out).squeeze(-1), dim1) context (out * weights.unsqueeze(-1)).sum(dim1) return self.fc(context)代码把输出层之前的所有隐状态做加权求和权重由 softmax 归一化到和为 1。训练时网络会自动学会给考前一周的状态更高权重给两个月前的记录更低权重。训练循环、早停、评估流程全部复用前面第 4 章的代码只需把模型类换掉。第一次跑别急着替换原模型先拿基础 LSTM 的输出当 baseline。同一套测试集上同时评估两版注意力版本 MAE 下降超过 0.5 再考虑保留。数据量小于 200 条时这个提升完全可能是随机波动用 3 个随机种子跑完取均值再看。我自己的经验是先把基础 LSTM 跑通、指标能稳定复现再加注意力一次只加一个变量。这个顺序帮我避开了很多自我感动式的调参。希望帮到你。本文还有配套的精品资源点击获取