简介基于PyTorch的Transformer销售量预测Python代码包面向有深度学习基础、想用Transformer做时间序列多步预测的开发者。资源共69个文件以6个Python脚本为主覆盖数据处理、模型构建、训练、评估与可视化全流程另有60张预测效果对比图、1份CSV历史数据、1份环境配置及1份Markdown说明文档压缩包仅3.72MB。已有331人学习。目录清晰data存数据models/transformer_model.py实现核心网络utils提供预处理、绘图、训练和评估函数graphs按单步/多步保存逐epoch图。借助该代码可快速搭建销量预测任务模块化设计也便于迁移到其他单变量多步预测场景是学习Transformer回归建模的实用参考。1. Transformer预测销售量这份Python代码能帮你解决什么问题做销售预测的人通常有个惯性拿到历史销量数据第一反应是上ARIMA、指数平滑或者LSTM。但有一种场景很容易被忽略——当数据量足够、序列较长时基于自注意力机制的Transformer模型往往能给出比LSTM更稳的结果。这个项目就是一套用Python完整实现的Transformer销量预测代码从数据预处理、Transformer模型构建、训练循环到Matplotlib可视化绘图全部覆盖下载后按步骤跑就能复现。它适合两类人一类是想把销量预测落地到实际业务中的开发者拿这份代码做基线模型再改造另一类是正在学Transformer结构、想看看它如何应用在回归任务上的同学。模型预测的不是分类标签而是未来若干时间步的具体销量数值因此损失函数、评估指标和可视化方式都围绕数值回归来设计。整个项目思路清晰、依赖少核心代码集中在模型构建和训练流程上跑通之后改数据源、调超参数、换评估口径都很方便。2. 项目整体拆解目录结构、依赖与数据流2.1 目录结构与模块职责拿到压缩包之后先别急着运行花两分钟把目录结构过一遍。这套代码的模块划分比较常规但每个文件职责很清晰结构大致如下sales_forecast/ ├── data/ │ ├── sales.csv # 原始销量数据至少包含日期列和销量列 │ └── preprocess.py # 数据清洗、滑动窗口构造、归一化 ├── src/ │ ├── model.py # Transformer模型定义编码器部分 │ ├── train.py # 训练循环、验证、模型保存 │ └── predict.py # 加载模型、预测未来N个时间步 ├── results/ │ ├── loss_curve.png # 训练损失曲线Matplotlib绘制 │ └── prediction.png # 预测值 vs 真实值对比图 ├── checkpoints/ │ └── transformer_sales.pth # 训练好的权重文件 ├── requirements.txt └── README.mddata/preprocess.py负责把原始CSV转换成模型能吃的监督学习格式src/model.py只做一件事就是定义Transformer编码器结构不掺任何训练逻辑src/train.py是核心入口控制数据加载、批次划分、损失计算和模型保存src/predict.py是推理脚本加载训练好的权重对最后一段已知序列做外推预测。results目录里所有图片都由Matplotlib生成默认以PNG格式落盘方便直接贴进周报或报表。2.2 依赖环境与版本选择依赖文件写得比较克制核心库只有五个torch1.13.0 numpy1.24.0 pandas1.5.0 matplotlib3.6.0 scikit-learn1.2.0其中scikit-learn只用来做MinMaxScaler归一化如果你不想引入它完全可以用numpy手写一个最大最小值缩放函数几十行就搞定。Transformer模型的实现完全基于PyTorch原生API构建没有借用封装好的第三方Transformer库这意味着你在看代码时能直接看到多头注意力、位置编码的原始实现对理解原理帮助很大。如果你在Windows上跑建议Python版本选3.8到3.10之间太新的Python版本搭配旧版PyTorch偶尔会出现预编译轮子找不到的问题。数据的原始格式我也建议确认一下。常见的sales.csv大概长这样date,sales 2023-01-01,120 2023-01-02,98 2023-01-03,135 ...如果日期列不是标准格式pandas的pd.to_datetime会直接抛异常。我一般会在预处理脚本里加一个format参数遇到非标准日期格式时手动指定解析格式避免在数据加载阶段就翻车。2.3 数据预处理从CSV到训练样本预处理是整个项目中最容易忽视、也最容易出错的地方。Transformer本身不关心日期它只吃数值序列所以第一步是把销量列转成浮点数然后做归一化。# data/preprocess.py import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler def load_and_scale(csv_path, scale_range(0, 1)): df pd.read_csv(csv_path) df[date] pd.to_datetime(df[date]) df df.sort_values(date) scaler MinMaxScaler(feature_rangescale_range) df[sales_scaled] scaler.fit_transform(df[[sales]]) return df, scaler这里归一化到0到1之间是有讲究的。Transformer内部的激活函数和注意力机制对输入尺度比较敏感如果原始销量从几百到几万的量级直接喂进去梯度更新很容易震荡。用MinMaxScaler压缩到0到1区间后模型收敛速度快一个档次。注意scaler对象必须保存下来预测完再把结果逆变换回真实销量数值否则你画出的对比图纵轴是归一化值业务方根本看不懂。样本构造采用滑动窗口方式用过去input_window天的销量预测未来output_window天的销量def create_sequences(data, input_window30, output_window7): X, y [], [] for i in range(len(data) - input_window - output_window 1): X.append(data[i:i input_window]) y.append(data[i input_window:i input_window output_window]) return np.array(X), np.array(y)每个样本的输入是连续30天的销量标签是接下来7天的销量。这种构造方式直接把模型定义为一个多步回归问题。窗口大小是超参数具体取值取决于业务节奏。如果销售数据有很强的周周期性input_window至少拉长到30天以上让注意力机制有机会捕捉到跨周的依赖模式。相关热搜词里的transformer训练技巧时序预测窗口选择搜到的讨论基本都建议窗口不能太短否则注意力机制连一个完整周期都看不到。3. Transformer模型核心代码位置编码、多头注意力与残差连接3.1 位置编码让模型感知序列顺序Transformer的注意力机制本身是顺序无关的它会把序列中所有位置两两计算相关性如果不加位置信息把序列倒过来喂给模型输出的预测结果完全一样这对销量预测是致命的。销量数据有明显的趋势和季节性模型必须知道第1天和第30天的先后关系位置编码就是来解决这个问题的。# src/model.py 节选 import numpy as np import torch import torch.nn as nn 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) div_term torch.exp(torch.arange(0, d_model, 2) * (-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).float() self.register_buffer(pe, pe) def forward(self, x): return x self.pe[:, :x.size(1), :]这段代码用的是原版Transformer里的正弦位置编码。核心原理是不同频率的正弦/余弦函数叠加让模型通过三角函数的相对位置差值学会判断两个时间点之间的距离。div_term里的10000.0控制频率衰减速度d_model越大位置编码的周期越长。register_buffer的作用是把位置编码注册为模型的持久缓冲区不会参与梯度更新但会随模型一起保存和加载。位置编码的维度必须和词嵌入维度d_model一致这是新手最容易踩的坑。如果你的输入特征维度是1只有销量一个特征但d_model设成了128那输入进Transformer之前需要先过一个线性层把维度映射到128否则x pe这步会直接报维度不匹配的错。3.2 多头注意力机制注意力机制是Transformer的核心计算单元多头就是把注意力计算拆成多个子空间并行执行。通俗地说不同头可以关注不同维度的依赖关系有的头关注周一到周日的周期规律有的头关注节假日前后的销量突增多头并行让模型能在单层内同时捕捉多种时间模式。class MultiHeadAttention(nn.Module): def __init__(self, d_model, n_heads, dropout0.1): super().__init__() assert d_model % n_heads 0 self.d_model d_model self.n_heads n_heads self.d_k d_model // n_heads self.w_q nn.Linear(d_model, d_model) self.w_k nn.Linear(d_model, d_model) self.w_v nn.Linear(d_model, d_model) self.out_proj nn.Linear(d_model, d_model) self.dropout nn.Dropout(dropout) def forward(self, x): batch_size, seq_len, _ x.size() Q self.w_q(x).view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2) K self.w_k(x).view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2) V self.w_v(x).view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2) attn_scores torch.matmul(Q, K.transpose(-2, -1)) / (self.d_k ** 0.5) attn_probs torch.softmax(attn_scores, dim-1) attn_probs self.dropout(attn_probs) output torch.matmul(attn_probs, V) output output.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model) return self.out_proj(output)关键步骤就三步第一步Q、K、V分别通过三个权重矩阵映射然后按头数切分并转置得到形状[batch, n_heads, seq_len, d_k]的张量第二步Q和K做点积得到注意力分数除以sqrt(d_k)缩放防止内积值过大导致softmax梯度消失第三步softmax归一化得到权重作用到V上最后把所有头的结果拼接回去再过一层输出投影。注意力分数除以sqrt(d_k)这一步是原版Transformer论文里特意强调的细节。如果不做缩放当d_k很大时Q和K点积的方差会随着维度增大而线性增长softmax输入值过大输出分布会变得极其尖锐梯度基本传不动。缩放就是把方差拉回1附近的稳定区间。n_heads的取值常见的是4或8。d_model 128时如果设8个头每个头的d_k就是16。多头数不是越多越好数据量不大时头数过多反而容易过拟合。在销量预测这种单变量序列回归任务上4个头通常就够用跑起来也更快。3.3 前馈网络与残差连接注意力层之后是一个逐位置的前馈网络Feed-Forward Network它对序列中的每个时间点独立做两次全连接变换中间夹ReLU激活和Dropout。这个网络的作用是对注意力提取到的特征做非线性变换提升模型的表达能力。class FeedForward(nn.Module): def __init__(self, d_model, d_ff, dropout0.1): super().__init__() self.linear1 nn.Linear(d_model, d_ff) self.linear2 nn.Linear(d_ff, d_model) self.dropout nn.Dropout(dropout) def forward(self, x): return self.linear2(self.dropout(torch.relu(self.linear1(x))))d_ff通常设置为d_model的4倍也就是从128映射到512再映射回128。中间维度拉高是为了让模型有机会学到特征之间的交叉组合。如果d_model是128、d_ff还是128模型的非线性表达能力会明显不足训练损失很容易卡在一个较高位下不去。残差连接和层归一化把整个编码器层串起来class TransformerEncoderLayer(nn.Module): def __init__(self, d_model, n_heads, d_ff, dropout0.1): super().__init__() self.self_attn MultiHeadAttention(d_model, n_heads, dropout) self.ffn FeedForward(d_model, d_ff, dropout) self.norm1 nn.LayerNorm(d_model) self.norm2 nn.LayerNorm(d_model) self.dropout1 nn.Dropout(dropout) self.dropout2 nn.Dropout(dropout) def forward(self, x): x x self.dropout1(self.self_attn(self.norm1(x))) x x self.dropout2(self.ffn(self.norm2(x))) return x注意这里的顺序先LayerNorm再进注意力层。这是Pre-LN结构和原版Transformer先过注意力再归一化的Post-LN不同。Pre-LN在训练时更稳定对学习率不那么敏感对于销量数据这种样本量不算特别大的回归任务推荐直接用Pre-LN。整个模型就是堆叠若干个这样的编码器层最后一层输出接一个全连接层把d_model映射到output_window维度直接输出未来7天的预测值。这里的全连接层是唯一一个输出维度跟预测天数强耦合的地方改output_window时必须同步调整。4. 训练与可视化从损失曲线到预测对比图4.1 训练循环与损失函数训练脚本的核心逻辑是加载上一章构造好的X和y切分训练集/验证集定义回归用的损失函数MSE然后进入标准的PyTorch训练循环。因为任务是数值回归而非分类所以损失函数和评估指标都围绕预测值和真实值之间的数值差异展开。# src/train.py 节选 import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset def train_model(model, train_loader, val_loader, epochs200, lr0.001, devicecuda): optimizer torch.optim.Adam(model.parameters(), lrlr) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxepochs) criterion nn.MSELoss() model.to(device) for epoch in range(epochs): model.train() total_loss 0.0 for x_batch, y_batch in train_loader: x_batch, y_batch x_batch.to(device).float(), y_batch.to(device).float() optimizer.zero_grad() pred model(x_batch) loss criterion(pred, y_batch) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() * x_batch.size(0) train_loss total_loss / len(train_loader.dataset) val_loss evaluate(model, val_loader, criterion, device) scheduler.step() if (epoch 1) % 20 0: print(fEpoch {epoch1}/{epochs} | Train Loss: {train_loss:.6f} | Val Loss: {val_loss:.6f}) return model优化器选了Adam而不是SGD原因是自适应学习率方法在Transformer这类深层模型上收敛得更顺滑不用手动调太多参数。余弦退火调度器CosineAnnealingLR让学习率在训练过程中周期性衰减前期大步探索、后期小步精调这个搭配在时序回归任务上的表现稳定。梯度裁剪clip_grad_norm_是Transformer训练的关键保护措施注意力机制叠加多层后梯度范数容易突增裁剪到1.0以下可以有效防止loss炸成NaN。4.2 损失曲线绘制训练结束后Matplotlib把每个epoch的train loss和val loss画成两条曲线直观地反映模型是收敛了还是过拟合了。# src/train.py 绘图部分 import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False plt.figure(figsize(10, 5)) plt.plot(train_losses, labelTrain Loss) plt.plot(val_losses, labelValidation Loss) plt.xlabel(Epoch) plt.ylabel(MSE Loss) plt.title(Transformer Sales Prediction - Training Loss) plt.legend() plt.grid(True, linestyle--, alpha0.6) plt.savefig(results/loss_curve.png, dpi150, bbox_inchestight)这里有个细节中文字体配置。如果不设置SimHei或者系统中没有对应字体图里的标题和轴标签会显示成方块乱码。轻量做法是用plt.rcParams[font.sans-serif]指定一个系统已有的中文字体一劳永逸不用每次绘图都传fontproperties参数。保存图片时用bbox_inchestight去掉多余留白dpi150保证放大也不糊。图里的val loss曲线是一个很好的诊断工具——如果train loss持续下降但val loss在第50个epoch附近开始反弹说明模型开始记忆训练数据中的噪声了这时候应该减小模型层数、增大Dropout或者提前停止训练。4.3 预测值与真实值对比训练完模型后用验证集尾部的一段数据做推理把预测结果逆归一化还原成真实销量值同时把同区间的真实销量画在同一张图上按时间步一一对比。# src/predict.py 绘图部分 model.eval() with torch.no_grad(): last_window torch.tensor(test_x[-1:], dtypetorch.float32).to(device) pred_scaled model(last_window).cpu().numpy()[0] pred_sales scaler.inverse_transform(pred_scaled.reshape(-1, 1)).flatten() true_sales scaler.inverse_transform(test_y[-1:].reshape(-1, 1)).flatten() plt.figure(figsize(12, 5)) plt.plot(range(len(true_sales)), true_sales, labelTrue Sales, markero) plt.plot(range(len(pred_sales)), pred_sales, labelPredicted Sales, markerx) plt.xlabel(Day) plt.ylabel(Sales Volume) plt.title(Transformer Sales Prediction - Last 7 Days) plt.legend() plt.grid(True) plt.savefig(results/prediction.png, dpi150, bbox_inchestight)对比图看起来简单但它是判断模型质量最直观的手段。需要留意的两点一是预测结果必须经过scaler.inverse_transform还原否则画出来的纵轴是0到1的归一化值没法直接和业务讨论二是测试集的选择应该从时间序列末尾切分而不是从中随机抽时间序列数据随机切分会造成未来信息泄进训练集导致验证结果虚高。预测值和真实值的走势如果基本吻合、个别高峰低谷有偏差这是正常水平如果整体形状都对不上就需要回到前面章节去检查数据窗口、模型层数等设置。5. 常见问题与避坑训练Transformer销量预测时的几个坑5.1 数据泄露导致验证集指标虚高现象训练损失降得很好验证集MSE也低得惊人但拿到新月份的数据去预测效果一塌糊涂偏差大到离谱。原因时间序列数据是严格有序的如果随机打乱所有样本再划分训练集和验证集部分未来数据就被混进了训练集。Transformer注意力机制很擅长记忆输入序列中的模式一旦看见过未来的数据验证集上的表现自然漂亮但实际部署到未来数据上就彻底失灵。解决按时间顺序切分数据集。前70%或80%作为训练集后20%到30%作为验证集和测试集。具体做法是用索引直接切片train_x, val_x X[:split_idx], X[split_idx:]。注意切片前不要调用任何shuffle方法。5.2 位置编码维度不匹配现象模型构建时报错RuntimeError: The size of tensor a (1) must match the size of tensor b (128) at non-singleton dimension 2。原因位置编码的d_model设成了64但输入张量经过嵌入映射后最后一维是1只有销量一个特征跟位置编码的最后一维对不上。原始数据是单变量序列输入维度是1直接和位置编码相加必然报错。解决在Transformer编码器前插入一个输入映射层nn.Linear(input_dim, d_model)把维度从1映射到128再加位置编码。或者是直接把位置编码的d_model设计成输入维度。推荐前者因为线性映射层让模型有权重去学习特征升维灵活性更好。5.3 学习率过大导致训练发散现象训练刚开始几个epochloss从0.01骤降到0.003然后突然变成NaN之后所有loss都是NaN模型输出全是NaN。原因学习率设太大比如0.1梯度更新幅度过大参数直接越过最优区域进入发散状态也可能是深层Transformer本身的梯度方差较大。NaN并非模型写错了而是数值溢出。解决把学习率降到0.001或更低。我更建议第一天先用lr0.0001跑100个epoch看loss曲线形态如果loss下降过于缓慢再逐步提升到0.0005、0.001。另外保证训练脚本里有梯度裁剪逻辑nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)这个操作在销量预测这种数值序列任务上几乎是必需品不要省。5.4 归一化逆变换后预测值全是同一个数现象预测曲线是一条水平直线每个时间步的预测值几乎完全一样反归一化后更是看不出波动。原因最常见的原因是模型没有学到足够的信息直接趋近于输出一个常量。具体到代码层面要么是输入窗口过短模型只学到了预测值约等于近期均值这个捷径要么是训练epoch不够模型还在欠拟合阶段就被拿去推理了。解决把输入窗口拉长到至少覆盖一个完整业务周期比如30天或60天增加模型层数和d_ff容量同时把训练epoch提升到500以上。另一个排查方向是检查归一化后的标签——如果标签是大量接近的数值且变化幅度极微小模型确实很难学到差异这种情况要检查数据本身是不是存在质量异常。6. 二次开发把固定代码改成可配置的预测管线拿到这份代码之后最简单的改造切入点是引入配置文件把散落在脚本里的超参数全部抽出来。我用yaml格式管理原因是可读性好业务方即使不懂代码也能看懂每个参数的含义。改造后的核心思路是用配置文件驱动训练和预测而不是改代码重启。# config.yaml data: csv_path: data/sales.csv input_window: 30 output_window: 7 model: d_model: 128 n_heads: 4 n_layers: 3 d_ff: 512 dropout: 0.1 train: epochs: 300 lr: 0.001 batch_size: 64 device: auto在train.py里加一个十几行的配置加载函数用yaml.safe_load读入字典然后逐项传给模型和训练循环。这样所有超参数集中在同一个文件里调参只需要改yaml再跑脚本不用翻代码定位常量。设备配置device: auto可以自动检测有无CUDA避免换台机器还要改代码。进一步的改造是支持不同的业务数据源。原项目假设输入是单变量序列只有销量很多实际业务还有价格、促销标记、天气等外生变量。改造方式是把input_dim设为动态值数据预处理时把多列特征拼接成一个二维张量模型输入映射层nn.Linear(input_dim, d_model)自动适配。位置编码、注意力结构都不需要改动因为它们是维度无关的。这是我改造这类项目时最常用的习惯——模型结构尽量通用数据适配放在预处理阶段边界清晰。预测置信区间也是值得加的一块。Transformer本身只输出点预测值但业务决策通常需要知道低/中/高三种情形。常见做法是MC Dropout——在推理时保持Dropout开启前向传播T次比如50次对每一次输出计算均值然后把T个结果的分布用来估算上下界。代码改动量很小model.train()挂上Dropout放入一个循环收集预测结果最后用numpy计算分位数。有了置信区间预测结果的业务说服力会强很多。验证方法也顺手说一句。改造完之后不要只看训练损失曲线拿历史数据做一次回测固定截止日期重新训练预测未来7天和当月实际值对比反复滚动几个月观察误差在业务容忍范围内是否稳定。这套滚动验证流程是我每次改完预测类项目都强制走一遍的不为别的就图一个安心。希望这些改造思路能帮到你跑通了之后你会发现换数据、调参数这些事情比想象中轻松。本文还有配套的精品资源点击获取