☰
用Python实现Transformer销量预测:从数据预处理到训练可视化
2026/10/18 12:58:50 网站建设 项目流程

简介:基于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.md

data/preprocess.py负责把原始CSV转换成模型能吃的监督学习格式;src/model.py只做一件事,就是定义Transformer编码器结构,不掺任何训练逻辑;src/train.py是核心入口,控制数据加载、批次划分、损失计算和模型保存;src/predict.py是推理脚本,加载训练好的权重,对最后一段已知序列做外推预测。results目录里所有图片都由Matplotlib生成,默认以PNG格式落盘,方便直接贴进周报或报表。

2.2 依赖环境与版本选择

依赖文件写得比较克制,核心库只有五个:

torch>=1.13.0 numpy>=1.24.0 pandas>=1.5.0 matplotlib>=3.6.0 scikit-learn>=1.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_range=scale_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_window=30, output_window=7): 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_len=512): 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, dropout=0.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, dropout=0.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, dropout=0.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, epochs=200, lr=0.001, device='cuda'): optimizer = torch.optim.Adam(model.parameters(), lr=lr) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs) 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_norm=1.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(f"Epoch {epoch+1}/{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, label='Train Loss') plt.plot(val_losses, label='Validation Loss') plt.xlabel('Epoch') plt.ylabel('MSE Loss') plt.title('Transformer Sales Prediction - Training Loss') plt.legend() plt.grid(True, linestyle='--', alpha=0.6) plt.savefig('results/loss_curve.png', dpi=150, bbox_inches='tight')

这里有个细节:中文字体配置。如果不设置SimHei或者系统中没有对应字体,图里的标题和轴标签会显示成方块乱码。轻量做法是用plt.rcParams['font.sans-serif']指定一个系统已有的中文字体,一劳永逸,不用每次绘图都传fontproperties参数。

保存图片时用bbox_inches='tight'去掉多余留白,dpi=150保证放大也不糊。图里的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:], dtype=torch.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, label='True Sales', marker='o') plt.plot(range(len(pred_sales)), pred_sales, label='Predicted Sales', marker='x') 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', dpi=150, bbox_inches='tight')

对比图看起来简单,但它是判断模型质量最直观的手段。需要留意的两点:一是预测结果必须经过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 学习率过大导致训练发散

现象:训练刚开始几个epoch,loss从0.01骤降到0.003,然后突然变成NaN,之后所有loss都是NaN,模型输出全是NaN。

原因:学习率设太大(比如0.1),梯度更新幅度过大,参数直接越过最优区域进入发散状态;也可能是深层Transformer本身的梯度方差较大。NaN并非模型写错了,而是数值溢出。

解决:把学习率降到0.001或更低。我更建议第一天先用lr=0.0001跑100个epoch看loss曲线形态,如果loss下降过于缓慢再逐步提升到0.0005、0.001。另外保证训练脚本里有梯度裁剪逻辑:nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.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天,和当月实际值对比,反复滚动几个月,观察误差在业务容忍范围内是否稳定。这套滚动验证流程是我每次改完预测类项目都强制走一遍的,不为别的,就图一个安心。希望这些改造思路能帮到你,跑通了之后你会发现,换数据、调参数这些事情比想象中轻松。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询