简介:这份资源面向计算机、人工智能相关专业的本科生与研究生,提供一套可直接运行的CNN-LSTM混合网络小时级天气预测源码,适用于毕业设计、期末大作业与课程设计等场景。项目将卷积神经网络与长短期记忆网络结合,用于从历史气象数据中挖掘时空特征并输出精细化逐小时预测结果,同时附带RNN、LSTM、Bi-LSTM、GRU等对比模型脚本,便于横向比较不同序列建模方案的预测效果。压缩包共26个文件,约1.82MB,其中8个Python源码文件承载模型定义、训练与可视化逻辑,11张jpg与4张png图像记录损失曲线和真实值对比预测图,另有CSV数据文件、docx说明手册及Markdown文档,代码注释较为完整,新手也能理解整体流程。目前已有163人学习下载,下载后简单部署即可复现实验,既能作为大作业直接提交,也可作为学习时序预测与深度学习模型融合的实践参考。
1. 从一份高分大作业说起:CNN-LSTM 怎么做小时级天气预测
如果你手头正拿着一份“基于 CNN-LSTM 混合网络的精细化小时级天气预测源码”,大概率是三种人之一:要交课程大作业的学生、想快速跑通一个时序预测 baseline 的算法新人、或者想拿气象数据练手却卡在数据预处理上的工程师。这份源码的核心价值不在于模型多深,而在于它把“小时级天气预测”这个任务拆成了一条能跑通的链路:历史气象观测数据进,未来若干小时的温度、湿度、风速等目标出。CNN 负责从多变量时间序列里抽局部变化模式,LSTM 负责记长期依赖,两者串起来比单用 LSTM 或单用 CNN 在小时级粒度上更稳。这篇文章不假设你手里有那份源码的正文,而是按这个标题最常见的实现路径,把数据、模型、训练、调参、踩坑一条线讲清楚,让你能自己复现,也能看懂别人给的代码到底在干什么。
2. 小时级天气预测的数据长什么样:从原始观测到模型输入
2.1 气象时间序列的字段与粒度
小时级天气预测和日级预测最大的区别是噪声大、周期短、对缺失值敏感。常见的数据来源是气象站观测记录,字段一般包括:干球温度、露点温度、相对湿度、气压、风速、风向、降水量、能见度等。每条记录带一个时间戳,粒度是 1 小时。你要预测的目标通常是未来 1 到 24 小时的某个或某几个变量,比如温度或湿度。
这里第一个容易翻车的地方是:很多人直接把原始 CSV 丢进模型,结果 loss 不降。原因不是模型不行,而是时间戳没排序、缺失值没处理、不同量纲的字段混在一起。小时级数据里,温度可能在 -10 到 40 之间,气压在 1000 上下,风速在 0 到 20 之间,如果不做归一化,梯度会被大量纲字段主导。
常见做法是先把数据整理成一张宽表,每行是一个小时,每列是一个字段,时间戳作为索引并升序排列。然后对每个字段单独做缺失值填充和归一化。缺失值少的时候用线性插值,连续缺失超过 3 小时的段落建议直接标记为无效样本,不要硬填。
2.2 用滑动窗口把时序转成监督学习样本
CNN-LSTM 吃的是三维张量:样本数、时间步长、特征数。你需要用滑动窗口把连续时间序列切成一个个样本。假设你用过去 24 小时的数据预测未来 6 小时的温度,那么每个样本的输入是 24 行、每行 N 个特征,输出是 6 个温度值。
import numpy as np import pandas as pd def make_windows(data, target_col, input_len=24, output_len=6): """ data: 归一化后的 DataFrame,索引为时间戳 target_col: 要预测的列名 input_len: 输入时间步长 output_len: 输出时间步长 """ X, y = [], [] values = data.values target_idx = data.columns.get_loc(target_col) for i in range(len(data) - input_len - output_len + 1): X.append(values[i:i + input_len, :]) # 过去 input_len 小时的所有特征 y.append(values[i + input_len:i + input_len + output_len, target_idx]) # 未来 output_len 小时的目标 return np.array(X), np.array(y) # 假设 df 已经按时间升序、填充缺失、归一化 # X shape: (样本数, 24, 特征数) # y shape: (样本数, 6) X, y = make_windows(df, target_col='temperature', input_len=24, output_len=6)这段代码的逻辑是:从第 i 行开始取 24 行作为输入,紧接着取 6 行的目标列作为输出。参数input_len决定模型能看到多长的历史,output_len决定预测多远。小时级天气里,input_len 一般取 24 或 48,output_len 取 1、3、6、12、24。注意不要用未来信息泄漏到输入里,窗口切分时输入和输出不能重叠。
2.3 训练集、验证集、测试集怎么切才不泄漏
时间序列不能随机打乱切分,否则相邻小时的数据会同时出现在训练和测试里,造成虚高精度。正确做法是按时间顺序切:前 70% 训练,中间 15% 验证,最后 15% 测试。如果你的数据跨年,最好保证测试集覆盖不同季节,否则模型在冬季数据上训练、在夏季数据上测试,误差会大得让你怀疑人生。
另一个细节是归一化参数只能用训练集算。很多人图省事在全量数据上算均值和方差,然后切分,这等于把测试集的分布信息泄漏给了训练。正确流程是:先切分,再用训练集的均值和方差去变换验证集和测试集。
3. CNN-LSTM 混合网络的结构设计与 Keras 实现
3.1 为什么是 CNN 在前、LSTM 在后
小时级天气数据里,相邻小时的温度、湿度变化有很强的局部连续性,CNN 的一维卷积核能在时间轴上滑动,提取这种短时模式,比如“过去 3 小时温度持续上升”。LSTM 则负责把这些局部模式串起来,记住更长时间范围内的趋势,比如“过去 12 小时气压持续下降,可能要变天”。
如果把 LSTM 放在前面、CNN 放在后面,LSTM 输出的隐藏状态序列再经过卷积,也能跑,但常见做法还是 CNN 先做局部特征压缩,再把压缩后的序列喂给 LSTM。这样 LSTM 的输入长度变短、特征更干净,训练更快,过拟合风险也低一些。
3.2 一个能跑通的 Keras 模型定义
下面是一个典型的 CNN-LSTM 结构:一维卷积 + 最大池化 + LSTM + 全连接输出。输入形状是(input_len, n_features),输出是(output_len,)。
from tensorflow.keras import layers, models def build_cnn_lstm(input_len, n_features, output_len): model = models.Sequential([ # 一维卷积:提取局部时间模式 layers.Conv1D(filters=64, kernel_size=3, activation='relu', padding='same', input_shape=(input_len, n_features)), layers.MaxPooling1D(pool_size=2), layers.Conv1D(filters=32, kernel_size=3, activation='relu', padding='same'), layers.MaxPooling1D(pool_size=2), # LSTM:学习长期依赖 layers.LSTM(64, return_sequences=False), layers.Dropout(0.3), # 全连接输出 layers.Dense(32, activation='relu'), layers.Dense(output_len) ]) model.compile(optimizer='adam', loss='mse', metrics=['mae']) return model model = build_cnn_lstm(input_len=24, n_features=X.shape[2], output_len=6) model.summary()参数说明:filters=64是第一层卷积核数量,小时级数据特征不多,64 够用,太多会过拟合。kernel_size=3表示每次看 3 个小时的局部窗口,这是小时级天气里比较稳的选择。pool_size=2把时间步长减半,减少 LSTM 的输入长度。LSTM(64)是隐藏单元数,和卷积核数量匹配即可。Dropout(0.3)放在 LSTM 后面,防止记忆训练集噪声。最后一层Dense(output_len)直接输出未来 6 小时的值,不加激活函数,因为回归任务输出范围不受限。
3.3 训练时的批次、学习率和早停
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau callbacks = [ 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=64, callbacks=callbacks, verbose=1 )batch_size=64是小时级数据常用的起点,样本少就降到 32。EarlyStopping的patience=10表示验证损失连续 10 轮不降就停,并恢复最优权重。ReduceLROnPlateau在验证损失停滞时把学习率减半,避免在局部最小值附近震荡。这两个回调基本是时序预测的标配,能省很多手动调参时间。
4. 训练完不算完:评估、误差分析和调参方向
4.1 回归指标不能只看 MSE
MSE 对异常值敏感,小时级天气里偶尔一次极端温度会让 MSE 很难看。建议同时看 MAE、RMSE 和 MAPE。MAE 反映平均绝对误差,RMSE 放大大误差的权重,MAPE 看相对误差。如果 MAPE 很大但 MAE 很小,说明模型在低值区间误差比例高,比如夜间温度接近 0 时稍微偏一点百分比就很大。
from sklearn.metrics import mean_absolute_error, mean_squared_error import numpy as np y_pred = model.predict(X_test) mae = mean_absolute_error(y_test, y_pred) rmse = np.sqrt(mean_squared_error(y_test, y_pred)) print(f'MAE: {mae:.3f}, RMSE: {rmse:.3f}')注意:如果你的目标做了归一化,评估前要反归一化回原始量纲,否则 MAE 看起来很小,实际误差可能很大。
4.2 按预测步长拆开看误差
未来 1 小时和未来 24 小时的预测难度完全不同。把output_len个步长的误差分别算出来,你会看到误差随时间步长增加而上升。如果第 1 步误差就很大,说明模型连短期模式都没学到,问题在输入特征或网络结构。如果第 1 步还行、后面几步迅速变差,说明 LSTM 的记忆能力不够或训练数据太少。
for step in range(y_test.shape[1]): step_mae = mean_absolute_error(y_test[:, step], y_pred[:, step]) print(f'Step {step+1} MAE: {step_mae:.3f}')这个拆解能帮你判断是该加 LSTM 单元数、加卷积层,还是该换更长的输入窗口。
4.3 调参的优先级顺序
血泪经验是:先调数据窗口,再调网络深度,最后调学习率。input_len从 24 加到 48 往往比加一层 LSTM 更有效,因为小时级天气的日周期是 24 小时,给够一个完整周期,模型才能学到日变化规律。output_len如果设成 24,模型要一次输出全天,难度大,可以改成 seq2seq 结构或逐步预测。卷积核数量从 64 降到 32 有时反而更好,因为小时级数据噪声大,容量太大容易记住噪声。
5. 避坑与排查:小时级天气预测里最容易翻车的 5 个地方
5.1 现象:训练 loss 正常下降,验证 loss 从一开始就很高
原因:训练集和验证集分布不一致。最常见的是按时间切分时,验证集落在某个特殊天气段,比如连续降雨或寒潮,而训练集没有类似模式。另一个原因是归一化参数用了全量数据,验证集信息泄漏,导致验证 loss 看起来异常低或异常高。
解决:检查切分点前后的数据分布,画一下训练集和验证集的目标变量直方图。如果差异大,改用滚动窗口验证,或者确保训练集覆盖至少一个完整年度周期。归一化严格只用训练集统计量。
5.2 现象:模型预测值几乎是一条直线,没有波动
原因:LSTM 输出被全连接层压平了,或者学习率太低、训练轮数不够。更隐蔽的原因是输入特征里目标列的历史值被错误地归一化到接近零,模型学不到变化。
解决:先检查输入里是否包含目标列的历史值,如果有,确认它的归一化没有把方差压没。然后看 LSTM 的return_sequences设置,如果你只需要最后一步输出,return_sequences=False是对的。如果还是直线,把学习率调到 1e-3 或 1e-4,增加训练轮数,观察 loss 是否还在降。
5.3 现象:预测未来 1 小时很准,未来 6 小时完全不能用
原因:模型过度依赖最近一步的输入,LSTM 没有真正学到长期依赖。常见于input_len太短,比如只给了 6 小时历史,模型只能靠最后几小时外推。
解决:把input_len加到 24 或 48,确保覆盖一个完整日周期。如果数据允许,加到 72 小时覆盖三天。同时检查 LSTM 单元数是否太少,64 是起点,可以试 128。另外,输出层直接预测 6 步比逐步递归预测更稳,不要用预测值喂回输入。
5.4 现象:换了台机器或换了 TensorFlow 版本,结果对不上
原因:随机种子没固定,或者 CuDNN 的 LSTM 实现在不同 GPU 上数值精度有差异。CPU 和 GPU 的结果也可能不同。
解决:在代码开头固定 Python、NumPy、TensorFlow 的随机种子。如果要求完全可复现,用 CPU 训练,或者设置tf.config.experimental.enable_op_determinism()。但要注意这会降低训练速度。大作业里固定种子就够了,不必追求 bit 级复现。
import random, numpy as np, tensorflow as tf def set_seed(seed=42): random.seed(seed) np.random.seed(seed) tf.random.set_seed(seed) set_seed(42)5.5 现象:MAE 看起来不错,但画图一看相位完全错位
原因:模型学到了平均趋势,但预测的峰值和谷值时间点偏移了。小时级温度预测里,模型可能把下午最高温预测到了傍晚。这通常是损失函数只优化 MSE,而 MSE 对相位偏移不敏感。
解决:在损失函数里加入一阶差分项,让模型关注变化趋势。或者改用 Huber loss,对异常值更鲁棒。另一个办法是把目标变量做差分,预测变化量而不是绝对值,最后再累加回去。这个技巧在小时级温度预测里很实用。
6. 让预测再稳一点:两个进阶技巧和我的固定习惯
第一个技巧是给模型加一个“日周期特征”。小时级天气有很强的 24 小时周期,但原始时间戳对模型没有直接意义。你可以把小时数转成 sin 和 cos 两个特征加进输入,模型就能感知到“现在是凌晨还是下午”。这个改动很小,但在温度预测上通常能把 MAE 降 5% 到 10%。
df['hour_sin'] = np.sin(2 * np.pi * df.index.hour / 24) df['hour_cos'] = np.cos(2 * np.pi * df.index.hour / 24)第二个技巧是预测多个目标变量。不要只预测温度,把湿度、气压也一起作为输出,多任务学习能让 LSTM 学到更通用的天气表征。输出层改成Dense(output_len * n_targets),然后 reshape。这样即使你只关心温度,湿度作为辅助任务也能提升温度预测的稳定性。
验证方法上,我习惯留出一段连续一周的数据做“盲测”,不参与任何训练和调参,最后只跑一次。如果这一周的预测曲线和真实曲线在趋势和峰值时间上都对得上,这个模型才敢拿去用。如果只是随机切分的测试集好看,实际部署大概率翻车。
最后说一个我的固定习惯:每次改完模型结构或窗口参数,先把训练 loss 和验证 loss 画在一张图上,看两条线的间距。间距大就是过拟合,间距小但都高就是欠拟合。这个图比任何指标都直观。小时级天气预测没有银弹,CNN-LSTM 只是一个起点,数据质量、窗口长度、归一化方式往往比网络结构更决定成败。希望帮到你。
本文还有配套的精品资源,点击获取