资讯动态

Transformer预测M5销量:非自回归结构、数据构造与避坑指南

发布时间:2026/10/13 7:41:47 来源:尧图企业网站定制
简介面向时间序列竞赛与深度学习进阶者的 Python 实现方案完整展示了如何用 Transformer 架构处理 M5 比赛的多维销售数据。从数据预处理、序列构造、模型搭建到训练验证与批量预测均有覆盖适合希望掌握注意力机制在时间序列中应用的读者。压缩包共 29 个文件包含 12 个 Python 脚本、6 个 pyc 编译文件、4 个项目配置 xml、1 个模型权重文件、1 个说明文档及 loss 曲线图等整体约 47.75MB。代码按注意力层、编码器、解码器、辅助运算、训练流程、序列处理等模块拆分目录结构清晰便于按需查阅。已有 194 人学习下载。从这份资源中可获取完整赛题方案既有自行实现的 Transformer 层模块也有训练、测试、预测脚本与训练好的模型权重配合说明文档能快速复现实验同时可学习针对 M5 多序列销售数据的时间戳编码、多通道展开与批量预测等实用设计。1. 用Transformer预测M5销量先想清楚再动手M5比赛数据集在销量预测圈子里几乎是绕不开的试金石层级多、序列长、价格在评测期直接消失任何模型到了这里都会显形。我见过不少把Transformer搬上去的团队第一轮就吃了败仗误差不降反升最后灰溜溜退回树模型。问题从来不在架构而在方案——数据怎么聚合、目标怎么构造、预测怎么输出每一步都决定了Transformer是发挥并行注意力的优势还是变成一个昂贵的黑匣子。这篇笔记会把这套从数据到参数的路数完整拆开目标是让没接触过M5的人能照着复现让被“Transformer跑不起来”坑过的熟手知道问题出在哪。2. M5数据集结构为什么Transformer在这里既可能很强、也可能翻车2.1 M5的数据层级与评测逻辑M5数据集来自一个大型零售平台的真实销售记录时间跨度约五年覆盖三个区域、十个店铺、三千多个部门、三万多条单品序列。评测任务不是单序列预测而是层级预测给定历史销售数据要预测未来二十八天每一天的销量并且误差会在每个层级上分别计算再按销量占比加权汇总。这个设计决定了你不能只盯着单条序列看。顶层聚合序列相对平滑底层单品序列则充斥着零值、弱趋势和突发波动。数据文件通常拆成销售表、日历表、价格表三类销售表记录每一天每个单品的销量日历表提供日期属性和事件标记价格表则在训练期可见、在评测期完全缺失。这个价格缺口是M5最阴险的设计之一很多队伍训练时用着价格特征精度很高评测时一上线就崩。处理M5的第一步动作通常是合并三张表再按层级聚合。我一般会在原始粒度上把销售数据与日历、价格对齐然后分别生成总-区域-店铺-部门-单品五层聚合结果。每一层都保留完整的日期索引方便后续滑窗时按同一套时间轴切分。如果你只想快速验证模型跑三个区域汇总的顶层和中层数据就够了不必一上来就全量跑三万多条单品。2.2 Transformer的强项与M5的场景是否匹配Transformer的核心机制是自注意力它能把序列里任意两个位置直接建立关联长距离依赖的建模能力天然强于RNN。在M5这个场景里这个特性有时是优势有时是错觉。直观来说销售序列里的长期依赖往往表现为年度季节性二十八天的预测窗口并不算长LSTM理论上也能覆盖但Transformer的并行训练速度和灵活的多协变量融合能力是实打实的优势。真正让Transformer在M5上翻车的不是注意力机制本身而是对输入数据的处理方式。单品序列里大量零值和极端促销波动会扰乱注意力分配如果不做聚合增强或序列归一化模型学到的权重会偏向那些波动大的时间点。另一个常见误区是直接把三万多条序列丢进去训练注意力矩阵和批处理复杂度都会爆炸训练时间变得不可接受效果却未必优于按层级分组训练。我的经验结论是Transformer不是用来替换树模型而是用来替换LSTM的序列建模层。你仍然可以用特征工程和树模型做基线把Transformer方案当作一个能同时输出多个层级预测的统一模型。当你有足够多的历史序列、需要按外部事件对齐多变量信息时Transformer才真正值得投入。2.3 三种结构选型自回归、非自回归、混合用Transformer做销量预测结构上大致有三条路线。第一条是标准自回归Decoder结构把前一天的预测值当作下一天的输入逐位生成未来二十八天。这条路在长文本生成里很有效但在销量预测里是隐患每一轮预测都会累积误差早一步预测偏了后面的结果跟着整体漂移而且训练时要用Teacher Forcing推理时却面临完全不同的输入分布模型很容易在这两者之间找不到平衡点。第二条是我个人在M5上最常用、也最推荐的结构——非自回归输出。编码器读入历史九十天序列解码器不再逐位预测而是把未来二十八天对应的日期特征和事件标记作为输入一次性输出整段预测。这样做的好处是计算快、结构简单、不会误差累积同时因为解码器的输入是未来时间段的确定性特征而非模型预测值训练和推理的行为是一致的。解码器的输入可以理解为“告诉模型未来这段时间有哪些特殊日子”注意力机制负责在历史里找相似年份、相似事件对应的销售模式。第三条路线是把Transformer当作特征抽取器池化历史序列的隐状态再拼接外部特征交给树模型输出预测。这条路如果你已经在现有系统里重度依赖树模型迁移成本最低。常见做法是把Transformer编码器输出的最后一层向量当作序列摘要特征替代手工统计特征。缺点是推理链路变长调试起来麻烦一般适合已有成熟特征管线的团队。3. 数据预处理与滑窗构造把五年历史切成模型能吃的样本3.1 三张表的合并与层级聚合数据预处理是整个流程里最不性感但决定成败的一步。我通常会把销售表、日历表、价格表先合并成一张完整的长表再做层级聚合。下面这段代码描述的是合并和顶层聚合的常见做法。import pandas as pd sales pd.read_csv(sales_train.csv) # 日期 x 单品id的销售宽表 calendar pd.read_csv(calendar.csv) # 日期、事件、星期属性 prices pd.read_csv(sell_prices.csv) # 单品在不同周的售价 # 把宽表面转为长表一列是日期一列是商品一列是销量 sales_long sales.melt( id_vars[item_id, store_id], var_namedate, value_namesales ) # 合并日期属性和价格注意价格用前向填充补齐每日价格 sales_long sales_long.merge(calendar, ondate, howleft) sales_long sales_long.merge(prices, on[item_id, store_id, wm_yr_wk], howleft) sales_long[price] sales_long.groupby([item_id, store_id])[price].ffill() # 聚合到区域层级方便后续快速验证 agg_state sales_long.groupby([date, state_id]).agg( sales_sum(sales, sum), price_avg(price, mean), event1(event_type_1, first), ).reset_index()这段代码有几个关键点。宽表转长表是第一步不要跳过因为Transformer的输入是按时间序列组织的不是按商品矩阵组织的。价格列必须做前向填充因为原始价格表是周粒度记录而销售是日粒度直接合并会产生大量空值。聚合到区域层级只是为了快速跑通流程正式训练时你可以在部门或店铺层级重复同样的逻辑。这里还有一个容易犯的错误如果把价格列直接放进模型训练时表现很好评测时会因为价格缺失而报错或产生NA。常见处理策略是只保留价格的统计特征比如过去四周平均价格、相对平均价格的折扣率而不是把原始价格序列当作逐日输入。折扣率在未来会缺失但可以用近期均值填充。3.2 滑窗样本构造输入九十天、输出二十八天Transformer训练需要固定长度的输入输出对。M5的预测目标是未来二十八天所以输出长度固定为28输入长度我通常选90天或56天太短学不到季节模式太长干扰噪声增多。滑窗步长也要注意训练时步长可以大一些验证时步长必须等于预测周期长度否则验证集的构造会和真实评测不一致。def create_windows(series, input_len90, output_len28, stride7): windows_x [] windows_y [] length len(series) for start in range(0, length - input_len - output_len 1, stride): end start input_len x series[start:end] y series[end:end output_len] windows_x.append(x) windows_y.append(y) return windows_x, windows_y这个函数看起来简单但参数选择有讲究。训练阶段stride可以设为7或14这样能把五年数据切出足够的样本量又不让相邻样本高度相关。验证阶段stride必须设为28让窗口恰好从训练期结束位置滑到验证期开始位置。一个典型的坑是训练用stride1生成大量重叠样本模型反而过拟合到相邻窗口的细微差别上泛化能力下降。如果你要预测多个层级建议每一层独立调用这个函数并把同一时间段的样本对齐保存方便后续做层级一致性后处理。样本数量可以直接估算五年大约1800天扣除输入和输出长度后stride7大约能生成两百多个样本单一层级训练数据够用底层单品建议先聚合到部门层级再跑。3.3 归一化与特征编码别让价格和销量打架Transformer的注意力机制对输入尺度非常敏感。销量序列动辄上千事件标记是0/1如果直接拼接注意力权重会被数值大的维度主导。我一般对连续特征做标准化对类别特征做嵌入或简单one-hot。这里给出一个常用归一化策略它和图像领域的batch norm不同是针对每条序列独立统计的。def normalize_series(series): mean series.mean() std series.std() 1e-6 return (series - mean) / std, mean, std这条序列级归一化是Transformer时间序列预测里最核心的trick之一。它消掉了不同商品之间的销量尺度差异让模型学到的是“形状”而不是“绝对值”。注意统计量必须在训练窗口上计算然后把同样的均值标准差应用到后续预测结果的反归一化上。如果错误地整条序列统一计算均值会造成数据泄漏验证集上看到非常漂亮的分数真实评测却惨不忍睹。特征编码我一般会准备四类日期属性星期、月份、年事件标记事件类型、事件距今天数价格相关特征过去四周均价、折扣率以及历史统计滞后7/14/28天销量。事件标记对未来时间点是已知的因此可以安全地用在解码器端输入里。这是Transformer相对树模型在处理未来特征上非常大的一个便利。4. 模型实现搭一个轻量级Transformer预测器4.1 输入映射与位置编码Transformer本身没有序列顺序概念必须把位置信息编码进输入。M5这里的序列是固定九十天的滑窗我建议位置编码的最大长度设置成训练时窗口长度即可不需要按整段五年历史编码。位置编码可以选择固定正弦余弦函数也可以选择可学习位置嵌入下面用固定位置编码展示实现最简单。import torch import torch.nn as nn import math class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len512): super().__init__() pe torch.zeros(max_len, d_model) position torch.arange(0, max_len).unsqueeze(1).float() div_term torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(1000.0) / d_model)) pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) self.register_buffer(pe, pe) def forward(self, x): return x self.pe[:x.size(1), :]固定位置编码的好处是推理时长度超出训练长度也不会报错但M5场景里不需要超出长度。真正要注意的是编码器的位置编码和未来二十八天的解码器位置编码不能共用一个从头开始的索引而应该是历史窗口末尾的延续。我通常直接把解码器位置偏移设定为输入窗口长度模型才能理解“未来”是接在“过去”之后的。4.2 Encoder-Decoder主体结构M5场景不需要特别深的Transformer。层数超过四层之后收益递减反而容易过拟合到训练期的促销波动。我一般用两层Encoder加一层Decoder隐藏维度128注意力头数4这组参数在M5上训练速度和精度比较平衡。class M5Transformer(nn.Module): def __init__(self, input_dim, d_model128, nhead4, num_encoder2, num_decoder1, output_len28): super().__init__() self.input_proj nn.Linear(input_dim, d_model) self.pos_enc PositionalEncoding(d_model, max_len256) encoder_layer nn.TransformerEncoderLayer(d_model, nhead, dim_feedforward256, dropout0.1, batch_firstTrue) self.encoder nn.TransformerEncoder(encoder_layer, num_encoder) decoder_layer nn.TransformerDecoderLayer(d_model, nhead, dim_feedforward256, dropout0.1, batch_firstTrue) self.decoder nn.TransformerDecoder(decoder_layer, num_decoder) self.output_proj nn.Linear(d_model, output_len) def forward(self, src, tgt_feats): src_emb self.input_proj(src) src_emb self.pos_enc(src_emb) tgt_emb self.input_proj(tgt_feats) tgt_emb self.pos_enc(tgt_emb) memory self.encoder(src_emb) out self.decoder(tgt_emb, memory) return self.output_proj(out)这段代码里最容易被忽略的设计是tgt_feats。它并不是模型自己生成的前一个预测值而是未来二十八天的外部特征包括日期属性和事件标记。模型通过解码器交叉注意力从历史记忆中筛选与未来时间段相似的销售模式然后一次性输出预测。我前面强调过这种非自回归设计是M5比赛方案里被验证最稳定的做法训练并行度也高。dim_feedforward设为256不需要更大。输出层的output_len28意味着直接从每个token输出一个标量但这和未来时间点是一一对应的可以理解为解码器输出的每一个位置对应未来第几天。4.3 损失函数、Mask与输出形状M5的销量数据长尾分布严重直接回归原始销量会导致模型把所有精力放在高销量商品上中低销量序列被完全忽略。常见做法是用log1p变换压缩尺度或使用分位数损失直接建模分布。我推荐在训练时用分位数损失它不仅更贴近M5的评估方式还能顺带获得预测区间。def quantile_loss(pred, target, tau0.5): diff target - pred loss torch.maximum(tau * diff, (tau - 1) * diff) return loss.mean()tau0.5时这个损失等价于平均绝对误差相对MSE更抗异常值。如果你想要更好效果可以同时输出三个分位点tau分别取0.5、0.1、0.9用三个并行的输出头实现最终提交时用0.5分位作为点预测。batch内如果使用了Mask要特别注意Transformer的两个MaskEncoder的padding mask因为同一batch内不同序列长度一致时可以不设Decoder的attention mask非自回归结构不需要因果mask。很多实现直接套用文本Transformer的因果mask把解码器每个位置限制成只能看前面的位置这反而打破了“一次性输出28天”的设计初衷。5. 训练配置与避坑参数怎么设、哪里容易翻车5.1 理解WRMSSE评估指标M5竞赛的官方指标是WRMSSE全称加权均方根标度误差。它的计算逻辑是对每个层级、每个序列把预测误差除以一个基准误差这个基准误差是历史上最后一天作为naive预测的误差。得到的比值再按层级权重加权。简单理解就是你的误差比“拿昨天销量当明天预测”这个最笨方法好多少然后按层级重要性汇总。这个指标有一个直接后果低销量序列容易把比值放大因为它们的历史波动小任何偏差都会被标度放大。所以训练时别把loss直接设为MSE或MAE可以按序列销量做权重归一化让每条序列在平均意义上贡献相同。我自己做的时候用每个序列训练窗口内的平均销量作为缩放因子效果直观地优于不缩放。验证集构造逻辑是把训练数据的最后二十八天当作验证期前面的历史用于训练。滑窗函数中stride在验证时设为28就是为了保证验证样本的时间范围与官方评测一致。如果你用stride1切割验证样本验证分数会虚高因为相邻窗口几乎重叠。5.2 训练超参数学习率、warmup、早停训练超参数直接影响最终分数。经过多次实验我一般按下面这组参数起步再根据验证集微调。参数推荐值说明learning rate3e-4AdamW默认配置warmup步数2000避免前期不稳定batch size32按GPU显存调整dropout0.1对M5不会欠拟合早停patience8个epoch防止过拟合最大epoch50一般10-20收敛学习率warmup在Transformer里不是玄学是保证训练稳定的必要条件。没有warmup时注意力层的梯度在初期波动极大容易把模型推向坏的局部最优。warmup步数2000听起来多但配合batch size 32也就几十个step的事代价很小。损失函数选择上面提过分位数损失。训练时把验证集WRMSSE作为早停依据每轮结束计算一次。如果模型在验证集上连续多个轮次没有改善回退到历史最佳参数再评估然后结束训练。5.3 五个踩坑记录现象、原因、解决第一个坑是价格特征在评测集上直接失效。现象是本地验证表现优秀提交到评测后误差飙升。原因是价格列在评测期为缺失值训练用的特征分布和测试完全不匹配。解决方案是只保留价格统计特征如过去四周均价和折扣率同时在评测期用训练末期均值填充。第二个坑是序列级归一化计算泄漏。现象是验证指标好到不真实换了另一种切分方式后指标大幅恶化。原因是整条序列统一计算均值和方差包含了未来信息验证窗口的分布被未来数据污染。解决方案是把归一化的均值方差限定在训练窗口内计算验证时用同一统计量。第三个坑是解码器误用因果Mask。现象是模型训练的loss下降很慢而且预测结果呈现奇怪的滞后性预测值几乎等于最近几天的移动平均。原因是非自回归解码器被因果Mask限制每个未来位置只能看到未来的前几个位置失去了整体信息。解决方案是去掉解码器的因果Mask让每个位置能看到完整未来特征。第四个坑是验证集滑窗步长与预测周期不一致。现象是本地验证得分很高真实评测惨败。原因是验证时用stride1生成大量重叠窗口导致验证误差被重叠平滑。解决方案是训练stride7验证stride28保证验证窗口和真实评测窗口对齐。第五个坑是模型输出的预测值出现负数。现象是loss没异常但反归一化后预测销量大量小于零。原因是归一化后的序列经过了线性输出层模型并不天然知道销量非负。解决方案是在输出后加ReLU或Softplus激活或者直接对log1p后的目标做回归再反变换回原尺度。5.4 一个可度量的基线对照判断Transformer方案到底值不值的唯一办法是跑基线。我会在同样的数据窗口和特征下跑一组LightGBM、一组LSTM、一组本文的Transformer。轻量级实验可以在区域层级上进行每天跑一次生成未来二十八天预测。从我自己跑的结果来看区域层级和店铺层级上Transformer能明显优于LSTM与调优后的树模型持平部门层级以下Transformer和树模型互有胜负树模型在稀疏序列上略占优势。如果你在底层单品序列上强行上Transformer训练时间增加数倍指标却可能不如简单的线性外推。这类对比实验的最大价值是帮你确定模型投入的边界Transformer更适合作为统一预测框架而不是每个层级的最优单模型。后续如果你想追求最高精度一套Transformer加上树模型集成再配合层级调和会比单独压榨某个模型省力得多。6. 进阶操作从拿到baseline到稳态方案当基础模型能稳定跑通后下一步就不是继续堆模型层数而是围绕M5特有的几个问题做针对性修补。我的习惯是优先处理层级一致性问题因为它直接关系到最终加权指标的稳定性。用Transformer分别预测了顶层、区域层、店铺层之后你会发现各层预测的数字合不上所有区域预测值加起来和顶层预测值有明显出入。这个问题叫层级不一致。常见做法是保持底层预测不变自下而上逐层相加生成高层预测或者保持顶层预测不变把高层预测按底层占比拆分到下层。我通常选择后者因为顶层序列更平滑、预测更准作为总额约束后按比例分配到下层能显著压低高层的WRMSSE贡献。另外一个值得投入的方向是概率输出。M5数据集适合概率预测这不仅能得到更完整的业务信息在后续集成时也更有用。给模型加两个额外输出头分别预测0.1和0.9分位用分位数损失训练。集成时对多个分位求平均再排序通常能得到比单模型更稳定的中位数预测。集成策略上最简单有效的做法是多个不同种子的模型做均值。注意不要只对最终预测做均值更好的做法是对编码器输出的隐状态做平均再喂给解码器。前一种方式只会平滑噪声后一种方式能保留更完整的注意力信息。最后说一个我自己吃过亏的地方第一次完整跑通这个方案时我非常自信地提交了验证集分数最好的那份模型忽略了它对价格特征的依赖结果正式评测直接崩掉。从那以后我养成了一个习惯训练完第一件事不是看分数而是检查模型在特征缺失场景下的表现把价格列强行置空、把事件标记置空观察验证指标的波动幅度。如果波动大说明模型过度依赖某个过期特征趁早修正比事后后悔好得多。M5数据集值得做不是因为它的分数多有含金量而是它逼着你把数据、建模、评估、部署这一整条链路的边界摸清。希望这篇笔记能帮你少走一段弯路。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑