简介面向Python入门者的小时级天气预测实战代码基于CNN-LSTM混合网络实现精细化预报。项目融合卷积神经网络与长短期记忆网络的互补优势可处理气象时序数据中的局部特征与长期依赖覆盖数据展示、模型构建、训练评估、多模型对比等完整流程适合课程设计、期末大作业或深度学习时序预测的进阶练习。压缩包共26个文件包括8个Python源码、11张JPG与4张PNG可视化图、1个CSV气象数据文件及docx说明手册整体仅1.82MB轻量易用目录结构清晰便于按需查看。可视化图直观呈现CNN-LSTM、LSTM、GRU、RNN、BiLSTM等模型的预测效果与损失曲线便于横向对比不同网络结构的表现CSV数据可直接用于复现实验docx说明文档从环境配置到代码结构逐步讲解利于小白快速上手。目前已有235人学习适合希望在短时间内掌握深度学习时序预测流程的入门用户。1. CNN-LSTM小时级天气预测先搞懂这个网络在学什么做农业气象服务、光伏功率预测或者户外活动风险管理的人常被同一个问题卡住客户要的是“明天下午三点大概多少度”不是全天平均气温。数值天气预报的网格太粗ARIMA这类纯统计方法又抓不住冷空气过境的短时突变。CNN-LSTM混合网络是小时级天气预测里最常见的落地baseline卷积层捕捉局地短时变化LSTM延续时间趋势整套东西用python写出来也就上百行。这里要拆解的是从零跑通一套天气预测python源码的完整路径包括网络搭建、数据清洗、训练调参和踩坑记录。适合python入门不久、想拿真实气象数据跑出第一个预测模型的人也适合已经跑过别的时序模型、想评估CNN-LSTM性价比的熟手。2. CNN-LSTM混合网络为什么适合小时级天气预测原理先立住再跑通最小python源码2.1 局地气象数据的两个结构特征决定网络怎么搭小时级气象预测的输入一般是“过去N个小时、每个小时一组观测值”这组观测值包含温度、湿度、气压、风速、风向、降水量可能还有站点编号。输出则是未来某个时刻的温度或者未来多个小时的一条曲线。这段序列有两个互相叠加的结构时间依赖和局部突变。时间依赖很好理解今天14点的气温和昨天14点、前天14点高度相关这是日周期在起作用冬季持续低温则是更大尺度的天气过程。LSTM天生为这类依赖设计门控结构能让信息跨越多步传递这也是它在气象时序任务里比普通全连接网络更讨喜的原因。但纯粹用LSTM对局部突变反应偏慢。冷空气过境可以在两三个小时内让气温下降6到8度这种模式在时间轴上表现为一个短促的“跳变”用RNN去表达模型必须学习复杂的时序状态转移规则参数和样本量都不够时容易学歪。CNN的卷积核是共享权重的局部算子天生能捕捉这种短窗口内的变化结构而且因为权重共享训练参数远少于全连接层不容易过拟合。混合网络的分工因此很明确先用一维卷积在时间轴上扫描提取局部变化模式——可以把卷积核理解成在找“降温事件”“湿度突变”这类模板卷积输出的序列再交给LSTM去建模这些局部模式如何组合、如何随时间延续和衰减。这种结构贴合了气象序列的实际生成过程而不是靠堆参数硬拟合。2.2 最小可跑通的CNN-LSTM源码骨架PyTorch版无论标题里提到的源码包是从什么渠道拿到的我都会先劝你按工程上最常见的写法亲手复现一套能直接落地的骨架。即便下载包里的实现细节不同主干流程——卷积接LSTM接全连接——不会有本质差异。你能跑通一份自己敲出来的代码遇到问题才有排查的底气。这里用PyTorch写原因有三API设计直观、python报错信息友好、环境配置简单。无论你是系统装的python还是在vscode里配好的python环境版本在3.8以上都能跑。import torch import torch.nn as nn class CNNLSTMModel(nn.Module): def __init__(self, n_features, n_hidden64, n_output1, seq_len24): super(CNNLSTMModel, self).__init__() # 一维卷积输入(batch, n_features, seq_len) self.conv1 nn.Conv1d(in_channelsn_features, out_channels32, kernel_size3, padding1) self.relu nn.ReLU() # LSTM输入特征维度是卷积输出信道数32 self.lstm nn.LSTM(input_size32, hidden_sizen_hidden, num_layers1, batch_firstFalse) # 全连接输出 self.fc nn.Linear(n_hidden, n_output) def forward(self, x): # x: (batch, features, seq_len) x self.conv1(x) x self.relu(x) # 转成(seq_len, batch, channels)喂给LSTM x x.permute(2, 0, 1) lstm_out, (h_n, c_n) self.lstm(x) # 取最后一个时间步的隐状态 out self.fc(lstm_out[-1]) return out这份代码的逻辑拆开看。Conv1d的in_channels是特征数out_channels设成32含义是卷积核在时间轴滑动把原始特征组合成32种局部时间模式。padding1是为了让卷积不缩短序列长度这样后面LSTM收到的每个时间步都有完整的卷积响应。ReLU负责去掉负响应让模型只保留“确实出现了某种模式”的证据。permute这一步是全篇最容易疏忽的地方——Conv1d和LSTM对序列维度的约定恰好错位Conv1d期望时间维在最后LSTM期望时间维在最前不转置会直接报维度错误而且报错信息对新手极不友好需要在理解层面对齐。LSTM设置num_layers1。气象序列的有效依赖长度有限日周期24小时一场天气过程三五天单层LSTM的隐藏状态容量已经够用。堆第二层会让参数显著增加但在样本量只有几万条时收益很小过拟合风险反而变大。全连接最终把隐藏状态压成输出n_output1意味着只预测未来一个时刻的温度。如果想一次预测未来24小时把n_output改成24训练数据里把未来24小时的真实温度曲线作为标签即可模型结构和训练循环都不用动。2.3 拿到源码后先改的三个参数无论你从哪里拿到标题对应的CNN-LSTM源码先别急着训练把下面三个参数弄清楚。第一个是seq_len历史回看窗口。小时级天气用24比较稳妥基本覆盖一个完整日周期。如果只关心气温24够了如果还要预测气压的连续变化可以放到48。第二个是n_hiddenLSTM隐藏单元数。数据集规模在一年到两年小时数据约一两万条样本时64到128之间选一个即可再增大收益有限。第三个是n_output预测步长。预测未来1小时误差最小但业务价值低且容易把模型带进“复制学习”的坑预测未来3到6小时模型被迫学习趋势而不是复制当前值能明显减少预测滞后问题这个坑后面专章讲。提示先把模型在CPU上跑通一遍再上GPU。天气预测数据量不大CPU也能训练GPU主要加快调参迭代。3. 小时级气象数据的预处理与样本构造数据怎么喂模型就怎么长3.1 数据源选型与字段筛选的工程判断数据是模型的天花板。CNN-LSTM再精巧喂进去的序列时间戳错了输出也只是把错误信息“预测”得更平滑。先选数据源。最理想的是当地气象站的小时级观测记录这类数据可以从气象数据平台下载历史归档导出CSV。其次是能按小时拉取的历史天气查询接口缺点是字段少、粒度粗很多只有温度和天气现象没有气压和湿度。日值数据不能用——小时级预测的输入必须是小时间隔的连续序列日值插值出来的小时数据没有新增信息只会让模型学到条状伪影。字段上要做减法。我一般保留六项温度、相对湿度、气压、平均风速、风向正弦、风向余弦降水量单独留作晴雨分类的标签。风向不用原始角度直接喂网络因为角度0度和360度是同一个方向数值上却差360模型会误以为差异巨大。转成sin和cos两个分量后方向信息变成连续平滑的编码。降水量观测特性特殊大量时段为零、少数时段突然变大直接做回归很容易被模型忽略常见做法是单独建一个晴雨分类分支或者用它作为训练时的样本权重依据。时区统一也是容易翻车的地方。数据平台给的是UTC你要预测的是本地时区的小时天气如果不先把时间戳转换过去序列的日周期会整体偏移。模型会把“当地中午的高温”和“UTC早晨的低温”对齐训练loss明面上下不去实际上是对齐错了位置。3.2 缺失值、异常值与归一化先处理脏数据再谈建模小时级数据最常见的问题是缺测。自动站设备故障、通信中断都会导致连续几小时没有记录。处理方式按连续缺测时长分两档缺测不超过3小时用前后值线性插值超过3小时直接删除该段连续性不佳的时间区间。插值太多会伪造出一段平稳变化的数据模型学到了但真实场景里不存在这是数据里掺了水分。import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler df pd.read_csv(weather_hourly.csv, parse_dates[time], index_coltime) df df.sort_index() # 1. 只对连续缺测3小时以内的做线性插值 df df.interpolate(methodlinear, limit3) # 2. 剔除仍含缺失值的区间 df df.dropna() # 3. 异常值裁剪均值加减3倍标准差 mean df.mean() std df.std() df df.clip(lowermean - 3 * std, uppermean 3 * std) # 4. 归一化MinMax 保留每个特征的相对比例 scaler MinMaxScaler() data scaler.fit_transform(df)异常值用clip而不是直接删除是因为气象记录里有些极端值比如罕见寒潮本身是真实的删掉会丢失重要分布信息clip只是把明显超出物理合理范围的观测拉回边界。归一化选择MinMax而不是Z-score湿度是百分数、温度是负值、风速右偏Z-score会让模型对不同特征施加隐含权重MinMax则把每个特征等比映射到0到1区间。这里有一条必须遵守的约束归一化参数scaler的min、max只能在训练集上fit验证集和测试集只能调用transform。如果先对整个数据集归一化再切分测试集的统计信息已经渗入训练过程模型在测试集上的误差会虚低这就是数据泄漏。我见过不少次“测试集效果很好、上线即失灵”的项目多半就栽在这一处。3.3 滑动窗口样本构造与时间切分边界预处理后的数据是二维数组形状是(time_len, n_features)。模型不能直接吃原始序列需要切成“过去lookback小时的特征对应未来horizon小时的标签”这样的监督样本。def make_samples(data, lookback24, horizon3): X, y [], [] total data.shape[0] for i in range(total - lookback - horizon 1): X.append(data[i : i lookback]) # 假设预测目标温度在第一列 y.append(data[i lookback : i lookback horizon, 0]) X np.array(X) # (samples, lookback, n_features) y np.array(y) # (samples, horizon) return X, y samples, labels make_samples(data, lookback24, horizon3) print(f样本数: {samples.shape[0]}, 单个样本形状: {samples.shape[1:]})lookback决定每个样本看多长的历史horizon决定要预测未来多远。窗口长度有两个边界坑。一是末尾不足一个窗口的样本会被丢弃数据量大时无所谓数据量小就要考虑用左侧padding保持样本数。二是样本之间高度重叠连续两个样本共享了23个小时的输入导致相邻样本强相关模型在测试集上面对的是“自己见过的历史模式的重组”误差天然乐观。缓解方法是在验证集和训练集之间留出几天完全不参与训练的缓冲期。切分方式上天气预测必须按时间顺序切前70%做训练、中间15%做验证、最后15%做测试。随机切分在天气预测里是标准的数据泄漏方式因为未来时段的真实天气过程会和过去样本混进同一批训练数据。如果数据跨多年建议再做滚动验证用第1年训练、第2年测试再用前2年训练、第3年测试依次类推观察误差是否逐年漂移。3.4 多站点数据如何塞进CNN如果项目需要预测一个区域内多个站点的天气有个简单的做法把所有站点同一时刻的特征按固定顺序拼接把特征维度从单站点的n_features扩展成n_features乘以站点数。这样Conv1d的in_channels会变大卷积核在特征维度上自动建立站点间的线性组合关系——可以理解成卷积核在比较“邻近站点的温差和风速差异”。这个空间维度不需要专门构造复杂的图结构对入门项目足够。等后续数据量大了再考虑换成图卷积或者Transformer那套那是后话。4. CNN-LSTM的训练与调参从能跑通到误差降下来4.1 最小训练循环MSE损失、Adam优化器、早停与模型保存模型定义完、样本构造完就进入训练环节。很多入门者卡在这里不知道跑几个epoch、不知道loss降多少算合理。先给一份完整的训练代码骨架。import torch from torch.utils.data import TensorDataset, DataLoader import torch.nn as nn # samples, labels 来自 make_samples 的输出 X torch.tensor(samples, dtypetorch.float32).permute(0, 2, 1) # (samples, features, lookback) y torch.tensor(labels, dtypetorch.float32) # 按时间顺序切分不用随机切分 idx_split int(len(X) * 0.8) X_train, X_val X[:idx_split], X[idx_split:] y_train, y_val y[:idx_split], y[idx_split:] train_loader DataLoader(TensorDataset(X_train, y_train), batch_size64, shuffleTrue) val_loader DataLoader(TensorDataset(X_val, y_val), batch_size256, shuffleFalse) model CNNLSTMModel(n_featuresX.shape[1], n_hidden64, n_outputlabels.shape[1], seq_lenX.shape[2]) optimizer torch.optim.Adam(model.parameters(), lr1e-3) loss_fn nn.MSELoss() best_val float(inf) patience, no_improve 10, 0 for epoch in range(100): model.train() train_loss 0.0 for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) loss loss_fn(pred, yb) loss.backward() optimizer.step() train_loss loss.item() * xb.size(0) train_loss / len(train_loader.dataset) model.eval() val_loss 0.0 with torch.no_grad(): for xb, yb in val_loader: pred model(xb) val_loss loss_fn(pred, yb).item() * xb.size(0) val_loss / len(val_loader.dataset) if val_loss best_val: best_val val_loss torch.save(model.state_dict(), best_cnnlstm.pth) no_improve 0 else: no_improve 1 if no_improve patience: print(fepoch {epoch}: early stop, best val loss {best_val:.4f}) break print(fepoch {epoch}: train {train_loss:.4f}, val {val_loss:.4f})训练循环里有几个地方要解释清楚。X必须permute成(samples, features, lookback)这是Conv1d的输入约定。MSE作为损失函数会惩罚大误差对温度预测这类连续回归任务足够但验证时要同时打印MAE因为MAE的单位是摄氏度业务人员更容易理解。验证集batch给256是为了减少评估抖动推理没有梯度开销很小。早停参数patience10的含义是连续10个epoch验证loss没有刷新最优值就停止训练并保留历史最优模型。天气数据噪声不小val_loss往往在训练早期就触底再往后只是过拟合早停是防止后期翻车最廉价的手段。每次达到更优验证loss就覆盖保存一次模型确保最终拿到的是泛化最好的那版权重。4.2 真正影响误差的三个参数实际调参不需要动几十个参数下面这个速查表覆盖了主要旋钮。参数建议起点现象与调整方向lr1e-3loss震荡则降到3e-4下降过慢可试1e-2但要盯验证集lookback24日周期不明显时可试48误差上升则回退n_hidden64过拟合风险低时可加到128LSTM层数1数据量低于5万样本时不建议堆两层batch_size64数据量大可加到128小数据集用32三个必须调的是学习率、lookback、n_hidden。学习率从1e-3出发如果训练日志里loss在震荡从3e-4往下调如果loss下降太慢尝试1e-2但要在最初几个epoch盯紧验证集。lookback从24起步预测温度时24已经覆盖日周期加到48或72通常只在增加计算量预测气压或风速这类持续性更强的变量时长窗口收益更明显。n_hidden在64附近试val_loss在训练后期持续下降但训练loss迅速压低说明模型在背数据而不是学规律此时不要加隐藏单元反而要加早停强度。4.3 验证策略按小时拆误差看到模型在哪里翻车只跑出一个整体MAE等于什么都没看见。天气预测的误差不是均匀分布的夜间低温时段、午后高温时段、降雨前的气压跳变时段误差相差可能超过一倍。把预测结果按小时聚合统计误差是最快定位弱点的办法。下面的示例假设你已经把验证集的样本按原时间顺序排列并从时间戳中抽取了每个样本对应的hour值代码省去了构造过程聚焦在误差聚合逻辑上。import numpy as np preds, trues, hours [], [], [] for xb, yb in val_loader: with torch.no_grad(): p model(xb).numpy() preds.extend(p[:, 0]) trues.extend(yb.numpy()[:, 0]) # hours 与 preds 按顺序一一对应 mae_by_hour {} for hour in range(24): hour_mask [h hour for h in hours] if sum(hour_mask) 0: continue mae_by_hour[hour] np.mean(np.abs( np.array(preds)[hour_mask] - np.array(trues)[hour_mask]))拆完小时误差你会看到凌晨2点到5点的MAE通常偏高。原因是夜间逆温层让气温变化规律性强但幅度小模型对持续低温的敏感度不够。另一个观察维度是天气状态晴好样本占多数模型优化主力全在气温的平滑日变化上一旦测试集里雨天比例上来误差迅速扩大。这种按板块拆误差的思路在python量化交易策略代码里也很常见——按行业板块拆策略收益回测别让平均业绩骗了你。天气预测同理整体指标好看不等于每个时段都可靠。5. CNN-LSTM天气预测避坑5个让人血压升高的常见问题5.1 预测曲线滞后模型学会了复制粘贴现象把预测值和真实值画在同一张图上形状大致对但整条曲线比真实曲线往后平移了一个小时预测的高温顶点总比实际晚到一小时。原因当horizon1时下一小时气温与当前小时气温的自相关系数极高模型发现最优策略就是输出当前值反正loss很小。这种“复制学习”在气温这类强自相关序列里特别普遍。解决把预测步长horizon从1改成3或6模型就没办法靠复制糊弄或者对训练目标做差分预测t1时刻减去t时刻的差值。我常用的是直接加大horizon因为差分在还原预测值时误差会累积业务侧也不好解释。5.2 训练集误差很低测试集直接翻车现象训练集MAE0.6度测试集MAE3.9度怎么调都压不下来甚至调整学习率也没用。原因最常见的是数据切分方式错了。随机切分会把同一周的样本同时送进训练集和测试集模型等于在考自己背过的题另一种常见原因是测试集跨越了不同季节模型从未见过冬天低温模式自然给不出合理外推。解决严格按时序切分并在训练集与验证集之间留几天缓冲带如果数据跨多个年份做滚动时间窗验证。测试误差不可能低于模型真正吃过的分布要求测试集的季节构成和实际业务使用时段一致才能拿到可信的误差预估。5.3 雨天预测误差爆炸晴天还挺好现象模型总体MAE好看业务方反馈“一到下雨就完全不能用”查看分天气误差后发现雨天误差是晴天的两倍以上。原因降水天气在样本中占比可能只有15%MSE被晴天样本主导模型对降水相关的气压、湿度突变成“选择性失明”。回归任务天然不喜欢少数类样本损失函数被多数类牢牢控制。解决构造DataLoader时按天气类型做分层采样让每个batch里雨天比例不低于总体占比更简单的是对样本损失按天气类型加权给雨天样本的损失乘1.5到2.0逼模型把容量花在极端条件上。加权重之后雨天误差会明显下降代价是晴天误差略微升高这个权衡要结合业务场景来决定。5.4 别人源码跑得好你复现就拉胯现象同一份网络结构别人报告MAE 1.2度自己复现跑出2.3度甚至翻倍开始怀疑显卡或者python版本。原因所谓“同一份源码”数据侧细节根本不透明。有的版本对缺测做了插值有的只删不补有的归一化用min-max有的用z-score有的训练集包含了测试集的时间段。源码能复现的是网络结构数据前处理才是各家模型效果差异的真正黑匣子。解决固定随机种子把预处理每一步写成带日志的函数记录训练、验证、测试集各自的真实时间范围。没有这些记录调参就是在跟空气博弈。这是老手的血泪经验也是项目交接时最有价值的资产。5.5 推理时维度报错又是batch维度的玄学现象训练一切正常单独加载一个样本去predict报shape mismatch或permute维度越界。原因训练数据构造时是三维的(samples, features, lookback)推理时只传一个样本形状变成二维(features, lookback)Conv1d和LSTM的维度约定全乱套了。解决写一个专用的predict函数统一在外面包一层batch维度再交给模型处理。def predict_one(model, sample): # sample: (n_features, lookback) 或 (lookback, n_features) if sample.ndim 2: sample sample.unsqueeze(0) # 变成(1, features, lookback) model.eval() with torch.no_grad(): pred model(sample) return pred.squeeze().numpy()这个函数把单样本变成带batch维度的四维张量预测后再用squeeze去掉多余的维度保证训练和推理路径完全一致。把predict固定成统一入口之后不管是批处理还是单条请求都走同一个函数能少踩很多维度错位的坑。提示代码里唯一的“玄学”变量是随机种子其他问题都能通过打印中间张量的shape定位。6. 让模型持续可用滚动预测、定时重训与误差审计小时级天气预测最终要落地到业务模型得能输出未来24小时的逐小时温度曲线。滚动预测是最常见的做法用过去24小时的真实观测作初始输入预测出第1小时的值然后把这个预测值补到输入序列尾部丢掉最老的一个观测再预测第2小时一路滚下去。def rolling_forecast(model, initial_series, steps24): # initial_series: (features, lookback) 已按训练时顺序排列并归一化 model.eval() input_seq initial_series.clone() forecasts [] with torch.no_grad(): for _ in range(steps): pred model(input_seq.unsqueeze(0)) # (1, features, lookback) forecast pred[0, 0].item() forecasts.append(forecast) # 将预测值作为新一步的输入并丢弃最早时刻 new_frame input_seq[:, 1:].clone() new_frame torch.cat( [new_frame, torch.full((1, 1), forecast)], dim-1 ) input_seq new_frame return forecasts滚动预测有个明显弱点误差会随步长累积。第一步误差0.5度第二步会把有误差的预测值当输入放大的速度取决于序列的自相关性。因此部署时要区分变量温度、湿度可以滚动多步降水不建议滚动因为分布极度不均衡滚动很容易把少量真实降水信号抹平。降水业务单独做晴雨二分类更实际别在数值回归上死磕。模型跑通之后还要解决“新数据不断进来、旧模型逐渐失效”的问题。天气统计特性随季节变化夏季训练出来的模型到冬季误差会明显扩大。常见做法是每月触发一次增量重训用最近三个月数据在已有权重基础上多训几个epoch保留验证集误差日志。更稳妥的方式是设定阈值每周跑一次固定回测集如果连续四周MAE比历史中位数高出30%以上就触发全量重训。最后一步是误差审计。我每周生成一份简单报表本周预测的逐小时MAE、晴天和雨天分别的平均误差以及和上周的对比。这个习惯帮我熬过很多次“模型不是好好的吗怎么这个月不好用”的困局——绝大多数时候不是模型坏了而是数据分布漂移重新缩放一下归一化参数就能救回来。我个人的习惯是每次改动任何一处预处理哪怕只是把插值上限从3小时改成4小时都要重跑一遍固定回测脚本确保改进来自模型本身而不是数据侧悄悄变化。把训练脚本和预处理脚本绑成一体你的CNN-LSTM模型才有资格从demo变成能长期跑的服务。希望帮到你。本文还有配套的精品资源点击获取