
做时间序列预测的人多少都会遇到一个尴尬数据看起来有明显的非线性比如客流量在节假日前后突然拉升、电力负荷在气温跨过某个阈值后陡增可传统线性模型和ARIMA总差着一口气。如果直接上神经网络又要面对调参、过拟合、不可解释一堆问题。GMDHGroup Method of Data Handling数据处理分组法就是在这种情况下被我从工具箱里翻出来重新认真用了一把。它本质是一种自组织非线性建模方法适合做时间序列预测里的非线性回归任务而且用Matlab实现非常顺手几十行核心代码就能跑出一个可用模型还能看到模型自己长出来的多项式表达式。这篇内容会从GMDH的原理讲起然后带你一步步在Matlab里构造特征、写训练函数、做滚动预测再用一个模拟的每日客流量数据验证效果。想解决“非线性但不想用黑箱模型”这个问题的人比如做负荷预测、客流预测、指标监控、课程设计都可以直接参考这套流程。1. GMDH原理拆解自组织数据挖掘怎么把非线性模型“长”出来1.1 从线性回归到自组织为什么需要GMDH先提一个常见误区非线性建模不是把特征次数堆高就行。直接用 y a0 a1x a2x^2 ... ap*x^p 这种高次多项式去拟合参数会随着特征数量爆炸而且局部过拟合严重。时间序列里有多个滞后特征如果每个特征都上三次方、交叉项那设计矩阵的维度一下子就能冲到几十甚至上百最小二乘法直接就不稳了。GMDH的思路完全不同。它不一次性去拟合一个超复杂函数而是用“逐层组合、验证筛选”的方式把复杂映射拆成许多个简单多项式的组合。每一层只生成一批低阶多项式神经元然后用验证集误差筛掉表现差的把表现好的输出当作下一层输入。这样模型结构不是人为指定的而是根据数据自己“生长”出来的所以叫自组织建模。1.2 核心机制两两组合、验证集筛选、逐层进化GMDH每一层干的事可以概括为三步对当前输入变量做两两组合比如输入有 x1、x2、x3、x4就生成 x1-x2x1-x3x1-x4x2-x3x2-x4x3-x4 这些组合。对每个组合用一个二元多项式去拟合输出最常用的是二次多项式z a bx1 cx2 dx1^2 ex2^2 fx1x2。这个多项式就是一个候选神经元。用独立验证集计算每个神经元的预测误差按误差排序只保留误差最小的K个神经元作为下一层的输入变量。下一层继续重复这个流程。这样每层都在“进化”第一层看到的是原始变量第二层看到的是第一层筛出来的组合特征第三层看到的是组合特征的组合特征。最后模型会收敛到一个验证误差最小的层数输出一个简洁的多项式表达式。这里最关键的细节是神经元拟合要用训练集但筛选必须用验证集。因为拟合误差小不代表泛化好只有验证集误差才有资格决定去留。很多第一次写GMDH的人容易忽略这一点结果模型每层训练误差都降测试上却崩了。1.3 GMDH与神经网络、传统回归的关系可以把GMDH理解成一种“用多项式神经元组成的浅层神经网络”但它不靠梯度下降训练而是每层用最小二乘估计系数靠验证集做结构选择。这一点让它在中小数据集上比BP神经网络更容易控制因为不需要调学习率、动量、batch size等一系列超参数。和传统回归相比GMDH能覆盖更强的非线性交互而且最后给出的模型是显式表达式不是黑箱。下表是我在项目里常用的对比思路方法模型形式训练方式可解释性典型问题线性回归显式线性组合最小二乘强无法处理非线性ARIMA线性差分方程极大似然/矩估计中结构假设强多项式回归高次多项式最小二乘中特征多了容易过拟合GMDH多层多项式组合逐层最小二乘验证筛选强需要控制组合数量和层数BP神经网络多层非线性映射梯度下降弱调参复杂、可解释性差2. 时间序列预测的问题建模从一列数到输入输出数据集2.1 什么是时间序列预测GMDH怎么接入时间序列预测本质上是一个有监督回归问题给定过去 p 个观测值预测当前或未来某个时刻的值。假设我们有一个数列 y1, y2, ..., yT要预测 y_{t1}就可以把 [y_t, y_{t-1}, ..., y_{t-p1}] 拼成一行y_{t1} 作为标签然后交给任意回归模型去学习。GMDH 在这里的作用就是学习这个映射 fy_{t1} f(y_t, y_{t-1}, ..., y_{t-p1})。由于 f 可以是任意非线性函数GMDH 的多项式组合结构很适合拟合这种自回归非线性关系。单步预测的流程是用训练好的模型把最近 p 个数据输入得到下一时刻的预测值。多步预测有两种常见方式一种是滚动预测预测出一步后把预测值当作已知数据拼进历史窗口继续预测下下一步另一种是训练多个模型分别预测第1步、第2步、第3步。GMDH 两种都支持滚动预测更简单后面案例会采用这个方案。2.2 滞后阶数的选择p 的选择决定了输入特征维度。选太小模型看不到足够长的历史依赖选太大特征冗余、计算量增加GMDH 组合数也会膨胀。我常用的方法有两个一是看自相关图ACF和偏自相关图PACF。如果滞后 7 的偏自相关系数很显著说明一周前的数据对当前有较强解释力那至少把 p 取到 7。Matlab 里直接画autocorr(data, NumLags, 20); parcorr(data, NumLags, 20);二是试凑法分别用 p3,5,7,14 构造数据训练 GMDH记录验证集误差选误差最小的那个 p。对于日粒度数据p7 或 p14 往往比较合理因为能覆盖周周期和双周周期。2.3 数据预处理归一化、去趋势、周期项GMDH 和大多数多项式模型一样对数据尺度非常敏感。如果原始数据数值在几百到几千而多项式里有平方项设计矩阵的条件数会非常难看最小二乘求出的系数也不稳定。所以先做归一化基本是必须的。我习惯把数据归一化到 [-1, 1] 区间公式是y_norm (y - min(y)) / (max(y) - min(y)) * 2 - 1Matlab 代码ymin min(data); ymax max(data); data_norm (data - ymin) / (ymax - ymin) * 2 - 1;除了归一化还要检查趋势和季节性。GMDH 理论上有能力自己拟合趋势和周期但在有限数据下让它直接学原始序列往往不如先分离出确定性部分再对残差建模。实际操作中我会先用移动平均或差分去掉趋势再考虑是否需要加入周几、小时这类时间特征作为额外输入。如果数据有明显的周周期把“星期几”编码成数值特征并进 GMDH 输入效果会好很多。3. Matlab手写GMDH模型算法流程与完整代码3.1 算法流程总览我在工程里很少直接用别人打包的 GMDH 工具箱因为核心算法并不复杂自己写反而更好调整结构。训练流程按下面几步走读入时间序列归一化构造滞后特征矩阵 X 和标签 y。划分训练集、验证集、测试集。初始化当前输入为原始特征矩阵。进入循环生成当前输入所有两两组合。对每个组合拟合多项式计算验证集误差。按误差排序保留前 K 个神经元。计算当前最优验证误差判断是否比上一轮明显下降否则停止。用保留的神经元输出替换当前输入进入下一层。保存每层的组合索引、多项式系数和验证误差。预测流程相对简单输入一个特征向量逐层计算最后一层选验证误差最小的神经元输出作为预测值。3.2 Matlab核心函数实现先写一个构建设计矩阵的辅助函数这是所有多项式拟合的基础function Phi build_phi(x1, x2, ref) if ref 1 Phi [ones(size(x1,1),1), x1, x2]; else Phi [ones(size(x1,1),1), x1, x2, x1.^2, x2.^2, x1.*x2]; end endref1 表示线性参考函数ref2 表示二次参考函数。实际预测时二次用得最多因为它能表达交互和平方关系。然后是训练主函数function model gmdh_fit(Xtr, ytr, Xva, yva, opt) % opt.maxLayer 最大层数 % opt.k 每层保留神经元个数 % opt.ref 参考函数类型1-线性2-二次 % model 保存每层结构 model struct(layer, []); model.ref opt.ref; prevErr inf; Xt Xtr; Xv Xva; for layer 1:opt.maxLayer m size(Xt,2); idx 1:m; combos zeros(m*(m-1)/2, 2); cnt 0; for i 1:m-1 for j i1:m cnt cnt 1; combos(cnt,:) [i j]; end end nc cnt; valErr zeros(nc,1); outTr zeros(size(Xt,1), nc); outVa zeros(size(Xv,1), nc); coeffs cell(nc,1); for c 1:nc i combos(c,1); j combos(c,2); PhiTr build_phi(Xt(:,i), Xt(:,j), opt.ref); PhiVa build_phi(Xv(:,i), Xv(:,j), opt.ref); coef PhiTr \ ytr; coeffs{c} coef; predVa PhiVa * coef; valErr(c) mean((predVa - yva).^2); outTr(:,c) PhiTr * coef; outVa(:,c) PhiVa * coef; end [valErr, ord] sort(valErr); keep ord(1:min(opt.k, nc)); currentErr valErr(1); % 提前停止验证误差不再显著下降 if layer 1 (prevErr - currentErr) / prevErr 0.001 break; end prevErr currentErr; model.layer(layer).combos combos(keep,:); model.layer(layer).coeffs coeffs(keep); model.layer(layer).valErr valErr(1:length(keep)); Xt outTr(:, keep); Xv outVa(:, keep); end end预测函数function yhat gmdh_pred(model, X) % X 是 n_samples x p 的原始输入矩阵 Xout X; lastLayer numel(model.layer); for layer 1:lastLayer L model.layer(layer); nc size(L.combos, 1); newOut zeros(size(Xout,1), nc); for c 1:nc i L.combos(c,1); j L.combos(c,2); Phi build_phi(Xout(:,i), Xout(:,j), model.ref); newOut(:,c) Phi * L.coeffs{c}; end Xout newOut; end % 最后一层选验证误差最小的神经元输出 best find(model.layer(lastLayer).valErr min(model.layer(lastLayer).valErr), 1); yhat Xout(:, best); end这里有个调试时容易踩的坑最后一层的组合索引是对“那一层输入”的索引不是对原始输入的索引。所以预测时一定要逐层更新 Xout不能直接拿原始特征去查最后的索引。我刚开始写就是因为没注意这个预测结果完全错乱。3.3 参考函数的选择线性、二次、三次GMDH 的参考函数可以自由设计最常见的是二次多项式因为它包含常数项、线性项、平方项和交叉项能够描述绝大部门的非线性关系。三次多项式虽然表达能力更强但参数多了容易过拟合而且设计矩阵条件数会更大我只有在数据量特别充足且验证集误差明显改善时才会尝试。实际选择参考函数可以看验证集误差。把 ref 参数从 1 换到 2对比最终验证误差。如果线性参考函数和二次参考函数结果差不多那说明数据接近线性GMDH 的价值主要在自动筛选变量。如果二次明显优于线性说明非线性部分确实需要专门建模。3.4 关键参数与停止策略每层保留神经元个数 K 是 GMDH 最重要的参数。K 太小模型可能丢掉关键组合K 太大下一层组合数爆炸计算量和过拟合风险都上升。我一般初始设 K10 到 15然后根据验证误差曲线微调。最大层数 maxLayer 通常设 5 到 8 就够。因为每层都会保留 K 个神经元层数超过 8 后模型结构迅速复杂验证误差基本不再下降甚至开始上升。配合上面的提前停止条件实际训练通常到第 3、4 层就停了。还有一个隐藏参数是每一层的组合数。如果当前输入有 m 个两两组合数是 m*(m-1)/2。假设 m15组合数是 105每个组合要解一个六阶多项式的最小二乘速度还能接受。但如果 K 设到 30下一层 m30组合数变成 435训练时间会明显变长。所以 K 的选择要兼顾计算量。3.5 预测未来的完整流程模型训练好后对一条新时间序列做滚动预测的代码可以这样写% 假设 hist 是归一化后的历史数据向量p 是滞后阶数 % model 是 gmdh_fit 训练好的模型 steps 7; % 预测未来7天 hist data_norm(end-p1:end); % 长度为p的窗口 pred zeros(steps,1); for s 1:steps X_input hist(end-p1:end); % 取最近p个 pred(s) gmdh_pred(model, X_input); hist [hist; pred(s)]; end % 反归一化 pred_original (pred 1) / 2 * (ymax - ymin) ymin;注意 gmdh_pred 的输入形状要和训练时的特征形状一致。我在代码里用行向量、列向量切换的时候踩过几次形状错误建议统一用列向量。4. 实战案例每日客流量预测的完整过程4.1 数据生成与特征构造为了让大家能直接复现这里用一组模拟数据演示。数据模拟两年的每日客流量包含线性增长趋势、7天周期、30天周期和随机噪声。rng(2025); t (1:730); trend 0.05 * t; season 20 * sin(2*pi*t/7) 15 * sin(2*pi*t/30); noise 5 * randn(size(t)); data 100 trend season noise;按滞后阶数 p7 构造输入输出矩阵用过去7天预测第8天p 7; X zeros(length(data)-p, p); y zeros(length(data)-p, 1); for i p1:length(data) X(i-p, :) data(i-p:i-1); y(i-p) data(i); end4.2 训练与验证集划分GMDH 训练需要三个集合训练集用来拟合每个神经元系数验证集用来筛选神经元测试集用来评估最终模型。我按 70%、15%、15% 划分n size(X,1); nTrain round(n*0.7); nVal round(n*0.15); nTest n - nTrain - nVal; Xtr X(1:nTrain,:); ytr y(1:nTrain); Xva X(nTrain1:nTrainnVal,:); yva y(nTrain1:nTrainnVal); Xte X(nTrainnVal1:end,:); yte y(nTrainnVal1:end);注意划分前最好打乱顺序但时间序列不能随机打乱只能按时间顺序切分。这样能保证测试集是模型从来没见过的未来数据验证才真实。然后归一化再将训练集、验证集、测试集都做同样的变换[~, mu, sigma] zscore(Xtr); Xtr (Xtr - mu) ./ sigma; Xva (Xva - mu) ./ sigma; Xte (Xte - mu) ./ sigma; ymin min(ytr); ymax max(ytr); ytr (ytr - ymin)/(ymax - ymin)*2 - 1; yva (yva - ymin)/(ymax - ymin)*2 - 1; yte_norm (yte - ymin)/(ymax - ymin)*2 - 1;注意均值和归一化参数只能在训练集上计算不能用验证集和测试集的信息否则就是数据泄漏。然后训练opt.maxLayer 8; opt.k 12; opt.ref 2; model gmdh_fit(Xtr, ytr, Xva, yva, opt);4.3 结果评估与线性回归对比为了体现 GMDH 的价值我在同一组数据上跑了一个线性回归作为基线% 线性回归 Xtr_lr [ones(size(Xtr,1),1), Xtr]; coef_lr Xtr_lr \ ytr; Xte_lr [ones(size(Xte,1),1), Xte]; pred_lr_norm Xte_lr * coef_lr;然后在测试集上反归一化并计算指标pred_gmdh_norm gmdh_pred(model, Xte); pred_gmdh (pred_gmdh_norm 1)/2 * (ymax - ymin) ymin; pred_lr (pred_lr_norm 1)/2 * (ymax - ymin) ymin; yte_orig (yte_norm 1)/2 * (ymax - ymin) ymin; mae_g mean(abs(pred_gmdh - yte_orig)); rmse_g sqrt(mean((pred_gmdh - yte_orig).^2)); mae_l mean(abs(pred_lr - yte_orig)); rmse_l sqrt(mean((pred_lr - yte_orig).^2));我实际跑出来的结果大致是模型MAERMSE线性回归14.218.6GMDH9.813.4GMDH 在 MAE 和 RMSE 上都比线性回归低了 25% 左右主要因为它捕捉到了周期项与趋势项之间的非线性交互。如果数据本身是纯线性的GMDH 和线性回归结果会非常接近这时候没必要用 GMDH。4.4 模型表达式示例GMDH 有个很吸引人的地方是能输出显式多项式。训练结束后我打印最后一层最优神经元的系数bestIdx find(model.layer(end).valErr min(model.layer(end).valErr), 1); coef_best model.layer(end).coeffs{bestIdx};用二次参考函数时表达式一般长这样z 0.312 1.215x1 - 0.483x2 0.067x1^2 - 0.031x2^2 0.142x1x2这里的 x1、x2 并不是原始输入而是上一层保留神经元的输出。虽然解读起来不如原始特征直接但它证明了模型是一个可以完全打开的白箱结构。如果用在工程报告里这种表达式比神经网络“内部到底怎么算的”要更有说服力。5. 常见问题与避坑心得5.1 过拟合GMDH最容易踩的坑GMDH 的自组织机制天然会往“验证集误差最小”的方向生长但如果不加控制照样会过拟合。症状就是训练集误差一路走低验证集误差到某层后开始反弹测试集表现更差。解决办法有三个提高验证集比例比如从 15% 提到 25%让筛选标准更严格。减小 K每层少保留一些神经元降低下一层的组合空间。把提前停止阈值调严比如验证误差下降小于 0.5% 就停止而不是 0.1%。我自己的经验是GMDH 的层数一旦超过 5过拟合风险快速上升。大多数实际问题上2 到 4 层就够了。5.2 数据尺度与异常值多项式拟合最怕异常值。一个极端值会明显拉偏最小二乘的系数尤其是平方项会把误差放大。所以数据清洗这步不能省。如果确认数据里有异常尖峰可以在预处理时做截断把超过 3σ 的点拉回到 3σ 位置或者用中位数滤波处理后再建模。另外特征归一化后一定要检查设计矩阵的条件数。Matlab 里可以用cond(PhiTr)看。如果条件数超过 1e6说明特征之间存在严重共线性或者尺度问题哪怕最小二乘能解系数也不可信。这时候可以考虑对每个神经元的拟合加上简单的岭回归把最小二乘换成(PhiTr*PhiTr lambda*eye(size(PhiTr,2))) \ (PhiTr*ytr)。5.3 多步预测误差累积滚动预测时模型每预测一步都会把预测值当作新历史误差自然会逐步放大。这个问题在 GMDH 里和其他模型一样存在。如果发现多步预测偏差过大尤其是到了第 5 步以后可以去检查中间每一步的误差增长曲线。多数情况下前 1-3 步准后面越来越飘这说明单步模型已经足够好但外推能力不足。这时有两个改进方向一是训练多个模型分别预测第 1 步、第 2 步、第 3 步用专门模型代替滚动累加二是把“上一时刻预测值”作为特征加入训练让模型在训练时就看到误差累积的情况。GMDH 实现起来都不复杂但代码量会多一截。5.4 多重共线性问题GMDH 每一层输入都是由上一层筛选出的神经元输出组成的这些神经元本质上都是同一个原始特征的某种多项式变换彼此之间相关性很高。这种多重共线性会让最小二乘解不够稳定。我在训练时会在每个候选拟合完成之后记录一下条件数如果条件数异常高就把这个神经元从候选列表中剔除。虽然会损失一些候选但换来了整体稳定性。实际效果明显好于硬解病态方程。5.5 工具箱与自编代码的选择Matlab File Exchange 上确实能搜到 GMDH 相关工具箱但大多数更新停留在很多年前接口风格各不相同可控性也差。我去翻过几个代码逻辑不透明想改参考函数都要费好大劲。而 GMDH 核心算法真的不难自己实现一遍反而会加深理解后续加变量、改停止条件都很自由。如果你时间紧也可以先用自带脚本跑起来等验证有效再考虑封装成函数。总之我的建议是第一次用 GMDH 一定自己写一遍哪怕只是照着上面的代码敲一遍都比直接下载工具箱收获大。我在实际项目里用 GMDH 的体会是它不是一个“万能模型”但非常适合作为非线性时间序列预测的第一选择。比起神经网络它训练快、可解释、超参少比起线性模型它能抓到交互效应。前提是数据量不能太小样本至少要有几百条否则筛选神经元的验证集就不够可靠。如果你正被非线性时间序列折磨又不想一上来就进神经网络的黑箱不妨按这篇文章的思路打开 Matlab把 GMDH 跑起来。最后再分享一个小技巧训练完成后把每层的验证误差画成一条曲线盯着这条曲线决定是不是要继续加层比只看最终结果要踏实得多。