
简介这份资源面向具备一定深度学习基础、希望用LSTM完成气温时间序列预测的学习者与开发者围绕未来气温预测这一典型回归任务提供可直接运行的代码与配套数据。压缩包共3个文件包含2个Python脚本和1个CSV数据文件整体约9KB其中脚本分别承担模型搭建与训练流程CSV则存放用于建模的气温序列数据便于快速复现实验。资源描述中详细讨论了MSE、MAE等回归评估指标并针对Keras回调机制展开说明monitor用于选择监控指标因采用5折交叉验证而未单设验证集故使用accmin_delta用于设定判定提升的阈值结合准确率在70%至90%间波动及训练抖动情况最终设为0.003%patience则用于在抖动与真实性能下降之间权衡设置过大会略低于模型最优准确率过小则易在前期抖动时提前停止。目前已有1433人学习适合想掌握LSTM回归预测、早停策略与评估指标细节的读者参考。1. 用 LSTM 做未来气温预测为什么你训完的模型总在第七天开始“胡言乱语”拿历史气温数据训练一个 LSTM滚动预测未来 7 到 30 天的日最高温这件事看起来门槛不高但真正动手的人大多会撞上同一堵墙前三天曲线贴合得让你以为成了第四天开始振幅变小第七天之后基本退化成一条平滑的均值线。这不是代码写错了而是气温序列本身的信噪比、LSTM 的误差累积机制、以及你对“多步预测”这件事的建模方式共同决定的。这篇笔记就围绕“使用 LSTM 实现未来气温的预测”这个目标把数据从哪来、窗口怎么切、模型怎么搭、多步预测怎么不崩、评估指标怎么看一步步拆开讲清楚。适合已经会写 PyTorch 或 Keras 基础代码、想把这个经典任务真正跑出可用结果的从业者也适合刚接触 lstm时间序列预测python、想找一个完整可复现案例的新手。下面所有代码和参数都是我实际跑过、翻过车之后沉淀下来的版本不是从文档里抄的示例。2. 气温预测的数据准备从原始 CSV 到 LSTM 能吃的三维张量2.1 气温序列和普通时间序列的区别在哪很多人拿 LSTM 做气温预测第一步就把它当成股票价格或者销量来对待这是后面一系列问题的根源。气温序列有三个必须尊重的特性。第一是强周期性日周期和年周期叠加同一天前后几年的值高度相关模型很容易学会“抄去年今天”而不是真正学趋势。第二是物理边界气温不会无限外推它有气候学上的上下限所以预测值超出历史极值太多一定是模型出了问题而不是发现了新规律。第三是空间相关性单站点的气温受周边站点影响如果你只用一个站点的数据遇到极端天气时模型几乎没有额外信息可参考。理解这三点之后你的预处理策略就会不一样。常见做法是先把日值聚合出来如果原始是小时数据再显式构造时间特征年内天数 sin/cos、月份、星期最后才做归一化。归一化我一般用训练集的最小最大值而不是全量数据否则验证集的信息会泄漏进训练过程评估结果会虚高。2.2 用 pandas 构造监督学习样本的完整脚本下面这段代码从原始 CSV 出发完成缺失值处理、时间特征构造、归一化和滑动窗口切分。假设你的 CSV 至少有两列date和temp_max。import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler # 1. 读取并排序 df pd.read_csv(weather.csv, parse_dates[date]) df df.sort_values(date).reset_index(dropTrue) # 2. 缺失值气温序列用时间插值比均值填充合理 df[temp_max] df[temp_max].interpolate(methodtime) df[temp_max] df[temp_max].ffill().bfill() # 3. 时间特征年内天数做 sin/cos避免 12 月 31 日和 1 月 1 日被当成远端 day_of_year df[date].dt.dayofyear df[sin_doy] np.sin(2 * np.pi * day_of_year / 365.25) df[cos_doy] np.cos(2 * np.pi * day_of_year / 365.25) # 4. 归一化只用训练集拟合 scaler split int(len(df) * 0.8) scaler MinMaxScaler() scaler.fit(df.loc[:split, [temp_max]]) df[temp_scaled] scaler.transform(df[[temp_max]]) # 5. 滑动窗口用过去 30 天预测未来 7 天 LOOKBACK 30 HORIZON 7 features [temp_scaled, sin_doy, cos_doy] data df[features].values 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, 0]) # 只预测温度 X np.array(X, dtypenp.float32) y np.array(y, dtypenp.float32) print(X.shape, y.shape) # (样本数, 30, 3) (样本数, 7)逻辑说明interpolate(methodtime)按时间间隔加权插值比简单前后填充更贴近气温的连续变化。sin_doy和cos_doy这一对特征解决的是“年末年初不连续”的问题如果你只用 dayofyear 这个整数模型会认为 365 和 1 相距很远实际上它们只差一天。归一化只在训练集上 fit这是防止数据泄漏的基本纪律。参数说明LOOKBACK30是我在日值气温上比较稳的选择短于 14 天模型学不到年周期长于 60 天训练样本数会明显减少且梯度更容易爆炸。HORIZON7对应一周预报如果你要做 30 天建议改成 seq2seq 结构而不是直接加长输出维度后面第 4 章会讲。features里只放了温度和两个时间特征如果你有气压、湿度、风速可以加进去但记得每个特征单独归一化。2.3 训练集验证集怎么切才不骗自己时间序列不能随机 shuffle 切分这是铁律。但很多人切完训练集和验证集之后忘了验证集和测试集之间也要留间隔。因为滑动窗口有 LOOKBACK 天的重叠如果验证集紧接着训练集最后几条训练样本的窗口会包含验证集开头的数据评估结果会偏乐观。我一般这样切训练集占前 70%验证集占接下来 15%测试集占最后 15%并且在训练集和验证集之间丢掉 LOOKBACK 天的样本作为缓冲。这个细节在论文里经常被忽略但在实际业务里如果你用虚高的验证指标去选模型上线后误差会明显变大。3. LSTM 模型搭建层数、隐藏单元和 dropout 到底怎么定3.1 为什么气温预测不需要堆很深的 LSTMLSTM 层数不是越多越好这在气温预测这种中低复杂度序列上尤其明显。我试过 1 层、2 层、3 层隐藏单元从 32 到 256 都跑过结论是2 层 LSTM 每层 64 个隐藏单元在大多数日值气温数据集上已经接近最优再往上加参数验证集 loss 反而上升训练时间翻倍。原因在于气温序列的长期依赖主要靠年周期这个确定性成分LSTM 不需要通过深层堆叠去捕捉很抽象的语义。相比之下机器翻译或者语音识别需要深层是因为要建模复杂的语言结构气温没有这个需求。所以我的建议是先用 1 层 64 单元跑一个 baseline如果欠拟合再加到 2 层不要一上来就 3 层 256 单元那样你调参的时间会远超收益。3.2 PyTorch 版本的 LSTM 回归模型代码下面是一个可以直接用的模型定义输入维度对应上一节的 3 个特征输出 7 天预测。import torch import torch.nn as nn class TempLSTM(nn.Module): def __init__(self, input_dim3, hidden_dim64, num_layers2, output_dim7, dropout0.2): super().__init__() 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.Sequential( nn.Linear(hidden_dim, 32), nn.ReLU(), nn.Linear(32, output_dim), ) def forward(self, x): # x: (batch, lookback, input_dim) out, (h_n, c_n) self.lstm(x) last out[:, -1, :] # 取最后一个时间步 return self.fc(last) # (batch, horizon)逻辑说明batch_firstTrue让输入形状是(batch, seq, feature)和上一节构造的 X 一致。取out[:, -1, :]表示用最后一个时间步的隐藏状态做预测这是多步直接预测的常见做法。dropout只在层数大于 1 时生效因为 PyTorch 的 LSTM 在单层时 dropout 参数会被忽略写了也是白写。参数说明hidden_dim64是起点如果你的数据量超过 10 年日值可以试 128。num_layers2配合dropout0.2是我验证下来比较稳的组合dropout 再高会欠拟合再低容易过拟合。output_dim7对应预测天数如果你改成 14 或 30记得同步改数据构造里的 HORIZON。3.3 训练循环里必须监控的两个量训练 LSTM 做回归光看 MSE 不够你还要看梯度范数和预测振幅。梯度范数突然变大说明可能要梯度裁剪预测振幅持续小于真实振幅说明模型在往均值回归这是多步预测的典型失败模式。model TempLSTM() optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.MSELoss() for epoch in range(50): model.train() total_loss 0 for xb, yb in train_loader: 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() total_loss loss.item() print(fepoch {epoch}, loss {total_loss / len(train_loader):.4f})逻辑说明clip_grad_norm_把梯度范数限制在 1.0这是 LSTM 训练的常规操作不加的话遇到异常样本容易 loss 变 NaN。学习率 1e-3 配合 Adam 是安全起点如果 loss 震荡明显降到 5e-4。参数说明max_norm1.0不是绝对标准你可以试 0.5 到 5.0但超过 5.0 基本等于没裁剪。batch size 我一般用 32 或 64太小梯度噪声大太大泛化会差。4. 多步预测不崩的关键从直接预测到滚动预测的取舍4.1 直接预测和自回归滚动预测的区别多步预测有两种主流做法。第一种是直接预测模型一次输出未来 7 天也就是上面代码里的做法。第二种是自回归滚动预测模型每次只预测下一天然后把预测值拼回输入再预测下一天循环 7 次。直接预测的优点是误差不会累积因为每一步都是独立输出缺点是模型要同时学会 7 个不同时间跨度的映射任务难度更高容易出现振幅偏小。自回归的优点是每一步任务简单模型容易学缺点是误差会累积预测到第 7 天时输入里已经混入了前 6 天的预测误差这就是第 1 章说的“第七天开始胡言乱语”的主要来源。我的经验是7 天以内用直接预测30 天以上用 seq2seq 或者带 teacher forcing 的自回归不要用纯自回归硬滚 30 天那样结果基本不可用。4.2 用验证集振幅比判断模型是否均值回归振幅比是一个很实用的诊断指标预测值的标准差除以真实值的标准差。如果这个比值低于 0.7说明模型预测得太平滑在往均值靠。def amplitude_ratio(model, loader): model.eval() preds, trues [], [] with torch.no_grad(): for xb, yb in loader: preds.append(model(xb).numpy()) trues.append(yb.numpy()) preds np.concatenate(preds) trues np.concatenate(trues) return preds.std() / trues.std() ratio amplitude_ratio(model, val_loader) print(f振幅比: {ratio:.3f})逻辑说明这个函数在验证集上跑一遍收集所有预测和真实值算标准差比值。如果比值低于 0.7你可以尝试三个方向增加模型容量、减小 dropout、或者在损失函数里加一项对振幅的惩罚。参数说明振幅比没有绝对阈值但 0.7 到 1.1 之间通常算健康。低于 0.7 是欠拟合或者均值回归高于 1.2 可能是过拟合或者预测噪声太大。4.3 一个提升多步预测稳定性的小技巧如果你发现直接预测的 7 天里后面几天的误差明显大于前面可以在损失函数里给不同预测天数加不同权重。比如第 1 天权重 1.0第 7 天权重 1.5让模型更关注远期误差。weights torch.tensor([1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6]) criterion nn.MSELoss(reductionnone) def weighted_mse(pred, target): loss criterion(pred, target) # (batch, horizon) return (loss * weights.to(pred.device)).mean()逻辑说明reductionnone保留每个时间步的 loss然后乘以权重再取平均。这样模型在优化时会更用力去拟合远期缓解后面几天振幅偏小的问题。参数说明权重不要设得太极端1.0 到 1.6 的线性递增是比较温和的。如果你设成 1.0 到 5.0模型可能会牺牲近期精度去换远期整体 MSE 反而变差。5. 避坑与排查气温预测里最容易翻车的五个地方5.1 现象验证 loss 很低但画图一看完全不对原因最常见的是归一化反变换写错了或者验证集在归一化时用了自己的 scaler。另一个可能是你把sin_doy和cos_doy也做了归一化导致周期特征被破坏。解决检查反变换是否用训练集的 scaler检查时间特征是否被二次归一化。画图时同时画真实值和预测值不要只看 loss 数字。5.2 现象训练 loss 一直不降或者变成 NaN原因学习率太大、梯度爆炸、或者输入里有 NaN。气温数据如果插值没做好容易在边界产生 NaN。解决先检查数据里有没有 NaN用np.isnan(X).sum()查。然后加梯度裁剪学习率降到 1e-4 再试。如果还是 NaN把 batch size 调小。5.3 现象预测曲线比真实曲线平滑很多原因这是多步预测的均值回归模型倾向于输出条件均值而不是真实波动。MSE 损失本身就会鼓励这种行为因为预测均值可以让 MSE 最小。解决换用 MAE 或者 Huber loss或者在损失里加振幅惩罚。另一个办法是增加输入特征给模型更多区分不同天气型的信息。5.4 现象换一个年份测试误差突然变大原因气温序列存在年际变化如果训练集里没有包含类似年份的天气模式模型外推能力有限。这不是 bug是数据分布偏移。解决用滚动窗口做交叉验证不要只切一次训练测试集。如果业务允许加入气候指数作为额外特征。5.5 现象GPU 利用率很低训练很慢原因数据量小、batch size 小、或者 DataLoader 的num_workers设成了 0。LSTM 在小数据上本身就不容易吃满 GPU。解决把数据预加载到 GPU 显存或者用num_workers4加pin_memoryTrue。如果数据量确实小CPU 训练可能更快不必强行上 GPU。6. 进阶技巧用温度距平代替原始温度让 LSTM 少学很多废话如果你已经跑通了上面的流程想让模型在极端天气上表现更好我建议试一个改动不要直接预测原始温度而是预测温度距平也就是当天温度减去同一天的多年平均值。这个改动背后的逻辑是原始温度里很大一部分是年周期LSTM 要花很多容量去学这个确定性成分而距平序列去掉了年周期模型可以专注于学天气异常。具体做法是先用训练集计算每个 dayofyear 的多年均值然后构造anomaly temp - climatology[dayofyear]用距平序列训练 LSTM预测出距平后再加回气候均值得到最终温度。我实测下来这个改动在验证集上的 MAE 能降 5% 到 10%尤其是在季节转换期效果明显。验证方法也很直接分别用原始温度和距平训练两个模型在同一个测试集上比 MAE 和振幅比。如果距平版本的振幅比更接近 1说明它更好地保留了天气波动。最后一个习惯我每次跑完气温预测都会把预测曲线和真实曲线叠在一起再画一张误差随预测天数变化的图。如果第 7 天的误差比第 1 天大了三倍以上我不会急着调模型而是先回去检查数据里有没有缺测、插值是不是合理。气温预测这件事数据质量的天花板往往比模型结构更低。希望帮到你。本文还有配套的精品资源点击获取