资讯动态

库存需求预测实战:GCN+GRU融合模型捕捉多SKU关联结构

发布时间:2026/10/14 4:09:39 来源:尧图企业网站定制
做过库存需求预测的朋友应该都有同感当预测对象从一个SKU扩展到几百上千个SKU时纯粹的时间序列模型很快就到瓶颈了。单品看每个SKU的销量曲线都像随机噪声放到一起看SKU之间却存在明显的联动关系——同品类、同门店、同供应商的商品需求曲线往往一起涨一起跌。这种“横截面结构”恰恰是传统时序模型忽略掉的信息也是我最终转向图卷积神经网络GCN的原因。这篇文章想聊的是我在库存需求预测项目里把GCN和GRU这类递归结构结合起来的一套完整方案为什么需要图、图怎么建、GCN和递归网络怎么融合、数据管道有什么坑、评估指标怎么选。内容既适合刚接触图神经网络的读者建立直觉也适合已经跑通基础模型的人做架构选型和调参参考。1. 库存需求预测的痛点单看时序忽略了大盘结构1.1 多SKU联动被大多数模型忽略的横截面信号我最早做库存预测用的是LSTM和Prophet这类经典方案单SKU模型每个商品各跑各的。效果怎么说呢——平稳品还凑合一旦遇到促销、季节交替、同品类爆款带动预测偏差就非常难看。后来我把几个SKU的销量曲线叠在一起看发现问题很明显某个SKU销量上涨之前往往同品类的另一个SKU已经先动了。比如某地区门店的饮料功能饮料和碳酸饮料的销量存在明显的此消彼长或同涨同跌关系。这种关系如果只用单变量时序模型是不可能捕捉到的。这就是图结构的价值。把SKU或门店当成节点把他们的关联关系当成边一个规模很大的库存系统就变成了一张图。GCN做的就是在这张图上做信息聚合——每个节点的特征不再只是自己的历史销量还包括邻居节点的状态。1.2 传统模型和纯时序模型的卡点分别在哪传统统计模型比如ARIMA、指数平滑假设序列是线性自相关的处理不了复杂的外部冲击和多序列交互。Seq2Seq类的深度模型能拟合非线性但输入通常是形状为[时间步, 特征维度]的矩阵它本质上假设每个SKU是独立样本只是在参数层面共享。问题在于库存系统的物理结构不是独立的。共享仓库、共同供应链、替代品和互补品关系、门店覆盖重叠这些结构会真实地传导需求波动。忽略结构等于默认SKU之间没有关系——但库存管理的人都知道这恰恰是最大的噪声来源。所以我的思路很直接用图卷积神经网络GCN来建模SKU之间的空间结构用GRU来建模每个节点的时间演化两者结合既看得到“谁和谁一起动”又记得住“每个节点自己过去怎么动”。2. GCN在这个场景里到底在算什么2.1 把库存快照变成图信号输入输出的形状问题先说清楚GCN的输入输出很多新手在这里卡住。假设我们有N个SKU每个SKU在t时刻有F维特征历史销量、价格、库存量、促销标记等。那么t时刻的图信号矩阵就是X^t形状是[N, F]。图结构用邻接矩阵A表示形状是[N, N]A[i][j]表示节点i和节点j的关联强度。GCN做的是逐层传播H^(l1) activation( Ã · H^(l) · W^(l) )其中Ã是归一化后的邻接矩阵W^(l)是可学习的权重矩阵H^(l)是第l层的节点表示。一开始H^(0) X^t经过几层传播后每个节点的表示就融合了多跳邻居的信息。在库存预测场景里这个变换的物理意义是一个SKU的需求预测特征在每一层图卷积里都会被周围SKU的特征“修正”一遍。比如某个SKU近7天销量开始下滑但它的互补品销量在涨那么经过图卷积后它的隐含特征会包含“情况可能没那么糟互补品在承接需求”的信息。2.2 邻接矩阵怎么定三类构建方法我最后用了组合方案图神经网络效果好坏一半以上取决于图建得对不对。A矩阵如果建错了后面全是白算。我实际对比过三类构建方法基于销量相关性计算每个SKU历史销量序列的皮尔逊相关系数取相关性大于阈值的对作为边。优点是好理解、容易实现缺点是只能捕捉线性同步关系且容易把“一起被促销带动”的伪相关也建进去。基于时序形态距离用动态时间规整DTW距离衡量两条销量曲线的形态相似度。相比皮尔逊相关DTW能捕捉相位偏移的相似形态比如某个SKU的销量提前另一SKU两天变动。这种滞后关系在库存传导里非常常见。基于业务先验同品类、同仓库、同供应商、同价格带——这些业务标签本身就是天然的结构信息。比如同一仓库的SKU共享物流资源断货风险会互相传导同供应商的SKU会一起受供货周期波动影响。最终我采用的是组合方案业务先验边作为必选边保证图结构有稳定的业务解释相关性边作为补充边只保留前K个强相关邻居DTW边用于捕捉有滞后性的联动。构建方法边的含义优点缺点皮尔逊相关同步线性联动简单、可解释漏掉滞后关系、易混入伪相关DTW距离形态相似联动能捕捉滞后联动计算量大、阈值敏感业务先验供应链物理关联稳定、可解释覆盖不全需要人工维护注意所有相似度都要转成邻接矩阵后做归一化通常用对称归一化Ã D^(-1/2)(A I)D^(-1/2)。加单位阵I是为了让节点在聚合时保留自身信息否则一层卷积之后每个节点就只剩邻居的平均了。2.3 谱域GCN和空间域GCN我为什么选后者图卷积大体分两派。谱域方法从图拉普拉斯矩阵的特征分解出发在频域做滤波数学漂亮但泛化性差——图结构一变特征分解就得重算。空间域方法直接在邻居节点上做聚合更像“对局部结构做共享参数的特征提取”。库存预测的图结构是随时间变化的新SKU上架、老SKU下架、季节性的联动关系变化。空间域GCN天然支持这种动态性只需要在每一步把邻接矩阵更新一下。我选的是空间域GCN更准确地说是GraphSAGE风格的邻居聚合配合注意力机制给不同邻居分配不同权重。这样同一个SKU的“互补品邻居”和“替代品邻居”对预测的贡献可以自动学出差异。3. 图与递归结构结合的两种主流架构3.1 堆叠式GCN抽空间特征GRU管时间演化最直观的做法是把两者串起来每个时间步t先用GCN对这个时刻的图信号X^t做空间编码得到空间增强后的特征Z^t然后把这些Z^t按时间顺序送入GRU由GRU学习时间依赖。模型结构可以用伪代码表达class GCN_GRU_Stack(nn.Module): def __init__(self, in_dim, hidden_dim, num_layers): super().__init__() self.gcn GCN(in_dim, hidden_dim) # 空间特征提取 self.gru nn.GRU(hidden_dim, hidden_dim, num_layers) self.regressor nn.Linear(hidden_dim, 1) def forward(self, x, adj): # x: [batch, seq_len, num_nodes, in_dim] # adj: [num_nodes, num_nodes] seq_len x.size(1) encoded [] for t in range(seq_len): z_t self.gcn(x[:, t, :, :], adj) # 每个时刻单独做图卷积 encoded.append(z_t) encoded torch.stack(encoded, dim1) # [batch, seq_len, num_nodes, hidden] # 把节点维度视作batch时序维度送入GRU b, t, n, h encoded.shape encoded encoded.permute(0, 2, 3, 1) # [batch, num_nodes, hidden, seq_len] encoded encoded.reshape(b * n, h, t) out, _ self.gru(encoded) out out[:, -1, :] out out.reshape(b, n, -1) return self.regressor(out).squeeze(-1)堆叠式的优点是结构清晰、容易实现、训练稳定。我在早期版本里用的就是这种方案。但有个潜在问题GCN和GRU是串行连接的每个时间步的图卷积没有感知时间上下文它只是对当前时刻快照做空间增强。对于“邻居的影响有滞后”这种情况表达能力差一些。3.2 交互式把GRU内部的矩阵乘法换成图卷积交互式的思路更激进不是让GCN和GRU串行连接而是把GCN直接嵌入到GRU的单元内部。GRU的核心计算是reset门r_t sigmoid(W_ir · x_t b_ir W_hr · h_{t-1} b_hr) update门z_t sigmoid(W_iz · x_t b_iz W_hz · h_{t-1} b_hz) 候选状态n_t tanh(W_in · x_t b_in r_t * (W_hn · h_{t-1} b_hn)) 输出h_t (1 - z_t) * n_t z_t * h_{t-1}交互式做法的核心改动是把x_t和h_{t-1}送入门控计算之前先用GCN做一次空间聚合。也就是说门控的输入从“自己的特征”变成“自己和邻居特征融合后的结果”。这种方式在时序建模和图结构学习之间建立了更强的耦合每个时间步的时序状态更新都会感知当前图结构下的邻居状态。实现上有点像GraphGRU的思路def graph_gru_cell(x_t, h_prev, adj, w_ir, w_hr, gcn_func): # x_t: [batch, num_nodes, in_dim] # h_prev: [batch, num_nodes, hidden_dim] x_g gcn_func(x_t, adj) # 对输入做图卷积 h_g gcn_func(h_prev, adj) # 对隐藏状态做图卷积 r torch.sigmoid(w_ir(x_g) w_hr(h_g)) z torch.sigmoid(w_iz(x_g) w_hz(h_g)) n torch.tanh(w_in(x_g) r * w_hn(h_g)) h (1 - z) * n z * h_prev return h交互式的优势在于图结构的信息不仅仅是输入侧的“特征增强”而是深度参与到了信息的遗忘和记忆决策中。例如仓库库存告急时某个SKU的断货风险会通过邻居节点的隐藏状态直接传导到门控计算模型可以学到“邻居状态差时要调整对自身需求的记忆”。3.3 两种架构的对比和我的最终选择我在实际测试中堆叠式训练速度快很多参数量少对图质量要求也相对低交互式在测试集上的sMAPE大约能再低3到5个百分点尤其是在联动效应明显的品类上提升显著但对邻接矩阵的噪声更敏感训练时需要更多耐心调学习率。最终我采用的是交互式变体但没有对每个时间步的h都做图卷积——那样计算量太大。我做了个折中输入侧每个时间步做图卷积隐藏状态侧每隔两个时间步做一次图卷积。这样保持了空间信息的参与感又控制了训练开销。4. 数据工程决定模型上限的前置步骤4.1 从订单流水到滑窗样本模型输入需要的是滑窗样本给定过去T步的图信号序列预测未来h步的需求。以日粒度为例我用过去28天的数据预测未来14天的需求。每条样本的构造过程是按SKU和时间戳对齐生成[N, 时间天数]的销量矩阵加入外部特征当日价格、是否促销、是否节假日、天气温度、库存水位按时间滑窗切割第i到iT-1天作为输入窗口第iT到iTh-1天作为预测目标把窗口内的每个时间步拼成图信号X^t形状为[N, F]。这里容易忽略的是预测目标不能只取一个点库存补货决策需要的是未来h天的累计需求分布。所以我的预测头有两个分支一个输出未来h天的期望值一个输出分位数用于安全库存计算。4.2 归一化和训练集切分容易踩的坑库存销量的分布极度不均匀有的SKU日均销量几百件有的只有一两件。如果不做归一化直接训练loss会被大销量SKU主导小销量SKU的预测质量会非常差。我用的方案是每个SKU单独做归一化用训练集的均值方差归一化验证集和测试集使用训练集的统计量避免数据泄漏。训练集和测试集的切分也需要注意。库存数据有强烈的季节性最简单的时间切分很容易让模型“记住”消费节奏而非学习泛化规律。我推荐滑动窗口交叉验证第一次用第1-12周训练、预测第13-14周第二次用第4-16周训练、预测第17-18周以此类推。这样每个季度都有机会出现在验证集里。4.3 外部特征怎么进入图结构价格、促销、节假日这些特征如果直接拼在X^t矩阵里GCN也能学到它们的空间传播效应。但有个更好的做法把外部特征拆成两类。一类是节点自身属性直接进X^t另一类是全局环境变量不经过图卷积在GRU的输出阶段拼接进去和图的隐向量一起送入预测头。我这样做的原因很实际促销和节假日影响的是整个大盘不是某个节点特有的邻居关系让它们绕开图卷积可以减少图结构对全局信号的干扰。实测下来这种拆法比全部塞进图信号的方案稳定。5. 实验设计、消融评估怎么证明图结构真的有用5.1 基线该怎么选选谁才有说服力要让团队和业务方信服这套方案不能只靠一张预测效果图。我跑了四组基线单变量LSTM经典时序模型不建模SKU关系带特征的XGBoost把历史销量和外部特征拉平按SKU分组训练纯GCN 线性时序头验证递归结构是否存在必要GCN GRU堆叠式和交互式对比架构差异。结果有意思的是纯GCN因为缺少时序记忆在需求波动大的SKU上表现很差——这从侧面说明递归结构在图场景下依然是必需品图结构不能替代时间记忆只能增强它。5.2 评估指标别只用RMSE库存场景要用sMAPE和MASE库存预测不能只看RMSE因为不同SKU的量纲差异太大RMSE会被头部SKU主导。我建议至少看三个指标sMAPE对称平均绝对百分比误差sMAPE 200% * |预测值 - 实际值| / (|实际值| |预测值|)。它对大销量和小销量都公平但注意实际值接近0时还是会失真需要加平滑项。MASE平均绝对标度误差用模型误差除以naive预测误差。所谓naive预测就是用昨天销量预测今天销量。MASE小于1说明模型比naive强这个指标很适合向业务方解释。Pinball Loss分位数损失如果想输出概率区间用于安全库存分位数损失是标准选择。分位数预测的价值在于业务方需要的不只是“未来卖多少”更是“未来需求在什么区间”区间宽度直接决定补货数量和资金占用。5.3 消融实验的三个关键对比我做过的消融实验里最有说服力的三个对比是去掉邻接矩阵里的DTW边只看相关性和业务先验边——滞后联动的SKU组预测误差上升明显把交互式改成堆叠式——整体误差上升验证了深度耦合的价值把邻接矩阵随机打乱保持参数规模不变——误差涨得最多用来证明模型学到的不是噪声。第三点尤其重要。跑完随机图对比后业务方基本就认可了这套方案不是玄学。6. 踩过的坑和调参经验6.1 冷启动问题新SKU没有历史销量怎么办库存系统永远不缺新SKU。新品的邻接矩阵没有历史相关性可算如果直接让它的特征全是零图卷积聚合出来的表示也接近零预测自然很离谱。我的处理分三步。第一步用业务先验边的信息做“邻居初始化”新品上线时自动匹配同品类、同价格带、同供应商的老SKU作为邻居用邻居的历史销量均值做特征初始值。第二步在邻接矩阵里给新品加一个很小的随机扰动避免所有邻居权重完全相同导致梯度消失。第三步模型对冷启动SKU的预测设置更大置信区间宁宽勿窄。6.2 过平滑GCN不是越深越好图卷积一个著名的问题就是层数多了会过平滑——每个节点的表示越来越像最后趋同。库存图上这个现象尤其明显因为大部分节点的度分布高度不均匀少数热门SKU连接了大量边。实测下来2到3层图卷积在库存场景最合适。不要盲目堆深度如果觉得感受野不够优先考虑增大每层隐藏维度或者加上残差连接而不是加层数。我甚至在最终版本里只保留了2层GCN加上一层注意力聚合这是调参实验里效果和训练速度的平衡点。6.3 训练不稳定图结构引入的方差怎么压GCN训练初期很容易震荡。一个原因是邻接矩阵的归一化方法不对导致不同节点的梯度尺度差异巨大。我建议严格使用对称归一化并在训练初期对图卷积层的输出做LayerNorm。另外一个原因是数据样本之间的关联度太高——同一张图、相邻时间窗口的样本在特征上高度相似模型容易过拟合到时间邻近的样本上。解决办法是每个epoch里对训练样本做随机滑窗采样同时引入时间维度的dropout随机屏蔽部分历史时间步强制模型不依赖特定时间位置的信息。6.4 预测结果的可解释性图模型容易被认为是黑盒库存预测的模型要过业务评审可解释性绕不开。我做的方案是在预测完成后把最近一个时间步的注意力权重取出来看每个节点聚合了哪些邻居的信息转成一张“需求关联热力图”。这样做有两个实际价值一是数据团队可以核对图结构是否符合业务直觉比如“可乐和雪碧应该强关联如果热力图显示它们没有关联那说明图建得有问题”二是市场团队可以把热力图当作促销效果的分析工具看历史促销是否真的激活了关联品类的需求。个人经验图神经网络落地的关键从来不是模型本身的精度而是图结构是否和业务逻辑对齐。技术团队和业务团队一起审图、一起定义边的含义比单独追求评估指标提升有效得多。做了这一整套之后库存预测项目的数据大概是这样常规SKU的预测准确率提升不算夸张但在联动效应明显的品类上sMAPE相比原先的单变量LSTM方案下降了8到10个百分点冷启动SKU的预测误差下降了差不多20个百分点。这个收益不在于模型本身多精妙而在于它把库存系统里一直存在但没被利用的结构信息真正用起来了。如果你也在做多SKU库存预测我建议从构建一张靠谱的关联图开始先把图建对再去纠结GCN的层数和GRU的隐藏维度。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑