news 2026/9/2 3:22:06

Transformer在M5销量预测中的实战:从数据预处理到模型优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Transformer在M5销量预测中的实战:从数据预处理到模型优化

简介:这是一份面向时间序列预测学习者和竞赛玩家的Python实战项目,围绕M5销量数据,利用Transformer架构中的自注意力机制处理多维、多频次的商品销售序列。项目从原始数据预处理开始,涉及多通道序列展开、时间戳编码等关键步骤,并完整实现了基于Encoder的Transformer预测模型,包含数据脚本、模型定义、训练与验证代码,可帮助读者理解如何将NLP领域的Transformer迁移至时序预测,并掌握MAE/MSE损失设计、Adam优化器调参、时间序列分割验证等竞赛实用技巧。资源共29个文件,涵盖12个Python脚本、6个字节码文件、模型权重pth、说明文档md、配置xml以及训练损失曲线png等,压缩包整体约47.75MB,目录内同时包含训练日志与预测脚本,结构清晰,便于逐个模块复现。目前已有192人学习下载,适合具备一定Python和深度学习基础、希望了解Transformer实战细节的读者。

1. 项目整体设计与思路拆解

1.1 为什么用Transformer来做M5预测

先说结论:M5这个比赛数据集,非常适合用来验证Transformer在时间序列预测上的能力,但同时它也非常“刁钻”,会暴露Transformer的不少短板。当初我做这个项目的时候,就是想看看去掉RNN那套循环结构之后,纯注意力机制在零售销量这种强周期性、强随机性的数据上到底能跑成什么样。

M5数据集是沃尔玛公开的销售数据,核心任务是预测未来28天内商品、商店、部门三个层级的销量。它有三个非常突出的特点:一是序列数量极多,有30490条商品-商店组合序列;二是历史数据长,每条序列有1941天的日销售记录;三是数据里混合了强趋势、强季节性、节假日扰动和价格变动等因素。用传统时序模型比如ARIMA,跑到这个规模基本是灾难;用LSTM,序列拉长之后梯度传播是个问题;而Transformer的全局注意力机制,可以把任意位置的信息直接关联起来,天然适合处理长序列中的跨周期依赖,比如“今年感恩节前后的销量模式”和“去年感恩节前后的销量模式”之间的关联。

当然,Transformer不是没有代价。它的标准注意力是O(n²)复杂度,n是序列长度,输入序列越长计算量越大。M5的序列虽然1941天很长,但实际做训练时不会把整个历史一次性塞进模型,通常会切成固定长度的窗口,比如用过去90天预测未来28天。这样窗口内部用Transformer编码,既保留了长程依赖的建模能力,又把计算量控制在了合理范围内。

1.2 与LSTM、TCN等方案的对比选型

我一开始不是直接上Transformer的,中间对比过LSTM和TCN(时间卷积网络)。简单说一下实测感受:

方案优点在M5上的问题
LSTM实现简单,时序建模自然长序列下梯度易衰减,训练速度慢,对并行计算不友好
TCN训练快,感受野可调,内存占用小感受野受卷积核和层数限制,跨期依赖的捕捉能力不如注意力机制
Transformer全局依赖建模能力强,训练可并行计算量大,位置编码需要对时间序列专门设计,对噪声敏感

M5这种零售数据里,最值钱的特征恰恰是“很久以前某个特殊事件对销量的影响”,比如去年同期的大促、某次天气异常。这些信息距离预测日可能隔着两三百天,TCN想覆盖这么长的感受野需要堆很深的层数,LSTM则容易在传播过程中把这些遥远的信号磨没。Transformer的注意力机制可以直接计算当前位置与历史任意位置的相关性,这种能力对零售预测来说非常契合。

另外从工程角度看,Transformer的并行训练特性在数据量大时优势明显。M5有30490条序列,如果每条序列都单独过LSTM,训练效率会比较低下;而Transformer可以把固定窗口的数据打包成batch并行计算,GPU利用率高不少。

1.3 模型整体架构选型:Encoder-only还是Encoder-Decoder

时间序列预测里Transformer有两种常见用法:一种是标准的Encoder-Decoder结构,比如Informer、Autoformer这些变体;另一种是只用Encoder,把预测任务建模成“从历史序列回归未来值”的问题。我做这个项目时选择了Encoder-only方案,原因很简单:M5的预测目标是未来28天的多点输出,本质上是个多步回归问题,而不是序列生成问题。

Encoder-only的实现方式是把历史窗口输入Encoder,取输出层每个位置的特征,经过一个全连接映射头直接输出未来28天的预测值。相比Encoder-Decoder,它少了自回归解码的累积误差问题,训练时也不需要mask技巧,实现起来干净利落。缺点是模型对远期时间依赖的建模完全依靠输入窗口内的信息,如果窗口长度不够,很多长周期信号会丢掉。实测下来,输入窗口设为90天(约13个星期,能覆盖完整季度内的周期性)比较合理。

模型的整体结构如下:输入特征经过embedding映射,加入时间位置编码,进入多层Transformer Encoder,每个block包含多头自注意力和前馈网络,中间用LayerNorm和残差连接。Encoder输出经过一个全局池化或直接展平,再过两层全连接,最后输出维度为28(未来28天销量)。

2. M5数据集核心细节与评估体系

2.1 数据组成与层级结构

M5数据集由三张表组成:calendar.csv包含1969天(2011-01-29到2016-04-24)的日历信息,包括日期、星期、事件种类(如Super Bowl、Mother's Day等);sell_prices.csv是每个商品在各店铺的每日售价;sales_train_validation.csv是训练数据,列名是d_1d_1941,行是每个商品-商店组合。

需要注意,sales_train_validation.csv只给了1941天数据,而日历表有1969天,最后28天(d_1942d_1969)是"预测目标",比赛时没有放出,用来在测试集上做评估。验证集则使用d_1914d_1941这28天,用前1913天做训练。这个划分方式很重要,做验证的时候必须严格对齐日历,否则容易把未来的信息泄漏到模型里。

数据集的层级结构是:3个州(CA、TX、WI)→ 10个商店 → 3个部门(食品、家庭、爱好)→ 商品。总共有30490个商品-商店组合,每个组合一条独立的时间序列。但预测时最终的评估指标不是对每条序列单独计算的,而是要求你在多个层级上做聚合预测,这就需要考虑层级一致性:底层预测值相加后是否和上层预测值一致。

2.2 WRMSSE指标的计算逻辑

M5的官方评估指标是WRMSSE(Weighted Root Mean Squared Scaled Error),它是在12个层级上分别计算RMSSE再加权求和。这12个层级包括:全量(所有商品所有店铺)、州层级(3个)、商店层级(10个)、部门层级(3个)、州+部门层级(9个)、商店+部门层级(30个)、单个商品序列(30490个),并剔除掉一些没有预测价值的组合。

RMSSE的计算方式和M4比赛用的MASE有点像,核心是先算“朴素预测误差”作为缩放因子。具体来说,对每条序列,先计算训练期内相邻两天销量差的绝对值均值,然后以未来28天预测误差的均方根除以这个缩放因子。公式是:

RMSSE = sqrt(mean((y_actual - y_pred)²) / mean((y_t - y_{t-1})²))

其中第二个mean是在训练序列上计算的。分母的直觉是:模型好不好,不是看绝对误差,而是看相比“拿昨天当今天”这种最简单预测提升了多少。如果模型预测的误差平方和比朴素预测还大,RMSSE就会大于1,说明模型连baseline都不如。

WRMSSE则是将12个层级的RMSSE按一定权重加权求和,权重是根据每个层级包含的商品销售额占比确定的。实际计算时可参考M5官方提供的evaluation.R代码,把它改写为Python版本,用numpy和pandas实现。

2.3 预测任务定义与数据划分

预测目标是未来28天,即d_1942d_1969。验证集是d_1914d_1941,训练集是d_1d_1913。这个划分模拟的是真实业务场景:你手上只有截止某一天的历史数据,要预测未来一个月。

实际项目里,我建议先不管官方测试集,而是自己从训练数据里再切出一段“本地验证集”,比如用d_1886d_1913作为训练,预测d_1914d_1941。这样在调参时可以快速验证模型效果,不需要频繁提交到比赛平台。

另外要特别留意:M5的销售数据里有大量0值。节假日、促销之前可能有很长的零销量区间,尤其是一些冷门商品。处理不当会让模型过度适应全零序列,拉低整体指标。

3. 数据预处理与特征工程实操

3.1 序列切窗与样本构造

M5每条序列1941天,训练时如果整条序列直接输入,显存吃不消,而且模型很难学到“近期行为和远期行为”的差异。我的做法是用滑动窗口将长序列切成训练样本:窗口长度设为90天(输入),预测长度28天(输出),滑动步长可以设为1天或7天。步长为1样本量最大,但重叠严重,训练较慢;步长为7可以大幅减少样本数,同时保证每周的周期性能被覆盖到。我实际测试,步长设为7时,效果几乎没有下降,但训练速度快了接近5倍。

切窗时要注意:训练样本的预测段不能和验证段重叠。比如验证段是d_1914d_1941,那么构造训练样本时,最后一个样本的预测段必须落在d_1913之前,不能在d_1886d_1913的序列里切出一个预测段延伸到d_1914之后的样本。这个细节看似简单,但一旦写错,验证集效果会虚高,提交到官方测试却掉线。

3.2 特征工程:价格与日历特征

M5的特征工程远比想象中重要。单纯用销量历史做输入,Transformer能学到的信号有限。我把特征分成三大类:基础销量特征、外部特征、时间特征。

  • 基础销量特征:过去90天的销量值(这是核心输入),以及销售额占比、销量排名等衍生特征。其中销量排名在序列间有可比性,放入模型中效果不错。
  • 外部特征:价格。sell_prices.csv给出了商品在各店铺的日常售价,价格变动对销量有直接影响。我把价格做了两种编码:一是绝对价格,输入给模型;二是价格相对变动率,即(当前价格 - 前N天最低价) / 前N天最低价。这个相对变动率比绝对价格更能反映促销效应。
  • 时间特征:星期几、月份、节假日。日历表里有两个关键列:event_name_1event_type_1,记录了当天的节假日类型,比如宗教节日、文化节日。我将其进行one-hot编码,同时计算“距离最近节假日还有几天、已经过去几天”两个特征。后者对零售销量预测帮助极大,因为节假日前后几天的销量变化规律往往比节假日当天本身更重要。

特征处理的另一个重要点是标准化。Transformer对输入尺度敏感,销量数据跨度从0到几百甚至上千,直接喂进去会让注意力权重偏向数值大的维度。我的做法是逐条序列做z-score标准化:用训练部分销量的均值方差归一化,预测的时候再用同样参数还原。注意归一化参数必须在训练段上计算,不能包含验证段的统计量,否则仍是泄漏。

3.3 数据泄漏的三大陷阱

数据泄漏是时间序列预测最容易踩的坑,我梳理一下M5上最常见的三种:

第一,用未来价格预测过去销量。sell_prices.csv里包含完整时间范围的价格,但实际预测时不可能提前知道未来28天的价格走势。训练时如果直接使用销售日后28天的真实价格特征,模型会学到“借用未来信息”的作弊路径,验证集效果很好但提交后严重下滑。处理方式是把价格特征做时序对齐:训练时使用预测窗口之前的已知价格作为特征。

第二,用全局统计量替代时序统计量。比如对整个训练集计算所有商品销量的均值方差,再将标准化后的销量输入模型。这样做的问题是标准化的结果会吸收未来的分布信息,因为全局统计量里包含验证段数据。正确做法是逐条序列、按时间截止点计算统计量。

第三,切窗时未清理重叠区间。步长为1天切窗时,相邻两个窗口有89天重叠,这会让模型见过“几乎一样的样本”很多次,容易过拟合。用步长为7天可以缓解这个问题,另一个可选方案是随机采样窗口,而不是固定滑动。

4. Transformer模型实现与训练细节

4.1 位置编码:时间序列版Positional Encoding

时间序列预测中,位置编码是最容易忽略但影响最大的设计之一。NLP里的sinusoidal位置编码针对的是离散token的相对位置,而时间序列的输入每个位置本身已经带有时间信息(比如星期几、月份)。直接用正弦余弦编码,模型会很难区分“同一个星期几在不同历史阶段”的差异。

我采用的方法是构造一个时间戳特征矩阵:对输入窗口内的每个时间点,生成一组特征表示——星期几(one-hot)、月份(one-hot)、年份趋势(归一化的日期偏移量)、节假日前后天数。这些特征和销量特征拼接后一起过embedding层。等价于让模型自己学习“不同时间位置应该赋予怎样的先验权重”,比固定编码灵活得多。

如果你仍然想使用类sinusoidal位置编码,也可以,但建议在编码中叠加一个“距离当前预测原点的时间差”特征。比如窗口内第i天距预测起始日的天数为(90 - i),将其归一化后拼接到输入里。这个特征能让注意力机制感知到“窗口内哪些位置距离预测更近”,在实际项目中效果提升明显。

4.2 Encoder层配置与注意力细节

我的Encoder配置如下(参考了常见时序Transformer的设置并做了调优):

参数数值说明
输入序列长度909天×10周,覆盖季节周期
特征维度64embedding输出的维度
Encoder层数4超过4层在M5上收益不明显
注意力头数8多头注意力让模型同时关注不同模式
前馈网络隐藏维度128比特征维度大一倍即可
Dropout0.1防过拟合,实际试验0.1效果较佳

注意力计算的核心公式就是标准的softmax(QK^T/√d)V。在M5的实际数据上我观察到,注意力权重的分布会自发性地向“去年同期附近”和“最近几周”聚集,这验证了Transformer能自动学到周期性依赖。但也正因为如此,如果输入窗口不够长,比如只给28天,注意力机制很容易把权重全压到最近几天上,忽略更早的周期模式。

预测头的设计上,我尝试过两种:一是取Encoder最后一个位置的输出接全连接层;二是对所有位置输出做全局平均池化再接全连接。实测全局平均池化效果更好,因为M5的序列较短,平均池化能平滑掉局部噪声。

4.3 训练细节:损失函数、优化器与batch策略

损失函数我建议用MSE,虽然比赛指标是RMSSE,但RMSSE的分母是常数(训练集相邻差均值),在训练时可以作为常数缩放因子,等价于用带权MSE。如果直接优化RMSSE反而更复杂。也可以用Huber Loss降低离群值影响,我测试下来MSE和Huber在M5上差异不大,MSE收敛略快。

优化器用AdamW,学习率建议设1e-3,并配合余弦退火调度器。batch size从64到256都可以,取决于显存。我之前用的RTX 3090,batch size设为128时训练一轮(32万样本)大约耗时3分钟,共训练20个epoch。

有一点值得单独提出:M5的序列长度不一,但切窗后所有样本长度一致,不需要padding。这让Transformer的训练特别干净,没有NLP里常见的pad_mask等额外处理。

4.4 WRMSSE的Python实现

计算WRMSSE时需要先按层级聚合预测值,再逐层计算RMSSE并加权。核心代码如下:

import numpy as np import pandas as pd def rmsse(y_true, y_pred, y_train): # y_true, y_pred: (n_series, horizon) # y_train: 训练段相邻差均值,作为缩放因子,shape (n_series,) numerator = np.mean((y_true - y_pred) ** 2, axis=1) denominator = np.mean(y_train ** 2, axis=1) # 已提前算好的相邻差 return np.sqrt(numerator / denominator) def weighted_rmsse(y_true_dict, y_pred_dict, train_scale, weights): """ y_true_dict, y_pred_dict: 每个层级的键如 'all', 'state_CA', 'store_1' 等 weights: 每个层级对应的权重 """ total = 0.0 for key in y_true_dict: r = rmsse(y_true_dict[key], y_pred_dict[key], train_scale[key]) total += weights.get(key, 1.0) * r return total

这里train_scale需要在训练时按层级提前算好,是每条序列训练段相邻销量差的均值。计算时要特别注意:相邻差必须使用“预测截止日之前”的数据,不能用整条序列的全部历史,否则验证指标的参考意义会打折扣。

5. 常见问题与排查技巧实录

5.1 预测值整体偏低,尤其零销量序列

M5里大量冷门商品长期销量为零,模型在训练时看到这些全零序列,会倾向把所有序列的预测值都压低,导致热门商品也预测不足。我的解决方法是给零销量序列做特殊处理:训练时对这些序列的损失函数乘以一个较低的权重(比如0.5),让模型把更多注意力放在有销售模式的序列上。同时,在特征中加入“历史零销量占比”,帮助模型区分冷门和热门商品。

如果预测结构中仍然出现全局负值(因为用了线性输出层),需要做clip到0的处理。但要注意:不能全局clip,而应按层级聚合后再clip,避免“底层为负、上层为正”的矛盾。

5.2 显存不足或训练速度过慢

Transformer的注意力计算在序列长度较长时显存占用很大。如果你把窗口长度设为180天或更大,显存会迅速吃紧。我遇到过batch size设为256时直接OOM的情况。排查思路有三个:降低batch size、减少注意力头数或特征维度、使用梯度累积。实际中,我把batch size降到64,配合8步梯度累积,等效batch size仍为512,且显存占用大幅下降。

另一个容易被忽略的问题:DataLoader里的num_workers对训练速度影响巨大。M5的样本构造涉及大量pandas操作,把num_workers从2提高到8后,数据加载不再是训练瓶颈。

5.3 验证集效果不错,但本地测试集却崩了

这是典型的层级一致性问题的表现。M5的评估要求各个层级都计算RMSSE,如果你只在单序列层级上优化模型,那么聚合到商店、州层级时,误差会被放大。为缓解这个问题,我引入了层级一致性损失(hierarchical consistency loss):在训练时把同一batch内所有样本按州做聚合,计算聚合后的预测与真实聚合值之间的MSE,加到总损失里。这是我在项目中后期加入的一个改进项,对WRMSSE的降低有明显帮助。

如果不想改损失函数,也可以在后处理阶段做层级调和(reconciliation):用简单的线性回归把底层预测值调整到与上层聚合值一致。M5官方提供了hts包的R代码,可以参考它的调和逻辑移植到Python。

5.4 训练不收敛或loss剧烈波动

如果出现loss在某个数值附近反复震荡,优先检查学习率。Transformer对学习率比较敏感,太高了容易震荡,太低了收敛极慢。我建议先用一个较小的batch(64)跑5个epoch做学习率扫描,找到合适的峰值学习率再全量训练。

另一个常见问题是输入特征的尺度不平衡。价格动辄几十,销量可能只有0到二位数,两者拼接后如果没有做标准化,注意力机制会被价格维度主导。务必对所有连续特征做标准化,或至少保证每个特征维度均值接近0、方差接近1。

6. 总结与实操扩展建议

最后分享一点我在这个项目里沉淀下来的实操体会,也可以当作后续扩展方向。

第一,Transformer用在M5这类数据上,核心收益是它能捕捉长周期依赖,但前提是输入窗口足够长、特征工程足够细。如果你只是简单地把销量塞进模型,效果很可能打不过调好参的LightGBM。我在项目初期就用LightGBM跑过一个baseline,WRMSSE大概在0.68左右,而第一版Transformer只有0.72,直到加上价格特征、节假日前后天数和层级一致性损失之后才反超到0.60附近。这说明特征工程和损失函数的设计,比模型结构本身对最终结果的影响更大。

第二,M5预测的未来可以沿着两个方向扩展:一是用Informer、Autoformer这些针对长序列优化的Transformer变体,它们对M5这种长度和规模的序列可能有更好的效率表现;二是引入外部数据,比如天气、宏观经济指标,甚至社交媒体热度,进一步丰富特征体系。不过M5本身的数据已经足够支撑一个有说服力的实验,先跑通标准Transformer再逐步改进,是更稳妥的路径。

第三,不要迷信公开的baseline代码。M5有很多开源实现,但不少存在数据泄漏或评估指标计算错误的问题。我在项目中踩过“验证集指标虚高但提交后下滑”的坑,后来逐个排查消除泄漏才恢复正常。做时序预测,每一步都要问自己:如果现在是真实业务场景,我在预测时刻能拿到哪些信息?把这些信息对齐到训练流程里,模型才有真正的实用价值。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 3:21:11

利用FM子载波与三极管混频器实现137MHz单边带发射

在业余无线电和射频实验领域,如何利用手头常见的低成本元件实现高质量的调制信号发射,一直是许多爱好者和学生研究者探索的课题。传统的单边带(SSB)信号生成通常依赖昂贵的晶体滤波器或复杂的数字信号处理(DSP&#xf…

作者头像 李华
网站建设 2026/9/2 3:20:08

解锁DJI Mino隐藏价值:从设备激活器到移动创作效率中枢

大疆的 DJI Mino 应用,你真的用对了吗?或者说,你真的“用”了吗?如果你是一位大疆手持云台(如 Osmo Mobile、OM 系列)的用户,那么“DJI Mino”这个名字你一定不陌生。它是官方配套的、功能最强大…

作者头像 李华
网站建设 2026/9/2 3:20:01

多源位置信号如何交叉关联?FastAPI实现位置情报聚合服务

看到“蝙蝠侠的宿敌总能找到他”这个标题,先别急着联想到哥谭市的剧情。把它换成技术语言,其实是安全分析里非常典型的问题:攻击者为什么总能锁定目标位置?答案通常不是某个反派拥有特殊能力,而是目标的数字身份在不同…

作者头像 李华
网站建设 2026/9/2 3:19:45

BQ79616与BQ79600菊花链通信底层驱动设计与实现

简介:面向BMS电芯电压采集场景,这份资源围绕TI的BQ79616与BQ79600两款电池监控芯片,提供了可直接参考的底层驱动程序源码。驱动覆盖I2C/SPI接口初始化、电压与温度数据读取、菊花链通信管理、中断处理、均衡控制及故障诊断等关键环节&#xf…

作者头像 李华
网站建设 2026/9/2 3:17:55

Python Tkinter实战:从零构建桌面计算器应用

你是不是被“tk”这个词搞得有点懵?搜了一圈,发现它既能出现在 Python 教程里,又能变成一个“星球”的名字,甚至还有专门的讨论网址。作为一个技术写作者,我可以直接告诉你一个判断: 在这个上下文里&#…

作者头像 李华