ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多作物混装LSTM预测包:EMD/EEMD/小波预处理与一元多元差分多步建模实战

多作物混装LSTM预测包:EMD/EEMD/小波预处理与一元多元差分多步建模实战

简介:这份资源围绕LSTM模型在时间序列预测中的完整实践展开,面向具备一定深度学习基础、希望系统掌握序列建模与预测流程的学习者与开发者。内容从信号预处理到模型搭建层层递进,涵盖经验模式分解、增强经验模式分解与小波分析等数据预处理手段,并对比多元与一元LSTM、差分处理、多步预测以及简单RNN、反向传播等基础实现,帮助读者理解门控机制如何缓解梯度消失、捕获长期依赖。资源包共350个文件,以82个ipynb交互式笔记、39个py脚本、18个h5模型权重、68张png结果图及csv、xlsx数据文件为主,另含少量配置与说明文档,整体约14.63MB,结构便于按主题检索复现。目前已有3908人学习下载,适合作为时间序列预测的实战参考与代码模板,帮助读者快速搭建从分解、建模到评估的完整链路。

1. 从一份“多作物混装”的 LSTM 预测包说起:它到底能跑出什么

如果你手头有一批按品种切好的时间序列 CSV,又不想从零搭一套 LSTM 时间序列预测的脚手架,这个资源包值得先拆开看。它把苹果、土豆、猪、姜四类序列数据放在一起,同时塞进了 EMD、EEMD、小波三种预处理路线,以及一元、多元、差分、多步四套 LSTM 建模脚本,外加一个 SimpleRNN 和 BP 作为对照。换句话说,它不是单个模型 demo,而是一份把“信号分解 + 序列建模 + 多步预测”串起来的实验合集。适合已经会写 Keras 或 PyTorch 基础层、但没想清楚预处理和模型怎么搭配的从业者;也适合拿它当模板,替换成自己行业的序列数据。文件命名里的interpolate暗示插值补全,difference暗示差分去趋势,这些细节决定了它能不能直接套到你的数据上。

2. 数据与预处理:EMD、EEMD、小波三条路怎么选

2.1 先看清 CSV 的排列逻辑

资源包里的 CSV 按品种分两类:原始序列(apple.csv、potato.csv、pig.csv、ginger.csv)和插值后序列(apple_interpolate.csv、potato_interpolate.csv、pig_interpolate.csv)。插值文件通常是为了补齐缺失时间点或统一采样频率,常见做法是线性插值或样条插值。拿到手先别急着喂模型,用 pandas 做一次完整性检查,确认时间列是否单调、缺失比例是否超过 10%。如果缺失太多,插值会引入虚假趋势,后面 EMD 分解出来的 IMF 分量会失真。

import pandas as pd import numpy as np # 以苹果为例,先看原始与插值后的差异 raw = pd.read_csv('apple.csv', parse_dates=['date']).set_index('date') interp = pd.read_csv('apple_interpolate.csv', parse_dates=['date']).set_index('date') print('原始缺失数:', raw['value'].isna().sum()) print('插值后缺失数:', interp['value'].isna().sum()) print('原始时间跨度:', raw.index.min(), '->', raw.index.max()) print('插值后时间跨度:', interp.index.min(), '->', interp.index.max()) # 检查插值是否改变了统计分布 print('原始均值/方差:', raw['value'].mean(), raw['value'].var()) print('插值均值/方差:', interp['value'].mean(), interp['value'].var())

这段代码的逻辑是先对齐时间索引,再对比缺失数量和分布变化。参数上,parse_dates指定时间列,set_index把时间变成索引方便后续重采样。如果插值后方差明显缩小,说明插值过度平滑,后续建模要考虑加权或改用更保守的填充策略。

2.2 EMD 与 EEMD 的分解差异

EMD.ipynb和EEMD.ipynb分别对应经验模式分解和增强经验模式分解。EMD 把非平稳序列拆成若干 IMF 和一个残差,适合非线性信号;但 EMD 有模态混叠问题,同一频段可能散落在不同 IMF 里。EEMD 通过加白噪声多次分解再取平均,缓解混叠,代价是计算量成倍增加。选型上,如果序列长度小于 500 点,EMD 够用;超过 1000 点且噪声明显,优先 EEMD。常见做法是分解后只保留前几个高频 IMF 做去噪,把残差和低频 IMF 留给 LSTM 学趋势。

from PyEMD import EMD, EEMD import numpy as np signal = interp['value'].values # EMD 分解 emd = EMD() imfs_emd = emd.emd(signal) # EEMD 分解,trials 控制加噪次数 eemd = EEMD() eemd.trials = 100 imfs_eemd = eemd.eemd(signal) print('EMD IMF 数量:', imfs_emd.shape[0]) print('EEMD IMF 数量:', imfs_eemd.shape[0])

参数说明:trials是 EEMD 的集成次数,100 是常用起点,低于 50 结果不稳定,高于 200 收益递减。分解后每个 IMF 单独做归一化再拼回多维输入,这是EEMD_LSTM.ipynb的核心思路。

2.3 小波分析作为替代预处理

Wavelets.ipynb走的是另一条路:用离散小波变换把序列拆成近似系数和细节系数,再重构回去。相比 EMD,小波需要选母小波和分解层数,选错母小波会导致频率泄漏。常见做法是选db4或sym8,分解层数按log2(N)取整。小波的优势是计算快、可逆,适合在线预测场景;劣势是基函数固定,对突变信号的适应性不如 EMD。

import pywt signal = interp['value'].values # 选用 db4 小波,分解 4 层 coeffs = pywt.wavedec(signal, 'db4', level=4) # 重构时把最后一层细节系数置零做去噪 coeffs[-1] = np.zeros_like(coeffs[-1]) denoised = pywt.waverec(coeffs, 'db4') print('原始长度:', len(signal), '去噪后长度:', len(denoised))

这里level=4是经验值,序列越长层数可以越高,但超过 6 层后边界效应会吃掉两端数据。重构后长度可能比原始多一个点,需要截断对齐。

3. 一元、多元与差分 LSTM:模型结构怎么落地

3.1 一元 LSTM 与差分处理的边界

lstm_univariate.ipynb是最小可用单元:单特征输入,滑动窗口构造样本,LSTM 输出下一步预测。lstm_univariate_difference.ipynb在此基础上先做一阶差分,把趋势去掉再建模,预测完再累加回去。差分适合有明显上升或下降趋势的序列,比如猪价长期上涨;但如果序列本身平稳,差分反而放大噪声。判断标准是 ADF 检验,p 值大于 0.05 才考虑差分。

from statsmodels.tsa.stattools import adfuller series = interp['value'].values result = adfuller(series) print('ADF 统计量:', result[0], 'p 值:', result[1]) if result[1] > 0.05: diff_series = np.diff(series) print('做一阶差分,新长度:', len(diff_series)) else: diff_series = series print('序列平稳,不做差分')

参数上,adfuller默认带常数项不带趋势项,如果序列有明显趋势要加regression='ct'。差分后的序列少一个点,构造滑动窗口时要注意对齐原始时间轴。

3.2 多元 LSTM 的输入组织

lstm_multivariate.ipynb处理多维输入,典型场景是把苹果、土豆、猪、姜四个序列拼成一个矩阵,让模型学跨品种的关联。输入张量形状是(样本数, 时间步, 特征数),特征数等于序列条数。常见坑是不同品种量纲差异大,必须先做标准化,否则 LSTM 的门控会被大量纲特征主导。

from sklearn.preprocessing import StandardScaler # 假设四个序列已经对齐成 DataFrame,列名为 apple, potato, pig, ginger data = pd.concat([apple, potato, pig, ginger], axis=1).dropna() scaler = StandardScaler() scaled = scaler.fit_transform(data) # 构造滑动窗口,window_size 为 30 def make_windows(arr, window_size): X, y = [], [] for i in range(len(arr) - window_size): X.append(arr[i:i+window_size]) y.append(arr[i+window_size, 0]) # 以 apple 为预测目标 return np.array(X), np.array(y) X, y = make_windows(scaled, 30) print('输入形状:', X.shape, '标签形状:', y.shape)

这里window_size=30是超参数,序列周期性强可以取一个周期长度,比如月度数据取 12。y只取 apple 那一列,如果要多目标预测就把y改成二维。

3.3 多步预测的实现方式

lstm_univariate_multistep.ipynb做的是预测未来多个时间点。有两种常见策略:直接多输出(模型最后一层 Dense 节点数等于步数)和递归预测(预测一步后把结果喂回输入)。直接多输出训练稳定但步数多了精度下降;递归预测灵活但误差会累积。资源包里大概率用的是直接多输出,因为代码结构更简单。

from tensorflow.keras import layers, models model = models.Sequential([ layers.LSTM(64, activation='tanh', input_shape=(30, 1)), layers.Dense(7) # 预测未来 7 步 ]) model.compile(optimizer='adam', loss='mse') model.summary()

LSTM(64)的 64 是隐藏单元数,序列复杂度高可以加到 128,但要注意过拟合。Dense(7)对应多步输出,如果改成递归预测,这里就是Dense(1),外面套循环。

4. 避坑与排查:跑不通时先看这几处

4.1 损失不下降,输出一条直线

现象:训练几十轮后 loss 卡在某个值,预测结果几乎是常数。原因通常是输入没有归一化,或者差分后序列接近白噪声,LSTM 学不到东西。解决:先确认StandardScaler或MinMaxScaler是否作用在训练集上,再检查差分后的 ADF 检验 p 值是否过小。如果序列本身随机性太强,换 EEMD 分解后取低频分量再试。

4.2 EEMD 分解报内存错误

现象:跑EEMD.ipynb时进程被 kill,或者trials设到 100 以上就卡死。原因:EEMD 每次 trial 都做一次完整 EMD,序列长、trials 多时内存线性增长。解决:把trials降到 50,或者先用小波去噪再 EEMD。另一个办法是分块分解,每 500 点一段,段间重叠 50 点。

4.3 多步预测误差累积过快

现象:预测第 1 步还准,第 7 步完全偏离。原因:递归策略下每步误差被放大,或者直接多输出时训练标签没有做相应缩放。解决:改用直接多输出,并在损失函数里给远期步数更高权重;或者把多步拆成多个单步模型,每个模型只预测固定步长。

4.4 插值文件与原始文件时间轴对不上

现象:把apple.csv和apple_interpolate.csv合并时出现大量 NaN 或重复索引。原因:插值文件可能改了时间频率,比如从日频变成周频,或者时区不一致。解决:统一用pd.to_datetime解析后resample到同一频率,再reindex对齐。对齐后检查缺失比例,超过 20% 就放弃这条序列。

4.5 SimpleRNN 与 LSTM 的结果倒挂

现象:SimpleRNN.ipynb的测试集误差比 LSTM 还低。原因:序列很短(小于 50 步)时,LSTM 的门控参数还没训好,SimpleRNN 反而更稳;或者 LSTM 过拟合了训练集。解决:先确认序列长度,短序列优先 SimpleRNN;如果坚持用 LSTM,把隐藏单元降到 32,加 Dropout 0.2,并早停。

5. 进阶技巧:把 EEMD 分量喂给多元 LSTM 的完整链路

走到这里,最值得试的一条链路是:对每个品种先做 EEMD,取前 3 个 IMF 和残差,拼成 4 维特征,再用多元 LSTM 预测 apple 的下一步。这样既保留了分解去噪的好处,又让模型学到跨分量的关联。具体做法是先把EEMD_LSTM.ipynb里的分解部分抽出来,对四个 CSV 各跑一遍,把 IMF 对齐后存成新的 DataFrame。

from PyEMD import EEMD import pandas as pd import numpy as np def eemd_features(series, trials=50, n_imfs=3): eemd = EEMD() eemd.trials = trials imfs = eemd.eemd(series) # 取前 n_imfs 个 IMF,不足补零 selected = imfs[:n_imfs] while len(selected) < n_imfs: selected = np.vstack([selected, np.zeros_like(series)]) residual = series - selected.sum(axis=0) return np.vstack([selected, residual]).T # 形状 (N, n_imfs+1) # 对四个品种分别提取特征 features = {} for name in ['apple', 'potato', 'pig', 'ginger']: df = pd.read_csv(f'{name}_interpolate.csv', parse_dates=['date']).set_index('date') features[name] = eemd_features(df['value'].values) # 对齐长度后拼成多元输入 min_len = min(v.shape[0] for v in features.values()) X_multi = np.hstack([features[n][:min_len] for n in ['apple', 'potato', 'pig', 'ginger']]) print('多元特征形状:', X_multi.shape)

这段代码的关键参数是n_imfs=3,取太多会引入高频噪声,取太少会丢趋势。trials=50是内存和稳定性的折中。拼好后按第 3 章的滑动窗口构造样本,输入维度变成n_imfs+1乘以品种数。训练时把 apple 的残差作为预测目标,因为残差代表长期趋势,比原始序列更平滑。

验证方法上,不要只看 MSE。把预测结果做一阶差分后和真实差分算方向准确率,方向对了才有交易或调度价值。我一般会留最后 20% 做测试,前 80% 里再切 10% 做验证,早停耐心设 10 轮。如果验证 loss 连续 10 轮不降就回滚到最佳权重。

从那以后我每次拿到新的序列包,都强制先跑一遍 ADF 和缺失检查,再决定要不要差分和插值。这套流程帮我省掉了至少三次“模型没问题、数据有问题”的翻车。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进