很多人用 Python 做时间序列预测时第一反应是选模型。用 LSTM还是 GRU要不要加 CNN要不要双向要不要用 GPU这些问题当然重要但如果一上来就纠结网络层很容易忽略更根本的问题你的数据到底是怎样进入模型的时间序列预测不是普通表格回归。普通表格回归里一行样本通常就是一组并列特征。可时间序列里第 1 个点、第 2 个点、第 3 个点有先后关系。最近的点更靠近未来较早的点可能提供周期背景中间的局部波动可能暗示趋势正在变化。所以本文做的事情可以先用一句话概括用一段按时间排列的历史窗口预测未来的一个连续数值。这张图把任务身份拆开了输入不是一张图片也不是一行普通表格而是一段历史窗口输出不是类别标签而是未来数值应用场景是时间序列预测。弄清这件事之后我们再看 CNN-RNN就不会把它当成一堆网络层的拼接而会把它看成一条数据旅程。为了后面不乱我们先在脑子里放一个小例子。假设我们用最近 12 个月的数据预测第 13 个月。这里的 12 个点不是 12 个可以随便调换的普通特征而是一段按时间发生的历史。第 1 个点比较早第 12 个点离未来最近中间某些点可能形成上升、下降、波峰、波谷这些局部变化。如果你把这 12 个点当成普通表格特征模型也许仍然能训练但它需要自己费力地猜哪些点相邻哪些变化是一段趋势哪些位置更接近未来CNN-RNN 的思路就是把这些事情拆开处理让每个模块做自己擅长的部分。所以接下来不要急着记网络名。我们先看一条时间序列怎样变成模型能学习的样本再看 CNN 和 RNN 分别接过了哪一段工作。一、模型不是直接吃长序列而是吃窗口样本我们用chickenpox.mat里的月度序列做例子。原始数据是一条长长的数值队伍每个点对应一个月份。人看这条序列时可以凭直觉说这段在上升那段有波峰某些月份可能有周期。但模型训练时需要更明确的形式。它需要知道这一次输入是什么答案是什么。这就要用到滑动窗口。在 Python demo 里options{lookBack:12,}lookBack 12的意思是用前 12 个历史点预测下一个点。窗口向后滑一格就得到第二个样本再滑一格就得到第三个样本。如果把前三个样本写成文字会更直观样本1第 1~12 个月 → 第 13 个月 样本2第 2~13 个月 → 第 14 个月 样本3第 3~14 个月 → 第 15 个月把这三个样本放回真实数值里看会更直观图中使用的是chickenpox.mat的真实前 15 个数值。蓝色格子是模型收到的 12 个历史点橙色格子是这一道题的标准答案。第二行并没有重新造一批数据只是把窗口整体向后移动了一个位置。这一步很像把一本长长的时间账本拆成一张张练习题卡。题目是过去 12 个月答案是下一个月。模型训练时不是一次读完整本账本而是反复做这些题卡。KHMD_IMAGE_003到这一步数据已经发生了第一次变化。它不再是一整条长序列而是很多个“历史窗口 → 下一个点”的样本。每个窗口长度为 12目标值是窗口后面紧跟着的真实数值。这里看似只是换了一种数据组织方式其实非常关键。因为后面的 CNN 和 RNN 都不是直接面对原始长序列而是面对这些已经切好的窗口。如果窗口长度选得太短模型可能看不到完整周期如果窗口长度选得太长模型又可能背上太多无关历史训练更难。这里有一个时间序列预测里的大坑测试集不能随机抽。如果随机抽未来片段可能提前混进训练过程。模型看起来很准但那不是真正的未来预测。它只是提前闻到了答案的味道。所以代码默认按时间顺序切分rTrain:0.8,validationRatio:0.15,前 80% 用于训练验证最后一段作为测试集。训练验证部分内部再拿一小段作为验证集用来观察模型有没有开始过拟合。现在模型终于拿到了合格的窗口样本。下一步它要先从窗口里看出局部变化。二、CNN 在这里负责看局部波动很多人以为 CNN 只适合图像。其实 CNN 真正擅长的是用一个小窗口在局部范围内寻找形状。在图像里它可以看边缘、角点、纹理。在一维时间序列里它看的就是局部波动。比如过去 12 个点里可能有一段连续上升一段缓慢下降一个小波峰或者一段短期震荡。它们都是局部形状。KHMD_IMAGE_004你可以把 CNN 想象成一个小印章。它沿着历史窗口移动不是马上判断未来而是先把局部片段里的形状拓下来。Python 版里对应的是Conv1d及其后面的几个层convChannels:16,kernelSize:3,poolSize:2,dropoutProb:0.1,kernelSize表示一次看几个相邻点。默认 3意味着先从很短的局部片段开始看。convChannels表示要提取多少种局部特征。默认 16可以理解成准备了 16 个不同的小印章。poolSize会压缩局部特征让特征对轻微位置变化更稳。dropoutProb用来降低过拟合风险。经过 CNN 之后模型手里的数据已经不是原始窗口而是一组局部波动特征。这里要注意CNN 在本文里不是直接给最终答案的那个人。它更像一个先检查局部形状的前处理工人。它沿着 12 个历史点滑动把局部片段里的变化拓下来这里像短期上升那里像快速下降某个位置可能有比较强的波动。至于这些局部变化连起来到底意味着未来会上升还是下降还要交给后面的 RNN 去判断。换句话说CNN 解决的是“局部像什么”还没有解决“这些局部按时间排起来意味着什么”。但问题还没有结束。因为局部波动只是局部时间序列真正重要的还有“先后关系”。这些特征按什么顺序出现出现在窗口前半段还是后半段意义并不一样。所以下一步需要 RNN。三、RNN 把局部特征按时间顺序读完如果 CNN 是小印章RNN 就像记忆抽屉。CNN 盖出来的特征卡片会按时间顺序一张张交给 RNN。RNN 每读一张就更新一次自己的内部状态。等整段窗口读完它得到的是对这段历史的压缩记忆。KHMD_IMAGE_005为什么不能把这些特征直接平均一下就完事因为顺序本身有意义。一个上升片段出现在窗口开头可能只是很早之前的变化如果它出现在窗口末尾可能说明最近趋势正在抬头。RNN 的作用就是让模型带着“前面发生过什么”的记忆继续往后读。Python 版支持的循环结构比较多networkType:LSTM可选RNN、LSTM、GRU、BiLSTM、BiGRU。默认用 LSTM通常比较稳。GRU 参数更少训练可能更轻。双向版本会在给定的历史窗口内部从两个方向读不是偷看测试集未来。另一个重要参数是rnnHidden:32它可以理解为记忆容量。太小装不下足够信息太大模型又可能记住训练集里的偶然噪声。现在一条样本已经经历了两次加工原始历史窗口先变成局部波动特征局部特征再被 RNN 读成一份历史记忆。这也是为什么这里要用混合模型。如果只用 CNN它看局部很方便但对先后顺序的表达会弱一些如果只用 RNN它能按顺序读但需要直接从原始数值里自己摸索局部波动。CNN-RNN 的分工就是CNN 先减轻局部特征提取的负担RNN 再把这些特征按顺序串起来。混合模型的价值不在于名字更长而在于它把问题拆成了两件更清楚的事先看局部再读顺序。最后一步是把这份记忆变成未来数值。四、一条数据在 Python 模型里的完整旅程我们现在把整条路串起来。KHMD_IMAGE_006一段历史窗口进入 CNNCNN 负责提取局部波动。局部特征进入 RNNRNN 负责理解顺序关系。最后线性输出层把这份历史记忆变成一个未来数值。Python 版还需要在 CNN 和循环层之间交换一次维度。把默认参数下的完整尺寸变化摊开如下KHMD_IMAGE_007图中的B表示一个批次里有多少条窗口样本。窗口先从(B,12)增加通道维经过卷积和池化后变成(B,16,6)随后交换成(B,6,16)让循环层把长度 6 当作时间顺序、把 16 当作每一步的特征最后 LSTM 用 32 个数概括历史全连接层为每条样本输出一个预测。Python 单步预测的调用是fore_data,fore_data_train,fore_data_val,model,infoFunCNNRNNsForecast(data,options)fore_data是测试集预测值fore_data_train是训练集预测值fore_data_val是验证集预测值。model是训练好的 PyTorch 模型info里保存训练过程、数据划分和评价指标。这里的封装函数不只是“跑一个模型”。它实际上把读取数据后的主要流程都包起来了造窗口、按时间切分、归一化、搭建 CNN-RNN、训练、预测、画图、算指标。如果deviceSel设置为auto程序会优先使用可用 GPU没有 GPU 时会自动回到 CPU。五、模型是怎么学会的现在还有一个问题模型第一次训练时并不知道什么是“有用的局部波动”也不知道哪些记忆该保留那它是怎么学会的可以把训练过程想象成反复做题。每个窗口都是一道题真实的下一个点就是标准答案。模型先根据当前参数给出一个预测值然后程序拿预测值和真实答案比较。差得多说明这次做错得多差得少说明方向比较接近。接下来程序会调整 CNN、RNN 和最后输出层里的参数。慢慢地CNN 会更容易提取有用的局部波动RNN 会更倾向于记住对未来有帮助的顺序信息最后的输出层也会学会把这份历史记忆换成一个合适的未来数值。这也是为什么训练集、验证集、测试集要分开。训练集像平时练习验证集像做题过程中用来观察有没有死记硬背的小测验测试集则更像最后拿一段没见过的后续时间来考试。理解这一点后再看结果图才有意义。我们看的不是“模型跑起来了没有”而是它是否真的抓住了趋势误差是否集中在特殊点以及测试集表现是否明显落后于训练集。六、单步预测先看模型有没有学到基本规律理解了数据怎样走过模型再来看结果图就更容易了。单步预测中每个测试样本都使用真实历史窗口作为输入然后预测下一个点。它适合先判断模型是否掌握了基本走势。KHMD_IMAGE_008读这张图时不要只看一个 R²。先看预测曲线是否跟住真实曲线的整体起伏再看波峰波谷附近是否滞后最后看相对误差是不是集中在少数点。本次 Python 单步预测测试集 R² 约为 0.8153MAE 约为 99.63RMSE 约为 135.71。也就是说在单步预测设定下模型对测试段有比较稳定的跟踪能力。不过单步预测还不是最难的场景。真实使用时我们经常想预测未来一整段。这时问题会变得明显更难。七、多步递归预测误差会回到输入里预测未来第一个点时输入窗口里还有真实历史。可是预测第二个未来点时第一个未来点的真实值并不知道。怎么办只能把模型刚刚预测出来的第一个点放回窗口再继续预测第二个点。这就是多步递归预测。先把它和单步预测并排比较KHMD_IMAGE_009左边的单步预测每次都拿到真实历史适合检验模型是否学会基本规律右边从第二步开始会使用自己的预测因此更接近未知未来也更容易累积误差。两者可以使用同一类 CNN-RNN真正不同的是输入窗口来自哪里。KHMD_IMAGE_010多步递归更接近真实外推但它也有天然难点误差会回流。第一个点如果偏了一点这个偏差会进入第二步的输入第二步再偏一点后面就可能越滚越歪。Python 多步版调用fore_data,fore_data_train,fore_data_val,model,infoFunCNNRNNsForecastMul(data,options)为了让多步递归更稳Python 版加入了“基准值 模型残差”的思路。对应参数是baselineMode:seasonal,seasonPeriod:12,residualScale:1,baselineMode seasonal表示参考上一周期同位置。月度数据里seasonPeriod 12就是上一年同期。residualScale控制模型修正量的力度。它可以设为固定数值也可以设为auto让程序根据验证集自动估计。直观理解就是不要让模型在多步递归时完全放飞而是给它一根防漂移绳。Python 多步递归结果如下。KHMD_IMAGE_011本次实跑中Python 多步递归预测测试集 R² 约为 0.5008MAE 约为 172.15RMSE 约为 224.63。这个结果没有单步预测那么轻松但它更接近真实未来预测的难度。对多步递归来说我们更应该关注曲线有没有快速漂移而不是只追求一个漂亮指标。八、“一行代码”完成 CNN-RNN 时间序列预测原理讲明白之后真正自己动手时麻烦才刚刚开始。你需要把一条长序列切成历史窗口需要按时间先后划分训练集、验证集和测试集还要避免归一化器提前看到测试段。CNN 输出以后需要转换张量顺序才能交给 RNN、LSTM 或 GRU。训练结束后还要恢复最佳权重、反归一化、计算指标、画预测曲线和相对误差图。任何一个环节漏掉程序都可能报错即使程序没有报错数据切分或尺寸处理不正确结果也可能不可信。为了让大家把精力放在自己的数据和模型本身而不是反复拼接这些固定流程我把单步预测过程封装成了FunCNNRNNsForecast函数。设置好数据和参数后真正启动模型只需要一行fore_data,fore_data_train,fore_data_val,model,infoFunCNNRNNsForecast(data,options)这一行代码背后会依次完成滑动窗口构造、时间顺序切分、训练数据归一化、CNN-RNN 网络搭建、模型训练、提前停止、最佳权重恢复、训练集/验证集/测试集预测、反归一化、指标计算和结果绘图。函数的两个输入也很直接data按时间先后排列的一维 NumPy 数组。本文中使用的是chickenpox.mat里的月度数据。options窗口长度、数据划分、网络结构、训练过程、归一化、运行设备和绘图输出的设置。五个输出分别是fore_data测试集预测结果。fore_data_train训练集预测结果。fore_data_val验证集预测结果。model训练好的 PyTorch 模型。info训练、验证、测试的时间索引归一化器评价指标预测结果以及训练损失记录。以本文的鸡痘月度序列为例完整调用代码如下importnumpyasnpfromscipy.ioimportloadmatfromFunCNNRNNsForecastimportFunCNNRNNsForecast matloadmat(chickenpox.mat)datanp.asarray(mat[data],dtypenp.float64).reshape(-1)# 2. 设置窗口与数据划分options{lookBack:12,# 用前 12 个时间点预测下一个点rTrain:0.80,# 80% 窗口样本用于训练验证其余用于最终测试validationRatio:0.15,# 从训练验证数据中再划出 15% 做验证shuffle:True,# 只打乱训练批次不改变时间顺序切分seed:42,# 固定随机种子使结果尽量可复现# 3. 设置 CNN-RNN 网络networkType:LSTM,# 支持 RNN、LSTM、GRU、BiLSTM、BiGRUconvChannels:16,# 卷积通道数越大能学习更多局部波动模式kernelSize:3,# 每次观察 3 个相邻时间点必须为正奇数poolSize:2,# 池化窗口设为 1 相当于不池化rnnHidden:32,# 循环层隐藏单元数dropoutProb:0.1,# Dropout 概率用于缓解过拟合# 4. 设置训练参数epochs:200,# 最大训练轮数可能因为早停提前结束batchSize:32,# 每个小批次使用的窗口数量lr:0.005,# 初始学习率太大易震荡太小收敛慢weightDecay:1e-4,# L2 正则强度gradClip:1.0,# 梯度裁剪阈值patience:30,# 验证损失连续不改善 30 轮就停止learnRateSchedule:piecewise,# 分段降低学习率也可设置为 nonelearnRateDropPeriod:50,# 每 50 轮降低一次学习率learnRateDropFactor:0.5,# 每次将学习率乘以 0.5# 5. 设置归一化、绘图和运行设备mapflag:True,# 自动归一化与反归一化只用训练数据拟合参数figflag:True,# 自动绘制并保存结果图deviceSel:auto,# 优先使用 GPU不可用时自动切换到 CPU}# 6. 一行代码开始训练与预测fore_data,fore_data_train,fore_data_val,model,infoFunCNNRNNsForecast(data,options)看起来参数不少但它们各自只回答一个很具体的问题。先用一张表把这些参数归位KHMD_IMAGE_012这张表不是让你一次把所有参数都改掉而是帮助你根据症状先找到参数组。窗口和划分决定数据怎样进入模型CNN、RNN 决定模型容量训练参数决定模型怎样学习多步稳定参数只在递归漂移时重点处理。lookBack、rTrain和validationRatio管的是样本怎么造、数据怎么分。这里lookBack12表示每道题都用前 12 个月预测下一个月。rTrain0.80不是说 80% 全部拿去训练而是先把前 80% 的窗口样本划成“训练验证数据”剩下 20% 留作最终测试然后再从这 80% 里拿出 15% 做验证集。训练集负责更新网络参数验证集负责观察模型是否开始过拟合测试集要等训练结束后才第一次参与评估。networkType、convChannels、kernelSize、poolSize和rnnHidden管的是模型容量。convChannels越大CNN 能学习的局部波动模式越多kernelSize越大CNN 一次查看的相邻时间点越多poolSize越大中间特征压缩越强rnnHidden越大循环层的记忆容量越强。它们不是越大越好数据量不够时容量太大反而可能让训练集很好、测试集变差。epochs、lr、batchSize和patience管的是训练节奏。学习率太大损失可能上下震荡学习率太小训练会很慢。最大训练轮数只是上限真正是否提前停止要看验证损失是否连续多轮不改善。mapflagTrue会自动完成归一化和反归一化但归一化器只从训练数据中学习避免模型提前“偷看”验证集和测试集。Python 版可以使用哪几种 RNN本项目 Python 版的循环网络模块一共支持RNN、LSTM、GRU、BiLSTM和BiGRU五种结构通过networkType参数即可切换options[networkType]RNN# 普通循环网络options[networkType]LSTM# 单向 LSTM本文默认设置options[networkType]GRU# GRU门控结构相对精简options[networkType]BiLSTM# 双向 LSTMoptions[networkType]BiGRU# 双向 GRU实际使用时五行只保留一行即可。LSTM 是本文演示采用的默认结构GRU 的结构更精简双向结构会从两个方向理解已经给定的历史窗口但参数量和计算量也会增加。哪一种更好不能只看名称需要在相同的数据划分下比较验证集和测试集表现。怎样切换到多步递归预测如果要连续预测一整段未知未来只需换用FunCNNRNNsForecastMul并增加三个多步稳定参数fromFunCNNRNNsForecastMulimportFunCNNRNNsForecastMul options.update({baselineMode:seasonal,# 使用上一周期同位置作为预测基准seasonPeriod:12,# 月度数据中12 表示上一年同期residualScale:1,# 01 数值或 auto})fore_data,fore_data_train,fore_data_val,model,infoFunCNNRNNsForecastMul(data,options)多步版的输入输出与单步版保持一致区别在于测试段采用递归滚动前一步预测值会进入下一步窗口。baselineModeseasonal会给预测拉一根周期基准线residualScale越接近 0结果越贴近基准、通常更保守越接近 1越充分采用模型修正也更容易放大递归误差。Python 版还可以把它设置为auto让程序根据验证集从多个候选值中自动选择。函数运行结束后你不必再手工整理一堆零散结果。程序会自动给出训练集、验证集和测试集的 MAE、MSE、RMSE、MAPE、R² 和相关系数同时生成收敛过程、训练集预测、验证集预测、测试集预测、相对误差以及指标对比图并保存到figure文件夹。KHMD_IMAGE_013上图用来比较训练集、验证集和测试集之间的差距。如果训练集明显好于后两组模型可能已经过拟合或者后续时间段的规律发生了变化。KHMD_IMAGE_014损失曲线用来观察模型是否真正收敛以及验证损失是否在后期停止改善。它和测试指标要放在一起看不能只凭训练损失下降就判断模型有效。最后把常见的三类异常结果放在一起KHMD_IMAGE_015如果训练集也不好先查数据、窗口和学习率如果训练集很好而验证、测试差先查过拟合和时间分布变化如果单步正常但多步漂移先查周期基准、residualScale和递归跨度。这个顺序通常比直接增加网络层更有效。至此本文中的月度序列已经走完完整旅程先被切成“前 12 个月预测第 13 个月”的窗口样本再由 CNN 提取局部波动由 LSTM 汇总时间顺序最后输出一个未来数值。前面讲的是这条流水线为什么成立这一节解决的则是如何把它真正跑起来。如何获取完整代码本文使用的完整 Python 代码包括单步预测核心函数、多步递归预测核心函数、两个演示脚本、鸡痘月度示例数据、评价函数和依赖说明。代码包含详细的参数说明和使用示例可以自动生成训练过程、单步预测、多步递归预测、相对误差和指标对比图并计算 MAE、MSE、RMSE、MAPE、R²、相关系数等评价指标。完整代码和详细说明可以在以下官网页面获取https://www.khsci.com/docs/index.php/2026/07/19/cnn-rnn-forecast-python/