LSTM多变量时间序列预测实战:从数据预处理到温度预测模型构建
2026/9/16 23:34:28 网站建设 项目流程

这是一个我踩了一整周坑才跑通的项目。做的时候我一度怀疑是自己的打开方式不对,后来把问题一个个拆开才发现,坑确实不少,但每个坑都能填。这篇东西我不想写成教科书,就按我实际折腾的顺序来,从环境准备到模型训练再到结果分析,把每一步的原理、代码和教训都摆出来,希望你少走点弯路。

先说这个项目到底在做什么。简单讲,就是给一堆历史气象数据(温度、湿度、气压、风速这些),用LSTM去预测未来某个时刻的温度。它属于典型的多变量时间序列预测:输入不止温度这一列,而是把多个相关的环境变量一起喂给模型,让模型自己学会它们之间的耦合关系。比起只用温度单变量去预测,多变量在天气这种强耦合场景里通常能拿到更稳定的结果。适合谁来参考?正在学Python、刚接触深度学习、打算用LSTM做时序预测但不知道从哪下手的同学,尤其是那种“装环境装半天、一跑就报错”的新手。

1. 整体思路拆解:为什么温度预测要选LSTM

1.1 温度预测本质上是时间序列问题

我们面对的是一串按时间排列的观测值:昨天、今天、明天,上一小时、这一小时、下一小时。温度数据天然带有时间顺序,相邻时刻的值之间存在强相关性——中午的气温大概率比凌晨高,寒潮来了温度会连着几天走低。这种“前后关联”的特征,让它天然适合用循环神经网络(RNN)系列模型来处理。

普通的全连接网络处理这类问题有个麻烦:它把每个输入当成独立的点,根本不知道“上一个时刻”和“下一个时刻”有什么联系。这就好比让一个只看过零散照片的人去猜电影剧情,信息严重不足。RNN之所以适合时序数据,是因为它有一个“记忆”机制,能把过去时刻的信息通过隐藏状态往后传。LSTM(Long Short-Term Memory)是RNN的改进版,专门解决了传统RNN在长序列上容易“忘事”的梯度消失问题。

LSTM内部有三个门:遗忘门决定丢掉多少旧信息,输入门决定写入多少新信息,输出门决定当前时刻输出什么。这套机制让信息可以在几十甚至上百个时间步里被有选择地保留。对温度预测来说,今天的温度可能受过去24小时甚至过去一周的天气过程影响,LSTM这种长程记忆能力刚好对路。

1.2 为什么不直接用ARIMA或传统机器学习

很多人第一反应是用ARIMA或者XGBoost来做。确实能跑,但各有各的局限:

  • ARIMA要求数据平稳,天气数据季节性强、受突发因素影响大,往往要做差分、做季节性分解,前处理一堆事,而且它本质还是线性模型,对多变量的非线性交互关系拟合能力有限。
  • XGBoost这类树模型可以做多变量,但它没有序列记忆。你把过去10天的数据展开成几百个特征喂给树模型,它能学,但特征工程工作量极大,而且很难学到“时间顺序”本身。
  • LSTM则直接把“时间”融进网络结构里,输入是三维张量(样本数、时间步长、特征数),模型在图结构上就天然感知顺序。这不代表LSTM在所有场景都碾压树模型,但在温度这种有强时序依赖、多变量耦合的预测任务里,LSTM确实是个很自然的选择。

1.3 完整流程先过一遍

我实际跑的流程分六步:

  1. 准备数据:获取包含温度、湿度、气压、风速等字段的历史气象数据。
  2. 数据清洗:处理缺失值、异常值,把时间列解析成索引。
  3. 特征构造与归一化:选择参与预测的特征列,统一缩放到0到1区间。
  4. 构建序列样本:用滑动窗口把原始数据切成“过去N小时预测未来M小时”的样本对。
  5. 搭建LSTM模型并训练:用Keras搭两层LSTM加全连接输出层,跑训练。
  6. 评估与可视化:用RMSE、MAE这些指标评估,把预测值和真实值画在一起对比。

整条链路里,真正决定模型上限的不是网络有多深,而是数据怎么处理、窗口怎么切、训练怎么防过拟合。后面我会逐个讲。

2. 环境准备与数据获取:装环境的坑比模型本身还多

2.1 Python环境安装:选对版本先赢一半

我用的是Python 3.9。为什么选3.9而不是最新的3.12?因为TensorFlow和Keras对太新的Python版本支持往往滞后,尤其是Windows下,经常出现“装上了但导入报错”的诡异问题。如果你不想折腾,直接用3.8~3.10都行。

装Python之后第一件事是换国内pip源,不然下载依赖包能让你怀疑人生。在用户目录下建一个pip.ini(Windows)或pip.conf(Linux/Mac),写上:

[global] index-url = https://pypi.tuna.tsinghua.edu.cn/simple trusted-host = pypi.tuna.tsinghua.edu.cn

我用的是清华源,实测下载速度能到几MB每秒,比默认源快了一个量级。装依赖库的时候,直接用pip install一次性装齐:

pip install numpy pandas matplotlib scikit-learn tensorflow

这里要提醒一句:如果你的电脑有NVIDIA显卡,想用GPU训练,那还需要额外装CUDA和cuDNN,步骤比较繁琐。我这个项目数据量不算大,CPU训练也就几分钟一个epoch,完全够用,没必要一上来就折腾GPU。

2.2 数据集怎么找:开源气象数据最省事

数据是项目的粮食。我不建议自己搭传感器采集,前期太浪费时间。我的做法是直接下载公开数据集,既省事又能保证数据质量。

可选的数据源有三个:

  • 全球公开气象站数据:各大气象机构都有历史观测数据开放下载,字段最全,包含气温、露点、气压、风速、云量等。
  • 国内公开数据集:也有不少高校和机构公开的站点气象数据,按城市/站点整理好的CSV,比较友好。
  • 自己造一个小规模数据来调试代码:先用模拟数据把流程跑通,再换真实数据。

我最后用的是某气象站逐小时观测数据,包含temperaturehumiditypressurewind_speedwind_dir几个字段,约两万条记录,跨度两年多。我特意选了多变量都能拿到的数据,因为项目叫“多变量温度预测”,单列温度就没意义了。

2.3 数据读取与初步探查:先看再动手

拿到CSV后,我第一件事不是急着建模,而是先看数据长什么样。

import pandas as pd df = pd.read_csv('weather_data.csv', parse_dates=['date'], index_col='date') print(df.head()) print(df.info()) print(df.describe())

这一步能发现几个关键问题:有没有空值、各列的数值范围差多大(温度可能是0到40,气压可能是980到1040)、时间索引是否连续。describe()输出的统计量尤其有用,可以用它来判断后续归一化的边界。我当时发现湿度列大概有0.3%的缺失值,而且时间索引中间跳过了一些小时,这两件事都得先处理,不然喂给模型的就是错位数据。

3. 数据处理:序列化与归一化是决定成败的关键

说实话,我一开始以为建模是最难的,跑完才发现数据处理才是真正的分水岭。很多论文里一笔带过的细节,实操时全是坑。

3.1 缺失值与异常值处理:不能直接扔掉

缺失值有两种处理方式:删除或者填充。删除会破坏时间连续性,如果缺的量不大、位置分散,倒也能接受;但如果像我这次一样缺了几十个小时,我倾向于用前后向填充法(fillna(method='ffill'))来补。温度、湿度这类气象变量短时间内变化不会太剧烈,前向填充不会引入太大误差。

异常值我用的是箱线图思路:把超出四分位距(IQR)3倍以上的点视为异常。比如7月下午温度突然跳到60度,这明显是传感器故障,我会把它替换成前后两小时的平均值。

3.2 归一化:LSTM的隐形刚需

LSTM的激活函数是tanh和sigmoid,它们的输出范围分别在(-1,1)和(0,1)。如果输入特征数值差距太大(比如气压1000、温度30、湿度0.8),模型训练时梯度会被大数值特征主导,小数值特征根本学不到东西。就好比一个班里有同学考100分也有同学考1分,平均分一下就被拉过去了。

我用的是sklearn.preprocessing.MinMaxScaler,把所有特征缩放到0到1之间。这里有个极其重要的细节:拟合scaler必须只用训练集的数据,不能把测试集的数据一起放进去fit。简单说,scaler.fit(train_data)之后再用scaler.transform(test_data),注意顺序。如果先对整个数据集fit,测试集信息就泄露到训练过程里了,虽然测试指标会“好看”,但那是假的,部署到真实场景时模型表现会立刻打回原形。

3.3 滑动窗口:怎么构造样本对

有了归一化数据,下一步是构造输入输出对。LSTM的输入是三维的:(样本数量, 时间步长, 特征数量)。时间步长就是“用过去多少小时的数据来预测”,特征数量就是参与预测的因素个数。

我设置的窗口大小是24小时,也就是用过去24个小时的数据,预测未来1个小时的温度。你可以根据业务需求改成“预测未来6小时”“未来24小时”,那就要改动输出层的设计。窗口大小的选择有讲究:太小,模型看不到足够的历史信息;太大,计算量上去了,信息增益可能反而不明显。对小时级气象数据,24或48是比较常见的起点。

def create_sequences(data, seq_length): X, y = [], [] for i in range(len(data) - seq_length): X.append(data[i:i + seq_length]) y.append(data[i + seq_length, 0]) # 预测目标是温度列(第0列) return np.array(X), np.array(y)

注意这里y取的是i + seq_length这一时刻的温度值,也就是“未来一小时后的温度”。如果你想让模型预测“未来6小时”,就把索引改成i + seq_length + 6,同时注意数据末尾要留出足够的长度。

3.4 训练集/验证集/测试集划分:时序数据不能随机打乱

这一点我见过无数人栽跟头。普通机器学习里,我们会用train_test_split随机划分数据,把样本打乱保证分布一致。但时间序列数据绝对不能随机打乱,否则模型就在偷看未来信息:训练集里混着后面的数据,测试集里混着前面的数据,指标再好看也没有实际意义。

正确做法是按时间顺序切分,比如前70%训练、中间15%验证、最后15%测试:

train_size = int(len(X) * 0.7) val_size = int(len(X) * 0.15) X_train, X_val, X_test = X[:train_size], X[train_size:train_size+val_size], X[train_size+val_size:] y_train, y_val, y_test = y[:train_size], y[train_size:train_size+val_size], y[train_size+val_size:]

这个“测试集最后切”的做法,模拟的是真实场景:我们用今天之前的所有数据训练,去预测未来的天气。

4. LSTM模型搭建与训练:结构、参数、训练细节

4.1 网络结构选型:几层LSTM、加不加Dropout

我的模型结构是两层LSTM加一个全连接层。第一层LSTM有64个单元,return_sequences=True表示把每个时刻的隐藏状态都传给下一层;第二层LSTM有32个单元,return_sequences=False表示只保留最后一个时刻的输出,这个输出就是整个序列的“总结”,再接一层全连接输出预测温度值。

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model = Sequential() model.add(LSTM(64, return_sequences=True, input_shape=(seq_length, n_features))) model.add(Dropout(0.2)) model.add(LSTM(32, return_sequences=False)) model.add(Dropout(0.2)) model.add(Dense(1)) model.compile(optimizer='adam', loss='mse')

为什么用两层而不是一层?一层LSTM能捕捉到一定的时间依赖,但在变量较多、关系较复杂时,堆叠两层可以让模型学到更高层的抽象特征。比如第一层可能学到“温度、湿度这些变量的短期变化模式”,第二层把这些模式组合成“天气过程的演变趋势”。当然不是越深越好,气象时序还没复杂到需要三四层的地步,层数过多反而容易过拟合。

Dropout的作用是随机让一部分神经元在训练时“失活”,减少神经元之间的协同依赖,防止模型死记硬背训练数据。我这里是0.2,如果发现过拟合严重可以调到0.3或0.4。

4.2 损失函数与优化器:MSE + Adam是默认答案

损失函数我用了均方误差(MSE),即预测值与真实值差的平方的平均值。它对大误差的惩罚更重,比较适合温度这种连续性数值的回归任务。如果项目里更关心“预测值偏了多少度”的可解释性,也可以用MAE(平均绝对误差),但训练时MSE通常收敛更稳。

优化器选了Adam,这是目前最省心的默认选择。它自适应调整学习率,对不同参数一视同仁地给一个合理的更新步长。学习率设为默认的0.001即可,要不是在训练过程中观察到loss剧烈震荡,我一般不手动调。

4.3 训练过程:epoch数、batch size与Early Stopping

from tensorflow.keras.callbacks import EarlyStopping early_stop = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True) history = model.fit( X_train, y_train, validation_data=(X_val, y_val), epochs=100, batch_size=32, callbacks=[early_stop], verbose=1 )

batch size设为32,意思是一批喂32个样本进模型,算一次梯度更新。这个值不是越大越好也不是越小越好:大了容易内存爆炸,小了梯度噪声大、收敛慢。32到64在大多数场景是个甜点区。

epochs我设了100,但真正跑满的情况很少。Early Stopping会在验证集loss连续10个epoch不下降时自动停止,并且恢复到验证集loss最小的那组权重。这能有效防止过拟合,也省时间。

训练过程中还需要监听一个信号:如果训练loss在下降而验证loss反而上升,那就是过拟合信号,说明模型开始“记答案”而不是“学规律”。出现这种情况,优先考虑减少层数或神经元数、增大Dropout、减少epoch数。

4.4 训练结果诊断:loss曲线怎么看

训练结束后,我会把训练集和验证集的loss曲线画出来:

plt.plot(history.history['loss'], label='train loss') plt.plot(history.history['val_loss'], label='val loss') plt.legend() plt.show()

一个健康的曲线是:两条线都下降,最终趋于平缓,而且两者之间的差距不大。如果train loss一路走低、val loss在某个点开始反弹,就是经典的过拟合。如果两条线都还很高、下降缓慢,可能是学习率太低或模型容量不够。这些判断虽然朴素,但在实际项目中非常管用。

5. 评估指标与可视化:预测得好不好,不能靠肉眼

5.1 RMSE、MAE、R²:三个指标的侧重点

模型训完,先别急着画图看效果,先把指标算出来。我常用三个指标:

  • RMSE(均方根误差):对误差平方后取平均再开根,单位和温度一致(摄氏度),对大误差敏感。
  • MAE(平均绝对误差):误差绝对值的平均,直观好懂,不受大误差的过度影响。
  • R²(决定系数):模型解释了数据的多少方差,越接近1越好。
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score y_pred = model.predict(X_test) y_pred_inv = scaler.inverse_transform(np.concatenate([y_pred, np.zeros((len(y_pred), n_features-1))], axis=1))[:, 0] y_test_inv = scaler.inverse_transform(np.concatenate([y_test.reshape(-1, 1), np.zeros((len(y_test), n_features-1))], axis=1))[:, 0] rmse = np.sqrt(mean_squared_error(y_test_inv, y_pred_inv)) mae = mean_absolute_error(y_test_inv, y_pred_inv) r2 = r2_score(y_test_inv, y_pred_inv)

这里有个细节要注意:模型输出的是归一化后的预测值,必须用之前fit好的scaler做逆变换,把结果还原成真实温度。上面代码里我用的inverse_transform是把预测值和其余特征(补零)拼在一起再还原,因为scaler是5个特征一起拟合的,逆变换也需要5个维度的输入。如果你懒,也可以单独对温度列fit一个一维scaler做逆变换,更省事。

我那次跑出来的结果是RMSE大约1.8度,MAE约1.3度,R²约0.94。对于小时级温度预测来说,这个误差水平已算不错。不过指标好不好要看场景:如果只是预测大概趋势,2度误差可以接受;如果要精确到空调系统控制的层面,1.8度就还得继续优化。

5.2 预测曲线可视化:注意“横坐标太密集”的老大难

画预测值vs真实值的曲线时,我踩过一个特别常见的问题:测试集如果有几千条数据,横坐标标签全挤成一团,什么也看不清。搜“python画图横坐标太密集”能看到一堆人问,解法其实很简单:用plt.xticks隔一段时间显示一个刻度,或者直接只画测试集的一部分。

import matplotlib.pyplot as plt plt.figure(figsize=(14, 5)) plt.plot(y_test_inv[:200], label='True', linewidth=1.5) plt.plot(y_pred_inv[:200], label='Pred', linewidth=1.5, linestyle='--') plt.xticks(rotation=45) plt.legend() plt.show()

我习惯只画前200个点,这样能看清细节;想看全局就把[:200]去掉。另外,rotation=45可以旋转刻度标签,比默认横排好看很多。如果想看误差分布,可以画误差直方图,直观看到预测值多数集中在哪些偏差范围。

5.3 效果分析:什么时候预测得好,什么时候系统误差大

从图上观察,有几个规律很有意思:

  • 平稳天气时段(连续几天温度波动小)预测误差很小,曲线几乎贴合。
  • 突变天气(比如冷空气来袭、短时间内温度骤降)误差明显变大,模型反应滞后。
  • 每天的最高温、最低温出现时刻附近,预测值容易偏低或偏高1到2度。

这背后是LSTM的特性:它本质上是学一个“大概率延续历史趋势”的函数,遇到未出现过的剧烈模式,外推能力天然有限。这不是bug,是所有统计模型共有的天花板。所以预测任务里有个心法:评估模型时要分场景讨论,不要总指望一个模型在所有天气状况下都完美。

6. 踩坑实录:从入门到入坑的十个典型问题

这章算是全篇最“值钱”的部分。我把整个项目里遇到过的、和网友交流时高频出现的问题整理成了一张速查表,再挑几个重点展开讲。

症状原因解决办法
训练loss不下降归一化没做或数据范围差异过大检查是否做了MinMaxScaler,检查学习率
val_loss先降后升过拟合增大Dropout、减少网络层数、加EarlyStopping
预测曲线整体滞后一天窗口太短或特征不足增大seq_length,尝试加入更多天气变量
测试集指标奇高数据泄露检查scaler是否只fit训练集,检查是否随机打乱了时序
输入维度报错输入shape不对打印X.shape确认是(samples, timesteps, features)
反向传播梯度爆炸学习率太高或网络太深调低学习率,或加梯度裁剪
训练极慢数据量大或batch过大减小batch_size,用GPU训练
LSTM输出全是同一个值模型欠拟合增加网络层数/神经元数,增加训练轮数
画图横坐标重复拥挤刻度标签太多plt.xticks隔N个点显示,或rotation=45
模型在真实新数据上失效训练数据分布与真实场景分布不一致重新采集训练数据或做增量学习

6.1 归一化顺序出错:数据泄露的经典案例

有个朋友拿着几乎一样的代码来找我,说他的测试集RMSE只有0.3度,简直完美。我让他把数据切分的代码发过来发现,他是先对整个数据集做了fit_transform,然后再切分。这样测试集的信息在训练时已经被模型间接“见过”了,所以测试指标才那么好看。这不是模型强,是数据泄了题。我建议他用严格按时间顺序切分、只用训练集fit一遍的方法重跑,他的RMSE立刻回到1.9度左右。这个数据泄露的问题,算是时间序列预测里最隐蔽也最影响判断的坑。

6.2 “预测曲线滞后”是怎么回事

另一个常见现象是:预测曲线看着和真实曲线形状几乎一样,但整体向右平移了一截,仿佛模型只是把昨天的温度搬到了今天。这是预测模型的“返祖现象”:当模型学不到有效规律时,它最稳妥的做法就是输出上一个时刻的观测值,因为温度相邻时刻确实高度相关,这样loss也不会太差。

解决滞后问题没有银弹,但有几个方向值得尝试:

  • 增大输入窗口长度,让模型看到更长的历史过程;
  • 加入更多有预测力的特征,比如气压变化率、露点温度;
  • 如果你预测的是未来多步,考虑用seq2seq结构(Encoder-Decoder),而不是一步到位。

6.3 训练时间长到怀疑人生怎么办

如果不加EarlyStopping,我那个模型100个epoch跑完大概要20多分钟,看起来不算太久,但每次调参都要重跑一遍,累积起来就非常熬人。优化的思路有三个:

  • 在代码开头设置随机种子,保证每次跑的结果可复现,调参才有意义;
  • 先把数据切成小批量,跑5个epoch快速验证代码逻辑,确认没问题再全量训练;
  • 用EarlyStopping,验证集loss不降就停,省掉大量无效训练时间。

6.4 随机种子:想复现结果就别省这一步

说到随机种子,这也是一个很容易被忽视的点。深度学习模型初始化权重、数据加载顺序都带随机性,两次跑的loss曲线可能完全不同。为了让实验可对比、可复现,我习惯在代码开头写:

import numpy as np import tensorflow as tf import random np.random.seed(42) tf.random.set_seed(42) random.seed(42)

这样至少保证在同一环境、同一数据下,每次训练的结果是确定的。想跟别人交流调参心得时,这一步非常关键,不然你说“我调了学习率效果变好了”,对方没法复现验证,就很难判断到底是学习率起了作用还是随机性造成的假象。

7. 后续还能怎么扩展

我做完这个项目之后,顺手试了几个方向的扩展,简单说一下思路:

  • 把“预测未来1小时”改成“预测未来24小时的温度曲线”,这需要把输出层改成24个神经元,并且训练标签也要改成未来24个时刻的温度序列。模型结构会变成序列到序列的预测,复杂度提升,但对实际场景更有用。
  • 尝试把LSTM和注意力机制结合。LSTM擅长捕捉顺序,但长序列里有些关键时间点(比如寒潮开始的那一刻)会淹没在大量普通时刻里。注意力机制可以让模型自动聚焦到这些关键点上。
  • 换Transformer。很多热词里都在提Transformer,它确实能建模长距离依赖,但在气象这种中小规模时序任务上不一定比LSTM好多少,而且数据量和训练成本要求更高。如果你时间充裕,可以两种都试,对比一下再下结论。

这些扩展方向都不是必须做的,但确实能帮你把LSTM吃得更透。根据我个人经验,做这类项目最忌“一口气吃成大胖子”,先把一条链路完整跑通,再考虑花式扩展,反而效率更高。你要是卡在某个环节,回头看看上面那十类常见问题,大多数情况都能对号入座。

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

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

立即咨询