简介:这份资源面向具备一定深度学习基础、希望用LSTM完成气温时间序列预测的学习者与开发者,围绕未来气温预测这一典型回归任务,提供可直接运行的代码与配套数据。压缩包共3个文件,包含2个Python脚本和1个CSV数据文件,整体约9KB,其中脚本分别承担模型搭建与训练流程,CSV则存放用于建模的气温序列数据,便于快速复现实验。资源描述中详细讨论了MSE、MAE等回归评估指标,并针对Keras回调机制展开说明:monitor用于选择监控指标,因采用5折交叉验证而未单设验证集,故使用acc;min_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(drop=True) # 2. 缺失值:气温序列用时间插值比均值填充合理 df["temp_max"] = df["temp_max"].interpolate(method="time") 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, dtype=np.float32) y = np.array(y, dtype=np.float32) print(X.shape, y.shape) # (样本数, 30, 3) (样本数, 7)逻辑说明:interpolate(method="time")按时间间隔加权插值,比简单前后填充更贴近气温的连续变化。sin_doy和cos_doy这一对特征解决的是“年末年初不连续”的问题,如果你只用 dayofyear 这个整数,模型会认为 365 和 1 相距很远,实际上它们只差一天。归一化只在训练集上 fit,这是防止数据泄漏的基本纪律。
参数说明:LOOKBACK=30是我在日值气温上比较稳的选择,短于 14 天模型学不到年周期,长于 60 天训练样本数会明显减少且梯度更容易爆炸。HORIZON=7对应一周预报,如果你要做 30 天,建议改成 seq2seq 结构而不是直接加长输出维度,后面第 4 章会讲。features里只放了温度和两个时间特征,如果你有气压、湿度、风速,可以加进去,但记得每个特征单独归一化。
2.3 训练集验证集怎么切才不骗自己
时间序列不能随机 shuffle 切分,这是铁律。但很多人切完训练集和验证集之后,忘了验证集和测试集之间也要留间隔。因为滑动窗口有 LOOKBACK 天的重叠,如果验证集紧接着训练集,最后几条训练样本的窗口会包含验证集开头的数据,评估结果会偏乐观。
我一般这样切:训练集占前 70%,验证集占接下来 15%,测试集占最后 15%,并且在训练集和验证集之间丢掉 LOOKBACK 天的样本作为缓冲。这个细节在论文里经常被忽略,但在实际业务里,如果你用虚高的验证指标去选模型,上线后误差会明显变大。
3. LSTM 模型搭建:层数、隐藏单元和 dropout 到底怎么定
3.1 为什么气温预测不需要堆很深的 LSTM
LSTM 层数不是越多越好,这在气温预测这种中低复杂度序列上尤其明显。我试过 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_dim=3, hidden_dim=64, num_layers=2, output_dim=7, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=input_dim, hidden_size=hidden_dim, num_layers=num_layers, batch_first=True, dropout=dropout 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_first=True让输入形状是(batch, seq, feature),和上一节构造的 X 一致。取out[:, -1, :]表示用最后一个时间步的隐藏状态做预测,这是多步直接预测的常见做法。dropout只在层数大于 1 时生效,因为 PyTorch 的 LSTM 在单层时 dropout 参数会被忽略,写了也是白写。
参数说明:hidden_dim=64是起点,如果你的数据量超过 10 年日值,可以试 128。num_layers=2配合dropout=0.2是我验证下来比较稳的组合,dropout 再高会欠拟合,再低容易过拟合。output_dim=7对应预测天数,如果你改成 14 或 30,记得同步改数据构造里的 HORIZON。
3.3 训练循环里必须监控的两个量
训练 LSTM 做回归,光看 MSE 不够,你还要看梯度范数和预测振幅。梯度范数突然变大,说明可能要梯度裁剪;预测振幅持续小于真实振幅,说明模型在往均值回归,这是多步预测的典型失败模式。
model = TempLSTM() optimizer = torch.optim.Adam(model.parameters(), lr=1e-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_norm=1.0) optimizer.step() total_loss += loss.item() print(f"epoch {epoch}, loss {total_loss / len(train_loader):.4f}")逻辑说明:clip_grad_norm_把梯度范数限制在 1.0,这是 LSTM 训练的常规操作,不加的话遇到异常样本容易 loss 变 NaN。学习率 1e-3 配合 Adam 是安全起点,如果 loss 震荡明显,降到 5e-4。
参数说明:max_norm=1.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(reduction="none") def weighted_mse(pred, target): loss = criterion(pred, target) # (batch, horizon) return (loss * weights.to(pred.device)).mean()逻辑说明:reduction="none"保留每个时间步的 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_workers=4加pin_memory=True。如果数据量确实小,CPU 训练可能更快,不必强行上 GPU。
6. 进阶技巧:用温度距平代替原始温度,让 LSTM 少学很多废话
如果你已经跑通了上面的流程,想让模型在极端天气上表现更好,我建议试一个改动:不要直接预测原始温度,而是预测温度距平,也就是当天温度减去同一天的多年平均值。这个改动背后的逻辑是,原始温度里很大一部分是年周期,LSTM 要花很多容量去学这个确定性成分,而距平序列去掉了年周期,模型可以专注于学天气异常。
具体做法是:先用训练集计算每个 dayofyear 的多年均值,然后构造anomaly = temp - climatology[dayofyear],用距平序列训练 LSTM,预测出距平后再加回气候均值得到最终温度。我实测下来,这个改动在验证集上的 MAE 能降 5% 到 10%,尤其是在季节转换期效果明显。
验证方法也很直接:分别用原始温度和距平训练两个模型,在同一个测试集上比 MAE 和振幅比。如果距平版本的振幅比更接近 1,说明它更好地保留了天气波动。
最后一个习惯:我每次跑完气温预测,都会把预测曲线和真实曲线叠在一起,再画一张误差随预测天数变化的图。如果第 7 天的误差比第 1 天大了三倍以上,我不会急着调模型,而是先回去检查数据里有没有缺测、插值是不是合理。气温预测这件事,数据质量的天花板往往比模型结构更低。希望帮到你。
本文还有配套的精品资源,点击获取