ARTICLE DETAIL

资讯详情

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

京东销量预测深度学习实战:从LSTM到Transformer的完整方案

京东销量预测深度学习实战:从LSTM到Transformer的完整方案 简介一份基于深度学习的京东商品销量预测完整源码包面向电商数据分析、算法工程及机器学习进阶学习者针对销量时序预测、多维特征构建与模型评估等核心问题提供可直接运行的工程化实践方案。资源共51个文件压缩包约6.96MB包含10个Python源码文件、11个CSV数据集、10个SQL建表与特征构造脚本、12张ROC及训练过程可视化图、YAML配置与说明文档分别承担模型实现、数据存储、样本生成、结果展示与运行配置等职能。系统以RNN作为时序预测主体run.py串联数据预处理、模型训练与评估流程多份SQL脚本构造用户、商品与行为多维特征并额外提供规则模型与GBDT对比方案。预测结果、ROC曲线、分步结果图及多个提交结果文件便于直观比较模型性能适合参考完整赛题方案或复现商品销量预测流程。目前已有342人学习下载对有竞赛或实战需求的开发者具有一定参考价值。1. 京东销量预测的深度学习方案到底解决什么问题这款设计源码能给你什么电商销量预测是所有大促运营反复追问的第一需求京东场景里尤其难SKU动辄百万级促销节奏密集秒杀和百亿补贴会让销量曲线瞬间失真。传统时间序列模型ARIMA、指数平滑对这类突发波动几乎无能为力所以“基于深度学习的京东商品销量预测设计源码”这个标题本质上是在提供一个能接住“促销趋势季节性”三方压力的预测系统骨架核心价值是把黑匣子式的预测任务拆成一套可落地的数据、模型、训练、上线闭环。这篇笔记围绕一套常见且可靠的从业方案展开用商品静态信息、价格、流量日志、营销日历构建特征用 LSTM/Transformer 结构替代统计模型再把预测结果过一层规则守卫去修正大促峰值。适合正在做电商中台销量预测、或想用深度学习改造传统库存系统的后端工程师和数据工程师。跟着下面的流程走你能在本地数据上跑通一个最小可用的销量预测管线并知道哪些参数一到双 11 就会翻车。2. 京东销量预测的任务定义与数据构造先搞清“预测什么”再碰模型销量预测在京东这类平台上有明确的粒度之争。你预测的是 SPU标准化产品单元还是 SKU库存量单位预测的是未来 1 天、7 天还是 30 天目标不同数据构造和模型输出头就完全不同。2.1 任务粒度与目标窗口的选择决定了整个项目架构常见的做法是预测 SKU 在未来 7 天的日销量序列而不是只预测一个总数。这样做的好处有二一是库存补货通常按日滚动二是序列输出能保留销量变化的形状对大促前的爬坡段有感知。预测步长如果用 7模型结构上就是一个 sequence-to-sequence 的编码器解码器结构如果用 1那退化成回归问题用带时序特征的全连接层就够了但信息量会损失很多。另一个关键点是要区分“销量正品口径”。京东后台的销量数据里用户下单后取消的订单、售后退款的商品都会污染标签。我一般会建议团队直接使用订单表的支付成功且未退款明细作为正样本口径否则模型会把取消率高的品类学出周期性假象。特征构造也围绕这个口径对齐绝不能把流量日志的天级总量去对齐订单天级明细一旦对不齐会出现“特征未来泄漏”的隐患。2.2 特征工程把京东的商品、流量、营销字段转成模型输入原始数据依赖京东的多个数据源商品主表提供类目、品牌、上下架状态流量日志提供浏览、加购、收藏的渠道来源促销表提供满减、秒杀、优惠券的计划力度。要把这些异构数据揉成模型能吃的整数和浮点数我一般把特征分为四组静态特征SPU/SKU 编码、类目 id、品牌 id、价格区间分桶 id、是否自营。时序统计特征近 7/14/30 天销量均值、方差、最大单日销量、环比变化率同期浏览到下单的转化率。营销特征未来窗口内是否有促销、促销剩余天数、折扣深度原价减活动价再除原价、限购数量。日历特征星期、月份、是否月初/月末、节气与电商节点标签。代码实现上会用 SQL 或 pandas 先做宽表拼接再统一走 sklearn 的 LabelEncoder 做 ID 类特征编码。关键点在时节营销因子京东的 618、双 11、家电盛典等节点不能只用“是否双 11”这个二值特征而要用节点前 N 天倒计时数值。倒计时数值能让模型学到“大促前蓄水、大促后回落”的形状。特征处理的样本代码如下假设已经拿到三张原始表import pandas as pd from sklearn.preprocessing import LabelEncoder # item_df: 商品主表; sales_df: 日销量宽表; promo_df: 促销计划表 item_df pd.read_csv(jd_item_meta.csv) sales_df pd.read_csv(jd_item_daily_sales.csv) promo_df pd.read_csv(jd_promo_plan.csv) # 按 SKU 与日期对齐 df sales_df.merge(item_df[[sku_id, cate3_id, brand_id, price]], onsku_id, howleft) df df.merge(promo_df[[sku_id, promo_start_date, promo_end_date, discount]], onsku_id, howleft) # 距促销开始/结束的倒计时天数缺失值填 0 表示无促销 df[promo_starts_in] (pd.to_datetime(df[promo_start_date]) - pd.to_datetime(df[date])).dt.days df[promo_ends_in] (pd.to_datetime(df[promo_end_date]) - pd.to_datetime(df[date])).dt.days df[[promo_starts_in, promo_ends_in]] df[[promo_starts_in, promo_ends_in]].fillna(0).clip(lower0) # 类目与品牌做标签编码缺失类目落到 -1 for col in [cate3_id, brand_id]: le LabelEncoder() df[col] df[col].fillna(-1).astype(int) df[col _enc] le.fit_transform(df[col])这段代码的逻辑是把促销表里的起止日期转成两个相对时间特征而不是绝对日期避免模型过拟合到某一次促销LabelEncoder 之后要保留一份映射字典供线上服务复用否则线上新出现的品牌 id 会直接编码报错。这里有个隐性坑LabelEncoder 对训练集之外的 id 没有处理能力线上流转时必须在编码前先走“unknown”桶。2.3 序列样本切分按 SKU 滚动生成训练样本禁止全局随机切分销量预测和普通分类任务最大的不同是时间依赖。训练集、验证集、测试集必须按时间顺序切分不能用 train_test_split 随机打乱否则模型看见了未来数据验证集指标会虚高到没有参考价值。我常用的切分方式如下def build_sequences(df, sku_id_col, feature_cols, target_col, history_len30, predict_len7): X, y [], [] df df.sort_values([sku_id, date]).reset_index(dropTrue) for sku, grp in df.groupby(sku_id_col): values grp[feature_cols].values targets grp[target_col].values # 每个样本是历史上 30 天特征 未来 7 天销量标签 for i in range(len(grp) - history_len - predict_len 1): X.append(values[i:i history_len]) y.append(targets[i history_len:i history_len predict_len]) return np.array(X, dtypenp.float32), np.array(y, dtypenp.float32)切完样本后用前 80% 时间窗做训练、接着 10% 验证、最后 10% 测试。每个 SKU 的生产周期长短不一短周期品类样本量不够是常态如果某个 SKU 历史不足 45 天直接丢弃不要拿小样本硬凑。3. 模型选型与训练策略从 LSTM 基线到 Transformer 的演进路径许多从业者一上来就堆 Transformer这个做法在销量预测上风险很大因为电商日销量序列很短且信噪比极低。正确路径是先跑一个 LSTM 基线再评估是否升级到 Transformer。3.1 为什么从 LSTM 开始而不是直接上 TransformerLSTM 的优势在于参数少、对短序列拟合快、对数据量要求低。京东商品里大量长尾 SKU 的日销量集中在 0-10 件这类序列几乎全是零膨胀和个位数突变Transformer 的自注意力机制在这种低信息密度数据上是浪费算力的。LSTM 作为首发基线能快速确认两件事特征管线有没有泄漏、标签口径是否正确。基线结构我喜欢采用双层 LSTM 全连接输出头。第一层 LSTM 负责捕捉近期波动第二层把隐含状态压缩到 7 个未来时间步。输入张量的形状为 (batch, history_len, feature_dim)输出为 (batch, predict_len)训练 loss 用 Huber它对偶发大促尖峰不敏感MAE 会被少数爆品带偏。import torch import torch.nn as nn class SalesLSTM(nn.Module): def __init__(self, feature_dim, hidden_dim64, num_layers2, predict_len7): super(SalesLSTM, self).__init__() self.lstm nn.LSTM(feature_dim, hidden_dim, num_layers, batch_firstTrue) self.head nn.Sequential( nn.Linear(hidden_dim, 32), nn.ReLU(), nn.Linear(32, predict_len) ) def forward(self, x): # x: (batch, history_len, feature_dim) out, _ self.lstm(x) last_hidden out[:, -1, :] return self.head(last_hidden)这里没做序列解码输出而是只取最后时间步的隐状态映射到 7 个销量值参数少、训练稳定。想进一步改善可以在head前加一个 LayerNorm但如果训练集只有几千个 SKU任何多余结构都容易导致过拟合。3.2 Transformer 方案值得在什么时候替换数据量过万且时序依赖明显如果验证集上 LSTM 的 MAPE 稳定在 30% 以上降不下去且业务方要求捕捉更长周期的蓄水效应比如提前 15 天预热就可以考虑换上 Transformer。这里的 Transformer 不需要像 NLP 那样豪华取 Encoder 部分不做自回归解码用可学习的 positional encoding 注入日序信息。训练是在重现一个 encoder-only 结构输入 30 天特征序列在序列末尾加一个 式的聚合 token把它在 Transformer 输出端的向量接全连接层输出 7 天销量。相比 LSTM它在捕捉“促销前 15 天浏览渐增”这种远距离依赖上确实更好但参数量膨胀明显至少需要 2 万以上的 SKU 历史数据才值得。3.3 损失函数与评估指标的细节MAPE 与 Pinball Loss 如何取舍评估销量预测模型只用 MAPE 是片面的。当一个 SKU 真实销量是 1 而预测是 3MAPE 高达 200%但库存补货层面只多备了 2 件影响很小反过来真实销量 1000 预测 700误差率 30% 乘以高单价商品库存占用就直接超标了。所以我会在验证阶段同时看分位损失Pinball Loss。模型输出 50 分位作为主预测用于日常补货90 分位作为安全库存预测用于大促备货。这就要求输出头从 7 变成 7 * 2 或者再接一个分位数头。训练时两个头各自算 loss 再加权相加50 分位权重给 0.790 分位权重给 0.3避免安全库存预测反客为主。4. 训练管线搭建与参数设置从原始特征到可复现模型的完整流程有了模型结构和数据处理逻辑这一章讲具体的训练编排。核心原则是一切按 SKU 分组、一切按时间滚动、一切随机种子固定。4.1 用 PyTorch 训练跑通最小流程学习率、批次大小、早停策略训练代码的骨架如下这里用 PyTorch 的 Dataset 和 DataLoader 做序列迭代训练循环里固定随机种子并记录验证集上最优模型的权重。import numpy as np import torch from torch.utils.data import TensorDataset, DataLoader from torch.optim import AdamW x_arr, y_arr build_sequences(df, sku_id, feature_cols, target_col) # 按时间先切训练/验证前 80% 时间窗为 train split int(len(x_arr) * 0.8) train_ds TensorDataset(torch.from_numpy(x_arr[:split]), torch.from_numpy(y_arr[:split])) val_ds TensorDataset(torch.from_numpy(x_arr[split:]), torch.from_numpy(y_arr[split:])) train_loader DataLoader(train_ds, batch_size128, shuffleTrue, drop_lastTrue) val_loader DataLoader(val_ds, batch_size256, shuffleFalse) model SalesLSTM(feature_dimx_arr.shape[2], hidden_dim64, predict_len7) optimizer AdamW(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.5) criterion torch.nn.HuberLoss(delta1.0) best_mape float(inf) best_state None for epoch in range(60): model.train() for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() scheduler.step() model.eval() val_preds, val_trues [], [] with torch.no_grad(): for xb, yb in val_loader: pred model(xb) val_preds.append(pred.numpy()) val_trues.append(yb.numpy()) val_preds np.concatenate(val_preds) val_trues np.concatenate(val_trues) mape np.mean(np.abs(val_preds - val_trues) / (val_trues 1e-6)) if mape best_mape: best_mape mape best_state {k: v.clone() for k, v in model.state_dict().items()} if best_state: model.load_state_dict(best_state) torch.save(model.state_dict(), sales_lstm_best.pt)逻辑说明shuffleTrue对样本顺序洗牌但洗的是样本而不是时间轴每个样本内部的时间顺序保持不变。drop_lastTrue丢弃最后不足一个 batch 的样本防止某些 SKU 序列过短造成 batch 维度不一致。训练时加了梯度裁剪因为销量序列里的极端尖峰会把 LSTM 梯度推到 NaN。StepLR每 10 个 epoch 降一半学习率对 LSTM 这类参数量小的网络足够如果你换到 Transformer建议改用CosineAnnealingLR稳定性更好。参数经验值history_len 试过 15、30、4530 是性价比拐点hidden_dim 试过 32/64/128在验证集上 64 与 128 的差距小于 2%但训练时间翻倍所以 64 作为基线。batch size 在 128 到 256 之间没有显著差异。需要注意LSTM 类模型对初始学习率敏感lr1e-3起步十有八九没事但如果你自己重写了编码器建议先用 1e-4 跑 5 个 epoch 看 loss 趋势再恢复到 1e-3。4.2 类别不均衡问题长尾 SKU 比爆品多得多模型会被低频带偏京东的销量分布极度长尾。头部 5% 的 SKU 贡献了 80% 销量尾部大量 SKU 日均销量在 0-2 件。如果不处理这个问题训练出的模型会对所有 SKU 都倾向输出低销量因为全局 loss 被长尾样本主导。处理方案不是简单的过采样而是按销量分层采样。我一般会把训练集按近 30 天总销量分成 4 个桶高销量、中高、中低、低销量。每个 epoch 从各桶中按 3:2:2:3 的比例采样保证高销量和低销量 SKU 都被充分学习。代码上只需自定义一个 WeightedRandomSampler权重与桶内样本数成反比。sales_sum df.groupby(sku_id)[sales].transform(sum) bins pd.qcut(sales_sum, q4, labelsFalse) weights 1.0 / (bins.value_counts().loc[bins].values 1e-6) sampler torch.utils.data.WeightedRandomSampler(weights, num_sampleslen(weights), replacementTrue) train_loader DataLoader(train_ds, batch_size128, samplersampler, drop_lastTrue)这段代码按总销量分为 4 层给低频桶更高的采样权重让模型在每个 batch 里都能看到足够的尾部 SKU。缺点是低频 SKU 样本本身噪声大容易让训练 loss 波动所以配合 Huber 损失而不是 MSE 很关键。4.3 特征归一化的玄学时间序列里不能全局 fit要按每组 SKU 滚动归一化销量序列的幅度差异极大爆品日销 5000长尾品日销 0。如果你拿全局 min-max 归一化长尾品会被压缩到接近 0模型梯度更新直接失效。正确做法是对每个 SKU 单独归一化训练时用该 SKU 历史窗口内的均值和标准差去做标准化推理时同样用截止到当天的滚动统计量。from sklearn.preprocessing import StandardScaler def normalize_by_sku(df, feature_cols): scaler_map {} normalized_list [] for sku, grp in df.groupby(sku_id): scaler StandardScaler() grp[feature_cols] scaler.fit_transform(grp[feature_cols]) scaler_map[sku] scaler normalized_list.append(grp) return pd.concat(normalized_list), scaler_mapscaler_map在线上推理时必须保存下来对同一个 SKU 用历史的均值方差做变换如果线上新 SKU 没有 scaler则用同三级类目的聚合统计量兜底。这里还隐藏着一个重要坑做缩放时只能 fit 到历史数据绝不能把整个 SKU 时间序列包括未来拿去 fit否则又是未来泄漏。5. 销量预测的五个逼真踩坑现场现象、原因、解决模型训练和上线过程中我见过太多团队把时间耗在调参上最终发现问题出在数据处理和业务口径层面。这里整理五条最高频的踩坑血泪经验每一条都按现象、原因、解决列出。5.1 验证集 MAPE 非常低上线后预测值始终偏高或偏低现象是训练集和验证集都在同一数据分布上表现很好MAPE 在 15% 以内但实际预测未来 7 天销量时预测值持续偏低 30% 以上。原因多半是训练样本里包含了大量无促销期的平淡历史数据模型的统计均值主导了输出而你在验证时用到的历史区间恰好有一段轻微上升趋势模型学到的是“延续当前趋势”的能力一旦线上进入促销爆发期趋势突变超出训练分布。解决方法是做对抗验证把促销日与非促销日切分出两个评估集分别看 MAPE。促销日评估集如果明显差于非促销日说明模型对促销因子的表达不足。此时优先增加“折扣深度变化率”这类连续特征而不是增加模型复杂度。5.2 部分 SKU 预测结果恒为 0检查发现是序列切片的锅现象是低销量长尾 SKU 的模型输出全是 0无论怎么调整学习率都不见起色。原因是样本构建时很多 SKU 的历史序列里大量连续 0 值模型学到的最优策略就是“保守预测为 0”。这种预测在业务上不可用因为补货算法拿到 0 预测就直接不补货形成缺货死循环。我的解决思路是把销量标签做一个 log1p 变换后训练让 0 到 1 的区分变得敏感降低全零占优的局面。推理时对输出做 expm1 还原。如果是间歇性需求比如某些 SKU 只在周末出单则额外加一个布尔特征“昨日是否有销量”对平滑有帮助。5.3 模型对 618 和双 11 的预测严重低估且峰值永远滞后一拍现象是大促当天的真实销量是预测值的 3-5 倍模型产生的峰值总是比真实峰值晚 1 天。原因是模型结构里用历史窗口 30 天去预测未来 7 天而大促的销量爆发更多依赖当天的流量投放和资源位曝光这些信息在历史序列里没有提前量。处理方案通常是加两路信号一是促销计划的确定性特征倒计时和折扣力度已经在前面的特征工程中构造二是让模型用多任务学习同时输出“曝光流量”作为辅助任务。流量是可被运营计划部分控制的强相关变量把曝光预测作为辅助 loss能让主模型在大促前捕捉到流量爬坡信号销量预测峰值也会提前。5.4 数值稳定性翻车训练过程中 loss 突然变 NaN现象是前几个 epoch 正常某个 batch 处理后 loss 变成 NaN后续全部崩掉。我发现最常见的原因有两个一是某些 SKU 的销量字段里混入了超大值比如单日销量 999999 的异常订单或测试订单标准化也拉不回来二是 LSTM 的梯度在长序列上累积爆炸即使加了 clip 也可能已经溢出。解决方法是入口过滤在数据加载时直接把销量大于 99 分位数的值截断为 99 分位值再做 log1p 变换。同时在训练循环里加一个 loss 检查逻辑如果是 NaN 就跳过当前 batch不退出程序至少保住已训练权重。5.5 特征与预测目标错位一天模型每天都在“预测昨天”现象是模型整体精度不错但逐日预测曲线整体右移一天真实销量峰值出现在预测值后一天。原因是数据落表延迟。京东订单表的支付时间通常比销量统计口径晚数小时到一天如果 ETL 是按自然日跑的当天晚上跑出来的“当日销量”其实包含了昨日深夜订单而你构造特征时同样用了这个延迟数据导致特征时间轴比标签晚了一天。解决方法是统一时间口径销量标签使用支付且发货的确认时间作为归属日流量特征使用行为发生时间作为归属日两者独立清洗后再按 sku_id date 对齐。这个修正通常能把时序偏置整体移除。6. 上线部署与增量改进把预测结果送入库存系统的最后一步训练完成的模型只是离线产物真正要产生价值是把它接进补货和运营的决策链路。这一步的关键是写一个快速的推理服务同时让模型具备定时重训和人工修正能力。6.1 用 ONNX 导出模型做低延迟推理PyTorch 模型直接部署到 Java/Golang 服务里不现实最常见做法是导出 ONNX用 ONNX Runtime 做 CPU 推理。导出时把输入输出名字固定并绑定每个 SKU 的标准化参数。import torch.onnx model.eval() dummy_input torch.randn(1, 30, feature_dim) torch.onnx.export( model, dummy_input, sales_lstm.onnx, input_names[history_features], output_names[pred_sales], dynamic_axes{history_features: {0: batch_size}}, opset_version12 )这样导出后推理服务只需要加载标准化参数并拼好特征矩阵一次推理大概在几毫秒级别完全满足定时补货计算的吞吐要求。6.2 规则守卫层深度学习输出不能直接作为补货依据这是我个人的习惯也是我认为最稳妥的工程做法深度模型输出进库存系统前必须过一层基于业务规则的守卫。规则层负责处理模型不可见的信息比如某 SKU 正在参加“买一赠一”活动或某商品由于品牌公关事件被临时下架。规则层的覆盖逻辑大体为若预测值小于近 7 天平均销量的 0.2 倍则抬升到 0.2 倍作为安全下限若预测值大于近 7 天平均销量的 10 倍则拦截并输出告警由运营人工确认。这个规则不是限制模型能力而是防止极端输入导致库存震荡。6.3 模型的自学习闭环周期性重训与 badcase 反馈销量预测不存在一劳永逸的模型我的习惯是每周重训一次用过去 90 天的数据并在每次重训后对比新模型与线上模型的预测差异。如果差异超过 10%则备份旧模型并灰度发布新模型一天。灰度方式最简单的是按 SKU 品类维度分桶而不是按用户流量分桶这样业务反馈更直观。在这个闭环上叠加一个 badcase 收集表运营在补货系统里看到预测值与实际上架销量差异过大的 SKU手动标记原因。每周用这些标记样本做一次特征分析看能否抽象成新特征或新规则。这比不停调参有更高性价比。我个人的体感是电商销量预测这个方向70% 的时间花在数据对齐和特征构造上15% 在模型训练最后 15% 在部署与规则打磨。如果你刚开始接触这个源码方案请一定先跑通最小 LSTM 基线、确认评价指标能复现再扩展成 Transformer 和分位数输出。希望这份笔记能帮你在自己的补货数据上少走几条弯路。本文还有配套的精品资源点击获取
返回列表