简介:基于循环神经网络(RNN)的Python时间序列预测代码包,面向机器学习初学者与有基础的数据分析、算法开发人员,可用于时序数据回归预测任务的快速上手与实验扩展。项目采用TensorFlow构建多层SimpleRNN网络,融合Dropout正则化,配套完整的数据生成、模型训练、预测和误差分析流程,只需运行main.py即可自动完成全流程并输出可视化图表。压缩包共6个文件、724KB,其中包含Python主程序、依赖环境清单、说明文档,以及训练历史、预测结果、误差分析三张可视化图片,轻量紧凑便于直接部署学习。已有86人浏览学习。参考README即可了解模型架构与超参数配置,适合作为课堂作业、毕业设计或入门RNN的参考模板。
1. 循环神经网络 RNN 凭什么做时间序列预测:先看懂它的记忆机制再动手
时间序列预测是序列数据处理里最常被问到的场景,而循环神经网络 RNN 是这类任务最基础的模型形态。这份资源不是讲概念的 PPT,也不是半成品 demo,而是一个能用python main.py直接跑通的完整工程:合成数据生成、多层 SimpleRNN 搭建、训练、预测、可视化一次走完,最终输出三张图(训练历史、预测结果、误差分析)供你判断模型效果。适合刚入门深度学习、想搞清楚 RNN 内部状态和普通神经网络差在哪的从业者,也适合懒得从头搭环境、想拿现成代码改数据的同学。先说结论:RNN 能记住序列里前面的信息,是因为它对每个时间步都维护了一个隐藏状态h_t,这个状态像接力棒一样一步步往后传;但它的记忆也有边界,梯度消失和长期依赖问题会在多层叠加时暴露得很明显,这份代码恰恰能让你亲眼看到这些现象。
2. 模型架构与数据准备:SimpleRNN 多层堆叠的选型逻辑
2.1 为什么选 SimpleRNN 而不是 LSTM 或 GRU
项目摘要里明确写了模型主体是多个 SimpleRNN 层,这是值得琢磨的一个选型点。做时间序列预测,LSTM 和 GRU 在长序列上通常表现更好,但它们的参数量大、内部门控机制复杂,初学阶段很难从训练曲线里看出门道。SimpleRNN 的总参数量只由输入维度、隐藏单元数和输出维度决定,结构透明,梯度传播路径清晰,出了异常也容易定位是哪个时间步的问题。
这份代码用 SimpleRNN 堆叠多层,目的是让你在最短时间内建立「输入序列 → 隐藏状态传递 → 输出预测」的整体认知。等你把这份代码跑通、理解每个张量的 shape 变化,再切换到 LSTM 或 GRU 就是改动一两行的事。模型结构大致是:输入层接收形状为(batch_size, timesteps, features)的序列数据,中间叠加两层带 Dropout 的 SimpleRNN,最后一层是输出预测值的全连接层。
2.2 数据是怎么来的:合成时间序列与滑窗切分
项目运行时不需要外部数据文件,主程序会自动生成合成时间序列数据。这个设计对复现非常友好——你不需要先去爬数据、清洗数据,直接就能看到完整流程跑完是什么样。常见做法是用正弦波叠加噪声来模拟有周期规律的时间序列,再通过滑窗把连续序列切成(输入窗口, 预测目标)的样本对。
滑窗切分是这份代码的核心数据操作,理解它对后续替换真实数据至关重要。假设原始序列长度是 1000,预测步长设为 10,那么前 10 个点作为输入特征,第 11 个点作为标签;然后窗口向后滑动一个点,第 2 到第 11 个点作为输入,第 12 个点作为标签,以此类推。这样做出来的样本数量大约有 990 条。滑窗宽度决定了模型能看到的「回看深度」,回看太短抓不住周期模式,回看太长对 SimpleRNN 来说学习压力陡增。
2.3 数据归一化为什么不能少
合成数据生成后,程序会调用 Scikit-learn 的MinMaxScaler把数据缩放到 0~1 区间,这一步在时间序列任务里几乎是强制性的,理由是 RNN 内部使用 tanh 激活函数,输入数值如果太大,tanh 很容易饱和,梯度直接趋近于零,训练根本走不动。经验做法是:先在全量训练数据上fit缩放器,再分别transform训练集和测试集,切忌对测试集单独做归一化拟合,否则会引入未来信息泄露,预测结果虚高。
3. 核心代码拆解:从数据生成到预测出图一条龙
3.1 项目文件结构与入口逻辑
拿到资源后先看目录,压缩包里主要包含以下文件:main.py是唯一的主程序,所有逻辑都在里面;requirements.txt用来一键安装依赖;README.md是项目说明文档;rnn_training_history.png、rnn_predictions.png、rnn_error_analysis.png是运行后生成的三张结果图。整个工程是「单文件跑通」的结构,对调试和二次开发都很友好,你不需要在多个 .py 文件之间跳来跳去找逻辑。
3.2 main.py 的流程:数据生成、归一化与样本切分
程序入口逻辑很直接,依次执行「生成数据 → 归一化 → 构建滑窗样本 → 划分训练集测试集 → 定义模型 → 训练 → 反归一化预测 → 画图」。下面先看数据准备部分的代码骨架:
import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler # 生成带趋势和噪声的合成时间序列 t = np.arange(0, 1000, 0.1) data = np.sin(t) + 0.2 * np.sin(20 * t) + 0.05 * np.random.randn(len(t)) # 归一化到 [0,1] 区间,消除量纲影响 scaler = MinMaxScaler(feature_range=(0, 1)) data_scaled = scaler.fit_transform(data.reshape(-1, 1)).flatten() # 滑窗切分:用过去 look_back 个点预测下一点 def create_sequences(data, look_back=10): X, y = [], [] for i in range(len(data) - look_back): X.append(data[i:i + look_back]) y.append(data[i + look_back]) return np.array(X), np.array(y) look_back = 10 X, y = create_sequences(data_scaled, look_back)create_sequences函数的核心逻辑就是滑窗:对每个位置i,取data[i:i+look_back]作为输入,取data[i+look_back]作为标签。这里有一个细节——为什么要把标签放在y里而不是直接在X的最后补一位?因为 RNN 的输入输出必须严格分离,模型看到的是一个长度为look_back的序列,输出是下一个时间步的值,如果混在一起,模型在训练时等于提前看到了答案。look_back是这个工程最重要的超参数之一,默认取 10,代表模型回看 10 个时间步;这个值可以按你的序列周期去试,比如处理以 24 小时为周期的数据,look_back至少设为 24 才有意义。
3.3 模型定义与训练循环
接下来看模型构建部分。注意这里用的是 TensorFlow 的 Keras 接口,RNN 层用SimpleRNN,叠加两层并用Dropout做正则化,输出层是Dense(1)回归单值:
import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import SimpleRNN, Dense, Dropout # 多层 SimpleRNN + Dropout 结构 model = Sequential() model.add(SimpleRNN(units=64, activation='tanh', return_sequences=True, input_shape=(look_back, 1))) model.add(Dropout(0.2)) model.add(SimpleRNN(units=32, activation='tanh', return_sequences=False)) model.add(Dropout(0.2)) model.add(Dense(units=1)) model.compile(optimizer='adam', loss='mse')return_sequences=True这一行是关键。第一个 SimpleRNN 层要输出完整的时间步序列给下一层 RNN,所以必须打开这个开关;最后一个 SimpleRNN 层只需要输出最后一个隐藏状态,所以设成False。units参数决定隐藏状态向量的维度,64 和 32 是这份工程里的默认配置,对合成数据够用;如果你的数据复杂程度更高,可以尝试 128 起步。模型参数量等于输入维度 + 隐藏维度 + 偏置再乘以隐藏维度,SimpleRNN 的参数量是(输入维度 + 隐藏维度) * 隐藏维度 + 偏置,所以 64 个单元的实际参数量大约是(1 + 64) * 64 + 64 = 4224,非常轻量。
训练部分用model.fit跑指定轮次,批次大小设为 128,验证集划分出自训练数据尾部:
# 数据 reshape 为 RNN 需要的三维格式 (样本数, 时间步, 特征数) X_train = X_train.reshape((X_train.shape[0], look_back, 1)) history = model.fit(X_train, y_train, epochs=50, batch_size=128, validation_split=0.2, verbose=1)reshape这一步是新手最容易忽视的:Keras 的 RNN 层严格规定输入是三维(batch_size, timesteps, features),哪怕你只有一维特征也必须显式补出第三维。epochs=50对这份合成数据来说足够,因为数据本身规律性强;换成真实数据后你会发现 loss 曲线下降得慢得多,这时别急着加轮次,先检查数据是否归一化、滑窗窗口是否合理。训练结束后,模型预测输出形状是(样本数, 1),但它是归一化后的值,需要用当时拟合好的scaler.inverse_transform还原成真实量纲,再和原始序列画在同一张图里对比。
3.4 三张可视化图分别告诉你什么
rnn_training_history.png画的是训练集和验证集的 loss 曲线,用来判断模型有没有收敛、有没有过拟合。理想状态是两条曲线同步下降并趋于平缓;如果训练 loss 持续走低而验证 loss 先降后升,说明过拟合了,这时可以增大 Dropout 比例或减小units。
rnn_predictions.png展示真实值和预测值的对比曲线。这里要看两点:一是预测曲线是否跟上了真实值的整体趋势,二是相位有没有偏移。RNN 预测常见的问题是预测值比真实值滞后若干步,这是因为窗口内信息被平均化了,你可以适当调大look_back试试。
rnn_error_analysis.png通常是预测残差或误差分布图。误差如果呈现明显的周期性,说明模型没学完数据里的所有模式;如果误差方差很大且毫无规律,可能是归一化方式不对或模型容量不足。从这张图能直观判断后续要往哪个方向调参。
4. 训练与调参:从 loss 曲线判断模型状态的实用方法
4.1 先跑通再调参:首次运行的标准动作
拿到代码后不要急着改参数,先完整跑一遍默认配置。首次运行的目的不是追求最低 loss,而是确认环境没问题、数据流能被模型吞下、三张图能正常生成。运行命令很简单:
pip install -r requirements.txt python main.pyrequirements.txt里锁定了 TensorFlow 2.15.0、NumPy 1.24.3、Matplotlib 3.7.2、Pandas 2.0.3、Scikit-learn 1.3.0。这些版本组合在 Python 3.7+ 环境下可以直接安装,如果你本地 Python 版本较高,TensorFlow 2.15 依旧兼容;如果安装的是更新的 TensorFlow 2.16+,代码里的接口基本不变,但要注意 Keras 的版本对应关系,以免出现AttributeError这类底层 API 变动问题。跑完一次后,你会得到三条信息:模型在多少轮开始收敛、测试集上的 loss 大致量级、预测曲线的形状是否合理。
4.2 loss 曲线怎么看:过拟合和欠拟合的判别方法
训练日志里输出的loss和val_loss是最直接的判断依据。如果训练结束时的 loss 值比验证 loss 低很多,大概率是过拟合了,特征被模型死记硬背下来;如果两个 loss 都高居不下,说明模型欠拟合,这时要同时调整多个方向。我在实际项目里判断是否过拟合,会重点观察验证 loss 曲线的拐点:如果在第 30 轮以后验证 loss 开始反弹而训练 loss还在降,说明第 30 轮左右的权重就是最优解,下次训练直接设epochs=30或者加 EarlyStopping 回调。
欠拟合的处理方向则不一样。先检查数据归一化是否做对,再看look_back窗口是否太短。以 24 小时周期的数据为例,如果look_back=10,模型根本没有机会见到一个完整周期,预测自然抓不住规律;此时把look_back改成 48 或 72,预测效果会有明显提升。这个过程很依赖经验,我一般会用一组对照组跑两三次,每次只改一个参数,记录 loss 和图像趋势,而不是一次性把所有参数都动一遍。
4.3 units、Dropout 与学习率怎么配合
SimpleRNN 的units控制了隐藏状态的容量,64 和 32 的组合对中短序列够用,但数据更复杂时可以把第一层扩到 128,Dropout 相应从 0.2 提到 0.3。Dropout 的本质是训练时随机丢弃一部分神经元输出,防止某些节点过度依赖,推理时自动按比例缩放回来,所以它对抑制过拟合非常有效。学习率用的是 Adam 优化器默认值 0.001,这个值在大部分场景下都能正常工作;如果发现 loss 曲线振荡剧烈、迟迟不下降,可以把学习率降到 0.0005,但这份代码里没有显式暴露学习率参数,需要你手动改编译那行代码:
from tensorflow.keras.optimizers import Adam model.compile(optimizer=Adam(learning_rate=0.0005), loss='mse')Adam 优化器对严重非凸的 loss 曲面有较强的自适应能力,对简单的合成数据几乎不需要手调学习率。改学习率这招主要是为了应对真实数据里 loss 振荡的情况。调整时注意一个原则:先调数据侧(归一化、滑窗),再调结构侧(units、Dropout),最后才动优化器参数。
4.4 训练时间与资源占用估算
这份工程的模型很小,参数量也就几万个级别,在普通 CPU 上用 50 轮训练合成数据,耗时大约在几十秒到一两分钟之间。如果你用的是带 CUDA 的 GPU,训练时间基本可以忽略不计,瓶颈反而在 Matplotlib 画图那一步。需要注意的是 TensorFlow 在 CPU 上跑时可能会提示缺少 AVX 指令集优化,这是正常现象,不影响结果,只是速度稍微慢一点,无需处理。如果你有真实数据要训练,建议先把epochs调大到 100 并观察第 50 轮左右的 loss 是否已经明显收敛,以此估算总耗时。
5. 避坑指南:RNN 时间序列预测中的四个典型翻车现场
5.1 归一化泄露:测试集单独拟合成最大坑
现象:预测曲线和真实曲线高度重合,连细微噪声都拟合了,看起来效果完美,但换一组新数据立刻崩溃。
原因:这是时间序列预测最容易犯的错误——对测试集单独调用fit_transform,相当于模型在评估时「偷看」了未来的最大值和最小值。MinMaxScaler 会把测试数据压缩到一个由测试集自身决定的区间,预测值被扭曲,指标虚高。这个坑我在处理股票数据时踩过一次,当时测试集上的准确率高得惊人,结果线上预测一塌糊涂。
解决:标准做法是先在全量训练集上fit缩放器,然后把同一套缩放参数分别transform训练集和测试集。代码里正确写法是先拟合训练集,再转换全集,顺序一定不能反。验证这一步很简单:把预测值inverse_transform还原后,检查还原数据的最大值是否和原始数据的最大值在同一量级。
5.2 reshape 维度错乱:RNN 输入必须有三维形状
现象:运行model.fit时报ValueError: Input 0 of layer "simple_rnn" is incompatible with the layer: expected ndim=3, found ndim=2。
原因:很多人把 Keras 的 Dense 层习惯带到 RNN 上,直接传二维数组。RNN 层要求输入的维度是(样本数, 时间步长, 特征数),即使特征是单维也要补 1。
解决:在喂给模型前统一加一步X_train.reshape((X_train.shape[0], look_back, 1)),测试集同理。养成这个习惯后,换用 LSTM 或 GRU 也不会再踩同一块石头。我一般会在数据准备函数末尾打印X.shape确认形状,再进入训练流程。
5.3 预测结果整体“滞后一拍”:滑窗架构的固有现象
现象:预测曲线比真实曲线向右平移了一个时间步,相位明显滞后,但 loss 数值并不高。
原因:滑窗预测的本质是用过去look_back个点去预测下一个点,模型学到的最优策略往往是复制最近的那个点,再加一点修正,因为时间序列在短时间尺度上具有很强的自相关性。这是 SimpleRNN 这类浅层记忆模型的通病,不是代码 bug。
解决:调整look_back让它覆盖数据的完整周期,让模型有机会看到更多历史变化;如果滞后依旧明显,可以考虑多步预测,即让模型一次输出未来多个时间步的值,而不是递归式地一步套一步。递归预测会把上一轮的误差不断放大,这是另一个常见误区。
5.4 loss 已经很小但预测曲线是直线
现象:训练 loss 和验证 loss 都收敛到非常低的水平,但rnn_predictions.png里的预测结果几乎是一条水平直线。
原因:模型陷入了局部最优,把所有输入都映射到了接近训练集均值的位置。这通常发生在序列本身波动幅度大、而损失函数用 MSE 的场景下,因为预测均值能让整体误差最小,但完全丢失了动态变化。
解决:检查归一化区间是否合理,确认数据是否被意外压平;如果是真实数据,考虑用差分或对数变换强化序列的波动特征。另外可以减小 Dropout 比例,防止信息丢失过度。遇到这种情况,我会做的第一件事是把训练好的模型换个随机种子重新跑一次,排除随机初始化带来的影响。
6. 把合成数据换成真实数据:三步改造法与效果验证技巧
6.1 改造第一步:把数据生成段替换为文件读取
这份工程唯一需要动刀的地方就是数据准备那几行。以 CSV 格式的单变量时间序列为例,常见做法是用 Pandas 读取后只保留需要的数值列,再统一走归一化和滑窗流程。替换时迎着头皮想清楚一件事:真实数据的格式千差万别,你需要把「读文件、清洗缺失值、类型转换」这些步骤补进原来的合成数据段。Pandas 读取 CSV 的典型替换逻辑是:
import pandas as pd df = pd.read_csv('your_data.csv', parse_dates=['timestamp']) # 按时间排序并取数值列 df = df.sort_values('timestamp') data = df['value'].values.astype(float) # 基础清洗:去掉 NaN 和无穷值 data = data[~np.isnan(data) & ~np.isinf(data)]排序这一步千万别省,时间序列数据一旦乱序,滑窗切出来的样本就全乱了。如果文件里有重复时间戳,最好先去重;如果是多列特征,先把需要的列拼成矩阵,再走归一化。真实数据往往还带有不断增长的趋势,比如销量数据、传感器数据,这种情况下直接用原始值训练,模型的预测会偏向趋势方向而忽略波动细节,所以在归一化之前做一阶差分通常很有帮助。
6.2 改造第二步:验证集按时间顺序切,不做随机打散
换成真实数据后,测试集切分方式必须改变。合成数据阶段通常按总样本的 80% 取前段做训练、20% 取后段做验证,代码里validation_split=0.2其实已经在按顺序切分了,但如果你急于求成自己写随机切分,就违背了时间序列预测的基本原则——未来的数据不能被模型在训练时提前见过。正确做法是:先按时间顺序计算出训练样本的起止下标,再切分滑窗后的样本矩阵:
split_index = int(len(X) * 0.8) X_train, X_test = X[:split_index], X[split_index:] y_train, y_test = y[:split_index], y[split_index:]这里的X是滑窗后的样本矩阵,形状为(样本数, look_back)。注意切分必须发生在滑窗之后,而不是对原始序列切分后再分别滑窗,否则测试集窗口会与训练集末尾的样本重叠,造成信息泄漏。切分完成后,再 reshape 成三维形状丢进模型。验证集也应该遵循同样的时间顺序,数据混洗会破坏序列的时间结构,导致验证结果失真。
6.3 改造第三步:用误差图做多步预测的可靠性判断
当你的目标从单步预测变成多步预测,比如用过去 24 小时预测未来 6 个小时,模型结构基本不用动,但训练样本的标签要从「下一个点」改成「未来第 6 个点」,或者改成同时输出 6 个值。后者通常效果更好,因为模型一次性学到的是一个输出向量,而不是递归地把自己的预测结果再喂给自己。误差分析图此时会显示出明显的规律:如果残差随预测步长增大而系统性放大,说明模型内部的误差累积已经失控,这时候应该减小预测距离或换用 LSTM。
增强版本里,还可以在rnn_error_analysis.png上叠加绝对误差的滑动平均线,漏掉某段时间误差突增就能一眼发现。真实数据的复杂程度远不是合成数据能比的,跑通这份 RNN 工程最大的收获不是得到一个能用的模型,而是亲手验证了「数据质量决定预测上限,模型结构只是逼近这个上限的工具」这条经验。从那以后,我每次接时间序列项目都强制先画一遍数据走势和自相关图,再决定滑窗宽度和模型选型,而不是直接套模板调参数。这个习惯帮我在真实场景里少走了不少弯路,希望帮到你。
本文还有配套的精品资源,点击获取