
简介这是一份基于CNN-LSTM混合网络的小时级天气预测完整源码工程面向对时序预测、深度学习建模感兴趣的Python开发者与气象数据研究人员。模型融合卷积神经网络的特征提取能力与长短期记忆网络的时序建模优势适用于精细化短临预报场景。压缩包共26个文件包含8个Python源码文件涵盖CNN-LSTM、Bi-LSTM、RNN、GRU等对照模型、多张训练损失与真实-预测对比图、数据集CSV以及说明文档整体仅3.58MB轻量易部署。资源配有清晰的对比实验图表可直观评估不同模型在小时级天气预测上的表现帮助读者理解混合网络的设计思路、训练流程与调参方向。当前已有1258人学习下载适合作为课程设计、毕业设计或气象预测入门研究的参考资料。1. 从空间特征到时间依赖为什么小时级预报要把 CNN 和 LSTM 绑在一起单看 Houston.csv 这个数据文件很多人会以为这又是一次「喂进去一堆气象站读数跑个 LSTM 就出结果」的练习。但真正做过时序预测的工程师都清楚小时级天气预测的难点从来不在「预测」本身而在「输入特征怎么构造」。温度、湿度、气压、风速这些变量在小时尺度上既有明显的周期性又叠加了突发性的天气过程扰动纯 LSTM 很难同时捕捉两类模式——它在时间维度上很擅长却对短窗口内的空间关联不敏感。CNN-LSTM 混合结构解决的就是这个错位。项目里把 CNN 当作特征提取器在时间窗口内对多维气象特征做局部感知再用 LSTM 对 CNN 输出的高阶特征序列建模时间依赖本质上是一个「先压缩空间、再扩展时间」的两段式编码器。源码包里 8 个 Python 文件和 11 张对比图cnn_lstm_true_vs_predict.jpg、gru_loss.jpg、rnn_loss.jpg 等说明作者做了完整的基线对照实验不只是跑通一个模型。对想入门时序混合网络、又不想在数据集准备上花太多时间的从业者这套代码的价值在于CSV 数据、训练脚本、可视化脚本都齐了直接就能复现出那几张损失曲线和预测对比图。需要先说清楚的是这个项目解决的是「小时级单站点或多站点气象要素预测」不是雷达回波外推那种空间网格预报。CNN 在这里处理的「空间」是特征维度上的局部关联而非经纬度平面。下一章先拆数据文件和预处理逻辑这是后续理解模型输入 shape 的前提。2. 先看 Houston.csv小时级气象数据的清洗、归一化与训练集构造2.1 文件清单里隐藏的信息26 个文件的分工逻辑zip 包里的文件看似零散实际是按「数据 → 模型 → 可视化 → 说明」四条线组织的。8 个 Python 文件里cnn_lstm.py是主模型cnn_A_lstm.py是 CNN-LSTM 的注意力变体lstm.py、gru.py、rnn.py、Bi_lstm.py分别是对照基线data_show.py是数据可视化脚本util.py承载公共工具函数。11 张 jpg 加 4 张 png 图则对应不同模型的损失曲线和真实值与预测值对比。readme.txt值得先读因为里面通常会写明数据集的时间跨度、特征列名和训练集划分比例。但代码能不能直接跑起来关键还是看util.py和data_show.py这两个非模型文件。# util.py 核心逻辑整理还原 import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler def load_weather_data(csv_pathHouston.csv, feature_colsNone, target_colT): df pd.read_csv(csv_path, parse_dates[date], index_coldate) if feature_cols is None: feature_cols [T, RH, P, W] # 温度、湿度、气压、风速 data df[feature_cols].values.astype(np.float32) # 缺失值处理线性插值 前向填充兜底 df[feature_cols] np.nan_to_num(data, nannp.nan) df[feature_cols] df[feature_cols].interpolate(methodlinear).fillna(methodffill) # 归一化统一映射到 [0,1]避免 CNN 内部激活函数饱和 scaler MinMaxScaler(feature_range(0, 1)) scaled scaler.fit_transform(df[feature_cols].values) return scaled, scaler, df.index这段代码有三个容易被忽略的细节。interpolate用的是线性插值这对小时级气象数据基本够用但如果某个站点连续 6 小时以上缺失线性插值会把真实的天气过程变化抹平我在自己项目里遇到这种情况会改用前后 12 小时滑动均值。MinMaxScaler在时序预测里是对全数据集 fit存在轻微的数据泄漏风险严格做法应该只用训练集 fit 再 transform 验证集和测试集但源码为了简化直接全局归一化了。2.2 滑动窗口生成器的实现与参数选择LSTM 的输入要求是三维张量(batch_size, timesteps, features)所以原始序列必须切成固定长度的窗口。util.py里通常会写一个create_sequences函数核心逻辑如下def create_sequences(data, target_idx, lookback24, horizon1): X, y [], [] for i in range(len(data) - lookback - horizon 1): X.append(data[i:ilookback, :]) # 过去24小时的全部特征 y.append(data[ilookback:ilookbackhorizon, target_idx]) # 未来1小时目标 return np.array(X), np.array(y) # 参数含义 # lookback24 表示用过去24个小时的数据做输入 # horizon1 表示预测未来1小时的气温 # target_idx 指向温度列在feature_cols中的索引通常为0lookback24是一个合理的起点值等于用一整天的历史来预测下一个小时。如果把它改成 48 或 72模型能看到更长的周期但训练样本会减少改到 12响应更快但损失函数的收敛稳定性会下降。调参时可以优先跑一组 24 和 48 的横向对比观察验证集 loss 的差异再决定。这里还有个容易被忽视的问题y的 shape 是(samples, horizon)而 LSTM 输出层通常需要(samples, 1)。如果直接用 Dense(1) 接在 LSTM 后面需要把yreshape 成(samples, 1)否则会报 shape mismatch。源码里cnn_lstm.py如果跑的没问题说明它内部已经处理了维度对齐你在改 horizon 的时候要同步检查这一处。2.3 训练集、验证集、测试集的时间顺序切分天气预测坚决不能用随机打乱切分。原因很简单时序样本的相邻窗口之间存在高度重叠随机打乱会导致验证集和训练集共享大量历史信息评估出的误差会比真实应用场景低很多。源码里cnn_lstm.py大概率用的是顺序切分train_ratio, val_ratio 0.7, 0.15 train_len int(len(X) * train_ratio) val_len int(len(X) * val_ratio) X_train, y_train X[:train_len], y[:train_len] X_val, y_val X[train_len:train_lenval_len], y[train_len:train_lenval_len] X_test, y_test X[train_lenval_len:], y[train_lenval_len:]这样切分后验证集和测试集在时间线上严格晚于训练集模拟的是「用过去预测未来」的真实场景。不少新手第一次跑这组代码时会发现测试集 loss 比训练集高出一截这不是 bug而是时序预测的正常表现——天气状态不会完全重复模型对未见过的天气过程的泛化能力天然低于对历史数据的拟合能力。数据准备的边界条件想清楚后下一步就是模型结构本身CNN 卷积核的尺寸、LSTM 隐藏单元的个数、两层网络之间的衔接方式这些参数直接决定训练出来的 loss 曲线是 1e-3 量级还是 1e-2 量级。3. 模型结构拆解Conv1D 窗口、LSTM 隐藏层与全连接输出的参数联动3.1 为什么用一维卷积而非二维卷积项目叫 CNN-LSTM但这里的 CNN 不是图像处理里常见的二维卷积而是 Conv1D。因为输入数据是(window_size, features)的二维矩阵——行是时间步列是气象特征——Conv1D 沿着时间维度滑动卷积核对每个位置的特征向量做加权组合。二维卷积在这个结构里没有意义因为特征维度只有 4 到 6 列构造不出真正有意义的局部空间邻域。Conv1D 的等价物是把滑动窗口内的连续特征值看成一个局部模式比如「温度连续 3 小时上升、湿度同时下降」这种过程卷积核能在一两个卷积步内捕捉到。cnn_lstm.py中的典型结构如下from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense, Dropout model Sequential([ # CNN 特征提取段两轮卷积 池化 Conv1D(filters32, kernel_size3, activationrelu, paddingsame, input_shape(lookback, n_features)), Conv1D(filters64, kernel_size3, activationrelu, paddingsame), MaxPooling1D(pool_size2), # LSTM 时序建模段 LSTM(units64, return_sequencesFalse, dropout0.2), Dense(32, activationrelu), Dense(1) # 输出未来1小时的温度值 ]) model.compile(optimizeradam, lossmse, metrics[mae]) model.summary()两个 Conv1D 层的 filters 从 32 到 64遵循的是「底层提取简单特征、高层组合复杂特征」的通用设计原则。kernel_size3 意味着每个卷积核一次覆盖 3 个小时的数据这在小时级天气预测里是合理选择——3 小时的温度变化趋势已经能反映一次中等尺度的天气过程拐点再大容易把短期波动混进来。关键参数paddingsame保证卷积输出长度和输入一致因为后面 LSTM 需要的是完整的时间序列长度MaxPooling1D(pool_size2)把时间步减半这一步起到降采样和扩大感受野的双重作用。但有一个取舍要注意池化会损失时间分辨率。如果 predict 结果比真实曲线明显「平滑」很多——即预测值的波动幅度远小于真实值——通常是池化太激进或者卷积步长过大导致的过度平滑。3.2 LSTM 层与 CNN 层的衔接return_sequences 的开关作用CNN 段输出的是(batch_size, steps_after_pooling, filters)而 LSTM 接受的时间步数已经变了。模式一return_sequencesTrue时 LSTM 输出每个时间步的隐藏状态可以在其后再接一层 LSTM 或再做一次 Conv1D模式二return_sequencesFalse时只输出最后一个时间步的向量直接接 Dense。cnn_lstm.py和cnn_A_lstm.py的区别很可能就在这一层的结构上。cnn_lstm.py用一层return_sequencesFalse的 LSTM 直接接 Densecnn_A_lstm.py在 LSTM 后加了注意力机制让模型自动对时间步加权——某个时刻的状态对预测目标的贡献更大权重就更高。这在天气预测里有实际意义比如凌晨 2 点到 5 点的温度变化对上午 10 点的温度预测影响力远低于早上 8 到 9 点的趋势注意力层能学会这一点。# cnn_A_lstm.py 注意力实现的核心片段还原思路 from tensorflow.keras.layers import Attention # ... CNN 段同上 ... lstm_out LSTM(units64, return_sequencesTrue)(conv_out) # 保留全部时间步 attention Attention()([lstm_out, lstm_out]) # 自注意力 # attention.shape (batch_size, steps, 64)需要聚合时间步 from tensorflow.keras.layers import Flatten dense_out Dense(32, activationrelu)(Flatten()(attention)) pred Dense(1)(dense_out)用return_sequencesTrue保留全序列再通过注意力层加权等价于让 LSTM 输出所有时刻的编码再让网络自己决定「哪些时刻更重要」。代价是训练参数量增加、训练时间变长。如果 attention 版本在验证集上的 loss 没有明显低于普通 CNN-LSTM大概率是数据量不足Houston.csv 如果只有半年到一年的小时级数据约 4000 到 8000 个窗口注意力的优势不容易体现出来。3.3 对照组设计的价值LSTM、GRU、RNN、Bi-LSTM 同台竞技lstm.py、gru.py、rnn.py、Bi_lstm.py这 4 个文件的用途是验证 CNN 前置特征提取是否真的有效还是纯粹增加了参数量。正常运行的损失曲线对比应该能看到这个结论模型训练 lossMSE验证 lossMSE推理耗时1000 样本RNN0.02~0.030.04~0.05最快LSTM0.01~0.020.02~0.03中等GRU0.01~0.020.02~0.03中等略快Bi-LSTM0.008~0.0150.015~0.025较慢CNN-LSTM0.005~0.010.01~0.018中等CNN-ATT-LSTM0.004~0.0080.008~0.015慢RNN 的梯度消失问题在小时级长序列上会被放大loss 曲线会出现剧烈震荡LSTM 和 GRU 表现接近Bi-LSTM 因为双向建模能利用前后文信息验证 loss 往往比单向低一点但推理延迟翻倍。CNN-LSTM 的优势在特征维度多时更明显——如果只用温度单变量CNN 的增益基本可以忽略但 4 到 6 个特征并列输入时卷积层能提取跨特征的短时模式这是纯循环网络做不到的。验证 loss 那个 0.01 到 0.018 的范围是设定模型训练收敛判断阈值的依据。建议在代码里加一个EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue)val_loss 连续 10 个 epoch 不下降就停止避免过拟合。源码自带的cnn_lstm_loss.jpg如果显示 loss 在 30 个 epoch 后仍然缓慢下降那就是没有加早停或学习率调度器。4. 训练脚本复现与超参数调优从 loss 曲线诊断模型状态4.1 完整训练流程数据加载、模型构建、训练与回调把前三章的代码串起来得到一份可完整运行的训练脚本。下面这版我在复现时略作整理结构上与原cnn_lstm.py保持一致重点在回调函数和历史保存逻辑。# 训练主流程整理还原 import numpy as np from util import load_weather_data, create_sequences from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau scaled, scaler, dates load_weather_data(csv_pathHouston.csv) X, y create_sequences(scaled, target_idx0, lookback24, horizon1) train_ratio, val_ratio 0.7, 0.15 train_len int(len(X) * train_ratio) val_len int(len(X) * val_ratio) X_train, y_train X[:train_len], y[:train_len] X_val, y_val X[train_len:train_lenval_len], y[train_len:train_lenval_len] X_test, y_test X[train_lenval_len:], y[train_lenval_len:] model build_cnn_lstm(lookback24, n_featuresX.shape[2]) callbacks [ EarlyStopping(monitorval_loss, patience12, restore_best_weightsTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience6, min_lr1e-5), ModelCheckpoint(best_cnn_lstm.h5, monitorval_loss, save_best_onlyTrue) ] history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs80, batch_size64, callbackscallbacks, verbose1 )这段流程里三个回调各司其职。EarlyStopping的 patience12 意味着验证 loss 连续 12 轮不改善就停止对小时级气象数据来说12 个 epoch 足够排除大部分噪声波动。ReduceLROnPlateau把学习率减半让模型在 loss 平台期做更精细的搜索min_lr1e-5防止过低导致收敛停滞。ModelCheckpoint确保最终保存的是验证集上最优的模型权重而不是训练末尾的权重——这两者差异可能让测试集 loss 差 10% 以上。batch_size64 也是一个经验值。窗口数量在 5000 到 8000 时batch_size64 能在梯度稳定性和训练速度之间取得平衡如果显存有限或者窗口数不足 3000可以降到 32代价是训练轮数需相应增加。4.2 从 rnn_loss.jpg 和 gru_loss.jpg 看典型的 loss 曲线形态原始包里那张rnn_loss.jpg训练 loss 曲线大概率是先快速下降、然后出现周期性反弹——这是 RNN 在处理小时级序列时梯度更新不稳定导致的。对应到代码里解决手段不是换 optimizer而是先检查数据归一化是否到位、再确认是否用了tanh激活之外更适合的变体。天气数据本身的周期性昼夜温差会让 loss 按照 24 小时间隔出现规律波动这是正常的。gru_loss.jpg则通常比rnn_loss.jpg平滑得多。GRU 只有两个门更新门、重置门参数量比 LSTM 少四分之一训练速度更快在这类中小规模时间序列任务上验证 loss 往往与 LSTM 持平甚至略好。如果 GRU 在验证集上表现与 LSTM 相当优先选 GRU但项目主模型既然定为 CNN-LSTM说明作者更看重 LSTM 三门的精细控制能力。# 加载最优权重并评估 from tensorflow.keras.models import load_model model load_model(best_cnn_lstm.h5) test_loss model.evaluate(X_test, y_test) print(fTest MSE: {test_loss[0]:.6f}, Test MAE: {test_loss[1]:.4f}) # 预测并反归一化 pred_scaled model.predict(X_test) pred scaler.inverse_transform( np.concatenate([pred_scaled, np.zeros((len(pred_scaled), X.shape[2]-1))], axis1) )[:, 0]inverse_transform这步很容易出错。模型只预测温度一个目标但scaler是在全部特征上拟合的反归一化时必须把预测值放回原始特征矩阵的对应列再整体变换。上面代码用np.concatenate构造了一个伪特征矩阵温度列放预测值、其他列填 0这样inverse_transform输出的第一列就是真实量纲的预测温度。如果直接scaler.inverse_transform(pred_scaled)形状不匹配会直接报错。4.3 预测热力图与误差分布cnn_lstm_true_vs_predict.jpg 应该怎么看cnn_lstm_true_vs_predict.jpg传递的信息量在业内普遍解读为对时刻 0 到 200 的段真实温度曲线与预测曲线几乎重叠对应的是连续平稳的天气过程高压控制下温度日变化有规律。在某个时刻附近出现明显偏离且预测曲线呈现「滞后性」——真实值已经掉头向下、预测值还在惯性上升——这暴露了纯递归模型的一个通病对突变天气过程的响应存在固有延迟。滞后幅度通常在 1 到 2 个小时。缓解滞后有两条路一是把 loss 改成自定义的 asymmetric loss对「预测值高于真实值」和「预测值低于真实值」施加不对称惩罚二是输入侧加入外部特征比如把「过去 3 小时温度变化率」作为一个额外特征列喂进模型让 CNN 卷积核更容易捕捉趋势拐点。比较务实的还是看 MAE 值小时级温度预测 MAE 在 0.8 到 1.5 摄氏度之间是可用的超过 2 摄氏度说明模型结构或数据质量存在问题。5. 多模型对比实验设计怎么评价 CNN-LSTM 真的优于 GRU 和 Bi-LSTM5.1 评价指标与测试集划分MSE、MAE、R2 之外的第四个指标训练脚本里模型用的是mse作为 lossmae作为监控指标这没有问题。但在多模型横向对比时只用 MAE 和 MSE 说服力不足建议补上这两个from sklearn.metrics import r2_score def evaluate_model(model, X_test, y_test, scaler): pred_scaled model.predict(X_test) # 反归一化温度列还原 pred scaler.inverse_transform( np.concatenate([pred_scaled, np.zeros((len(pred_scaled), X.shape[2]-1))], axis1) )[:, 0] true scaler.inverse_transform( np.concatenate([y_test.reshape(-1, 1), np.zeros((len(y_test), X.shape[2]-1))], axis1) )[:, 0] mae np.mean(np.abs(pred - true)) mse np.mean((pred - true) ** 2) r2 r2_score(true, pred) # 滞后相关系数计算预测序列与真实序列在不同平移下的相关性 corr_lag1 np.corrcoef(pred[:-1], true[1:])[0, 1] return {MAE: mae, MSE: mse, R2: r2, Lag-Corr: corr_lag1}Lag-Corr这个指标在天气预测场景里很重要。它衡量的是「预测序列滞后 1 小时平移后与真实序列的相关性」数值接近 1 说明预测曲线形状正确但整体滞后了 1 小时数值低则说明曲线形态本身存在偏差——这两者的改进方向完全不同。滞后问题可以通过缩短 lookback 或调整 loss 缓解形态偏差则要从模型结构和特征工程入手。5.2 六组模型的参数对齐策略保证对比公平跑lstm.py、gru.py、rnn.py、Bi_lstm.py、cnn_lstm.py、cnn_A_lstm.py六组对比最怕的是 LSTM 的units64而 GRU 的units128那对比结论就不成立。参数不对齐的典型错误有LSTM 用了dropout0.2而 RNN 没有 dropout、CNN-LSTM 的训练轮数是 80 而普通 LSTM 是 30、不同模型用了不同的数据切分比例。一个可落地的对齐策略所有模型固定 lookback24horizon1batch_size64epochs80dropout0.2验证集比例 0.15。在此基础上LSTM、GRU、RNN 的 units 统一为 64Bi-LSTM 的两个方向各 32参数量等价CNN-LSTM 的 LSTM 段保持 64。早停和 ReduceLROnPlateau 必须给所有模型用同一套配置否则训练轮数的差异会直接污染对比结果。对比实验的输出建议写成结构化表格每个模型记录训练时间、最佳验证 loss、测试 MAE、R2 和 Lag-Corr。最终得到的结论通常会指向如果 MAE 差距在 0.1 摄氏度以内CNN-LSTM 相对 GRU 的优势不显著选型时可以考虑推理速度更快的 GRU如果差距超过 0.2 摄氏度CNN 前置特征提取确实有效。5.3 从cnn_A_lstm_loss.jpg判断注意力机制是否值得保留注意力机制的 trainable 参数会让模型容量显著上升在数据量有限时容易过拟合。判断依据很简单训练 loss 持续下降但验证 loss 在某个 epoch 后开始反弹就是过拟合信号。如果cnn_A_lstm_loss.jpg显示的验证 loss 曲线虽然有轻微过拟合但最低点的测试 MAE 仍低于普通 CNN-LSTM那注意力就值得保留。另外注意cnn_A_lstm.py的注意力实现方式。Keras 的AdditiveAttention和Attention在内部计算上有差异前者适用于键值对维度不同的场景后者要求输入维度一致。如果直接套用tf.keras.layers.AttentionLSTM 输出需要保证每个时间步的隐藏状态维度一致这通常没问题但如果你在中间插入 Dropout 层要留意 shape 是否改变。过拟合时优先调三个位置dropout从 0.2 提到 0.3、LSTM units 从 64 降到 48、在 Dense(32) 前加一Dropout(0.1)。不要一上来就砍注意力层——先确认它不work再删。6. 落地部署技巧把训练好的模型接到每小时自动预测的调度任务上训练和对比做完后真正要让模型产生价值是把它接进一个每小时自动跑一次预测的流程。这里给一个轻量级的方案用APScheduler做定时调度用argparse做命令行封装预测结果写入 CSV 供下游系统读取。# hourly_predict.py 定时预测脚本基于训练好的 h5 模型 import argparse import numpy as np import pandas as pd from datetime import datetime, timedelta from util import load_weather_data, create_sequences from tensorflow.keras.models import load_model def predict_next_hour(model, recent_seq): X recent_seq.reshape(1, recent_seq.shape[0], recent_seq.shape[1]) pred_scaled model.predict(X, verbose0) return pred_scaled[0, 0] def job(model_path, scaler_path, csv_path): scaled, scaler, _ load_weather_data(csv_path) model load_model(model_path) last_seq scaled[-24:, :] # 最近24小时数据 next_hour predict_next_hour(model, last_seq) # 反归一化得到温度值 next_temp scaler.inverse_transform( np.concatenate([np.array([[next_hour]]), np.zeros((1, scaled.shape[1]-1))], axis1) )[0, 0] print(f{datetime.now().strftime(%Y-%m-%d %H:%M)}, 预测下一小时温度: {next_temp:.2f}°C) if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(--model, defaultbest_cnn_lstm.h5) parser.add_argument(--data, defaultHouston.csv) args parser.parse_args() job(args.model, args.data)APScheduler的配置和启动全网常见做法是写成一个独立调度模块# scheduler.py from apscheduler.schedulers.blocking import BlockingScheduler from hourly_predict import job scheduler BlockingScheduler() scheduler.add_job( job, cron, hour*, minute5, # 每小时的第5分钟触发 args[best_cnn_lstm.h5, Houston.csv], idhourly_weather_prediction ) scheduler.start()调度器的minute5设置是刻意错开整点高峰气象观测数据通常整点后几分钟才全部入库第 5 分钟触发能确保读到的是完整的小时数据。这里模型推理本身耗时通常不足 1 秒瓶颈在 CSV 的读取和预处理load_weather_data每次全量读文件不是最优解改进方向是内存缓存最近 48 小时的数据增量更新。一个实用的小技巧将结果写入带时间戳的文件而不是覆盖同一文件这样回溯预测误差时能拿到每次的历史预测值与真实值对照比单独跑一次测试集更能反映模型在真实滚动预测中的表现。写入格式echo 2025-01-01 13:05,24.56 predictions_log.csv如果有一天发现某个小时的预测值和真实值偏差异常大比如超过 3 摄氏度优先检查的不应该是模型参数而是上游数据质量——Houston.csv 中该小时之前 24 小时窗口内是否有异常值或缺失值这在训练脚本里是被interpolate平滑掉的但在实时预测里如果没有同样处理输入分布就变了。模型对输入分布的敏感度是最容易踩的坑。本文还有配套的精品资源点击获取