循环神经网络预测天气:从时序数据到可运行代码的完整路径
简介:这份资源面向具备Python与深度学习基础、希望上手时间序列预测的开发者与学习者,提供一套用循环神经网络预测天气的完整Python源码,帮助理解RNN、LSTM或GRU在气象数据建模中的实际落地方式。压缩包为rar格式,仅含1个py文件,体积约2KB,属于轻量级单文件脚本,便于直接阅读与二次修改。内容围绕历史气象数据(温度、湿度、风速、气压等)的预处理、模型构建、损失函数与优化器选择、训练验证及均方根误差等评估指标展开,可帮助读者梳理从数据清洗、归一化到序列建模的完整链路,并体会梯度消失、长期依赖等问题的应对思路。目前已有373人学习,适合作为入门序列预测的练手案例,也可在此基础上尝试超参数调优与模型融合,提升预测性能。
1. 循环神经网络预测天气:从时序数据到可运行代码的完整路径
天气预报本质上是一个时序建模问题:温度、湿度、气压、风速这些指标按小时或按天排列,前后数据之间存在强依赖关系。传统统计方法(如 ARIMA)能捕捉线性趋势,但面对多变量、非线性、长周期的气象数据时往往力不从心。循环神经网络(RNN)及其变体 LSTM、GRU 的设计初衷就是处理这类序列依赖——隐藏状态像一条记忆链,把过去若干时刻的信息压缩后传给下一时刻。这个方向适合有 Python 基础、想用深度学习做真实时序预测的开发者,也适合已经跑过 demo 但发现预测曲线“滞后一截”想找原因的人。下面从数据准备、模型搭建、训练调参到避坑,把一套可复现的方案拆开讲。
2. 天气时序数据的准备与特征工程:把原始 CSV 变成 RNN 能吃的张量
2.1 为什么天气预测必须做滑动窗口
RNN 的输入不是单条记录,而是一个形状为(batch_size, time_steps, features)的三维张量。time_steps就是滑动窗口长度——用过去 N 个时刻的数据预测下一个时刻的值。这个 N 的选择直接决定模型能“看到”多长的历史。太小(比如 3)会导致模型学不到周期性;太大(比如 720)会让训练变慢且容易过拟合。常见做法是先用 24(一天的小时数据)或 168(一周的小时数据)作为起点,再根据验证集表现调整。
假设你手头有一份weather.csv,字段包括date、temp、humidity、pressure、wind_speed。第一步是解析时间、排序、检查缺失值。
import pandas as pd import numpy as np # 读取并解析时间列 df = pd.read_csv('weather.csv', parse_dates=['date']) df = df.sort_values('date').reset_index(drop=True) # 检查缺失值比例 missing_ratio = df.isnull().mean() print(missing_ratio[missing_ratio > 0]) # 对数值列做线性插值(气象数据短缺口用插值比填均值合理) num_cols = ['temp', 'humidity', 'pressure', 'wind_speed'] df[num_cols] = df[num_cols].interpolate(method='linear', limit=6) df[num_cols] = df[num_cols].bfill().ffill()逻辑说明:interpolate的limit=6表示连续缺失不超过 6 个点才插值,超过则留给后续bfill/ffill兜底。参数上,如果你的数据是分钟级,limit可以放宽到 30;如果是日级,limit=2更稳妥。缺失值处理不干净,后面归一化时会出现NaN传播,训练直接报Loss is nan。
2.2 归一化与滑动窗口构造
不同特征的量纲差异极大:气压约 1013 hPa,风速可能只有 3 m/s。不做归一化,梯度会被大量纲特征主导。我一般用 MinMax 归一化到 [0,1],因为气象数据有物理边界,不像金融数据存在极端长尾。
from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) scaled = scaler.fit_transform(df[num_cols]) def make_windows(data, time_steps): X, y = [], [] for i in range(len(data) - time_steps): X.append(data[i:i+time_steps]) y.append(data[i+time_steps, 0]) # 第0列是temp,预测目标 return np.array(X), np.array(y) TIME_STEPS = 24 X, y = make_windows(scaled, TIME_STEPS) print(X.shape, y.shape) # (样本数, 24, 4) (样本数,)参数说明:TIME_STEPS=24表示用过去 24 小时预测第 25 小时的温度。y取data[i+time_steps, 0]是因为目标列是temp,索引 0。如果你要预测多个变量,把y改成data[i+time_steps, :],同时模型输出维度也要改。注意:归一化器必须在训练集上fit,再transform验证集和测试集,否则数据泄露会让验证指标虚高。
2.3 按时间切分数据集,别用随机切分
时序数据的铁律:不能随机打乱。常见做法是前 70% 训练、中间 15% 验证、最后 15% 测试。随机切分会让未来信息泄露到训练集,模型在测试集上表现“异常好”,上线后直接翻车。
n = len(X) train_end = int(n * 0.7) val_end = int(n * 0.85) X_train, y_train = X[:train_end], y[:train_end] X_val, y_val = X[train_end:val_end], y[train_end:val_end] X_test, y_test = X[val_end:], y[val_end:] print(X_train.shape, X_val.shape, X_test.shape)到这里,数据管道就通了。记住三个检查点:缺失值是否处理干净、归一化是否只在训练集 fit、切分是否按时间顺序。这三点任何一条出问题,后面模型再调也白搭。
3. 用 PyTorch 搭 LSTM 预测模型:结构、训练循环与早停
3.1 为什么选 LSTM 而不是朴素 RNN
朴素 RNN 的隐藏状态在反向传播时会出现梯度消失,超过 10 个时间步基本学不到东西。LSTM 通过输入门、遗忘门、输出门控制信息流,能稳定捕捉 100 步以上的依赖。GRU 是 LSTM 的简化版,参数少三分之一,训练更快,但在气象数据上两者精度差距通常不到 5%。我一般先用 LSTM 跑基线,如果训练时间太长再换 GRU。
import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset class WeatherLSTM(nn.Module): def __init__(self, input_size, hidden_size, num_layers, output_size): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=0.2 if num_layers > 1 else 0 ) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): # x: (batch, time_steps, input_size) out, _ = self.lstm(x) out = out[:, -1, :] # 取最后一个时间步的隐藏状态 return self.fc(out)参数说明:input_size=4(四个特征),hidden_size从 64 起步,num_layers=2时加dropout=0.2防过拟合,output_size=1(预测温度)。batch_first=True让输入维度是(batch, seq, feature),符合 PyTorch 默认习惯。取out[:, -1, :]是因为只有最后一个时间步的隐藏状态包含了整个窗口的汇总信息。
3.2 训练循环与早停策略
训练时用 MSE 损失和 Adam 优化器,学习率从 1e-3 开始。早停的耐心值设 10 个 epoch——验证损失连续 10 轮不下降就停,同时保存验证损失最低的模型权重。
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = WeatherLSTM(input_size=4, hidden_size=64, num_layers=2, output_size=1).to(device) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) train_ds = TensorDataset(torch.FloatTensor(X_train), torch.FloatTensor(y_train)) train_loader = DataLoader(train_ds, batch_size=32, shuffle=False) # 时序数据不 shuffle val_x = torch.FloatTensor(X_val).to(device) val_y = torch.FloatTensor(y_val).to(device) best_val_loss = float('inf') patience, wait = 10, 0 for epoch in range(200): model.train() for xb, yb in train_loader: xb, yb = xb.to(device), yb.to(device) optimizer.zero_grad() pred = model(xb).squeeze() loss = criterion(pred, yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() model.eval() with torch.no_grad(): val_pred = model(val_x).squeeze() val_loss = criterion(val_pred, val_y).item() if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), 'best_lstm.pth') wait = 0 else: wait += 1 if wait >= patience: print(f'Early stop at epoch {epoch}') break if epoch % 10 == 0: print(f'Epoch {epoch}, Val Loss: {val_loss:.6f}')关键点:shuffle=False在训练集上也要保持时序顺序,否则窗口之间的时间连续性被打乱。clip_grad_norm_把梯度范数限制在 1.0,防止 LSTM 梯度爆炸。早停的patience=10是经验值,数据噪声大可以调到 15,数据干净可以降到 5。
3.3 预测结果反归一化与评估
模型输出的是归一化后的温度,必须用同一个 scaler 反变换回摄氏度。评估指标用 MAE 和 RMSE,别只看 MSE——MSE 对异常值敏感,MAE 更直观。
model.load_state_dict(torch.load('best_lstm.pth')) model.eval() with torch.no_grad(): test_x = torch.FloatTensor(X_test).to(device) pred_scaled = model(test_x).squeeze().cpu().numpy() # 反归一化:构造与原始特征同宽度的数组,只替换温度列 def inverse_temp(scaled_vals, scaler, temp_idx=0): dummy = np.zeros((len(scaled_vals), scaler.n_features_in_)) dummy[:, temp_idx] = scaled_vals return scaler.inverse_transform(dummy)[:, temp_idx] pred_temp = inverse_temp(pred_scaled, scaler) true_temp = inverse_temp(y_test, scaler) mae = np.mean(np.abs(pred_temp - true_temp)) rmse = np.sqrt(np.mean((pred_temp - true_temp) ** 2)) print(f'MAE: {mae:.2f}°C, RMSE: {rmse:.2f}°C')反归一化时不能直接对预测值做scaler.inverse_transform,因为 scaler 是多列拟合的,输入维度必须匹配。构造 dummy 数组只填温度列,其他列置零,反变换后只取温度列。这个细节很多教程不讲,但不做就会报维度错误。
4. 训练过程中最容易翻车的五个地方:排查与修复
4.1 损失降到某个值就不动了
现象:训练损失和验证损失都在 0.05 附近震荡,不再下降。原因通常是学习率太大导致在最优解附近跳来跳去,或者模型容量不够。解决:先把学习率降到 1e-4 试 20 个 epoch;如果还不行,把hidden_size从 64 加到 128,num_layers从 2 加到 3。注意加层后必须加 dropout,否则验证损失会先降后升。
4.2 预测曲线整体滞后一个时间步
现象:预测值看起来就是把真实值向右平移了一格。原因:模型学到了“用当前值预测当前值”的捷径,因为温度自相关性极强。解决:在特征里加入差分项——用temp[i] - temp[i-1]作为额外输入,强迫模型学变化量而不是绝对值。另一个办法是把预测目标改成temp[i+time_steps] - temp[i+time_steps-1],最后再累加回去。
4.3 验证损失远低于训练损失
现象:训练损失 0.08,验证损失 0.03。原因:训练时 dropout 开启,验证时关闭,导致验证表现“虚好”。这是正常现象,但如果差距超过 3 倍,说明训练集和验证集分布不一致。检查切分点附近是否有极端天气事件,或者归一化时验证集出现了训练集没见过的极值。解决:用训练集的 min/max 裁剪验证集,或者改用 RobustScaler。
4.4 多变量预测时某个特征完全没学到
现象:温度预测还行,湿度预测就是一条直线。原因:不同特征的损失量级差异大,MSE 被温度主导。解决:对每个输出维度分别计算损失再加权,或者先单独训练每个变量的模型,再考虑多任务学习。另一个常见原因是该特征在窗口内变化太小,归一化后接近常数,模型自然学不到东西。
4.5 GPU 显存够但训练速度极慢
现象:batch_size=32,time_steps=168,训练一个 epoch 要几分钟。原因:LSTM 在长序列上是串行计算,GPU 利用率低。解决:把time_steps降到 72 或 48,或者改用一维卷积 + LSTM 的混合结构,先用卷积降采样再送 LSTM。如果必须用长序列,把batch_size加到 128,让 GPU 打满。
5. 让预测精度再上一截:双向 LSTM 与注意力机制的实战技巧
单向 LSTM 只能利用过去的信息,但天气序列在离线预测场景下,未来时刻的上下文同样有价值。双向 LSTM(BiLSTM)用两个方向的隐藏状态拼接,能同时捕捉前后依赖。代价是推理时必须等整个序列输入完毕,不适合实时流式预测。如果你的场景是“用过去 7 天预测未来 24 小时”,BiLSTM 是划算的。
class WeatherBiLSTM(nn.Module): def __init__(self, input_size, hidden_size, num_layers, output_size): super().__init__() self.bilstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, bidirectional=True, dropout=0.2 ) self.fc = nn.Linear(hidden_size * 2, output_size) def forward(self, x): out, _ = self.bilstm(x) out = out[:, -1, :] # 双向拼接后的最后时间步 return self.fc(out)注意self.fc的输入维度是hidden_size * 2,因为双向输出拼接后维度翻倍。训练时学习率要比单向 LSTM 低一些,我一般用 5e-4,因为双向结构的梯度路径更长,容易震荡。
注意力机制是另一个提升点。在 LSTM 输出后加一个加性注意力层,让模型自动给不同时间步分配权重。比如寒潮来临时,前 6 小时的数据比前 24 小时的更重要。实现上可以用一个线性层计算每个时间步的分数,再 softmax 归一化后加权求和。
class Attention(nn.Module): def __init__(self, hidden_size): super().__init__() self.attn = nn.Linear(hidden_size, 1) def forward(self, lstm_out): # lstm_out: (batch, time_steps, hidden_size) scores = self.attn(lstm_out).squeeze(-1) # (batch, time_steps) weights = torch.softmax(scores, dim=1) context = torch.bmm(weights.unsqueeze(1), lstm_out).squeeze(1) return context把 LSTM 的out从out[:, -1, :]改成Attention(hidden_size)(out),就能让模型动态选择重要时间步。实测在温度预测上,MAE 能从 1.8°C 降到 1.4°C 左右,但训练时间增加约 20%。如果你的数据量少于 5000 条,注意力层容易过拟合,建议先不加。
最后说一个我踩过的坑:不要一上来就堆 BiLSTM + Attention + 多层堆叠。先用单层 LSTM 跑通全流程,确认数据管道、归一化、反归一化都没问题,再逐项加复杂度。每次只改一个变量,记录验证集 MAE。我习惯在 notebook 里维护一张对比表,每跑完一组参数就记一行,最后选验证集最优的那组。这个习惯帮我省下了大量“改了五个地方不知道哪个起作用”的时间。希望帮到你。
本文还有配套的精品资源,点击获取