LSTM与ARIMA实战对比:时间序列预测的模型选择与避坑指南
简介面向希望掌握深度学习时间序列预测实战的Python学习者这份资源以标普500指数与太阳黑子两组数据集为实验场景完整覆盖数据预处理、特征构建、模型训练与效果评估等关键环节适合用于毕业设计、课程设计、工程实训或初期项目立项参考。资源压缩包共包含19个文件其中Python脚本12个、CSV数据文件4个、Markdown说明文档2个另有1个数据文件夹整体大小仅177KB目录按照标普500、太阳黑子与数据集三个模块划分便于按需查阅与复现。目前已有158人学习下载内容精炼兼顾入门与进阶场景。脚本部分实现自回归移动平均模型、长短期记忆网络、支持向量机及自回归模型等算法配合数据集可直接开展对比实验帮助读者系统梳理时间序列预测的完整实现思路是一份轻量实用的参考资料。1. 时间序列预测为什么我建议你用 LSTM 和 ARIMA 做对比实验做时间序列预测的毕设或者课程设计最怕的不是模型跑不通而是答辩时导师问一句“你为什么要用这个模型”答不上来。这份基于 Python 深度学习的时间序列预测项目把 LSTM 和 ARIMA、SVM、ARMA 放在同一个框架里做对比覆盖了太阳黑子数据集和标普500指数两个典型场景——前者是周期性明显的平稳序列后者是噪声大、带趋势的金融序列。你不需要自己去爬数据、整理格式项目已经把 spv2.csv、sp2005-2015.csv、sunspot_ms.csv 这些数据按场景分好类代码也按 sp500 和 sunspot 两个目录拆开loader、model、predict、evaluation 各司其职。适合的人群很明确正在做毕业设计、课程大作业或者想系统入门深度学习时序预测但不想从零开始搭框架的人。看完这份项目你能得到三样东西一套能直接跑通的数据加载流程一个参数可调的 LSTM 预测实现以及一个可以和 ARIMA/SVM 做横向对比的评估代码。接下来我从数据准备开始把整个项目的代码逻辑和踩过的坑都过一遍。2. 数据加载与预处理loader.py 的标准化写法和两个数据集的差异2.1 太阳黑子数据为什么要做差分sunspot_ms.csv 与 sunspot_ms_diff.csv 的关系打开 dataset 目录你会看到 sunspot_ms.csv 和 sunspot_ms_diff.csv 两个文件。前者是原始月度太阳黑子数据后者是做了差分之后的版本。为什么要做差分因为 LSTM 虽然理论上能拟合非平稳序列但实际训练时非平稳序列的均值漂移会让 LSTM 的隐状态很难收敛——loss 经常震荡训练十几个 epoch 都看不到明显下降。差分的目的就是把非平稳序列转成平稳序列让模型只学“变化量”而不学“绝对水平”。sunspot 目录下的 loader.py 是这样处理的import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler def load_sunspot_data(file_path, diffTrue, look_back12): df pd.read_csv(file_path) data df[sunspot].values.astype(float32) if diff: data_diff np.diff(data, n1) print(f差分后数据长度: {len(data_diff)}) else: data_diff data scaler MinMaxScaler(feature_range(0, 1)) data_scaled scaler.fit_transform(data_diff.reshape(-1, 1)) X, y [], [] for i in range(look_back, len(data_scaled)): X.append(data_scaled[i - look_back:i, 0]) y.append(data_scaled[i, 0]) X np.array(X).reshape(-1, look_back, 1) y np.array(y) return X, y, scaler, data_diff这段代码的逻辑很清晰读原始 CSV、取 sunspot 列、做一阶差分、MinMax 归一化到 [0,1] 区间、滑动窗口切样本。look_back12表示用过去 12 个月预测下一个月这个参数对太阳黑子这种 11 年周期数据来说是合理的——12 个月刚好覆盖一年内的季节性波动。这里有个关键点差分只做一次。有些同学会在差分后还想再差分一次觉得“越平稳越好”。实际上一阶差分后序列已经基本平稳再做二阶差分虽然能进一步降低非平稳性但会引入更多噪声LSTM 学到的往往是对噪声的过拟合而不是对周期的捕捉。scaler保存下来是为了后面预测时做 inverse_transform把预测结果还原到原始量纲。2.2 标普500的数据加载为什么 sp2005-2015.csv 要用收盘价而不是开盘价sp500 目录下的 loader.py 逻辑类似但处理的是另一个问题——金融数据的粒度选择。打开 sp2005-2015.csv 你能看到日期、开盘价、最高价、最低价、收盘价、成交量这些列。预测目标选哪个这一版 loader 默认取收盘价Close这是最常规的做法因为收盘价是市场对当天信息的综合定价噪声相对开盘价小。import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler def load_sp500_data(file_path, date_colDate, close_colClose, look_back20): df pd.read_csv(file_path, parse_dates[date_col]) df df.sort_values(date_col).reset_index(dropTrue) data df[close_col].values.astype(float32) scaler MinMaxScaler(feature_range(0, 1)) data_scaled scaler.fit_transform(data.reshape(-1, 1)) X, y [], [] for i in range(look_back, len(data_scaled)): X.append(data_scaled[i - look_back:i, 0]) y.append(data_scaled[i, 0]) X np.array(X).reshape(-1, look_back, 1) y np.array(y) return X, y, scaler, df[date_col].values[look_back:]look_back20对应大约一个月的交易日数量对捕捉短期趋势比较合适。注意这里没有做差分因为标普500的收盘价虽然带趋势但这个数据集横跨 2005 到 2015——包含 2008 年金融危机前后的完整周期——LSTM 在这种长度上可以学到一部分趋势信息做差分反而会丢失宏观结构。加载代码里返回了df[date_col].values[look_back:]这是对应每个预测样本的真实日期画图时直接用这个做横轴非常方便。很多初学者会忽略这个细节预测完了画图时发现横轴对不上只能重新翻原始数据。2.3 滑动窗口切样本的边界问题前 look_back 个点去哪了滑动窗口切样本时有一个天然损失——序列开头的前look_back个点无法形成完整的输入窗口。以太阳黑子数据为例如果原始数据有 2800 个月look_back12时训练样本数就是 2788 个。这个损失不是 bug是滑动窗口方法的固有边界。但有一个细节容易翻车切分测试集时如果直接用现有顺序切测试集前面也会损失look_back个点。更稳妥的做法是记录切分位置让训练集和测试集各自保留自己的窗口边界。这个项目里没有显式处理这个边界你在复现时如果发现预测曲线开头有异常的尖峰可以先检查是不是窗口边界没对齐——这种情况通常是训练集和测试集的前几个预测点失真导致的不影响整体评估但会让曲线图很难看。3. LSTM 模型构建model.py 和 lstm.py 里藏着哪些可调参数3.1 从 model.py 看 LSTM 网络结构设计sunspot 目录下的 model.py 是核心模型定义文件打开后能看到一个基于 Keras 的 LSTM 网络结构。这一版代码用 Sequential 容器搭建了两层 LSTM 加一个全连接输出层from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout def build_lstm_model(look_back12, n_units64, dropout_rate0.2, output_dim1): model Sequential() model.add(LSTM(unitsn_units, return_sequencesTrue, input_shape(look_back, 1))) model.add(Dropout(dropout_rate)) model.add(LSTM(unitsn_units // 2, return_sequencesFalse)) model.add(Dropout(dropout_rate)) model.add(Dense(unitsoutput_dim)) model.compile(optimizeradam, lossmse, metrics[mae]) return model第一层 LSTM 的return_sequencesTrue是为了把完整的隐藏状态序列传给第二层 LSTM——如果第一层就设成 False第二层拿到的只是最后一个时间步的输出信息损失很严重。第二层return_sequencesFalse表示只输出最后一个时间步然后接 Dense 层得到预测值。n_units64对应隐藏单元数n_units // 2让第二层容量减半减少过拟合风险。这个结构看起来很常规但有一个容易被忽略的设计为什么中间要加 Dropout时序数据训练时LSTM 的隐状态很容易记住训练集的细节噪声Dropout 在这里的作用是让部分神经元随机失活强行让模型学到更鲁棒的模式。dropout_rate0.2是经验值——太低了没用太高了模型欠拟合。我一般会从 0.2 起步如果训练集 loss 很低但测试集 loss 居高不下才会往上调到 0.3。3.2 lstm.py 里的训练循环epochs、batch_size 和验证集划分lstm.py 是整个训练流程的调度者它负责把 loader 生成的数据喂给 model并在训练过程中输出关键指标。关键代码段如下from sklearn.model_selection import train_test_split from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau def train_lstm(X, y, model, epochs100, batch_size32, val_split0.2): X_train, X_val, y_train, y_val train_test_split( X, y, test_sizeval_split, shuffleFalse ) early_stop EarlyStopping( monitorval_loss, patience15, restore_best_weightsTrue ) reduce_lr ReduceLROnPlateau( monitorval_loss, factor0.5, patience5, min_lr1e-5 ) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochsepochs, batch_sizebatch_size, callbacks[early_stop, reduce_lr] ) return historyshuffleFalse是时序预测里极其重要的一个参数。如果默认shuffleTrue训练集和测试集的数据会混合随机打散时间顺序被完全破坏——模型看到的训练样本可能来自测试时间段测试时自然表现很好但这是典型的泄露毫无实际意义。这个坑我见过很多次毕设答辩时一旦被导师问“你的验证集为什么看起来这么平滑”基本都是这个原因。EarlyStopping 的patience15表示连续 15 个 epoch 验证集 loss 不下降就停止训练restore_best_weightsTrue会把模型权重恢复到验证集表现最好的状态而不是停在最后一个 epoch。ReduceLROnPlateau 则在验证集 loss 停滞 5 个 epoch 后将学习率减半让模型在接近最优解时更精细地搜索。min_lr1e-5是防止学习率无限下降。3.3 训练集和测试集占比怎么定时序数据不能随机切分上面的代码用train_test_split按比例切分训练集和验证集但因为设了shuffleFalse实际是按时间顺序前 80% 训练、后 20% 验证。这里有一个很多人会问的问题验证集到底应该是随机切还是按时间切答案是必须按时间切。时间序列预测的核心是“用过去预测未来”如果验证集的数据时间上早于训练集那就违反了因果方向。更严格的做法是不要用train_test_split手动按索引位置切分——因为train_test_split即使shuffleFalse切出来的两个集合也是连续的但如果你传入的数据本身就已经被时序打乱过结果就会出问题。我一般会这样写train_size int(len(X) * 0.8) X_train, X_val X[:train_size], X[train_size:] y_train, y_val y[:train_size], y[train_size:]这样做的好处是边界清晰不会出现窗口重叠的问题。使用这个训练流程时我建议把epochs初始设为 100——配合 EarlyStopping实际跑多少轮由验证集表现决定但上限要足够高否则容量大的模型在复杂数据集上可能没跑完就被截断了。4. 对比模型实现ARIMA、SVM 和 ARMA 在三个文件里怎么调4.1 arima.py 的差分阶数确定太阳黑子数据适合什么参数arima.py 处理的是经典统计模型 ARIMA。这个文件的核心逻辑是用 ADF 检验判断平稳性、用 ACF/PACF 图确定阶数、然后 fit 和 predict。关键代码如下import pandas as pd import numpy as np from statsmodels.tsa.stattools import adfuller from statsmodels.graphics.tsaplots import plot_acf, plot_pacf from statsmodels.tsa.arima.model import ARIMA def fit_arima(data, order(2, 1, 2)): adf_stat, p_value adfuller(data) print(fADF p-value: {p_value:.4f}) if p_value 0.05: print(序列非平稳建议差分) data_fit np.diff(data) else: data_fit data model ARIMA(data_fit, orderorder) model_fit model.fit() return model_fit这里order(2, 1, 2)是太阳黑子数据上比较常用的值——p2 表示自回归项数为 2d1 表示一阶差分q2 表示移动平均项数为 2。ARIMA 的阶数确定没有标准答案你可以自己画 ACF 和 PACF 图来观察如果 PACF 在滞后 2 阶后截尾p 取 2 是合理的如果 ACF 在滞后 2 阶后拖尾q 也可以取 2。但说实话手动看图定阶比较费时间太阳黑子这类有明显周期性且平稳性不错的数据order(2, 1, 2)或order(4, 1, 2)都能跑出不错的效果。一个非常常见的翻车点是 ADF 检验的滞后阶数。adfuller默认会按数据长度自动选择滞后阶数但太阳黑子数据有明显的周期性默认参数可能把周期波动误判为非平稳。我习惯加上autolagAIC让函数用 AIC 准则自动选滞后阶这样对周期性数据更友好。另外注意np.diff之后数据的长度少了 1做预测时forecast的步数要按差分后的长度去对应否则结果会对不上原始数据的时间轴。4.2 svm.py 做回归预测SVR 在高维映射上的优势和参数设置svm.py 用支持向量回归SVR来做时间序列预测。SVR 的思路是把输入窗口映射到高维空间找一个间隔最大的回归超平面。它的优势在于小样本下不容易过拟合和 LSTM 形成鲜明对比——LSTM 需要大量数据来训练而 SVR 在几百个样本时就能稳定出结果。from sklearn.svm import SVR from sklearn.preprocessing import StandardScaler def svm_predict(train_X, train_y, test_X, test_y, kernelrbf, C1.0, gammascale): scaler_X StandardScaler() scaler_y StandardScaler() train_X_scaled scaler_X.fit_transform(train_X.reshape(-1, look_back)) train_y_scaled scaler_y.fit_transform(train_y.reshape(-1, 1)).ravel() test_X_scaled scaler_X.transform(test_X.reshape(-1, look_back)) model SVR(kernelkernel, CC, gammagamma, epsilon0.01) model.fit(train_X_scaled, train_y_scaled) pred_scaled model.predict(test_X_scaled) pred scaler_y.inverse_transform(pred_scaled.reshape(-1, 1)).ravel() return pred用 SVR 时train_X.reshape(-1, look_back)这一步很重要——loader 输出的 X 是三维的(样本数, look_back, 1)LSTM 需要三维输入但 sklearn 的 SVR 只接受二维输入所以要先把最后一维压平。如果你在复现时发现 SVR 报ValueError: Found input variables with inconsistent numbers of samples大概率就是这里的三维没压平。SVR 参数方面C1.0是正则化系数C 越大越容易过拟合训练集C 越小模型越平滑。gammascale表示 gamma 按 1/(n_features * X.var()) 自动计算这是比较稳健的默认值。epsilon0.01是不敏感带的宽度epsilon 越大回归超平面附近的噪声容忍度越高预测曲线就越平滑。金融数据上如果你发现预测曲线是一条直线多半是 epsilon 太大或者 C 太小模型把所有变化都当成了噪声。4.3 arma.py 和 ARIMA 的差别什么时候用 ARMA 就够sunspot目录下的 arma.py 和sp500目录下的 arima.py 看起来重复但它们的适用场景不同。ARMA 要求输入数据已经平稳它不做差分ARIMA 里面的“I”就是差分。太阳黑子数据经过预处理也就是 2.1 中提到的np.diff之后已经平稳所以 arma.py 直接对差分后的数据建模是合理的——少一步让模型自己差分的步骤参数更少、更容易收敛。arma.py 的核心逻辑如下from statsmodels.tsa.arima.model import ARIMA def fit_arma(data, order(4, 0, 2)): model ARIMA(data, orderorder) model_fit model.fit() return model_fit def predict_arma(model_fit, steps12): forecast model_fit.forecast(stepssteps) return forecast注意这里order的第二个值d设成了 0等价于 ARMA(p, q)。太阳黑子的周期大约是 11 年如果按月粒度预测steps12预测未来一年。对于周期性数据ARMA 的预测结果在中期未来 20-30 个点表现还行但如果预测步数拉太长预测值会趋向于序列均值——这是 ARMA 类模型的内在局限它们只捕捉线性依赖对长期非线性周期结构无能为力。5. 避坑与排查训练时间序列模型最常见的五个问题5.1 训练 loss 下降但测试集输出全是常量现象训练时 loss 从 0.01 降到 0.001看着很正常但用测试集预测时输出的值几乎没变化画出来是一条近似水平的线。原因模型把输入窗口的最后一个值直接作为预测输出也就是“复制粘贴”策略。这在时序预测里很常见——如果目标变量存在强自相关LSTM 学到的最省事方案就是直接输出最后一个时间步的值。验证集看起来 loss 也不高因为相邻时间点的值本身就很接近。解决检查模型是否真的学到了模式可以做一个 baseline 对比——用y_pred X_test[:, -1, 0]做“预测”直接把每个样本的最后一个时间步当预测值算这个 baseline 的 MSE。如果 LSTM 的测试集 MSE 和 baseline 差不多说明模型在偷懒。缓解方法增加n_units和层数或者减小look_back让输入信息更丰富迫使模型学习真实映射。5.2 训练集和测试集的归一化参数不一致导致预测值漂移现象训练时模型收敛很好但预测结果整体偏大或偏小几个量级曲线形态正确但数值位移。原因如果在训练和预测时分别调用了MinMaxScaler.fit_transform两个集合的 min/max 不同归一化到不同尺度模型拿到的是“分布漂移”的输入输出自然不对。解决加载数据时只对训练集调用fit_transform测试集只调用transform。上面的 loader 代码里对全部数据做了一个整体fit_transform——这在样本量够大时问题不大但如果测试集中有极端值比如 2008 年的暴跌整体归一化会把训练集部分压缩到很小的区间。scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_data.reshape(-1, 1)) test_scaled scaler.transform(test_data.reshape(-1, 1))5.3 predict.py 里 inverse_transform 维度对不上现象模型预测完成后做inverse_transform报错ValueError: non-broadcastable output operand with shape (n,) doesnt match the broadcast shape (n, 1)。原因scaler.inverse_transform要求输入是(n, 1)形状而模型输出的y_pred是(n,)。解决在逆变换前先 reshapey_pred_reshaped y_pred.reshape(-1, 1) y_pred_original scaler.inverse_transform(y_pred_reshaped).ravel()这个坑我在自己写的 predict.py 里踩过主要原因是 Keras 的model.predict输出形状有时是(n, 1)有时会被压成(n,)取决于 Dense 层的output_dim和调用方式。统一在逆变换前手动 reshape 是标准做法。5.4 太阳黑子预测曲线整体滞后一个月现象把预测值和真实值画在同一张图上预测曲线看起来像真实曲线向右平移了一个月——形态完全一致但“晚了一拍”。原因look_back12的情况下模型学到的预测规则本质上是“用过去 12 个月推断下一个月”如果序列的月份之间存在强自相关最优解就是输出近似等于上一个月的值。这个问题也叫“滞后效应”在金融数据和周期性数据上都很明显。解决不要只看 loss 数值要看预测曲线和真实曲线是否在峰值对齐。可以试试减小look_back到 6 或 3让模型更关注近期变化而不是长期平均或者用diff后的数据训练预测时再差分还原这样模型学的是变化趋势而不是水平值滞后程度会显著降低。5.5 训练速度慢到无法接受怎么判断是数据量问题还是模型结构问题现象同样是 LSTM太阳黑子数据几十秒跑完标普500 数据要跑十几分钟而且 CPU 占用率很高。原因标普500 的数据量大大约 2700 多个交易日且模型在金融数据上收敛慢、EarlyStopping 触发得晚。太阳黑子数据做过差分和归一化更容易学所以训练更快。解决先缩小epochs和patience确认模型结构和参数没问题后再放大用ReduceLROnPlateau让大学习率快速逼近最优区域如果机器有 GPU在代码开头加一句import tensorflow as tf physical_devices tf.config.list_physical_devices(GPU) print(physical_devices)如果输出为空列表可能就是 TensorFlow CPU 版本或者 GPU 环境没配好。CPU 训练时batch_size64比 32 能显著提速前提是批量大小不严重影响收敛质量。6. 评估指标与预测可视化EvaluationIndex.py 里的指标选取和画图技巧6.1 用 MAE、MSE、RMSE、MAPE 四张指标表横向对比模型EvaluationIndex.py 是评估模块定义了回归类问题常用的指标。看代码前先说结论毕设答辩时表格里有 MAE、MSE、RMSE、MAPE 四个指标比单看 loss 有说服力得多。import numpy as np from sklearn.metrics import mean_absolute_error, mean_squared_error def evaluate_metrics(y_true, y_pred): y_true np.array(y_true).ravel() y_pred np.array(y_pred).ravel() mae mean_absolute_error(y_true, y_pred) mse mean_squared_error(y_true, y_pred) rmse np.sqrt(mse) mask y_true ! 0 mape np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100 return {MAE: mae, MSE: mse, RMSE: rmse, MAPE: mape}MAE 反映平均绝对误差幅度直观但受量纲影响大MSE 对大误差惩罚重适合突出异常点RMSE 和 MSE 本质一致但 RMSE 的量纲和原始数据一致画图时更好解释。MAPE 是相对误差但注意——如果y_true里有 0 值MAPE 会变成无穷大所以代码里用mask y_true ! 0把零值过滤掉。太阳黑子数据基本没有 0但这个保护逻辑在标普500 数据上可能触发因为某些日期收盘价可能接近 0 或者数据里有空值。我一般会在表格里把 LSTM、ARIMA、SVM、ARMA 四个模型的指标并排列出用加粗标出每个指标下的最优值。如果能做到“LSTM 在 RMSE 上最优ARIMA 在 MAPE 上最优”这个结果在答辩时很有讨论空间——说明你理解不同模型在不同误差度量下的表现差异。这一版的调用约定是metrics_lstm evaluate_metrics(y_test, y_pred_lstm) metrics_arima evaluate_metrics(y_test, y_pred_arima)两个数组必须在同一时间轴上对齐。如果 ARIMA 预测的是未来 12 个点而 LSTM 预测的是测试集的全部点指标对比就没有意义——建议统一预测同一个时间段比如测试集的后 12 个点。6.2 把预测曲线画在真实数据上面时间轴对齐和可视化可视化虽然不是评估指标但在毕设展示里起到的作用常常比表格更大。一张“真实曲线 vs LSTM 预测曲线 vs ARIMA 预测曲线”三线图让导师一眼看出模型的拟合能力。画图需要注意的是时间轴对齐问题。LSTM 在测试集上预测时每个样本对应一个真实日期ARIMA 的forecast(steps12)返回的是未来第 1 到第 12 个点在时间轴上和测试集的末尾相接而不是对齐到测试集的每个点。正确的画法是import matplotlib.pyplot as plt def plot_predictions(dates, y_true, y_pred_lstm, y_pred_arima, titlePrediction Comparison): plt.figure(figsize(14, 5)) plt.plot(dates, y_true, labelTrue, colorblack, linewidth1.5) plt.plot(dates, y_pred_lstm, labelLSTM, colorblue, linestyle--) plt.plot(dates, y_pred_arima, labelARIMA, colorred, linestyle:) plt.legend() plt.title(title) plt.grid(alpha0.3) plt.xticks(rotation45) plt.tight_layout() plt.show()注意dates数组要和预测值长度一致。如果y_pred_arima只有 12 个点那它的横轴应该从dates的末尾开始对齐而不是从头对齐。一种更稳妥的方式是把 ARIMA 预测结果也贴到原始日期数组的尾部用np.concatenate拼接后再画。6.3 验证预测结果的一个小习惯残差图和预测值的分布检查除了指标表和曲线图我每次都会额外画一张残差图——真实值减预测值的分布。如果残差呈现明显的周期性或趋势性说明模型还有没捕捉到的模式如果残差分布类似噪声且集中在零附近模型基本可以交付。这是一个很简单的技巧但在毕设报告里写“残差无自相关模型已捕捉主要时间依赖模式”这句话比写“预测准确率高”更有说服力。关于这份资源和代码的完整使用流程我自己是把sp500目录和sunspot目录各跑一遍作为两个实验场景用同一个评估模块对比指标。因为两个目录的代码结构基本一致做横向对比很方便。从那以后我每次跑时间序列预测都会先调一遍 loader 的look_back和diff参数再看一眼训练曲线里有没有滞后特征最后才看指标表。这套流程帮我避开了很多“指标好看但实际上无效”的模型。希望帮到你。本文还有配套的精品资源点击获取