尧图精选

LSTM气温预测实战:从数据预处理到多步滚动预测的完整指南

🕒 发布时间:2026/10/1 17:49:42 📁 来源:尧图网络
简介这份资源是一套基于LSTM的气温预测与可视化Python项目面向计算机、人工智能、通信工程等专业的在校学生与教师也适合作为毕设、课程设计或项目立项演示的参考案例。项目通过bs4从中国天气网爬取北京、上海、广州、郑州四城2011至2021年共3652条天气数据涵盖日期、天气等字段并完成数据预处理、模型训练与结果可视化全流程。压缩包共16个文件以8个py源码文件为核心辅以4个pyc编译文件、2个xlsx数据表与2个md说明文档整体约723KB结构紧凑、便于快速上手。目前已有241人学习关注。代码均经测试运行成功答辩评审平均分达96分读者可据此掌握LSTM时序建模、爬虫采集与可视化呈现的完整思路并在此基础上修改扩展实现更多预测功能。1. 从一份气温数据说起LSTM 预测到底能解决什么问题拿到的是一份逐日或逐小时的历史气温记录目标是预测未来若干天的温度走势并且把预测结果画成能直接看的曲线图。这件事听起来简单但真正动手做过的人都知道用线性回归或者移动平均去拟合气温序列短期还行一旦跨度拉长误差会迅速累积到没法看。原因在于气温序列同时包含趋势性、周期性和突发扰动传统方法很难同时抓住这三层结构。LSTM长短期记忆网络之所以在这个场景里被反复提起是因为它的门控机制天然适合处理这种「既有长期规律又有短期波动」的时间序列。遗忘门决定丢掉哪些旧信息输入门控制新信息写入输出门决定当前时刻暴露多少状态。这套结构让模型在几十甚至上百个时间步之后仍然能记住关键模式而不是像普通 RNN 那样梯度消失到什么都学不到。这份方案适合两类人一是手上有气温、能耗、销量等单变量或多变量时间序列数据想跑通一个完整预测流程的工程师二是正在学 LSTM 时间序列预测需要一个能直接复现、有数据有文档的最小项目来建立手感的人。下面从数据准备一路讲到训练、评估和可视化把每一步的参数和坑都摊开说。2. 数据准备与序列构造把原始气温表变成 LSTM 能吃的张量2.1 气温数据的清洗与缺失值处理原始气温数据最常见的三个问题是时间戳不连续、缺失值标记混乱、单位不统一。拿到数据后第一步不是急着建模而是先把时间轴对齐。假设数据是逐日的用 pandas 重建一个完整的日期索引把缺失的日期显式补出来这样后续做滑动窗口才不会错位。import pandas as pd import numpy as np # 读取原始数据假设两列date 和 temp df pd.read_csv(temperature.csv, parse_dates[date]) df df.sort_values(date).set_index(date) # 重建完整日期索引缺失日期补 NaN full_idx pd.date_range(df.index.min(), df.index.max(), freqD) df df.reindex(full_idx) # 缺失值处理先看缺失比例 missing_ratio df[temp].isna().mean() print(f缺失比例: {missing_ratio:.2%}) # 缺失少于 5% 用线性插值否则考虑前向填充加标记 if missing_ratio 0.05: df[temp] df[temp].interpolate(methodlinear) else: df[temp] df[temp].ffill().bfill() # 简单异常值处理超出物理合理范围的值置为 NaN 再插值 df.loc[(df[temp] -60) | (df[temp] 60), temp] np.nan df[temp] df[temp].interpolate(methodlinear)这段代码的逻辑是先把时间轴补全再根据缺失比例选择插值策略。参数上interpolate(methodlinear)适合连续几天的缺失ffill().bfill()适合缺失较多但变化平缓的场景。异常值阈值 -60 到 60 摄氏度是物理边界超过这个范围的基本可以判定为传感器故障或录入错误。注意不要用全局均值填充那会破坏时间序列的自相关性模型学到的就是假的平稳性。2.2 滑动窗口构造与归一化LSTM 的输入是固定长度的序列片段所以要把一维气温序列切成「输入窗口 → 预测目标」的样本对。窗口长度lookback和预测跨度horizon是两个核心参数。气温数据一般有较强的日周期和年周期如果数据是逐日的lookback 取 7 到 30 比较合理如果逐小时取 24 或 48 能覆盖一个完整日周期。from sklearn.preprocessing import MinMaxScaler def make_sequences(data, lookback14, horizon1): X, y [], [] for i in range(len(data) - lookback - horizon 1): X.append(data[i : i lookback]) y.append(data[i lookback : i lookback horizon]) return np.array(X), np.array(y) values df[temp].values.reshape(-1, 1) # 归一化必须只用训练集拟合避免数据泄漏 train_size int(len(values) * 0.8) scaler MinMaxScaler(feature_range(0, 1)) scaler.fit(values[:train_size]) values_scaled scaler.transform(values) lookback 14 horizon 1 X, y make_sequences(values_scaled, lookback, horizon) # 按时间顺序切分不能随机打乱 X_train, X_test X[:train_size - lookback], X[train_size - lookback:] y_train, y_test y[:train_size - lookback], y[train_size - lookback:] print(f训练集样本: {X_train.shape}, 测试集样本: {X_test.shape})这里有几个容易翻车的点。第一归一化的 scaler 只能用训练集拟合如果用全量数据拟合再切分测试集的极值信息就泄漏到训练过程里了评估结果会虚高。第二切分必须按时间顺序不能train_test_split(shuffleTrue)时间序列打乱后模型会「看到未来」。第三lookback 的选择要看数据的自相关函数气温数据一般 lag 7 和 lag 365 附近有峰值如果算力允许lookback 取 30 能覆盖月内模式。horizon 如果大于 1就变成多步预测误差会随步数累积建议先用 horizon1 跑通再扩展。2.3 用 PyTorch Dataset 封装批量加载数据量不大时可以直接用 numpy 数组喂给模型但养成用 Dataset 和 DataLoader 的习惯后续换大数据集或加多变量特征时不用重构。下面这个封装同时支持单变量和多变量输入。import torch from torch.utils.data import Dataset, DataLoader class TempDataset(Dataset): def __init__(self, X, y): # X: (N, lookback, features), y: (N, horizon) self.X torch.tensor(X, dtypetorch.float32) self.y torch.tensor(y, dtypetorch.float32) def __len__(self): return len(self.X) def __getitem__(self, idx): return self.X[idx], self.y[idx] batch_size 32 train_loader DataLoader(TempDataset(X_train, y_train), batch_sizebatch_size, shuffleTrue) test_loader DataLoader(TempDataset(X_test, y_test), batch_sizebatch_size, shuffleFalse)训练集可以 shuffle测试集绝对不能 shuffle否则评估时的时间顺序就乱了。batch_size 取 32 或 64 是常见起点数据量小于 1000 时可以用 16。如果显存吃紧把 batch_size 减半同时把学习率也适当调小不要只改一个。3. LSTM 模型搭建与训练层数、隐藏单元和早停怎么定3.1 模型结构设计与关键参数含义一个用于气温预测的 LSTM 模型核心参数就四个输入维度、隐藏单元数、层数、输出维度。输入维度等于特征数单变量就是 1输出维度等于预测跨度。隐藏单元数决定模型的记忆容量气温这种规律性强的序列32 到 128 通常够用再大容易过拟合。层数一般 1 到 2 层两层以上对小数据集收益很小。import torch.nn as nn class LSTMForecaster(nn.Module): def __init__(self, input_dim1, hidden_dim64, num_layers2, output_dim1, dropout0.2): super().__init__() self.hidden_dim hidden_dim self.num_layers num_layers self.lstm nn.LSTM( input_sizeinput_dim, hidden_sizehidden_dim, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0.0, ) self.fc nn.Linear(hidden_dim, output_dim) def forward(self, x): # x: (batch, lookback, input_dim) lstm_out, (h_n, c_n) self.lstm(x) # 取最后一个时间步的输出做预测 last_step lstm_out[:, -1, :] out self.fc(last_step) return outbatch_firstTrue让输入形状是 (batch, seq, feature)符合大多数人的直觉。dropout只在层数大于 1 时生效单层 LSTM 加 dropout 没有意义。取lstm_out[:, -1, :]是取序列最后一个时间步的隐藏状态也可以对所有时间步做注意力池化但气温预测里最后一步通常已经编码了足够信息。如果发现模型对近期变化不敏感可以改成取最后 3 步做平均。3.2 训练循环、损失函数与学习率调度气温预测是回归任务损失函数用 MSE 或 Huber。MSE 对异常值敏感如果数据里还有没清干净的尖峰Huber 更稳。优化器用 Adam学习率从 1e-3 起步配合 ReduceLROnPlateau 在验证损失不降时衰减。import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model LSTMForecaster(input_dim1, hidden_dim64, num_layers2, output_dim1).to(device) criterion nn.HuberLoss(delta1.0) optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-5) scheduler optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, factor0.5, patience5) epochs 100 best_loss float(inf) patience_counter 0 early_stop_patience 15 for epoch in range(epochs): model.train() train_loss 0.0 for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() # 梯度裁剪防止 LSTM 梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss loss.item() * xb.size(0) train_loss / len(train_loader.dataset) # 验证 model.eval() val_loss 0.0 with torch.no_grad(): for xb, yb in test_loader: xb, yb xb.to(device), yb.to(device) pred model(xb) val_loss criterion(pred, yb).item() * xb.size(0) val_loss / len(test_loader.dataset) scheduler.step(val_loss) if val_loss best_loss: best_loss val_loss torch.save(model.state_dict(), best_lstm.pt) patience_counter 0 else: patience_counter 1 if patience_counter early_stop_patience: print(fEarly stop at epoch {epoch}) break if epoch % 10 0: print(fEpoch {epoch}: train_loss{train_loss:.6f}, val_loss{val_loss:.6f})梯度裁剪clip_grad_norm_是 LSTM 训练的标配max_norm 取 1.0 是经验值取 5.0 也可以但不要不设。Huber 的 delta 参数控制对异常值的容忍度delta1.0 表示误差在 1 以内按 MSE 处理超过 1 按线性处理。早停的 patience 设 15 意味着验证损失连续 15 轮不改善就停这个值太小容易停早了太大浪费算力10 到 20 之间比较合理。学习率调度器的 patience 设 5比早停更早触发让模型在平台期先降学习率再决定是否停。3.3 多变量输入的扩展方式如果数据里除了气温还有湿度、气压、风速可以把 input_dim 改成特征数每个时间步输入一个向量。注意不同特征的量纲差异大归一化要逐列做不能整体做一个 scaler。from sklearn.preprocessing import MinMaxScaler features [temp, humidity, pressure] data_multi df[features].values scaler_multi MinMaxScaler() scaler_multi.fit(data_multi[:train_size]) data_multi_scaled scaler_multi.transform(data_multi) X_m, y_m make_sequences(data_multi_scaled, lookback14, horizon1) # 此时 X_m 形状为 (N, 14, 3)input_dim 改为 3 model_multi LSTMForecaster(input_dim3, hidden_dim64, num_layers2, output_dim1).to(device)多变量输入不一定比单变量好。如果加入的特征和目标相关性弱反而引入噪声。判断方法是先算各特征和目标之间的互信息或皮尔逊相关系数只保留相关性显著的。另外预测目标如果只是气温输出维度仍然是 1不要把所有特征都当输出。4. 预测结果反归一化与可视化把曲线画对才算跑通4.1 反归一化与评估指标计算模型输出的是归一化后的值必须用训练时拟合的 scaler 反变换回原始量纲否则算出来的 MAE、RMSE 没有物理意义。model.load_state_dict(torch.load(best_lstm.pt)) model.eval() with torch.no_grad(): X_test_tensor torch.tensor(X_test, dtypetorch.float32).to(device) pred_scaled model(X_test_tensor).cpu().numpy() # 反归一化 pred scaler.inverse_transform(pred_scaled) true scaler.inverse_transform(y_test) from sklearn.metrics import mean_absolute_error, mean_squared_error mae mean_absolute_error(true, pred) rmse np.sqrt(mean_squared_error(true, pred)) mape np.mean(np.abs((true - pred) / true)) * 100 print(fMAE: {mae:.2f} °C, RMSE: {rmse:.2f} °C, MAPE: {mape:.2f}%)MAE 反映平均绝对偏差RMSE 对大误差更敏感MAPE 是百分比误差。气温预测里 MAE 在 1 到 2 摄氏度以内算不错超过 3 就要检查数据质量或模型结构。注意 MAPE 在真实值接近 0 时会爆炸如果气温数据里有接近 0 度的样本MAPE 参考价值有限以 MAE 和 RMSE 为主。4.2 用 Matplotlib 画出预测对比曲线可视化不只是画一条线要把真实值、预测值、误差区间都呈现出来才能判断模型在哪些时段表现差。import matplotlib.pyplot as plt import matplotlib.dates as mdates # 取测试集对应的时间索引 test_dates df.index[train_size : train_size len(true)] fig, axes plt.subplots(2, 1, figsize(14, 8), sharexTrue) # 上图真实 vs 预测 axes[0].plot(test_dates, true, label真实气温, color#2c3e50, linewidth1.5) axes[0].plot(test_dates, pred, labelLSTM 预测, color#e74c3c, linewidth1.5, linestyle--) axes[0].set_ylabel(气温 (°C)) axes[0].legend() axes[0].set_title(LSTM 气温预测对比) # 下图逐点误差 error true.flatten() - pred.flatten() axes[1].bar(test_dates, error, colornp.where(error 0, #3498db, #e67e22), width0.8) axes[1].axhline(0, colorblack, linewidth0.8) axes[1].set_ylabel(误差 (°C)) axes[1].set_xlabel(日期) axes[1].xaxis.set_major_formatter(mdates.DateFormatter(%Y-%m)) axes[1].xaxis.set_major_locator(mdates.MonthLocator(interval2)) plt.xticks(rotation45) plt.tight_layout() plt.savefig(lstm_temperature_prediction.png, dpi150) plt.show()上图用实线和虚线对比真实与预测下图用柱状图展示逐点误差蓝色表示预测偏低橙色表示预测偏高。这种布局能一眼看出模型在季节切换点是否滞后。如果误差柱在温度骤升骤降的日期集中出现大值说明 lookback 窗口没覆盖到足够的前兆信息可以尝试加大 lookback 或加入气压特征。保存图片时 dpi 设 150 以上论文或报告里用 300。4.3 多步预测的滚动可视化如果要做未来 7 天预测有两种方式直接多输出和滚动单步。直接多输出是把 output_dim 改成 7一次预测 7 个值滚动单步是每次预测 1 个值把它拼回输入序列再预测下一个。前者误差会累积但实现简单后者每步都用真实值修正适合评估模型短期能力。def rolling_forecast(model, initial_seq, steps, scaler, device): model.eval() seq initial_seq.copy() # (1, lookback, 1) preds [] with torch.no_grad(): for _ in range(steps): x torch.tensor(seq, dtypetorch.float32).to(device) next_val model(x).cpu().numpy() # (1, 1) preds.append(next_val[0, 0]) # 把预测值拼到序列末尾去掉最早的一个 seq np.concatenate([seq[:, 1:, :], next_val.reshape(1, 1, 1)], axis1) preds scaler.inverse_transform(np.array(preds).reshape(-1, 1)) return preds.flatten() # 用测试集最后 lookback 个点作为起点 initial X_test[-1:] # (1, lookback, 1) future_pred rolling_forecast(model, initial, steps7, scalerscaler, devicedevice) print(未来 7 天预测:, future_pred)滚动预测的关键是每次把预测值拼回输入序列这要求输入序列的形状始终保持 (1, lookback, 1)。seq[:, 1:, :]去掉最早的时间步next_val.reshape(1, 1, 1)把新预测值放到末尾。注意滚动预测的误差会随步数放大7 天之后的预测只能看趋势不能看绝对值。如果发现第 3 天开始预测曲线变成一条直线说明模型在自回归过程中丢失了动态信息这时候应该改用直接多输出模式。5. 避坑与排查气温预测项目里最容易翻车的 5 个地方5.1 损失降到很低但预测曲线是条直线现象训练 loss 持续下降验证 loss 也很低但画出来的预测曲线几乎是一条水平线完全没有波动。原因通常是归一化方式不对或者模型学到了均值。如果用了 MinMaxScaler 但数据里存在极端异常值归一化后大部分值挤在很窄的区间里模型输出均值就能获得不低的 loss 表现。解决方法是先做异常值检测把超出 3 倍标准差的点处理掉再归一化或者改用 StandardScaler 让分布更均匀。另外检查一下forward里是不是误用了h_n而不是lstm_out[:, -1, :]h_n是最后一层的隐藏状态形状和用法都不同。5.2 测试集指标好得离谱上线后完全不能用现象测试集 MAE 只有 0.3 度换一段新数据预测误差超过 5 度。原因几乎可以确定是数据泄漏。常见泄漏点有三个归一化 scaler 用全量数据拟合、切分时 shuffle 了、构造滑动窗口时先切分再滑窗导致训练集和测试集有重叠。正确的顺序是先按时间切分原始序列再分别构造窗口或者先构造窗口再按时间顺序切分但切分点要减去 lookback 避免重叠。检查方法是打印训练集和测试集的时间范围确认没有交集。5.3 训练过程中 loss 变成 NaN现象前几个 epoch 正常突然 loss 变成 nan之后再也降不下来。LSTM 的梯度爆炸是常见原因尤其是学习率设得偏大或者序列较长时。解决方法是加梯度裁剪clip_grad_norm_(model.parameters(), max_norm1.0)放在loss.backward()之后、optimizer.step()之前。如果已经出现 NaN把学习率降到 1e-4 再试。另外检查数据里有没有 inf 或 nan归一化之前用np.isfinite过滤一遍。5.4 预测结果整体滞后一个相位现象预测曲线形状和真实曲线很像但总是慢半拍温度上升时预测还在低位温度下降时预测还在高位。这是单步预测的固有滞后因为模型倾向于用上一个时刻的值来估计当前值。缓解方法是把 lookback 加大让模型看到更长的历史或者在输入里加入差分特征也就是当前值减去前一个值让模型学变化量而不是绝对值。如果滞后仍然明显考虑换成 Seq2Seq 结构或者加入注意力机制。5.5 多变量输入后效果反而变差现象加入湿度、气压后测试集 MAE 比单变量还高。原因通常是特征量纲差异大且没有逐列归一化或者引入了和目标无关的噪声特征。解决方法是先算每个特征和目标之间的互信息只保留排名前 50% 的特征归一化必须逐列做用MinMaxScaler对每一列单独拟合如果特征之间高度共线用 PCA 降维后再输入。不要因为「多一个特征总没坏处」就无脑加LSTM 对无关特征很敏感。6. 把单步预测改成多步一个能直接套用的滚动预测技巧单步预测跑通之后下一步自然是做未来多天预测。直接多输出和滚动单步各有适用场景但很多人卡在滚动预测的序列拼接上。这里给一个封装好的滚动预测函数同时支持单变量和多变量并且把反归一化集成进去避免每次手动处理形状。def multi_step_forecast(model, history, steps, scaler, device, feature_dim1): history: (lookback, feature_dim) 的 numpy 数组未归一化 steps: 预测步数 scaler: 训练时拟合的 scaler feature_dim: 特征数单变量为 1 返回: (steps,) 的预测值已反归一化 model.eval() # 先归一化历史序列 history_scaled scaler.transform(history.reshape(-1, feature_dim)) seq history_scaled.reshape(1, -1, feature_dim) # (1, lookback, feature_dim) preds_scaled [] with torch.no_grad(): for _ in range(steps): x torch.tensor(seq, dtypetorch.float32).to(device) next_val model(x).cpu().numpy() # (1, output_dim) preds_scaled.append(next_val[0]) # 只把预测的目标维度拼回去其他特征保持不变或用预测值 next_step next_val.reshape(1, 1, feature_dim) seq np.concatenate([seq[:, 1:, :], next_step], axis1) preds_scaled np.array(preds_scaled) # (steps, output_dim) preds scaler.inverse_transform(preds_scaled) return preds.flatten() # 使用示例取最后 14 天真实数据作为历史 lookback 14 history df[temp].values[-lookback:].reshape(-1, 1) future_7 multi_step_forecast(model, history, steps7, scalerscaler, devicedevice) print(未来 7 天气温预测:, np.round(future_7, 1))这个函数的关键点在于seq np.concatenate([seq[:, 1:, :], next_step], axis1)这一行它实现了滑动窗口的滚动更新。seq[:, 1:, :]去掉最早的时间步next_step把新预测值拼到末尾窗口长度始终保持 lookback。多变量场景下如果其他特征也需要预测可以把next_step换成完整的多维预测值如果其他特征有真实值可用也可以只更新目标维度其他维度保持最后已知值。参数上steps不要设太大气温预测里 7 到 14 天是合理范围超过 14 天误差会大到没有参考价值。history的长度必须等于训练时的 lookback否则模型输入形状对不上。如果发现预测到第 5 天之后曲线变平说明模型在自回归过程中把不确定性放大了这时候可以改用直接多输出模型一次预测所有步虽然训练难度高一些但不会累积自回归误差。验证多步预测效果时不要只看最后一天的误差要把每一步的误差都画出来。我一般会画一张「预测步数 vs MAE」的折线图如果第 1 步 MAE 是 1 度第 7 步涨到 4 度这个衰减速度是可以接受的如果第 3 步就超过 5 度说明模型对中长期依赖的建模能力不够需要加层数或换更复杂的结构。最后说一个我踩过的坑滚动预测时如果忘了把预测值反归一化就拼回序列模型会收到量纲完全错误的输入后面几步的预测会直接崩掉。这个 bug 很隐蔽因为程序不会报错只是结果离谱。养成习惯在拼接之前确认next_val是归一化空间的值反归一化只在最后输出时做一次。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →