简介:这份资源围绕M5比赛数据集,完整演示了如何利用Transformer架构进行多维时间序列预测,适合具备Python与深度学习基础的中高级开发者、竞赛选手及时间序列研究者。压缩包共29个文件,大小约47.75MB,以Python源码、编译后的pyc文件、XML配置、Markdown说明和模型权重文件为主;其中12个py脚本覆盖序列处理、价格预处理、Encoder/Decoder实现、训练与预测等核心模块,目录划分清晰,还提供了checkpoint与损失曲线图,可辅助理解模型效果,pyc文件便于直接复用,XML配置有利于IDE项目导入。目前已有192人学习,资源内包含可直接运行的工程源代码、训练配置、README说明及预处理脚本,便于对照理论完成复现。通过该项目可以掌握Transformer在销售预测中的落地流程,包括多维序列展开、时间戳编码、自注意力机制应用、损失函数与优化器选择、批量并行预测等关键思路。 M5时序预测这个项目,我用Transformer架构重新做了一遍,把M5比赛数据集从数据处理到模型推理完整跑通。如果你正打算用Transformer做销量预测或类似的多序列时序任务,这篇文章里包含了我踩过的坑、调参的细节和一些不太容易从论文里直接看出来的实践经验,应该能帮你省不少时间。
M5比赛是沃尔玛零售数据集的预测任务,核心是预测30490个商品序列未来28天的销量。这个数据集有个特点:序列多、层级多、外部特征多,传统的ARIMA、Prophet这类模型根本扛不住这么大的规模,而LSTM虽然能处理序列,但训练速度慢且难以捕捉多个序列之间的横向关系。Transformer架构恰好解决了这些问题:多头注意力机制能同时关注所有时间步,模型可以并行训练,还能通过共享嵌入层让不同商品序列互相“借鉴”信息。这篇文章我会从数据构造、模型设计、训练评估到常见坑位,全程还原我的实现过程。
1. M5比赛与Transformer的组合逻辑
1.1 M5数据集到底难在哪里
先聊聊M5这个数据集的真实难度,很多人低估了它。表面上看就是一个销量预测问题,但实际操作起来会发现三个特别棘手的地方。
第一个难点是序列数量巨大。数据里有30490个商品序列,每个序列对应一个具体的商品在某家店的销售记录。这意味着你的模型必须同时处理三万个相互独立但有相似模式的序列。如果给每个序列单独训练一个模型,先不说计算资源,光是数据量就不够用——很多商品的日销量是零,单独建模基本没法学到有效信息。
第二个难点是层级聚合评估。M5的官方评估指标WRMSSE不是只看单一序列的预测误差,而是要求模型在12个不同的聚合层级上都表现良好。什么叫聚合层级?就是把商品按部门、品类、门店、州等维度汇总。比如你预测了每个商品的销量,但这些商品汇总到某个品类后,误差会被再次计算。这就意味着模型不能只对单个序列精确,还必须保证聚合后的总量也准确。实际训练中你会发现,有些模型单序列指标不错,一算WRMSSE就崩了,就是因为聚合层级上偏差累积严重。
第三个难点是外部特征的复杂性。M5数据里包含了价格、促销、节假日、SNAP(食品补助计划)等多个外部变量。这些特征和销量的关系不是线性的,比如促销可能让销量翻倍,但不同商品对促销的敏感度差异极大。Transformer的优势在于,它可以通过注意力机制自动学习外部特征与历史销量之间的复杂交互,而不需要像传统模型那样手动设计一大堆交叉特征。
1.2 为什么最终选了Transformer而不是LSTM
我在做这个项目之前,先用了LSTM做了一版基线,效果确实够用,但有几个问题让我最终下定决心换到Transformer。
第一个问题是LSTM的长期依赖能力有限。M5数据集的时间跨度是1941天,训练时窗口通常取100到200天。LSTM在这么长的序列上,早期信息很难传递到最后,梯度消失问题虽然通过门控机制有所缓解,但本质上还是按时间步逐个处理的,长距离信息衰减是必然的。而Transformer的注意力机制允许任意两个时间步直接交互,第1天的信息可以直接影响第100天的预测,中间没有衰减。
第二个问题是训练效率。LSTM必须按时间顺序逐个计算,无法并行。在30490个序列上训练,一个epoch耗时很长。Transformer是全序列并行计算的,在GPU上的利用率高得多,同样的数据,我实测训练时间能缩短5到8倍。
第三个原因是跨序列信息共享的能力。LSTM处理M5这种多序列任务时,通常的做法是把所有序列拼成一个batch,但模型内部是各算各的,序列之间的共性只能通过共享权重间接学习。Transformer则可以通过类似“全局注意力”的机制,让一个商品序列在计算时参考其他序列的统计特征。不过这需要做一些设计,我在第3章会详细说。
2. 数据准备:把零售时序变成Transformer能吃的样本
2.1 数据结构的理解与预处理要点
M5数据集分三张表:日历表、价格表和销售表。日历表包含了每一天的日期类型(普通日、宗教节日、文化节日)、SNAP标记、事件名;价格表是每个商品在每个店每天的价格和促销标记;销售表则是每个商品在每个店从2011-01-29到2016-04-24的每日销量。
第一步先处理缺失值。M5的销售数据在2016年之后有28天的预测期(test set),这部分没有真实销量,需要预测,所以不存在缺失填充的问题。但价格表和日历表有一些需要注意的地方:价格表中,促销标记对应的价格如果缺失,我会用前一天的价格填充,实操中用ffill(前向填充)就够了,不要用均值填充,因为价格是连续变量,均值会引入未来信息,造成数据泄漏。
第二步是数据格式转换。每个商品的销售记录是一个(30490, 1941)的矩阵,但Transformer需要的是(序列长度, 特征维度)的形式。我先把销售数据按“商品-门店”组合进行分组,然后对每个组合生成二维的特征矩阵。这里要注意内存问题,30490个序列全量加载,float32格式大概占2.2GB,还是可以接受的,但如果用float64就会非常吃力,建议统一转成float32。
第三步是特征工程。M5数据集提供的特征并不多,除了销量和价格,主要就是时间特征(星期几、月份、节假日标记等)和SNAP标记。我在实操中额外构造了几个特征:销量的一阶差分、7日移动平均、滞后7天/14天/28天的销量。这些滞后特征对Transformer学习周期性非常关键。此外,我还对价格做了处理:价格不是直接用绝对值,而是用“促销标记”二值特征加上“促销力度”特征(正常价格减去当前价格后的差值),这样比单纯喂价格数值更好学。
2.2 构造训练样本:滑动窗口与批量生成
Transformer训练需要的是固定长度的输入序列和对应的目标序列。我的做法如下:
def create_windows(data, input_len=100, output_len=28): windows = [] for i in range(len(data) - input_len - output_len + 1): x = data[i:i + input_len] y = data[i + input_len:i + input_len + output_len] windows.append((x, y)) return windowsinput_len我设为100,output_len是28(M5要求预测未来28天)。滑动窗口的步长设为1还是更大,影响数据量和训练时间。我最初用步长1,生成的数据量巨大,训练太慢;后来改成步长7,既保留了足够的样本多样性,又显著减少了重复计算。不过要注意,M5的最终任务是预测最后1941天之后的28天,所以在验证时,滑动窗口的区间划分要严格保证训练集和验证集不重叠,否则会引入泄漏。
这里有个容易被忽略的问题:M5有30490个序列,每个序列生成几万个窗口,全量加载到内存训练不现实。我采用了在线生成的方式,每次epoch随机抽样一部分窗口。PyTorch里用Dataset重写一个M5Dataset,在__getitem__里实时切片,配合DataLoader的num_workers多进程加载,实测效率很高,内存占用也控制得住。
2.3 静态特征与动态特征如何融合
M5的每个序列除了动态变化的销量和价格,还有静态属性:商品属于哪个部门、哪个品类、在哪个店、哪个州卖、有没有SNAP资格。这些静态特征如果直接丢掉,等于放弃了大量信息——不同品类的商品销量模式差异巨大,比如食品和电子产品完全是两个世界。
我的做法是把静态特征做embedding,然后在Transformer的输入层与动态特征拼接。具体来说:
# 动态特征: [batch, seq_len, dynamic_dim] # 静态特征经过embedding后: [batch, static_emb_dim] # 将static_emb扩展到每个时间步,再与动态特征拼接 static_emb = static_embedding(categorical_features) # [batch, static_emb_dim] static_emb = static_emb.unsqueeze(1).repeat(1, seq_len, 1) model_input = torch.cat([dynamic_features, static_emb], dim=-1)这样每个时间步的输入都携带了该序列的静态身份信息。实际操作中,商品ID、部门ID、门店ID这些类别特征做embedding时,维度不用太大,8到16维足够了,太大反而容易过拟合。还要注意一点:embedding层的参数更新速度和Transformer主体不同,最好用较小的学习率或者单独设置参数组,不然训练后期容易震荡。
3. Transformer模型核心环节实现
3.1 位置编码对时序预测的意义
Transformer本身没有顺序感知能力,所以位置编码是必备组件。在时序预测里,这一步尤其重要,因为销量数据的顺序直接决定了趋势和周期性。M5数据里,每周有销售周期,每年有季节性波动,位置编码必须能让模型感知到这种节奏。
我一开始用的是标准正弦位置编码,效果中规中矩。后来换成了可学习的位置编码,也就是把位置索引直接映射到一个可训练的embedding。实测在M5上,可学习位置编码比正弦编码的WRMSSE能低0.3到0.5个百分点,提升不算巨大但很稳定。原因可能是M5的周期性比较固定,通过训练学出来的位置表征比正弦函数更贴合实际数据的周期模式。
还有一个技巧:位置编码的维度一般维持和模型维度一致,但如果模型维度很大(比如512),位置编码参数量也会很大。我最终用的模型维度是128,位置编码在128维空间里完全够用。如果后续你想扩大模型,可以考虑把位置编码维度缩小再投影,这样能省一些显存。
3.2 多头注意力在销量预测里到底在学什么
多头注意力是Transformer的灵魂。对时序预测来说,它的意义在于:模型能自动找到每个时间步应该关注的历史时间点。比如预测今天的销量时,模型可能重点看昨天、上周同一天、去年同一天,这种“按需关注”的能力远比LSTM按顺序记忆更灵活。
我用的是4头注意力,每头64维。为什么不加更多头?因为M5的序列长度只有100,头数太多会导致每头能看到的信息过少,反而影响表达能力。你可以这样理解:多头注意力就像开会时多个专家同时发言,专家太多,每个专家的发言时间就短了,信息碎片化。4到8头在序列长度100到200的场景下是比较合理的区间。
在具体实现上,我用的是PyTorch自带的nn.TransformerEncoderLayer,内部封装了自注意力和前馈网络。这里有三个细节需要注意:
- dropout设成0.1到0.2之间。M5数据量大但噪声也不小,dropout太小容易过拟合,太大又会让模型训练变慢。
- 前馈网络的隐藏层维度设为模型维度的4倍(128 -> 512),这是Transformer的默认设计,通常不需要额外改动。
- 层归一化的位置在多头注意力和前馈之后都要加。我遇到过一个坑:只有一层归一化的话,训练后期loss会出现间歇性跳变,加了第二层归一化后稳定了很多。
3.3 预测输出:直接回归还是自回归
M5要求预测未来28天,这里有两种策略:直接回归和自回归预测。直接回归是让模型一次输出28个值;自回归是模型一次输出1个值,然后把这个值作为输入继续预测下一步。
两种我都试过,说下实际差异。直接回归有个天然的优势:训练和推理速度快,一次前向就能得到28天预测。但它有个问题,28天的预测是独立的,模型无法显式捕捉这28天之间的内部依赖。实际表现就是预测曲线在前几天比较准,越往后越平,因为模型在预测第28天时没有用到第7天的中间结果,缺少序列内部的连贯性。
自回归则更符合时间序列的自然逻辑。每一步的预测都基于上一步的结果,序列内部的依赖关系被建模了。但缺点也很明显:训练时用真实值做输入(teacher forcing),推理时用预测值做输入,不一致会导致误差累积。我用自回归方案时,加了5%的teacher forcing概率(推理时随机用真实值或预测值),误差累积问题缓解了不少,最终WRMSSE比直接回归低了大约1.2。
我的最终方案是:编码器用TransformerEncoder处理历史100天,解码器用一个简单的线性层逐步预测28天,中间加了一个自回归机制。这个方案的结构比较轻量,训练速度和精确度比较平衡。
4. 训练策略与评估指标
4.1 损失函数与WRMSSE的匹配问题
M5官方评估指标WRMSSE(加权缩放均方误差)不是普通的MSE。它的计算方式是:先对每个商品序列单独计算均方误差,再除以该序列训练集的均方误差做缩放,最后按照各层级的销售额加权汇总。
这里有一个关键点:如果你直接用MSE作为损失函数训练模型,单序列误差大、销售规模大的商品会主导梯度。但WRMSSE是每个序列先归一化再加权,低销量序列的误差同样重要。所以我最终没有用MSE,而是用“缩放均方误差”作为训练损失,也就是每个序列的MSE除以该序列训练集的方差。这个改动很直观地反映到最后的WRMSSE上,大概能降低0.5到0.8分。
此外,WRMSSE的加权系数是销售额,也就是说销量大的商品对最终得分影响更大。我在训练时给每个样本也加上了对应的销售额权重,让模型优先学好那些重要序列。这个方法在比赛后期帮我把分数提升了不少。
4.2 超参数配置与调参实录
我最终的模型结构是一个2层TransformerEncoder加一个ReLU的MLP解码头。embedding维度128,前馈网络512。训练使用AdamW优化器,初始学习率3e-4,batch_size是256。学习率调度用的是CosineAnnealingLR,总共训练了60个epoch。
这里说下为什么不是层级越多越好。我最初试过4层TransformerEncoder,训练时间翻倍,但验证集效果并没有提升。原因是M5的序列长度只有100,信息量有限,太深的网络会把噪声也学进去。2层在这个数据规模上是一个比较理想的深度,如果你的序列长度很长(比如500以上),再考虑加深层次。
另一个值得说的经验是gradient clipping。时序预测的loss偶尔会出现一个特别大的尖峰,不处理的话梯度会爆炸。我设置了max_grad_norm=1.0,训练过程立刻就稳定了。这个参数千万别省,我见过很多人因为没做梯度裁剪,训练到一半loss变NaN,折腾半天才找到原因。
还有一个细节是EMA(指数移动平均)的使用。我用权重EMA保留了训练过程中模型参数的移动平均版本,这样在推理时比直接用最后一轮参数更稳。操作很简单,但效果非常好,WRMSSE能下降0.3左右。如果你还没用过,建议试一下。
5. 常见问题与排坑经验
5.1 训练速度慢的优化方案
在30490个序列上训练Transformer,如果不做优化,一个epoch可能需要半小时以上。我从三个方面做了优化,最终训练时间缩短了接近70%。
第一是DataLoader加速。数据加载要用num_workers开启多进程,同时pin_memory设为True。如果数据在内存里,这两个参数能明显减少GPU等待的时间。第二是batch_size要足够大。Transformer在batch_size比较小的时候,GPU利用率很低,我实测M5数据上batch_size 256比64快了两倍多。第三是混合精度训练,用torch.cuda.amp自动混合精度,显存减少30%,训练速度提升40%。这几项加在一起,效果非常显著。
如果你的显存不够大,还可以把序列长度从100降到70左右。M5的销量数据虽然有长期趋势,但70天窗口的局部信息已经能覆盖主要的周期性,缩短序列长度虽然会损失一些精度,但优势是训练速度大幅提升。
5.2 过拟合的识别与处理技巧
M5的数据量虽然大,但单一商品序列的样本其实有限,如果特征工程做得过于复杂,模型很容易过拟合训练集,表现为训练loss持续下降但验证集WRMSSE不降反升。
我遇到过最典型的情况:加了商品ID的embedding之后,训练集分数非常漂亮,验证集却崩了。原因很简单,M5训练集和验证集的时间范围不重叠,某些商品在验证期出现了训练期从未见过的销售模式,模型学到的是“哪个商品”而不是“商品怎么销售”。这个问题的解决办法是少用或者不用商品ID这种唯一标识,而是改用商品所属的类别特征,让模型泛化到“像这样的商品”而不是“这个商品”。
另一个有效的手段是早停。看验证集WRMSSE,如果连续10个epoch没有提升就停止训练,同时恢复最优模型权重。我在60个epoch的训练中,通常在第35到45轮就early stopping了,再往下训练的效果不仅没有提升,还会逐步变差。
5.3 分数上不去的几个隐藏原因
如果你发现模型无论怎么调参,WRMSSE都停在某个水平上不去,多半是以下几个原因。
第一个是滞后特征没有做干净。我在第2章提到过,训练时用了7天、14天、28天的滞后销量作为特征,但推理时这个特征应该来自模型的预测值而不是真实值。如果推理代码里取的是真实历史数据(这在验证集上可行,但到了真正的测试集就失效了),模型就会被“喂答案”,验证集分数虚高,换到真实预测就崩。这个问题必须用自回归的方式逐步生成预测值再计算滞后特征。
第二个是聚合层级重视不够。WRMSSE计算12个层级的加权,如果你只盯着单序列的loss看,很容易忽略了聚合误差。我做过一个实验:模型在单序列指标上表现很不错,但WRMSSE很一般。后来我把训练样本按部门分组,在batch内部同时计算部门级loss和单序列loss,两者加权相加。这个改动让聚合层级的预测明显更准,WRMSSE也是肉眼可见地下降。
第三个是数据泄漏。M5的日历事件特征里,有一些事件只在某一天出现。如果你在构造特征时,把标签(未来28天销量)的统计值用于特征归一化,比如把整个序列的均值和标准差都计算出来后统一归一化,验证集分数会虚高。正确的做法是,特征归一化参数只从训练区间的数据计算,验证集和测试集使用同样的参数。
写在最后
做M5这个项目最大的收获,是我意识到Transformer用在时间序列预测上,核心价值不是“模型有多强大”,而是“它能让数据中的周期性、跨序列共性、外部因素影响这些信息被自动挖掘出来,不需要人工设计复杂的规则”。如果你刚接触这个方向,我的建议是不要急着上大模型,先把手上的数据吃透,把特征工程做好,再考虑模型结构。我把完整的训练配置和数据处理逻辑都整理在了项目里,遇到具体问题可以对照着排查。动手试几轮之后你会发现,这些细节才是真正决定分数高低的地方。
本文还有配套的精品资源,点击获取