搞预测的人估计都有同感:数据量不够大的时候,神经网络不敢用,回归模型又经常被异常点带偏。这时候灰色模型往往是大家第一个想到的救兵。而如果你真正拿GM(1,1)去比赛或者做小样本预测,很快就会发现一个值得玩味的问题:累加生成、白化微分方程、最小二乘求解这套“老三样”,写在PPT里特别漂亮,可面对真实数据时经常一波三折——级比检验出界、还原误差莫名其妙偏大、预测结果总觉得少了一条趋势线。今天要聊的DNGM(1,1)模型,正是冲着这几个老毛病去的。
可以把它理解成“离散化的非齐次灰色预测模型”。它用差分递推替代微分方程,在驱动项里加入随时间变化的线性项,专门用来补传统GM(1,1)在非齐次增长、线性趋势这类小样本序列上的短板。如果你正在做销量预测、用电量预测、客流预测,或者论文里需要一个小样本建模方法,这篇东西值得花十分钟读完。
1. 传统GM(1,1):成名绝技与固有瓶颈
1.1 经典流程并不复杂,但背后有一整套“妥协”
先快速过一遍GM(1,1)的标准流程。它总共五步:第一步,准备非负的原始序列x(0)(k),k=1,2,…,n;第二步,对原始序列做一次累加生成(AGO),得到累加序列x(1)(k);第三步,假设累加序列满足一阶线性白化微分方程dx(1)/dt + a·x(1) = b;第四步,用背景值z(1)(k)=0.5·[x(1)(k)+x(1)(k-1)]做离散化,再用最小二乘法估计参数a和b;第五步,求出时间响应式,累减还原成原始序列的拟合值和预测值。
为什么GM(1,1)长期受欢迎?因为它抓住了小样本预测的核心矛盾:样本越少,越不能指望统计方法估计出复杂分布。累加就是那招“化随机为平滑”的关键——把一页一页看起来乱糟糟的进货台账,变成月底累计数,趋势一下就露出来了。灰色模型正是因为利用了这种平滑性,才敢在只有三五个数据点的时候出手。
但注意,这一步已经开始“妥协”了。GM(1,1)假设系统发展符合一阶微分方程,而这个方程的解天然是指数函数。它天生适合“每期变化量按固定比例增长”的数据,比如每年销售额增长10%。可现实中的小样本数据经常不是这种规律,而是“增长量本身在变化”:某产品一季度多卖5台,二季度多卖7台,三季度多卖9台。这不是齐次指数增长,而是线性趋势驱动的非齐次增长。把这种序列硬塞进齐次指数模型,误差自然会冒出来。
1.2 三大先天局限,越用越明显
我把这些年用GM(1,1)踩过的坑做了个总结,它们不是调参能解决的,属于模型设计层面的大问题。
第一,微分方程离散化的信息损耗。GM(1,1)在离散化时使用背景值z(1)(k),本质是用梯形面积近似积分。梯形公式默认累加曲线在相邻两点之间是直线,可实际累加序列往往更像指数曲线。步长越大,这个近似误差就越大,最后全部传导到发展系数a上。你检查参数时可能会发现a明明估得很准,但还原出来的原始序列却对不上,十有八九就是这一步损耗掉了。
第二,模型、估计、预测三层不一致。白化方程是连续形式,参数却由离散数据估计,预测时又回到离散还原。相当于你准备用一把欧式尺子量零件,却用英式刻度标了读数,到最后再换算回去,误差在两头都要承担一遍。这种不一致对于理论推导可能无所谓,但工程落地时很痛。
第三,驱动项过于朴素。经典GM(1,1)的方程右侧只有一个常数b,相当于假设系统外部作用力恒定不动。可许多实际系统的外部作用本身随时间变化:促销力度逐月加大、市场渗透率逐步提升、政策每年加码。恒定驱动项拟合不出这种动态。这也是为什么很多老手对GM(1,1)的结论又爱又恨:小样本场景下它确实能算出结果,但结果经常是“看着有趋势,细看差口气”。
2. DNGM(1,1)的核心原理:差分递推+非齐次驱动项
2.1 命名拆解:D、N、G、M各自代表了什么
DNGM(1,1)这个缩写,在灰色系统文献里最常被解释成Discrete Non-homogeneous Grey Model,也就是离散非齐次灰色模型。数字(1,1)和GM(1,1)的含义一样:一阶差分、一个系统变量。D代表“离散化”,N代表“非齐次”,这两个词几乎就是它存在的全部理由。
市面上关于这类模型的叫法并不完全统一。有人把它写成NGM(1,1,k)的离散改进版,有人直接叫DGM(1,1)的扩展,不同的论文里细节略有出入。但这不影响理解核心思想:把传统GM(1,1)的连续白化方程换成一族线性差分递推关系,并在递推项中加入时间变量k。只要抓住这个骨架,文献里再换什么马甲你都能认出来。
2.2 模型式子与计算逻辑
设原始序列为x(0)(1), x(0)(2), …, x(0)(n),一次累加后得到x(1)(1), x(1)(2), …, x(1)(n)。DNGM(1,1)采用的递推式是:
x(1)(k) = β1 · x(1)(k-1) + β2 · k + β3,k=2,3,…,n
右边第一项是累加序列的“惯性项”,第二项是随时间变化的线性驱动项,第三项是常数驱动项。三个参数共同决定下一个累加值。
这个式子看起来像一元回归,但它和普通回归有明显区别:回归预测的是原始输出,这里预测的是累加序列。原始序列的随机扰动经过累加后被压制了,递推式的输入噪声自然更小。这和深度学习里“对信号先做移动平均再预测”的思路一个道理。
参数β1、β2、β3用最小二乘估计。把k=2到n的式子写成矩阵形式:
Y = B·θ
其中Y是x(1)(2)到x(1)(n)组成的列向量,B的第一列是x(1)(1)到x(1)(n-1),第二列是时间点2,3,…,n,第三列全是1。解为θ = (BᵀB)⁻¹BᵀY。矩阵维数只有3×3,计算负担极小,这也是灰色模型家族适合工程落地的优势之一。
2.3 为什么离散递推能突破传统局限
用一句话概括:DNGM(1,1)让“模型假设”和“数据生成过程”更一致了。
把x(1)(k)想象成月底累计销量。GM(1,1)的做法是先假设每天销量按固定比例增长,拟合出一条连续指数曲线,再反过来逐日还原。DNGM(1,1)的做法则更直接:用上月底累计值,加上本月的增长规律,算出本月底累计值。每一步都在模仿真实世界里数据的递推过程,中间少了“连续曲线→离散取值”这一步转换,误差自然更小。
再看非齐次项β2·k带来的能力扩展。GM(1,1)的指数解相当于给每个相邻点的变化量绑定了同一个增长因子,而β2·k允许这个变化量随着时间线性增减。数据整体呈线性上升时,β2会主导;数据呈指数上升时,β1会主导;两者叠加时,模型还能自动分配权重。一个小式子覆盖了更多数据形态,这叫结构性增强,不是单纯调参可以媲美的。
3. 手把手建模实操:从级比检验到参数估计
3.1 建模前检查:级比检验和预处理
建模前先看数据配不配得上灰色模型。拿一个我常用的示意数据:某产品近五期销量为15、20、27、36、49。原始序列必须非负,这点不用多说;随后要算级比λ(k)=x(0)(k-1)/x(0)(k),看是否落在可行区间内。当样本量n=5时,可行区间是e^(-2/6)到e^(2/6),也就是约0.7169到1.3956。
这段序列的级比分别是20/15=1.3333、27/20=1.35、36/27=1.3333、49/36=1.3611,全部落在区间内,说明数据符合灰色建模的基本要求。
如果级比出界怎么办?常见做法是对整个序列做平移变换,也就是每个数加上同一个常数c。平移量太小不起作用,太大又可能把趋势变化抹平,我的习惯是写个循环,从c=1开始逐次增加,直到所有级比都落在可行区间。也可以用开方变换或对数变换,但要注意变换会影响还原后的误差指标,后续解释起来会麻烦很多。这一步请务必不要跳过,级比检验是灰色模型的“安全网”,我见过太多人图省事直接建模型,结果后验差比值惨不忍睹。
3.2 一次累加与矩阵装配
原始序列做一次累加得到:
x(1) = {15, 35, 62, 98, 147}
把k=2,3,4,5代入递推式,得到四个方程。矩阵形式如下:
Y = {35, 62, 98, 147}ᵀ
B的第一列是x(1)(1)到x(1)(4),即15、35、62、98;第二列是2、3、4、5;第三列是1、1、1、1。
完整的矩阵B是:
| 15 | 2 | 1 | | 35 | 3 | 1 | | 62 | 4 | 1 | | 98 | 5 | 1 |
计算BᵀB和BᵀY后,解最小二乘方程得到三个参数:
β1 ≈ 1.3769(累加序列惯性系数) β2 ≈ -0.8037(线性驱动项系数) β3 ≈ 16.0235(常数驱动项)
β1在1附近,说明累加序列保留了很强的惯性;β2为负,说明只看时间项的话,增长速度在边际上有所放缓。这两个系数放在一起,正好模拟了“基数变大以后增长放缓”的真实场景。不少第一次接触这个模型的人看到β2是负值会慌,其实完全没必要,它是模型表达非线性趋势的方式。
3.3 预测还原与误差对照
外推第6期时,以实际累加终值147作为递推起点:
x(1)_hat(6) = 1.3769 × 147 + (-0.8037) × 6 + 16.0235 ≈ 213.63
第6期销量预测值 = 213.63 - 147 ≈ 66.63。
如果这期真实销量是64,相对误差约4.1%;如果真实值是67,相对误差只有0.6%。样本内的拟合值也很有说服力:
| 期数 | 真实值 | DNGM拟合值 | 绝对误差 |
|---|---|---|---|
| 1 | 15 | 15.00 | 0 |
| 2 | 20 | 20.07 | 0.07 |
| 3 | 27 | 26.85 | 0.15 |
| 4 | 36 | 36.16 | 0.16 |
| 5 | 49 | 48.99 | 0.01 |
平均相对误差MAPE约0.35%,在我经手的小样本案例里属于相当能打的表现。实际项目中我还会补算后验差比值C和小误差概率p,一级精度基本是常态。
3.4 十行Python搞定整个流程
把上面的流程落到代码,其实就是个10多行的小函数。我这里用numpy的lstsq做最小二乘求解,会比直接求逆矩阵更稳。
import numpy as np def dngm11(x0, steps=1): x0 = np.asarray(x0, dtype=float) x1 = np.cumsum(x0) n = len(x0) y = x1[1:] B = np.column_stack([x1[:-1], np.arange(2, n + 1), np.ones(n - 1)]) theta, *_ = np.linalg.lstsq(B, y, rcond=None) b1, b2, b3 = theta # 样本内拟合值重建,用于误差检验 fitted_x1 = np.empty(n) fitted_x1[0] = x1[0] for k in range(1, n): fitted_x1[k] = b1 * fitted_x1[k-1] + b2 * (k + 1) + b3 fitted = np.r_[fitted_x1[0], np.diff(fitted_x1)] # 滚动外推,以真实累加终值为起点 outs = [] cur = x1[-1] idx = n + 1 for _ in range(steps): nxt = b1 * cur + b2 * idx + b3 outs.append(nxt - cur) cur = nxt idx += 1 return fitted, np.array(outs), [b1, b2, b3] x0 = [15, 20, 27, 36, 49] fitted, forecast, beta = dngm11(x0, steps=1) print("拟合值:", fitted) print("预测值:", forecast) print("参数:", beta)运行结果大致是:
- 拟合值:[15.0, 20.07, 26.85, 36.16, 48.99]
- 预测值:[66.63]
- 参数:[1.3769, -0.8037, 16.0235]
代码里有个细节值得说明:滚外推时我用的是真实累加值x1[-1]作为起点,而不是拟合序列的最后一个值。原因是灰色模型外推时,以当前最新真实状态为起点能省掉前面所有误差的累积,这是很多论文里默认的处理方式。如果你希望完全依赖模型递推,也可以用拟合序列继续滚,但要意识到误差会随步数增加而逐渐放大。
4. 模型选型对比与检验指标:DNGM(1,1)值不值得换
4.1 同一份数据,GM和DNGM的对照
我把同一份示意数据分别塞进经典GM(1,1)和DNGM(1,1),看结果差异有多大。GM(1,1)先算出背景值序列25、48.5、80、122.5,再做最小二乘,得到发展系数a约-0.2967、灰作用量b约12.53。用时间响应式还原后,拟合结果如下:
| 期数 | 真实值 | GM(1,1)拟合值 | DNGM(1,1)拟合值 |
|---|---|---|---|
| 1 | 15 | 15.00 | 15.00 |
| 2 | 20 | 19.77 | 20.07 |
| 3 | 27 | 26.60 | 26.85 |
| 4 | 36 | 35.77 | 36.16 |
| 5 | 49 | 48.17 | 48.99 |
| 平均相对误差 | - | 约1.24% | 约0.35% |
(第1期作为初值给定,不参与误差统计)
这个对比不是全面的实验,但趋势非常直观:DNGM的每一期拟合点都更贴近真实值,尤其最后一期,GM已经出现“增速跟不上”的迹象,DNGM仍然跟得比较紧。原因就是那个非齐次项β2·k补偿了线性趋势。
我把这视为一个实用经验:只要原始数据不是教科书里那种完美指数序列,DNGM至少在样本内拟合上会压制GM(1,1)。而现实中又有多少数据是完美指数呢?所以我的默认选型已经悄悄变成了DNGM。
4.2 误差检验的两个常用指标
模型建完不能只看吻合度够不够漂亮,还要检验可靠性。两个最通用的指标是后验差比值C和小误差概率p。C = S2 / S1,其中S1是原始序列的标准差,S2是残差序列的标准差。C小于0.35时模型精度为一级,0.35到0.5之间为二级,0.5到0.65为三级,大于0.65基本要返工。
在刚才的例子里,原始序列均值是29.4,标准差约12.08;残差序列的标准差约0.13,C算下来约0.01,属于很优秀的一级精度。p是残差绝对值小于0.6745倍S1的概率,这个例子里p接近1。
需要提醒的是,C指标对残差标准差非常敏感,而这恰恰暴露了灰色模型的软肋:如果原始序列里混进一个离谱的异常点,残差标准差会被瞬间拉大,后验差比值直接爆表。建模前做一次数据清洗,把明显的促销高峰、临时断货这类异常点剔除或修正,比后边用什么高级模型都重要。
4.3 什么场景别盲目切换
DNGM(1,1)不是万能药,它的家族边界仍然清晰。如果你的数据带明显周期性跳跃,比如电商平台大促前后的销量尖峰,灰色模型整个家族都会失准,此时应先用更精细的数据清洗处理周期分量,或者干脆换用能处理季节性模型。如果数据已经超过30期,又是相对平稳的序列,ARIMA、指数平滑等经典时序模型通常更容易驾驭。
灰色模型的看家本领始终是小样本、短趋势、中等噪声。DNGM(1,1)把传统GM(1,1)的本领范围扩大了一圈,但没有改变家族边界。你手上的数据如果少于4期,或者中间有大量负值和突变,换什么灰色变体都救不回来,不如直接改用简单的均值回归。
5. 实战进阶:把DNGM(1,1)用出花的三种姿势
5.1 滚动窗口更新参数
用DNGM(1,1)做周期性预测时,我强烈建议不要一次建完模型就一劳永逸。每拿到一个新的实际值,就把最早一个样本剔除,用最近n期重新估计β1、β2、β3。模型参数就不会老化。
举个例子,刚才的五期序列预测出第6期66.63后,如果第6期实测值回来了是64,那么下一轮建模就改用20、27、36、49、64这五个数,重新估计参数再预测第7期。这一步成本几乎为零,但效果立竿见影。我在实际项目里踩过很多次坑,最后发现灰色模型长期预测翻车,绝大多数不是模型的问题,而是“参数保鲜”没做好。
5.2 残差二次建模
模型的拟合值和真实值之间总会留下一段残差,这段残差里有时还藏着可挖掘的小趋势。我的标准操作是:先训练DNGM主模型,算出残差序列,再对残差序列单独建一个DNGM(1,1),把它的预测结果叠加到主模型预测上。
思路类似机器学习里的残差学习。注意两个前提:一是残差序列要满足非负和级比可接受,如果含有负数,要做整体平移或绝对值处理;二是叠加后要检查最终残差是否真的降低了,如果没降低,说明残差已经接近白噪声,强行建模只是在拟合噪音,没有任何泛化意义。
5.3 多序列并行建模与组合预测
做电商库存或者多门店销售预测时,单个SKU或单个门店往往只有七八个数据点。单独建回归模型很不靠谱,但把每个序列分别用DNGM(1,1)滚动建模,就能保留每条序列自身独有的增长形态。
所有子序列预测完再汇总,常常比“先把所有数据汇成一条大序列、再预测总量”更稳。原因在于每条子序列的异质性被单独建模吸收了,而不是在汇总阶段被平均掉。如果某条子序列的级比检验始终不通过,优先做平移变换,实在不行才考虑剔除该序列,因为它可能已经不满足灰色建模的前置条件。
最后再分享一个很多人不知道的小技巧:DNGM(1,1)对初值x(0)(1)非常敏感。有些变体会把初值也当作待估参数参与最小二乘优化,而不是直接用原始首项。如果你发现外推结果对首期数据依赖过重,可以把初值纳入参数估计,算完后再比较两版预测精度,选择误差更小的方案。这招在我处理几组回归趋势很强的数据时,确实把预测误差再压低了10%以上。