多变量LSTM温度预测实践:从数据预处理到模型调参全指南
2026/9/16 5:56:13 网站建设 项目流程

先说说我为什么写这篇。网上搜“LSTM温度预测”,能出来的教程大半是跑通一个demo就收工,loss曲线画得漂亮,但没人告诉你数据预处理埋了多少雷、模型训练到第几个epoch开始过拟合、为什么预测曲线总是比真实曲线慢半拍。我做这个多变量温度预测项目前后折腾了两周多,从入门到入坑,再把坑一个个填平,今天把这套完整链路连同踩坑记录一起放出来。这篇不是教你背API,是带你走一遍从原始气象数据到能用的温度预测模型的全过程,适合刚学完Python基础、想用深度学习做时序预测的读者。

1. 为什么温度预测最终选了LSTM而不是ARIMA

1.1 温度序列的本质决定了模型的边界

先说清楚一个底层认知:温度数据不是一堆随机波动的数字,它有明显的周期性、趋势性和非线性特征。每天的昼夜温差、季节性的冷暖交替、寒潮过境时的突变,这些因素叠在一起,构成了一个典型的非线性非平稳时序。

如果你直接用ARIMA这类线性模型去拟合,会发现残差里还留着大量天气突变带来的结构信息,模型怎么调都不对劲。ARIMA本质是自回归加滑动平均,它对线性关系刻画很强,但面对“前天温度突变导致今天湿度剧变,进而影响温度”这种多变量耦合关系,线性模型天生表达力不足。

LSTM能记住长时间范围内的依赖关系,这是它在时序任务里最大的价值。温度预测有个特点:当前时刻的温度不仅仅取决于前一小时,它还受到过去24小时甚至一周的天气过程影响。寒潮不是突然出现的,它有一个酝酿过程,这个过程在数据上表现为气压持续下降、风速逐步增大、湿度缓慢抬升。普通循环神经网络(RNN)在反向传播时梯度会指数级衰减,长序列前面的信息传不到后面。LSTM用门控机制把这条“信息高速公路”修通了。

1.2 多变量特征的加入让问题从一个序列变成了一个窗口

单变量预测就是把温度这一个序列喂进去,让模型自己找规律。但温度本身是其他气象因素共同作用的结果,风速带走热量、湿度影响体感温度和辐射吸收、气压变化预示天气系统移动方向。把这些变量一起喂给模型,模型才有机会学到变量之间的交互效应。

我在项目里选了四个特征:温度、相对湿度、海平面气压、风速。选这四个变量不是拍脑袋,是参考了气象常识再结合数据相关性做的权衡。输入维度是四维,输出是一维温度,这就是“多变量输入、单步预测”的基本设定。

实际编码的时候,要把原始的一列数据改造成“滑动窗口样本”:每个样本由过去N小时的四个变量组成,标签是未来一小时的温度。这一步是时序预测里最容易被新手忽略的环节,也是后面所有问题的基础。

2. 数据预处理:样本构造与三条铁律

2.1 特征选择与缺失值处理

我先说明一下项目数据来源:某气象站的小时级观测记录,时间跨度一整年,共约8760条记录。包含date、temp、humidity、pressure、wind_speed五个字段。数据下载下来一看,果然有缺失值,大概是设备维护和网络传输丢包导致的。

import pandas as pd import numpy as np df = pd.read_csv('weather_data.csv', parse_dates=['date'], index_col='date') print(df.isnull().sum())

缺失值处理我没用什么高级算法,直接采用相邻时段填充。气象数据的变化是渐进的,某小时缺失的温度和前后几小时比,差异不会很大,用前向填充加后向填充的组合方式就够了。需要注意别把填充顺序搞反,先ffill再bfill,避免序列开头和结尾留空洞。

特征这块有个细节值得说:不是变量越多越好。我开始把十来个气象变量全塞进去,结果模型训练慢、泛化差,还出现了特征之间的多重共线性问题。后来用相关性分析筛掉部分冗余特征,只保留跟温度相关性较高且彼此独立的那几个,效果立刻改善了。

2.2 归一化和滑动窗口的自相关分析

LSTM默认激活函数是tanh,它对输入尺度非常敏感。如果输入特征是原始温度值(比如32.5)和原始风速值(比如3.8),数值范围差异太大,梯度更新会被大数值特征主导,小数值特征根本学不到东西。所以归一化是硬性要求。

我选用MinMaxScaler而不是StandardScaler,原因在于LSTM的输出层经过tanh或sigmoid,数据映射到0~1或-1~1之间最有利于激活函数的敏感区间。MinMaxScaler正好能把数据压到0~1区间,和tanh的输出范围天然匹配。

from sklearn.preprocessing import MinMaxScaler feature_cols = ['temp', 'humidity', 'pressure', 'wind_speed'] scaler = MinMaxScaler() scaled_data = scaler.fit_transform(df[feature_cols])

滑动窗口的长度,我用自相关函数(ACF)辅助判断。计算温度序列的自相关系数,发现滞后48小时后相关性依然显著,48小时之后的衰减速度明显加快。所以窗口长度定为48,也就是用过去两天的气象数据预测未来一小时温度。这个长度不是试出来的,是有统计依据的。

from statsmodels.graphics.tsaplots import plot_acf plot_acf(df['temp'], lags=72)

2.3 时序切分:保持顺序比随机打散更重要

训练集、验证集、测试集的划分是踩坑重灾区。

新手常见的错误做法是用train_test_split函数,默认shuffle=True把数据打乱随机划分。这在分类问题上没问题,但在时序预测里是致命的。打乱之后,训练集里出现了“未来的数据”,模型提前偷看了答案,验证集分数虚高,但一到真实场景就原形毕露。

正确做法是直接按时间顺序截断,比如前70%做训练、中间15%做验证、最后15%做测试。这样的划分才能真实模拟“用过去预测未来”的场景。另外有个更隐蔽的规则:归一化的scaler只能用训练集fit,然后拿同一个scaler去transform验证集和测试集。如果对全部数据先做了fit_transform再划分,测试集的信息悄悄混进了训练过程,这就是数据泄漏,会让评估结果失真。

我把错误代码和正确代码都贴出来给各位对比一下:

错误示范:

# 错误:先对全量数据归一化,再划分 scaled_all = scaler.fit_transform(df[feature_cols]) train_data = scaled_all[:7000] test_data = scaled_all[7000:]

正确示范:

# 正确:先划分,再用训练集的scaler变换测试集 train_df = df.iloc[:7000] test_df = df.iloc[7000:] scaler.fit(train_df[feature_cols]) train_scaled = scaler.transform(train_df[feature_cols]) test_scaled = scaler.transform(test_df[feature_cols])

这样保证测试集数据在归一化过程中不携带训练集统计信息之外的任何偏差。

滑动窗口的样本构造代码:

def create_sequences(data, window_size=48): X, y = [], [] for i in range(window_size, len(data)): X.append(data[i-window_size:i, :]) y.append(data[i, 0]) # 预测目标是温度,即第一列 return np.array(X), np.array(y) X, y = create_sequences(train_scaled, 48) print(X.shape, y.shape) # 大约是 (6952, 48, 4) (6952,)

3. LSTM网络结构设计与PyTorch实现

3.1 门控机制用大白话讲清楚

很多教材讲LSTM都在堆公式,什么遗忘门输入门输出门、细胞状态更新,看得人头皮发麻。我用一个简单的类比来理解它。

想象你管理一个仓库,每隔一小时有一批新货(当前输入)送进来,而你可能还惦记着三天前某批货(长期记忆)。

遗忘门决定的是“仓库清掉多少旧货”,它看当天的天气情况决定要不要把前几天的记忆清掉一部分。输入门决定“新到的货要存多少进去”,有些信息可能只是噪音,没必要全存。输出门决定“今天从仓库里拿多少出来影响你当下的判断”。

三个门组合起来,LSTM就拥有了一种能力:知道什么该记、什么该忘、什么时候输出。温度预测里最典型的就是,模型会学会记住前一天的冷空气趋势,同时忘掉白天的短时波动,这是普通RNN做不到的。

3.2 模型结构设计与代码

模型结构上我用了两层LSTM加一层全连接。第一层LSTM返回完整序列给第二层,第二层只取最后的隐藏状态,再通过全连接层输出预测温度。隐藏单元数选了64,这个值参考了序列长度(48)和特征数量(4),太小欠拟合,太大容易过拟合且训练慢。

import torch import torch.nn as nn class TemperatureLSTM(nn.Module): def __init__(self, input_size=4, hidden_size=64, num_layers=2, output_size=1): super(TemperatureLSTM, self).__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=0.2 ) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): # x: (batch, seq_len, input_size) out, _ = self.lstm(x) # 取最后一个时间步的输出 out = out[:, -1, :] out = self.fc(out) return out model = TemperatureLSTM()

有个容易搞错的地方是batch_first参数。PyTorch的LSTM默认输入维度是(seq_len, batch, input_size),用batch_first=True可以让你直接传(batch, seq_len, input_size)的数据,省去维度转置的麻烦。

训练循环里还需要注意初始隐藏状态的处理。虽然模型在forward里忽略了返回的hidden_state,但PyTorch的nn.LSTM在每次前向传播时会自动初始化为零,对独立样本训练是可以的。如果你想做状态保持的流式预测,那就得手动把hidden_state传进去。但在这里,我选择每次训练前重置状态,简单可靠。

3.3 训练配置:损失函数、优化器与学习率

损失函数用的是MSELoss,因为温度预测本质是回归问题,均方误差对大偏差更敏感,能迫使模型尽量减小明显偏离的预测。如果用MAE的话,大误差和小误差被同等对待,模型会更保守。

优化器选了Adam而不是SGD,Adam自适应调整每个参数的学习率,对新手来说容错率更高,不需要手动调整动量参数。学习率设为0.001,这是深度学习训练里一个不算捷径的通用经验值,太大loss震荡,太小收敛缓慢。

criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) batch_size = 64 epochs = 80

一个我反复调试时发现的关键点:数据集的shuffle在训练时可以做,但只限于训练集内部,验证集和测试集绝不能shuffle。我在一个早期版本里把全部数据打包成一个DataLoader,shuffle=True,导致验证结果好得离谱,换到测试集上立刻崩掉。验证的作用是模拟测试,如果验证时看了未来数据,验证就失去了意义。

完整训练循环:

from torch.utils.data import DataLoader, TensorDataset X_train_tensor = torch.FloatTensor(X) y_train_tensor = torch.FloatTensor(y).view(-1, 1) train_dataset = TensorDataset(X_train_tensor, y_train_tensor) train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) for epoch in range(epochs): model.train() total_loss = 0 for batch_X, batch_y in train_loader: optimizer.zero_grad() outputs = model(batch_X) loss = criterion(outputs, batch_y) loss.backward() optimizer.step() total_loss += loss.item() if (epoch + 1) % 10 == 0: print(f'Epoch {epoch+1}, Loss: {total_loss/len(train_loader):.6f}')

4. 训练和调参遇见的几个深坑

4.1 loss不降的排查链路

我第一次训练时,epoch跑了20轮loss纹丝不动,一直徘徊在0.09左右,看曲线像一条直线。当时第一反应是学习率太小,把学习率调大了一个数量级,结果loss直接发散到上百,彻底懵了。

排查问题得有个先后次序。

第一步检查输入数据有没有NaN或inf,一个NaN混进去,梯度更新直接毁掉整个模型。第二步检查归一化是否有问题,如果用的数据范围不对或者漏了某列,模型可能学到了错误尺度。第三步看学习率,0.001不降就试0.005,再不行就退回0.0005。第四步看网络结构输出维度对不对,LSTM输出层如果没有正确取最后一个时间步,模型学到的可能是中间过程的噪音。

最后发现我的问题出在一个很蠢的地方:数据加载时有个顺序问题,滑动窗口构造数据时把dataframe到numpy的转换写错了一次,导致输入特征矩阵的形状变成了(48, 4, 6952)而不是(6952, 48, 4),维度错位让模型完全学不到东西。

这个教训是:动手训练之前先用一个简单基线验证数据流是通的,比如构造一个batch走一遍前向传播,检查输出形状。

4.2 归一化泄漏:最隐蔽的数据错误

这个坑值得单独拉出来说。我在给验证集和测试集做归一化时,起初偷懒写成:

scaler.fit_transform(X_test)

后来发现测试集上的误差小得不正常,仔细检查才发现问题:用测试集自己的min和max做归一化,相当于让模型知道了测试集的取值范围。这就像考试前拿到了考试范围,测试分数当然好看,但一到未知数据就现原形。

正确的做法是把scaler先fit在训练集上,然后transform测试集:

scaler.fit(train_df[feature_cols]) train_scaled = scaler.transform(train_df[feature_cols]) test_scaled = scaler.transform(test_df[feature_cols])

这样测试数据的归一化用的训练集的min和max,才是一个合理的、未知数据下的数据流。

4.3 预测滞后:为什么曲线总是慢半拍

训练收敛后,我画出了测试集上的预测值与真实值对比曲线,第一眼看上去还行,但仔细一看发现模型预测的曲线走势比真实数据慢了将近一小时。峰值总比真实峰值晚到,谷底总比真实谷底晚到,这就是典型的时序预测滞后问题。

滞后现象的本质是模型学会了“近似复制最近时刻的值”。对于温度这种连续变化的物理量,前一天的温度和后一天的温度差距很小,模型通过最小化MSE,发现最简单的策略就是输出接近上一个时刻的值,而不是真正学习温度的动态变化规律。MSE越小,滞后往往越严重,因为“复制上一个值”在这种损失函数下收益很高。

要缓解这个问题,可以换一种视角来看待。如果只预测单步(下一小时),轻微滞后还在可接受范围。想彻底改善,需要引入更长期的预测目标,或者修改损失函数,让预测曲线和真实曲线的趋势变化也参与梯度计算。我在这个项目里选择接受单步预测的轻微滞后,因为在多步迭代预测里滞后误差不放大才是真正要关注的问题。

4.4 过拟合与早停的博弈

两层的LSTM参数量并不少,在近7000个训练样本上,到第60个epoch左右会出现过拟合信号:训练loss还在下降,验证loss开始反弹。这时候如果继续训练,模型学到的全是训练集上的气象模式细节,一上测试集就露馅。

我加了一个简单的早停机制:每训练一个epoch记录验证集loss,连续8个epoch验证loss没有变好就停止训练,并保存历史最好的模型参数。

best_val_loss = float('inf') patience = 0 for epoch in range(epochs): # ... 训练步骤 ... model.eval() with torch.no_grad(): val_pred = model(X_val_tensor) val_loss = criterion(val_pred, y_val_tensor).item() if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), 'best_model.pt') patience = 0 else: patience += 1 if patience >= 8: break

这比单纯固定epoch次数训练要科学得多。每个数据集过拟合的速度不一样,早停让训练时长自适应地匹配数据特性。

5. 试验结果评估:不只看loss曲线

5.1 回归指标的选择逻辑

训练结束后,我把结果从归一化空间还原回真实温度值再做评估。记住,绝对不能在归一化后的数据上算MAE和RMSE,那样得出的数字没有物理意义。

# 反归一化注意:要还原温度的预测值,只需要用scaler处理第一列 pred_temp = scaler.inverse_transform( np.concatenate([pred, np.zeros((len(pred), 3))], axis=1) )[:, 0] true_temp = scaler.inverse_transform( np.concatenate([y_true, np.zeros((len(y_true), 3))], axis=1) )[:, 0]

这里inverse_transform需要完整的四维输入才能还原,所以要拼上三个零列再来反变换。这是我调试时踩的另一个坑,第一次试图直接对一维数组做inverse_transform,怎么都报shape错误。

最终指标算出来如下:

指标数值
MAE1.35°C
RMSE1.82°C
0.94

MAE只有1.35度,整年平均温度大概在20度上下,这个误差范围是可以接受的,做一个温度和舒适度提示类应用绰绰有余。RMSE比MAE大0.47度,说明存在部分时间段预测误差偏大,主要是天气突变时段,比如冷暖空气交接引起的陡升陡降。

5.2 残差分析:模型失效的时段长什么样

残差=真实值-预测值,如果把残差按时间画出来,会发现明显的聚集效应。冷锋过境时,模型连续几个小时预测偏高;暖平流影响时,连续几个小时预测偏低。这是气象数据特有的持续性误差。

我还把残差绝对值大于2.5度的样本挑出来做分析,发现这些样本大多集中在从晴天转雨天的过渡期。这说明模型的训练数据里,气象突变的样本不足以让模型学会“逆转变”,这属于数据分布问题,不是模型问题。想改善需要加入更长时间跨度的数据或者额外引入天气现象特征(比如降水、云量)。

5.3 可视化:三条曲线的对比与解读

画图时我习惯同时画真实曲线、预测曲线和残差曲线,三层信息叠在一起看,比单独看数值更有感知。整体趋势拟合得不错,模型学到了白天高夜间低的昼夜节律,也学到了持续两三天的高温过程和冷空气过程。主要偏差出现在峰值处,LSTM对极值的刻画普遍偏保守,真实温度冲到32度时,模型通常预测到30.5度左右。这是用MSE训练回归模型的通病:模型为了把整体误差压小,会主动选择“不求有功,但求无过”的策略,对极端值的预测偏差自然偏大。

6. 后续优化方向与最终体会

6.1 从基线出发,别一上来就堆模型

我现在做时序预测项目,已经养成一个习惯:先跑一个简单的线性回归或者SVR做基线,把基线误差记下来,再上LSTM。如果LSTM连基线都打不过,说明问题不在模型复杂度上,而在数据处理或者特征设计上。很多团队用深度模型预测效果差,不是深度学习不行,是前面的环节就错了。

6.2 多变量的取舍靠证据不靠直觉

我在项目里用相关性矩阵辅助选变量,但要注意一种情况:A变量和温度在数据上有很强的相关性,不代表它对预测有帮助。比如白天温度和太阳辐射强度高度相关,但如果数据里没有太阳辐射这一列,用湿度、气压间接建模,效果就不一定好。真正判断一个特征是否有用,最靠谱的方式还是做消融实验:加进去试一轮,看验证loss有没有下降。

6.3 后续可以怎么扩展

这个预测模型是单步预测,也就是预测未来一小时。接下去如果你想让预言未来24小时,又上迭代预测,误差会指数级累积,到最后几小时误差大到没法看。从单步到多步预测之间的门道,比如分步迭代、输出24个值的seq2seq结构、或者引入注意力机制,每一步都有数不清的坑。如果读者真的有这个需求,我会专门写一篇来细聊。

回到这个项目的初衷,我做温度预测本来是想验证LSTM在真实物理世界数据上的上限,做完之后最大的体会是:深度学习模型的性能天花板,很大程度上在进入模型之前就已经决定了。数据预处理、特征选择、时序窗口构造,这些都是决定成败的地基,模型结构反而是脚手架。1.35度的平均误差对于天气温度本身来说,已经能说明这套多变量LSTM方案是站得住脚的。要是你也在做类似的气象时序预测,可以从这个链路出发,拿你手头的数据跑一遍,再回来对比一下误差量级。

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

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

立即咨询