简介:资源提供了基于Python的ARIMA-SSA-LSTM组合模型时间序列预测完整实现,面向需完成课程设计、期末大作业或毕业设计的计算机、电子信息、数学等专业学生,也适合希望快速上手深度学习时序预测的入门者。代码采用参数化编程,关键参数可灵活调整,并配有接近逐行的保姆级注释,能够显著降低理解门槛。压缩包内共3个文件,包括1个Python脚本与2个CSV数据文件,脚本涵盖数据读取、模型构建、训练与预测流程,CSV数据可用于直接测试和替换,整体仅51KB,轻量便于下载使用。目前已有509人学习,资源由资深算法工程师整理,代码结构清晰、注释详尽,既可作为完整项目方案参考,也可当作入门练习逐步研读,能帮助读者理解ARIMA、SSA与LSTM的协同建模思路,并迁移到其他时序预测任务中。
1. ARIMA-SSA-LSTM 时间序列预测不是三个模型拼接
把 ARIMA、SSA、LSTM 放在同一个预测管线里,常见做法不是让三个模型轮流表演,而是先用 SSA 把原始序列分离成主趋势、周期性成分和噪声,再让 ARIMA 接住线性的那一块,用 LSTM 去补 ARIMA 吃不到的非线性残差。这个思路在交通流量、电网负荷、设备温度,甚至水文径流预报的深度学习时间序列预测里,都比单模型更稳,原因是它把“趋势、周期、噪声”三类信息分开处理。
适合已经会用 ARIMA 和 LSTM 做基础预测,但还没想清楚怎么把两者干净地粘在一起的人。下面从原理、Python 实现、参数调整到上线验证,按一条可复现的路线讲完。
2. 先理解 SSA 分解、ARIMA 与 LSTM 在预测管线里的分工
组合模型最怕的不是某一步写错,而是三个模型都在做同一件事。为了让每一步都有存在意义,先要明确 SSA、ARIMA、LSTM 各自只能解决哪一类问题。
2.1 SSA 把序列拆成“可预测的主体”和“噪声/非线性残差”
奇异谱分析是一种数据驱动的分解方法,不需要预先把序列写成傅里叶基或小波基。它将一维时间序列嵌入到一个轨迹矩阵中,再做奇异值分解,最后把选中的主成分沿反对角线平均回一维序列。核心计算量集中在 SVD 上,对长度几千点的业务序列完全够用。
下面是最小实现,只做 SVD,不做重构:
import numpy as np def ssa_svd(series, L): series = np.asarray(series, dtype=float) K = len(series) - L + 1 X = np.lib.stride_tricks.sliding_window_view(series, window_shape=L).T U, s, Vt = np.linalg.svd(X, full_matrices=False) return U, s, Vt, Kwindow_shape=L会形成 K 个长度 L 的滑动窗口,转置后轨迹矩阵 X 的形状是(L, K)。奇异值 s 按从大到小排列,s[i]^2 / sum(s^2)表示第 i 个成分的能量占比。L 太小,SSA 只能看到局部抖动;L 至少要覆盖一个主周期的 0.5 到 1 倍,否则提取出来的所谓主成分更像滑动平均。注意np.lib.stride_tricks.sliding_window_view要求 NumPy 1.20 以上,老环境需要先升级 numpy。
| 模型/成分 | 擅长 | 不擅长 |
|---|---|---|
| SSA | 把主趋势、假周期和噪声分离 | 本身不产出预测结果,必须再接模型 |
| ARIMA | 线性趋势、差分平稳后的短程自相关 | 剧烈突变、多周期叠加、非线性模式 |
| LSTM | 长依赖序列、非线性残差模式 | 样本量小、噪声大时容易过拟合 |
2.2 ARIMA 处理主体,LSTM 处理残差
ARIMA 对非平稳序列通过 d 阶差分变成近似平稳序列,再用 AR 项和 MA 项描述线性自相关。它的优势是参数少、可解释、在短预测步数内很稳,劣势是对被噪声污染的非线性关系基本无能为力。LSTM 神经网络的门控结构可以记住更长时间窗口内的模式,但直接让 LSTM 从包含趋势、季节、噪声的原始序列里端到端学习,往往需要大量数据,而且对数据缩放和初始权重非常敏感。
常见做法是让两者处理不同的序列:先用 SSA 得到去噪后的主体序列,主体序列交给 ARIMA;原始序列减掉主体序列得到残差序列,残差序列交给 LSTM。残差里虽然还有噪声,但只要它不是纯白噪声,LSTM 就有机会学到可预测的非线性自相关。判断残差是否还有学习价值,稳定性检验和自相关图是两个快速指标,后面第 4 章会给出代码。
2.3 两种落地形态:残差修正与分量合并
组合时常见有两条路线,先说明白,后面代码按第一条实现。
- 残差修正路线:
SSA 去噪 -> ARIMA 预测去噪序列 -> LSTM 预测残差 -> 相加。优点是串联关系清楚,ARIMA 漏掉的信息才交给 LSTM,适合一般业务序列。 - 分量合并路线:
SSA 分解成趋势、周期、噪声三组 -> 趋势用 ARIMA,周期分量用 LSTM -> 各组预测相加。优点是可以显式控制周期,但分组和分量重构的调试成本高,适合周期非常稳定的水文或电力负荷数据。
这里采用第一条路线,因为当 SSA 窗口和主成分数选得不理想时,残差修正方式仍然能靠 LSTM 吸收一部分分解误差,整体容错更高。
3. 用 Python 实现 ARIMA-SSA-LSTM:数据、核心函数与一次完整预测
下面代码可以在本地直接运行,数据使用 statsmodels 内置的太阳黑子月均值,不需要额外下载外部文件。太阳黑子序列包含明显的长周期和噪声,适合演示 SSA 去噪后 ARIMA 与 LSTM 的分工。
3.1 环境依赖与数据准备
在 Linux 服务器上通常先确认 python 和 pip 可用,运行python3 -m pip --version,然后安装依赖:
pip install numpy pandas statsmodels pmdarima scikit-learn tensorflow matplotlibpmdarima 负责自动搜索 ARIMA 阶数;statsmodels 提供太阳黑子数据和 ADF 检验;tensorflow 只用来训练 LSTM。如果 sklearn 版本和 tensorflow 都来自新的 pip,一般不会有版本冲突,但建议在虚拟环境中安装。
import numpy as np from statsmodels.datasets import sunspots df = sunspots.load_pandas().data print(df.columns) series = df.iloc[:, 1].values.astype(float)df.columns通常包含YEAR和SUNACTIVITY,取第二列作为训练序列。下一步按固定长度切分训练集和测试集,测试集只保留 12 个点,方便观察多步预测误差累积。
train = series[:240] test = series[240:252]注意切分要在 SSA 之前完成。如果先对整段序列做 SSA 再切分,轨迹矩阵会用到未来窗口的数据,属于泄漏。
3.2 SSA 去噪与序列重构函数
SSA 去噪不是简单丢掉几个奇异值,而是把选中的主成分重构回一维序列。重构过程需要沿轨迹矩阵的反对角线做平均,直接实现如下。
def ssa_filter(series, L=24, top=2): series = np.asarray(series, dtype=float) n = len(series) K = n - L + 1 X = np.lib.stride_tricks.sliding_window_view(series, L).T U, s, Vt = np.linalg.svd(X, full_matrices=False) recon = np.zeros((L, K)) for idx in range(top): recon += s[idx] * np.outer(U[:, idx], Vt[idx]) out = np.zeros(n) count = np.zeros(n) for i in range(L): for j in range(K): out[i + j] += recon[i, j] count[i + j] += 1 return out / count这段代码中np.outer(U[:, idx], Vt[idx])得到第 idx 个奇异值对应的秩 1 矩阵;把前 top 个累加,再按t = i + j对角平均。top一般取 2 到 5,取值太大等于没去噪。调用后得到主体序列和残差序列:
smoothed_train = ssa_filter(train, L=24, top=2) noise_train = train - smoothed_trainL 取 24 是因为按月采样时先看两年窗口;如果序列有明显年度周期,L 应调整到主周期附近。太阳黑子的 11 年周期太长,这里用 24 个月作为示例,后续调参章节会说明怎么继续调。
3.3 ARIMA 自动定参并预测
ARIMA 的 p、d、q 用手写网格搜索会很啰嗦,实际工程里常用 pmdarima 的自动搜索,内部按 AIC 比较多次拟合结果。
from pmdarima import auto_arima arima_model = auto_arima( smoothed_train, start_p=1, max_p=5, d=1, max_d=2, start_q=1, max_q=5, seasonal=False, trace=False, error_action="ignore", suppress_warnings=True, stepwise=True, n_fits=10, ) arima_pred = arima_model.predict(n_periods=12)d固定从 1 开始搜索,因为太阳黑子序列不是平稳的;stepwise=True表示不穷举所有组合,速度更快。smoothed_train是 ARIMA 的拟合对象,不要误传原始带噪序列,否则噪声会占用 ARIMA 的模型容量。如果数据本身有固定周期且 SSA 保留了该周期,应该打开seasonal=True, m=周期长度,否则 ARIMA 会为了拟合周期而消耗更多滞后阶数。
3.4 LSTM 对残差建模并递归多步预测
残差序列noise_train的尺度可能很小,直接丢给 LSTM 会出现梯度问题。先做一个只基于训练残差的 MinMaxScaler,再构造滑窗样本。
from sklearn.preprocessing import MinMaxScaler from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense def build_dataset(x, lookback): X, y = [], [] for i in range(lookback, len(x)): X.append(x[i - lookback:i]) y.append(x[i]) return np.array(X), np.array(y) scaler = MinMaxScaler(feature_range=(-1, 1)) noise_scaled = scaler.fit_transform(noise_train.reshape(-1, 1)).ravel() lookback = 12 X, y = build_dataset(noise_scaled, lookback) X = X.reshape((X.shape[0], X.shape[1], 1)) model = Sequential([ LSTM(32, activation="tanh", input_shape=(lookback, 1)), Dense(1) ]) model.compile(optimizer="adam", loss="mse") model.fit(X, y, epochs=20, batch_size=16, validation_split=0.1, verbose=0)lookback=12使每个样本包含过去 12 个月的残差,LSTM 按时间步读取这 12 个值。Dense(1)输出一个残差预测值,损失函数用均方误差。训练轮次不多,因为目标是补足 ARIMA 的主体预测,而不是让 LSTM 单独拿下全部序列。时序数据用validation_split时必须接受它是按末尾切分这一事实,训练中不要手动打乱样本顺序。
递归预测需要把预测值持续放回窗口,否则只能预测一步。
def lstm_forecast(model, last_window, h): vals = list(last_window) preds = [] for _ in range(h): x = np.array(vals[-lookback:]).reshape(1, lookback, 1) p = model.predict(x, verbose=0)[0, 0] preds.append(p) vals.append(p) return scaler.inverse_transform(np.array(preds).reshape(-1, 1)).ravel() lstm_pred = lstm_forecast(model, noise_scaled[-lookback:], 12)代码中的vals[-lookback:]每次取最近 12 个值,预测出一个残差点后把它追加到vals。递归多步预测的缺点非常明显:第 12 步的输入包含前 11 步的模型预测,误差会累积。因此 h 越大,LSTM 部分越保守越好,不要为追求训练集表现而增加过多单元数。
3.5 合并预测并与真实值对比
最终预测是两段预测的直接相加:
final_pred = arima_pred + lstm_pred mae = np.mean(np.abs(final_pred - test)) rmse = np.sqrt(np.mean((final_pred - test) ** 2)) print(f"MAE={mae:.2f}, RMSE={rmse:.2f}")这个加法成立的前提是残差序列被 LSTM 预测后可以反变换回原尺度。ARIMA 在去噪序列上预测,LSTM 在残差上预测,两者相加就是完整预测。对比时应同时打印arima_pred单独算的误差,能快速看出 LSTM 有没有增量。若加了 LSTM 后 RMSE 反而变大,优先怀疑残差本身是白噪声,而不是模型结构有问题。
4. 参数怎么调:ARIMA-SSA-LSTM 的必调参数与评估陷阱
模型跑通只是第一步。ARIMA-SSA-LSTM 真正花时间的是参数搜索和评估方式,这里给出我常用的调参顺序。
4.1 必调参数表:SSA窗口、主成分数、ARIMA阶数、LSTM lookback
| 参数 | 推荐起点 | 调参方向 |
|---|---|---|
| SSA 窗口 L | 主周期的 0.5~1.0 倍 | 太小只滤掉高频,太大会让重构序列滞后真实序列 |
| SSA 主成分数 top | 2~3 | 看奇异值能量占比,前几个贡献超过 80% 就不要再加 |
| ARIMA p/q 范围 | p 1~5,q 1~5 | 数据长度大可以放宽,长度小于 200 时不要超过 7 |
| ARIMA d | ADF 检验后取最小差分次数 | d 过大会损失差分信息,过小又非平稳 |
| LSTM lookback | 主周期的一半到 1 倍 | 样本量小则调小,周期明确则调大 |
| LSTM units | 24~64 | 残差波动剧烈可以适当加大,超过 128 容易过拟合 |
| epochs | 15~30 | 观察 validation loss,连续不变就停止 |
SSA 窗口 L 对结果影响最大。业务时间序列常有一个明显周期,月度数据先按 12 或 24 起步;如果序列没有固定周期,用滞后相关图找第一个显著相关的滞后位置作为 L。top不要一次性设太大,奇异值能量占比在 80% 到 90% 之间通常已经够用。
数据长度也会改变参数边界。样本只有 200 个点时,LSTM 的 lookback 设到 24 会让训练样本只剩 176 个,再留出验证集就更少,这时应优先降低 lookback 或增大 batch_size。相反,数据是小时级且有几万条,LSTM units 可以提到 64,epochs 提到 30,但要在每个轮次后记录验证集损失。
4.2 用 ADF 检验和奇异值贡献率决定是否保留 LSTM
残差序列是否值得用 LSTM 学习,先用 ADF 检验看平稳性,再用自相关图看有没有非线性滞后依赖。
from statsmodels.tsa.stattools import adfuller adf_p = adfuller(noise_train)[1] print(f"residual ADF p-value: {adf_p:.4f}")如果 p 值大于 0.05,残差不平稳,说明 SSA 的窗口或 top 没选好,先回去调分解参数,而不是加强 LSTM。如果 p 值小于 0.05 但残差自相关图在滞后 1 或滞后 2 处仍显著,说明 ARIMA 或 SSA 还漏掉了某些线性短期相关,也可以先把这部分交给更高阶 ARIMA,而不是立刻加 LSTM。
4.3 对照实验:ARIMA、LSTM、ARIMA-SSA-LSTM 的差异
为了确认组合模型的价值,需要做三个对照:单独 ARIMA、单独 LSTM、完整组合。单独 ARIMA 用原始训练序列,单独 LSTM 用原始训练序列的滑窗,完整组合用上述 SSA 去噪后的 ARIMA 加残差 LSTM。
| 模型 | 适合情况 | 主要风险 |
|---|---|---|
| 单独 ARIMA | 序列相对平滑、非线性弱 | 低估周期性转折点 |
| 单独 LSTM | 数据量大、模式复杂 | 小样本过拟合,预测不稳定 |
| ARIMA-SSA-LSTM | 趋势和噪声混杂、有弱周期 | 调参步骤多,残差建模失败时约为纯 ARIMA |
对比时还要区分一步预测和多步滚动预测。ARIMA 的一步预测误差通常很低,但如果业务要求连续预测 12 个点,ARIMA 的误差会快速放大;LSTM 在递归模式下同样放大,只是放大曲线可能更平滑。评估 ARIMA-SSA-LSTM 时,我一般同时输出 h=1、h=6、h=12 三类指标,因为真正上线的预警系统多数关心的是第 6 步之后的表现,而不是第一步。若 h=1 表现极好但 h=12 发散严重,优先检查残差递归预测的截断和窗口长度,而不是怀疑模型结构。
5. 从可复现到可上线:验证、重训练与调试技巧
组合模型从 Jupyter 推到线上时,最先崩的通常不是预测精度,而是保存、重训练和判断 LSTM 是否还有存在意义。
5.1 保留训练对象和归一化器
ARIMA 模型可以用 joblib 保存,LSTM 的 keras 模型与权重可以单独保存,但残差 Scaler 和 ARIMA 的预测结果都要一并保存,否则预测环节无法把残差反变换回原尺度。
from joblib import dump dump(arima_model, "arima_model.pkl") model.save("lstm_model.keras") dump(scaler, "resid_scaler.pkl")加载预测时,先用joblib.load读回arima_model和scaler,再用tf.keras.models.load_model读取 LSTM。注意不要把arima_model的拟合数据范围保存下来,线上新样本到来时,重训比尝试“增量预测”更可靠。
5.2 设置预测上限和重训练频率
LSTM 在递归预测中可能输出极端残差值,导致最终预测出现负流量或负负荷。上线前要对lstm_pred做截断,常见做法是限制在训练残差的 1.5 倍标准差内:
clip_upper = np.percentile(noise_train, 99.5) clip_lower = np.percentile(noise_train, 0.5) lstm_pred = np.clip(lstm_pred, clip_lower, clip_upper)重训练频率与数据到达周期相关。月度数据至少要每月重训一次,周级或日级数据建议每周滚动重训。每次重训时固定随机种子,并记录训练集的 MAE、RMSE、MAPE,方便发现数据漂移导致的突变。
5.3 用“常数残差”做基线,确认 LSTM 增量
最后一个技巧很实用:先把 LSTM 残差预测全部置为 0,只跑 SSA+ARIMA,得到一个基线误差;再启用 LSTM 看误差变化。如果 LSTM 带来的改善很小,直接省略这一层,反而能减少预测波动。实际调参时,我会对多个 L 值重复这一过程,用一组对比结果决定最终的 ARIMA-SSA-LSTM 配置。
下次换数据时,先把 L 设为明显周期的 0.5~1.0 倍,用“SSA+ARIMA+常数残差”做基线,确认 LSTM 增量大于其随机波动后再加入;这样多数数据集都可以避免在无用残差上浪费训练时间。
本文还有配套的精品资源,点击获取