做库存需求预测做到一定阶段,你会发现一个很别扭的事:单序列模型走到头了。我之前负责某零售平台的销量预测,几万SKU一个一个用LightGBM或ARIMA建模,精度也能交差,但业务方每次追问“为什么A商品销量下去了、B商品反而涨了”的时候,我根本答不上来。直到我试着把商品之间的关联关系建模成图,用GCN配合LSTM一起做预测,这套基于图与递归结构融合的方案才真正让库存需求预测上了一个台阶。
本文完整梳理这套方案从需求分析、数据构造、模型设计到训练与部署的全过程,重点讲清楚为什么要用GCN、怎么和递归网络结合、实际工程里有哪些坑。适合正在做供应链预测的算法工程师,也适合想入门时空序列建模的同学参考。全文以实操为主,代码片段可以直接改改用。
1. 库存需求预测的痛点与建模思路切换
1.1 传统单序列预测的隐藏天花板
传统时序预测把每个商品当作独立个体,只看自己的历史销量曲线。这种方法的核心假设是商品之间的需求变化互不影响,但现实库存场景里几乎不成立。替代品、互补品、同品类商品之间的流量转移,促销时期的连带购买,甚至不同门店之间对同一商品的需求跷跷板,这些都是真实存在的联动效应。
单序列模型处理不了这种联动。当A商品缺货时,客户会转向B商品,B的销量会在几小时内冲高,但如果你只建模B自己的历史序列,模型会把这种突刺当成异常值,要么平滑掉,要么误判为促销效应。我踩过这个坑:某次大促前一个爆款断货,结果同品类另一款商品销量翻了三倍,两个模型都预测失误,导致一个狂备货一个零备货。
所以问题的本质不是预测算法不够强,而是特征表达缺失了“商品间关系”这一维度。这也是为什么我们需要把库存需求预测从纯时间序列问题升级为时空图问题。
1.2 图结构如何刻画商品关联
如果把每个商品看作图上的一个节点,商品之间的关联关系看作边,整个商品池就构成了一张有向或无向的图。这样设计的好处是空间结构可以被显式建模:节点的特征描述商品自身属性与历史状态,边描述商品间的影响强度。
构建这张图也不是凭空拍脑袋。业务上可用的关系信号很多:同品类归属关系、历史销量变化的相关性、购物车中共同出现的次数、价格区间的邻近性。这些信号可以组合成一个或多个邻接矩阵,每个矩阵描述商品之间的一种关联视角。
这本质上是把业务知识结构化。过去你在特征工程里艰难地构造“同品类其他商品销量均值”这样的手工交叉特征,现在可以直接用图结构让模型自己去学这种空间依赖。信息量变大了,手工特征工程的部分也缩了不少。
1.3 为什么选GCN而不是其他图模型
图领域可选模型很多:GraphSAGE、GAT、GIN都在各自场景里表现不错。我当时选择GCN核心原因是实现简单且足够稳定。GCN的卷积操作是一个固定权重的邻居特征聚合,训练效率高,对小规模商品图来说完全够用。
GAT虽然能学出边的注意力权重,理论上更灵活,但训练不稳定,调参成本高。GraphSAGE适合超大规模图,需要采样邻居,在几万个节点的场景下有点杀鸡用牛刀。库存商品图通常几千到几万个节点,规模不大,GCN这种全局邻居聚合的方式开销完全可控,效果也不差。
另外,GCN的谱域解释很成熟,拉普拉斯平滑的物理意义直观,遇到异常也容易排查。生产环境里,模型的可解释性和可维护性往往比刷一点精度更重要,这是我踩过多次坑后得到的深刻教训。
2. 数据准备与商品关系图谱构建
2.1 原始数据的组织与标准化
这次项目用的数据来自某模拟库存系统,包含商品主数据、每日销售流水、库存变动记录和促销排期。最核心的是每日销售流水,按商品ID与日期聚合后得到日销量矩阵。这个矩阵的维度是(商品数,天数),行是商品,列是日期。
拿到原始数据后的第一步是清洗。日销量里常见的问题包括:缺货日销量为0但实际需求不为0、数据回补导致负销量、大促期间销量尖峰被截断。处理方式是先用业务规则过滤明显异常,再把缺失值按前后七天滑动平均补齐。注意缺货日直接填0会让模型学到错误信号,最好用“当日是否有货”作为一个辅助特征喂给模型。
归一化也很关键。不同商品的销量量级差几十倍很正常,如果不做标准化,图的邻居聚合会被大销量商品主导。我对每个商品的销量序列分别做z-score标准化,以商品为粒度而不是全局粒度,否则小销量商品的信息会被淹没。
2.2 邻接矩阵构建的三种实用方法
邻接矩阵是图模型的输入基石。我实践下来有三种构建方式值得推荐,按实用程度排序:
第一种是基于品类关系构建。商品属于同一子类目就置1,这是一种静态、先验的图。优点是稳定,缺点是无法刻画跨品类的替代、互补关系。但作为初始图,它通常表现得很稳,适合当基线。
第二种是基于销量相关性构建。计算商品间历史销量序列的皮尔逊相关系数,取绝对值并设定阈值,超过阈值的建边。这种图完全从数据出发,能捕捉到同品类之外的隐性联动,比如A是B的上游配件。但要注意相关不等于因果,旺季同涨同跌会导致虚假关联,需要结合业务校验。
第三种是基于行为共现构建。利用用户购物篮数据统计商品被共同购买的次数,归一化后作为边的权重。这是最贴近实际需求的图,能捕捉到真实的连带消费关系,但依赖行为数据的质量与覆盖度。三种方法也可以组合,只要保证最终邻接矩阵是归一化后的对称矩阵,方便GCN做谱域计算。
2.3 时间滑窗与训练样本构造
图卷积的输入要求每个节点在每个时间步都有特征向量,所以我把历史销量、价格、促销标志、缺货标志拼接成一个多维特征。滑窗长度我最终定为30天,预测未来7天需求。滑窗过长会引入过多历史噪声,过短则无法捕捉季节性周期。
样本构造方式其实也是时空数据常规做法:在每一天t,取出所有商品过去30天的特征序列,形状是(节点数=商品数,时间步=30,特征维度=特征数),标签是未来7天销量之和或逐日销量。滑窗每步移动一天,重叠率很高,因此训练集样本量很充裕,但也要注意时间顺序切分,绝不能随机打乱。
我用前24个月数据做训练,中间3个月做验证,最后3个月做测试。这样能模拟模型在真实滚动预测时的表现,也方便观察有没有过拟合。时序数据划分的坑很多,随机切分等于把未来信息泄露给了模型,这点务必小心。
3. 图卷积与递归结构的融合模型设计
3.1 融合架构的整体设计
模型设计主要考虑两部分:空间依赖图结构用什么模块处理,时间依赖用什么模块处理。经过多轮实验对比,我最终采用了串行融合结构:先用GCN做空间特征变换,再把变换后的特征序列送入LSTM做时间建模。
具体来说,对每个时间步t,把当前时刻的商品特征矩阵X_t(形状为N×F)和预设的邻接矩阵A输入GCN层,得到空间融合后的特征Z_t。然后把整段时间序列的空间特征Z_1到Z_T依次送入LSTM,LSTM隐状态最终映射到预测输出。
为什么不直接对每个时间步单独跑GCN再拼接?因为库存需求的时间模式很重要,单靠GCN无法建模序列依赖。为什么不把LSTM放在GCN前面?因为每个时间步的节点特征需要先融合邻居信息,才能让时间模型看到带有空间上下文的信息。把空间变换放在时间建模之前,是经过对照实验验证的更优方案。
3.2 GCN层的数学逻辑与PyTorch实现
GCN的核心公式是谱域图卷积的一阶近似:H' = σ(D^(-1/2) A D^(-1/2) H W),其中A为邻接矩阵,D为度矩阵,H为节点特征矩阵,W为可学习的权重矩阵,σ为激活函数。D^(-1/2) A D^(-1/2) 就是对邻接矩阵做对称归一化,防止不同度数的节点在聚合时产生尺度失衡。
用PyTorch实现一个GCN层其实很简单,常规做法是这么写的:
class GCNLayer(nn.Module): def __init__(self, in_dim, out_dim): super().__init__() self.w = nn.Parameter(torch.randn(in_dim, out_dim)) self.bias = nn.Parameter(torch.zeros(out_dim)) def forward(self, x, adj_norm): # x: [B, N, F_in], adj_norm: [N, N] h = torch.einsum('bnf,nm->bmf', x, adj_norm) out = torch.einsum('bmf,fo->bmo', h, self.w) + self.bias return torch.relu(out)实际项目中我不会只叠一层GCN,而是叠了两层,中间加ReLU激活,第二层输出维度与LSTM隐藏维度对齐。两层GCN可以聚合两跳邻居的信息,对大多数库存商品图来说,两跳覆盖的关联范围已经足够,再深容易导致过平滑,所有节点特征趋同。
3.3 递归部分选型:LSTM还是GRU
递归网络我对比过LSTM和GRU。LSTM参数更多、表达能力更强,在小数据集上容易过拟合;GRU参数少、训练快、在中等规模数据上效果几乎持平。考虑到商品数量几千、序列长度只有30,我最终选了LSTM但用了较小的隐藏维度。
LSTM端的输入是GCN输出后的序列,每个时间步的输入维度等于GCN第二层输出维度。我设置的隐藏维度是64,层数2层,dropout取0.2。这里的LSTM虽然起名是递归结构,本质上是对空间感知后的特征做时序编码,它学到的是“邻居信息影响下的需求演变模式”,而不是原始销量曲线。
如果算力紧张,也可以把LSTM换成GRU,参数量减少约四分之一,推理速度提升明显,精度损失通常在0.5个百分点以内。工程上的取舍永远要从部署资源和业务容忍度出发,不是越复杂的模型越好。
3.4 输出层与损失函数设计
输出层只需一个线性映射,把LSTM最后一个时间步的隐状态映射到预测长度维度。预测7天就输出7个值。如果想直接预测未来7天总需求,则映射到1个值。前者信息更丰富,便于业务按日拆解补货计划,我采用前者。
损失函数我用的是Huber Loss,平滑平均绝对误差。纯MSE对离群点过于敏感,大促和缺货反弹的尖峰会让训练被少数样本主导;纯MAE在尾部预测上又不够敏感。Huber Loss在残差小于阈值时表现如MSE,大于阈值时表现为MAE,兼顾稳定与敏感,实测下来比单纯用哪个都稳。
评估指标除了MAE和RMSE,我还额外算了一个业务更关心的指标:预测误差率MAPE。很多库存系统不关心绝对误差,只关心偏差比例,因为补货安全库存是按比例设定的。每个商品分别计算MAPE再取加权平均,权重用商品销量占比,这样可以防止小销量商品拉低整体分数。
4. 训练策略与关键参数调优
4.1 模型训练的完整配置
整个模型用一个端到端的框架训练,GCN和LSTM一起反向传播,没有分开训练。优化器用AdamW,初始学习率1e-3,权重衰减1e-4。批次大小设为32,每个批次包含不一样的子图采样和对应的时间序列样本。
批次怎么构造这一步值得说清楚。每次随机采样部分商品作为当前批次的节点子集,而不是把全图都放进来。一方面是因为全图节点太多,邻接矩阵太大,训练速度慢;另一方面是固定全图会导致模型反复看到同样的边结构,容易过拟合。子图采样相当于一种数据增强,让模型每次只看到局部的商品关系,泛化能力反而更强。
学习率我用了余弦退火策略,前10轮保持最大值1e-3,之后按余弦曲线衰减到1e-5。相比固定学习率,收敛更平稳,最终精度略微提升。训练轮数设在60轮左右,配合早停机制,验证集损失连续5轮不降就停。我的经验是训练轮数宁多勿少,配合早停不会浪费时间。
4.2 GCN层数与隐藏维度的选择
GCN层数我对比过1层、2层、3层。1层只能聚合直接邻居,信息有限;2层可以聚合两跳邻居,效果最好;3层出现明显的过平滑,所有节点的输出特征趋向一致,预测区分度大幅下降。这个结论和很多图深度研究的结论一致,过深的GCN在中小图上没有收益。
隐藏维度方面,我尝试过32、64、128。32维跑得最快但精度不足,128维精度提升有限却带来明显的显存压力。最终选用64维,这是精度和资源消耗最均衡的点。注意LSTM的隐藏维度和GCN输出维度要匹配,我用同样的64,这样连接处不用额外做维度变换。
关于dropout位置,我分别在GCN层之后和LSTM输入之前各加了一层0.2的dropout。放两个位置的原因是空间图和时间序列都可能过拟合,单独在一处做约束效果不够。如果你觉得显存紧张,可以考虑只在GCN输出处加一个dropout。
4.3 消融实验与效果对照
为了验证GCN确实带来了增益,我做了几组消融对照。第一组去掉GCN,只保留LSTM直接建模每个商品的销量序列,相当于把GCN输出替换成原始特征。第二组把图结构换成随机邻接矩阵,验证空间结构是否真的有信息量。第三组用全连接层替代GCN,看看是不是单纯增加参数带来了提升。
结果很有意思:完整方案比纯LSTM在整体MAPE上降低约11.6%,比随机图的GCN+LSTM降低约7.2%,比全连接替代GCN降低约5.8%。随机图的GCN仍然优于不带GCN的LSTM,是因为GCN本身的特征变换能力仍然起作用,但有效信息明显少了很多。这组实验让我确认了空间结构建模带来的增益是真实且显著的。
细节上还发现,图结构对互补品关系的预测提升最明显。基于行为共现构建的邻接矩阵在“买了A也常买B”这种场景下预测准确率提升最大,因为这类商品的销量本来就在彼此影响,单序列模型根本捕捉不到这种跨序列的因果。
5. 工程落地过程中的坑与排查技巧
5.1 邻接矩阵稀疏与标签传播问题
商品池里大部分商品之间的关联都很弱,导致邻接矩阵非常稀疏。过于稀疏的图会让部分节点的邻居信息几乎为零,GCN退化为普通线性变换。我试过几种缓解策略:一是对邻接矩阵做k近邻稀疏化后再补全,即每个节点保留相似度最高的K个邻居,其余置零;二是把图分成多个子图分别训练,让每个子图内部连接更密。
实际效果最好的是k近邻稀疏化,K取10到20比较合适。太小了容易切断有效连接,太大了又引入噪声。补全这方面,强烈建议给每个节点强制保留自环,即邻接矩阵对角线加1,否则GCN聚合时丢失自身特征,训练初期会很不稳定。
还有一个隐蔽的坑:不同时间窗口的邻接矩阵是否需要动态更新。理论上销量相关性矩阵应当随时间更新,但频繁更新会导致训练样本的分布不一致。我的做法是训练阶段使用固定邻接矩阵(基于全部历史相关性),线上预测时每两个月重新计算一次特征矩阵批次更新,平衡信息新鲜度和模型稳定性。
5.2 冷启动商品与新品如何预测
库存预测绕不开新品问题。新品没有历史销量,自然没有历史特征序列,也没有足够的数据来计算与其他商品的相关性,常规图模型直接处理不了。我当时的方案是用相似商品的特征作为替代:对每个新品,找到同品类下最相似的老品(基于属性相似度),把老品的特征序列作为新品的输入特征。
这个方法推到极端情况也有效:即使新品完全没有行为数据,只要品类和属性归属正确,模型也能借助相似老品的时序模式做预测。缺点是依赖相似度计算的准确性,如果属性标注本身很稀疏,匹配到的老品可能毫无参考价值。所以我又加了一条兜底规则:新品前两周强制走业务规则预测,积累到14天以上数据后再切到图模型。
线上表现中,用相似商品特征替代的冷启动预测MAPE在45%左右,看似很高,但已经优于业务之前用品类均值预测的80%以上误差。冷启动不是图模型能单独解决的问题,但图结构通过相似商品传递信息,已经比单序列模型的难度降低一大截。
5.3 训练中的过平滑与梯度消失问题
过平滑是GCN叠加过深时最容易出现的问题,表现是不同商品的预测输出几乎相同。如果连续几轮训练验证损失不降,但训练损失还在降,常常就是过平滑在起作用。解决方法是减少GCN层数、加残差连接、或者用带自环的归一化。
梯度消失则主要出现在LSTM的长序列部分。虽然原来的序列长度只有30,不算特别长,但叠加GCN的空间变换后,梯度流过两层的复合函数还是容易衰减。我的缓解手段主要有三个:梯度裁剪值设为5.0,选择LSTM而非RNN;LayerNorm加在GCN和LSTM连接处;对Huber Loss的阈值做动态调整,保证训练早期梯度不过小。
如果你在训练日志里看到某个时间步的隐状态范数趋近零,基本可以断定梯度链断裂了。这时优先检查两处:LSTM中是否忘记设置初始隐状态为零而不是随机,以及GCN的权重初始化是否合适。GCN权重我建议用Xavier初始化,比默认正态初始化收敛快得多。
5.4 推理速度与线上部署细节
库存预测通常是每天跑一次全量预测,对推理耗时的要求不算高,但也不是可以无限慢。几万个商品、两层GCN加两层LSTM,在单卡GPU上推理全量大约需要2到4秒,CPU上则需要15到30秒。如果每天的预测任务设在凌晨低峰期,CPU也够用,但如果需要小时级滚动预测,就必须上GPU。
部署时我把模型导出为ONNX格式,用对应运行时做推理。期间碰到一个奇怪的报错,问题出在GCN层里的einsum操作在ONNX导出时不支持动态维度。解决办法是把einsum改写成matmul或手动reshape,导出前先用固定序列长度的样例跑一遍验证图和参数保存。转换过程大约花了我半天时间,属于典型的模型转换坑。
另一个部署细节是图结构的更新频率。线上邻接矩阵是每两个月更新一次,我把邻接矩阵作为模型输入而非模型参数,这样更新图结构时不需要重新训练模型,只需要替换输入矩阵。这个设计让模型迭代成本大幅下降,业务调整商品关系时也能快速上线。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 推荐解法 |
|---|---|---|
| GCN层数加深后效果变差 | 过平滑 | 减层数、加残差、检查邻接矩阵归一化 |
| 部分节点预测值趋同 | 邻接矩阵稀疏或归一化不当 | k近邻稀疏化、强制加自环 |
| 训练损失下降但验证不降 | 过拟合 | 增加dropout、减小隐藏维度、加早停 |
| 新品预测误差极大 | 冷启动无历史特征 | 用相似老品特征替代、业务规则兜底 |
| 带上GCN后训练变慢很多 | 邻接矩阵计算量大 | 子图采样、稀疏矩阵乘法 |
| LSTM梯度消失 | 序列过长或网络太深 | 梯度裁剪、LayerNorm、换GRU |
6. 几轮迭代后的效果与思考
最终方案上线后,全量商品的预测MAPE从原来的约32%降到约25%,对高销量商品的预测误差降幅更明显,从约15%降到约10%。补货计划采纳率提升带来的直接效果是库存周转天数减少了大约12%,缺货率下降了约8%。业务方最直观的感受是:过去那种“预测不准导致一边积压一边断货”的投诉明显减少了。
这一轮做下来,我最大的体会是:模型结构的选择一定要从数据里到底有没有这种关系出发。如果你的商品之间确实没有联动关系,强行上GCN只会增加计算量和过拟合风险。判断方法很简单,算一下商品销量之间的平均相关系数,如果整体偏低,那图结构对你的场景可能帮助不大。
其次,图结构与业务知识的结合程度决定了模型的上限。纯数据驱动的邻接矩阵能捕捉到统计关联,但加上业务规则(如同品牌强绑定、替代品关系)之后,模型的预测能力和可解释性都有明显提升。我在后期的产品迭代中不断增加业务侧的边关系,每次增加都带来可量化的预测精度提升。
这套方案后续还能往两个方向扩展:一是把时序图注意力网络引入,对边的权重做动态学习;二是把多个图通道并行输入,分别建模品类关系、价格带关系和促销连带关系。就目前的部署稳定性来看,GCN加LSTM这个基础框架还会在库存预测场景里支撑相当长的时间。