简介:一份基于CNN-LSTM混合网络的精细化小时级天气预测完整源码,面向Python课程设计、期末大作业或毕业设计场景,适合需要快速搭建天气预测项目并作出效果对比的学生。压缩包共26个文件,体积仅1.82MB;以8个Python源码文件为核心,既有CNN-LSTM主模型,也包含LSTM、RNN、GRU、BiLSTM等对照模型及数据可视化脚本,另有11张jpg、4张png结果图、CSV气象数据集和DOCX说明手册,能够直接支撑实验分析与报告撰写。代码附有详细注释,结构清晰、部署门槛低,新手也可以根据注释理清数据预处理、特征提取、时序建模到结果对比的完整流程;结果图直观展示各模型在真实气温序列上的拟合曲线与损失变化,便于理解CNN特征提取与LSTM时序建模结合后的预测效果,也方便在报告中复用和二次修改。目前已有163人浏览学习,适合作为高分大作业的参考实现。
1. 基于CNN-LSTM混合网络的气象要素时空预测:为什么小时级任务需要混合架构
小时级天气预测是一个典型的时空序列建模问题,它和传统的日级预报有本质区别:你预测的是未来24到72小时内每一个小时的气温、湿度、风速或降水,而不是一个日均值。日均值会把夜间辐射冷却、午后对流峰值这些小时尺度信号全部抹平,模型自然看不到真实的物理过程。另一个反直觉的结论是,小时级预测的瓶颈通常不在模型深度,而在输入序列怎么构造、时空特征怎么对齐——很多拿到源码跑不出论文效果的人,问题几乎都出在数据预处理,而不是CNN或LSTM本身。
这个题目在高校的人工智能、数据挖掘课程中非常常见,尤其是作为大作业,它要求你同时展示卷积神经网络在空间特征提取上的能力、长短期记忆网络在时序依赖建模上的能力,以及两者如何通过合理的架构拼接产生一加一大于二的效果。本文会从混合网络的结构设计和选型逻辑讲起,落到可运行的PyTorch源码、训练调参和时间序列交叉验证的具体写法,最后给出提升小时级预测精度的三个关键技巧。适合正在做课程设计、需要完成可解释代码并写出高质量实验报告的同学,也适合想把时空预测思路迁移到风功率、负荷等其他连续变量预测场景的工程师。
2. CNN-LSTM混合网络的结构拆解:卷积层和时序层的三种拼接方式
2.1 为什么纯LSTM和纯CNN都不够:小时级数据里的空间与时间信号
天气站点或再分析格点数据里,每个时刻观测到的不是一个孤立数值,而是一组多要素状态:气温、相对湿度、气压、风速、风向、降水,外加可能的海拔和地理坐标。这些要素之间存在明显的局部相关性——比如气温和湿度共同影响体感温度,风速增大往往伴随湿度下降。CNN在提取这种局部特征上有天然优势,它用固定尺寸的卷积核滑动过输入矩阵,把相邻要素之间的关系抽象成高阶特征。这一点在格点气象数据上尤其明显,一个网格点周围3×3邻域内的气压梯度、温度差异,能直接反映锋面和局地环流的信息。
但CNN的卷积核只能看见一个局部窗口,对于时间维度上的长期依赖,它需要堆叠很多层才能扩大感受野。LSTM则恰好相反,它通过门控机制维护一条跨时间步传递的记忆线,能够记住几十个时间步之前的模式——比如夜间持续降温的趋势会延续到次日凌晨。问题是纯LSTM把每一时刻的多个要素当成一个扁平向量,无法显式建模要素之间的局部结构关系。把两者组合成CNN-LSTM混合网络,本质上是让CNN先完成空间维度的特征提取,再把压缩后的特征序列交给LSTM做时序建模,各干一段自己最擅长的事。
2.2 三种主流拼接架构:串联、并联、双段式
在实际实现中,CNN和LSTM的组合方式决定了模型的能力边界,常见的架构有三种:
| 架构类型 | 连接方式 | 适用场景 | 参数量特征 |
|---|---|---|---|
| 串联式特征学习 | CNN提取特征后序列输入LSTM | 要素间存在局部关联的多通道数据 | 中等,训练稳定 |
| 并联双塔结构 | CNN和LSTM分别处理空间与时间,后拼接 | 需要同时保留原始时序信息和空间特征 | 较大,需防止过拟合 |
| 双段式预测 | 前段CNN+LSTM编码,后段全连接解码 | 未来多步递归预测任务 | 整体适中,解码器决定输出维度 |
串联式是最常见也最适合大作业落地的选择。它的前向传播路径很清晰:输入张量形状为(batch_size, time_steps, num_features),先通过Permute把维度换到(batch_size, num_features, time_steps)即通道在前的格式,然后经过两层一维卷积完成空间特征提取,再Permute回(batch_size, time_steps, cnn_out_channels)输入LSTM。方便解释、好画结构图、调参时出问题容易定位,这是大作业最看重的三个特性。并联结构在复现论文时有优势,但对数据量要求高,容易出现过拟合。后文所有代码均以串联式为基础。选型时不需要盲目追求复杂结构,先跑通串联基线,再对比其他变体,实验结论反而更有说服力。
2.3 混合网络如何体现于小时级预测:时间窗口作为CNN-LSTM的桥梁
CNN-LSTM混合网络在小时级天气预测中的功效,取决于时间窗口如何搭建起两种网络之间的桥梁。窗口大小决定了LSTM能看到多长的历史,也决定了CNN能在多大范围内提取局部特征。一种常见做法是取过去72小时的逐小时观测,预测未来24小时——输入矩阵形状为(72, features),用两层Conv1d压缩特征,输出序列交给LSTM后,最后用全连接层生成24个预测值。这里要避免一个认知误区:LSTM的输出并不直接是预测值,它输出的是每个时间步的隐状态,你可以取最后一个隐状态,也可以取全部隐状态做加权和,具体取决于预测目标的细粒度。预测未来单点时取最后隐状态即可,预测未来多个连续小时值时,要么用多步递归策略,要么把最后一个隐状态复制扩展后经全连接一次解码出多个值。后者在训练阶段更有效率,因为它的梯度只经过一条计算路径,模型收敛更快。
3. 数据集的构建与归一化:决定CNN-LSTM效果的上游环节
3.1 小时级气象数据的来源与整理策略
公开气象数据中,最常用的是GFS预报数据、ERA5再分析数据和气象站点观测数据。GFS能提供未来数天的逐小时预报,但精度受初始场影响偏大;ERA5是再分析数据,精度高但存在发布延迟,更适合离线实验;站点观测数据精细、真实,但站点稀疏时容易出现空间维度的数据缺失。大作业场景下,最稳妥的做法是用站点观测数据加ERA5格点插值数据的融合集,用观测值校正再分析数据的气温与降水偏差,这是当前多数院校实验的标准做法,也便于在报告中写“数据质量对预测影响显著”这类真实的实验结论。
数据整理有一个关键步骤容易被忽略——按站点拆分时空数据。很多初学者把所有站点数据混在一起直接划分训练集和测试集,导致同一个时段的数据同时出现在训练和测试里,模型记住的是时间索引而非物理规律,评估指标虚高。正确的做法是:按时间顺序切分,前75%的时间窗作为训练集,后25%作为测试集。如果使用多个站点,则在每个站点内独立执行相同的划分策略,保证时间上的序列完整性。
3.2 滑动窗口采样:构建模型的输入输出对
小时级预测的样本构造离不开滑动窗口。设定窗口大小input_len = 72(过去72小时),预测步长pred_len = 24(未来24小时),步长stride = 1时,数据量基本等于总小时数减去96,9000小时的数据可以构造约8900个训练样本。stride的选取值得专门说明:stride为1时训练样本之间存在大量重叠,容易过拟合,但模型能学到更平滑的时序转换规律;stride为24时样本之间不重叠,训练更高效,但样本量骤减为原来的1/24。
import numpy as np def create_sequences(data, input_len=72, pred_len=24, stride=1): """ 将形状为 (total_hours, num_features) 的数组切分为监督学习样本 返回 X: (n_samples, input_len, num_features) 和 y: (n_samples, pred_len) """ X, y = [], [] for i in range(0, len(data) - input_len - pred_len + 1, stride): X.append(data[i : i + input_len]) y.append(data[i + input_len : i + input_len + pred_len, 0]) # 以第一列为目标变量 return np.array(X), np.array(y)这段代码的关键在于y取的是预测窗口内目标变量列——通常取气温列索引0——的全部数值,不是只取最后一个值。很多人在构造多步预测标签时误以为目标也应该是单个值,导致模型结构被迫改成递归式单步预测,训练时间和误差累积都显著增加。直接输出pred_len个值的训练方式叫作直接多步预测,它让每个预测值对应独立的解码权重,相比递归预测,误差不会像滚雪球一样累积。stride的设定逻辑与数据规模挂钩:数据量少于5000小时时,stride取1保留更多样本;数据量高于20000小时时,stride取6或12能在保持样本量充足的前提下显著降低训练耗时。
3.3 归一化与反归一化的参数绑定
归一化是CNN-LSTM这类基于梯度下降的模型必须做的一步,而且必须在划分训练集和测试集之后执行,用训练集的均值方差对测试集做变换,绝不能在全数据集上统一归一化——那属于标签泄漏,测试集的分布信息会通过均值方差进入训练过程,导致验证指标失效。对于多要素气象数据,推荐使用Z-Score归一化而非MinMax。理由在于气温、湿度、气压的量纲差异很大,Z-Score后各要素统一到标准正态分布,便于CNN的卷积核同时处理多种物理量特征。
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() # 按时间顺序切分:train_ratio 前的样本为训练集 train_data = raw_data[: int(len(raw_data) * 0.75)] test_data = raw_data[int(len(raw_data) * 0.75) :] scaler.fit(train_data) train_scaled = scaler.transform(train_data) test_scaled = scaler.transform(test_data) def inverse_scale(pred_scaled, scaler, feature_idx=0): """把归一化的预测值还原为原始物理量,返回与pred形状相同的数组""" n = pred_scaled.shape[0] dummy = np.zeros((n, scaler.mean_.shape[0])) dummy[:, feature_idx] = pred_scaled return scaler.inverse_transform(dummy)[:, feature_idx]这段代码中的inverse_scale函数非常实用:它构造一个和目标特征同形状的零矩阵,把预测值填进目标列,再调用inverse_transform,最后取出该列。这样做的好处是不用手动保存和重建均值方差,避免因为复制粘贴算错标准差方向而得到数值离谱的预测结果。训练时模型看到的是标准化数据,评估时则统一在原始物理量上计算指标,这一点必须在代码注释末尾写清楚,因为你最终要在报告里展示“预测气温与实际气温对比图”,图上坐标必须是真实温度,而不是标准化后的无量纲值。
4. 模型实现与训练参数:从PyTorch代码到收敛验证
4.1 基于PyTorch的CNN-LSTM模型原型
在数据准备好之后,模型的实现就变得直观。以下代码定义了一个适用于多要素输入的CNN-LSTM混合网络,它接收形状为(batch_size, input_len, num_features)的张量,CNN部分完成空间特征提取,LSTM部分负责时序规律建模,最终输出pred_len个预测值:
import torch import torch.nn as nn class CNNLSTMModel(nn.Module): def __init__(self, num_features, hidden_size=64, cnn_out_channels=32, kernel_size=3, num_layers=2, pred_len=24): super().__init__() self.conv1 = nn.Conv1d(num_features, cnn_out_channels, kernel_size, padding=kernel_size//2) self.conv2 = nn.Conv1d(cnn_out_channels, cnn_out_channels * 2, kernel_size, padding=kernel_size//2) self.relu = nn.ReLU() self.lstm = nn.LSTM(cnn_out_channels * 2, hidden_size, num_layers, batch_first=True, dropout=0.2) self.fc = nn.Linear(hidden_size, pred_len) def forward(self, x): # x: (batch_size, input_len, num_features) x = x.permute(0, 2, 1) # 转为 (batch, channels, length) 供Conv1d使用 x = self.relu(self.conv1(x)) x = self.relu(self.conv2(x)) x = x.permute(0, 2, 1) # 转回 (batch, length, channels) out, _ = self.lstm(x) # out: (batch, length, hidden_size) out = out[:, -1, :] # 取最后一个时间步的隐状态 return self.fc(out) # 输出 (batch, pred_len)两个Conv1d层把要素通道从num_features逐步扩展到cnn_out_channels的两倍,这个扩展过程就是CNN抽取高阶特征的过程。卷积的padding参数设为kernel_size // 2,保证卷积后序列长度不变,LSTM能继续按时间步处理。LSTM层的dropout设在除最后一层之外的所有层间——注意PyTorch中传入dropout参数后,最后一层实际上不会应用dropout,这一点和直觉略有出入,调参时如果想加大正则化力度,可以手动在forward中对最后一层的输出再施加一次nn.Dropout。输出前的x.permute操作必须存在,因为LSTM的输入序列维度和CNN的特征通道维度不兼容,漏掉这个维度转换是复现时最常见的报错原因之一。
4.2 损失函数与评估指标:MAE、RMSE和R2的取舍
天气预测的损失函数选择值得专门讨论。均方误差(MSE)对大误差样本施加更大的惩罚,梯度更新方向偏向于消除极端值偏差,这在降水、风速这类方差大的要素上有优势;平均绝对误差(MAE)对离群点更鲁棒,在气温这类相对平稳的要素上收敛更稳定。实际大作业里推荐以MSE作为训练损失,以MAE和R2作为测试评估指标。因为训练时MSE的梯度在接近零误差时也不完全消失,收敛过程不会过早停滞,而MAE在零附近的梯度不平滑,使用SGD或Adam优化时容易出现收敛抖动。评估时MAE比MSE更直观、更贴近人的直觉——MAE为2.3摄氏度的含义就是平均误差不到3度。
def evaluate_model(model, test_loader, scaler, feature_idx=0): model.eval() preds, trues = [], [] with torch.no_grad(): for X_batch, y_batch in test_loader: y_pred = model(X_batch) preds.append(y_pred.numpy()) trues.append(y_batch.numpy()) preds = np.concatenate(preds, axis=0) trues = np.concatenate(trues, axis=0) preds_inv = inverse_scale(preds, scaler, feature_idx) trues_inv = inverse_scale(trues, scaler, feature_idx) mae = np.mean(np.abs(preds_inv - trues_inv)) rmse = np.sqrt(np.mean((preds_inv - trues_inv) ** 2)) ss_res = np.sum((trues_inv - preds_inv) ** 2) ss_tot = np.sum((trues_inv - np.mean(trues_inv)) ** 2) r2 = 1 - ss_res / ss_tot return {"MAE": mae, "RMSE": rmse, "R2": r2}评估必须在反归一化之后进行,这一约束已经在3.3节的inverse_scale函数中铺垫。如果跳过反归一化直接算指标,得到的是一个无量纲的抽象数值,既不能写进报告,也无法解释其物理意义。RMSE和MAE之间的差值能反映误差分布的尾部特征——差值大说明存在少数明显偏离的预测点,检查数据时优先看极端天气时段。
4.3 训练循环、早停和学习率调整的三个细节
训练循环里最重要的三个工程细节是早停、学习率衰减和batch_size的选择。早停的判定标准设定为验证集MAE连续15个epoch不下降,而不是连续3或5个epoch就停,因为气象数据中包含跨周、跨月的自然变化周期,短期间隔内的波动可能只是正常震荡。学习率衰减采用CosineAnnealingLR比固定衰减更省心:
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50, eta_min=1e-5) best_mae = float("inf") patience, trigger = 15, 0 for epoch in range(100): train_one_epoch(model, train_loader, optimizer, criterion) val_metrics = evaluate_model(model, val_loader, scaler) scheduler.step() if val_metrics["MAE"] < best_mae: best_mae = val_metrics["MAE"] trigger = 0 torch.save(model.state_dict(), "best_model.pt") else: trigger += 1 if trigger >= patience: print(f"Early stopping at epoch {epoch}") break这段逻辑中,CosineAnnealingLR的T_max设为50表示余弦周期为50个epoch,学习率会从初始的1e-3平滑地下降到eta_min的1e-5,再在下一轮回升。对100轮的训练来说,这个周期能让学习率在训练中段保持较大的值加速收敛,在收尾阶段压缩到较小的值做精细调整。batch_size设在64或128之间比较稳妥——过小时梯度噪声大,LSTM的长期依赖建模容易受到干扰;过大时训练速度确实快,但每个epoch的参数更新次数少,模型收敛需要的epoch数会明显增加。此外,仅在验证集MAE下降时保存模型权重,保证最终拿出来的模型是验证集上表现最好的状态。
5. 精细化小时级预测的模型优化与常见误用规避
5.1 多步预测的误差累积控制:教师强迫与自由运行
许多人在测试阶段把24步预测拆成连续滚动的方式执行——每次用前一小时输出作为下一小时的输入,24步完成后统一计算误差。这种方法会造成误差累积:第一步的小误差会成为第二步的输入,误差逐级放大,预测序列可能在后期整体偏离。常见做法是训练时用教师强迫(Teacher Forcing),解码时直接用真实值作为下一步输入;而测试时改成自由运行(Free Running),用预测值作为下一步输入。这两者之间的gap如果过大,说明模型过度依赖上一步的真实观测,没有学好完整的时序动态。一个有效的缓解方案是在训练中随机切换教师强迫与自由运行:以概率p使用真实值,以概率(1-p)使用模型预测值,让模型逐步适应自身误差。
5.2 三类高频误用:全数据集归一化、打乱时间序列、单点输出标签
第一类全数据集归一化的危害在3.3节已详细展开,它会让测试集的统计信息泄漏到训练过程。第二类打乱时间序列的触发点在于使用默认的train_test_split函数时忘记设置shuffle=False——这会导致模型学到的时间顺序完全失效,CNN-LSTM会退化成无记忆的映射函数。第三类单点输出标签会把多步预测问题简化成单步回归问题,损失的多样性和时间上的连续性信息被大幅削减。自查的方法是打印一个batch的y形状:如果发现y.shape[1]是1而期望是24,说明构造标签时索引写错,取的是[i+input_len]而不是切片[i+input_len : i+input_len+pred_len]。
5.3 提升精度的三个可执行技巧
第一个技巧是在CNN之后、LSTM之前插入LayerNorm而非BatchNorm。气象数据的时间步之间存在明显的分布漂移——白天气温方差大,夜间方差小——BatchNorm在训练和测试时的批次统计量差异会引入扰动,LayerNorm则对每个样本独立计算均值和方差,对这个场景更稳定。
第二个技巧是将位置编码叠加到CNN后的特征序列上。LSTM本身对顺序敏感,但CNN输出的特征序列在进入LSTM前往往丢掉了部分位置信息,叠加一个可学习的正弦位置编码向量(形状与特征序列一致)可以强化时间信息传递。
第三个技巧是使用多要素目标加权损失,同时预测气温和湿度,将损失函数写为loss = 0.7 * mse_temp + 0.3 * mse_humidity。多任务学习让模型在共享的CNN-LSTM层中学习到更鲁棒的时空特征,这比单独预测气温的模型泛化能力强,均见于竞赛方案和高分作业报告中。加权系数需要依据两个要素的数值范围确定,气温以摄氏度为单位,数值在零下二十到四十之间;相对湿度以百分比为单位,数值在零到一百之间,直接相加会让梯度被湿度主导,系数需要先做归一化到同一量级。
本文还有配套的精品资源,点击获取