☰
Python LSTM多变量预测实战:从滑动窗口到成绩预测模型
2026/10/11 2:58:05 网站建设 项目流程

简介:一份面向Python开发者的LSTM(长短期记忆网络)预测实战资源,适合机器学习初学者及需要处理时序数据的技术人员。内容聚焦多变量预测、单变量预测与多步预测,覆盖从数据预处理、时间序列平稳化、有监督转换到模型构建、性能评估与参数优化的完整流程,可应用于股票走势、天气变化、商品销量等场景。包内含33个文件,14个Python脚本负责数据清洗、样本构造、模型定义与训练,19个CSV文件提供真实实验数据,脚本按单变量、多变量、多步预测等目录清晰划分,压缩包整体仅3.88MB。目前已有3419人学习,轻量便携便于快速上手。通过学习其中的代码,可掌握滑动窗口切分序列、观测值缩放、多步预测网络搭建等实用技巧,并理解如何评估MSE、MAE、R²等预测成绩指标,既能作为课程设计参考,也能为实际项目打下基础。

1. LSTM多变量预测:为什么这个Python模型值得亲手搭一遍

LSTM多变量预测是时间序列任务里最常用的深度学习方案之一,尤其适合像学生成绩预测这种同时受多个因素影响、又存在先后顺序的场景。很多人拿到一份成绩数据,第一反应是线性回归或随机森林,但一旦把“复习时长”“缺勤次数”“作业完成率”和历史成绩放进同一个序列,普通模型就难以捕捉“上一轮没考好,这轮才发力”这种带滞后的关系。LSTM靠门控结构把过去N个时间步的多维特征压缩成一个隐藏状态,再映射到下一轮成绩,天然比前馈网络更擅长这类依赖。这份资源就是用Python把“数据清洗→滑动窗口构造→模型训练→预测可视化”整条链路串起来,新手能直接照跑,熟手可以拿这套模板改造成电力负载、股价、流量等其他多变量场景。

2. 多变量LSTM输入构造:先把成绩序列变成监督学习格式

LSTM在Keras里的输入张量是三维的:(样本数, 时间步数, 特征数)。很多人第一次写代码就把(样本数, 特征数)丢进去,然后被报错按在地上摩擦。这一章先把最基础、也最容易出错的输入构造讲透。

2.1 从一维成绩序列到三维输入:时间步与特征维度怎么定

以学生成绩预测为例,假设你有一个DataFrame,每一行是一次考试的记录,列包括:

  • score:本次考试成绩(目标变量)
  • study_hours:考前一周累计学习时长
  • absence:这个学期累计缺勤次数
  • homework:作业平均完成率(0到1)

这是一个典型的多变量序列:成绩不仅受上一次成绩影响,还受这段时间的学习行为影响。要预测下一次考试,常见做法是取最近5次记录构成一个滑动窗口,窗口内所有变量作为输入,窗口后一次的score作为输出。动手前先想清楚两个参数:

  • time_steps:用过去几次记录预测下一次。成绩预测一般取5到10,太多会把很久远的行为噪声带进来,太少又捕捉不到趋势。
  • n_features:每个时间步里有几个变量,这里就是4(成绩加三个行为特征)。

确定了这两个数,输入形状就是(总样本数, time_steps, n_features)。我用下面的函数把DataFrame转换成监督学习格式:

import numpy as np import pandas as pd def create_sequence(data, time_steps=5, n_ahead=1, target='score'): """ 将多变量时间序列转为监督学习样本。 data: DataFrame,必须按时间排序 time_steps: 用几个历史时间步作为输入 n_ahead: 预测未来第几个时间步 target: 目标变量列名 """ X, y = [], [] data = data.reset_index(drop=True) for i in range(len(data) - time_steps - n_ahead + 1): X.append(data.iloc[i:i+time_steps].values) # 时间窗口内所有特征 y.append(data.iloc[i+time_steps+n_ahead-1][target]) # 窗口结束后第n_ahead个成绩 return np.array(X), np.array(y) # 假设data已经按考试日期排好序 X_data, y_data = create_sequence(data, time_steps=5, n_ahead=1) print('输入形状:', X_data.shape) # (样本数, 5, 4) print('目标形状:', y_data.shape) # (样本数,)

这段代码的核心逻辑是滑动窗口:从第0行开始,取i到i+time_steps-1行作为输入,取第i+time_steps+n_ahead-1行的目标变量作为标签。n_ahead设为1就是预测下一次考试,设为3就能预测“未来第三次考试”。预测步数越大,不确定性越高,误差会明显增长。

滑动窗口还有一个容易被忽略的代价:样本量从len(data)变成len(data) - time_steps - n_ahead + 1。如果一个学生只有十几场考试记录,窗口设10会直接丢一半样本。我一般先看数据量,少于50条就把time_steps压到3到5,或者用不同的窗口步长重复采样,相当于做数据增强。多数成绩预测场景,时间步设得过大反而容易过拟合,后面会专门讲。

2.2 归一化与反向还原:多变量必须统一量纲,预测结果才能读懂

成绩是0到100的分数量级,缺勤次数是0到10,作业完成率是0到1,学习时长可能是几十小时。如果不做归一化,LSTM的默认激活函数tanh很容易饱和——特征值大的维度主导梯度,模型学不到小量纲特征的影响。

常见做法是用MinMaxScaler把所有变量统一缩放到0到1。这里有两个容易犯错的地方:

  1. scaler只能用训练集来fit,不能先对整个数据集fit再切分,否则测试集的信息会通过统计量泄露进训练过程,验证指标会虚高。
  2. 归一化要把目标变量和其他特征放在一起缩放,因为预测的是连续值,逆变换时需要一个完整的一行才能还原出原始量纲。

下面给出正确的训练/测试切分和归一化代码:

from sklearn.preprocessing import MinMaxScaler # 按时间顺序切分,绝不要随机打乱 train_size = int(len(data) * 0.8) train_df = data.iloc[:train_size] test_df = data.iloc[train_size:] # 先fit训练集,再transform训练集和测试集 scaler = MinMaxScaler(feature_range=(0, 1)) scaled_train = scaler.fit_transform(train_df) scaled_test = scaler.transform(test_df) # 转成DataFrame以保留列名,方便后面的滑动窗口 scaled_train = pd.DataFrame(scaled_train, columns=train_df.columns) scaled_test = pd.DataFrame(scaled_test, columns=test_df.columns) # 分别构造序列 X_train, y_train = create_sequence(scaled_train, time_steps=5, n_ahead=1) X_test, y_test = create_sequence(scaled_test, time_steps=5, n_ahead=1)

这里的关键点是fit_transform只作用在train_df上,test_df用的是已经学好的最大最小值,不能自己再fit一次。

预测完成后,逆变换是另一个高频翻车点。模型的输出y_pred是归一化后的值,要还原成真正的成绩,不能直接scaler.inverse_transform(y_pred),因为inverse_transform期望输入的列数必须和fit时一致,也就是4列。常见做法是伪造一个占位矩阵,把预测值填进目标变量列,其他列填0,还原后再取目标列:

# y_pred形状为 (样本数, 1) 或 (样本数, ) num_features = train_df.shape[1] target_index = list(train_df.columns).index('score') dummy = np.zeros((len(y_pred), num_features)) y_pred_flat = y_pred.reshape(-1) dummy[:, target_index] = y_pred_flat inversed = scaler.inverse_transform(dummy) pred_scores = inversed[:, target_index]

这样还原出来的pred_scores才是和原始成绩同一量纲的值。逆变换只是机械地按缩放比例还原,如果模型预测归一化值是0.9,还原成90分,不代表模型真的学到了成绩分布,只能说明数值范围对得上。

这一章构造的数据格式直接决定后面的模型能不能跑通。多花五分钟检查维度,比之后对着报错发呆一小时划算得多。

3. Keras搭建LSTM模型:层数、神经元与成绩预测的损失函数

数据格式准备好之后,模型本身反而简单。Keras的LSTM层封装得很干净,正因为封装得好,很多人把参数当玄学调。这一章给出一个基线模型,然后解释每个参数为什么这么设。

3.1 从单层到堆叠:神经元数量不是玄学,而是由数据量决定

先用一个两层LSTM作为起步模型,适用于样本量在几百到几千之间的成绩预测场景:

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.optimizers import Adam n_steps = X_train.shape[1] # 5 n_features = X_train.shape[2] # 4 model = Sequential([ LSTM(64, activation='tanh', return_sequences=True, input_shape=(n_steps, n_features)), Dropout(0.2), LSTM(32, return_sequences=False), Dropout(0.2), Dense(1, activation='linear') ]) model.compile(optimizer=Adam(learning_rate=0.001), loss='mse', metrics=['mae']) model.summary()

输入层不需要指定样本数,只要(n_steps, n_features)。第一个LSTM的return_sequences=True是因为后面还要接LSTM层,需要输出完整序列;第二个LSTM输出一个向量,最后接Dense(1)得到连续值。

神经元数量为什么是64和32?LSTM参数量大约是4 * (n_features + units) * units,units=64时参数约17k,32时约4k,两层合计不到30k。对于几百个样本的数据,这个复杂度已经偏高;如果数据只有几百行,我一般先跑一个单层LSTM(32)作为基线,看验证集误差再慢慢加大。不建议一上来就上双向LSTM——双向会用未来信息做训练,真实预测时只能用过去序列,训练与预测的不对称会让线上效果打折扣。

如果数据量小,可以把LSTM换成GRU,参数更少、训练更快,效果基本持平。激活函数保持tanh不要动,LSTM内部的门控设计默认依赖它;输出层用linear,因为回归任务不约束输出范围。

3.2 损失函数与优化器:回归预测别默认用accuracy

Keras里新手最容易犯的错是回归任务也用accuracy做评估指标,然后在训练日志里看到一堆无法解释的分数。成绩预测是连续值回归,损失函数从mse和mae里选。

mse对大误差惩罚更大:一次预测差50分,比差2分带来的损失高几百倍,适合希望避免极端失误的场景。mae对异常值更鲁棒:如果成绩里有人缺考记为0分,mae不会让模型为了压低这个异常点而扭曲整体预测。我通常先用mae跑基线,再用mse调优,看测试集RMSE决定保留哪个。

优化器直接用Adam,初始学习率0.001通常够。如果训练震荡,降到0.0005。LSTM梯度爆炸也是常见问题,可以给Adam加clipnorm限制梯度范数:

from tensorflow.keras.optimizers import Adam optimizer = Adam(learning_rate=0.001, clipnorm=1.0) model.compile(optimizer=optimizer, loss='mae', metrics=['mse'])

clipnorm=1.0表示把梯度范数裁剪到不超过1,这是LSTM稳定训练的常用保护。

关于模型保存,Keras 3默认.keras格式,旧代码可能用.h5。为了少踩坑,训练时直接保存最稳的格式:

model.save('lstm_grade.keras')

加载时用load_model('lstm_grade.keras')。如果要在不同TensorFlow版本间搬运,最好只保存权重model.save_weights('lstm_grade.weights.h5'),换环境后先重建一模一样的结构,再load_weights,兼容性最好。

4. 训练与评估:数据切分、早停和误差可视化

模型结构定了,训练策略决定了最终泛化能力。这一章讲清楚切分原则、回调函数配置以及怎么判断模型是不是真的在预测。

4.1 时间序列切分:随机打乱等于穿越预测

训练集和测试集不能直接train_test_split(shuffle=True),否则测试集里会混入比训练集更早的数据,相当于模型提前“看过答案”。时间序列必须按时间顺序切分:前面的数据训练,后面的数据验证。

# 继续使用2.2中构造的X_train, y_train, X_test, y_test # 但为了调参,再从训练集末尾切一部分作为验证集 val_size = int(len(X_train) * 0.1) X_tr, X_val = X_train[:-val_size], X_train[-val_size:] y_tr, y_val = y_train[:-val_size], y_train[-val_size:]

注意这里X_val是从训练集末尾连续切出来的,它仍然晚于X_tr,早于X_test,符合时间逻辑。同时要保证所有序列来自同一个学生或同一个实体,不要让同一个学生的样本同时出现在训练和验证里,否则也是变相泄露。

如果多实体混合,比如多个学生的成绩序列,切分前还应该按学生ID分组,做分组切分:

# 以student_id为例,按学生划分 unique_students = np.unique(df['student_id']) train_students = unique_students[:int(len(unique_students)*0.8)] test_students = unique_students[int(len(unique_students)*0.8):] train_df = df[df['student_id'].isin(train_students)] test_df = df[df['student_id'].isin(test_students)]

这个细节决定模型能否泛化到“没见过的学生”,而不是仅仅复读某个学生的历史规律。

4.2 早停与检查点:别让模型在验证集上过拟合到底

LSTM训练轮数不好拍脑袋,靠epochs设个1000然后看loss死磕是低效做法。常用配置是三个回调一起上,让训练自动停止在验证集最优处:

from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau callbacks = [ EarlyStopping(monitor='val_loss', patience=20, restore_best_weights=True), ModelCheckpoint('best_lstm.keras', monitor='val_loss', save_best_only=True), ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=10, min_lr=1e-5) ] history = model.fit( X_tr, y_tr, validation_data=(X_val, y_val), epochs=200, batch_size=16, callbacks=callbacks, verbose=1 )

参数说明:

  • patience=20:验证loss连续20轮不下降就停止。数据量小、噪声大时,patience可以再大一点到30,防止在平稳期误停。
  • restore_best_weights=True:停止时自动回滚到验证集最优的权重,而不是最后一步的权重。这是后悔药,必须加。
  • ModelCheckpoint保存最优模型,后面预测直接用这个文件。
  • ReduceLROnPlateau在验证loss停滞时把学习率减半,min_lr=1e-5防止跑飞。

batch_size=16是常用默认值。样本只有几百时,批次设8或16,太小会梯度抖动,太大会显存不够或者收敛慢。成绩预测场景不用刻意追求大batch,因为LSTM训练本身已经比较吃资源。

4.3 误差指标与可视化:怎么看预测是否滞后

训练完先看测试集上的误差,再看预测曲线与真实曲线的形状拟合度:

import numpy as np from sklearn.metrics import mean_absolute_error, mean_squared_error y_pred = model.predict(X_test, verbose=0) # 假设pred_scores和true_scores已经通过2.2的逆变换还原回原始分数 # 这里直接用归一化值也可以评估,但曲线对比更建议用原始分数 mae = mean_absolute_error(true_scores, pred_scores) rmse = np.sqrt(mean_squared_error(true_scores, pred_scores)) print(f'MAE: {mae:.2f}, RMSE: {rmse:.2f}')

画真实值和预测值的折线图:

import matplotlib.pyplot as plt plt.figure(figsize=(12, 4)) plt.plot(true_scores, label='真实成绩', linewidth=1.5) plt.plot(pred_scores, label='LSTM预测', linewidth=1.5) plt.legend() plt.xlabel('测试集样本序号') plt.ylabel('成绩') plt.title('LSTM多变量成绩预测结果') plt.show()

看曲线时重点看两件事。第一是预测曲线是不是比真实曲线“慢半拍”——如果预测值整体右移一个点,说明模型只是在复读上一个时间步,这在时间序列里叫滞后预测,误差指标可能不高,但没有任何实际价值。第二是两头极端值有没有被“拉平”,LSTM天然倾向于预测均值,如果真实成绩经常出现满分或零分,预测曲线会在中间区域,这时要检查特征里是不是缺少关键突变信号。

5. 避坑与常见问题:多变量LSTM的五个翻车现场

这一章直接对着我自己踩过的坑写,每条都是“现象→原因→解决”,按频率排序。

5.1 输入维度报错:is incompatible 是每个LSTM新手的第一课

现象:model.fit(X_train, y_train)一开始就报错,提示Input 0 of layer lstm is incompatible with the layer: expected ndim=3, found ndim=2。

原因:输入数组是二维的,缺少时间步维度。最常见的来源是直接用原始DataFrame当X_train,或者用reshape((样本数, 特征数))把序列拍扁了。滑窗没做,模型拿到的不是序列,自然进不了LSTM。

解决:先打印X_train.shape,确认是三维,形状为(样本数, time_steps, features)。如果手头数据确实没有滑窗,临时补救是X_train = X_train.reshape((X_train.shape[0], 1, X_train.shape[1])),这等于用单时间步预测,效果通常很差,但至少能跑通。正确做法是回到2.1用create_sequence重新构造。

5.2 预测曲线滞后:模型像在复读上一步成绩

现象:预测值和真实值画在一起,两条曲线形状几乎一样,但预测整体右移了一个点,像“昨天的成绩”。测试集MAE低得可疑。

原因:滑动窗口里包含了与预测目标时间重叠的信息。具体来说,如果用第t次的成绩去预测第t次的成绩,特征里已经带了答案,模型直接把这个特征抄出来就行。另一种情况是特征里放了当期的缺勤或学习时长,而这些数据在真实预测时还没发生。

解决:检查特征矩阵的时间对齐。预测下一次考试,特征必须只包含“本次之前”的信息。我在create_sequence里已经确保了这一点:取i到i+time_steps-1作为特征,目标是i+time_steps+n_ahead-1,中间至少隔了n_ahead个时间步。如果你改了代码,建议人工抽查一条样本,确认X_data[i]最后一行的时间早于y_data[i]对应的时间。

5.3 验证集loss飙升:过拟合,还是学习率太大

现象:训练loss降到0.01,验证loss却从第30轮开始从0.08一路涨到0.3,并且没有回头趋势。

原因:这是过拟合的典型特征,尤其是数据量小、网络结构偏复杂。另一种可能是学习率太大导致loss震荡,但那种情况训练loss不会稳定下降,两者可以区分。

解决:先做三件事——把Dropout从0.2提到0.3,LSTM神经元砍一半(比如64→32),早停patience调小到15。如果还压不住,说明模型容量仍然超出数据信息量,考虑用单层LSTM加全局池化,或者改用GRU减少参数。

5.4 逆变换后数据全乱:占位列和顺序对不上

现象:预测值还原后出现负数,或者本来预测75分,结果变成7.5分。

原因:inverse_transform时占位矩阵的列顺序和fit时的列顺序不一致,或者目标列索引取错。比如训练时DataFrame列顺序是['score','study_hours','absence','homework'],但逆变换时把预测值放到了第0列,恰好第0列是score才侥幸对上;顺序一换就全错。

解决:构造占位矩阵时,明确用列名获取索引,绝不用硬编码数字:

cols = train_df.columns target_idx = list(cols).index('score') dummy = np.zeros((len(y_pred), len(cols))) dummy[:, target_idx] = y_pred.flatten() inversed = scaler.inverse_transform(dummy)

如果反推后仍然不对,再用一个笨办法验证:把训练集第一行做一次inverse_transform,看还原出来的值是否等于原始第一行。能对上再用于预测结果。

5.5 模型文件加载失败:h5还是keras,版本别糊里糊涂

现象:训练时保存model.save('model.h5'),换一台机器load_model('model.h5')报错无法解析,或者反过来在Keras 2里加载Keras 3保存的文件报错。

原因:TensorFlow/Keras版本不一致。Keras 3默认保存为.keras格式,旧版代码可能用.h5;旧版Keras不认识新格式。

解决:保存前统一格式。Keras 3推荐model.save('best.keras'),加载对应load_model('best.keras')。如果不确定目标环境版本,保存权重最保险:

# 训练完成后 model.save_weights('lstm_weights.h5') # 新环境加载前,先搭一模一样的结构 new_model = Sequential([...]) # 和训练时完全相同的层配置 new_model.load_weights('lstm_weights.h5')

这种方式绕开模型结构序列化差异,只要你把结构代码注释清楚,基本不会翻车。

6. 进阶:多步成绩预测的两种思路和上线前的快速验证习惯

预测下一次考试只是入门,实际需求经常是“未来3次考试能不能上85”。多步预测有两种主流思路,各有适用边界。

递归预测最简单:训练一个单步模型,把预测值作为下一步的输入,循环调用。代价是误差会像滚雪球一样累积,预测步数越多越不准。代码实现就是在预测时更新窗口里的目标变量列,其他特征保持最近观测值:

def predict_future(model, last_window, steps=3, score_idx=0): """ last_window: shape为(1, time_steps, n_features)的归一化窗口 steps: 要预测几步 score_idx: 目标变量在特征矩阵中的列索引 """ preds = [] current = last_window.copy() for _ in range(steps): p = model.predict(current, verbose=0).reshape(-1) preds.append(p[0]) # 构造下一个时间步:保留最新特征,只替换score列 next_step = current[0][-1].copy() next_step[score_idx] = p[0] # 窗口左移一步,拼入新步 current = np.concatenate( [current[:, 1:, :], next_step.reshape(1, 1, -1)], axis=1 ) return np.array(preds)

注意,study_hours、absence这些特征在递归时没有真实未来值,只能沿用最近一次观测值,这个假设本身就带来了偏差。所以递归预测适合短步数(3步以内),同时需要把“特征保持最近值”的假设写进文档,否则汇报结果时会被问“你的学习时长未来数据哪来的”。

另一条路是多输出头:把最后一层Dense(1)改成Dense(n_ahead),一次预测未来多个步。这样避免了误差累积,模型要同时拟合多个目标,训练难度更大,但步数在5以内时效果通常比递归好。

最后说一个我自己的血泪经验。有一次我把预测目标y和特征里的score时间对齐搞错了,模型验证集MAE看起来不错,但实际预测就是把上一轮成绩原样抄了一遍。从那以后,我每次跑LSTM前都会强制走一遍自检:打印X_train.shape确认三维,随机抽一条样本核对时间对齐,检查归一化范围是否都在0到1之间,再确认验证集时间晚于训练集,四步全过了才开训。这个习惯帮我挡住了大多数“看起来正常但实际是复读机”的模型,希望你也能用得上。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询