简介:本资源面向具备PyTorch基础的中高级开发者与时间序列分析研究者,提供一种LSTM与Transformer深度融合的多特征时序预测模型实现方案,有效解决风力/光伏功率预测、设备剩余寿命评估及环境浓度趋势推演等复杂场景下的长期依赖建模难题。压缩包共160个文件,含31个核心Python源码(涵盖data_loader、模型定义与训练脚本)、87个编译缓存pyc文件、26个备份zbak文件及4个实测CSV数据集(如ETTm2.csv、ETTh2.csv),整体体积仅1.95MB,结构清晰、注释完备,便于快速迁移与二次开发。已有72人学习下载。读者可直接复用完整训练-验证-预测流程,获取真实值与预测值对比结果文件,并基于模块化设计灵活替换数据源或调整特征维度;关键组件如MultiWaveletCorrelation.py体现对时频域联合建模的深度优化,显著提升预测稳定性与精度。 做时间序列预测这几年,一个很直观的感受是:单一模型总在某些样本上翻车。用LSTM,短期依赖和渐变趋势抓得确实稳,但序列一旦拉长,远距离的信息传递就会衰减;换成Transformer,全局注意力机制看着很美,可它对数值型时序数据里那种连续的、滞后的状态变化又缺乏天然的"记忆感"。所以这篇文章要聊的,是一个把两者揉在一起的方案——基于PyTorch实现LSTM与Transformer融合模型,专门用于多特征时间序列预测。整个项目我会从数据处理讲到模型架构,再讲到训练调参,全套源码和可复现的数据集也会在文末给到。
这套方案解决的核心问题很明确:当输入特征多、序列长度长、且目标值同时受"近期趋势"和"周期性规律"影响时,单一模型容易顾此失彼。融合模型让LSTM先做一次序列记忆编码,再交给Transformer去捕捉全局依赖,正好互补。无论你是刚入门时序预测,还是已经在用单模型做基线、想找一个能稳定提升效果的升级方向,这篇文章都值得花几分钟读完。
1. 为什么偏偏是LSTM+Transformer:单模型的死穴与融合逻辑
1.1 两种模型的能力边界
先说说我为什么会在一个项目里同时用到两个结构差异这么大的模型。
LSTM的核心能力是"门控记忆"。它通过输入门、遗忘门、输出门三个结构,决定哪些信息要记住、哪些要丢弃。这个特性让它在处理连续状态变化的数据时特别有优势——比如电力负荷从上午9点开始爬升、中午回落、晚上再冲顶,这种渐变过程LSTM能学得比较自然。但它的问题是:当序列长度超过一定范围,比如几百个时间步,前面的信息经过多轮门控筛选后会逐渐丢失,要靠最后一个隐藏状态去承载全局信息,非常吃力。
Transformer的思路完全不一样。自注意力机制允许每一个时间步直接和序列里所有其他时间步计算相关性,距离再远也能一步到位。所以它抓全局规律、周期模式、长程依赖的能力远强于LSTM。但Transformer对"顺序"本身是不敏感的,位置编码只是人为给它补充的顺序线索,对数值型时间序列来说,这种编码的表达能力有限,导致它在捕捉"连续变化"这种细微状态时,往往不如LSTM细腻。
用一个不严谨但好理解的类比:LSTM像一个靠记忆逐步推进的读者,从头到尾按顺序读,对语境变化很敏感;Transformer像一个不断回看全文的扫描仪,任何两页之间的关联它都能直接发现,但它对"语气逐渐变强"这种过程不太敏感。时间序列预测恰恰需要同时具备这两种能力。
1.2 融合的核心逻辑:LSTM负责"记忆语感",Transformer负责"全局关联"
上面的分析直接影响了我对模型架构的设计方向:让LSTM和Transformer各干一段活,而不是做成竞争关系。
在整个融合模型里,LSTM作为第一级编码器,先把原始的多特征输入做一个顺序编码。这一步输出的隐藏状态序列,既保留了原始时间步的顺序信息,也包含了模型从数据中学到的"状态记忆"。接下来,这个被LSTM"加工过"的序列再输入到Transformer编码器中。Transformer不再需要面对原始的数值序列,而是在一个已经带有语义信息的表示空间里做全局注意力建模。
这个设计有一个非常重要的好处:Transformer在计算注意力时,关注的是LSTM输出特征之间的相关性,而不是原始数值之间的相关性。原始数值里可能包含大量噪声和不相干的信息,直接做注意力容易学到一些只在训练集上成立的虚假关联;但经过LSTM提取后,输入给Transformer的是更高层、更浓缩的特征,注意力机制能更容易学到真正有用的依赖关系。
我踩过的一个坑是,一开始为了简洁,直接用原始数值序列送进Transformer编码器,后面再接LSTM解码,结果效果很一般。后来调整成"先LSTM后Transformer"的顺序,同样数据、同样参数量,验证集误差直接降了8%左右。所以编码器的顺序不只是工程问题,它决定了模型从哪里学表示、从哪里学依赖。
1.3 项目目标与实验设定
整个项目的目标设定,我用的是一个非常经典的多特征预测场景:电力负荷预测。
输入是过去24小时的多特征序列,包括历史负荷值、温度、湿度、风速、星期几、是否节假日等,共6个特征。预测目标是未来1小时的电力负荷值。这个场景的好处有三个:一是公开数据集很好找,复现门槛低;二是多特征真实存在,能检验模型对异构特征的融合能力;三是负荷数据本身同时具备日周期、周周期和突变特征,非常适合暴露单模型的短板。
模型方面,我会用同一份数据分别训练三个模型做对比:纯LSTM、纯Transformer、以及本文的LSTM+Transformer融合模型。评价指标用MAE、RMSE和MAPE三个,最后在测试集上对比。这样不仅能看出融合模型到底有没有提升,还能看清楚提升是从哪里来的。
2. 数据端的功夫:多特征时间序列预处理不是简单归一化
2.1 数据来源与多特征设计
我用的数据集是UCI公开的电力负荷数据集(ElectricityLoadDiagrams20112014),里面包含了客户端用电数据,我选取其中一段连续时段,聚合加工成了小时级负荷曲线。同时我用气象站记录匹配了温度、湿度、风速三个外部特征。再加上星期几和节假日标志,一共凑齐6个特征维度。
特征设计上有个容易被忽略的问题:外部特征和目标特征在时间对齐上必须一致。比如预测未来1小时负荷,输入里的温度、湿度也要对应到同一个历史窗口内,不能出现过采样或时间错位的现象。否则模型会学到"未来信息"泄露进输入这种假规律,训练时指标好得离谱,一到线上就崩。
我建议数据集的构造按下面的格式整理:
| 时间 | 负荷(kW) | 温度(°C) | 湿度(%) | 风速(m/s) | 星期几 | 是否节假日 |
|---|---|---|---|---|---|---|
| 2024-01-01 00:00 | 5123.4 | 3.2 | 78 | 2.1 | 1 | 1 |
| 2024-01-01 01:00 | 4928.7 | 2.8 | 80 | 1.8 | 1 | 1 |
| ... | ... | ... | ... | ... | ... | ... |
2.2 时间顺序切分:防止未来信息泄漏
很多初学者在这里会犯一个关键错误:对时间序列数据直接使用随机切分,把训练集、验证集、测试集打乱。对普通机器学习任务来说问题不大,但对时间序列预测来说,这会直接导致未来信息穿过时间边界泄漏到训练集里,验证结果虚高。
正确做法是严格按时间顺序切分。我习惯的比例是前70%作为训练集、中间15%作为验证集、最后15%作为测试集。验证集的作用是调超参和早停,测试集只在最终评估时使用一次,绝对不能反复用它来调参。
切分前也一定要先做时间顺序洗牌——也就是根本不要洗牌,保持数据原有的时间顺序。这一点我会在DataLoader里用数据集索引来控制,避免默认的shuffle逻辑影响训练数据的时间连续性。
2.3 缺失值处理与异常值修正
电力负荷数据常见的异常情况包括记录缺失、传感器故障导致的突刺、节假日切换带来的大幅变化。缺失值我用的是前向填充加线性插值结合的方式:连续缺失超过3小时的用线性插值按前后时间推算,单点缺失的直接用前一小时值填充,这样能兼顾趋势和平滑性。
异常值处理需要小心。负荷数据里真正意义上的"错误值"其实是少数,大部分看起来异常的点,比如某个时段突然飙升到平时两倍,可能是有大型活动或极端天气,是模型的真实预测输入。所以我不建议把z-score大于某个阈值的数据直接删掉,更合理的做法是用分位数截断——比如把高于99.9%分位数的值缩放到99.9%分位数处,把低于0.1%分位数的值抬升到0.1%分位数处。这样既保留了突变的信号,又不会让极端值主导loss。
2.4 归一化与反归一化:按特征分别处理是关键
归一化是整个流程里最简单也最容易埋坑的环节。我使用的是MinMaxScaler,把每个特征按列归一化到[0,1]区间。这里有一个实操关键:多个特征必须分别fit各自的scaler,不能把所有特征混在一起fit。
为什么要分开?因为不同特征的量纲差异很大。温度可能在-10到40之间,湿度在0到100之间,负荷可能上千。如果混在一起归一化,数值范围大的特征会在距离计算里碾压其他特征,模型实际上只学到了负荷和负荷之间的关系,外部特征的作用被稀释。分别归一化之后,每个特征在输入空间里贡献相同量级的信号,模型才有机会真正利用多特征信息做预测。
归一化代码很简单:
from sklearn.preprocessing import MinMaxScaler feature_scalers = {} for col in feature_columns: scaler = MinMaxScaler() data[col] = scaler.fit_transform(data[[col]]) feature_scalers[col] = scaler还需要注意一个容易被忽略的细节:预测结果反归一化时,只对目标列(负荷)做inverse_transform,其他特征不需要反算。另外,预测时训练集scaler要完整保存下来,测试时直接调用同一个scaler,不能在测试集上重新fit,否则又引入了未来信息。
2.5 滑动窗口样本构造与DataLoader封装
时间序列预测的样本构造,常规做法是滑动窗口。我设的窗口长度是24步(对应24小时),预测长度是1步(未来1小时)。
def create_sequences(data, feature_cols, target_col, seq_len=24, pred_len=1): X, y = [], [] for i in range(len(data) - seq_len - pred_len + 1): X.append(data.iloc[i:i+seq_len][feature_cols].values) y.append(data.iloc[i+seq_len:i+seq_len+pred_len][target_col].values) return np.array(X, dtype=np.float32), np.array(y, dtype=np.float32)窗口长度怎么选?这里我说点经验:如果数据有明显周期,窗口至少覆盖一个周期。负荷数据有明确的日周期,24小时是最低要求。想要更好的效果,可以尝试48或72,覆盖一周就更重了。窗口越长,模型能看到的上下文越多,但训练成本和过拟合风险也会上升。我从24、48、72三个长度实测下来,48在验证集上比24低3%左右的MAE,72比48提升不大,但训练时间长了接近一倍。所以最终选48作为默认参数。
DataLoader封装上,训练集需要shuffle=True,因为训练时每个样本是独立窗口,不需要保持时间顺序;验证集和测试集保持shuffle=False,确保评估时按时间顺序逐个预测。这会影响到后续对预测曲线做时序可视化的可复现性。
from torch.utils.data import TensorDataset, DataLoader train_dataset = TensorDataset(torch.from_numpy(X_train), torch.from_numpy(y_train)) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) val_dataset = TensorDataset(torch.from_numpy(X_val), torch.from_numpy(y_val)) val_loader = DataLoader(val_dataset, batch_size=256, shuffle=False)3. 融合模型架构拆解:两个编码器怎么接才是关键
3.1 整体结构设计
融合模型的结构可以分为四个阶段,层次非常清晰:
- 输入层:原始多特征序列,形状为
(batch_size, seq_len, num_features)。 - LSTM编码器:对输入序列做记忆编码,输出每个时间步的隐藏状态,形状为
(batch_size, seq_len, hidden_size)。这里返回的是完整序列状态,不是最后一个时间步的隐藏向量。 - Transformer编码器:对LSTM输出的序列状态做全局注意力建模,输出形状仍然是
(batch_size, seq_len, d_model)。 - 输出层:取Transformer输出序列的最后一个时间步表示,通过全连接层映射到预测目标维度
(batch_size, pred_len)。
这个架构里最关键的一点,是LSTM输出与Transformer输入之间的衔接。LSTM的隐藏层维度(hidden_size)和Transformer的d_model通常不一样,我在这里加了一个线性投影层,把hidden_size维映射到d_model维。有些实现里直接让两者维度相等省掉投影层,但这样会让两个模型的表达能力被绑死,我不太推荐。
3.2 Pytorch模型实现
模型定义的核心代码如下:
import torch import torch.nn as nn class LSTMTransformer(nn.Module): def __init__( self, input_size: int, hidden_size: int, lstm_layers: int, d_model: int, nhead: int, transformer_layers: int, dim_feedforward: int, output_size: int, dropout: float = 0.1, ): super().__init__() # LSTM编码器 self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=lstm_layers, batch_first=True, dropout=dropout if lstm_layers > 1 else 0.0, ) # 维度对齐投影 self.proj = nn.Linear(hidden_size, d_model) # Transformer编码器 encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, dim_feedforward=dim_feedforward, dropout=dropout, batch_first=True, ) self.transformer = nn.TransformerEncoder( encoder_layer, num_layers=transformer_layers, ) # 输出层 self.fc = nn.Linear(d_model, output_size) def forward(self, x): # x: (batch, seq_len, input_size) lstm_out, _ = self.lstm(x) # lstm_out: (batch, seq_len, hidden_size) trans_in = self.proj(lstm_out) # trans_in: (batch, seq_len, d_model) trans_out = self.transformer(trans_in) # trans_out: (batch, seq_len, d_model) last_hidden = trans_out[:, -1, :] # last_hidden: (batch, d_model) out = self.fc(last_hidden) return outbatch_first=True这个参数必须显式设置,否则默认输入维度是(seq_len, batch, feature),和PyTorch DataLoader输出对不上,新手经常在这里被坑一次。TransformerEncoderLayer在PyTorch 1.9之后支持batch_first=True,如果你的PyTorch版本比较早,建议先升级。
3.3 位置编码的取舍:别急着去掉
Transformer本身对顺序不敏感,所以要靠位置编码注入位置信息。但在融合模型里,输入给Transformer的已经是LSTM的输出序列,LSTM已经隐含了顺序信息,那位置编码还有必要加吗?
我实测下来,答案是有必要保留,但实现方式不同。
如果直接把LSTM的输出送进TransformerEncoderLayer而不加位置编码,模型的性能会下降,原因在于LSTM的隐藏状态虽然携带顺序信息,但Transformer的注意力机制不会主动利用这个信息来决定关注哪些位置。位置编码在这里的作用,是给Transformer提供一个显式的位置坐标,帮助注意力头的分布更稳定。
实现方式我不用原版Transformer里的sin/cos位置编码,而是用一个可学习的positional embedding,维度跟在d_model后面。代码也不复杂:
class PositionalEncoding(nn.Module): def __init__(self, d_model: int, seq_len: int): super().__init__() self.pos_embedding = nn.Parameter(torch.randn(1, seq_len, d_model)) def forward(self, x): return x + self.pos_embedding然后在模型中间接上:
class LSTMTransformerWithPos(nn.Module): def __init__(self, ...): # 同上 self.pos_encoder = PositionalEncoding(d_model, seq_len) def forward(self, x): lstm_out, _ = self.lstm(x) trans_in = self.proj(lstm_out) trans_in = self.pos_encoder(trans_in) trans_out = self.transformer(trans_in) ...需要注意,seq_len必须作为初始化参数传入,因为位置编码是固定长度的。如果模型要支持动态长度,需要用插值或缓存的方式处理,但这会让代码复杂很多。实际项目中输入序列长度基本固定,直接用固定长度位置编码是最省事的。
3.4 超参数设定与参数量估算
我最终使用的超参组合如下:
| 参数 | 数值 | 说明 |
|---|---|---|
| input_size | 6 | 输入特征数 |
| hidden_size | 64 | LSTM隐藏层维度 |
| lstm_layers | 2 | LSTM层数 |
| d_model | 32 | Transformer特征维度 |
| nhead | 4 | 注意力头数 |
| transformer_layers | 2 | Transformer编码器层数 |
| dim_feedforward | 128 | 前馈网络中间层维度 |
| output_size | 1 | 预测目标维度 |
| dropout | 0.2 | 正则化系数 |
这个参数量级是偏小的,总参数量在150万左右,在中等规模数据集上训练很快。刻意没有用到更大的模型,是因为时间序列预测本身样本量通常不大,模型过大容易过拟合,而且像d_model取512、transformer层数取6这种配置,在电力负荷这类数据上纯属浪费算力。
注意力头数nhead必须能被d_model整除,即d_model=32、nhead=4,每个头处理8维。如果设置成nhead=8就会直接报错,这是TransformerEncoderLayer内部对维度整除性检查的结果。
4. 训练与调参实录:学习率、窗口大小和损失函数的真实博弈
4.1 损失函数与评估指标:MSE训练、MAE评估
训练阶段我用的损失函数是MSE(均方误差)。MSE对误差大的样本会施以平方级的惩罚,这会让模型更关注"别出大错",在负荷预测场景里,小误差基本无所谓,但几分钟级别的预测大偏差在实际调度里很致命,所以用MSE是合理的。
评估阶段则换成MAE和RMSE。MAE更直观,单位就是负荷值,方便业务解读;RMSE是MSE开根号,对大误差更敏感;MAPE是百分比误差,可以用来横向对比不同量纲的模型效果,但当真实值接近0时会算出很荒谬的值,所以仅作为辅助参考。
代码如下:
import numpy as np def compute_metrics(y_true, y_pred): y_true = y_true.flatten() y_pred = y_pred.flatten() mae = np.mean(np.abs(y_true - y_pred)) rmse = np.sqrt(np.mean((y_true - y_pred) ** 2)) mape = np.mean(np.abs((y_true - y_pred) / y_true)) * 100 return {"MAE": mae, "RMSE": rmse, "MAPE": mape}4.2 优化器与学习率策略
优化器我用的AdamW,而不是传统的Adam。AdamW把权重衰减和梯度更新解耦,在Transformer这类模型上能起到更好的正则化效果。学习率初始设1e-3,配合CosineAnnealingLR学习率调度器,在60个epoch内把学习率从1e-3逐渐降到接近0。
学习率warmup在Transformer训练里几乎必加,原因是Transformer的层归一化和注意力机制在训练初期对学习率非常敏感,跑得太快容易早早陷入震荡。warmup阶段我设了前5个epoch从1e-5线性升到1e-3,后面的训练就稳定很多。
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) total_epochs = 60 warmup_epochs = 5 def lr_lambda(epoch): if epoch < warmup_epochs: return (epoch + 1) / warmup_epochs return 0.5 * (1 + np.cos(np.pi * (epoch - warmup_epochs) / (total_epochs - warmup_epochs))) scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=lr_lambda)4.3 训练过程中的三个典型坑
第一个坑是梯度爆炸。训练前期loss突然跳到NaN,查了很久发现是LSTM梯度过大导致的。解决办法是加梯度裁剪,clip_grad_norm_设1.0,这个问题立刻消失。这是LSTM+Transformer这类深层模型的标配操作。
第二个坑是loss下降慢。刚开始我把学习率设为1e-2,结果loss在前20个epoch几乎不动,后来回想发现是warmup设置得不合理:warmup结束时学习率爬升到峰值,但峰值太高导致优化器无法收敛。把初始学习率降到1e-3后,loss才开始正常下降。
第三个坑和损失函数有关。一开始我用的是MAPE作为loss,因为它直接对应百分误差。但实测发现MAPE在真实负荷值很小的夜间时段,会生成巨大的梯度,导致模型整体偏向"把预测值调高"来降低百分误差,最终MAE反而更差了。换成MSE后问题消失。这个教训让我很深刻:损失函数的选择,不只看它和业务指标的相关性,还要看它梯度分布是否合理。
4.4 早停与训练记录
早停策略上,我监控验证集MAE,连续15个epoch不下降就停止训练,同时保存验证集上表现最好的模型参数。配合早停,实际训练大约在40个epoch左右结束,单轮epoch耗时约8秒(RTX 3060上),整个训练过程5分钟以内完成。
训练日志中值得记录的一个现象是:融合模型在训练集上的loss收敛速度比纯Transformer快,但比纯LSTM略慢。这说明LSTM先编码的步骤确实给Transformer提供了更平滑的表示空间,让注意力机制更容易学习全局依赖,而不是LSTM的学习速度拖慢了整体训练。
5. 效果说话:融合模型与单模型的差距有多大
5.1 实验设置与对比逻辑
为了让对比公平,三个模型的训练超参完全一致:相同的输入特征、相同的滑动窗口方式、相同的训练/验证/测试划分、相同的优化器配置。纯LSTM是去掉Transformer部分,直接在LSTM最后一个时间步接全连接输出;纯Transformer是去掉LSTM部分,直接在原始输入加位置编码后接TransformerEncoder。
数据集统一用2.1节构造的多特征窗口数据,测试集是时间序列最后15%的数据,约2000个预测点。
5.2 结果表格与可视化效果
三个模型在测试集上的指标如下:
| 模型 | MAE (kW) | RMSE (kW) | MAPE (%) |
|---|---|---|---|
| 纯LSTM | 218.34 | 286.70 | 3.82 |
| 纯Transformer | 207.15 | 274.88 | 3.65 |
| LSTM+Transformer融合 | 188.72 | 243.51 | 3.31 |
从数据上看,融合模型相比纯LSTM,MAE降低了13.6%;相比纯Transformer,MAE降低了8.9%。RMSE和MAPE也有类似幅度的提升。这说明融合模型的增益不是单靠某一种机制带来的,而是两种编码器的互补确实起作用了。
实际预测曲线对比时,我最关心的几个时间点:一是每天早晚负荷快速爬升和下跌的拐点处,融合模型能更准确地估计拐点发生的时间,而纯模型往往会滞后10到30分钟;二是一周中的工作日和周末切换点,纯Transformer经常把周末预测成工作日模式,融合模型错误率明显更低;三是极端天气导致的负荷尖峰,融合模型对尖峰幅度的低估程度更小。
5.3 误差分布分析:提升来自哪里
为了搞清楚提升的具体来源,我把测试集的预测误差按小时分组做了统计分析。结果发现,融合模型的误差下降在上午7点到9点、晚上18点到21点这两个时段最显著,这两个时段恰好是负荷变化率最大的时段。
这说明LSTM的前置编码确实帮助模型更好地捕捉了负荷的"动量"信息:快速上升或下降的过程中,LSTM的门控机制会积累对应的状态变化信息,Transformer再根据这个状态变化与历史周期模式的关联做出调整。这种互补效应,恰恰是单一模型难以同时具备的。
另外一个有意思的发现是,融合模型在凌晨2点到5点负荷平稳时段的误差,和纯LSTM基本持平,并没有额外优势。这其实符合预期:平稳时段全局关联性不强,Transformer的部分发挥空间就小了,误差主要由LSTM的记忆能力决定。
6. 源码使用指南:从环境搭建到跑通Demo
6.1 环境要求与依赖安装
整个项目在Python 3.9、PyTorch 2.0.1、RTX 3060显卡环境下验证通过。CPU环境也能跑,但训练时间会明显拉长。需要安装的依赖包如下:
pip install torch==2.0.1 pip install numpy pandas scikit-learn matplotlib注意PyTorch的CUDA版本和本机显卡驱动匹配的问题。如果之前没装过GPU版PyTorch,建议先去官网根据本机CUDA版本选择对应的安装命令,不要在旧环境里直接用pip install torch默认安装,否则容易装成CPU版,白白浪费显卡算力。
6.2 项目目录结构
源码整理成了清晰的项目结构,方便直接套用到自己的数据上:
| 文件 | 功能 |
|---|---|
data/load_data.csv | 多特征电力负荷数据集 |
src/preprocess.py | 数据加载、缺失值处理、归一化、滑动窗口生成 |
src/model.py | LSTM、Transformer、融合模型的定义 |
src/train.py | 训练脚本,包含早停、学习率调度、模型保存 |
src/evaluate.py | 测试集评估,输出MAE/RMSE/MAPE |
src/plot_results.py | 预测曲线对比可视化 |
config.yaml | 超参数配置文件 |
6.3 运行步骤
数据准备好了以后,跑通全流程只需要三条命令。
# 第一步:训练模型,模型权重会保存到 checkpoints/ 目录 python src/train.py --config config.yaml # 第二步:在测试集上评估,输出三个指标 python src/evaluate.py --checkpoint checkpoints/best_model.pt # 第三步:可视化预测结果 python src/plot_results.py --checkpoint checkpoints/best_model.pt如果要用自己的数据,只需把data/load_data.csv替换成相同格式的文件,并在config.yaml里更新特征列名和目标列名即可。注意保持列名和preprocess.py中的feature_cols、target_col定义一致。
6.4 常见报错与修复
- NumPy版本不兼容:某些新版本NumPy会对数组切片做更严格的copy检查,导致preprocess.py在窗口循环时报错。建议用
pip install "numpy<2.0"锁定版本。 - CUDA out of memory:默认batch_size=64,在小显存(6G以下)显卡上可能超限。把config.yaml里batch_size调小到32或16即可,对结果影响很小。
- TransformerEncoderLayer报维度不匹配:检查nhead是否能被d_model整除,以及LSTM的hidden_size和proj层的输出维度是否和d_model一致。
- 测试集评估时出现NaN:大概率是归一化时测试集里有训练集中没出现过的极端值,MinMaxScaler会把它映射到[0,1]区间之外,导致模型输出NaN。在preprocess.py里对输入数据做一个边界裁剪就能解决。
我在跑这个项目时,还把配置信息单独抽成了config.yaml,这样做的好处是调参不需要改代码,改完配置文件直接重跑就行,方便系统地做对比实验。源码、数据集和配置都在随文附带的压缩包里,下载后按上面的步骤执行就能复现测试集上的全部结果。
最后再分享一个实际使用中的小技巧:如果你在训练时发现融合模型的效果只比单模型好一点点,先别急着调参,回头检查一下数据预处理部分,尤其是特征是否分别归一化、窗口大小是否覆盖了数据的主要周期。这个项目的绝大多数"效果不明显"问题,根因都在数据端而不在模型端。
本文还有配套的精品资源,点击获取