☰
CNN-A-LSTM小时天气预测源码解析:从数据管道到模型调优
2026/10/3 10:15:42 网站建设 项目流程

简介:这份资源是面向深度学习初学者与气象数据分析人员的CNN-A-LSTM小时天气预测实战项目,提供完整Python源码与配套文档,帮助读者理解如何将卷积神经网络与长短时记忆网络结合,用于处理时间序列气象数据并预测未来天气状况。压缩包共27个文件,约1.11MB,包含8个py脚本(涵盖cnn_lstm、cnn_A_lstm、lstm、gru、rnn、Bi_lstm等模型实现及util工具与数据展示代码)、1个csv数据集、2个md说明文档,以及11张jpg和4张png训练损失与预测对比图,便于直观评估各模型效果。目前已有249人学习。读者可借此掌握数据预处理、模型定义、训练验证与结果可视化的完整流程,并对比CNN、LSTM、GRU、双向LSTM等不同结构的预测表现,适合作为课程设计、科研入门或项目开发的基础模板。

1. 拆开这份 CNN-A-LSTM 小时天气预测源码:它到底能解决什么

小时级天气预测这件事,真正难的不是把模型跑起来,而是把「过去 24 小时的温度、湿度、气压」变成「未来 1 小时的温度」这种带时序依赖的回归问题。这份《基于 CNN-A-LSTM 的小时天气预测的 Python 源码+文档说明.zip》就是冲着这个场景来的:用 CNN 先提局部时序特征,再交给 LSTM 抓长程依赖,最后接全连接层输出预测值。它适合两类人——一类是刚学完 python 基础语法、想找一个完整时序项目练手的入门者,另一类是需要快速搭一个单站点小时预测 baseline 的从业者。整套代码是纯 Python 技术栈,不依赖冷门框架,文档说明把数据格式、训练流程和参数含义都写清楚了,拿到手改改数据列就能跑自己的站点数据。下面我按「数据怎么进、模型怎么搭、坑在哪」的顺序拆一遍。

2. 数据管道与 CNN-A-LSTM 结构:从原始 CSV 到可训练张量

2.1 为什么是 CNN 接 LSTM,而不是单独用 LSTM

单用 LSTM 做小时天气预测,最直接的问题是它把每个时间步当成同等重要的输入,而气象数据里相邻几个小时的温度、气压变化其实高度局部相关——比如连续 3 小时的降温趋势,这种短窗口模式用卷积核扫一遍比让 LSTM 自己学要高效得多。CNN 在这里的角色不是图像处理,而是一维卷积(Conv1D)沿时间轴滑动,提取「局部变化模式」;LSTM 接在后面,负责把这些局部模式串成更长的依赖关系。这个组合在中小规模时序数据上通常比纯 LSTM 收敛更快,也更不容易过拟合。

常见做法是把输入组织成滑动窗口:用过去 N 小时的多维特征预测下一小时的目标值。这份源码里 N 一般取 24,也就是拿一整天的小时数据预测下一个小时。特征维度取决于你的 CSV 有多少列气象变量,目标列通常是温度或你要预测的那个量。

2.2 数据准备:CSV 列结构与归一化

拿到源码第一步不是急着训练,而是确认你的数据列名和代码里读取的列对得上。典型的气象 CSV 长这样:一列时间戳,后面若干列数值特征,最后一列或指定列是预测目标。下面是我一般会先跑的检查脚本,用来确认数据能被正确读进来、没有缺失值断层:

import pandas as pd import numpy as np # 读取原始气象数据,parse_dates 把时间列转成 datetime 索引 df = pd.read_csv("weather_hourly.csv", parse_dates=["datetime"]) df = df.sort_values("datetime").reset_index(drop=True) # 检查缺失情况,小时数据最怕中间断档 print("总行数:", len(df)) print("缺失值统计:\n", df.isnull().sum()) # 对数值列做前向填充,气象传感器偶发丢点很常见 num_cols = ["temperature", "humidity", "pressure", "wind_speed"] df[num_cols] = df[num_cols].ffill().bfill() # 归一化:CNN 和 LSTM 对输入尺度敏感,不归一化收敛会很慢 from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler() df[num_cols] = scaler.fit_transform(df[num_cols]) print(df.head())

这段代码的逻辑是:先按时间排序保证时序不乱,再检查缺失——小时数据一旦中间缺几行,滑动窗口就会把不连续的两段拼在一起,模型学到的就是错的。前向填充是气象数据的常规处理,因为传感器丢点通常只是短暂中断。归一化用 MinMaxScaler 把特征压到 0 到 1,这一步不做的话,气压(约 1000 hPa)和风速(个位数)量级差太多,梯度会被大量级特征主导。

参数上要注意:parse_dates的列名必须和 CSV 里实际的时间列一致;num_cols要按你数据里真实存在的列改,别照抄。如果你的目标列也需要归一化,记得单独保存 scaler,预测完要反归一化才能得到真实温度值,这一步漏了是最常见的翻车点。

2.3 滑动窗口构造与 Dataset 封装

数据归一化完,下一步是把连续序列切成「输入窗口 + 目标值」的样本对。这份源码一般会有一个create_sequences之类的函数,核心逻辑如下:

def create_sequences(data, target_idx, window_size=24): """ data: 归一化后的 numpy 数组,形状 (时间步, 特征数) target_idx: 目标列在特征中的下标 window_size: 用过去多少小时预测下一小时 """ xs, ys = [], [] for i in range(len(data) - window_size): # 取连续 window_size 行作为输入 x = data[i:i + window_size] # 取窗口后一行的目标值作为标签 y = data[i + window_size, target_idx] xs.append(x) ys.append(y) return np.array(xs), np.array(ys) # 假设 temperature 在第 0 列 X, y = create_sequences(df[num_cols].values, target_idx=0, window_size=24) print("样本形状:", X.shape, y.shape) # 期望 (样本数, 24, 特征数)

逻辑说明:循环从第 0 行走到倒数第 window_size 行,每次取 24 行作为一个样本,第 25 行的目标值作为标签。这样每个样本的输入是(24, 特征数)的二维数组,正好对应 Conv1D 需要的(时间步, 通道)格式。参数window_size是最关键的超参——取太小模型看不到日变化周期,取太大样本数骤减且引入过多噪声,24 是小时数据里比较稳的起点。

提示:切训练集和测试集时一定要按时间顺序切,不能随机打乱。时序数据随机切分会让未来信息泄漏到训练集,测试指标虚高,上线就崩。

3. 模型搭建与训练:Conv1D + LSTM 的层序和参数怎么定

3.1 网络结构逐层拆解

这份源码的模型主体用 Keras 或 PyTorch 搭都常见,结构思路一致:一维卷积 → 池化 → LSTM → 全连接。下面用 Keras 写一个等价结构,方便对照源码里的层:

from tensorflow.keras import layers, models def build_cnn_lstm(window_size, n_features): model = models.Sequential([ # 一维卷积:64 个卷积核,核大小 3,沿时间轴提取局部模式 layers.Conv1D(filters=64, kernel_size=3, activation="relu", padding="same", input_shape=(window_size, n_features)), # 最大池化把时间步减半,降低 LSTM 的计算量 layers.MaxPooling1D(pool_size=2), # LSTM 抓长程依赖,64 个隐藏单元 layers.LSTM(64, return_sequences=False), # 全连接输出单个预测值 layers.Dense(32, activation="relu"), layers.Dense(1) ]) model.compile(optimizer="adam", loss="mse", metrics=["mae"]) return model model = build_cnn_lstm(window_size=24, n_features=len(num_cols)) model.summary()

逐层看:Conv1D的filters=64决定提取多少种局部模式,kernel_size=3表示每次看连续 3 小时,padding="same"保证输出时间步不变。MaxPooling1D(pool_size=2)把 24 步压成 12 步,既降维又保留最显著的局部特征。LSTM(64)的return_sequences=False表示只取最后一个时间步的输出,因为我们只要预测一个值。最后两层 Dense 做非线性映射到标量输出。

参数调整经验:如果数据量小(几千条以内),把filters和 LSTM 单元数降到 32 能明显减轻过拟合;如果欠拟合、训练 loss 下不去,先加 LSTM 单元数而不是加卷积核。kernel_size一般 3 或 5,再大对小时窗数据意义不大。

3.2 训练循环与早停

训练部分关键是回调配置,这份源码的文档说明里通常会提到 EarlyStopping。我一般这样配:

from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau callbacks = [ # 验证损失 10 轮不降就停,并恢复最优权重 EarlyStopping(monitor="val_loss", patience=10, restore_best_weights=True), # 损失停滞时把学习率砍半,帮助跳出平台 ReduceLROnPlateau(monitor="val_loss", factor=0.5, patience=5, min_lr=1e-6) ] history = model.fit( X_train, y_train, validation_data=(X_val, y_val), epochs=100, batch_size=32, callbacks=callbacks, verbose=1 )

EarlyStopping的patience=10是容忍度,验证损失连续 10 轮不改善就停,restore_best_weights=True保证拿到的是验证集最优那轮的权重,而不是最后一轮的。ReduceLROnPlateau在损失卡住时降学习率,这两个回调配合能省掉大量手动调参时间。batch_size=32是小时数据常用的起点,样本量特别小可以降到 16。

注意:validation_data必须是从训练集尾部切出来的连续时间段,不能从测试集里拿,否则早停的判据就被污染了。

3.3 预测与反归一化

训练完拿到模型,预测出来的值是归一化空间的,必须反变换回真实温度:

# 预测 y_pred_scaled = model.predict(X_test) # 反归一化:构造一个和目标列同维度的占位数组 dummy = np.zeros((len(y_pred_scaled), len(num_cols))) dummy[:, 0] = y_pred_scaled.flatten() # 目标在第 0 列 y_pred = scaler.inverse_transform(dummy)[:, 0] # 真实值同样反归一化后对比 dummy_true = np.zeros((len(y_test), len(num_cols))) dummy_true[:, 0] = y_test y_true = scaler.inverse_transform(dummy_true)[:, 0] from sklearn.metrics import mean_absolute_error print("MAE:", mean_absolute_error(y_true, y_pred))

这里容易踩的坑是inverse_transform要求输入维度和 fit 时一致,所以要先造一个和num_cols同宽度的占位数组,把预测值填进目标列再反变换。直接对一维数组调inverse_transform会报维度错误。MAE 是小时温度预测最直观的指标,一般能到 1 到 2 摄氏度以内算可用。

4. 避坑与排查:这份源码跑不起来时先看这几条

4.1 现象:训练 loss 一直是 nan

原因通常是输入里混进了 NaN 或无穷值,归一化前没处理干净,或者学习率过大导致梯度爆炸。解决:在create_sequences之前强制df = df.dropna(),并检查np.isfinite(X).all();学习率从默认 adam 的 1e-3 降到 1e-4 试一轮。

4.2 现象:验证 loss 远高于训练 loss,且越训差距越大

这是典型过拟合。原因可能是模型容量相对数据量太大,或者训练集和验证集分布不一致。解决:先把filters和 LSTM 单元数各砍一半,加Dropout(0.2)到 LSTM 后面;再确认切分是按时间顺序而非随机,随机切分会让验证集里混入和训练集相邻的样本,指标失真。

4.3 现象:预测曲线整体平移,形状对但数值差一截

多半是反归一化环节出错,比如 scaler 是对全部特征 fit 的,但反变换时目标列的位置填错了。解决:打印scaler.data_min_和data_max_,确认目标列对应的 min/max 是不是真实温度范围;再核对target_idx和填占位数组时的列下标是否一致。

4.4 现象:换自己的数据后报形状不匹配

源码里window_size、n_features往往是写死的,换数据后特征列数变了但模型input_shape没改。解决:把n_features改成len(num_cols)动态传入,window_size也做成参数,别在模型定义里硬编码。另外确认 CSV 时间列格式能被parse_dates正确解析,格式不对会整列变成 NaT,后续全崩。

4.5 现象:训练很快但预测结果几乎是一条直线

模型没学到东西,常见于归一化后目标列方差极小,或者 LSTM 单元数太少。解决:检查目标列归一化后的标准差,如果接近 0 说明这列本身没什么变化;把 LSTM 单元数加到 64 以上,并确认return_sequences=False时取的是最后一步而非被池化吃掉。

5. 进阶技巧:把单步预测改成多步,并验证模型没在偷懒

单步预测只能看下一小时,实际用起来往往想要未来 6 小时或 12 小时的曲线。最省事的改法是把输出层改成多神经元,标签也改成对应长度的向量:

def create_multistep_sequences(data, target_idx, window_size=24, horizon=6): xs, ys = [], [] for i in range(len(data) - window_size - horizon + 1): xs.append(data[i:i + window_size]) # 取窗口后连续 horizon 个目标值作为多步标签 ys.append(data[i + window_size:i + window_size + horizon, target_idx]) return np.array(xs), np.array(ys) X, y = create_multistep_sequences(df[num_cols].values, target_idx=0, window_size=24, horizon=6) # 输出层相应改成 Dense(horizon)

逻辑上就是把原来取一个点改成取连续horizon个点,模型最后一层Dense(1)改成Dense(horizon),损失仍是 MSE,只不过现在是对整段预测求平均误差。horizon越大,后面几步的误差通常越大,这是时序预测的固有特性,别指望 12 步和 1 步一样准。

改完多步之后,一定要做一件事:拿一个朴素基线对比。最简单的基线是「用窗口最后一个值当作未来所有步的预测」,如果模型 MAE 打不过这个基线,说明它根本没学到有效模式,只是输出了一个接近均值的平滑结果。我一般会这样验证:

# 朴素基线:用窗口最后一步的值重复 horizon 次 last_value = X_test[:, -1, 0] baseline_pred = np.repeat(last_value[:, None], horizon, axis=1) # 反归一化后对比 MAE # 模型 MAE 必须明显低于基线才算有效

这个对比是我踩过坑之后养成的习惯——有一次模型 MAE 看着不错,结果一对比基线发现只好了 0.05,等于白训。从那以后我每次做完时序模型,都强制走一遍基线对比,确认模型真的在预测而不是在复读。希望这份源码和上面的拆解能帮你少走几个弯路,拿到手先从数据检查脚本跑起,再动模型参数。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询