资讯动态

用PyTorch DNN预测长尾商品销量:特征工程与训练策略全解析

发布时间:2026/9/15 22:18:53 来源:尧图企业网站定制
简介针对长尾商品销量预测这一难题这份基于TensorFlow 1.13的DNN项目源码提供7天、30天与60天销量预测的完整实现适合供应链备货场景中的算法工程师也适合希望用低阶API深入理解DNN训练的开发者。资源共6个文件含5个Python脚本和1个说明文档压缩包仅20KB轻量易读。项目区分单输出与多输出目录分别对应单目标与多目标预测完整覆盖TensorBoard可视化tf.summary.FileWriter记录日志、模型保存tf.train.Saver、训练/验证/测试集划分、自定义EarlyStopping机制连续10轮未刷新最佳验证精度即停止等关键环节其中验证集用于超参数选择测试集用于评估模型泛化能力。离线预测阶段则使用import_meta_graph与graph.get_operation_by_name恢复模型并附有tensorboard启动方式便于复现训练过程。已有126人学习下载对于想掌握低阶TensorFlow API训练DNN回归任务、理解模型训练全流程的开发者是一份简洁实用的参考代码可作为快速上手的工程模板帮助打通从数据划分、模型训练到离线预测的完整链路。1. 长尾商品销量预测难点不在网络多深而在数据怎么喂做电商或供应链数据的人应该都有这个体感头部爆款走个移动平均就能预测个大概真正让人头疼的是长尾商品。一个月只卖几件、断货十几天、新上架没有任何历史销量模型要么预测出一堆负数要么把所有长尾都收敛成同一个均值。这个项目的标题很直白——用 Python 写一个 DNN 模型来预测长尾商品销量下载下来是一份项目源码。它想解决的并不是“把网络层数堆深一点”而是三个更具体的问题长尾分布的标签怎么变换、稀疏的高基数离散特征怎么进网络、验证集怎么切才不会被数据泄露坑掉。下文按这类项目最常见的一套实现路径从特征工程一直拆到模型验收。2. 把长尾销量装进张量预处理、数值变换与特征构造长尾销量预测的第一步不是nn.Sequential而是把业务表变成模型能吃的张量。原始数据通常是三列的明细表sku_id、dt、sales_qty偶尔带价格、类目、上下架状态。销量分布极端右偏——头部商品一天能卖出几百上千尾部商品一周成交两三笔还有大量连续为 0 的记录。直接把原始销量当标签喂给 DNN损失函数会被头部样本主导模型对尾部商品基本表现为长期预测一个全局均值。所以预处理的目标有两个把标签和特征压缩到模型好优化的量纲同时给网络提供足够的历史上下文。2.1 log1p 变换销量从右偏到近正态的必修课处理右偏标签的标准做法是在标签和强偏态数值特征上都做对数变换我一般选log1p而不是logimport numpy as np import pandas as pd def transform_labels(df, value_colsales_qty, group_colsku_id, date_coldt): df df.copy() # log1px0 时输出 0x 很大时增长放缓压住右尾 df[label_log] np.log1p(df[value_col].clip(lower0)) # sort 保证后续滚动窗口不回看未来 df df.sort_values([group_col, date_col]).reset_index(dropTrue) return df逻辑说明np.log1p(x)就是ln(1x)相比裸的np.log它在销量为 0 时有定义同时小值区的斜率更陡模型在小销量区间能有更细的梯度信号。预测完成后再用np.expm1还原原始单位。这个地方最常见的错误是还原时忘了-1导致整体预测系统性偏高尤其在预测值 1 时偏差会被放大对长尾商品几乎不可用。clip(lower0)的作用是把由于数据采集问题出现的负数销量归零电商场景里常见的原因是售后、超卖冲正这些负值对预测没有业务意义直接截断更稳。2.2 高基数离散特征为什么不用 one-hot而用实体嵌入长尾预测里最棘手的特征是sku_id本身。一个中型电商平台在售 SKU 动辄几十万用 one-hot 编码会得到一个几十万维的稀疏向量普通 MLP 根本训不动而且 one-hot 抹掉了商品之间的相似性——两个销量行为相似的商品在特征空间里没有任何距离概念。实体嵌入entity embedding处理这种情况是更常规的做法为每个sku_id学习一个低维稠密向量语义上相近的商品向量距离也近冷启动时还能借邻居商品的信息。class SkuEmbedding(nn.Module): def __init__(self, num_skus, embed_dim32): super().__init__() # num_skus 是去重后的 SKU 数量embed_dim 一般取 16~64 self.embed nn.Embedding(num_embeddingsnum_skus, embedding_dimembed_dim, padding_idx0) def forward(self, sku_id): # sku_id 是经过 LabelEncoder 的 tensor形状 [batch] return self.embed(sku_id)逻辑说明先对sku_id做数值编码0 号索引保留给未知商品然后经过nn.Embedding得到一个[batch, 32]的向量再和连续特征拼接后进全连接层。Embedding 的维度要和商品总量匹配——几十万量级时 32 维够用几万量级可以降到 16尽量避免一个超大 embedding 矩阵带来显存压力和过拟合。训练时要留意padding_idx0对应的行不能被更新否则未知商品的向量会被训练噪声反复拉动。2.3 滚动窗口特征与占比特征给模型可比较的历史记忆销量预测的核心信号是“最近一段时间卖了多少”但长尾商品直接取原始滚动销量有两个问题一是序列里大量为 0窗口均值几乎没有区分度二是量纲差异大头部和尾部商品的窗口均值不在同一个数量级。常见做法是构造相对特征而不是绝对特征feature_matrix df.copy() feature_matrix[recent_14_sum] ( feature_matrix .groupby(sku_id)[sales_qty] .transform(lambda x: x.shift(1).rolling(14, min_periods1).sum()) ) feature_matrix[recent_7_mean] ( feature_matrix .groupby(sku_id)[sales_qty] .transform(lambda x: x.shift(1).rolling(7, min_periods1).mean()) ) # 最近7天占比刻画近期相对于过去一个月的占比变化 feature_matrix[recent_7_ratio] ( feature_matrix[recent_7_mean] / (feature_matrix[recent_14_sum] 1.0) )逻辑说明.shift(1)是关键它把每个特征向后挪一行确保预测当天用不到当天的销量这是防止数据泄露最基础的一步。rolling(14, min_periods1)表示窗口长度 14 天最少有 1 个非空值就计算这保证了新品在样本量不足时也能得到部分特征而不是产生 NaN。recent_7_ratio是个比值特征刻画近一周相对近两周的占比用来捕捉“趋势加速”还是“趋势衰减”这个特征对长尾商品的过期爆款识别比绝对值敏感。对时间特征日期本身不要直接当数字喂进去20240601这类整数的量级没有意义。更稳的做法是拆出weekday、month用正弦余弦编码再带上一个holiday_flag。对品类、店铺这类中高基数的离散特征可以仿照 SKU 的做法用 embedding 同时训也可以退一步做 frequency encoding用出现次数替代类别 ID虽然损失部分语义但胜在省显存且泛化能力不弱。下面的表格是这套特征工程的选型参考特征编码方式维度适用条件sku_id实体嵌入16~64SKU 量大且有历史销量category_id实体嵌入或频率编码8~16类目粒度稳定recent_7_mean数值缩放后直入1全场景recent_14_sum数值缩放后直入1长尾为主的数据集weekdaysin/cos 编码2存在周期性is_new_item二值标签1有新品上架记录表格里第一个特征是 SKU 的实体嵌入向量维度按商品总量调整中间两个滚动窗口特征是连续值建议做 z-score 标准化后拼接进全连接层is_new_item对长尾冷启动非常关键它让模型在特征维度上就能区分“没有历史的新品”和“一直卖不动的老品”后续可以走不同的隐层分支这个话题放到第 4 章展开。3. 搭一个能跑起来的销量 DNNPyTorch 模型、损失与训练循环预处理做完之后进入模型实现环节。销量预测场景里DNN 相比 GBDT 的优势主要体现在两点一是能直接消费实体嵌入高基数离散特征的泛化能力比 XGBoost 的 one-hot 编码强二是在 log1p 变换后的标签空间里DNN 的拟合能力足够且预测结果天然是实数方便后处理。但这不意味着模型可以堆得很复杂长尾数据本身信息量有限模型参数越多越容易出现过拟合。3.1 模型架构三层 MLP 加 BatchNorm 和 Dropout 就够这套源码项目里最实用的模型结构我的经验是把 DNN 控制在 3 到 4 个全连接层每层宽度不超过 256中间穿插 BatchNorm 和 Dropoutimport torch.nn as nn class SalesDNN(nn.Module): def __init__(self, num_skus, embed_dim32, numeric_dim8, hidden_dim128): super().__init__() # SKU 实体嵌入 self.sku_embed nn.Embedding(num_embeddingsnum_skus, embedding_dimembed_dim, padding_idx0) # 第一层embedding 数值特征拼起来进 MLP self.fc1 nn.Linear(embed_dim numeric_dim, hidden_dim) self.bn1 nn.BatchNorm1d(hidden_dim) self.fc2 nn.Linear(hidden_dim, 64) self.bn2 nn.BatchNorm1d(64) self.dropout nn.Dropout(p0.3) self.out nn.Linear(64, 1) def forward(self, sku_id, numeric_features): embed_out self.sku_embed(sku_id) # 拼接离散和连续特征 x torch.cat([embed_out, numeric_features], dim1) x torch.relu(self.bn1(self.fc1(x))) x torch.relu(self.bn2(self.fc2(x))) x self.dropout(x) return self.out(x).squeeze(-1)逻辑说明输入层把 SKU 的 embedding 和数值特征拼接第一层全连接输出 128 维后面激活前先过 BatchNorm这能显著缓解训练初期梯度震荡中间层降到 64 维起到瓶颈作用强迫模型把主要信息压缩到低维空间Dropout 放在最后一层全连接之后只对最后输出层之前做随机失活对防止尾部样本的过拟合比放在输入端更有效。输出层不加激活函数因为目标是 log1p 空间的连续值。numeric_dim8对应上一章的滚动均值、滚动和、占比特征、时间正弦余弦等实际接入时注意数值特征要先做标准化否则 BatchNorm 的效果会被极端的量纲差距削弱。dropout 在推理时要记得调用model.eval()否则验证集上的预测会自带噪声结果不稳定。3.2 损失函数选型log1p 空间里的 MSE本质是近似优化 RMLSE这个项目最值得留意的设计决策是损失函数。如果直接在原始销量空间用 MSE模型会被少数大销量样本的平方误差带偏如果直接在地下用 MAE梯度恒定收敛不稳且对离群点完全没有惩罚梯度。我对这类销量预测项目的一个固定做法是在 log1p 变换后的标签空间里用 MSE等价于对原始空间的几何误差做惩罚这其实就是 RMLSE 的变体import torch.nn.functional as F def loss_fn_g(pred_log, target_log): # pred_log 和 target_log 都经过 log1p 变换 diff pred_log - target_log diff torch.abs(diff).clamp(max5.0) # 负误差更宽容正误差更严格实际缺货成本高于滞销 weight torch.where(pred_log target_log, 1.2, 1.0) return torch.mean(torch.square(diff) * weight)代码说明先算 log 空间的预测误差把差异截断在 5.0 以内防止极端样本的梯度爆炸再给低估样本更高权重——业务上缺货损失通常高于滞销所以当预测值低于真实值时权重是 1.2反之是 1.0。这个权重比例可根据业务调整保守型备货可以反向设置。这里没有用nn.MSELoss的原因是想保留非对称加权的灵活性TDNN 项目源码里一般也能看到这种自定义 loss 的写法。3.3 训练循环与验证策略时间序列必须按时间切分不随机打散长尾销量预测中最隐蔽的数据泄漏来自验证集划分。随机 K 折在这类任务里不正确同一天的数据在训练和验证集同时出现模型相当于看过了未来验证分数会虚高。常见做法是按日期留出最后一段作为验证集import pandas as pd from datetime import timedelta def split_by_time(df, date_coldt, val_days14): latest df[date_col].max() val_start latest - timedelta(daysval_days) train_df df[df[date_col] val_start] val_df df[df[date_col] val_start] return train_df, val_df训练时再配合Dataset和DataLoader把样本组织成 batch这一套流程大家都很熟悉真正要强调的两点是验证集内如果长尾商品占比过低指标的置信度会不够建议在验证集中额外抽取尾部子集单独统计另一个是训练时对每个 batch 内的 SKU 数量做限制防止同一商品的多个时刻在同一个 batch 里让抽样的多样性下降。4. 让 DNN 在稀疏销量上稳住训练策略、调参与冷启动处理模型结构定下来之后训练策略决定了它能否真正在尾部数据上收敛。长尾数据的特征是信号稀疏、噪点比例高常见的深度学习技巧在这里需要换成一套带有偏置的策略小批次制造梯度噪声、早停监控验证集、对零销量样本降权、新品走单独分支。4.1 小批次与学习率调度梯度噪声在稀疏数据上是一种正则化批次大小我会刻意设得比常规任务小32 或 64而不是 256 或 512。原因很简单长尾数据里相邻样本的信息量差异极大大批次计算出来的梯度会平滑掉尾部样本的“有效信号”导致模型更偏向学习头部模式。小批次的梯度带有较大的方差反而能迫使模型每步都照顾到更多样的样本。学习率调度用ReduceLROnPlateau比固定学习率好用因为它能在验证损失长时间不动时自动降学习率让模型在小范围内继续微调from torch.optim.lr_scheduler import ReduceLROnPlateau optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler ReduceLROnPlateau(optimizer, modemin, factor0.5, patience3, min_lr1e-6) # 每个 epoch 结束时: scheduler.step(val_loss)参数说明factor0.5表示验证损失不降低时学习率减半迭代两次就是降到原来的四分之一patience3表示连续 3 个 epoch 不下降才开始降给模型留出跳出局部极值的空间weight_decay是全连接层的 L2 正则长尾数据里目前看起来 1e-4 起步是稳妥的。监控验证损失时不要用训练 loss训练 loss 会在 Dropout 和 BatchNorm 的影响下表现为稳定下降但它并不能代表泛化水平。4.2 早停与模型保存按 RMLSE 而不是 MSE 决策早停的监控指标应当和业务目标一致。对销量预测任务我的做法是训练时监控验证集上的 RMLSE在验证指标连续多个 epoch 不改善时恢复最优权重并停止训练。这个做法的效果好于用 MSE 决策因为 RMLSE 天然更贴近“长尾预测准确性”这个业务目标import numpy as np def rmse_g(y_true_log, y_pred_log): return float(np.sqrt(np.mean(np.square(y_pred_log - y_true_log)))) best_score float(inf) patience_counter 0 for epoch in range(100): train_one_epoch(model, train_loader, optimizer, loss_fn_g) val_preds predict(model, val_loader) val_score rmse_g(val_targets, val_preds) if val_score best_score: best_score val_score torch.save(model.state_dict(), best_model.pt) patience_counter 0 else: patience_counter 1 if patience_counter 10: break逻辑说明torch.save保存的是模型权重而不是整个模型对象这样文件的平台兼容性更好换机器部署或换 PyTorch 版本都不容易出问题。patience10对长尾数据偏宽容因为尾部样本的验证分数波动大太小的 patience 会频繁触发早停配合ReduceLROnPlateau时两者的 patience 建议设定在 3 到 10 之间且早停的 patience 要大于学习率调度的 patience让学习率先降完再决定是否停。训练过程中一个容易踩的坑是验证集的长度抖动预测最后一排没有对齐的样本时要把时间特征也顺延否则日期编码不变会导致预测结果季节漂移。4.3 冷启动处理新品不硬套老品逻辑长尾场景里真正无解的分支是新品的冷启动——没有任何历史销量滚动窗口特征全部缺失。我对这类样本的做法是单独分支不让它们和老品混在同一个全连接层里。操作上有两种落地方式轻量方案是把特征缺失位填充类目均值同时增加一个is_new二值特征让模型自己学习偏移更重的方案是给冷启动样本单独训练一个小型模型只用商品属性、品牌、类目均值这些静态特征。# 冷启动样本处理类目均值填充 is_new 标记 def fill_new_item_features(df, category_colcategory_id): df df.copy() category_mean df.groupby(category_col)[sales_qty].transform(mean) df[is_new] df[recent_14_sum].isna().astype(int) df[recent_7_mean] df[recent_7_mean].fillna(category_mean) df[recent_14_sum] df[recent_14_sum].fillna(category_mean * 2) return df这里类目均值乘以 2 是经验性的初始值它让滚动窗口特征在缺失时保持一个较大值模型不至于完全失去方向。如果业务上对新品备货偏保守可以改成乘以 1如果默认新品会有一个爬坡期也可以填一个小常数。冷启动分支的训练数据量通常只有几百到几千条所以它的模型结构会更简单两层全连接加 dropout 并且早停的 patience 应该更小因为它更容易过拟合。下表汇总这套训练流程的关键参数和调整方向参数默认值调整方向说明batch_size32过拟合就减收敛慢就加梯度噪声正则化learning_rate1e-3震荡大就降到 1e-4AdamW 配 warmupembed_dim32SKU 超 50 万就升到 64显存有限时降 16dropout0.3尾部噪声大升到 0.5冷启动模型建议 0.5weight_decay1e-4参数规模大加 1e-3对宽模型更有效ReduceLROnPlateau.patience3验证波动大调到 5必须小于早停 patience参数里要重点说明的是batch_size的调整方向它和Dropout是两个可以互相配合的旋钮小批次加低 dropout 与大批次加高 dropout 在效果上往往等价但前者对长尾样本更友好因为每个 batch 结构更随机。显存充足也不要把 batch 开到 256 以上收益有限且会把上次提到的梯度噪声正则化效果抹掉。5. 预测结果怎么验收RMLSE、分桶校验与落库字段设计模型训练完成不等于项目结束长尾预测还要过验收这一关。常见的误区是只报一个总体的 RMLSE 就说效果达标但长尾场景里总指标会被头部商品拉得虚低。我的做法是算完总体指标后再按真实销量分桶去看模型在尾部子集上的表现。预测值和真实值都在 log 空间里算 RMLSE自己实现一个带分桶评估的版本import numpy as np import pandas as pd def eval_by_bucket(y_true, y_pred, bucket_edges[0, 1, 3, 10, 100]): df pd.DataFrame({true: y_true, pred: y_pred}) df[bucket] pd.cut(df[true], binsbucket_edges, rightFalse, labels[f[{bucket_edges[i]},{bucket_edges[i1]}) for i in range(len(bucket_edges) - 1)]) report {} for label, group in df.groupby(bucket, observedFalse): diff np.log1p(group[pred]) - np.log1p(group[true]) report[label] { count: len(group), rmlse: float(np.sqrt(np.mean(diff ** 2))), bias: float(np.mean(diff)) } return pd.DataFrame(report).T逻辑说明这个评估函数把真实销量按 0、1~2、3~9、10~99、100 以上分成五桶对每个桶单独计算 RMLSE 和 bias。bias 是预测减真实的均值在 log 空间的值如果 bias 明显为正说明该桶整体高估对备货的影响是库存积压为负则是整体低估更容易发生缺货。各桶的样本量也是重要信息——如果尾部桶的样本数过少说明验证集时间窗口太短需要拉长验证集。全量预测指标和分桶指标都确认后预测结果要落回到业务表中。我习惯在结果表里加三个字段pred_qty用np.expm1还原为整数销量pred_quantile存预测值的分位数比如 0.5 和 0.9用作安全库存的参考线model_version记录模型文件名方便回溯哪次预测用了哪套参数。数据落地时对长尾销量做一次下限截断比如max(pred_qty, 0)这是最容易被忽略但直接影响报表观感的细节。针对验证时发现的尾部预测系统性偏高的情况还有一个非常实用的调优技巧在 log1p 空间的预测结果上按桶做偏差修正。比如通过上面的eval_by_bucket发现销量在 1 到 2 件的商品整体高估 0.3log 空间就在预测时对这一桶统一减去 0.3再还原成真实销量。这种后处理在业务上叫“分桶纠偏”实现成本极低但通常能让尾部桶的 RMLSE 在有限样本下改善一到两个百分点比继续加宽网络收益更明显。整个项目的验收闭环也落在这里训练指标、分桶验证、偏差修正、结果落库每一步都是可验证的。本文还有配套的精品资源点击获取

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

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

免费获取报价