TCN-BiLSTM多变量时序预测实战:从模型搭建到GUI部署
2026/9/24 12:19:06 网站建设 项目流程

简介:这份资源面向具备一定编程基础、对深度学习与时序数据分析感兴趣的开发者和研究人员,提供一套基于Python的多变量时序预测完整项目实例。模型将时间卷积神经网络(TCN)与双向长短期记忆网络(BiLSTM)相结合,用于提升金融、电力、气象、交通、生产及健康监测等场景下的预测精度,并缓解长序列建模中的梯度消失问题。资源包为1个docx文档,约65KB,内容涵盖项目背景与目标、挑战及解决方案、模型架构、代码示例、特点与创新、应用领域、数据生成、目录结构、部署应用及未来改进方向等模块,并配有GUI设计说明。文档强调数据预处理、特征选择与模型训练调优,同时给出系统架构、GPU加速、实时数据流处理、可视化界面及安全性等部署要点。目前已有171人学习,适合希望系统掌握TCN-BiLSTM多变量时序预测方案、对照代码与目录结构快速复现项目的读者参考。

1. TCN-BiLSTM 多变量时序预测:为什么单一模型总是差一口气

做过多变量时序预测的人大概都有过这种体验:LSTM 跑出来的曲线在平稳段还行,一到突变点就慢半拍;TCN 靠膨胀因果卷积抓长程依赖很稳,但遇到前后文强关联的序列(比如气象、电力负荷、工业传感器),单向结构对"未来信息回看"的利用始终不够。这不是调参能救的,是结构本身的边界。

TCN-BiLSTM 这套组合的思路很直接:用 TCN 的膨胀因果卷积先把多变量序列的局部模式和长程依赖抽出来,再把特征序列喂给 BiLSTM,让前向和后向两个方向的隐状态共同决定当前时刻的输出。多变量场景下,每个时间步不是一个数而是一个向量,通道之间的耦合关系靠 TCN 的残差块和 BiLSTM 的门控一起建模。这篇文章面向的是已经会写 Python、跑过 LSTM 或 TCN 单模型、想把这套组合真正落地到自己的数据集上的人——从数据窗口切分、模型搭建、训练调参,到 GUI 推理界面和踩坑排查,全部给到可复现的代码和参数说明。

2. TCN-BiLSTM 的结构拆解与选型理由

2.1 TCN 的膨胀因果卷积到底解决了什么

普通一维卷积做时序预测有两个硬伤:一是感受野随层数线性增长,想覆盖 100 个时间步就得堆很多层;二是卷积核会"看到"未来时刻,造成信息泄漏。TCN 用两个手段解决:因果卷积(causal convolution)保证 t 时刻只依赖 t 及之前的输入,膨胀卷积(dilated convolution)让感受野随层数指数增长。

膨胀因子一般取 2 的幂次:1, 2, 4, 8, …。第 k 层的感受野是 (kernel_size - 1) × dilation + 1,堆 L 层后的总感受野约为 2^L × (kernel_size - 1) + 1。kernel_size=3、层数 4 时,感受野能覆盖 2^4 × 2 + 1 = 33 个时间步,足够多数工业场景用。

每个残差块的结构是:膨胀因果卷积 → 权重归一化 → 激活(常用 GELU 或 ReLU)→ Dropout → 再一层同样的卷积 → 残差相加。残差连接是关键,没有它深层 TCN 梯度会衰减得很快。

import torch import torch.nn as nn class Chomp1d(nn.Module): """裁掉因果卷积右侧多出来的 padding,保证输出长度与输入一致""" def __init__(self, chomp_size): super().__init__() self.chomp_size = chomp_size def forward(self, x): return x[:, :, :-self.chomp_size].contiguous() class TemporalBlock(nn.Module): def __init__(self, n_inputs, n_outputs, kernel_size, stride, dilation, padding, dropout=0.2): super().__init__() # 第一层膨胀因果卷积 self.conv1 = nn.Conv1d(n_inputs, n_outputs, kernel_size, stride=stride, padding=padding, dilation=dilation) self.chomp1 = Chomp1d(padding) self.relu1 = nn.ReLU() self.dropout1 = nn.Dropout(dropout) # 第二层 self.conv2 = nn.Conv1d(n_outputs, n_outputs, kernel_size, stride=stride, padding=padding, dilation=dilation) self.chomp2 = Chomp1d(padding) self.relu2 = nn.ReLU() self.dropout2 = nn.Dropout(dropout) self.net = nn.Sequential(self.conv1, self.chomp1, self.relu1, self.dropout1, self.conv2, self.chomp2, self.relu2, self.dropout2) # 残差连接:输入输出通道不一致时用 1x1 卷积对齐 self.downsample = nn.Conv1d(n_inputs, n_outputs, 1) if n_inputs != n_outputs else None self.relu = nn.ReLU() self.init_weights() def init_weights(self): self.conv1.weight.data.normal_(0, 0.01) self.conv2.weight.data.normal_(0, 0.01) if self.downsample is not None: self.downsample.weight.data.normal_(0, 0.01) def forward(self, x): out = self.net(x) res = x if self.downsample is None else self.downsample(x) return self.relu(out + res)

这段代码里padding = (kernel_size - 1) * dilation是因果卷积的标准写法,右侧多出来的部分由Chomp1d裁掉。downsample处理通道数变化,避免残差相加时维度不匹配。Dropout 放在两层卷积之后,训练时随机丢弃一部分特征,防止过拟合——时序数据样本量通常不大,这一步不能省。

2.2 BiLSTM 为什么接在 TCN 后面而不是前面

顺序很关键。如果先 BiLSTM 再 TCN,BiLSTM 的双向结构会在原始输入上就引入"未来信息",而 TCN 的因果约束又要求不能看未来,两者语义冲突。正确做法是 TCN 先做特征提取,输出的特征序列已经编码了每个时刻的局部上下文,再交给 BiLSTM 做双向建模。

BiLSTM 的前向 LSTM 从 t=1 读到 t=T,后向 LSTM 从 t=T 读到 t=1,两个方向的隐状态拼接后送入全连接层。对于多变量预测,BiLSTM 的输入维度是 TCN 输出的通道数,输出维度是隐藏单元数 × 2(双向)。

class TCNBiLSTM(nn.Module): def __init__(self, num_inputs, num_channels, kernel_size=3, dropout=0.2, lstm_hidden=64, lstm_layers=2, output_size=1): super().__init__() # 构建 TCN 层 layers = [] num_levels = len(num_channels) for i in range(num_levels): dilation_size = 2 ** i in_channels = num_inputs if i == 0 else num_channels[i-1] out_channels = num_channels[i] layers.append(TemporalBlock(in_channels, out_channels, kernel_size, stride=1, dilation=dilation_size, padding=(kernel_size-1)*dilation_size, dropout=dropout)) self.tcn = nn.Sequential(*layers) # BiLSTM:输入维度为 TCN 最后一层输出通道数 self.bilstm = nn.LSTM(num_channels[-1], lstm_hidden, num_layers=lstm_layers, batch_first=True, bidirectional=True, dropout=dropout) # 双向输出拼接后维度为 lstm_hidden * 2 self.fc = nn.Linear(lstm_hidden * 2, output_size) def forward(self, x): # x: (batch, num_inputs, seq_len) y = self.tcn(x) # (batch, num_channels[-1], seq_len) y = y.permute(0, 2, 1) # 转成 (batch, seq_len, features) 给 LSTM lstm_out, _ = self.bilstm(y) # (batch, seq_len, lstm_hidden*2) # 取最后一个时间步的输出做预测 out = self.fc(lstm_out[:, -1, :]) # (batch, output_size) return out

num_channels控制 TCN 每层的输出通道数,常见配置是[32, 64, 64][64, 64, 128]lstm_hidden一般取 32 到 128 之间,太大容易过拟合。permute那一步是必须的,因为 Conv1d 的输入格式是 (batch, channels, length),而 LSTM 要求 (batch, length, features)。

2.3 多变量输入的数据窗口怎么切

多变量时序预测的核心是把连续序列切成滑动窗口。假设有 N 个变量、总长度 L,窗口大小 win_len、预测步长 pred_len,那么每个样本的输入是 (win_len, N),标签是未来 pred_len 步的目标变量值。

import numpy as np from sklearn.preprocessing import MinMaxScaler def create_windows(data, target_col, win_len=48, pred_len=1): """ data: (L, N) 多变量序列 target_col: 要预测的变量列索引 返回: X (samples, win_len, N), y (samples, pred_len) """ scaler = MinMaxScaler() data_scaled = scaler.fit_transform(data) X, y = [], [] for i in range(len(data_scaled) - win_len - pred_len + 1): X.append(data_scaled[i:i+win_len, :]) y.append(data_scaled[i+win_len:i+win_len+pred_len, target_col]) return np.array(X), np.array(y), scaler

win_len的选择要看数据的周期性。日周期数据(比如每小时采样)一般取 24 或 48,周周期取 168。pred_len是预测步数,单步预测取 1,多步取 6、12、24。归一化用 MinMaxScaler 把数据压到 [0,1],因为 LSTM 的 sigmoid/tanh 门控对输入范围敏感,不归一化训练很难收敛。注意 scaler 只在训练集上 fit,验证和测试集用同一个 scaler 做 transform,否则会引入未来信息。

3. 从零跑通训练流程:数据、模型、训练循环

3.1 环境配置与依赖安装

Python 环境建议 3.8 以上,PyTorch 选 1.12 或更高。用 conda 或 venv 建虚拟环境都行,关键是版本对齐。

# 创建虚拟环境 python -m venv tcn_bilstm_env source tcn_bilstm_env/bin/activate # Windows 用 tcn_bilstm_env\Scripts\activate # 安装依赖 pip install torch numpy pandas scikit-learn matplotlib pyqt5

PyTorch 的安装命令根据 CUDA 版本不同,去官网查对应命令。如果只用 CPU 训练,pip install torch就够了。PyQt5 是给后面的 GUI 界面用的,不做界面可以不装。matplotlib 用来画预测对比图,调试阶段很有用。

3.2 数据集准备与标准化

以常见的多变量时序数据集为例(比如电力负荷、气象数据),假设 CSV 文件每行是一个时间步,每列是一个变量。加载后先检查缺失值和异常值。

import pandas as pd df = pd.read_csv('multivariate_data.csv', parse_dates=['timestamp']) df = df.sort_values('timestamp').reset_index(drop=True) # 缺失值处理:线性插值 df = df.interpolate(method='linear').fillna(method='bfill') # 选择数值列作为特征 feature_cols = [c for c in df.columns if c != 'timestamp'] data = df[feature_cols].values target_col = feature_cols.index('target') # 假设目标变量叫 target X, y, scaler = create_windows(data, target_col, win_len=48, pred_len=1) print(f"样本数: {X.shape[0]}, 输入形状: {X.shape[1:]}, 标签形状: {y.shape[1:]}")

缺失值用线性插值是最稳的做法,不要用均值填充——时序数据的均值填充会破坏趋势。异常值可以用 3σ 原则或 IQR 检测后替换。target_col是要预测的那一列,其他列都是辅助特征。如果目标变量本身也在特征里,注意预测时不要把它未来值泄漏进输入。

3.3 训练循环与关键参数

训练循环本身不复杂,关键是几个参数的配合:学习率、batch size、早停耐心值。

import torch from torch.utils.data import TensorDataset, DataLoader device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 划分训练/验证/测试集,按时间顺序切,不能打乱 n = len(X) train_end = int(n * 0.7) val_end = int(n * 0.85) X_train, y_train = X[:train_end], y[:train_end] X_val, y_val = X[train_end:val_end], y[train_end:val_end] X_test, y_test = X[val_end:], y[val_end:] # 转成 tensor,注意 Conv1d 要求 (batch, channels, length) def to_tensor(X, y): X_t = torch.FloatTensor(X).permute(0, 2, 1) # (samples, N, win_len) y_t = torch.FloatTensor(y) return TensorDataset(X_t, y_t) train_loader = DataLoader(to_tensor(X_train, y_train), batch_size=64, shuffle=True) val_loader = DataLoader(to_tensor(X_val, y_val), batch_size=64, shuffle=False) model = TCNBiLSTM(num_inputs=X.shape[2], num_channels=[32, 64, 64], kernel_size=3, dropout=0.2, lstm_hidden=64, lstm_layers=2, output_size=1).to(device) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=5, factor=0.5) best_val_loss = float('inf') patience_counter = 0 early_stop_patience = 15 for epoch in range(200): model.train() train_loss = 0 for xb, yb in train_loader: xb, yb = xb.to(device), yb.to(device) optimizer.zero_grad() pred = model(xb) loss = criterion(pred, yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() train_loss += loss.item() * xb.size(0) train_loss /= len(train_loader.dataset) model.eval() val_loss = 0 with torch.no_grad(): for xb, yb in val_loader: xb, yb = xb.to(device), yb.to(device) pred = model(xb) val_loss += criterion(pred, yb).item() * xb.size(0) val_loss /= len(val_loader.dataset) scheduler.step(val_loss) if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), 'best_model.pth') patience_counter = 0 else: patience_counter += 1 if patience_counter >= early_stop_patience: print(f"早停于 epoch {epoch}") break if epoch % 10 == 0: print(f"Epoch {epoch}: train_loss={train_loss:.6f}, val_loss={val_loss:.6f}")

几个参数要重点说。学习率 1e-3 是 Adam 的常用起点,如果 loss 震荡就降到 5e-4 或 1e-4。weight_decay=1e-5是 L2 正则,防止权重过大。clip_grad_norm_把梯度范数限制在 1.0,LSTM 容易梯度爆炸,这一步是保命的。ReduceLROnPlateau在验证 loss 不降时自动降学习率,patience=5 表示连续 5 个 epoch 没改善就降。早停 patience 设 15,太小学不到东西,太大浪费时间。

注意:训练集、验证集、测试集必须按时间顺序切分,绝对不能随机打乱后再切。时序数据的随机切分会导致未来信息泄漏,验证 loss 虚低,上线后直接翻车。

4. 预测结果反归一化与评估指标

4.1 反归一化与预测曲线还原

模型输出的是归一化后的值,必须用同一个 scaler 反变换回原始量纲才能算真实误差。

model.load_state_dict(torch.load('best_model.pth')) model.eval() X_test_t = torch.FloatTensor(X_test).permute(0, 2, 1).to(device) with torch.no_grad(): pred_scaled = model(X_test_t).cpu().numpy() # 反归一化:scaler 是对所有列 fit 的,需要构造完整维度的数组 def inverse_target(scaler, pred_scaled, target_col, n_features): dummy = np.zeros((len(pred_scaled), n_features)) dummy[:, target_col] = pred_scaled.flatten() return scaler.inverse_transform(dummy)[:, target_col] pred_real = inverse_target(scaler, pred_scaled, target_col, X.shape[2]) y_real = inverse_target(scaler, y_test, target_col, X.shape[2])

反归一化这里有个容易翻车的点:scaler 是在所有特征列上 fit 的,如果只把预测值传进去,维度对不上。正确做法是构造一个和原始特征同维度的零数组,把预测值填到目标列,再整体 inverse_transform,最后取出目标列。这个坑我第一次做的时候卡了半天,输出全是乱码。

4.2 评估指标:MAE、RMSE、MAPE 怎么选

三个指标各有侧重,实际项目里建议都算一遍。

指标公式适用场景注意事项
MAEmean(|y - ŷ|)误差量纲直观对异常值不敏感
RMSEsqrt(mean((y - ŷ)²))惩罚大误差受异常值影响大
MAPEmean(|y - ŷ| / |y|) × 100%跨量纲对比y 接近 0 时爆炸
from sklearn.metrics import mean_absolute_error, mean_squared_error mae = mean_absolute_error(y_real, pred_real) rmse = np.sqrt(mean_squared_error(y_real, pred_real)) # MAPE 要过滤掉接近 0 的真实值 mask = np.abs(y_real) > 1e-6 mape = np.mean(np.abs((y_real[mask] - pred_real[mask]) / y_real[mask])) * 100 print(f"MAE: {mae:.4f}, RMSE: {rmse:.4f}, MAPE: {mape:.2f}%")

MAPE 在目标变量有零值或接近零值时会产生极大值甚至无穷,必须加 mask 过滤。如果目标变量本身波动大,MAPE 参考价值有限,优先看 RMSE。评估时还要画预测曲线对比图,光看数字看不出相位偏移和趋势拟合情况。

4.3 和单模型对比:TCN、LSTM、TCN-BiLSTM 的差距

同一份数据、同一套窗口切分,把三个模型跑一遍对比,才能说明组合结构到底值不值。

# 简化对比:只改模型定义,训练循环复用 class PureTCN(nn.Module): def __init__(self, num_inputs, num_channels, output_size=1): super().__init__() layers = [] for i in range(len(num_channels)): dilation = 2 ** i in_ch = num_inputs if i == 0 else num_channels[i-1] layers.append(TemporalBlock(in_ch, num_channels[i], 3, 1, dilation, (3-1)*dilation, 0.2)) self.tcn = nn.Sequential(*layers) self.fc = nn.Linear(num_channels[-1], output_size) def forward(self, x): y = self.tcn(x) return self.fc(y[:, :, -1]) # 取最后时间步

对比时保持 TCN 部分通道配置一致,BiLSTM 换成单向 LSTM 或直接去掉,其他超参不变。典型结果是 TCN-BiLSTM 的 RMSE 比纯 TCN 低 8% 到 15%,比纯 LSTM 低 15% 到 25%,具体幅度取决于数据的双向依赖强度。如果数据本身双向依赖弱(比如纯随机游走),组合模型优势不明显,这时候上 TCN 就够了,不必硬堆 BiLSTM。

5. GUI 推理界面与踩坑排查

5.1 用 PyQt5 搭一个最小可用的预测界面

训练完模型总得有个地方跑推理,命令行不方便演示。PyQt5 搭一个简单界面:选 CSV 文件、加载模型、点按钮出预测曲线。

import sys import numpy as np import pandas as pd import torch from PyQt5.QtWidgets import (QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget, QFileDialog, QLabel) from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure class PredictWindow(QMainWindow): def __init__(self, model, scaler, target_col, n_features, win_len=48): super().__init__() self.model = model self.scaler = scaler self.target_col = target_col self.n_features = n_features self.win_len = win_len self.setWindowTitle('TCN-BiLSTM 时序预测') self.resize(900, 600) central = QWidget() layout = QVBoxLayout() self.btn_load = QPushButton('加载数据并预测') self.btn_load.clicked.connect(self.run_predict) self.label = QLabel('等待加载...') self.figure = Figure(figsize=(8, 4)) self.canvas = FigureCanvas(self.figure) layout.addWidget(self.btn_load) layout.addWidget(self.label) layout.addWidget(self.canvas) central.setLayout(layout) self.setCentralWidget(central) def run_predict(self): path, _ = QFileDialog.getOpenFileName(self, '选择CSV', '', 'CSV Files (*.csv)') if not path: return df = pd.read_csv(path) data = df.select_dtypes(include=[np.number]).values data_scaled = self.scaler.transform(data) if len(data_scaled) < self.win_len: self.label.setText('数据长度不足一个窗口') return x = data_scaled[-self.win_len:, :] x_t = torch.FloatTensor(x).unsqueeze(0).permute(0, 2, 1) self.model.eval() with torch.no_grad(): pred = self.model(x_t).item() # 反归一化 dummy = np.zeros((1, self.n_features)) dummy[0, self.target_col] = pred real_pred = self.scaler.inverse_transform(dummy)[0, self.target_col] self.label.setText(f'下一时刻预测值: {real_pred:.4f}') # 画历史曲线 self.figure.clear() ax = self.figure.add_subplot(111) ax.plot(data[:, self.target_col], label='历史') ax.axhline(real_pred, color='r', linestyle='--', label='预测') ax.legend() self.canvas.draw() if __name__ == '__main__': app = QApplication(sys.argv) # 加载模型和 scaler 后传入 window = PredictWindow(model, scaler, target_col, X.shape[2]) window.show() sys.exit(app.exec_())

界面逻辑很直白:读 CSV、取最后 win_len 行、归一化、送模型、反归一化、画图。permute(0, 2, 1)那步不能漏,否则 Conv1d 会把时间步当通道处理,输出完全错乱。matplotlib 嵌入 PyQt5 用FigureCanvasQTAgg,不要用 plt.show(),否则会阻塞主线程。

5.2 避坑清单:五个真实踩过的坑

现象一:训练 loss 一直不降,停在某个值不动。原因:数据没归一化,或者归一化用了全局 scaler 但训练集和测试集分布差异大。 解决:确认 MinMaxScaler 只在训练集 fit,检查数据范围是否在 [0,1]。如果 loss 还是不降,把学习率降到 1e-4 试试。

现象二:验证 loss 比训练 loss 低很多。原因:数据切分时随机打乱了,未来信息泄漏到训练集。 解决:按时间顺序切分,训练集在前、验证集在中、测试集在后。检查 DataLoader 的 shuffle 只在训练集设为 True。

现象三:预测曲线整体滞后一个时间步。原因:窗口切分时标签对齐错了,把 t 时刻的输入对应到了 t 时刻的输出而不是 t+1。 解决:检查create_windowsy.append(data_scaled[i+win_len:i+win_len+pred_len, target_col])的索引,确保标签是窗口之后的值。

现象四:BiLSTM 部分梯度爆炸,loss 变成 nan。原因:LSTM 层数多、序列长时梯度累积。 解决:加clip_grad_norm_(model.parameters(), max_norm=1.0),把 LSTM 层数降到 1 或 2,hidden 降到 64 以下。

现象五:GUI 点预测按钮后界面卡死。原因:模型推理在主线程执行,数据量大时阻塞 UI。 解决:把推理放到 QThread 里,或者限制输入数据长度。简单做法是推理前先QApplication.processEvents(),但根本方案还是多线程。

5.3 超参数怎么调:一份可复用的搜索顺序

调参不要一上来就网格搜索,按影响从大到小逐个调。

参数推荐范围调整方向
win_len24 / 48 / 168按数据周期选,先看自相关图
num_channels[32,64,64] / [64,64,128]数据复杂就加宽,过拟合就减
lstm_hidden32 / 64 / 128从 64 起步,过拟合降到 32
dropout0.1 ~ 0.3过拟合加,欠拟合减
lr1e-3 / 5e-4 / 1e-4loss 震荡就降
batch_size32 / 64 / 128显存够就大,小 batch 噪声大

先固定其他参数,只调 win_len,找到验证 loss 最低的窗口。再调 num_channels 和 lstm_hidden,最后微调 dropout 和 lr。每次只动一个参数,记录验证 loss,别同时改多个,否则根本不知道是哪个起了作用。

6. 多步预测与滚动推理的进阶技巧

单步预测跑通之后,实际项目往往要求预测未来 6 步、12 步甚至 24 步。直接改output_size=pred_len让模型一次输出多步是最简单的做法,但多步之间的时序一致性会差一些。另一种做法是滚动推理:每次预测一步,把预测值拼回输入序列,再预测下一步。

def rolling_predict(model, initial_window, steps, scaler, target_col, n_features): """ initial_window: (win_len, n_features) 归一化后的初始窗口 逐步预测未来 steps 步 """ model.eval() window = initial_window.copy() preds = [] for _ in range(steps): x = torch.FloatTensor(window).unsqueeze(0).permute(0, 2, 1) with torch.no_grad(): p = model(x).item() preds.append(p) # 把预测值拼到窗口末尾,去掉最早的一步 new_row = window[-1].copy() new_row[target_col] = p window = np.vstack([window[1:], new_row]) # 反归一化 dummy = np.zeros((len(preds), n_features)) dummy[:, target_col] = preds return scaler.inverse_transform(dummy)[:, target_col]

滚动推理的误差会累积,步数越多越明显。缓解办法有两个:一是训练时就用多步标签(pred_len > 1),让模型见过多步误差;二是每隔几步用真实值校正一次窗口,但推理时没有真实值,只能靠模型自身。实际项目里 6 步以内滚动推理还能用,超过 12 步建议直接训练多输出模型。

多步预测的评估要分步看,不能只看整体 RMSE。第 1 步的误差和第 12 步的误差可能差好几倍,分步报告才能看出模型在哪个 horizon 开始失效。

# 分步评估 for step in range(pred_len): step_mae = mean_absolute_error(y_real[:, step], pred_real[:, step]) print(f"Step {step+1} MAE: {step_mae:.4f}")

如果第 1 步 MAE 就很大,说明模型本身没学好,回去查数据和训练。如果前几步还行、后面突然变大,说明长程依赖没抓住,考虑加大 TCN 感受野(加层数或加大 kernel_size)或者增加 BiLSTM 的 hidden 维度。

最后说一个我自己的习惯:每次改完模型结构或超参,先在一个小数据集上跑 20 个 epoch 看 loss 曲线趋势,趋势对了再上全量数据。全量跑一次几十分钟,小数据几分钟就能判断方向对不对,省下来的时间够调好几轮参。模型保存时把 scaler、窗口参数、特征列名一起存进 checkpoint,不然过两周自己都忘了当时怎么处理的。希望帮到你。

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

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

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

立即咨询