
简介一份基于Transformer的销售量预测完整Python项目面向有一定Python基础、希望用深度学习做时间序列单变量多步预测的开发者与数据分析师。项目基于PyTorch实现Transformer模型整体目录分为data、models、utils、graphs等模块包含数据预处理、模型定义、训练、评估与可视化脚本并配置.env文件便于直接修改参数后运行。压缩包共69个文件主要含6个Python脚本、1个csv数据文件、60个预测结果对比图表png以及环境说明文档包体约3.72MB结构清晰易于复用。graph_one_step与graph_multi_step两个目录分别展示不同步长下的预测曲线覆盖epoch 1至30的迭代效果方便直观对比模型收敛过程。目前已有330人学习下载适合入门Transformer预测、参考完整代码框架或在此基础上改造适配自己的业务数据。1. 用 Transformer 预测销售量为什么值得从 ARIMA 换过来做销售量预测的人大多经历过这样的场景历史数据里明明有稳定的季节性周期ARIMA 却只学到过去的均值把预测曲线拉成一条平线LSTM 能学出一些波动但预测一拉长就开始抖。这个方案的核心是用 Transformers 直接做销售量预测让模型通过注意力机制自己找出「今天和哪几天的销量最相关」而不是靠固定窗口假设。本文会给出可复现的 Python 完整代码走完数据准备、Transformer 模型搭建、训练和 Matplotlib 绘图全流程。适合手里有历史销售数据、想评估深度学习方案是否值得投入的从业者。参数怎么定、为什么这样定、坑在哪下文都会讲到。2. Transformer 预测销售量的建模思路注意力机制与数据管线2.1 为什么在销售预测上选 Transformer三个具体理由第一个理由是长期依赖。销量数据里有「大促之后连续几天低迷」「节前备货高峰」这类隔得很远的关联。LSTM 靠门控一点一点传递信息距离超过几十个时间步后信息衰减明显Transformer 的注意力直接计算任意两个时间步之间的相关性大促那天的特征可以在一层之内直接影响后几天的预测结果。第二个理由是并行训练。TransformerEncoder 在训练时不需要像 LSTM 那样按时间步展开窗口内的所有位置可以同时参与计算。数据量上来之后训练速度的优势是实打实的调参周期可以压得很短。第三个理由是注意力权重可解释。训练完成后把最后一层注意力权重取出来能看到预测某一天时模型主要参考了哪些历史日期。对业务方来说这个解释比「黑匣子」说服力强得多。当然 Transformer 不是万能的数据量很小、序列高度随机时它未必赢过 ETS 或 XGBoost。做选择前先想清楚如果历史销量本身没有可学习的规律换什么模型都救不了。2.2 数据准备模拟销售数据与按时间切分先用一份可复现的模拟销售数据把整套流程跑通。实际使用时把你的历史销售表导出来保证至少包含日期和销售量两列然后替换掉代码里的 df 即可。模拟数据包含线性趋势、30 天周期和随机噪声接近真实零售数据的基本形态。import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler # 模拟 800 天销售数据趋势 30 天周期 噪声 np.random.seed(42) days 800 t np.arange(days) sales 50 0.02 * t 10 * np.sin(2 * np.pi * t / 30) np.random.normal(0, 1.5, days) df pd.DataFrame({ date: pd.date_range(2022-01-01, periodsdays, freqD), sales: sales }) # 按时间顺序切分先切分再标准化 train_size int(len(df) * 0.8) train_df df.iloc[:train_size] test_df df.iloc[train_size:] scaler MinMaxScaler() train_scaled scaler.fit_transform(train_df[[sales]].values) test_scaled scaler.transform(test_df[[sales]].values)有一个新手很容易踩的坑先把整份数据拿来 fit scaler再切分训练测试集。这会带来数据泄漏测试集的 min/max 信息已经参与了训练数据的缩放回测结果虚高而真实未来数据根本不会包含这个信息。正确做法是严格按时间先切分scaler 只 fit 训练集再用同一个 scaler transform 测试集。注意这里没有做随机打乱时间序列按时间顺序切分是底线。2.3 滑动窗口切分seq_len 和 pred_len 怎么定Transformer 的输入是一段连续历史窗口。把历史销售量按固定长度切成「过去 seq_len 天」和「未来 pred_len 天」的样本对序列这就是有监督训练数据。用 create_sequences 函数生成样本时窗口沿时间轴滑动每个窗口步长默认 1 天。def create_sequences(data, seq_len, pred_len): X, y [], [] n len(data) for i in range(n - seq_len - pred_len 1): X.append(data[i:i seq_len]) # 过去 seq_len 天 y.append(data[i seq_len:i seq_len pred_len]) # 未来 pred_len 天 return np.array(X), np.array(y) seq_len 60 # 覆盖两个 30 天周期 pred_len 7 # 一次预测未来 7 天 X_train, y_train create_sequences(train_scaled, seq_len, pred_len) print(X_train.shape, y_train.shape) # (样本数, 60, 1) 与 (样本数, 7)seq_len 的取值至少应覆盖一个完整周期。数据有明显月度周期时取 60 到 90让模型能看到两个以上波峰波谷取太短模型学不到周期性取太长会引入无关历史噪声训练也更慢。pred_len 取决于业务决策节奏做周计划就取 7做月度计划就取 30。窗口步长默认 1样本量太大时可以改成 2 或 3降低训练集冗余但要注意步长太大会丢掉窗口间的连续性信息。3. 搭建销售预测 Transformer 模型完整可运行代码3.1 模型结构拆解输入投影、位置编码、编码器栈先明确一点销售预测用 Encoder-only 结构就够了不需要完整的 Encoder-Decoder也不做自回归生成。Decoder 的逐令牌输出在训练和推理时行为不一致误差容易被放大而我们要的只是把「过去 60 天」映射成「未来 7 天」Encoder 输出最后一个时间步的向量再接一个全连接层就能同时给出 7 个预测值训练稳定、推理快。位置编码借用 NLP 的三角函数编码。销量序列本身没有「词」的概念但 Transformer 的自注意力是位置无关的如果不加位置信息模型会把第 1 天和第 50 天的输入当成等价的周期性就废了。位置编码把每个时间步的位置以固定模式加进向量模型才能区分「上周一」和「这周一」。这里直接注册为 buffer 而不是参数因为三角函数编码不参与训练。3.2 模型定义与超参数映射表核心是 input_proj 把单维销售量映射到 d_model 维位置编码叠加后进入多层 TransformerEncoder取最后一个时间步的输出再过 output_proj 得到 pred_len 个预测值。import torch import torch.nn as nn class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len1000, dropout0.1): super().__init__() self.dropout nn.Dropout(dropout) 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() * (-np.log(10000.0) / d_model)) pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) pe pe.unsqueeze(0) # (1, max_len, d_model) self.register_buffer(pe, pe) def forward(self, x): return self.dropout(x self.pe[:, :x.size(1)]) class SalesTransformer(nn.Module): def __init__(self, seq_len, pred_len, d_model64, nhead4, num_layers2, dropout0.1): super().__init__() self.input_proj nn.Linear(1, d_model) # 输入特征数当前为 1 self.pos_encoder PositionalEncoding(d_model, dropoutdropout) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dropoutdropout, batch_firstTrue) self.transformer nn.TransformerEncoder(encoder_layer, num_layersnum_layers) self.output_proj nn.Linear(d_model, pred_len) def forward(self, x): # x: (batch, seq_len, 1) x self.input_proj(x) # (batch, seq_len, d_model) x self.pos_encoder(x) # 叠加位置信息 x self.transformer(x) # 自注意力编码 out self.output_proj(x[:, -1, :]) # 取最后时间步 - (batch, pred_len) return out注意 batch_firstTrue。PyTorch 的 TransformerEncoderLayer 默认输入是 (seq_len, batch, d_model)batch_firstTrue 后可以直接使用 (batch, seq_len, d_model)和数据集形状一致少一次 transpose。模型输出 (batch, pred_len)也就是说一次前向直接输出未来 7 天的预测值这是 direct multi-step后面避坑章节会解释它为什么比递归预测稳。超参数的选择可以参考这张表参数取值说明d_model64历史窗口映射到多少维太小表达能力弱太大训练慢nhead4注意力头数通常保证 d_model / nhead 为 16 或 32num_layers2编码器层数小规模销售数据 2 层足够4 层容易过拟合dropout0.1位置编码和注意力里都做随机失活seq_len60输入历史天数pred_len7预测未来天数3.3 训练循环损失函数、优化器与梯度裁剪训练部分用 MSELoss优化器用 Adam。Transformer 对学习率比 LSTM 更敏感第一次跑建议 lr1e-3 起步配合梯度裁剪把最大梯度范数限制在 1.0可以避免训练中途 loss 跳到 NaN 的翻车现场。from torch.utils.data import DataLoader, TensorDataset def make_loader(X, y, batch_size64, shuffleTrue): dataset TensorDataset(torch.FloatTensor(X), torch.FloatTensor(y)) return DataLoader(dataset, batch_sizebatch_size, shuffleshuffle) train_loader make_loader(X_train, y_train) model SalesTransformer(seq_lenseq_len, pred_lenpred_len) optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.MSELoss() epochs 60 train_losses [] for epoch in range(epochs): model.train() epoch_loss 0.0 for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() epoch_loss loss.item() * xb.size(0) avg_loss epoch_loss / len(X_train) train_losses.append(avg_loss) if (epoch 1) % 10 0: print(fEpoch {epoch1}/{epochs}, Loss {avg_loss:.6f})shuffleTrue 在这里是安全的样本窗口之间虽有重叠但不存在未来信息进入过去窗口的问题。如果后续加了「假期、促销」这类随时间变化的特征就得重新审视 shuffle避免同一时间段的数据同时出现在训练和验证里。clip_grad_norm_ 的 max_norm1.0 是经验值数据波动大时可以放到 2.0但不要取消裁剪。4. 训练过程与 Matplotlib 可视化真实值 vs 预测值对比4.1 用 Matplotlib 绘制训练损失曲线判断收敛状态训练完成后第一件事不是看预测准不准而是看损失曲线长什么样。如果 loss 在 10 个 epoch 内快速下降然后进入平台期说明模型正常收敛如果 loss 一直锯齿状震荡说明学习率偏大或梯度裁剪阈值不合适如果训练 loss 持续下降但验证集误差开始回升说明模型开始过拟合应该减少层数或加大 dropout。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False plt.figure(figsize(10, 4)) plt.plot(train_losses, label训练损失) plt.xlabel(Epoch) plt.ylabel(MSE) plt.legend() plt.tight_layout() plt.show()如果本地没有 SimHei 字体换成你系统里已有的中文字体即可。服务器上常见情况是缺中文字体导致图里全是方框这时候用 plt.rcParams 指定一套已安装字体比如 Noto Sans CJK SC。建议每 10 个 epoch 在控制台打印一次 loss 的同时把训练过程的 loss 值列表存成 CSV方便后面画图复用不用重新训练。4.2 测试集整体对比真实销量与预测销量折线图整体对比的做法把测试集按同样的窗口切成样本逐窗口用模型预测每个窗口取第一个预测值拼接成一条连续曲线和历史真实销量画在一起。这样能快速看出模型是否学到了趋势和周期。model.eval() with torch.no_grad(): X_test_seq, y_test_seq create_sequences(test_scaled, seq_len, pred_len) pred_scaled model(torch.FloatTensor(X_test_seq)).numpy() pred_sales scaler.inverse_transform(pred_scaled) true_sales scaler.inverse_transform(y_test_seq) # 每个窗口只取第一天预测拼接成连续曲线 plt.figure(figsize(12, 5)) plt.plot(true_sales[:, 0], label真实销量, colortab:blue) plt.plot(pred_sales[:, 0], label预测销量窗口首日, colortab:red, linestyle--) plt.xlabel(测试集时间步) plt.ylabel(销量) plt.legend() plt.tight_layout() plt.show()窗口首日拼起来的曲线会偏乐观因为相邻窗口大量重叠误差被平滑了。它的作用是快速验证模型有没有学到整体形态不能作为最终上线评估。更严格的做法是 walk-forward 滚动回测第 6 章会讲。inverse_transform 用的是训练集的 scaler转换回去的数值是原始销量单位画图坐标才有业务意义。4.3 单窗口放大未来 7 天预测长什么样整体曲线看不出逐日误差结构。挑最后一个窗口把未来 7 天的真实值和预测值并排画出来检查模型在短周期里的表现。真实销售数据里促销日、节假日造成的突刺模型通常会预测得平滑一些这是回归目标本身的特性不一定是代码 bug。idx -1 # 最后一个测试窗口 x_axis np.arange(1, pred_len 1) plt.figure(figsize(10, 4)) plt.plot(x_axis, true_sales[idx], markero, label真实值) plt.plot(x_axis, pred_sales[idx], markerx, label预测值) plt.xlabel(未来第几天) plt.ylabel(销量) plt.xticks(x_axis) plt.legend() plt.tight_layout() plt.show()这张图的价值在于看误差分布如果第 1 天误差小、第 7 天误差大说明模型对远期信息把握不足需要检查输入序列里是否混入了过多噪声如果每一天误差方向一致比如全部高估说明模型对近期趋势的偏置理解有系统性问题回到第 6 章看外生特征的处理。这里的坐标也可以用真实日期代替「第几天」业务方读图更容易。5. Transformer 销售预测避坑指南5 个常见问题与排查5.1 预测曲线总是滞后一拍是模型原理问题不是 bug现象测试集预测曲线和真实曲线形态几乎一致但整体向右平移了一天真实值已经下跌预测还在高位。原因MSE 回归目标的最优解是条件均值模型在不确定时倾向于输出「最保险」的中间值。销售量从峰值回落时模型会认为高位的延续概率更大于是预测曲线比真实曲线慢半拍。这不是 Transformer 特有所有回归模型都会出现。解决最常见的做法是改预测目标把「预测销量」改成「预测销量增量」。先对序列做一阶差分让模型学习涨跌幅度预测出来后再逆差分还原绝对值。另一个方向是补特征把活动日、星期几、有无促销作为输入列给模型额外的转折点信号。差分后注意训练集长度会减 1窗口切分逻辑要同步调整。5.2 标准化泄漏测试集的 Min/Max 为什么必须用训练集的现象在历史数据上回测效果很好模型上线后表现明显变差差异大得不符合常理。原因如果先把全部数据 fit MinMaxScaler 再切分测试集的 min/max 已经被模型「见过」等于把未来信息带进了训练过程。真实场景里预测未来时根本拿不到未来数据的尺度信息。这个问题在销量预测里尤其隐蔽因为绝大多数教程代码都是先标准化再切分。解决严格按时间顺序先切分scaler 只 fit 训练集再用同一个 scaler transform 测试集。做滚动回测时每一折都要用该折之前的训练数据重新 fit不能复用全量 fit 的 scaler。这也是为什么第 6 章的 walk-forward 回测比单次切分更可信的原因。5.3 梯度爆炸与训练震荡学习率、warmup 和梯度裁剪现象训练到第 20 个 epoch 左右 loss 突然变成 NaN或者 loss 在一段区间上来回震荡不下降。原因Transformer 的梯度范数在深层编码器里可能快速增大Adam 虽然自适应缩放但挡不住单步梯度范数爆炸。学习率 1e-2 以上在这个任务里基本必崩。如果你改了 num_layers 到 4 层以上梯度爆炸的概率会明显上升。解决默认用 lr1e-3加 clip_grad_norm_ 把 max_norm 设在 1.0。如果还想更高学习率上手可以先跑 5 个 epoch 的线性 warmup从 1e-4 升到目标值。出现 NaN 后先查学习率和梯度裁剪不要急着调网络宽度。实践中我发现 d_model 调大后同样 lr 也会更容易震荡这时优先降 lr 而不是加裁剪阈值。5.4 只看 MSE 导致业务不认可换 MAPE 与误差分布现象训练损失降到很低但业务方拿真实销量一对认为预测「完全不可用」。原因MSE 对大销量的样本权重极高。如果数据里同时有日销 5 件的小众品和日销 500 件的爆款模型会把精力全放在拟合爆款上小销量商品的相对误差可能达到 100% 以上。业务方更关心相对误差不是绝对误差。解决评估时同步看 MAPE平均绝对百分比误差和误差分布。对销售预测来说MAPE 20% 算可用20% 到 30% 需要持续监控超过 30% 建议先回到特征和数据质量而不是继续调模型。如果你负责的品类里有销量差距极大的 SKU按 SKU 分组单独算 MAPE再按销量加权汇总这个指标业务方更容易接受。5.5 多步预测误差累积递归预测为什么越走越偏现象用模型预测未来 7 天没问题但把预测值当作输入继续递归预测第 8、9 天误差越来越大几天后预测曲线趋向一条平线。原因训练时输入都是真实历史值推断时输入却包含模型自己的预测值训练与推理行为不一致。误差会像滚雪球一样累积递归步数越多越明显。这个现象在销量数据里比在平稳序列里更严重因为销量序列方差大单步误差本身就不小。解决direct multi-step 预测一次输出 pred_len 个值本身就绕开了这个问题。如果业务要求 30 天以上的预测窗口优先加长 pred_len 让模型一次输出 30 天而不是递归。递归只适合对误差累积不敏感的粗粒度场景比如只看月总量趋势。如果训练数据量不足以支撑直接输出 30 天可以退一步用 seq2seq 架构配合 teacher forcing 训练。6. 进阶让 Transformer 预测销售量真正落地的三个验证手段6.1 Walk-forward 回测按业务节奏滚动评估固定切分一次的训练测试评估结果受切分点影响很大。上线前我习惯补一轮 walk-forward 回测用前 540 天训练预测未来 7 天然后把真实数据纳入训练集往后滚 7 天再预测重复整个测试周期。每轮只能用该轮之前的数据 fit scaler记录每一轮 MAPE。这个流程能暴露模型在跨季节、跨活动周期时的真实水平比单次切分的结论可靠得多。# 伪代码逐轮滚动每轮只使用当前时间点之前的数据 history all_data[:init_length] for day in range(init_length, all_len - pred_len, pred_len): model retrain_or_update(history) # 用当前 history 训练或微调 pred model.predict(history[-seq_len:]) # 预测未来 pred_len 天 record_mapes(pred, all_data[day:day pred_len]) # 与真实值比较 history all_data[:day pred_len] # 把真实段并入历史6.2 误差按星期几拆解发现模型没学到的周期把预测误差按星期几分组统计经常能发现周一系统性低估、周末系统性高估。原因是纯数值序列分不清「周几」Transformer 只能学到数值层面的周期学不到日历语义。给输入补一列星期几特征模型输入端从 1 个特征变成 2 个# 在 create_sequences 之前拼上星期几特征 train_day (train_df[date].dt.dayofweek.values / 7.0).reshape(-1, 1) train_feat np.concatenate([train_scaled, train_day], axis1) test_day (test_df[date].dt.dayofweek.values / 7.0).reshape(-1, 1) test_feat np.concatenate([test_scaled, test_day], axis1) # 对 train_feat / test_feat 做 create_sequences # 模型 input_proj 的输入维度从 1 改成 2对应 input_proj 的输入维度从 nn.Linear(1, d_model) 改成 nn.Linear(2, d_model)。这一步通常是提精度的第一杠杆改完再回头看超参数。类似的思路还可以加「是否促销」「是否是月初」这类二值特征。6.3 超参数微调的方向与停止条件小数据集上不要盲目堆网络。d_model 从 32 到 64 通常有明显收益再往上收益递减num_layers 超过 4 层在几千样本的销售数据上几乎必然过拟合。调参顺序建议先定 seq_len 覆盖完整周期再调 d_model 和 dropout最后动 lr。停止条件用验证集 MAPE而不是训练 loss——训练 loss 降不代表业务误差降。我的习惯是每次调参只改一个变量跑完 walk-forward 记录 MAPE攒一张「参数-指标」对照表。血泪经验告诉我销售预测项目的瓶颈八成在特征和时间跨度口径上只有两成在模型结构上。先把数据维度和评估方式做扎实再动 Transformer 的结构这条路最省时间。希望帮到你。本文还有配套的精品资源点击获取