简介:面向时间序列预测任务的LSTM+Transformer混合模型PyTorch实现,适合需要多特征输入、单变量输出场景的研究者与开发者;可直接迁移到风电、光伏、寿命预测、浓度预测等项目中,也适合作为学习Transformer与LSTM结合的入门范例。压缩包共160个文件、约1.94MB,以31个Python源码和87个pyc编译文件为主,另含4个csv示例数据、xml工程配置、txt说明及备份文件,目录结构清晰,便于按模型定义、数据加载、训练评估等模块查阅。模型融合Transformer的全局建模能力与LSTM的时序记忆能力,代码注释完整,支持csv和xlsx格式读取;内置ETTh2、ETTm2等多组公开数据以及真实值与预测值对比文件,替换数据即可完成训练与验证。已有145人学习,可作为时间序列预测的完整参考实现,覆盖从数据处理、模型训练到结果评估的完整链路,也能快速改造用于自有数据集。1. 时间序列预测选型:LSTM与Transformer为什么总被放在一起比在拿到一套带完整源码和数据的 LSTM Transformer 时间序列预测项目时,我通常先不急着跑训练,而是先看它把两个模型放在一起对比的边界条件。很多人默认 Transformer 一定更强,但换到小样本、强噪声的时间序列上,LSTM 反而更稳;而数据量足够大、序列足够长时,Transformer 的多头注意力才能体现出优势。这篇笔记会从源码目录、数据预处理、训练参数、调参对比和常见坑几个层面,把这个对比实验完整拆开,适合正在做预测、又想在两个模型之间做技术选型的开发者。这里说的“完整源码和数据”,核心价值不是两个模型类的实现,而是数据怎么切、参数怎么设、坑在哪里,正好是新手最容易卡住的地方。2. 源码包的文件结构与数据格式:动手前先核对这三处2.1 目录设计与数据文件说明:先确认你拿到的是完整源码一个能直接复现的时间序列预测源码,文件不应该只有模型定义,至少要有数据入口、预处理脚本和训练入口。我拿到压缩包先做三件事:打开 config.py 看全局参数;打开 data 下的 csv 看第一行和最后一行的时间;打开 train.py 看 DataLoader 有没有 shuffle。如果数据是从某天中间开始、时间间隔不固定,后面所有结果都没有意义。完整度比较高的源码包,目录通常长这样:project/ ├── config.py # 全局超参数 ├── train.py # 训练入口 ├── evaluate.py # 测试集评估与绘图 ├── data/ │ ├── raw_series.csv # 原始单变量时间序列 │ └── preprocess.py # 滑动窗口构造样本 ├── models/ │ ├── lstm_model.py # LSTM模型定义 │ ├── transformer_model.py # Transformer模型定义 │ └── __init__.py └── output/ ├── checkpoints/ # 模型权重 └── figures/ # loss曲线和预测对比图这里的核心是 data/preprocess.py 和 models 下的两个模型文件。如果只给了模型代码而没有数据预处理,你换自己的数据时还是要重写一遍,那这个源码包的复用价值就少了一半。我会先确认原始数据是不是只有时间和数值两列,列名是什么,时间间隔是小时还是分钟,因为后面构造滑动窗口时 seq_len 的单位完全跟着间隔走。时间戳缺失会造成隐性的样本错位,比数值异常更难发现。2.2 数据与预处理脚本:滑动窗口构造样本的写法时间序列预测本质上是把历史窗口映射到未来窗口的监督学习。常见做法是把一条长度为 N 的序列切成很多个长度为 seq_len 的输入,每个输入对应一个长度为 pred_len 的标签。这个步骤看似简单,但归一化顺序和时间顺序非常容易写错。# create_sequences.py import numpy as np import pandas as pd def create_sequences(data, seq_len=24, pred_len=1): xs, ys = [], [] for i in range(len(data) - seq_len - pred_len + 1): x = data[i : i + seq_len] y = data[i + seq_len : i + seq_len + pred_len] xs.append(x) ys.append(y) return np.array(xs), np.array(ys) df = pd.read_csv("data/raw_series.csv") values = df["value"].values.astype(np.float32) # 先归一化再划分,顺序不能反 mean, std = values.mean(), values.std() values = (values - mean) / std seq_len, pred_len = 24, 1 X, Y = create_sequences(values, seq_len, pred_len) # 按时间顺序划 70% / 15% / 15% split1 = int(len(X) * 0.7) split2 = int(len(X) * 0.85) X_train, Y_train = X[:split1], Y[:split1] X_val, Y_val = X[split1:split2], Y[split1:split2] X_test, Y_test = X[split2:], Y[split2:] print(X_train.shape, X_val.shape, X_test.shape)这段代码的关键点有三个。第一,先做归一化再切序列,不能先切训练集再各自算 mean 和 std,否则验证集和测试集的统计信息会泄漏到训练过程里。第二,划分必须按时间顺序,绝对不能用 sklearn 的 train_test_split 随机打乱,因为时间序列一旦打乱,模型会看到未来数据,验证集分数会虚高。第三,seq_len 最好覆盖一个完整周期,小时数据至少 24,日数据至少 7,否则模型学不到周期性。2.3 LSTM和Transformer的模型定义:输入输出怎么对齐两个模型在源码包里的定义都比较直接,核心是理解输入维度是 (batch, seq_len, input_size),输出维度是 (batch, pred_len)。LSTM 取最后一个时间步的隐藏状态接线性层,Transformer 则先做线性升维,再经过自注意力编码器,同样只取最后一个位置输出。# models/lstm_model.py import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, pred_len=1): super().__init__() self.lstm = nn.LSTM(input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=0.2) self.reg = nn.Linear(hidden_size, pred_len) def forward(self, x): # x shape: (batch, seq_len, input_size) out, _ = self.lstm(x) # out: (batch, seq_len, hidden_size) return self.reg(out[:, -1, :]) # 取最后一个时间步# models/transformer_model.py import torch import torch.nn as nn class TransformerPredictor(nn.Module): def __init__(self, input_size=1, d_model=64, nhead=4, num_layers=2, pred_len=1): super().__init__() self.embed = nn.Linear(input_size, d_model) encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, dim_feedforward=256, dropout=0.1, activation="relu", batch_first=True) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) self.reg = nn.Linear(d_model, pred_len) def forward(self, x): # x shape: (batch, seq_len, input_size) src = self.embed(x) # 线性升维到 d_model memory = self.encoder(src) # 自注意力编码 return self.reg(memory[:, -1, :])需要注意两个实现上的细节。Transformer 里 batch_first=True 必须显式设置,否则输入输出维度顺序会和 LSTM 混在一起,排查起来很耗时间。另一个是位置编码问题,上面这个简写版本没有加位置编码,自注意力层本身不带顺序信息,所以完整源码里通常会在 embed 之后加一个 sinusoidal positional encoding,否则序列顺序对模型来说是完全对称的。实际跑的时候,我会先用 LSTM 确认数据和 loss 流程没问题,再切到 Transformer,这样能把模型实现的 bug 和数据问题分开。3. 训练跑通:参数配置、训练循环与最小复现命令3.1 参数配置:seq_len、batch_size、lr这几个数怎么联动训练时间序列预测模型,参数之间不是孤立的。seq_len 决定模型看到多长的历史,但它也直接决定样本数量,seq_len 越大,能切出来的样本越少;batch_size 越大,梯度越稳定,但内存压力也越大;学习率往往要根据模型和批量大小一起调。Transformer 对学习率更敏感,同样数据下我用 3e-4 能训,换成 LSTM 可以稍微加大到 5e-4 或 1e-3。参数建议范围影响seq_len8~128上下文长度,也影响样本数pred_len1~24多步预测时误差会累积batch_size16~128太小 loss 不稳,太大显存不够lr1e-4~1e-3Transformer 通常比 LSTM 更需要小学习率hidden_size / d_model32~128模型容量,小数据别贪大num_layers2~4层数增加拟合能力,也增加过拟合风险dropout0.1~0.3防止过拟合,尤其是 Transformer源码包里的 config.py 一般写成这样:# config.py device = "cuda" if torch.cuda.is_available() else "cpu" seq_len = 24 pred_len = 1 batch_size = 64 lr = 3e-4 epochs = 100 hidden_size = 64 num_layers = 2 d_model = 64 nhead = 4 dropout = 0.2 seed = 42这里面需要特别注意 seq_len 和 batch_size 的联动:如果你把 seq_len 从 24 调到 48,样本量会明显减少,这时候 batch_size 可以适当调小,否则一个 epoch 里更新次数太少,模型收敛变慢。学习率在 batch_size 调大时要对应降低,常见的经验是 batch_size 翻倍,lr 可以乘以 0.8 左右,但这只是起点。3.2 训练主循环:早停、梯度裁剪与模型保存训练循环最值得你抄的其实是三句话:梯度裁剪、验证集早停、保存最优权重而不是最后一轮权重。时间序列模型在梯度回传时非常容易因为个别尖峰样本产生爆炸梯度,不裁剪的话 loss 会突然跳到 NaN。torch.manual_seed(seed) model = LSTMPredictor(...).to(device) # 或 TransformerPredictor optimizer = torch.optim.Adam(model.parameters(), lr=lr) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=5) loss_fn = nn.MSELoss() best_val_loss = float("inf") for epoch in range(epochs): model.train() train_loss = 0.0 for xb, yb in train_loader: xb, yb = xb.to(device), yb.to(device) optimizer.zero_grad() pred = model(xb) loss = loss_fn(pred, yb) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() train_loss += loss.item() val_loss = evaluate(model, val_loader) scheduler.step(val_loss) if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), "output/checkpoints/best.pth") print(f"epoch {epoch} train_loss={train_loss:.4f} val_loss={val_loss:.4f}")clip_grad_norm_ 的 max_norm 我一般取 1.0 到 2.0,太小会拖慢收敛,太大就没有保护作用。保存 state_dict 而不是整个 model,好处是后续做模型对比时可以很方便地把同一个结构加载到新的类里。ReduceLROnPlateau 是这套代码里最实用的调度器,它会看验证集 loss,连续 patience 轮不下降就降低学习率,能省下很多手动调 lr 的时间。3.3 判断训练是否正常:loss曲线和预测图只看终端打印的 loss 不够,我每次训练完都会强制自己画两张图:一张是 train/val loss 曲线,一张是测试集前 200 个点的真实值对比预测值。很多模型 loss 看起来很漂亮,画出来才发现预测曲线就是真实值往右平移了一个点。import matplotlib.pyplot as plt def plot_prediction(y_true, y_pred, path="output/figures/pred.png"): plt.figure(figsize=(10, 4)) plt.plot(y_true[:200], label="true") plt.plot(y_pred[:200], label="pred") plt.legend() plt.savefig(path, dpi=150) plt.close()画图时需要注意 y_true 和 y_pred 都必须是反归一化之后的值,否则标准化过的数据画出来,你根本看不出预测值是不是落在真实量纲的合理范围内。如果 val_loss 比 train_loss 明显高一大截,基本可以判定过拟合,先去调 dropout 或者减少 num_layers;如果 train_loss 根本不降,先检查学习率和归一化是否生效,而不是急着调模型结构。4. 调参对比:LSTM 和 Transformer 在时间序列上的真实差距4.1 收敛速度与过拟合:该盯训练loss还是验证loss在模拟项目X的逐小时数据上,我用同一份数据、同一个训练轮数做过多次对比,整体规律很稳定:LSTM 在 30 到 50 轮左右开始收敛,Transformer 往往要跑到 80 轮以上。更值得警惕的是过拟合,Transformer 在小样本上训练 loss 会下降得很快,但验证 loss 经常在第 40 轮左右开始反弹,而 LSTM 的过拟合来得更晚。所以对比两个模型时,千万别只看最后一个 epoch 的结果。正确做法是记录每个 epoch 的验证 loss,取最小值对应的权重做测试,同时打印那个最小值出现的 epoch。如果 Transformer 的 best val loss 出现在第 90 轮,而 LSTM 在第 35 轮,那 Transformer 的“更好”是以多出一倍多训练轮数为代价的,这个成本要在技术选型里算清楚。4.2 Transformer 的四个必调参数:d_model、nhead、num_layers、dropout的边界Transformer 参数比 LSTM 多,但真正需要反复动的就四个:d_model、nhead、num_layers 和 dropout。d_model 是自注意力向量的维度,它必须能被 nhead 整除,这是 PyTorch 里最常见的报错来源。nhead 一般取 4 或 8,小数据上取 4 更稳;num_layers 推荐 2 到 3,超过 4 层在小样本上几乎必过拟合。dropout 我建议从 0.1 起步,验证集 loss 反弹就加大到 0.2 或 0.3。d_model = 64 nhead = 8 # 必须保证 d_model % nhead == 0 num_layers = 3 dropout = 0.2 # dim_feedforward 默认是 d_model * 4,源码包里通常保留默认这里有一个隐藏陷阱:如果你把 d_model 从 64 调到 128,dim_feedforward 如果不跟着改,默认还是 512,模型参数量会瞬间翻倍,训练时间增加非常多,但预测精度未必提升。我的经验是样本量不到一万时,d_model 64 和 128 的差距通常小于 0.5%,完全没必要追大模型。4.3 数据规模与序列长度:什么时候Transformer不值得上判断用 LSTM 还是 Transformer,不需要看论文里的理论优势,直接看三个条件:样本量、序列长度、预测步长。样本少于 5000 条,Transformer 很难发挥注意力机制的全局建模能力,反而因为参数量大更容易过拟合;序列长度小于 50,注意力能看到的上下文有限,优势同样不明显;如果是多步递归预测,LSTM 的误差累积通常更可控。场景推荐模型原因样本 < 5000LSTMTransformer 参数量大,容易过拟合样本 > 10000 且 seq_len > 64Transformer长序列下注意力优势明显单步预测差距不大模型容量不是主要瓶颈多步递归预测LSTM 更稳误差累积对注意力扰动更敏感这个表不是绝对的,但它能帮你在一开始就把精力放在正确的模型上。我见过不少开发者拿着上千条数据硬上 Transformer,调了三天参数还不如 LSTM 跑一个默认配置,问题就出在选型前置条件没想清楚。5. 复现与部署避坑指南:时间序列预测源码里最常见的5个坑5.1 预测曲线整体滞后一个点现象:验证集 loss 很低,但把预测结果画出来,发现曲线明显比真实值晚了一个时间步,峰值全部向右错位。原因:单步预测任务中,模型学到的最省力策略是“用上一个时刻的值近似下一个时刻”,尤其数据连续性强时,这种滞后预测的 loss 并不会很大。解决:第一,用差分数据训练,把原始值变成相邻时刻的差值,模型必须学会变化趋势而不是抄上一时刻;第二,评估指标不要只用 MSE,加一个对滞后敏感的指标,或者直接看多步预测结果;第三,检查数据是否被误排序过,比如 pandas 的 sort_values 把原本的时间顺序打乱,就会造成这种看起来正确其实是泄漏的表现。5.2 训练到一半 loss 变成 NaN现象:前几十个 epoch loss 正常下降,突然某一个 batch 后 loss 变成 nan,后续训练无法恢复。原因:最常见的三个来源是数据里有 inf 或缺失值、学习率过大、模型梯度爆炸。时间序列原始数据里偶尔会出现空值,如果用某些方式填充成极大值,归一化后 std 可能变成异常值。解决:在预处理脚本里加一行np.isfinite(values).all()确保没有异常值;把学习率从 3e-3 降到 3e-4;训练循环里加上梯度裁剪clip_grad_norm_。如果是 Transformer,还要检查序列里有没有 padding 位置参与了注意力计算,未加 mask 的话 padding 位置会放大梯度。5.3 Transformer 训练比 LSTM 慢好几倍现象:同一份数据,Transformer 每个 epoch 的耗时是 LSTM 的三到五倍,显卡利用率还不高。原因:自注意力复杂度是 O(seq_len^2),seq_len 越长差距越明显;另外 TransformerEncoderLayer 内部包含两次 layer norm 和一次前馈网络,计算量本身就大。解决:如果序列长度没超过 64,LSTM 通常更划算。非要上 Transformer 的话,先把 num_layers 降到 2,dim_feedforward 降到 128,d_model 降到 32,很多任务不会因此变差。GPU 上可以试一下 torch.compile,但对小模型提升有时候只有 10%,不值得为了它折腾环境。5.4 预测结果几乎是一条水平线现象:测试集上预测值全部落在真实值均值附近,完全看不到波动,峰值被抹平。原因:回归任务用 MSE,模型对离群值惩罚很大,会倾向于输出更安全的条件均值;另一种可能是输出层被误加了 Sigmoid,导致输出被压缩到 0 到 1 之间;还有一种情况是标签做了归一化,但训练代码忘了对预测做反归一化,导致画图时所有值都挤在零附近。解决:检查模型最后的线性层是否被激活函数覆盖;确认评估脚本里对预测值和真实值用了同一组 mean 和 std 做还原;如果数据方差大,可以考虑 HuberLoss 代替 MSE,它不会让模型为了几个尖峰牺牲整体波动。5.5 复现指标和源码包描述对不上现象:在本地跑出来的结果比描述里的差很多,有时候换一台机器结果也不同。原因:随机种子没有固定、DataLoader 里 shuffle=True 导致训练样本顺序变化、GPU 上的浮点计算本身存在不确定的累加顺序、PyTorch 版本差异也会改变 Transformer 的实现细节。解决:在训练入口最前面固定所有随机源,代码可以这样写:def seed_everything(seed): import random, numpy as np, torch random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)DataLoader 里要传generator=torch.Generator().manual_seed(seed),并且设置shuffle=False或固定 generator,否则即使固定了脚本里的 seed,每次加载数据的顺序还是可能变。记录环境版本比重新训练更重要,我一般会把 PyTorch 版本、操作系统、GPU 型号直接写在实验记录文本里,方便后面排查差异。6. 把源码改成自己的数据:递归多步预测与反归一化6.1 用训练好的模型做递归多步预测:一个可套用的脚本源码包里的模型输出 pred_len 个点,但实际业务里往往需要预测未来更长的区间。常见做法是递归预测:把模型输出的第一个点拼到历史窗口末尾,去掉最旧的点,再继续预测下一个点,直到生成想要的长度。这个脚本可以直接套到训练好的 LSTM 或 Transformer 上:def recursive_predict(model, history, pred_len, device): model.eval() preds = [] x = torch.tensor(history, dtype=torch.float32).unsqueeze(0).to(device) with torch.no_grad(): for _ in range(pred_len): y = model(x) # y: (1, pred_len) pred = y[0, -1].item() preds.append(pred) new_val = torch.tensor([pred], dtype=torch.float32).to(device) new_val = new_val.unsqueeze(0).unsqueeze(-1) x = torch.cat([x[:, 1:, :], new_val], dim=1) return torch.tensor(preds)这里面有两个容易踩的地方。历史窗口必须和训练时的 seq_len 一致,否则维度拼接会报错;输出结果 y 取y[0, -1]是因为模型多步输出时最后一个位置通常聚合了更多信息,实际效果可以试。递归预测的误差会随着步长累积,预测 24 步以后曲线往往会变成一条斜线,这时候不要硬调递归,而是改成窗口滚动的方式,每预测一个点就重新从真实数据里取新的历史窗,业务允许延迟时这种半滚动方式更准。我之前吃过一次亏,模型训练完直接拿标准化后的输出画图,所有预测值都缩在零附近,后来才想起要对预测做反归一化。现在我的习惯是:把还原真实尺度的逻辑单独写成一个函数,放在 predict 脚本里,训练脚本永远只输出标准化后的数值。这样不管你换了哪份数据,画图之前都会先经过反归一化,不会再犯这个低级错误。希望帮到你。本文还有配套的精品资源,点击获取
简介:面向时间序列预测任务的LSTM+Transformer混合模型PyTorch实现,适合需要多特征输入、单变量输出场景的研究者与开发者;可直接迁移到风电、光伏、寿命预测、浓度预测等项目中,也适合作为学习Transformer与LSTM结合的入门范例。压缩包共160个文件、约1.94MB,以31个Python源码和87个pyc编译文件为主,另含4个csv示例数据、xml工程配置、txt说明及备份文件,目录结构清晰,便于按模型定义、数据加载、训练评估等模块查阅。模型融合Transformer的全局建模能力与LSTM的时序记忆能力,代码注释完整,支持csv和xlsx格式读取;内置ETTh2、ETTm2等多组公开数据以及真实值与预测值对比文件,替换数据即可完成训练与验证。已有145人学习,可作为时间序列预测的完整参考实现,覆盖从数据处理、模型训练到结果评估的完整链路,也能快速改造用于自有数据集。1. 时间序列预测选型:LSTM与Transformer为什么总被放在一起比在拿到一套带完整源码和数据的 LSTM Transformer 时间序列预测项目时,我通常先不急着跑训练,而是先看它把两个模型放在一起对比的边界条件。很多人默认 Transformer 一定更强,但换到小样本、强噪声的时间序列上,LSTM 反而更稳;而数据量足够大、序列足够长时,Transformer 的多头注意力才能体现出优势。这篇笔记会从源码目录、数据预处理、训练参数、调参对比和常见坑几个层面,把这个对比实验完整拆开,适合正在做预测、又想在两个模型之间做技术选型的开发者。这里说的“完整源码和数据”,核心价值不是两个模型类的实现,而是数据怎么切、参数怎么设、坑在哪里,正好是新手最容易卡住的地方。2. 源码包的文件结构与数据格式:动手前先核对这三处2.1 目录设计与数据文件说明:先确认你拿到的是完整源码一个能直接复现的时间序列预测源码,文件不应该只有模型定义,至少要有数据入口、预处理脚本和训练入口。我拿到压缩包先做三件事:打开 config.py 看全局参数;打开 data 下的 csv 看第一行和最后一行的时间;打开 train.py 看 DataLoader 有没有 shuffle。如果数据是从某天中间开始、时间间隔不固定,后面所有结果都没有意义。完整度比较高的源码包,目录通常长这样:project/ ├── config.py # 全局超参数 ├── train.py # 训练入口 ├── evaluate.py # 测试集评估与绘图 ├── data/ │ ├── raw_series.csv # 原始单变量时间序列 │ └── preprocess.py # 滑动窗口构造样本 ├── models/ │ ├── lstm_model.py # LSTM模型定义 │ ├── transformer_model.py # Transformer模型定义 │ └── __init__.py └── output/ ├── checkpoints/ # 模型权重 └── figures/ # loss曲线和预测对比图这里的核心是 data/preprocess.py 和 models 下的两个模型文件。如果只给了模型代码而没有数据预处理,你换自己的数据时还是要重写一遍,那这个源码包的复用价值就少了一半。我会先确认原始数据是不是只有时间和数值两列,列名是什么,时间间隔是小时还是分钟,因为后面构造滑动窗口时 seq_len 的单位完全跟着间隔走。时间戳缺失会造成隐性的样本错位,比数值异常更难发现。2.2 数据与预处理脚本:滑动窗口构造样本的写法时间序列预测本质上是把历史窗口映射到未来窗口的监督学习。常见做法是把一条长度为 N 的序列切成很多个长度为 seq_len 的输入,每个输入对应一个长度为 pred_len 的标签。这个步骤看似简单,但归一化顺序和时间顺序非常容易写错。# create_sequences.py import numpy as np import pandas as pd def create_sequences(data, seq_len=24, pred_len=1): xs, ys = [], [] for i in range(len(data) - seq_len - pred_len + 1): x = data[i : i + seq_len] y = data[i + seq_len : i + seq_len + pred_len] xs.append(x) ys.append(y) return np.array(xs), np.array(ys) df = pd.read_csv("data/raw_series.csv") values = df["value"].values.astype(np.float32) # 先归一化再划分,顺序不能反 mean, std = values.mean(), values.std() values = (values - mean) / std seq_len, pred_len = 24, 1 X, Y = create_sequences(values, seq_len, pred_len) # 按时间顺序划 70% / 15% / 15% split1 = int(len(X) * 0.7) split2 = int(len(X) * 0.85) X_train, Y_train = X[:split1], Y[:split1] X_val, Y_val = X[split1:split2], Y[split1:split2] X_test, Y_test = X[split2:], Y[split2:] print(X_train.shape, X_val.shape, X_test.shape)这段代码的关键点有三个。第一,先做归一化再切序列,不能先切训练集再各自算 mean 和 std,否则验证集和测试集的统计信息会泄漏到训练过程里。第二,划分必须按时间顺序,绝对不能用 sklearn 的 train_test_split 随机打乱,因为时间序列一旦打乱,模型会看到未来数据,验证集分数会虚高。第三,seq_len 最好覆盖一个完整周期,小时数据至少 24,日数据至少 7,否则模型学不到周期性。2.3 LSTM和Transformer的模型定义:输入输出怎么对齐两个模型在源码包里的定义都比较直接,核心是理解输入维度是 (batch, seq_len, input_size),输出维度是 (batch, pred_len)。LSTM 取最后一个时间步的隐藏状态接线性层,Transformer 则先做线性升维,再经过自注意力编码器,同样只取最后一个位置输出。# models/lstm_model.py import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, pred_len=1): super().__init__() self.lstm = nn.LSTM(input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=0.2) self.reg = nn.Linear(hidden_size, pred_len) def forward(self, x): # x shape: (batch, seq_len, input_size) out, _ = self.lstm(x) # out: (batch, seq_len, hidden_size) return self.reg(out[:, -1, :]) # 取最后一个时间步# models/transformer_model.py import torch import torch.nn as nn class TransformerPredictor(nn.Module): def __init__(self, input_size=1, d_model=64, nhead=4, num_layers=2, pred_len=1): super().__init__() self.embed = nn.Linear(input_size, d_model) encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, dim_feedforward=256, dropout=0.1, activation="relu", batch_first=True) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) self.reg = nn.Linear(d_model, pred_len) def forward(self, x): # x shape: (batch, seq_len, input_size) src = self.embed(x) # 线性升维到 d_model memory = self.encoder(src) # 自注意力编码 return self.reg(memory[:, -1, :])需要注意两个实现上的细节。Transformer 里 batch_first=True 必须显式设置,否则输入输出维度顺序会和 LSTM 混在一起,排查起来很耗时间。另一个是位置编码问题,上面这个简写版本没有加位置编码,自注意力层本身不带顺序信息,所以完整源码里通常会在 embed 之后加一个 sinusoidal positional encoding,否则序列顺序对模型来说是完全对称的。实际跑的时候,我会先用 LSTM 确认数据和 loss 流程没问题,再切到 Transformer,这样能把模型实现的 bug 和数据问题分开。3. 训练跑通:参数配置、训练循环与最小复现命令3.1 参数配置:seq_len、batch_size、lr这几个数怎么联动训练时间序列预测模型,参数之间不是孤立的。seq_len 决定模型看到多长的历史,但它也直接决定样本数量,seq_len 越大,能切出来的样本越少;batch_size 越大,梯度越稳定,但内存压力也越大;学习率往往要根据模型和批量大小一起调。Transformer 对学习率更敏感,同样数据下我用 3e-4 能训,换成 LSTM 可以稍微加大到 5e-4 或 1e-3。参数建议范围影响seq_len8~128上下文长度,也影响样本数pred_len1~24多步预测时误差会累积batch_size16~128太小 loss 不稳,太大显存不够lr1e-4~1e-3Transformer 通常比 LSTM 更需要小学习率hidden_size / d_model32~128模型容量,小数据别贪大num_layers2~4层数增加拟合能力,也增加过拟合风险dropout0.1~0.3防止过拟合,尤其是 Transformer源码包里的 config.py 一般写成这样:# config.py device = "cuda" if torch.cuda.is_available() else "cpu" seq_len = 24 pred_len = 1 batch_size = 64 lr = 3e-4 epochs = 100 hidden_size = 64 num_layers = 2 d_model = 64 nhead = 4 dropout = 0.2 seed = 42这里面需要特别注意 seq_len 和 batch_size 的联动:如果你把 seq_len 从 24 调到 48,样本量会明显减少,这时候 batch_size 可以适当调小,否则一个 epoch 里更新次数太少,模型收敛变慢。学习率在 batch_size 调大时要对应降低,常见的经验是 batch_size 翻倍,lr 可以乘以 0.8 左右,但这只是起点。3.2 训练主循环:早停、梯度裁剪与模型保存训练循环最值得你抄的其实是三句话:梯度裁剪、验证集早停、保存最优权重而不是最后一轮权重。时间序列模型在梯度回传时非常容易因为个别尖峰样本产生爆炸梯度,不裁剪的话 loss 会突然跳到 NaN。torch.manual_seed(seed) model = LSTMPredictor(...).to(device) # 或 TransformerPredictor optimizer = torch.optim.Adam(model.parameters(), lr=lr) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=5) loss_fn = nn.MSELoss() best_val_loss = float("inf") for epoch in range(epochs): model.train() train_loss = 0.0 for xb, yb in train_loader: xb, yb = xb.to(device), yb.to(device) optimizer.zero_grad() pred = model(xb) loss = loss_fn(pred, yb) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() train_loss += loss.item() val_loss = evaluate(model, val_loader) scheduler.step(val_loss) if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), "output/checkpoints/best.pth") print(f"epoch {epoch} train_loss={train_loss:.4f} val_loss={val_loss:.4f}")clip_grad_norm_ 的 max_norm 我一般取 1.0 到 2.0,太小会拖慢收敛,太大就没有保护作用。保存 state_dict 而不是整个 model,好处是后续做模型对比时可以很方便地把同一个结构加载到新的类里。ReduceLROnPlateau 是这套代码里最实用的调度器,它会看验证集 loss,连续 patience 轮不下降就降低学习率,能省下很多手动调 lr 的时间。3.3 判断训练是否正常:loss曲线和预测图只看终端打印的 loss 不够,我每次训练完都会强制自己画两张图:一张是 train/val loss 曲线,一张是测试集前 200 个点的真实值对比预测值。很多模型 loss 看起来很漂亮,画出来才发现预测曲线就是真实值往右平移了一个点。import matplotlib.pyplot as plt def plot_prediction(y_true, y_pred, path="output/figures/pred.png"): plt.figure(figsize=(10, 4)) plt.plot(y_true[:200], label="true") plt.plot(y_pred[:200], label="pred") plt.legend() plt.savefig(path, dpi=150) plt.close()画图时需要注意 y_true 和 y_pred 都必须是反归一化之后的值,否则标准化过的数据画出来,你根本看不出预测值是不是落在真实量纲的合理范围内。如果 val_loss 比 train_loss 明显高一大截,基本可以判定过拟合,先去调 dropout 或者减少 num_layers;如果 train_loss 根本不降,先检查学习率和归一化是否生效,而不是急着调模型结构。4. 调参对比:LSTM 和 Transformer 在时间序列上的真实差距4.1 收敛速度与过拟合:该盯训练loss还是验证loss在模拟项目X的逐小时数据上,我用同一份数据、同一个训练轮数做过多次对比,整体规律很稳定:LSTM 在 30 到 50 轮左右开始收敛,Transformer 往往要跑到 80 轮以上。更值得警惕的是过拟合,Transformer 在小样本上训练 loss 会下降得很快,但验证 loss 经常在第 40 轮左右开始反弹,而 LSTM 的过拟合来得更晚。所以对比两个模型时,千万别只看最后一个 epoch 的结果。正确做法是记录每个 epoch 的验证 loss,取最小值对应的权重做测试,同时打印那个最小值出现的 epoch。如果 Transformer 的 best val loss 出现在第 90 轮,而 LSTM 在第 35 轮,那 Transformer 的“更好”是以多出一倍多训练轮数为代价的,这个成本要在技术选型里算清楚。4.2 Transformer 的四个必调参数:d_model、nhead、num_layers、dropout的边界Transformer 参数比 LSTM 多,但真正需要反复动的就四个:d_model、nhead、num_layers 和 dropout。d_model 是自注意力向量的维度,它必须能被 nhead 整除,这是 PyTorch 里最常见的报错来源。nhead 一般取 4 或 8,小数据上取 4 更稳;num_layers 推荐 2 到 3,超过 4 层在小样本上几乎必过拟合。dropout 我建议从 0.1 起步,验证集 loss 反弹就加大到 0.2 或 0.3。d_model = 64 nhead = 8 # 必须保证 d_model % nhead == 0 num_layers = 3 dropout = 0.2 # dim_feedforward 默认是 d_model * 4,源码包里通常保留默认这里有一个隐藏陷阱:如果你把 d_model 从 64 调到 128,dim_feedforward 如果不跟着改,默认还是 512,模型参数量会瞬间翻倍,训练时间增加非常多,但预测精度未必提升。我的经验是样本量不到一万时,d_model 64 和 128 的差距通常小于 0.5%,完全没必要追大模型。4.3 数据规模与序列长度:什么时候Transformer不值得上判断用 LSTM 还是 Transformer,不需要看论文里的理论优势,直接看三个条件:样本量、序列长度、预测步长。样本少于 5000 条,Transformer 很难发挥注意力机制的全局建模能力,反而因为参数量大更容易过拟合;序列长度小于 50,注意力能看到的上下文有限,优势同样不明显;如果是多步递归预测,LSTM 的误差累积通常更可控。场景推荐模型原因样本 < 5000LSTMTransformer 参数量大,容易过拟合样本 > 10000 且 seq_len > 64Transformer长序列下注意力优势明显单步预测差距不大模型容量不是主要瓶颈多步递归预测LSTM 更稳误差累积对注意力扰动更敏感这个表不是绝对的,但它能帮你在一开始就把精力放在正确的模型上。我见过不少开发者拿着上千条数据硬上 Transformer,调了三天参数还不如 LSTM 跑一个默认配置,问题就出在选型前置条件没想清楚。5. 复现与部署避坑指南:时间序列预测源码里最常见的5个坑5.1 预测曲线整体滞后一个点现象:验证集 loss 很低,但把预测结果画出来,发现曲线明显比真实值晚了一个时间步,峰值全部向右错位。原因:单步预测任务中,模型学到的最省力策略是“用上一个时刻的值近似下一个时刻”,尤其数据连续性强时,这种滞后预测的 loss 并不会很大。解决:第一,用差分数据训练,把原始值变成相邻时刻的差值,模型必须学会变化趋势而不是抄上一时刻;第二,评估指标不要只用 MSE,加一个对滞后敏感的指标,或者直接看多步预测结果;第三,检查数据是否被误排序过,比如 pandas 的 sort_values 把原本的时间顺序打乱,就会造成这种看起来正确其实是泄漏的表现。5.2 训练到一半 loss 变成 NaN现象:前几十个 epoch loss 正常下降,突然某一个 batch 后 loss 变成 nan,后续训练无法恢复。原因:最常见的三个来源是数据里有 inf 或缺失值、学习率过大、模型梯度爆炸。时间序列原始数据里偶尔会出现空值,如果用某些方式填充成极大值,归一化后 std 可能变成异常值。解决:在预处理脚本里加一行np.isfinite(values).all()确保没有异常值;把学习率从 3e-3 降到 3e-4;训练循环里加上梯度裁剪clip_grad_norm_。如果是 Transformer,还要检查序列里有没有 padding 位置参与了注意力计算,未加 mask 的话 padding 位置会放大梯度。5.3 Transformer 训练比 LSTM 慢好几倍现象:同一份数据,Transformer 每个 epoch 的耗时是 LSTM 的三到五倍,显卡利用率还不高。原因:自注意力复杂度是 O(seq_len^2),seq_len 越长差距越明显;另外 TransformerEncoderLayer 内部包含两次 layer norm 和一次前馈网络,计算量本身就大。解决:如果序列长度没超过 64,LSTM 通常更划算。非要上 Transformer 的话,先把 num_layers 降到 2,dim_feedforward 降到 128,d_model 降到 32,很多任务不会因此变差。GPU 上可以试一下 torch.compile,但对小模型提升有时候只有 10%,不值得为了它折腾环境。5.4 预测结果几乎是一条水平线现象:测试集上预测值全部落在真实值均值附近,完全看不到波动,峰值被抹平。原因:回归任务用 MSE,模型对离群值惩罚很大,会倾向于输出更安全的条件均值;另一种可能是输出层被误加了 Sigmoid,导致输出被压缩到 0 到 1 之间;还有一种情况是标签做了归一化,但训练代码忘了对预测做反归一化,导致画图时所有值都挤在零附近。解决:检查模型最后的线性层是否被激活函数覆盖;确认评估脚本里对预测值和真实值用了同一组 mean 和 std 做还原;如果数据方差大,可以考虑 HuberLoss 代替 MSE,它不会让模型为了几个尖峰牺牲整体波动。5.5 复现指标和源码包描述对不上现象:在本地跑出来的结果比描述里的差很多,有时候换一台机器结果也不同。原因:随机种子没有固定、DataLoader 里 shuffle=True 导致训练样本顺序变化、GPU 上的浮点计算本身存在不确定的累加顺序、PyTorch 版本差异也会改变 Transformer 的实现细节。解决:在训练入口最前面固定所有随机源,代码可以这样写:def seed_everything(seed): import random, numpy as np, torch random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)DataLoader 里要传generator=torch.Generator().manual_seed(seed),并且设置shuffle=False或固定 generator,否则即使固定了脚本里的 seed,每次加载数据的顺序还是可能变。记录环境版本比重新训练更重要,我一般会把 PyTorch 版本、操作系统、GPU 型号直接写在实验记录文本里,方便后面排查差异。6. 把源码改成自己的数据:递归多步预测与反归一化6.1 用训练好的模型做递归多步预测:一个可套用的脚本源码包里的模型输出 pred_len 个点,但实际业务里往往需要预测未来更长的区间。常见做法是递归预测:把模型输出的第一个点拼到历史窗口末尾,去掉最旧的点,再继续预测下一个点,直到生成想要的长度。这个脚本可以直接套到训练好的 LSTM 或 Transformer 上:def recursive_predict(model, history, pred_len, device): model.eval() preds = [] x = torch.tensor(history, dtype=torch.float32).unsqueeze(0).to(device) with torch.no_grad(): for _ in range(pred_len): y = model(x) # y: (1, pred_len) pred = y[0, -1].item() preds.append(pred) new_val = torch.tensor([pred], dtype=torch.float32).to(device) new_val = new_val.unsqueeze(0).unsqueeze(-1) x = torch.cat([x[:, 1:, :], new_val], dim=1) return torch.tensor(preds)这里面有两个容易踩的地方。历史窗口必须和训练时的 seq_len 一致,否则维度拼接会报错;输出结果 y 取y[0, -1]是因为模型多步输出时最后一个位置通常聚合了更多信息,实际效果可以试。递归预测的误差会随着步长累积,预测 24 步以后曲线往往会变成一条斜线,这时候不要硬调递归,而是改成窗口滚动的方式,每预测一个点就重新从真实数据里取新的历史窗,业务允许延迟时这种半滚动方式更准。我之前吃过一次亏,模型训练完直接拿标准化后的输出画图,所有预测值都缩在零附近,后来才想起要对预测做反归一化。现在我的习惯是:把还原真实尺度的逻辑单独写成一个函数,放在 predict 脚本里,训练脚本永远只输出标准化后的数值。这样不管你换了哪份数据,画图之前都会先经过反归一化,不会再犯这个低级错误。希望帮到你。本文还有配套的精品资源,点击获取