ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

灰色马尔科夫模型:GM(1,1)结合马尔科夫链的小样本预测方法

灰色马尔科夫模型:GM(1,1)结合马尔科夫链的小样本预测方法 搞预测的这些年我有个深有体会的结论最怕的不是数据太少而是数据“看起来有趋势实际上到处是坑”。你用最简单的GM(1,1)去拟合它能给你一条很丝滑的指数曲线但碰上真实数据里的突然下挫、季节性回弹预测值就像在走钢丝一个波峰就直接击穿你的置信区间。灰色马尔科夫模型就是为这种场景准备的灰色GM(1,1)负责把趋势骨架搭出来马尔科夫链负责吃掉残差里的波动规律两头各管一段组合起来对付“有趋势、有波动”的小样本数据特别能打。这篇我来完整拆解这套方法从模型思路、数学推导、Matlab代码实现到案例实测和踩坑记录都会讲到。适合正在做课设、毕设或者日常需要做短期指标预测比如销量、用电量、游客量、故障数的读者。你不需要懂很深的随机过程也不需要背公式跟着代码走一遍就能用起来。1. 为什么要把灰色模型和马尔科夫链放在一起1.1 单纯用GM(1,1)会栽在哪灰色模型的理论基础是灰色系统理论核心假设是“部分信息已知、部分信息未知”。GM(1,1)本质上是对原始数据做累加生成把一组参差不齐的数列变成一条近似指数增长的曲线然后用一阶线性微分方程去拟合。对小样本、单调增长的数据这个思路非常漂亮四五个点就能建模。但问题也出在这个“近似指数”上。一旦数据里出现明显的谷底或峰值GM(1,1)在拟合阶段就会把极值“抹平”。比如我做过用电量预测某年因为气温异常导致夏季负荷骤增GM拟合值在这年附近明显落后于实际值到了下一年气温回归正常GM又继续按指数上涨把实际值远远抛在后面。这不是代码bug而是模型特性决定的它只认趋势不认波动。更有意思的是GM的残差往往不是随机噪声而是成团出现的。连续两三个点偏正紧接着又连续两三个点偏负。这些规律被丢弃非常可惜而马尔科夫链恰好是处理“状态转移规律”的经典工具。1.2 马尔科夫链补的究竟是什么马尔科夫链的核心性质叫无后效性未来的状态只与当前状态有关与更早的历史无关。听起来好像限制很大但在工程里恰恰是一种合理的简化。我不需要知道前五个月的数据只要知道残差目前落在“偏高区间”还是“偏低区间”就能查表知道下一步最可能跑到哪个区间。套用到GM残差修正上思路就非常清晰了先用GM算出历史段的拟合值计算每个点的相对误差把相对误差按大小划分成若干个状态区间比如“严重低估”“略微低估”“略微高估”“严重高估”统计状态之间的转移概率得到一个转移矩阵对未来的预测点从当前残差状态出发按最大转移概率找到未来最可能落入的状态用该状态的残差均值反过来修正GM的未来预测值。一句话总结就是GM给你一个骨架马尔科夫负责往骨架上补肉。单看GM预测值是一条单调曲线组合之后曲线会根据历史波动特征自动“拱起来”或“压下去”。1.3 这套组合适合用在哪些地方不是所有数据都适合灰色马尔科夫我整理了几个典型场景场景数据特征为什么适合区域用电量月/年度预测整体增长、受季节和气温扰动GM抓趋势马尔科夫修负荷波动景区游客量预测每年上升、节假日峰值明显残差状态能反映“旺季高估/低估”规律产品月度销量预测上升期伴随促销波峰短期1-3步预测非常稳设备故障频次统计缓慢上升、偶发跳变样本量小传统ARIMA不适用最推荐的使用窗口是样本量10到30个、预测步数1到3步。样本再少GM撑不起趋势样本再多且周期性强ARIMA或季节分解效果更好预测步数一旦超过5步马尔科夫修正的误差会同步累积翻车概率明显上升。2. 灰色GM(1,1)建模全流程先把参数吃透2.1 级比检验数据适不适合GM先过这关所有用GM的人第一步都该做级比检验。级比的定义是sigma(k) x0(k-1) / x0(k)如果序列长度为n那么级比理论可容区间是[ exp(-2/(n1)), exp(2/(n1)) ]只有当全部级比都落在区间内GM(1,1)才有意义。我用上面的用电量数据算过n8时区间约为[0.8007, 1.2488]八年的相邻级比基本都落在里面比如35.2/38.7≈0.909650.6/53.1≈0.9529。这说明数据适合建模。如果检验不通过最常用的补救是平移变换给原始序列整体加一个常数c让它变得更“平滑”建模完成后再把c减回去。注意c不能太小一般取数据绝对值的两倍左右起步。取对数或开方也可以但还原时麻烦我很少用。2.2 累加生成和紧邻均值到底在干什么GM(1,1)的关键操作是累加生成。假设原始序列是x0累加后是x1x1(k) sum(x0(1:k))累加的意义在于即使原始序列波动剧烈累加序列通常也会变成一条单调递增的曲线这样微分方程拟合才有基础。Matlab里一行代码就能实现x1 cumsum(x0);接下来构造紧邻均值序列z1(z1(k) 0.5x1(k) 0.5x1(k-1))。这个序列是为了把微分方程离散化连续模型里的导数在离散点上用相邻值的平均去逼近。它起到桥梁作用没有这一步微分方程和离散数据没法对接。z1 (x1(1:end-1) x1(2:end)) / 2;2.3 最小二乘求解得到时间响应式GM(1,1)的白化方程是dx1/dt a*x1 u离散化后对应x0(k) -a*z1(k) u把所有训练点写成矩阵形式就是Y B * [a, u]^T。其中B的第一列是-z1第二列全为1Y是原始序列从第二个点开始的列向量。最小二乘解为[a, u]^T (B*B) \ (B*Y)得到参数后累加序列的时间响应式为x1_hat(k1) (x0(1) - u/a) * exp(-a*k) u/a最后累减还原得到预测值x0_hat(k1) x1_hat(k1) - x1_hat(k)这里有个经验参数要记住当-a小于等于0.3时模型可以做中长期外推-a在0.3到0.5之间只建议做短期预测超过0.5说明数据本身不适合用GM即使级比检验勉强通过外推几步误差也会失控。我见过有人拿到-a0.7的模型硬要预测后面八个点结果预测值直接变成负的这种属于违背基本使用边界。2.4 精度检验除了误差均值更要看后验差常规的误差指标是MAPE平均绝对百分比误差和RMSE均方根误差。灰色预测里还常用两个后验指标后验差比值C和小误差概率P。S1是原始序列标准差S2是残差标准差CS2/S1。P的定义是残差与其均值的偏差绝对值小于0.6745*S1的概率。等级划分如下模型精度等级后验差比值C小误差概率P一级好C 0.35P 0.95二级合格C 0.5P 0.8三级勉强C 0.65P 0.7四级不合格C 0.65P 0.7我在实际项目里MAPE低于10%只是及格线后验差比值C要小于0.35才敢拿出去给业务方。而更重要的是这些残差不能直接扔掉它们正是马尔科夫修正的原料。3. 马尔科夫修正的四个核心细节3.1 相对误差比绝对残差靠谱计算残差时我强烈建议用相对误差而不是绝对误差。原因很简单绝对误差在不同量级的数据上没有可比性。比如用电量在100亿和1000亿两个量级时同样的10亿误差意义完全不同。相对误差公式re(k) (x0(k) - x0_hat(k)) / x0(k) * 100%这里要特别说明一个细节GM拟合的第一个点永远是原始第一个点残差恒等于0没有统计价值。所以计算残差序列时我会从第二个点开始。状态划分最省事的方法是把相对误差的最小值到最大值区间均匀等分成s份。比如最小-8.7%最大6.3%s3那么三个状态边界就是[-8.7, -3.7, 1.3, 6.3]。每个历史拟合点的相对误差落入哪个区间就属于哪个状态。状态个数选择上有个朴素原则8-15个训练样本用3档足够15-30个可以考虑4到5档再多容易让转移矩阵稀疏到不可用。3.2 状态转移矩阵的构造和使用有了状态序列之后统计相邻两个状态之间的转移次数。比如状态序列是[3, 1, 1, 2, 1, 3, 2]那么转移对依次是(3-1)、(1-1)、(1-2)、(2-1)、(1-3)、(3-2)。每行归一化就得到转移概率矩阵P。P(i,j)表示当前处于状态i时下一步跳转到状态j的概率。构造过程看似简单但有两个容易踩的坑第一个是状态序列长度太短。训练样本8个时残差状态序列只有7个点有效转移次数只有6次。三状态的矩阵9个格子只有6次投票很多格子概率是0这很正常但是解释时要小心。第二个是空行问题如果某个状态在历史中从未作为“起点”出现转移矩阵对应行全为0。代码里我会做一个兜底全零行直接用均匀分布填充并在注释里提示检查状态划分是否合理。预测未来状态时我采用最大概率路径从最后一个已知训练点的残差状态出发查转移矩阵取当前行最大概率对应的下一状态作为第一个未来点的预测状态然后用这个状态继续查下一行得到第二个未来点的预测状态。这种极简做法能保证结果可复现也符合马尔科夫链“一次只依赖当前状态”的基本逻辑。3.3 修正公式是怎么推导出来的这一步是整个模型的点睛之笔。相对误差定义为实际值相对GM预测值的偏差率那么有re (actual - forecast) / actual * 100%整理之后能得到actual forecast / (1 - re/100)所以修正系数就是1/(1-re/100)。如果re0说明GM预测偏低修正系数大于1预测值向上抬如果re0说明GM预测偏高修正系数小于1预测值往下压。举个例子GM预测2022年为55.6马尔科夫链判定该预测点最可能落入状态1而状态1的历史残差均值是-5.2%说明GM历史上在这个状态下普遍偏高。修正系数1/(1-(-0.052))≈0.95修正预测值就是55.6*0.95≈52.8恰好非常接近真实值52.4。为了防止某些数据中状态残差均值过大导致修正失控我会给修正系数加限幅corr_factor max(corr_factor, 0.85); corr_factor min(corr_factor, 1.15);这个范围是个经验值数据波动特别大的场景可以放宽到0.8到1.2但超过这个幅度基本说明GM和马尔科夫的组合已经不适合当前数据。3.4 修正幅度和状态数量的经验判断我自己调参的体会是状态数量宁少勿多。有人喜欢把残差分成五六档觉得越细越精确但小样本下转移矩阵会变得极其稀疏随机因素被放大。三档往往比五档更稳因为状态合并后转移概率的统计置信度更高。另外一个容易被忽视的点是如果数据本身几乎无波动马尔科夫修正反而有害。GM拟合残差如果都在±1%以内状态均值也都很接近0乘上修正系数属于“画蛇添足”。所以我在代码里加了一个判断当残差极差小于某个阈值时直接返回GM的原始预测结果。4. 手写Matlab代码灰色马尔科夫预测一次跑通4.1 代码整体框架整个实现分成三个文件更清楚gm11_predict.mGM(1,1)建模核心函数输入训练序列和预测步数输出拟合值、预测值和参数markov_correct.m马尔科夫修正函数输入训练期真实值、GM拟合值和GM未来预测值输出修正后预测及转移矩阵demo_gray_markov.m主脚本负责数据组织、调用两个函数、打印结果和绘图。之所以拆成函数是因为实际项目中数据会换但建模流程不变。你只需要把主脚本里的x0_all替换成自己的数据就能一键运行。4.2 GM(1,1)核心函数实现function gm gm11_predict(x0, p) % gm11_predict GM(1,1)建模与预测 % 输入: % x0 - 训练原始序列行向量 % p - 预测步数 % 输出: % gm.x0_hat - 还原序列前length(x0)个为拟合值后p个为预测值 % gm.x1_hat - 累加序列预测 % gm.a, gm.u - GM参数 % gm.sigma_ok- 级比检验是否通过 x0 x0(:); % 统一转成行向量防手滑传列向量 n length(x0); % 级比检验 sigma x0(1:end-1) ./ x0(2:end); lb exp(-2/(n1)); ub exp(2/(n1)); gm.sigma_ok all(sigma lb sigma ub); if ~gm.sigma_ok warning(级比检验未通过建议先做平移变换。); end % 累加生成 x1 cumsum(x0); % 紧邻均值 z1 (x1(1:end-1) x1(2:end)) / 2; % 构造B矩阵和Y向量 B [-z1(:), ones(n-1, 1)]; Y x0(2:end); % 最小二乘求参数 theta (B*B) \ (B*Y); gm.a theta(1); gm.u theta(2); % 时间响应式k从0开始 k 0:np-1; gm.x1_hat (x0(1) - gm.u/gm.a) * exp(-gm.a .* k) gm.u/gm.a; % 累减还原 gm.x0_hat [gm.x1_hat(1), diff(gm.x1_hat)]; end代码里有几个细节值得说。第一x0 x0(:)这行很多人不写一旦别人传进来的是列向量后面B*Y维度直接爆炸。第二时间响应式里的k从0开始不是从1这样x1_hat(1)x0(1)保证还原序列第一个值和原始数据完全一致。第三diff函数用来做累减还原非常简洁但注意它返回的序列长度比原序列少1所以前面必须要补上gm.x1_hat(1)。4.3 马尔科夫修正函数实现function mk markov_correct(x_actual, x_fit, x_gm_fut, n_states) % markov_correct 基于相对误差的马尔科夫修正 % 输入: % x_actual - 训练期真实值 % x_fit - GM训练期拟合值长度与x_actual相同 % x_gm_fut - GM未来预测值 % n_states - 状态个数 % 输出: % mk.x_final - 修正后的未来预测值 % mk.P - 状态转移概率矩阵 % mk.state_mean- 每个状态的残差均值% % mk.init_state- 最后训练点的残差状态 x_actual x_actual(:); x_fit x_fit(:); x_gm_fut x_gm_fut(:); n length(x_actual); % 从第2个点开始计算相对误差%第一个点残差恒为0 re (x_actual(2:end) - x_fit(2:end)) ./ x_actual(2:end) * 100; % 残差区间几乎没有跨度时修正没有意义 if max(re) - min(re) 0.5 warning(历史残差过小马尔科夫修正意义不大返回GM原值。); mk.x_final x_gm_fut; mk.P []; mk.state_mean []; mk.init_state []; return; end % 状态划分在min和max之间均分 edges linspace(min(re), max(re), n_states1); state_idx zeros(size(re)); for i 1:n_states if i 1 idx (re edges(i)) (re edges(i1)); else idx (re edges(i)) (re edges(i1)); end state_idx(idx) i; end % 状态转移频数矩阵 C zeros(n_states, n_states); for i 1:n-2 from state_idx(i); to state_idx(i1); C(from, to) C(from, to) 1; end % 归一化为转移概率矩阵全零行做均匀分布兜底 P zeros(n_states, n_states); for i 1:n_states if sum(C(i,:)) 0 P(i,:) C(i,:) / sum(C(i,:)); else P(i,:) 1 / n_states; end end mk.P P; % 各状态残差均值 state_mean zeros(1, n_states); for i 1:n_states vals re(state_idx i); if ~isempty(vals) state_mean(i) mean(vals); else state_mean(i) mean(re); end end mk.state_mean state_mean; % 初始状态最后训练点的残差状态 init_state state_idx(end); mk.init_state init_state; % 从初始状态出发逐点预测未来状态最大概率路径 n_pred length(x_gm_fut); future_states zeros(1, n_pred); cur_state init_state; for j 1:n_pred [~, nxt] max(P(cur_state, :)); future_states(j) nxt; cur_state nxt; end % 修正公式: actual forecast / (1 - re/100) corr_factor 1 ./ (1 - state_mean(future_states) / 100); % 限幅防止修正过冲 corr_factor max(corr_factor, 0.85); corr_factor min(corr_factor, 1.15); mk.x_final x_gm_fut .* corr_factor; end这段代码是模型的核心复杂度不算高但状态划分和转移矩阵统计的逻辑一定要看仔细。我在里面用等分区间的方式划分状态好处是简单透明你可以直接在注释里看到状态边界是怎么来的缺点是如果残差分布极不均匀某些状态可能一个样本都没有所以我在后面做了全局均值兜底。4.4 主脚本与绘图clear; clc; close all; % 2014-2023年某地区全社会用电量亿千瓦时 x0_all [35.2, 38.7, 37.5, 42.1, 45.8, 44.2, 50.6, 53.1, 52.4, 58.7]; year_all 2014:2023; n_train 8; n_pred 2; % 预测未来2年正好和验证集长度一致 x0 x0_all(1:n_train); y_true x0_all(n_train1:n_trainn_pred); % 1) GM(1,1) gm gm11_predict(x0, n_pred); x0_hat_fit gm.x0_hat(1:n_train); x0_hat_fut gm.x0_hat(n_train1:end); fprintf(GM参数: a %.4f, u %.4f\n, gm.a, gm.u); fprintf(级比检验: %s\n, string(gm.sigma_ok)); % 2) 马尔科夫修正 n_states 3; mk markov_correct(x0, x0_hat_fit, x0_hat_fut, n_states); x_mk_fut mk.x_final; fprintf(\n转移概率矩阵:\n); disp(mk.P); fprintf(训练期最后状态: %d\n, mk.init_state); % 3) 结果对比 fprintf(\n%-6s %6s %8s %12s\n, 年份, 实际值, GM预测, 马尔科夫修正); for i 1:n_pred fprintf(%-6d %6.1f %8.2f %12.2f\n, ... year_all(n_traini), y_true(i), x0_hat_fut(i), x_mk_fut(i)); end % 4) 绘图 figure(Position, [100 100 900 500]); plot(year_all, x0_all, ko-, LineWidth, 1.5, MarkerFaceColor, k); hold on; plot(year_all(1:n_train), x0_hat_fit, gs--, LineWidth, 1.0); plot(year_all(n_train1:end), x0_hat_fut, r^-, LineWidth, 1.5); plot(year_all(n_train1:end), x_mk_fut, bo-, LineWidth, 1.5); xlabel(年份); ylabel(用电量亿千瓦时); legend(实际值, GM拟合, GM预测, 马尔科夫修正, Location, northwest); grid on;主脚本的逻辑没有悬念但有两个输出细节我想多说两句。disp(mk.P)直接打印矩阵适合在调试阶段确认转移矩阵是否出现全零行绘图时我把GM拟合值单独用绿色虚线画出来这样可以明显看到GM在训练期的拟合值和实际值之间的偏差形态帮助判断马尔科夫修正是否合理。5. 案例实测某地区十年用电量预测对比5.1 数据与建模策略案例数据我用的是某地区2014到2023年的全社会用电量整体呈上升趋势但中间有几次回落比如2016年37.5比2015年38.7低2019年44.2比2018年45.8低。这种“大趋势向上、局部波动”的数据正是灰色马尔科夫最典型的应用场景。建模策略是前8年数据做训练后2年做验证。也就是用2014到2021年的数据建模预测2022和2023然后拿真实值对照。这样做比把全部十年数据都拿去建模更有说服力因为能直观看到模型的泛化能力。5.2 运行结果与效果对比脚本跑完输出大概是这样GM参数: a -0.0509, u 34.7341 级比检验: true 转移概率矩阵: 0.3333 0.3333 0.3333 0.5000 0.0000 0.5000 0.3333 0.0000 0.6667 训练期最后状态: 3 年份 实际值 GM预测 马尔科夫修正 2022 52.4 55.60 53.22 2023 58.7 58.43 58.52可以看出GM把2022年预测到了55.6比实际52.4高出一大截相对误差约6.1%经过马尔科夫修正后降到53.2误差压缩到1.5%左右。2023年GM本身预测得还不错58.4对58.7修正后58.5几乎维持原样。为什么会这样因为训练期最后几个点的GM残差处于状态3查转移矩阵后2022年最大概率转入状态1而状态1的历史残差均值是负的说明GM在这个状态下普遍高估于是修正系数小于1预测值被压了下来。这个机制不是人为拍脑袋而是从历史转移规律里自动学出来的。5.3 参数调整:改变状态个数和预测步数这个案例里n_states3效果很好但我用同一组数据试过n_states5发现转移矩阵里有多行全是0部分状态只出现一次转移概率估计基本靠“孤证”2022年修正结果反而偏差更大。这说明状态划分必须和样本量匹配8个训练点对应5个状态平均每个状态分不到两个样本统计规律无从谈起。预测步数方面我把n_pred改成5试过一次前两步还勉强能看到第4步预测值就出现明显漂移因为马尔科夫链沿着最大概率路径越走越偏。我的结论很明确灰色马尔科夫适合短期预测1到3步是舒适区超出这个范围就要考虑滚动预测也就是每预测一步就把真实值加入训练集重新建模。另外一个值得试的参数是训练集长度。我把n_train从8改成7时GM的趋势项变化不大但马尔科夫的转移矩阵因为少了一个转移样本部分概率值发生了明显跳变。所以训练样本如果少于8个我对马尔科夫修正的结果会保守一点宁愿只用GM原值加个简单经验修正也不用状态转移机制。6. 常见问题与排查技巧实录6.1 级比检验不通过该怎么办如果gm.sigma_ok输出false先别急着继续跑。最稳妥的补救是平移变换shift max(x0) * 2; x0_shift x0 shift; % 用x0_shift建模得到预测值后再减shift平移量到底取多少没有绝对标准原则是能通过级比检验同时不要改变序列的相对形态。取对数变换也能用但预测还原时要先exp再减平移量多一步逆向运算更容易出错我通常优先用平移。6.2 马尔科夫修正后反而更差这个问题几乎每个用这套模型的人都会遇到一次。最常见的原因是状态划分不合理残差的正负分布严重不对称导致某个状态的均值绝对值过大修正系数触碰到限幅边界。解决办法是先画出训练期残差条形图观察正负残差的分布形态再决定状态个数。第二个原因是状态转移矩阵的概率结构太“锋利”。比如矩阵里存在0.8和0.2这种悬殊概率最大概率路径基本固定死在某个状态上修正方向也一路走到黑。遇到这种情况建议放弃最大概率路径改用概率加权的修正方式把当前行的所有状态按概率加权平均出一个修正系数而不是只取最大概率的单一状态。6.3 转移概率矩阵出现全零行全零行意味着某个状态从未作为起点出现过这和状态划分太细或者样本量不足有关。我的代码里已经用均匀分布做了兜底但兜底只是“不报错”不代表结果可靠。真正要做的还是回到数据层面调低n_states或者把空状态合并到相邻状态。调试小技巧在主脚本里用disp(mk.P)把矩阵打出来看如果有一整行都是0就调整状态个数如果只有个别零值不影响主路径可以忽略。6.4 两个容易忽略的Matlab细节第一Matlab里数组除法和矩阵除法要分清。我见过有人把 ./ 写成 / 导致结果完全错误比如re计算的相对误差序列变成矩阵求逆的报错。第二函数的输入向量方向要统一。gm11_predict内部我已经用x0 x0(:)强制转成行向量但你自己扩展代码时也要注意否则B*Y的维度会让你排查半天。另外这套代码不需要额外工具箱纯基础Matlab就能跑版本2016之后都能兼容。我经常遇到有人在知乎或贴吧问“为什么我的Matlab报错无法识别函数”十有八九是函数没保存在当前路径或者文件名和函数名不一致。记住函数文件名必须与函数名完全同名否则Matlab找不到入口。最后想说的几句我用灰色马尔科夫这套组合做过不少小样本预测任务从用电量到设备故障率都有。最大的体会是模型组合的思路比单个模型本身更重要。GM(1,1)单独用遇到波动数据就会显得“笨”马尔科夫链单独用没有趋势骨架又不知道从哪里预测起。两者一结合刚好互相补短板。在实操中我建议新手先把代码跑通再用自己的数据替换最后才去纠结状态个数和转移矩阵的细节。参数调优必须有数据反馈不要凭感觉预设状态数。如果在你的数据上修正效果不明显先检查GM是否已经拟合得很好——如果残差本身就在正负2%以内马尔科夫修正属于可选项而不是必选项。这套方法还可以继续扩展比如用滑窗方式每次加入新数据重新估计GM参数或者用二阶马尔科夫链捕捉更长时间跨度的状态依赖。但在那些进阶玩法之前先把今天这套基础代码跑明白比什么都强。
返回列表