ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

气象数据预测实战:Python实现RNN/LSTM/GRU全流程拆解

气象数据预测实战:Python实现RNN/LSTM/GRU全流程拆解 简介基于循环神经网络实现气象数据预测的 Python 代码包面向本科、硕士及教研人员覆盖 RNN、LSTM、GRU 等主流时序模型在气象数据上的搭建、训练与评估流程也适合用作智能优化与神经网络预测方向的实验参考。压缩包为 zip 格式共 13 个文件、约 758KB内含 5 个 Python 脚本、5 张结果图、1 份 CSV 数据、1 个训练好的模型文件以及 1 份 Markdown 说明脚本按配置、读取、训练、测试与运行拆分png 图片展示不同学习率和迭代次数下的预测效果pt 文件支持直接加载模型继续实验readme 便于快速上手。目前已有 500 人学习下载。对想复现循环网络气象预测、调试超参数并迁移到其他时序任务的研究者这套代码提供了完整可运行的起点和直观的结果参考。1. 气象数据预测为什么要用 RNN/LSTM/GRU一个能跑的 Python 项目拆给你看做气象数据预测最常见的是拿温度、湿度、气压这类时序数据去推未来几小时或几天的值。传统做法用 ARIMA 或者线性回归但一旦数据带明显的周期性——比如日温差、季节变化——这些模型就经常翻车残差里全是结构性的自相关怎么调参数都压不下去。循环神经网络这类模型天然处理序列依赖能记住前几天的温度走势对今天的影响所以在气象这类长时序场景里LSTM 和 GRU 几乎是默认选择。这份资源就是一套完整可跑的 Python 实现不是 Demo 片段数据加载、滑窗构造、模型训练、测试评估、loss 曲线图、训练好的权重文件全都有。它同时包含了 RNN、LSTM、GRU 的接口你可以改一行配置就在三者之间切换。适合正在做时间序列预测课程设计、毕业论文或者想把深度学习用在自己手头气象数据上的人。我第一次跑通类似项目花了两天这份资源把那些坑基本都趟平了下面我把每个文件和参数掰开讲清楚。2. 循环神经网络的选型逻辑为什么是 LSTM 和 GRU而不是原生 RNN2.1 原生 RNN 的问题梯度消失和长期依赖原生 RNN 的结构很简单每个时间步的隐藏状态 h_t 由当前输入 x_t 和上一时刻的隐藏状态 h_{t-1} 共同决定。公式看着不复杂但反向传播的时候梯度要沿着时间维度一路乘回去。时间步一长梯度不是指数爆炸就是指数衰减最终结果是网络根本学不会“三天前的低温影响了今天的霜冻”这种长距离依赖。气象数据恰恰是长期依赖明显的场景。今天的气压走势往往和三天前的环流形势相关如果序列长度超过几十个时间步原生 RNN 基本就不干活了。这不是调参能解决的是结构上的先天缺陷。所以实际项目里很少有人直接上原生 RNN更多是拿它当教学示例。2.2 LSTM 的门控机制和它的三个门LSTM 在原生 RNN 的基础上加了一条“细胞状态”的传送带C_t 这条线可以一路穿过多步而不被反复非线性压缩梯度消失问题因此被大幅缓解。它靠三个门控制信息流遗忘门决定上一时刻的细胞状态有多少被保留输入门决定当前时刻的新信息有多少写入输出门决定当前时刻的隐藏状态输出什么。# 一个标准 LSTM 单元的前向计算PyTorch 风格示意 import torch import torch.nn as nn class LSTMCell(nn.Module): def __init__(self, input_size, hidden_size): super().__init__() self.input_size input_size self.hidden_size hidden_size # 四个门的权重合并在一大块里方便一次性矩阵乘法 self.W_ih nn.Linear(input_size, 4 * hidden_size) self.W_hh nn.Linear(hidden_size, 4 * hidden_size) def forward(self, x, state): h_prev, c_prev state gates self.W_ih(x) self.W_hh(h_prev) # 把拼接好的四个门向量拆开 i, f, g, o gates.chunk(4, dim1) i torch.sigmoid(i) # 输入门 f torch.sigmoid(f) # 遗忘门 g torch.tanh(g) # 候选细胞状态 o torch.sigmoid(o) # 输出门 c f * c_prev i * g h o * torch.tanh(c) return h, (h, c)上面这段是把 LSTM 单元拆开看内部结构实际工程里直接用nn.LSTM就行。四个门合并成一个大矩阵乘法是常见的工程优化把原本四次矩阵乘法压缩成一次GPU 上更高效。chunk(4, dim1)就是把拼接的结果重新切成四个部分分别对应输入门、遗忘门、候选状态和输出门。参数input_size是每个时间步的特征维度hidden_size是隐藏状态维度这两个值直接决定了模型的参数量。2.3 GRULSTM 的简化版参数更少训练更快GRU 把 LSTM 的三个门简化成两个——更新门和重置门——并且把细胞状态和隐藏状态合并成一条线。参数比 LSTM 少了四分之一左右训练速度更快在小数据集上往往效果不输 LSTM。气象数据如果只有单一站点的温度湿度数据量通常不会特别大GRU 有时候反而是更稳妥的选择没那么容易过拟合。选型上我一般这么判断数据量大、特征多优先试 LSTM数据量小、想要快速迭代优先试 GRU如果只是验证思路原生 RNN 跑通流程也行。这个项目的配置里就是在三者之间切换的训练一次对比 loss 就能看出来谁更合适。3. 项目文件结构与核心代码拆解config、data、模型三件套3.1 文件清单每一个文件是干什么的项目解压后的文件不算多但各司其职。config.py是全局配置中心所有超参数都集中在这里data.py负责读 CSV、归一化、构造时间序列滑窗LSTM.py定义网络结构running.py是训练入口test.py是评估和预测入口tq.csv是气象数据样本lstm_model.pt是训练好的权重文件res1 到 res5 开头的 PNG 是不同参数组合下的 loss 曲线图。# 项目根目录结构 ├── config.py # 超参数配置 ├── data.py # 数据加载与预处理 ├── LSTM.py # 模型定义 ├── running.py # 训练脚本 ├── test.py # 测试/预测脚本 ├── tq.csv # 气象数据集 ├── lstm_model.pt # 训练好的模型权重 ├── res1_M1_lr0.001_eps10.png ├── res2_M1_lr0.05_eps20.png ├── res3_M1_lr0.01_eps6.png ├── res4_M2_lr0.01_eps6.png ├── res5_M2_lr0.005_rps6.png └── readme.mdres 系列图片的命名信息量很大M1和M2应该是不同的模型结构lr是学习率eps是训练轮数rps可能是rolling predict step滚动预测步数。看到这些命名就知道作者当时做了多组对照实验这也是我们自己调参时的参考方向。3.2 config.py把超参数集中管理别在代码里到处写魔法数字这个设计对做实验的人来说非常实用。所有可能变的参数统一放一处换数据集、换模型、换学习率都不用去翻训练代码。# config.py 典型结构 class Config: # 数据相关 data_path tq.csv seq_len 10 # 用过去 10 个时间步预测未来 predict_len 1 # 预测未来 1 个时间步 split_ratio 0.8 # 训练集占比 80% # 模型相关 model_type lstm # 可选 rnn / lstm / gru input_size 1 # 单变量预测tq.csv 里只取一列 hidden_size 64 # LSTM 隐藏层维度 num_layers 2 # LSTM 层数深度网络能学更复杂的映射 dropout 0.2 # 防止过拟合只在层间生效 # 训练相关 learning_rate 0.01 batch_size 32 epochs 10 device cuda # 没有 GPU 改成 cpuseq_len 10是你用多长的历史窗口去预测未来这个值是整个项目里最需要根据数据特性去调整的参数。气象数据如果按小时采样10 个小时的窗口未必够如果按天采样10 天的窗口通常能覆盖一个完整的天气过程。predict_len 1表示单步预测想要多步预测要么改这里要么在测试阶段做滚动预测。model_type是三个模型切换的开关改成gru或者rnn就行。3.3 data.py时间序列滑窗和归一化是预测效果的隐形决定因素时间序列预测的数据处理跟图像分类完全不同不能随机打乱样本否则就造成了数据泄漏——模型“偷看”了未来的数据。标准做法是按时间顺序滑动窗口切样本。# data.py 核心逻辑 import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler def load_data(cfg): df pd.read_csv(cfg.data_path) # 假设 tq.csv 有一列 temp 是温度这里按列名取 data df[temp].values.reshape(-1, 1) # 归一化到 [0, 1]LSTM 对输入尺度敏感 scaler MinMaxScaler(feature_range(0, 1)) data_scaled scaler.fit_transform(data) # 构造滑窗样本X 是连续的 seq_len 个点y 是后面 predict_len 个点 X, y [], [] for i in range(len(data_scaled) - cfg.seq_len - cfg.predict_len 1): X.append(data_scaled[i:i cfg.seq_len]) y.append(data_scaled[i cfg.seq_len:i cfg.seq_len cfg.predict_len]) X np.array(X).reshape(-1, cfg.seq_len, 1) y np.array(y).reshape(-1, cfg.predict_len) # 按时间顺序切分不打乱 split int(len(X) * cfg.split_ratio) return X[:split], X[split:], y[:split], y[split:], scalerMinMaxScaler把温度数据压缩到 0 到 1 之间这对 LSTM 这类用梯度下降训练的模型非常重要。原始温度如果是 35 度和 36 度数值差异小loss 下降慢归一化之后梯度更新更均匀。reshape(-1, seq_len, 1)是把数据变成 PyTorch LSTM 需要的三维格式——(batch_size, seq_len, input_size)第三个维度 1 是因为我们只用了一列温度特征。切分时用split按比例截断不打乱顺序这是时间序列预测和普通分类任务在数据预处理上最大的区别。4. 训练与参数实测从 loss 曲线图反推调参方向4.1 LSTM.py模型定义和 forward 的写法模型定义部分最关键的是forward里对 LSTM 输出的处理。LSTM 返回两个东西output是所有时间步的隐藏状态序列(h_n, c_n)是最后一步的状态。做预测只需要最后一步的输出然后接一个全连接层映射到预测维度。# LSTM.py 模型定义 import torch import torch.nn as nn class WeatherLSTM(nn.Module): def __init__(self, cfg): super().__init__() self.lstm nn.LSTM( input_sizecfg.input_size, hidden_sizecfg.hidden_size, num_layerscfg.num_layers, batch_firstTrue, dropoutcfg.dropout if cfg.num_layers 1 else 0 ) self.fc nn.Linear(cfg.hidden_size, cfg.predict_len) def forward(self, x): # x shape: (batch, seq_len, input_size) out, _ self.lstm(x) # 取最后一个时间步的输出 last_step out[:, -1, :] # (batch, hidden_size) return self.fc(last_step) # (batch, predict_len)batch_firstTrue让输入格式更直观——第一维是 batch第二维是时间步不用在维度上做转置。out[:, -1, :]取序列最后一个位置的输出这一步相当于把整个历史信息压缩成一个向量再交给全连接层。predict_len 1时全连接层输出一个数训练 loss 用 MSE 衡量预测值和真实值的差距。4.2 running.py训练循环和 loss 曲线的输出训练脚本的流程是标准的 PyTorch 套路加载数据、初始化模型、定义 loss 和优化器、循环迭代。这个项目用的是 Adam 优化器时间序列预测里 Adam 比 SGD 省心得多——自适应学习率不用手动调动量参数。# running.py 训练循环核心片段 model WeatherLSTM(cfg).to(cfg.device) optimizer torch.optim.Adam(model.parameters(), lrcfg.learning_rate) criterion nn.MSELoss() train_losses [] for epoch in range(cfg.epochs): model.train() epoch_loss 0 for i in range(0, len(X_train), cfg.batch_size): batch_X torch.tensor(X_train[i:icfg.batch_size], dtypetorch.float32).to(cfg.device) batch_y torch.tensor(y_train[i:icfg.batch_size], dtypetorch.float32).to(cfg.device) optimizer.zero_grad() pred model(batch_X) loss criterion(pred.squeeze(), batch_y.squeeze()) loss.backward() optimizer.step() epoch_loss loss.item() avg_loss epoch_loss / (len(X_train) // cfg.batch_size) train_losses.append(avg_loss) print(fEpoch {epoch1}/{cfg.epochs}, Loss: {avg_loss:.6f})训练逻辑本身不复杂但有几个工程细节要提醒batch_X要转成float32PyTorch 默认不接受float64计算梯度每个 batch 开始前要调optimizer.zero_grad()否则梯度会跨 batch 累积导致 loss 震荡不收敛.squeeze()是把预测结果从(batch, 1)压成(batch,)跟真实标签的形状对齐否则 MSE 计算时维度不匹配会出广播错误结果看着对但实际数值是错的。4.3 从 res 系列图片看调参学习率和训练轮数的配合项目里附带的那几张 loss 曲线图本身就是很好的调参教材。res1_M1_lr0.001_eps10的学习率是 0.001训练 10 轮res2_M1_lr0.05_eps20学习率是 0.05训练 20 轮。同一模型结构下学习率差了 50 倍。学习率 0.001 通常收敛慢但稳定loss 曲线平滑下降学习率 0.05 可能前期下降很快但后期会在最低点附近震荡甚至直接发散loss 变成 nan。如果你的 loss 曲线像锯齿一样剧烈抖动先降学习率而不是加训练轮数。res4_M2_lr0.01_eps6只训练了 6 轮说明这个参数组合下 6 轮就已经收敛得差不多再训练意义不大。# 训练完成后会生成模型权重保存方式常见如下 # running.py 末尾 torch.save(model.state_dict(), lstm_model.pt)torch.save只保存模型的state_dict——就是所有的权重和偏置张量。加载时先实例化模型再load_state_dict关键是模型结构必须和保存时完全一致。hidden_size改了之后去加载旧权重会直接报size mismatch错误这不是 bug是维度对不上。5. 避坑指南气象序列预测最容易翻车的五个地方5.1 现象loss 下降很快但预测值全是直线这是时间序列预测里最经典的黑匣子问题。模型在训练集上 loss 很低但测试集上的预测几乎是一条水平线数值在训练集的均值附近波动。原因模型学到了“输出接近历史均值”这种偷懒策略。对 MSE loss 来说如果序列的波动相对均值较小预测均值能拿到很低的 loss模型就不会再费劲去学波动规律。另一个常见原因是滑窗标签构造错了预测目标变成了用未来数据预测过去。解决先打印几条训练样本的 X 和 y肉眼确认 X 的时间索引确实在 y 之前。然后在测试阶段计算一个基准——如果模型输出方差明显小于测试集真实值方差说明模型没有学到动态规律需要减小seq_len或者增加hidden_size。我一般还会把归一化之前的真实温度值恢复回去看误差而不是盯着归一化后的 loss 数值。5.2 现象训练时 GPU 显存不足或者 CPU 慢到无法忍受原因num_layers和batch_size设置太大seq_len过长导致计算图占用显存过多。气象数据本身不算大数据但 LSTM 的反向传播要存储每个时间步的中间状态序列越长显存占用线性增长。解决把config.py里的device改成cpu这是最简单的兜底方案。如果确实要 GPU 跑把batch_size从 32 降到 16 或者 8hidden_size从 64 降到 32。一个经验值是seq_len超过 100 时LSTM 的显存占用会快速上涨如果不是特别需要长期依赖先试试 20 到 30 的窗口。我自己的习惯是第一轮训练用 CPU 跑通全流程再切 GPU 调参避免在数据加载阶段就浪费 GPU 资源。5.3 现象预测结果和真实值整体“错位”——差了一个时间步原因这是滑窗构造或者数据对齐的问题。常见情况是predict_len 1但实际应该预测的步数和滑窗的偏移量不一致。比如第 i 个样本的 y 取的是data[iseq_len]而模型输出的时间含义是当前时刻——错位一个样本就导致曲线整体滞后。解决检查滑窗的索引关系。X[i]对应时间范围[i, iseq_len-1]那y[i]应该对应[iseq_len, iseq_lenpredict_len-1]。这个偏移量在构造数据集时错一个数字预测结果就全错位。验证方法很简单把测试集第一个样本的 X 和 y 打印出来人工看看时间先后是否合理。5.4 现象加载lstm_model.pt时报错原因报错信息一般是size mismatch for lstm.weight_ih_l0: copying a param with shape torch.Size([...]) from checkpoint, the shape in current model is torch.Size([...])。这说明当前实例化的模型结构和保存权重时的模型结构不一致。最常见的是改了hidden_size、num_layers或者input_size。解决确认加载权重时Config里的参数和训练时完全一致。我把模型的hidden_size和num_layers也存进权重文件名里比如lstm_model_h64_l2.pt这样就不会搞混。这里不是代码问题是权重和结构绑定关系的问题。5.5 现象loss 曲线直接变成 NaN原因学习率太高Adam 在这种情况下也可能无法稳定。另一个原因是归一化没做彻底输入数据里有inf或极大值梯度更新一步就溢出。解决先用print(np.isnan(data).sum())检查原始数据里有没有 NaN。去掉 NaN 之后再检查归一化结果的范围应该在 0 到 1 之间。如果数据没问题把learning_rate降到 0.001 以下重跑一次。我遇到过tq.csv里某一天缺测pandas 自动填了 NaN归一化之后还是 NaN最后模型权重全变成 NaN预测结果是一堆nan——数据清洗这步省不得。6. 验证模型效果保存权重复现、多步滚动预测和误差指标的实战技巧模型训练完不是结束能复现、能对比才是关键。这个项目里test.py承担验证职责但很多人在这一步就停了——只看看 loss 曲线就认为万事大吉。我一般会做三件事来验证一个气象预测模型是不是真的能用。第一件事是复现训练结果。加载lstm_model.pt在测试集上跑一遍预测反归一化得到真实温度值计算 RMSE 和 MAE。RMSE 对较大误差更敏感MAE 更直观。气象预测里两个指标都要看如果 RMSE 远大于 MAE说明预测在某些时刻出现了比较大的偏差——通常是天气过程突变的时候模型没跟上。# test.py 评估流程 model.load_state_dict(torch.load(lstm_model.pt, map_locationcpu)) model.eval() with torch.no_grad(): test_tensor torch.tensor(X_test, dtypetorch.float32) pred_norm model(test_tensor).numpy() # 反归一化恢复到真实温度 pred_real scaler.inverse_transform(pred_norm.reshape(-1, 1)) y_test_real scaler.inverse_transform(y_test.reshape(-1, 1)) # 计算误差指标 rmse np.sqrt(np.mean((pred_real - y_test_real) ** 2)) mae np.mean(np.abs(pred_real - y_test_real)) print(fRMSE: {rmse:.2f}, MAE: {mae:.2f})第二件事是做多步滚动预测。这个项目的predict_len 1只能预测下一个时间步但在真实业务里我们往往想预测未来 6 个小时甚至 24 小时。滚动预测的思路是用预测出来的值作为下一步的输入一步步往前推。这种做法误差会累积预测步数越远越不可靠曲线会慢慢变成均值线——这是正常现象不是 bug。# 滚动预测示例预测未来 steps 个时间步 def rolling_predict(model, init_seq, scaler, steps24): model.eval() seq init_seq.copy() # shape: (seq_len, 1) predictions [] with torch.no_grad(): for _ in range(steps): input_tensor torch.tensor(seq, dtypetorch.float32).unsqueeze(0) pred model(input_tensor).item() # 预测下一步 predictions.append(pred) # 把预测值拼进序列末尾丢掉最前面的旧值 seq np.roll(seq, -1, axis0) seq[-1, 0] pred # 反归一化 return scaler.inverse_transform(np.array(predictions).reshape(-1, 1))滚动预测的np.roll(seq, -1, axis0)是关键动作把整个历史窗口往前平移一格然后用新预测值填充最后一个位置。这样模型看到的总是最近seq_len个数据点。第三件事是画预测对比图。把测试集前 100 个点的真实值和预测值画在同一张图上肉眼判断追涨和滞后情况。两张曲线贴合度高但细节缺失说明模型学到了主要趋势但丢失了高频波动曲线明显滞后一个相位说明滑窗偏移量有问题回到第 5.3 节去查。做完这三步一个模型是不是真的能用就心里有数了。从那以后我每跑完一组实验都强制走一遍这套流程——复现比对、滚动预测、画图——哪怕最终结论只是“这个模型不行”也有一套可量化的证据链而不是凭感觉说“好像还行”。项目里那几个 res 开头的 loss 曲线图就是在多组对照实验里筛出来的。希望这份拆解能帮你在自己的数据上少走几个弯路。本文还有配套的精品资源点击获取
返回列表