dl-ts-dt-cb-merge-1

112 阅读1小时+

时间序列数据的深度学习秘籍(二)

原文:annas-archive.org/md5/412c498dab2bec44414eb60081361de1

译者:飞龙

协议:CC BY-NC-SA 4.0

第四章:使用 PyTorch Lightning 进行预测

在本章中,我们将使用 PyTorch Lightning 构建预测模型。我们将探讨该框架的几个方面,例如创建数据模块来处理数据预处理,或创建 LightningModel 结构来封装神经网络的训练过程。我们还将探索 TensorBoard 来监控神经网络的训练过程。接下来,我们将描述几种用于评估深度神经网络预测效果的指标,如 均方绝对缩放误差 (MASE) 和 对称平均绝对百分比误差 (SMAPE)。在本章中,我们将重点讨论多变量时间序列,这些序列包含多个变量。

本章将引导你完成以下几个实例:

  • 准备多变量时间序列进行监督学习

  • 使用多变量时间序列训练线性回归预测模型

  • 用于多变量时间序列预测的前馈神经网络

  • 用于多变量时间序列预测的 LSTM 神经网络

  • 评估深度神经网络的预测效果

  • 使用 Tensorboard 监控训练过程

  • 使用回调函数 – EarlyStopping

技术要求

在本章中,我们将使用以下 Python 库,所有这些库都可以通过 pip 安装:

  • PyTorch Lightning (2.1.4)

  • PyTorch Forecasting (1.0.0)

  • torch (2.2.0)

  • ray (2.9.2)

  • numpy (1.26.3)

  • pandas (2.1.4)

  • scikit-learn (1.4.0)

  • sktime (0.26.0)

本章的代码可以在本书的 GitHub 仓库找到:github.com/PacktPublishing/Deep-Learning-for-Time-Series-Data-Cookbook

准备多变量时间序列进行监督学习

本章的第一个实例解决了如何准备多变量时间序列进行监督学习的问题。我们将展示在上一章中使用的滑动窗口方法如何扩展来解决这个任务。接着,我们将演示如何使用 TimeSeriesDataSet(一个 PyTorch Forecasting 类,用于处理时间序列的预处理步骤)来准备时间序列数据。

准备工作

我们将使用在 第一章 中分析的相同时间序列。我们需要使用以下代码,通过 pandas 加载数据集:

import pandas as pd
data = pd.read_csv('assets/daily_multivariate_timeseries.csv',
                   parse_dates=['Datetime'],
                   index_col='Datetime')

下图展示了时间序列的示例。请注意,为了便于可视化,坐标轴已被转置:

图 4.1:多变量时间序列示例。系列的变量显示在 x 轴上,以便于可视化

图 4.1:多变量时间序列示例。系列的变量显示在 x 轴上,以便于可视化

上述数据集包含九个与气象条件相关的变量。就像在 第三章 中一样,目标是预测下一个太阳辐射值。我们将使用额外可用变量的滞后值作为输入解释变量。在下一章中,你将学习如何为需要预测多个变量的情况准备多变量时间序列。

如何做到这一点……

我们将对多变量时间序列进行监督学习的转换。首先,我们将描述如何使用我们在 第三章 中使用的滑动窗口方法。然后,我们将展示如何使用基于 PyTorch 的 TimeSeriesDataSet 数据结构简化此过程。

使用滑动窗口

在上一章中,我们使用滑动窗口方法将单变量时间序列从一个序列转换为矩阵格式。为监督学习准备多变量时间序列需要类似的过程:我们对每个变量应用滑动窗口技术,然后将结果合并。这个过程可以按以下方式进行:

TARGET = 'Incoming Solar'
N_LAGS = 7
HORIZON = 1
input_data = []
output_data = []
for i in range(N_LAGS, data.shape[0]-HORIZON+1):
    input_data.append(data.iloc[i - N_LAGS:i].values)
    output_data.append(data.iloc[i:(i+HORIZON)][TARGET])
input_data, output_data = np.array(input_data), np.array(output_data)

上述代码遵循以下步骤:

  1. 首先,我们定义滞后数和预测视野。我们将滞后数设置为 7N_LAGS=7),预测视野设置为 1HORIZON=1),目标变量设置为 Incoming Solar.

  2. 然后,我们遍历多变量时间序列中的每个时间步骤。在每个点,我们检索前 N_LAGS 的数据,将其添加到 input_data 中,并将下一个太阳辐射值添加到输出数据中。这意味着我们将使用每个变量的过去 7 个值来预测下一个太阳辐射值。

  3. 最后,我们将输入和输出数据从 Python 列表转换为 NumPy array 结构。

output_data 是一个一维向量,表示未来的太阳辐射值。input_data 有三个维度:第一个维度表示样本数量,第二个维度表示滞后数量,第三个维度表示序列中的变量数量。

使用 TimeSeriesDataSet

到目前为止,我们一直在使用滑动窗口方法来预处理时间序列,供监督学习使用。这个功能和训练神经网络所需的其他预处理任务都通过 TimeSeriesDataSet 类进行了自动化,该类可在 PyTorch Forecasting 库中找到。

TimeSeriesDataSet 提供了一种简单且有效的方法来准备数据并将其传递给模型。让我们来看一下如何使用这个结构来处理多变量时间序列。首先,我们需要将时间序列组织成一个包含三种主要信息的 pandas DataFrame 结构:

  • group_id:一个列,用于标识时间序列的名称。如果数据集包含单一时间序列,该列将显示一个常量值。有些数据集涉及多个时间序列,可以通过此变量区分。

  • time_index:存储某一时间点上给定序列捕获的值。

  • 其他变量:存储时间序列值的额外变量。多变量时间序列包含多个变量。

我们的时间序列已经包含了多个变量。现在,我们需要添加关于time_indexgroup_id的信息,可以通过如下方式完成:

mvtseries['group_id'] = 0
mvtseries['time_index'] = np.arange(mvtseries.shape[0])

group_id的值始终为0,因为我们正在处理单个时间序列。我们随便使用0。你可以使用任何适合的名称。我们使用np.arange``()函数来创建这个时间序列的time_index。这会创建一个变量,对第一个观察值给出0,对第二个观察值给出1,依此类推。

然后,我们必须创建TimeSeriesDataSet类的一个实例,如下所示:

dataset = TimeSeriesDataSet(
    data=mvtseries,
    group_ids=["group_id"],
    target="Incoming Solar",
    time_idx="time_index",
    max_encoder_length=7,
    max_prediction_length=1,
    time_varying_unknown_reals=['Incoming Solar',
                                'Wind Dir',
                                'Snow Depth',
                                'Wind Speed',
                                'Dewpoint',
                                'Precipitation',
                                'Vapor Pressure',
                                'Relative Humidity',
                                'Air Temp'],
)

我们可以将TimeSeriesDataSet数据集转换为DataLoader类,如下所示:

data_loader = dataset.to_dataloader(batch_size=1, shuffle=False)

DataLoader用于将观察值传递给模型。以下是一个观察值的示例:

x, y = next(iter(data_loader))
x['encoder_cont']
y

我们使用next``()iter``()方法从数据加载器中获取一个观察值。这个观察值被存储为xy,分别表示输入和输出数据。主要的输入是encoder_cont项,表示每个变量的7个滞后值。这个数据是一个 PyTorch 张量,形状为(1, 7, 9),表示(批量大小、滞后数、变量数)。批量大小是一个参数,表示神经网络每次训练迭代中使用的样本数。输出数据是一个浮动值,表示太阳辐射变量的下一个值。

它是如何工作的……

TimeSeriesDataSet构造函数需要一些参数:

  • data:一个包含之前描述的三个元素的时间序列数据集

  • group_idsdata中标识数据集每个时间序列的列

  • targetdata中我们想要预测的列(目标变量)

  • time_idxdata中包含每个观察值的时间信息的列

  • max_encoder_length:用于构建自回归模型的滞后数

  • max_prediction_length:预测视野——即,应该预测多少未来时间步长

  • time_varying_unknown_realsdata中列出的描述哪些数值变量随时间变化的列

还有其他与time_varying_unknown_reals相关的参数。这个特定的输入详细描述了所有未来值对用户未知的数值观测值,例如我们想要预测的变量。然而,在某些情况下,我们知道一个观测值的未来值,例如产品价格。这类变量应该包含在time_varying_known_reals输入中。还有time_varying_known_categoricalstime_varying_unknown_categoricals输入,可用于代替数值型变量的分类变量。

关于预测任务,我们在这个示例中进行的转换是名为自回归分布滞后模型ARDL)的一种建模方法的基础。ARDL 是自回归的扩展,也包括外生变量的滞后作为输入。

使用多元时间序列训练线性回归模型进行预测

在这个示例中,我们将使用 PyTorch 训练一个线性回归模型,作为我们第一个在多元时间序列上拟合的预测模型。我们将展示如何使用TimeSeriesDataSet处理训练模型的数据预处理步骤,并将数据传递给模型。

准备工作

我们将从之前示例中使用的mvtseries数据集开始:

import pandas as pd
mvtseries = pd.read_csv('assets/daily_multivariate_timeseries.csv',
            parse_dates=['datetime'],
            index_col='datetime')

现在,让我们看看如何使用这个数据集来训练一个 PyTorch 模型。

如何做到这一点…

在接下来的代码中,我们将描述准备时间序列和构建线性回归模型所需的步骤:

  1. 我们从预处理时间序列开始。这包括创建组标识符和时间索引列:

    mvtseries["target"] = mvtseries["Incoming Solar"]
    mvtseries["time_index"] = np.arange(mvtseries.shape[0])
    mvtseries["group_id"] = 0
    
  2. 然后,我们必须将数据划分为不同的部分。对于这个示例,我们只保留训练集的索引:

    time_indices = data["time_index"].values
    train_indices, _ = train_test_split(
        time_indices,
        test_size=test_size,
        shuffle=False)
    train_indices, _ = train_test_split(train_indices,
                                        test_size=0.1,
                                        shuffle=False)
    train_df = data.loc[data["time_index"].isin(train_indices)]
     train_df_mod = train_df.copy()
    
  3. 然后,我们必须使用StandardScaler操作符对时间序列进行标准化:

    target_scaler = StandardScaler()
    target_scaler.fit(train_df_mod[["target"]])
    train_df_mod["target"] = target_scaler.transform
        (train_df_mod[["target"]])
    train_df_mod = train_df_mod.drop("Incoming Solar", axis=1)
     feature_names = [
        col for col in data.columns
        if col != "target" and col != "Incoming Solar"
    ]
    
  4. 预处理后的时间序列被传递给一个TimeSeriesDataSet实例:

    training_dataset = TimeSeriesDataSet(
        train_df_mod,
        time_idx="time_index",
        target="target",
        group_ids=["group_id"],
        max_encoder_length=n_lags,
        max_prediction_length=horizon,
        time_varying_unknown_reals=feature_names,
        scalers={name: StandardScaler()
                 for name in feature_names},
    )
    loader = training_dataset.to_dataloader(batch_size=batch_size,
                                            shuffle=False)
    

    TimeSeriesDataSet对象被转换成一个数据加载器,可以用于将样本批次传递给模型。这是通过to_dataloader()方法完成的。我们将所有这些数据准备步骤封装成一个名为create_training_set的函数。你可以在本书的 GitHub 仓库中查看该函数的源代码。

  5. 接下来,我们调用create_training_set()函数来创建训练数据集:

    N_LAGS = 7
    HORIZON = 1
    BATCH_SIZE = 10
    data_loader = create_training_set(
        data=mvtseries,
        n_lags=N_LAGS,
        horizon=HORIZON,
        batch_size=BATCH_SIZE,
        test_size=0.3
    )
    
  6. 然后,我们必须使用 PyTorch 定义线性回归模型,如下所示:

    import torch
    from torch import nn
    class LinearRegressionModel(nn.Module):
        def __init__(self, input_dim, output_dim):
            super(LinearRegressionModel, self).__init__()
            self.linear = nn.Linear(input_dim, output_dim)
        def forward(self, X):
            X = X.view(X.size(0), -1)
            return self.linear(X)
    

    在这里,我们定义了一个名为LinearRegressionModel的类,来实现多元线性回归模型。它包含一个线性变换层(nn.Linear)。这个类接受输入和输出的大小作为输入,分别对应train_inputtrain_output对象的第二维。我们通过传入这些参数来创建该模型。

  7. 现在,我们将按照以下方式创建这个模型的一个实例:

    num_vars = mvtseries.shape[1] + 1
    model = LinearRegressionModel(N_LAGS * num_vars, HORIZON)
    

    num_vars包含时间序列中的变量数量。然后,我们将模型的输入定义为num_vars乘以N_LAGS,输出则定义为预测时长。

  8. 我们可以使用以下代码进行训练过程:

    criterion = nn.MSELoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    num_epochs = 10
    for epoch in range(num_epochs):
        for batch in data_loader:
            x, y = batch
            X = x["encoder_cont"].squeeze(-1)
            y_pred = model(X)
            y_pred = y_pred.squeeze(1)
            y_actual = y[0].squeeze(1)
            loss = criterion(y_pred, y_actual)
            loss.backward()
            optimizer.step()
            optimizer.zero_grad()
        print(f"epoch: {epoch + 1}, loss = {loss.item():.4f}")
    

    在这里,我们将学习率设置为0.001,优化器设置为 Adam。Adam 是一个常见的替代方法,比 SGD 等方法具有更好的收敛特性。

    在每个训练周期,我们从数据加载器中获取每个批次的滞后,并使用模型对其进行处理。注意,每个批次都会被重新调整为线性模型所需的二维格式。这是在LinearRegressionModel类的forward()方法中完成的。

它是如何工作的…

我们使用TimeSeriesDataSet类来处理数据准备过程。然后,我们通过to_dataloader()方法将数据集转换为DataLoader类。这个数据加载器为模型提供数据批次。虽然我们没有显式定义它,但每个批次都遵循自回归的建模方式。输入基于时间序列的过去几次观测,输出代表未来的观测值。

我们将线性回归模型实现为一个类,以便它遵循与上一章相同的结构。为了简化,我们可以通过model = nn.Linear(input_size, output_size)来创建模型。

用于多变量时间序列预测的前馈神经网络

在这个食谱中,我们将重新关注深度神经网络。我们将展示如何使用深度前馈神经网络为多变量时间序列构建预测模型。我们将描述如何将DataModule类与TimeSeriesDataSet结合,以封装数据预处理步骤。我们还将把PyTorch模型放在LightningModule结构中,这样可以标准化神经网络的训练过程。

准备就绪

我们将继续使用与太阳辐射预测相关的多变量时间序列:

import pandas as pd
mvtseries = pd.read_csv('assets/daily_multivariate_timeseries.csv',
                        parse_dates=['datetime'],
                        index_col='datetime')
n_vars = mvtseries.shape[1]

在这个食谱中,我们将使用来自pytorch_lightning的数据模块来处理数据预处理。数据模块是包含所有数据预处理步骤并与模型共享数据的类。以下是数据模块的基本结构:

import lightning.pytorch as pl
class ExampleDataModule(pl.LightningDataModule):
    def __init__(self,
                 data: pd.DataFrame,
                 batch_size: int):
        super().__init__()
        self.data = data
        self.batch_size = batch_size
    def setup(self, stage=None):
        pass
    def train_dataloader(self):
        pass
    def val_dataloader(self):
        pass
    def test_dataloader(self):
        pass
    def predict_dataloader(self):
        pass

所有数据模块都继承自LightningDataModule类。我们需要实现几个关键方法:

  • setup():此方法包含所有主要的数据预处理步骤

  • train_dataloader()val_dataloader()test_dataloader()predict_dataloader():这些是获取相应数据集(训练、验证、测试和预测)数据加载器的一组方法

除了DataModule类外,我们还将利用LightningModule类来封装所有模型过程。这些模块具有以下结构:

class ExampleModel(pl.LightningModule):
    def __init__(self):
        super().__init__()
        self.network = ...
    def forward(self, x):
        pass
    def training_step(self, batch, batch_idx):
        pass
    def validation_step(self, batch, batch_idx):
        pass
    def test_step(self, batch, batch_idx):
        pass
    def predict_step(self, batch, batch_idx, dataloader_idx=0):
        pass
    def configure_optimizers(self):
        pass

让我们更仔细地看看ExampleModel

  • 我们在类的属性中定义任何必要的神经网络元素(例如self.network

  • forward()方法定义了网络元素如何相互作用并建模时间序列

  • training_stepvalidation_steptesting_step分别描述了网络的训练、验证和测试过程

  • predict_step详细描述了获取最新观测值并进行预测的过程,模拟了部署场景

  • 最后,configure_optimizers()方法详细描述了网络的优化设置

让我们看看如何创建一个数据模块来预处理多变量时间序列,以及它如何与TimeSeriesDataSet结合。然后,我们将实现一个LightningModule结构来处理前馈神经网络的训练和测试过程。

如何进行…

以下代码展示了如何定义数据模块以处理预处理步骤。首先,让我们看一下类的构造函数:

from pytorch_forecasting import TimeSeriesDataSet
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
class MultivariateSeriesDataModule(pl.LightningDataModule):
    def __init__(
            self,
            data: pd.DataFrame,
            n_lags: int,
            horizon: int,
            test_size: float,
            batch_size: int
    ):
        super().__init__()
        self.data = data
        self.feature_names = 
            [col for col in data.columns if col != "Incoming Solar"]
        self.batch_size = batch_size
        self.test_size = test_size
        self.n_lags = n_lags
        self.horizon = horizon
        self.target_scaler = StandardScaler()
        self.training = None
        self.validation = None
        self.test = None
        self.predict_set = None

在构造函数中,我们定义了所有必要的数据准备元素,如滞后数、预测时间跨度和数据集。这包括初始化target_scaler属性,该属性用于标准化时间序列的值。

然后,我们创建setup()方法,其中包括数据预处理逻辑:

def setup(self, stage=None):
    self.preprocess_data()
    train_indices, val_indices, test_indices = self.split_data()
    train_df = self.data.loc
        [self.data["time_index"].isin(train_indices)]
    val_df = self.data.loc[self.data["time_index"].isin(val_indices)]
    test_df = self.data.loc
        [self.data["time_index"].isin(test_indices)]
     self.target_scaler.fit(train_df[["target"]])
    self.scale_target(train_df, train_df.index)
    self.scale_target(val_df, val_df.index)
    self.scale_target(test_df, test_df.index)
    train_df = train_df.drop("Incoming Solar", axis=1)
    val_df = val_df.drop("Incoming Solar", axis=1)
    test_df = test_df.drop("Incoming Solar", axis=1)
    self.training = TimeSeriesDataSet(
        train_df,
        time_idx="time_index",
        target="target",
        group_ids=["group_id"],
        max_encoder_length=self.n_lags,
        max_prediction_length=self.horizon,
        time_varying_unknown_reals=self.feature_names,
        scalers={name: StandardScaler() for name in 
            self.feature_names},
    )
    self.validation = TimeSeriesDataSet.from_dataset
        (self.training, val_df)
    self.test = TimeSeriesDataSet.from_dataset(self.training, test_df)
    self.predict_set = TimeSeriesDataSet.from_dataset(
    self.training, self.data, predict=True)

一些方法,如self.preprocess_data(),已被省略以简化内容。你可以在本书的 GitHub 仓库中找到它们的源代码。

最后,我们必须构建数据加载器,负责将数据传递给模型:

    def train_dataloader(self):
        return self.training.to_dataloader
            (batch_size=self.batch_size, shuffle=False)
    def val_dataloader(self):
        return self.validation.to_dataloader
            (batch_size=self.batch_size, shuffle=False)
    def test_dataloader(self):
        return self.test.to_dataloader
            (batch_size=self.batch_size, shuffle=False)
    def predict_dataloader(self):
        return self.predict_set.to_dataloader
            (batch_size=1, shuffle=False)

让我们仔细看看这个数据模块:

  • 数据预处理步骤在setup()方法中完成。这包括通过包括time_indexgroup_id变量来转换时间序列,以及训练、验证和测试拆分。数据集使用TimeSeriesDataSet类来构建。请注意,我们只需要为其中一个数据集定义一个TimeSeriesDataSet实例。我们可以使用from_dataset()方法为另一个数据集设置一个现有的TimeSeriesDataSet实例。

  • 预处理步骤的信息可以通过DataModule类的构造函数传递,例如滞后数(n_lags)或预测的horizon

  • 数据加载器可以通过在相应的数据集上使用to_dataloader()方法获得。

然后,我们可以设计神经网络架构。我们将创建一个名为FeedForwardNet的类,来实现一个包含三层的前馈神经网络:

import torch
from torch import nn
class FeedForwardNet(nn.Module):
    def __init__(self, input_size, output_size):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_size, 16),
            nn.ReLU(),
            nn.Linear(16, 8),
            nn.ReLU(),
            nn.Linear(8, output_size),
        )
    def forward(self, X):
        X = X.view(X.size(0), -1)
        return self.net(X)

网络架构在self.net属性中定义。网络的各层通过nn.Sequential容器堆叠在一起:

  • 第一层接收大小为input_size的输入数据。这是一个线性变换(nn.Linear),包含16个单元,并使用ReLU()激活函数(nn.ReLU)。

  • 结果会传递到第二层,这一层也是线性变换类型和激活函数。该层包含8个单元。

  • 最后一层也是对来自前一层的输入进行线性变换。其大小与output_size相同,在时间序列的情况下,指的是预测的时间跨度。

然后,我们将此神经网络插入到LightningModule模型类中。首先,让我们看一下类的构造函数和forward()方法:

from pytorch_forecasting.models import BaseModel
class FeedForwardModel(BaseModel):
    def __init__(self, input_dim: int, output_dim: int):
        self.save_hyperparameters()
        super().__init__()
        self.network = FeedForwardNet(
            input_size=input_dim,
            output_size=output_dim,
        )
        self.train_loss_history = []
        self.val_loss_history = []
        self.train_loss_sum = 0.0
        self.val_loss_sum = 0.0
        self.train_batch_count = 0
        self.val_batch_count = 0
    def forward(self, x):
        network_input = x["encoder_cont"].squeeze(-1)
        prediction = self.network(network_input)
        output = self.to_network_output(prediction=prediction)
        return output

构造函数存储网络元素,而forward()方法详细说明了这些元素在网络前向传播中的交互方式。forward()方法还使用to_network_output()方法将输出转化为原始数据尺度。训练步骤和网络优化器定义如下:

def training_step(self, batch, batch_idx):
    x, y = batch
    y_pred = self(x).prediction
    y_pred = y_pred.squeeze(1)
    y_actual = y[0].squeeze(1)
    loss = F.mse_loss(y_pred, y_actual)
    self.train_loss_sum += loss.item()
    self.train_batch_count += 1
    self.log("train_loss", loss)
    return loss
def configure_optimizers(self):
    return torch.optim.Adam(self.parameters(), lr=0.01)

configure_optimizers()方法是我们设置优化过程的地方。在训练步骤中,我们获取一批样本,将输入传递给神经网络,然后使用实际数据计算均方误差。然后,我们将误差信息存储在不同的属性中。

验证和测试步骤与训练阶段的工作方式类似:

def validation_step(self, batch, batch_idx):
    x, y = batch
    y_pred = self(x).prediction
    y_pred = y_pred.squeeze(1)
    y_actual = y[0].squeeze(1)
    loss = F.mse_loss(y_pred, y_actual)
    self.val_loss_sum += loss.item()
    self.val_batch_count += 1
    self.log("val_loss", loss)
    return loss
def test_step(self, batch, batch_idx):
    x, y = batch
    y_pred = self(x).prediction
    y_pred = y_pred.squeeze(1)
    y_actual = y[0].squeeze(1)
    loss = F.mse_loss(y_pred, y_actual)
    self.log("test_loss", loss)

在预测步骤中,我们只需将输入数据传递给神经网络,然后获取其输出:

def predict_step(self, batch, batch_idx):
    x, y = batch
    y_pred = self(x).prediction
    y_pred = y_pred.squeeze(1)
    return y_pred

让我们看一下前面的FeedForwardModel模块:

  • 基于PyTorch的神经网络在self.network属性中定义

  • forward()方法描述了神经网络如何处理从数据加载器获取的实例

  • 优化器设置为Adam,学习率为0.01

  • 最后,我们使用Trainer类来训练模型:

datamodule = MultivariateSeriesDataModule(data=mvtseries,
                                          n_lags=7,
                                          horizon=1,
                                          batch_size=32,
                                          test_size=0.3)
model = FeedForwardModel(input_dim=N_LAGS * n_vars, output_dim=1)
trainer = pl.Trainer(max_epochs=30)
trainer.fit(model, datamodule)

训练过程运行30个周期。为了测试模型,我们可以使用Trainer实例中的test()方法:

trainer.test(model=model, datamodule=datamodule)
forecasts = trainer.predict(model=model, datamodule=datamodule)

未来的观察结果通过predict()方法进行预测。在这两种情况下,我们将模型和数据模块都传递给Trainer实例。

它是如何工作的……

数据模块封装了所有准备步骤。任何需要在数据集上执行的特定转换都可以包含在setup()方法中。与模型相关的逻辑由LightningModule实例处理。使用DataModuleLightningModule方法提供了一种模块化、更整洁的深度学习模型开发方式。

TimeSeriesDataSet类中的scalers参数用于传递应该用于预处理时间序列的解释变量的缩放器。在这种情况下,我们使用了以下内容:

scalers={name: StandardScaler() for name in self.feature_names}

在这里,我们使用StandardScaler将所有解释变量转换为一个共同的数值范围。我们通过self.target_scaler属性标准化了时间序列的目标变量,其中包括一个StandardScaler操作符。我们在TimeSeriesDataSet之外对目标变量进行了归一化,以便对目标变量拥有更多的控制权。这可以作为一个示例,展示如何进行那些在软件包中可能不可用的转换。

还有更多内容……

我们使用nn.Sequential容器定义了前馈神经网络。另一种可能的方法是将每个元素定义为自己的类属性,并在forward方法中显式调用它们:

class FeedForwardNetAlternative(nn.Module):
    def __init__(self, input_size, output_size):
        super().__init__()
        self.l1 = nn.Linear(input_size, 16)
        self.relu_l1 = nn.ReLU()
        self.l2 = nn.Linear(16, 8)
        self.relu_l2 = nn.ReLU()
        self.l3 = nn.Linear(8, output_size)
    def forward(self, x):
        X = X.view(X.size(0), -1)
        l1_output = self.l1(x)
        l1_actf_output = self.relu_l1(l1_output)
        l2_output = self.l2(l1_actf_output)
        l2_actf_output = self.relu_l2(l2_output)
        l3_output = self.l3(l2_actf_output)
        return l3_output

两种方法是等效的。虽然第一种方法更加整洁,但第二种方法更具灵活性。

用于多变量时间序列预测的 LSTM 神经网络

在这个示例中,我们将继续构建一个模型,利用多变量时间序列预测太阳辐射的下一个值。这一次,我们将训练一个 LSTM 递归神经网络来解决这个任务。

准备就绪

数据设置与我们在前面的食谱中所做的类似。所以,我们将使用之前定义的数据模块。现在,让我们学习如何使用LightningModule类构建 LSTM 神经网络。

如何操作……

使用 PyTorch Lightning 训练 LSTM 神经网络的工作流程是相似的,但有一个小而重要的细节。对于 LSTM 模型,我们将输入数据保持在一个三维结构中,形状为(样本数、滞后数、特征数)。以下是模块的代码,从构造函数和forward()方法开始:

class MultivariateLSTM(pl.LightningModule):
    def __init__(self, input_dim, hidden_dim, num_layers, output_dim):
        super().__init__()
        self.hidden_dim = hidden_dim
        self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers, 
            batch_first=True)
        self.fc = nn.Linear(hidden_dim, output_dim)
    def forward(self, x):
        h0 = torch.zeros(self.lstm.num_layers, x.size(0), 
            self.hidden_dim).to(self.device)
        c0 = torch.zeros(self.lstm.num_layers, x.size(0), 
            self.hidden_dim).to(self.device)
        out, _ = self.lstm(x, (h0, c0))
        out = self.fc(out[:, -1, :])
        return out

这一次,我们不需要将网络的输入压缩成二维向量,因为 LSTM 接受的是三维输入。LSTM 背后的逻辑在forward()方法中实现。其余的方法与我们在前面食谱中所做的完全相同。以下是training_step的示例:

    def training_step(self, batch, batch_idx):
        x, y = batch
        y_pred = self(x['encoder_cont'])
        y_pred = y_pred.squeeze(1)
        loss = F.mse_loss(y_pred, y[0])
        self.log('train_loss', loss)
        return loss

你可以在本书的 GitHub 仓库中找到其余的方法。

定义完模型后,我们可以按如下方式使用它:

n_vars = mvtseries.shape[1] - 1
model = MultivariateLSTM(input_dim=n_vars,
                         hidden_dim=10,
                         num_layers=1,
                         output_dim=1)
trainer = pl.Trainer(max_epochs=10)
trainer.fit(model, datamodule)
trainer.test(model, datamodule.test_dataloader())
forecasts = trainer.predict(model=model, datamodule=datamodule)

如前面的代码所示,PyTorch Lightning 使得测试和预测过程在各个模型中保持一致。

它是如何工作的……

LSTM 是一种递归神经网络架构,旨在对时间序列等顺序数据进行建模。这类网络相较于前馈神经网络,包含一些额外的元素,如额外的输入维度或隐藏单元状态。在本节中,我们在 LSTM 层上堆叠了两个全连接层。LSTM 层通常会传递给全连接层,因为前者的输出是一个内部状态。因此,全连接层会在我们所需的特定维度上处理该输出。

LSTM 的类构造函数接收四个输入参数——时间序列中的变量数目(input_size)、预测的时间范围(output_size)、LSTM层的数量(num_layers)以及每个LSTM层中的隐藏单元数(hidden_size)。

我们在__init__构造函数方法中定义了三层。除了LSTM外,我们创建了两个全连接层,其中一个代表输出层。

网络的前向传播如下工作:

  1. 使用零初始化隐藏状态(h0)和单元状态(c0)。这是通过调用init_hidden_state()方法来完成的。

  2. 将输入数据传递给 LSTM 堆栈。LSTM 返回它的输出以及每个 LSTM 层的隐藏状态和单元状态。

  3. 接下来,我们获取最后一个 LSTM 层的隐藏状态,将其传递给ReLU()激活函数。

  4. ReLU的输出被传递到第一个全连接层,其输出再次通过ReLU函数进行转换。最后,输出被传递到一个线性全连接输出层,该层提供预测结果。

这一逻辑在LightningModule实例的forward()方法中进行了编写。

还有更多内容……

我们创建了一个具有单个 LSTM 层的深度神经网络(num_layers=1)。然而,我们可以根据需要增加该值。具有多个 LSTM 层的模型被称为堆叠 LSTM模型。

使用 Tensorboard 监控训练过程

训练深度学习模型通常需要调整多个超参数、评估不同的架构等。为了便于这些任务,必须使用可视化和监控工具。tensorboard是一个强大的工具,可以在训练过程中追踪和可视化各种指标。本节将指导你如何将tensorboard与 PyTorch Lightning 集成,用于监控训练过程。

准备工作

在使用tensorboard与 PyTorch Lightning 之前,你需要先安装tensorboard。你可以使用以下命令进行安装:

pip install -U tensorboard

安装完成后,确保你正在利用 PyTorch Lightning 内置的tensorboard日志记录功能。

如何实现……

以下是如何使用tensorboard来监控训练过程:

  1. 首先,确保tensorboard已导入到你的脚本中。

  2. 接下来,你需要创建一个tensorboard日志记录器,并将其传递给 PyTorch Lightning 的Trainer

    from lightning.pytorch.loggers import TensorBoardLogger
    import lightning.pytorch as pl
    logger = TensorBoardLogger('logs/')
    trainer = pl.Trainer(logger=logger)
    
  3. 然后,你可以通过在终端运行以下命令来启动tensorboard

    tensorboard --logdir=logs/
    
  4. 在你的网页浏览器中打开tensorboard,通过访问终端中显示的 URL;通常是http://localhost:6006。你将看到实时更新的各种指标,例如周期数、训练、验证和测试损失等。

以下图展示了上一章节中 LSTM 性能的一些图示。在此案例中,我们可以看到周期数以及训练和验证损失是如何变化的:

图 4.2:周期、训练损失和验证损失的比较

图 4.2:周期、训练损失和验证损失的比较

它是如何工作的……

tensorboard提供了各种训练指标、超参数调优、模型图形等的可视化。当与 PyTorch Lightning 集成时,以下内容会发生:

  • 在训练过程中,日志记录器将指定的指标发送到tensorboard

  • tensorboard读取日志并提供交互式可视化

  • 用户可以实时监控训练的各个方面

还有更多……

以下是一些需要注意的额外细节:

  • 你可以记录其他信息,例如图像、文本、直方图等

  • 通过探索不同的可视化内容,你可以深入了解模型的表现,并进行必要的调整

  • Tensorboard 与 PyTorch Lightning 的集成简化了监控过程,使得模型开发更加高效

使用tensorboard与 PyTorch Lightning 提供了一个强大的解决方案,用于监控和可视化训练过程,使得在模型开发中可以做出更明智的决策。

评估用于预测的深度神经网络

评估预测模型的表现对于理解它们如何对未见数据进行泛化至关重要。常用的评估指标包括均方根误差RMSE)、平均绝对百分比误差MAPE)、平均绝对缩放误差MASE)和对称平均绝对百分比误差SMAPE)等。我们将使用 Python 实现这些指标,并向您展示如何应用它们来评估模型的表现。

准备好了吗

我们需要来自训练模型的预测值和相应的真实值,以计算这些指标。因此,我们必须先在测试集上运行我们的模型,以获取预测结果。

为了简化实现,我们将使用scikit-learnsktime库,因为它们提供了有用的类和方法来帮助我们完成这个任务。由于我们还没有安装sktime,请运行以下命令:

pip install sktime

现在,是时候导入用于不同评估指标的类和方法了:

from sklearn.metrics import mean_squared_error
from sktime.performance_metrics.forecasting 
import mean_absolute_scaled_error, MeanAbsolutePercentageError
import numpy as np

如何实现…

为了评估我们的模型表现,我们必须计算scikit-learn库中的相关指标。对于sktime库,它提供了现成可用的函数来计算这些指标。

以下是计算这些指标的代码:

def mean_absolute_percentage_error(y_true, y_pred):
    y_true, y_pred = np.array(y_true), np.array(y_pred)
    return np.mean(np.abs((y_true - y_pred) / y_true)) * 100
y_pred = model(X_test).detach().numpy()
y_true = y_test.detach().numpy()
rmse_sklearn = np.sqrt(mean_squared_error(y_true, y_pred)) print(f"RMSE (scikit-learn): {rmse_sklearn}")
mape = mean_absolute_percentage_error(y_true, y_pred) 
print(f"MAPE: {mape}")
mase_sktime = mean_absolute_scaled_error(y_true, y_pred) 
print(f"MASE (sktime): {mase_sktime}")
smape_sktime = symmetric_mean_absolute_percentage_error
    (y_true, y_pred)
 print(f"SMAPE (sktime): {smape_sktime}")

它是如何工作的…

这些指标各自评估模型表现的不同方面:

  • RMSE:该指标计算预测值与实际值之间的平均平方差的平方根。它对较大误差给予更高的惩罚。

  • 1表示与朴素预测相等的表现,而 MASE 值小于1则表示模型表现优于朴素预测。

  • MAPE:该指标计算实际值与预测值之间绝对百分比差异的平均值。它以百分比的形式表达平均绝对误差,这在您想了解相对预测误差时非常有用。

  • SMAPE:该指标计算平均绝对百分比误差,处理低估和高估的误差时给予同等的权重。它将误差表示为实际值的百分比,这对于比较模型和预测不同规模的数据非常有用。

还有更多内容…

记住,评估指标的选择取决于具体问题和业务需求。例如,如果低估模型的成本高于高估模型的成本,那么一个区分这两种误差类型的指标可能更合适。根据问题的不同,其他指标,如 MAE,也可以使用。使用多种指标评估模型始终是个好主意,这样可以更全面地了解模型的表现。

使用回调——EarlyStopping

在 PyTorch Lightning 中,回调是可重用的组件,允许你在训练、验证和测试的各个阶段注入自定义行为。它们提供了一种将功能与主训练逻辑分离的方式,提供了一个模块化和可扩展的方法来管理辅助任务,例如记录指标、保存检查点、早停等。

通过定义一个自定义类继承自 PyTorch Lightning 的基础Callback类,你可以重写与训练过程中的不同阶段相对应的特定方法,例如on_epoch_starton_batch_end。当训练器初始化时,如果传入一个或多个这些回调对象,定义的行为将自动在训练过程中的相应阶段执行。这使得回调成为组织训练管道的强大工具,能够增加灵活性而不使主训练代码变得混乱。

准备工作

在定义并训练 LSTM 模型后,如上一节所述,我们可以通过引入早停技术进一步增强训练过程。该技术通过在指定的指标停止改进时暂停训练过程来避免过拟合。为此,PyTorch Lightning 提供了一个早停回调,我们将把它集成到现有的训练代码中。

如何操作…

要应用早停,我们需要通过添加EarlyStopping回调来修改现有的 PyTorch Lightning Trainer。下面是实现该功能的代码:

import lightning.pytorch as pl
from lightning.pytorch.callbacks import EarlyStopping
early_stop_callback = EarlyStopping(
    monitor="val_loss",
    min_delta=0.00,
    patience=3,
    verbose=False,
    mode="min"
)
trainer = pl.Trainer(max_epochs=100,
                     callbacks=[early_stop_callback]) 
trainer.fit(model, datamodule)

在这段代码中,monitor设置为验证损失(val_loss),如果该值在patience连续的验证周期中没有至少减少min_delta,训练过程将停止。

它是如何工作的…

早停是一种正则化技术,可以防止神经网络的过拟合。它监控一个指定的指标(在这里是验证损失),并在该指标停止改进时暂停训练过程。

这是在我们 LSTM 模型中的工作方式:

  • val_loss)在验证阶段。

  • 对于patience连续的训练周期,若min_delta未达到要求,训练过程将被暂停。

  • mode参数可以设置为minmax,表示被监控的指标应该最小化还是最大化。在我们的案例中,我们希望最小化验证损失。

通过提前停止训练过程,我们可以节省时间和资源,并且可能获得一个在未见数据上泛化更好的模型。

还有更多内容…

让我们看看一些进一步的细节:

  • 早停回调(early stopping callback)是高度可配置的,允许你根据特定需求调整其行为——例如,你可以更改patience参数,使得停止标准更加严格或宽松。

  • 早停可以与其他回调和技术结合使用,例如模型检查点(model checkpointing),以创建一个强大而高效的训练管道。

  • 适当地使用早停可以使模型在未见数据上表现更好,因为它能防止模型过拟合训练数据。

这个EarlyStopping回调与 PyTorch Lightning 以及我们现有的 LSTM 模型完美集成,展示了 PyTorch Lightning 回调系统的可扩展性和易用性。

第五章:全球预测模型

在本章中,我们将探讨各种时间序列预测场景,并学习如何使用深度学习处理这些场景。这些场景包括多步和多输出预测任务,以及涉及多个时间序列的问题。我们将涵盖这些案例,解释如何准备数据、训练适当的神经网络模型,并对其进行验证。

本章结束时,你应该能够为不同的时间序列数据集构建深度学习预测模型。这包括超参数优化,这是模型开发中的重要阶段。

本章将引导你完成以下配方:

  • 多变量时间序列的多步预测

  • 多变量时间序列的多步和多输出预测

  • 为全局模型准备多个时间序列

  • 使用多个时间序列训练全局 LSTM

  • 季节性时间序列的全球预测模型

  • 使用 Ray Tune 进行超参数优化

技术要求

本章需要以下 Python 库:

  • numpy(1.26.3)

  • pandas(2.0.3)

  • scikit-learn(1.4.0)

  • sktime(0.26.0)

  • torch(2.2.0)

  • pytorch-forecasting(1.0.0)

  • pytorch-lightning(2.1.4)

  • gluonts(0.14.2)

  • ray(2.9.2)

你可以使用 pip 一次性安装这些库:

pip install -U pandas numpy scikit-learn sktime torch pytorch-forecasting pytorch-lightning gluonts

本章中的配方将遵循基于 PyTorch Lightning 的设计理念,这种理念提供了一种模块化和灵活的方式来构建和部署 PyTorch 模型。有关本章代码,可以在以下 GitHub URL 中找到:github.com/PacktPublishing/Deep-Learning-for-Time-Series-Data-Cookbook

多变量时间序列的多步预测

到目前为止,我们一直在处理单一变量时间序列的下一个值预测。预测下一个观测值的值被称为一步预测。在本配方中,我们将扩展上一章中开发的模型,以进行多步预测。

准备工作

多步预测是提前预测多个观测值的过程。这个任务对于减少时间序列的长期不确定性非常重要。

事实证明,我们之前所做的大部分工作也适用于多步预测的设置。TimeSeriesDataSet 类使得将一步预测问题扩展到多步预测变得非常简单。

在本配方中,我们将预测范围设置为 7,并将滞后期数设置为 14

N_LAGS = 7
HORIZON = 14

实际上,这意味着预测任务是基于过去 14 天的数据来预测未来 7 天的太阳辐射。

如何实现…

对于多步预测问题,需要改变两件事:

  • 其中一项是神经网络模型的输出维度。与表示下一个值的1不同,输出维度需要与预测步数相匹配。这可以通过模型中的 output_dim 变量来实现。

  • 数据模块的预测长度需要设置为预测时段。这可以通过TimeSeriesDataSet类中的max_prediction_length参数来完成。

这两个输入可以按如下方式传递给数据和模型模块:

datamodule = MultivariateSeriesDataModule(data=mvtseries,
    n_lags=N_LAGS,
    horizon=HORIZON,
    batch_size=32,
    test_size=0.3)
model = MultivariateLSTM(input_dim=n_vars,
    hidden_dim=32,
    num_layers=1,
    output_dim=HORIZON)

然后,模型的训练和测试保持不变:

early_stop_callback = EarlyStopping(monitor="val_loss",
    min_delta=1e-4,
    patience=10,
    verbose=False,
    mode="min")
trainer = Trainer(max_epochs=20, callbacks=[early_stop_callback])
trainer.fit(model, datamodule)
trainer.test(model=model, datamodule=datamodule)

我们训练了模型 20 个周期,然后在测试集上评估了它,测试集通过数据模块中定义的数据加载器进行获取。

它是如何工作的…

传统的监督学习模型通常从一维目标变量中学习。在预测问题中,这个变量可以是,例如,下一个时间段时间序列的值。然而,多步预测问题需要在每个时间点预测多个值。深度学习模型天生就是多输出算法。因此,它们可以使用一个模型处理多个目标变量。

其他针对多步预测的方法通常涉及创建多个模型或将相同的模型用于不同的预测时段。然而,多输出方法更为可取,因为它能够捕捉不同预测时段之间的依赖关系。这可能带来更好的预测性能,正如以下文章所记录的那样:Taieb, Souhaib Ben 等,基于 NN5 预测竞赛的多步时间序列预测策略回顾与比较。《专家系统与应用》39.8(2012):7067-7083

还有更多…

我们可以使用深度学习神经网络进行多步预测的其他方法有很多。以下是另外三种流行的方法:

  • 递归: 训练一个神经网络进行一步预测,并通过递归方式使用它进行多步预测

  • 直接: 为每个预测时段训练一个神经网络

  • DirRec: 为每个预测时段训练一个神经网络,并将前一个预测结果作为输入传递给下一个预测

使用多元时间序列进行多步和多输出预测

在这个案例中,我们将扩展 LSTM 模型,以预测多元时间序列的多个变量的多个时间步。

准备工作

到目前为止,在这一章节中,我们已经构建了多个模型来预测某一特定变量——太阳辐射的未来。我们利用时间序列中的额外变量来改善太阳辐射的建模。

然而,在处理多元时间序列时,我们通常关心的是预测多个变量,而不仅仅是一个。一个常见的例子是在处理时空数据时出现的。时空数据集是多元时间序列的一个特例,其中在不同位置观察到一个现实世界的过程。在这种数据集中,目标是预测所有这些位置的未来值。同样,我们可以利用神经网络是多输出算法的特点,在一个模型中处理多个目标变量。

在这个例子中,我们将继续使用太阳辐射数据集,和之前的例子一样。不过,我们的目标是预测三个变量的未来值——太阳辐射、蒸气压和气温:

N_LAGS = 14
HORIZON = 7
TARGET = ['Incoming Solar', 'Air Temp', 'Vapor Pressure']
mvtseries = pd.read_csv('assets/daily_multivariate_timeseries.csv',
    parse_dates=['datetime'],
    index_col='datetime')

关于数据准备,过程与我们之前所做的类似。不同之处在于,我们将目标变量(TARGET)设置为前面列出的变量,而不是仅仅设置为太阳辐射。TimeSeriesDataSet类和数据模块会处理所有的预处理和数据共享工作。

如何实现…

我们首先调整数据模块,以处理多个目标变量。下面的代码展示了我们所做的必要更改。让我们从定义模块的构造函数开始:

class MultivariateSeriesDataModule(pl.LightningDataModule):
    def __init__(
            self,
            data: pd.DataFrame,
            target_variables: List[str],
            n_lags: int,
            horizon: int,
            test_size: float = 0.2,
            batch_size: int = 16,
    ):
        super().__init__()
        self.data = data
        self.batch_size = batch_size
        self.test_size = test_size
        self.n_lags = n_lags
        self.horizon = horizon
        self.target_variables = target_variables
        self.target_scaler = {k: MinMaxScaler() 
            for k in target_variables}
        self.feature_names = [col for col in data.columns
            if col not in self.target_variables]
        self.training = None
        self.validation = None
        self.test = None
        self.predict_set = None
        self.setup()

构造函数包含了一个新的参数target_variables,我们用它来传递目标变量的列表。除此之外,我们还对self.target_scaler属性做了小改动,现在它是一个字典对象,包含了每个目标变量的缩放器。接着,我们构建了如下的setup()方法:

def setup(self, stage=None):
    self.preprocess_data()
    train_indices, val_indices, test_indices = self.split_data()
    train_df = self.data.loc
        [self.data["time_index"].isin(train_indices)]
    val_df = self.data.loc[self.data["time_index"].isin(val_indices)]
    test_df = self.data.loc
        [self.data["time_index"].isin(test_indices)]
    for c in self.target_variables:
        self.target_scaler[c].fit(train_df[[c]])
    self.scale_target(train_df, train_df.index)
    self.scale_target(val_df, val_df.index)
    self.scale_target(test_df, test_df.index)
    self.training = TimeSeriesDataSet(
        train_df,
        time_idx="time_index",
        target=self.target_variables,
        group_ids=["group_id"],
        max_encoder_length=self.n_lags,
        max_prediction_length=self.horizon,
        time_varying_unknown_reals=self.feature_names + 
            self.target_variables,
        scalers={name: MinMaxScaler() for name in self.feature_names},
    )
    self.validation = TimeSeriesDataSet.from_dataset
        (self.training, val_df)
    self.test = TimeSeriesDataSet.from_dataset(self.training, test_df)
    self.predict_set = TimeSeriesDataSet.from_dataset(
        self.training, self.data, predict=True
    )

与之前的例子相比,主要的不同点如下。我们将目标变量的列表传递给TimeSeriesDataSet类的目标输入。目标变量的缩放过程也变更为一个for循环,遍历每个目标变量。

我们还更新了模型模块,以处理多个目标变量。让我们从构造函数和forward()方法开始:

class MultiOutputLSTM(LightningModule):
    def __init__(self, input_dim, hidden_dim, num_layers, 
        horizon, n_output):
        super().__init__()
        self.n_output = n_output
        self.horizon = horizon
        self.hidden_dim = hidden_dim
        self.input_dim = input_dim
        self.output_dim = int(self.n_output * self.horizon)
        self.lstm = nn.LSTM(input_dim, hidden_dim,
            num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_dim, self.output_dim)
    def forward(self, x):
        h0 = torch.zeros(self.lstm.num_layers, x.size(0),
            self.hidden_dim).to(self.device)
        c0 = torch.zeros(self.lstm.num_layers, x.size(0),
            self.hidden_dim).to(self.device)
        out, _ = self.lstm(x, (h0, c0))
        out = self.fc(out[:, -1, :])
        return out

forward()方法与上一章相同。我们在构造函数中存储了一些额外的元素,比如预测时间跨度(self.horizon),因为它们在后续步骤中是必要的:

    def training_step(self, batch, batch_idx):
        x, y = batch
        y_pred = self(x['encoder_cont'])
        y_pred = y_pred.unsqueeze(-1).view(-1, self.horizon, 
            self.n_output)
        y_pred = [y_pred[:, :, i] for i in range(self.n_output)]
        loss = [F.mse_loss(y_pred[i], 
            y[0][i]) for i in range(self.n_output)]
        loss = torch.mean(torch.stack(loss))
        self.log('train_loss', loss)
        return loss
    def test_step(self, batch, batch_idx):
        x, y = batch
        y_pred = self(x['encoder_cont'])
        y_pred = y_pred.unsqueeze(-1).view(-1, self.horizon, 
            self.n_output)
        y_pred = [y_pred[:, :, i] for i in range(self.n_output)]
        loss = [F.mse_loss(y_pred[i],
            y[0][i]) for i in range(self.n_output)]
        loss = torch.mean(torch.stack(loss))
        self.log('test_loss', loss)
    def predict_step(self, batch, batch_idx, dataloader_idx=0):
        x, y = batch
        y_pred = self(x['encoder_cont'])
        y_pred = y_pred.unsqueeze(-1).view(-1,
            self.horizon, self.n_output)
        y_pred = [y_pred[:, :, i] for i in range(self.n_output)]
        return y_pred
    def configure_optimizers(self):
        return torch.optim.Adam(self.parameters(), lr=0.001)

让我们来分析一下前面的代码:

  • 我们向构造函数添加了一个n_output参数,详细说明了目标变量的数量(在本例中是3

  • 输出维度设置为目标变量数乘以预测时间跨度(self.n_output * self.horizon

  • 在训练和测试步骤中处理数据时,预测结果会被重新调整为合适的格式(批大小、时间跨度和变量数)

  • 我们为每个目标变量计算 MSE 损失,然后使用torch.mean(torch.stack(loss))对它们取平均。

然后,剩余的过程与我们在之前基于 PyTorch Lightning 的例子中所做的类似:

model = MultiOutputLSTM(input_dim=n_vars,
    hidden_dim=32,
    num_layers=1,
    horizon=HORIZON,
    n_vars=len(TARGET))
datamodule = MultivariateSeriesDataModule(data=mvtseries,
    n_lags=N_LAGS,
    horizon=HORIZON,
    target_variables=TARGET)
early_stop_callback = EarlyStopping(monitor="val_loss",
    min_delta=1e-4,
    patience=10,
    verbose=False,
    mode="min")
trainer = pl.Trainer(max_epochs=20, callbacks=[early_stop_callback])
trainer.fit(model, datamodule)
trainer.test(model=model, datamodule=datamodule)
forecasts = trainer.predict(model=model, datamodule=datamodule)

工作原理…

本例中使用的建模方法遵循了向量自回归VAR)的思想。VAR 通过将多元时间序列中各变量的未来值建模为这些变量过去值的函数来工作。预测多个变量在多个场景下可能很有意义,例如时空预测。

在本节中,我们将 VAR 原理应用于深度学习环境,特别是通过使用 LSTM 网络。与传统的 VAR 模型根据过去的观察线性预测未来值不同,我们的深度学习模型能够捕捉多时间步和多个变量之间的非线性关系和时间依赖性。

为了计算我们模型的loss函数——这对于训练和评估模型性能至关重要——我们需要对training_step()test_step()方法做一些修改。在网络生成预测后,我们按变量对输出进行分段。这种分段允许我们分别计算每个变量的 MSE 损失。然后,这些单独的损失会被聚合,形成一个复合损失度量,指导模型的优化过程。

为全球模型准备多个时间序列

现在,是时候开始处理涉及多个时间序列的时间序列问题了。在本节中,我们将学习全球预测模型的基本原理及其工作方式。我们还将探索如何为预测准备包含多个时间序列的数据集。同样,我们利用TimeSeriesDataSetDataModule类的功能来帮助我们完成这项任务。

准备开始

到目前为止,我们一直在处理涉及单一数据集的时间序列问题。现在,我们将学习全球预测模型,包括以下内容:

  • 从本地模型到全球模型的过渡:最初,我们在时间序列预测中只处理单一数据集,其中模型根据一个系列的历史数据预测未来值。这些所谓的本地模型是针对特定时间序列量身定制的,而全球模型则涉及处理多个相关的时间序列,并捕捉它们之间的相关信息。

  • 利用神经网络:神经网络在数据丰富的环境中表现出色,使其成为全球预测的理想选择。这在零售等领域尤为有效,在这些领域中,了解不同产品销售之间的关系可以带来更准确的预测。

我们将学习如何使用一个关于运输的数据集构建全球预测模型,名为NN5。这个数据集曾在一个先前的预测竞赛中使用,包括 111 个不同的时间序列。

数据可以通过gluonts Python 库获得,并可以通过以下方式加载:

N_LAGS = 7
HORIZON = 7
from gluonts.dataset.repository.datasets import get_dataset
dataset = get_dataset('nn5_daily_without_missing', regenerate=False)

这里是数据集中五个时间序列的样本:

图 5.1:NN5 时间序列数据集样本

图 5.1:NN5 时间序列数据集样本

此数据集的原始来源可以在以下链接找到:zenodo.org/records/3889750

现在,让我们构建一个DataModule类来处理数据预处理步骤。

如何实现……

我们将构建一个LightningDataModule类,处理包含多个时间序列的数据集,并将其传递给模型。以下是构造函数的样子:

import lightning.pytorch as pl
class GlobalDataModule(pl.LightningDataModule):
    def __init__(self,
                 data,
                 n_lags: int,
                 horizon: int,
                 test_size: float,
                 batch_size: int):
        super().__init__()
        self.data = data
        self.batch_size = batch_size
        self.test_size = test_size
        self.n_lags = n_lags
        self.horizon = horizon
        self.training = None
        self.validation = None
        self.test = None
        self.predict_set = None
        self.target_scaler = LocalScaler()

本质上,我们存储了训练和使用模型所需的元素。这包括基于LocalScaler类的self.target_scaler属性。

LocalScaler类的主要方法是transform()

def transform(self, df: pd.DataFrame):
    df = df.copy()
    df["value"] = LogTransformation.transform(df["value"])
    df_g = df.groupby("group_id")
    scaled_df_l = []
    for g, df_ in df_g:
        df_[["value"]] = self.scalers[g].transform(df_[["value"]])
        scaled_df_l.append(df_)
    scaled_df = pd.concat(scaled_df_l)
    scaled_df = scaled_df.sort_index()
    return scaled_df

此方法对数据集应用了两种预处理操作:

  • 对时间序列进行对数转换以稳定方差

  • 对数据集中每个时间序列进行标准化

你可以扩展此类,以包括你需要对数据集执行的任何转换。LocalScaler类的完整实现可以在 GitHub 仓库中找到。

接着,我们在setup()函数中对数据进行了预处理:

def setup(self, stage=None):
    data_list = list(self.data.train)
    data_list = [pd.Series(ts['target'],
        index=pd.date_range(start=ts['start'].to_timestamp(),
        freq=ts['start'].freq,
        periods=len(ts['target'])))
        for ts in data_list]
    tseries_df = pd.concat(data_list, axis=1)
    tseries_df['time_index'] = np.arange(tseries_df.shape[0])
    ts_df = tseries_df.melt('time_index')
    ts_df = ts_df.rename(columns={'variable': 'group_id'})
    unique_times = ts_df['time_index'].sort_values().unique()
    tr_ind, ts_ind = \
        train_test_split(unique_times,
            test_size=self.test_size,
            shuffle=False)
    tr_ind, vl_ind = \
        train_test_split(tr_ind,
            test_size=0.1,
            shuffle=False)
    training_df = ts_df.loc[ts_df['time_index'].isin(tr_ind), :]
    validation_df = ts_df.loc[ts_df['time_index'].isin(vl_ind), :]
    test_df = ts_df.loc[ts_df['time_index'].isin(ts_ind), :]
    self.target_scaler.fit(training_df)
    training_df = self.target_scaler.transform(training_df)
    validation_df = self.target_scaler.transform(validation_df)
    test_df = self.target_scaler.transform(test_df)
    self.training = TimeSeriesDataSet(
        data=training_df,
        time_idx='time_index',
        target='value',
        group_ids=['group_id'],
        max_encoder_length=self.n_lags,
        max_prediction_length=self.horizon,
        time_varying_unknown_reals=['value'],
    )
    self.validation = TimeSeriesDataSet.from_dataset
        (self.training, validation_df)
    self.test = TimeSeriesDataSet.from_dataset(self.training, test_df)
    self.predict_set = TimeSeriesDataSet.from_dataset
        (self.training, ts_df, predict=True)

在前面的代码中,我们将数据分成了训练集、验证集、测试集和预测集,并设置了相应的TimeSeriesDataSet实例。最后,数据加载器与我们在之前的实例中做的类似:

    def train_dataloader(self):
        return self.training.to_dataloader(batch_size=self.batch_size,
            shuffle=False)
    def val_dataloader(self):
        return self.validation.to_dataloader
            (batch_size=self.batch_size, shuffle=False)
    def test_dataloader(self):
        return self.test.to_dataloader(batch_size=self.batch_size,
            shuffle=False)
    def predict_dataloader(self):
        return self.predict_set.to_dataloader(batch_size=1,
            shuffle=False)

我们可以像下面这样调用数据模块:

datamodule = GlobalDataModule(data=dataset,
    n_lags=N_LAGS,
    horizon=HORIZON,
    test_size=0.2,
    batch_size=1)

使用此模块,数据集中的每个独立时间序列都将以使用最后N_LAGS个值来预测下一个HORIZON个观察值的方式进行处理。

它是如何工作的……

全局方法在多个时间序列上进行训练。其思路是不同时间序列之间存在共同模式。因此,神经网络可以利用这些序列的观察值来训练更好的模型。

在前一节中,我们通过get_dataset()函数从gluonts Python 库中检索了一个包含多个时间序列的数据集。准备一个包含多个时间序列的监督学习数据集的过程与我们之前做的类似。TimeSeriesDataSet实例的关键输入是group_id变量,它详细说明了每个观察值所属的实体。

主要的工作发生在setup()方法中。首先,我们将数据集转换为具有长格式的pandas DataFrame。以下是该数据的示例:

图 5.2:NN5 时间序列数据集的长格式示例

图 5.2:NN5 时间序列数据集的长格式示例

在这种情况下,group_id列不是常量,它详细说明了观察值所对应的时间序列。由于每个时间序列是单变量的,因此有一个名为value的数值变量。

使用多个时间序列训练全局 LSTM

在前一个实例中,我们学习了如何为全局预测模型准备多个时间序列的监督学习数据集。在本实例中,我们将继续这一主题,描述如何训练一个全局 LSTM 神经网络进行预测。

准备工作

我们将继续使用在前一个实例中使用的数据模块:

N_LAGS = 7
HORIZON = 7
from gluonts.dataset.repository.datasets import get_dataset, dataset_names
dataset = get_dataset('nn5_daily_without_missing', regenerate=False)
datamodule = GlobalDataModule(data=dataset,
    n_lags=N_LAGS,
    horizon=HORIZON,
    batch_size=32,
    test_size=0.3)

让我们看看如何创建一个 LSTM 模块来处理包含多个时间序列的数据模块。

如何做到……

我们创建了一个包含 LSTM 实现的LightningModule类。首先,让我们看一下类的构造函数和forward()方法:

class GlobalLSTM(pl.LightningModule):
    def __init__(self, input_dim, hidden_dim, num_layers, output_dim):
        super().__init__()
        self.hidden_dim = hidden_dim
        self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers, 
            batch_first=True)
        self.fc = nn.Linear(hidden_dim, output_dim)
    def forward(self, x):
        h0 = torch.zeros(self.lstm.num_layers, x.size(0), 
            self.hidden_dim).to(self.device)
        c0 = torch.zeros(self.lstm.num_layers, x.size(0), 
            self.hidden_dim).to(self.device)
        out, _ = self.lstm(x, (h0, c0))
        out = self.fc(out[:, -1, :])
        return out

神经网络的逻辑与我们之前处理单一时间序列数据集时所做的相似。对于剩下的方法也是如此:

    def training_step(self, batch, batch_idx):
        x, y = batch
        y_pred = self(x['encoder_cont'])
        loss = F.mse_loss(y_pred, y[0])
        self.log('train_loss', loss)
        return loss
    def validation_step(self, batch, batch_idx):
        x, y = batch
        y_pred = self(x['encoder_cont'])
        loss = F.mse_loss(y_pred, y[0])
        self.log('val_loss', loss)
        return loss
    def test_step(self, batch, batch_idx):
        x, y = batch
        y_pred = self(x['encoder_cont'])
        loss = F.mse_loss(y_pred, y[0])
        self.log('test_loss', loss)
    def predict_step(self, batch, batch_idx, dataloader_idx=0):
        x, y = batch
        y_pred = self(x['encoder_cont'])
        return y_pred
    def configure_optimizers(self):
        return torch.optim.Adam(self.parameters(), lr=0.01)

接下来,我们可以调用模型并进行训练,如下所示:

model = GlobalLSTM(input_dim=1,
    hidden_dim=32,
    num_layers=1,
    output_dim=HORIZON)
early_stop_callback = EarlyStopping(monitor="val_loss",
    min_delta=1e-4,
    patience=10,
    verbose=False,
    mode="min")
trainer = pl.Trainer(max_epochs=20, callbacks=[early_stop_callback])
trainer.fit(model, datamodule)
trainer.test(model=model, datamodule=datamodule)
forecasts = trainer.predict(model=model, datamodule=datamodule)

使用 PyTorch Lightning 设计,训练、测试和预测步骤与我们在其他基于该框架的食谱中所做的类似。

它是如何工作的…

如您所见,包含 LSTM 的LightningModule类与我们为单一多变量时间序列构建的完全相同。这个类仅处理模型定义的部分,因此无需更改。主要工作是在数据预处理阶段完成的。因此,我们只需要修改数据模块中的setup()方法,以反映之前食谱中所解释的必要更改。

我们从一个本地 LSTM 模型过渡到了一个全球 LSTM 模型,后者能够同时处理多个时间序列。主要的区别在于数据的准备和呈现方式,而不是神经网络架构本身的变化。无论是本地模型还是全球模型,都使用相同的 LSTM 结构,其特点是能够处理数据序列并预测未来值。

在本地 LSTM 设置中,模型的输入通常遵循结构[batch_sizesequence_lengthnum_features],输出的形状与预测的时间跨度匹配,通常为[batch_sizehorizon]。此设置非常直观,因为它处理的是来自单一序列的数据。

转向全球 LSTM 模型后,输入和输出配置在维度上保持基本一致。然而,现在输入聚合了多个时间序列的信息。它增强了神经网络学习新模式和依赖关系的能力,不仅限于单一序列,还跨越多个序列。因此,全球 LSTM 模型的输出旨在同时为多个时间序列生成预测,反映整个数据集的预测结果。

全球预测模型用于季节性时间序列

本食谱展示了如何扩展数据模块,将额外的解释变量包含在TimeSeriesDataSet类和DataModule类中。我们将使用一个关于季节性时间序列的特定案例。

准备工作

我们加载了在上一个食谱中使用的那个数据集:

N_LAGS = 7
HORIZON = 7
from gluonts.dataset.repository.datasets import get_dataset
dataset = get_dataset('nn5_daily_without_missing', regenerate=False)

该数据集包含了日粒度的时间序列。在这里,我们将使用Fourier级数建模周季节性。与我们在上一章中所做的不同(在处理季节性:季节虚拟变量和 Fourier 级数一节中),我们将学习如何使用TimeSeriesDataSet框架来包含这些特征。

如何操作…

这是更新后的DataModule,包含了Fourier级数。为了简洁起见,我们只描述了setup()方法的一部分,其余方法保持不变,您可以在 GitHub 仓库中查看:

from sktime.transformations.series.fourier import FourierFeatures
def setup(self, stage=None):
    […]
    fourier = FourierFeatures(sp_list=[7],
        fourier_terms_list=[2],
        keep_original_columns=False)
    fourier_features = fourier.fit_transform(ts_df['index'])
    ts_df = pd.concat
        ([ts_df, fourier_features], axis=1).drop('index', axis=1)
    […]
    self.training = TimeSeriesDataSet(
        data=training_df,
        time_idx='time_index',
        target='value',
        group_ids=['group_id'],
        max_encoder_length=self.n_lags,
        max_prediction_length=self.horizon,
        time_varying_unknown_reals=['value'],
        time_varying_known_reals=['sin_7_1', 'cos_7_1',
            'sin_7_2', 'cos_7_2']
    )

setup()方法中,我们使用数据集的日期和时间信息计算Fourier项。这样会生成四个确定性变量:sin_7_1cos_7_1sin_7_2cos_7_2。这些是我们用来建模季节性的Fourier级数。将它们通过pd.concat([tseries_long, fourier_features], axis=1)加入数据集后,我们使用time_varying_known_reals参数来告知这些特征随时间变化,但变化是可预测的。

在 LSTM 中,我们需要将输入维度更新为5,以反映数据集中变量的数量(目标变量加上四个Fourier级数)。这一步骤如下所示:

model = GlobalLSTM(input_dim=5,
    hidden_dim=32,
    num_layers=1,
    output_dim=HORIZON)
datamodule = GlobalDataModuleSeas(data=dataset,
    n_lags=N_LAGS,
    horizon=HORIZON,
    batch_size=128,
    test_size=0.3)
early_stop_callback = EarlyStopping(monitor="val_loss",
    min_delta=1e-4,
    patience=10,
    verbose=False,
    mode="min")
trainer = pl.Trainer(max_epochs=20, callbacks=[early_stop_callback])
trainer.fit(model, datamodule)
trainer.test(model=model, datamodule=datamodule)
forecasts = trainer.predict(model=model, datamodule=datamodule)

再次强调,训练和推理阶段与之前的步骤类似,因为这里唯一的不同是在数据模块处理的数据预处理阶段。

它是如何工作的……

使用Fourier级数建模季节性涉及通过傅里叶变换从数据集中提取额外变量来丰富数据集。这种方法在DataModule实例的setup()方法中实现,将这些变量并入到TimeSeriesDataSet对象中。

Fourier级数分解使我们能够通过将复杂的周期性模式分解成更简单的正弦波来捕捉季节性。Fourier级数的每个组件对应于不同的频率,捕捉时间序列数据中的不同季节性周期。这对神经网络特别有益,原因有几点:

  • Fourier级数作为自动特征工程,创建了直接编码周期性行为的有信息特征。这可以显著提升模型识别和预测季节性模式的能力,哪怕在复杂或嘈杂的数据中也能发挥作用。由于Fourier特征是加入到输入数据中的,它们可以与任何神经网络算法或架构兼容。

  • Fourier级数可以同时建模这些多重季节性水平,提供一种更为细致的数据表示,这种表示是传统季节性分解方法难以实现的。

  • 改进泛化能力:通过提供季节性的明确数学表示,Fourier特征帮助神经网络更好地从观测数据推断到未见的未来时期。这减少了过拟合噪声和数据异常的风险,使模型的学习更加专注于潜在的周期性趋势。

还有更多……

你可以访问以下网址,学习如何将额外的类别变量(如节假日)包含到数据集中:pytorch-forecasting.readthedocs.io/en/stable/tutorials/stallion.html#Load-data

使用 Ray Tune 进行超参数优化

神经网络有一些超参数,这些超参数定义了其结构和学习过程。超参数包括学习率、隐藏层的数量和单元数等。不同的超参数值会影响学习过程和模型的准确性。不恰当的值可能导致欠拟合或过拟合,从而降低模型的性能。因此,优化超参数值以最大限度地发挥深度学习模型的作用非常重要。在本教程中,我们将探讨如何使用 Ray Tune 进行超参数优化,包括学习率、正则化参数、隐藏层的数量等。这些参数的优化对于我们模型的表现至关重要。往往,由于超参数选择不当,我们在拟合神经网络模型时会得到较差的结果,这可能导致欠拟合或过拟合未见数据。

准备工作

在我们开始进行超参数优化之前,如果尚未安装 Ray Tune,我们需要先进行安装。可以使用以下命令:

pip install -U 'ray[data,train,tune,serve]'

我们将使用相同的数据和 LSTM 模型进行优化:

class GlobalDataModule(pl.LightningDataModule):
    ...
class GlobalLSTM(pl.LightningModule):
    ...
from ray.train.lightning import RayTrainReportCallback
from ray import tune
from ray.tune.schedulers import ASHAScheduler
from ray.train import RunConfig, ScalingConfig, CheckpointConfig
from ray.train.torch import TorchTrainer

在上述代码中,我们还导入了所有本教程所需的库。

如何实现…

让我们讨论一下如何使用 Ray Tune 实现超参数优化:

  1. 定义搜索空间:首先,定义你想要探索的超参数空间。

  2. 配置 Ray Tune:初始化 Tune 实验,设置所需的设置,例如试验次数、资源等。

  3. 运行优化:通过传递训练函数和定义的搜索空间来执行实验。

  4. 分析结果:利用 Ray Tune 的工具分析结果,并确定最佳超参数。

让我们首先定义搜索空间:

search_space = {
    "hidden_dim": tune.choice([8, 16, 32]),
    "num_layers": tune.choice([1, 2]),
}

在这个示例中,我们只优化两个参数:LSTM 神经网络中隐藏单元的数量和层数。

然后,我们在一个函数中定义训练循环:

def train_tune(config_hyper):
    hidden_dim = config_hyper["hidden_dim"]
    num_layers = config_hyper["num_layers"]
    model = GlobalLSTM(input_dim=1,
        hidden_dim=hidden_dim,
        output_dim=HORIZON,
        num_layers=num_layers)
    data_module = GlobalDataModule(dataset,
        n_lags=N_LAGS,
        horizon=HORIZON,
        batch_size=128,
        test_size=0.3)
    trainer = Trainer(callbacks=[RayTrainReportCallback()])
    trainer.fit(model, data_module)

定义完训练函数后,我们将其传递给 TorchTrainer 类实例,并与运行配置一起使用:

scaling_config = ScalingConfig(
    num_workers=2, use_gpu=False, 
        resources_per_worker={"CPU": 1, "GPU": 0}
)
run_config = RunConfig(
    checkpoint_config=CheckpointConfig(
        num_to_keep=1,
        checkpoint_score_attribute="val_loss",
        checkpoint_score_order="min",
    ),
)
ray_trainer = TorchTrainer(
    train_tune,
    scaling_config=scaling_config,
    run_config=run_config,
)

ScalingConfig 实例中,我们配置了计算环境,指定了该过程是否应在 GPU 或 CPU 上运行、分配的工作节点数量以及每个工作节点的资源。同时,RunConfig 实例用于定义优化过程,包括在此过程中应监控的指标。

然后,我们创建一个 Tuner 实例,将这些信息结合在一起:

scheduler = ASHAScheduler(max_t=30, grace_period=1, reduction_factor=2)
tuner = tune.Tuner(
    ray_trainer,
    param_space={"train_loop_config": search_space},
    tune_config=tune.TuneConfig(
        metric="val_loss",
        mode="min",
        num_samples=10,
        scheduler=scheduler,
    ),
)

Tuner 实例需要一个调度器作为其输入之一。为此,我们使用 ASHAScheduler,它采用异步成功折半算法ASHA)高效地在不同配置之间分配资源。这种方法通过根据性能反复缩小搜索空间来帮助识别最有效的配置。最终,通过运行这个过程,我们可以确定最佳配置:

results = tuner.fit()
best_model_conf = \
    results.get_best_result(metric='val_loss', mode='min')

在前面的代码中,我们获得了最小化验证损失的配置。

在根据验证损失选择最佳超参数后,我们可以在测试集上评估模型。从检查点中获取模型权重,并加载调优过程中的最佳超参数。然后,使用这些参数加载模型并在测试数据上进行评估:

path = best_model_conf.get_best_checkpoint(metric='val_loss',
    mode='min').path
config = best_model_conf.config['train_loop_config']
best_model = \
    GlobalLSTM.load_from_checkpoint(checkpoint_path=f'{path}/
        checkpoint.ckpt',
        **config)
data_module = GlobalDataModule(dataset, n_lags=7, horizon=3)
trainer = Trainer(max_epochs=30)
trainer.test(best_model, datamodule=data_module)

在前面的代码中,我们加载了具有最佳配置的模型,并在DataModule类中定义的测试集上进行测试。

它是如何工作的……

我们的超参数优化过程包括定义搜索空间、配置和执行优化,以及分析结果。本节共享的代码片段提供了一个逐步指南,说明如何将 Ray Tune 集成到任何机器学习工作流中,从而帮助我们探索并找到最适合模型的超参数:

  • search_space字典定义了超参数搜索空间。

  • train_tune()函数封装了训练过程,包括模型配置、数据准备和拟合。

  • ScalingConfig类定义了优化过程的计算环境,例如是否在 GPU 或 CPU 上运行。

  • RunConfig类设置了优化的执行方式,例如在此过程中应跟踪的指标。

  • ASHAScheduler类是一个调度器,定义了如何从不同的可能配置中进行选择。

Ray Tune 通过使用诸如随机搜索、网格搜索或更先进的算法(如 ASHA)等多种算法,高效地探索超参数空间。它并行化试验以有效利用可用资源,从而加速搜索过程。

还有更多……

Ray Tune 提供了几个额外的功能和优势。它可以与其他库集成,使其与流行的机器学习框架(如 PyTorch、TensorFlow 和 Scikit-Learn)兼容。此外,它还提供了先进的搜索算法,如贝叶斯优化和基于群体的训练,使用户能够灵活地尝试不同的优化策略。最后,Ray Tune 支持可视化工具,允许用户利用 TensorBoard 或 Ray 提供的自定义工具有效地可视化和分析超参数搜索过程。

第六章:用于时间序列预测的高级深度学习架构

在前面的章节中,我们学习了如何使用不同类型的神经网络创建预测模型,但到目前为止,我们只处理了基本的架构,如前馈神经网络或 LSTM。本章将介绍如何使用最先进的方法,如 DeepAR 或 Temporal Fusion Transformers 来构建预测模型。这些方法由 Google 和 Amazon 等科技巨头开发,并已在不同的 Python 库中提供。这些先进的深度学习架构旨在解决各种类型的预测问题。

我们将涵盖以下几个食谱:

  • 使用 N-BEATS 进行可解释的预测

  • 使用 PyTorch Forecasting 优化学习率

  • 使用 GluonTS 入门

  • 使用 GluonTS 训练 DeepAR 模型

  • 使用 NeuralForecast 训练 Transformer

  • 使用 GluonTS 训练 Temporal Fusion Transformer

  • 使用 NeuralForecast 训练 Informer 模型

  • 使用 NeuralForecast 比较不同的 Transformer

到本章结束时,你将能够训练最先进的深度学习预测模型。

技术要求

本章需要以下 Python 库:

  • numpy(1.23.5)

  • pandas(1.5.3)

  • scikit-learn(1.2.1)

  • sktime(0.24.0)

  • torch(2.0.1)

  • pytorch-forecasting(1.0.0)

  • pytorch-lightning(2.1.0)

  • gluonts(0.13.5)

  • neuralforecast(1.6.0)

你可以通过 pip 一次性安装这些库:

pip install -U pandas numpy scikit-learn sktime torch pytorch-forecasting pytorch-lightning gluonts neuralforecast

本章的代码可以在以下 GitHub 地址找到:github.com/PacktPublishing/Deep-Learning-for-Time-Series-Data-Cookbook

使用 N-BEATS 进行可解释的预测

本食谱介绍了 用于可解释时间序列预测的神经基扩展分析N-BEATS),这是一种用于预测问题的深度学习方法。我们将向你展示如何使用 PyTorch Forecasting 训练 N-BEATS 并解释其输出。

准备工作

N-BEATS 特别设计用于处理多个单变量时间序列的问题。因此,我们将使用前一章中介绍的数据集(例如,参见 为全局模型准备多个时间序列 食谱):

import numpy as np
import pandas as pd
from gluonts.dataset.repository.datasets import get_dataset
from pytorch_forecasting import TimeSeriesDataSet
import lightning.pytorch as pl
from sklearn.model_selection import train_test_split
dataset = get_dataset('nn5_daily_without_missing', regenerate=False)
N_LAGS = 7
HORIZON = 7
datamodule = GlobalDataModule(data=dataset,
    n_lags=N_LAGS,
    horizon=HORIZON)

我们的目标是根据过去七个滞后值(N_LAGS),预测时间序列的下七个值(HORIZON)。

如何操作…

让我们创建训练、验证和测试数据集:

  1. 我们通过调用 GlobalDataModule 类中的 setup() 方法开始:

    datamodule.setup()
    
  2. N-BEATS 已经可以在 PyTorch Forecasting 中直接使用。你可以按如下方式定义模型:

    from pytorch_forecasting import NBeats
    model = NBeats.from_dataset(
        dataset=datamodule.training,
        stack_types=['trend', 'seasonality'],
        num_blocks=[3, 3],
        num_block_layers=[4, 4],
        widths=[256, 2048],
        sharing=[True],
        backcast_loss_ratio=1.0,
    )
    

    我们使用前面的代码通过 from_dataset() 方法创建了一个 NBeats 实例。以下参数需要定义:

    • dataset:包含训练集的 TimeSeriesDataSet 实例。

    • stack_types:你希望运行 N-BEATS 的模式。trendseasonality 类型的堆栈使得模型具有可解释性,而 ['generic'] 设置通常更为准确。

    • num_blocks:块是 N-BEATS 模型的基石。它包含一组完全连接层,用于建模时间序列。

    • num_block_layers:每个块中完全连接层的数量。

    • widths:每个块中完全连接层的宽度。

    • sharing:一个布尔参数,表示每个堆栈块是否共享权重。在可解释模式下,该参数应设置为 True

    • backcast_loss_ratio:模型中反向预测损失的相关性。反向预测(预测输入样本)是 N-BEATS 训练中的一个重要机制。这个参数平衡了反向预测损失与预测损失。

  3. 创建模型后,您可以将其传递给 PyTorch Lightning 的 Trainer 进行训练:

    import lightning.pytorch as pl
    from lightning.pytorch.callbacks import EarlyStopping
    early_stop_callback = EarlyStopping(monitor="val_loss",
        min_delta=1e-4,
        patience=10,
        verbose=False,
        mode="min")
    trainer = pl.Trainer(
        max_epochs=30,
        accelerator="auto",
        enable_model_summary=True,
        gradient_clip_val=0.01,
        callbacks=[early_stop_callback],
    )
    
  4. 我们还包括了一个早停回调,用于指导训练过程。模型使用 fit() 方法进行训练:

    trainer.fit(
        model,
        train_dataloaders=datamodule.train_dataloader(),
        val_dataloaders=datamodule.val_dataloader(),
    )
    

    我们将训练数据加载器传递给模型进行训练,并使用验证数据加载器进行早停。

  5. 拟合模型后,我们可以评估其测试性能,并用它进行预测。在此之前,我们需要从保存的检查点加载模型:

    best_model_path = trainer.checkpoint_callback.best_model_path
    best_model = NBeats.load_from_checkpoint(best_model_path)
    
  6. 您可以通过以下方式从测试集获取预测值及其真实值:

    predictions = best_model.predict(datamodule.test.to_dataloader(batch_size=1, shuffle=False))
    actuals = torch.cat(
        [y[0] for x, y in iter(
            datamodule.test.to_dataloader(batch_size=1, 
                shuffle=False))])
    
  7. 我们通过计算这两个数量之间的平均绝对差来评估预测性能(即,平均绝对误差):

    (actuals - predictions).abs().mean()
    

    根据您的设备,您可能需要使用 predictions.cpu()predictions 对象转换为 PyTorch 的 tensor 对象,然后再计算前面代码中指定的差异。

  8. 数据模块还简化了为新实例进行预测的工作流:

    forecasts = best_model.predict(datamodule.predict_dataloader())
    

    本质上,数据模块获取最新的观测值并将其传递给模型,后者会生成预测。

    N-BEATS 最有趣的方面之一是其可解释性组件。这些组件对于检查预测及其背后的驱动因素非常有价值:

  9. 我们可以将预测拆解成不同的组件,并使用 plot_interpretation() 方法将它们绘制出来。为此,我们需要事先获取原始预测,如下所示:

    raw_predictions = best_model.predict
        (datamodule.val_dataloader(),
        mode="raw",
        return_x=True)
    best_model.plot_interpretation(x=raw_predictions[1],
        output=raw_predictions[0],
        idx=0)
    

在前面的代码中,我们为测试集的第一个实例调用了绘图(idx=0)。以下是该绘图的样子:

图 6.1:将 N-BEATS 预测拆解成不同部分

图 6.1:将 N-BEATS 预测拆解成不同部分

上图展示了预测中的 trendseasonality 组件。

它是如何工作的…

N-BEATS 基于两个主要组件:

  • 一个包含预测和反向预测的残差连接双堆栈。在 N-BEATS 的上下文中,反向预测是指重建时间序列的过去值。它通过强迫模型在两个方向上理解时间序列结构,帮助模型学习更好的数据表示。

  • 一个深层的密集连接层堆栈。

这种组合使得模型既具有高预测准确性,又具备可解释性能力。

训练、评估和使用模型的工作流程遵循 PyTorch Lightning 提供的框架。数据准备逻辑是在数据模块组件中开发的,特别是在 setup() 函数中。建模阶段分为两个部分:

  1. 首先,你需要定义 N-BEATS 模型架构。在这个示例中,我们使用 from_dataset() 方法根据输入数据直接创建 NBeats 实例。

  2. 然后,训练过程逻辑在 Trainer 实例中定义,包括你可能需要的任何回调函数。

一些回调函数,比如早停,会将模型的最佳版本保存在本地文件中,你可以在训练后加载该文件。

需要注意的是,解释步骤是通过 plot_interpretation 部分进行的,这是 N-BEATS 的一个特殊功能,帮助从业人员理解预测模型所做的预测。这也有助于了解模型在实际应用中不适用的条件。

N-BEATS 是一个在预测工具库中非常重要的模型。例如,在 M5 预测竞赛中,该竞赛包含了一组需求时间序列,N-BEATS 模型被应用于许多最佳解决方案。你可以在这里查看更多细节:www.sciencedirect.com/science/article/pii/S0169207021001874

还有更多……

有一些方法可以最大化 N-BEATS 的潜力:

关于可解释性,除了 N-BEATS 外,你还可以采用另外两种方法:

使用 PyTorch Forecasting 优化学习率

在本例中,我们展示了如何基于 PyTorch Forecasting 优化模型的学习率。

准备工作

学习率是所有深度学习方法的基石参数。顾名思义,它控制着网络学习过程的速度。在本示例中,我们将使用与前一个示例相同的设置:

datamodule = GlobalDataModule(data=dataset,
    n_lags=N_LAGS,
    horizon=HORIZON,
    batch_size=32,
    test_size=0.2)
datamodule.setup()

我们还将以 N-BEATS 为例。然而,所有基于 PyTorch Forecasting 的模型,其过程是相同的。

如何做到这一点…

学习率的优化可以通过 PyTorch Lightning 的 Tuner 类进行。以下是使用 N-BEATS 的示例:

from lightning.pytorch.tuner import Tuner
import lightning.pytorch as pl
from pytorch_forecasting import NBeats
trainer = pl.Trainer(accelerator="auto", gradient_clip_val=0.01)
tuner = Tuner(trainer)
model = NBeats.from_dataset(
    dataset=datamodule.training,
    stack_types=['trend', 'seasonality'],
    num_blocks=[3, 3],
    num_block_layers=[4, 4],
    widths=[256, 2048],
    sharing=[True],
    backcast_loss_ratio=1.0,
)

在前面的代码中,我们定义了一个 Tuner 实例,作为 Trainer 对象的封装。我们还像前一部分一样定义了一个 NBeats 模型。然后,我们使用 lr_optim() 方法优化学习率:

lr_optim = tuner.lr_find(model,
    train_dataloaders=datamodule.train_dataloader(),
    val_dataloaders=datamodule.val_dataloader(),
    min_lr=1e-5)

完成此过程后,我们可以查看推荐的学习率值,并检查不同测试值的结果:

lr_optim.suggestion()
fig = lr_optim.plot(show=True, suggest=True)
fig.show()

我们可以在下图中可视化结果:

图 6.2:使用 PyTorch Forecasting 进行学习率优化

图 6.2:使用 PyTorch Forecasting 进行学习率优化

在此示例中,推荐的学习率大约是 0.05

它是如何工作的…

PyTorch Lightning 的 lr_find() 方法通过测试不同的学习率值,并选择一个最小化模型损失的值来工作。此方法使用训练和验证数据加载器来实现这一效果。

选择合适的学习率非常重要,因为不同的学习率值会导致不同性能的模型。较大的学习率收敛较快,但可能会收敛到一个次优解。然而,较小的学习率可能会需要过长的时间才能收敛。

在优化完成后,您可以像我们在之前的示例中一样,使用选定的学习率创建一个模型。

还有更多…

您可以在 PyTorch Forecasting 的 教程 部分了解更多关于如何充分利用像 N-BEATS 这样的模型的内容,详情请访问以下链接:pytorch-forecasting.readthedocs.io/en/stable/tutorials.html

开始使用 GluonTS

GluonTS 是一个灵活且可扩展的工具包,用于使用 PyTorch 进行概率时间序列建模。该工具包提供了专门为时间序列任务设计的最先进的深度学习架构,以及一系列用于时间序列数据处理、模型评估和实验的实用工具。

本节的主要目标是介绍gluonts库的基本组件,强调其核心功能、适应性和用户友好性。

准备开始

为了开始我们的学习之旅,确保安装了gluonts及其后端依赖pytorch

pip install gluonts pytorch

安装完成后,我们可以深入探索gluonts的功能。

如何操作…

我们首先访问由库提供的示例数据集:

from gluonts.dataset.repository.datasets import get_dataset
dataset = get_dataset("nn5_daily_without_missing", regenerate=False)

这将加载nn5_daily_without_missing数据集,这是gluonts提供的用于实验的数据集之一。

加载数据集后,可以检查其特性,使用get_dataset()函数进行操作。每个dataset对象包含元数据,提供关于时间序列频率、相关特征和其他相关属性的信息。你可以通过查看以下元数据了解数据集的更多信息:

print(dataset.metadata)

为了增强时间序列数据,gluonts提供了一系列转换器。例如,AddAgeFeature数据转换器为数据集添加了一个age特征,表示每个时间序列的生命周期:

from gluonts.transform import AddAgeFeature
transformation_with_age = Chain([
    AddAgeFeature(output_field="age",
    target_field="target",
    pred_length=dataset.metadata.prediction_length)
])
transformed_train_with_age = TransformedDataset(dataset.train, 
    transformation_with_age)

用于gluonts训练的数据通常表示为一个字典集合,每个字典代表一个时间序列,并附带可能的特征:

training_data = list(dataset.train)
print(training_data[0])

gluonts中的基本模型之一是SimpleFeedForwardEstimator模型。以下是它的设置:

首先,通过确定预测长度、上下文长度(表示考虑的前几个时间步的数量)和数据频率等参数来初始化估算器:

from gluonts.torch.model.simple_feedforward import SimpleFeedForwardEstimator
estimator_with_age = SimpleFeedForwardEstimator(
    hidden_dimensions=[10],
    prediction_length=dataset.metadata.prediction_length,
    context_length=100,
    trainer_kwargs={'max_epochs': 100}
)

要训练模型,只需在估算器上调用train()方法并提供训练数据:

predictor_with_age = estimator_with_age.train
    (transformed_train_with_age)

该过程使用提供的数据训练模型,生成一个准备好进行预测的预测器。以下是我们如何从模型中获取预测:

forecast_it_with_age, ts_it_with_age = make_evaluation_predictions(
    dataset=dataset.test,
    predictor=predictor_with_age,
    num_samples=100,
)
forecasts_with_age = list(forecast_it_with_age)
tss_with_age = list(ts_it_with_age)
fig, ax = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
ts_entry_with_age = tss_with_age[0]
ax[0].plot(ts_entry_with_age[-150:].to_timestamp())
forecasts_with_age[0].plot(show_label=True, ax=ax[0])
ax[0].set_title("Forecast with AddAgeFeature")
ax[0].legend()

在前面的代码中,可以使用make_evaluation_predictions()方法生成预测,然后将其与实际值进行对比绘制。以下是包含预测和实际值的图表:

图 6.3:使用和不使用 AddAgeFeature 的预测对比分析

图 6.3:使用和不使用 AddAgeFeature 的预测对比分析

在前面的图中,我们展示了使用和不使用AddAgeFeature的预测对比分析。使用该特征可以提高预测准确性,这表明它是此数据集中一个重要的变量。

它是如何工作的…

GluonTS 提供了一系列内置功能,有助于时间序列分析和预测。例如,数据转换器使你能够快速基于原始数据集构建新特征。如我们实验中所用,AddAgeFeature转换器将一个age属性附加到每个时间序列。时间序列的年龄往往能为模型提供相关的上下文信息。一个典型的应用场景是股票数据,较旧的股票可能会展现出与较新的股票不同的波动模式。

在 GluonTS 中训练采用基于字典的结构,每个字典对应一个时间序列,并包含其他相关的特征。这种结构使得附加、修改或删除特征变得更为容易。

在我们的实验中,我们测试了一个简单的模型,使用了SimpleFeedForwardEstimator模型。我们定义了两个模型实例,一个使用了AddAgeFeature,另一个没有使用。使用age特征训练的模型显示了更好的预测准确性,如我们在图 6.3中所看到的那样。这一改进突显了在时间序列分析中,特征工程的重要性。

使用 GluonTS 训练 DeepAR 模型

DeepAR 是一种先进的预测方法,利用自回归循环网络来预测时间序列数据的未来值。该方法由亚马逊提出,旨在解决需要较长预测周期的任务,如需求预测。当需要为多个相关的时间序列生成预测时,这种方法特别强大。

准备工作

我们将使用与前一个示例相同的数据集:

from gluonts.dataset.repository.datasets import get_dataset
dataset = get_dataset("nn5_daily_without_missing", regenerate=False)

现在,让我们看看如何使用这些数据构建 DeepAR 模型。

如何做…

我们从格式化数据开始进行训练:

  1. 我们通过使用ListDataset数据结构来实现:

    from gluonts.dataset.common import ListDataset
    from gluonts.dataset.common import FieldName
    train_ds = ListDataset(
        [
            {FieldName.TARGET: entry["target"], 
                FieldName.START: entry["start"]}
            for entry in dataset.train
        ],
        freq=dataset.metadata.freq,
    )
    
  2. 接下来,使用DeepAREstimator类定义 DeepAR 估计器,并指定诸如prediction_length(预测周期)、context_length(滞后数)和freq(采样频率)等参数:

    from gluonts.torch.model.deepar import DeepAREstimator
    N_LAGS=7
    HORIZON=7
    estimator = DeepAREstimator(
        prediction_length=HORIZON,
        context_length=N_LAGS,
        freq=dataset.metadata.freq,
        trainer_kwargs={"max_epochs": 100},
    )
    
  3. 在定义估计器后,使用train()方法训练 DeepAR 模型:

    predictor = estimator.train(train_ds)
    
  4. 使用训练好的模型对测试数据进行预测并可视化结果:

    forecast_it, ts_it = make_evaluation_predictions(
        dataset=dataset.test,
        predictor=predictor,
        num_samples=100,
    )
    forecasts = list(forecast_it)
    tss = list(ts_it)
    fig, ax = plt.subplots(1, 1, figsize=(10, 6))
    ts_entry = tss[0]
    ax.plot(ts_entry[-150:].to_timestamp())
    forecasts[0].plot(show_label=True, ax=ax, intervals=())
    ax.set_title("Forecast with DeepAR")
    ax.legend()
    plt.tight_layout()
    plt.show()
    

这是预测结果的图表:

图 6.4:DeepAR 预测与我们数据集中的真实值的比较

图 6.4:DeepAR 预测与我们数据集中的真实值的比较

该模型能够紧密匹配真实值。

它是如何工作的…

DeepAR 使用 RNN 架构,通常利用 LSTM 单元或 GRU 来建模时间序列数据。

context_length参数至关重要,因为它决定了模型在做出预测时将考虑多少过去的观测值作为其上下文。例如,如果将context_length设置为7,则模型将使用过去一周的数据来预测未来的值。

相反,prediction_length参数定义了预测的时间范围(即模型应预测的未来步数)。在给定的代码中,我们使用了一周的预测范围。

DeepAR 的一个突出特点是能够生成概率性预测。它不仅提供单一的点估计,而是提供一个可能未来值的分布,从而帮助我们理解预测中蕴含的不确定性。

最后,在处理多个相关时间序列时,DeepAR 利用序列间的共性来提高预测的准确性。

还有更多…

当满足以下条件时,DeepAR 表现尤为出色:

  • 你有多个相关时间序列;DeepAR 可以利用所有序列中的信息来改善预测。

  • 你的数据具有季节性或周期性模式。

  • 你希望生成概率性预测,这些预测会给出点估计并提供不确定性区间。我们将在下一章讨论不确定性估计。

你可以训练一个单一的 DeepAR 模型来处理全局数据集,并为数据集中的所有时间序列生成预测。另一方面,对于单独的时间序列,DeepAR 也可以分别训练每个序列,尽管这可能效率较低。

该模型特别适用于零售需求预测、股票价格预测和网站流量预测等应用。

使用 NeuralForecast 训练 Transformer 模型

现在,我们将目光转向近年来在人工智能各领域推动进步的 Transformer 架构。在本节中,我们将展示如何使用 NeuralForecast Python 库训练一个基础的 Transformer 模型。

准备工作

Transformer 已成为深度学习领域的主流架构,尤其在自然语言处理NLP)任务中表现突出。Transformer 也已被应用于 NLP 以外的各种任务,包括时间序列预测。

与传统模型逐点分析时间序列数据不同,Transformer 能够同时评估所有时间步。这种方法类似于一次性观察整个时间线,确定每个时刻与其他时刻的相关性,以便对特定时刻进行评估。

Transformer 架构的核心是注意力机制。该机制根据特定输入的相关性,计算输入值或来自前一层的值的加权和。与逐步处理输入的 RNN 不同,这使得 Transformer 能够同时考虑输入序列的所有部分。

Transformer 的关键组成部分包括以下内容:

  • 自注意力机制:计算所有输入值对的注意力得分,然后基于这些得分创建加权组合。

  • 多头注意力机制:该模型可以通过并行运行多个注意力机制,针对不同的任务或原因,集中注意力于输入的不同部分

  • 逐位置前馈网络:这些网络对注意力层的输出应用线性变换

  • 位置编码:由于 Transformer 本身没有任何固有的顺序感知,因此会将位置编码添加到输入嵌入中,以为模型提供序列中每个元素的位置相关信息

让我们看看如何训练一个 Transformer 模型。在本教程中,我们将再次使用 gluonts 库提供的数据集。我们将使用 NeuralForecast 库中提供的 Transformer 实现。NeuralForecast 是一个 Python 库,包含了多种专注于预测问题的神经网络实现,包括几种 Transformer 架构。

如何执行…

首先,让我们为 Transformer 模型准备数据集。与逐步处理输入序列的序列到序列模型(如 RNN、LSTM 或 GRU)不同,Transformer 会一次性处理整个序列。因此,如何格式化和输入数据可能会有所不同:

  1. 让我们从加载数据集和必要的库开始:

    from gluonts.dataset.repository.datasets import get_dataset
    import pandas as pd
    from sklearn.preprocessing import StandardScaler
    import matplotlib.pyplot as plt
    from neuralforecast.core import NeuralForecast
    from neuralforecast.models import VanillaTransformer
    dataset = get_dataset("nn5_daily_without_missing", regenerate=False)
    N_LAGS = 7
    HORIZON = 7
    
  2. 接下来,将数据集转换为 pandas DataFrame 并进行标准化。请记住,标准化是任何深度学习模型拟合的关键:

    data_list = list(dataset.train)
    data_list = [
        pd.Series(
            ds["target"],
            index=pd.date_range(
                start=ds["start"].to_timestamp(),
                freq=ds["start"].freq,
                periods=len(ds["target"]),
            ),
        )
        for ds in data_list
    ]
    tseries_df = pd.concat(data_list, axis=1)
    tseries_df[tseries_df.columns] = 
        \StandardScaler().fit_transform(tseries_df)
    tseries_df = tseries_df.reset_index()
    df = tseries_df.melt("index")
    df.columns = ["ds", "unique_id", "y"]
    df["ds"] = pd.to_datetime(df["ds"])
    
  3. 数据准备好后,我们将训练一个 Transformer 模型。与使用递归架构的 DeepAR 模型不同,Transformer 将依靠其注意力机制,在做出预测时考虑时间序列的各个部分:

    model = [
        VanillaTransformer(
            h=HORIZON,
            input_size=N_LAGS,
            max_steps=100,
            val_check_steps=5,
            early_stop_patience_steps=3,
        ),
    ]
    nf = NeuralForecast(models=model, freq="D")
    Y_df = df[df["unique_id"] == 0]
    Y_train_df = Y_df.iloc[:-2*HORIZON]
    Y_val_df = Y_df.iloc[-2*HORIZON:-HORIZON]
    training_df = pd.concat([Y_train_df, Y_val_df])
    nf.fit(df=training_df, val_size=HORIZON)
    
  4. 最后,展示预测结果:

    forecasts = nf.predict()
    Y_df = df[df["unique_id"] == 0]
    Y_hat_df = forecasts[forecasts.index == 0].reset_index()
    Y_hat_df = Y_test_df.merge(Y_hat_df, how="outer", 
        on=["unique_id", "ds"])
    plot_df = pd.
        concat([Y_train_df, Y_val_df, Y_hat_df]).set_index("ds")
    plot_df = plot_df.iloc[-150:]
    fig, ax = plt.subplots(1, 1, figsize=(20, 7))
    plot_df[["y", "VanillaTransformer"]].plot(ax=ax, linewidth=2)
    ax.set_title("First Time Series Forecast with Transformer", fontsize=22)
    ax.set_ylabel("Value", fontsize=20)
    ax.set_xlabel("Timestamp [t]", fontsize=20)
    ax.legend(prop={"size": 15})
    ax.grid()
    plt.show()
    

下图展示了 Transformer 预测值与时间序列实际值的对比:

图 6.5:Transformer 预测与我们数据集的真实值对比

图 6.5:Transformer 预测与我们数据集的真实值对比

它是如何工作的…

neuralforecast 库要求数据采用特定格式。每个观察值由三部分信息组成:时间戳、时间序列标识符以及对应的值。我们从准备数据集开始,确保其符合此格式。Transformer 实现于 VanillaTransformer 类中。我们设置了一些参数,例如预测范围、训练步数或与提前停止相关的输入。你可以在以下链接查看完整的参数列表:nixtla.github.io/neuralforecast/models.vanillatransformer.html。训练过程通过 NeuralForecast 类实例中的 fit() 方法进行。

Transformer 通过使用自注意力机制对整个序列进行编码来处理时间序列数据,从而捕捉依赖关系,而不考虑它们在输入序列中的距离。这种全局视角在存在长时间跨度的模式或依赖关系时特别有价值,或者当过去数据的相关性动态变化时。

位置编码用于确保 Transformer 识别数据点的顺序。没有它们,模型会将时间序列视为一堆没有内在顺序的值。

多头注意力机制使 Transformer 能够同时关注不同的时间步长和特征,特别适用于具有多个交互模式和季节性变化的复杂时间序列。

还有更多…

由于以下原因,Transformer 在时间序列预测中可能非常有效:

  • 它们捕捉数据中长期依赖关系的能力

  • 在大规模数据集上的可扩展性

  • 在建模单变量和多变量时间序列时的灵活性

与其他模型一样,Transformer 也能通过调整超参数来受益,例如调整注意力头的数量、模型的大小(即层数和嵌入维度)以及学习率。

使用 GluonTS 训练一个时序融合变换器

TFT 是一种基于注意力机制的架构,由 Google 开发。它具有递归层,用于学习不同尺度的时间关系,并结合自注意力层以提高可解释性。TFT 还使用变量选择网络进行特征选择,门控层用于抑制不必要的成分,并采用分位数损失作为其损失函数,用以生成预测区间。

本节将深入探讨如何使用 GluonTS 框架训练并进行 TFT 模型的推理。

准备工作

确保你的环境中安装了 GluonTS 库和 PyTorch 后端。我们将使用来自 GluonTS 仓库的nn5_daily_without_missing数据集作为工作示例:

from gluonts.dataset.common import ListDataset, FieldName
from gluonts.dataset.repository.datasets import get_dataset
dataset = get_dataset("nn5_daily_without_missing", regenerate=False)
train_ds = ListDataset(
    [
        {FieldName.TARGET: entry["target"], FieldName.START: entry["start"]}
        for entry in dataset.train
    ],
    freq=dataset.metadata.freq,
)

在接下来的部分,我们将使用这个数据集训练一个 TFT 模型。

如何实现…

数据集准备好后,接下来定义 TFT 估计器:

  1. 我们将从指定超参数开始,例如预测长度、上下文长度和训练频率:

    from gluonts.torch.model.tft import TemporalFusionTransformerEstimator
    N_LAGS = 7
    HORIZON = 7
    estimator = TemporalFusionTransformerEstimator(
        prediction_length=HORIZON,
        context_length=N_LAGS,
        freq=dataset.metadata.freq,
        trainer_kwargs={"max_epochs": 100},
    )
    
  2. 在定义估计器后,继续使用训练数据集训练 TFT 模型:

    predictor = estimator.train(train_ds)
    
  3. 训练完成后,我们可以使用模型进行预测。利用make_evaluation_predictions()函数来实现这一点:

    from gluonts.evaluation import make_evaluation_predictions
    forecast_it, ts_it = make_evaluation_predictions(
        dataset=dataset.test,
        predictor=predictor,
        num_samples=100,
    )
    
  4. 最后,我们可以通过可视化预测结果来了解模型的表现:

    import matplotlib.pyplot as plt
    ts_entry = tss[0]
    ax.plot(ts_entry[-150:].to_timestamp())
    forecasts[0].plot(show_label=True, ax=ax, intervals=())
    ax.set_title("Forecast with Temporal Fusion Transformer")
    ax.legend()
    plt.tight_layout()
    plt.show()
    

以下是模型预测与数据集实际值的比较。

图 6.6:TFT 预测与我们数据集中的真实值比较

图 6.6:TFT 预测与我们数据集中的真实值比较

它是如何工作的…

我们使用gluonts中提供的 TFT 实现。主要参数包括滞后数(上下文长度)和预测时段。你还可以测试模型某些参数的不同值,例如注意力头的数量(num_heads)或 Transformer 隐状态的大小(hidden_dim)。完整的参数列表可以在以下链接中找到:ts.gluon.ai/stable/api/gluonts/gluonts.torch.model.tft.estimator.html

TFT 适用于多种使用场景,因为它具备完整的特征集:

  • 时间处理:TFT 通过序列到序列模型解决了整合过去观察值和已知未来输入的挑战,利用 LSTM 编码器-解码器。

  • 注意力机制:该模型使用注意力机制,能够动态地为不同的时间步分配重要性。这确保了模型只关注相关的历史数据。

  • 门控机制:TFT 架构利用门控残差网络,提供在建模过程中的灵活性,能够适应数据的复杂性。这种适应性对于处理不同的数据集尤其重要,特别是对于较小或噪声较多的数据集。

  • 变量选择网络:该组件用于确定每个协变量与预测的相关性。通过加权输入特征的重要性,它过滤掉噪声,仅依赖于重要的预测因子。

  • 静态协变量编码器:TFT 将静态信息编码为多个上下文向量,丰富了模型的输入。

  • 分位数预测:通过预测每个时间步的不同分位数,TFT 提供了可能结果的范围。

  • 可解释输出:尽管 TFT 是一个深度学习模型,但它提供了特征重要性方面的见解,确保预测的透明性。

还有更多……

除了架构创新之外,TFT 的可解释性使其成为当需要解释预测是如何生成时的良好选择。诸如变量网络选择和时序多头注意力层等组件,揭示了不同输入和时间动态的重要性,使 TFT 不仅仅是一个预测工具,还是一个分析工具。

使用 NeuralForecast 训练 Informer 模型

在本教程中,我们将探索neuralforecast Python 库,用于训练 Informer 模型,Informer 是另一种基于 Transformer 的深度学习预测方法。

准备开始

Informer 是一种针对长期预测而量身定制的 Transformer 方法——即,具有较长预测时段的预测。与标准 Transformer 相比,Informer 的主要区别在于其改进的自注意力机制,这大大减少了运行模型和生成长序列预测的计算需求。

在这个教程中,我们将向你展示如何使用neuralforecast训练 Informer 模型。我们将使用与之前教程相同的数据集:

from gluonts.dataset.repository.datasets import get_dataset
dataset = get_dataset('nn5_daily_without_missing')

如何实现……

这次,我们不是创建DataModule来处理数据预处理,而是使用基于neuralforecast模型的典型工作流程:

  1. 我们首先准备时间序列数据集,以符合neuralforecast方法所期望的特定格式:

    import pandas as pd
    from sklearn.preprocessing import StandardScaler
    data_list = list(dataset.train)
    data_list = [pd.Series(ds['target'],
        index=pd.date_range(start=ds['start'].to_timestamp(),
            freq=ds['start'].freq,
            periods=len(ds['target'])))
        for ds in data_list]
    tseries_df = pd.concat(data_list, axis=1)
    tseries_df[tseries_df.columns] = \
        StandardScaler().fit_transform(tseries_df)
    tseries_df = tseries_df.reset_index()
    df = tseries_df.melt('index')
    df.columns = ['ds', 'unique_id', 'y']
    df['ds'] = pd.to_datetime(df['ds'])
    n_time = len(df.ds.unique())
    val_size = int(.2 * n_time)
    
  2. 我们将数据集转化为一个包含三列的 pandas DataFrame:dsunique_idy。它们分别表示时间戳、时间序列的 ID 和对应时间序列的值。在前面的代码中,我们使用scikit-learn的标准缩放器将所有时间序列转换为一个共同的数值范围。我们还将验证集的大小设置为时间序列大小的 20%。现在,我们可以如下设置 Informer 模型:

    from neuralforecast.core import NeuralForecast
    from neuralforecast.models import Informer
    N_LAGS = 7
    HORIZON = 7
    model = [Informer(h=HORIZON,
        input_size=N_LAGS,
        max_steps=1000,
        val_check_steps=25,
        early_stop_patience_steps=10)]
    nf = NeuralForecast(models=model, freq='D')
    
  3. 我们将 Informer 的上下文长度(滞后数)设置为7,以便在每个时间步预测接下来的 7 个值。训练步数设置为1000,我们还设置了早期停止机制以帮助拟合过程。这些仅是设置 Informer 时可以使用的部分参数。你可以通过以下链接查看完整的参数列表:nixtla.github.io/neuralforecast/models.informer.html。模型被传递到NeuralForecast类实例中,我们还将时间序列的频率设置为每日(D关键字)。然后,训练过程如下进行:

    nf.fit(df=df, val_size=val_size)
    
  4. nf对象用于拟合模型,然后可以用来进行预测:

    forecasts = nf.predict()
    forecasts.head()
    

预测结果以 pandas DataFrame 的形式结构化,因此你可以通过使用head()方法查看预测的样本。

它是如何工作的……

neuralforecast库提供了一个简单的框架,用于训练强大的时间序列问题模型。在这种情况下,我们将数据逻辑处理放在框架外部,因为它会在内部处理数据传递给模型的过程。

NeuralForecast类实例接受一个模型列表作为输入(在本例中只有一个Informer实例),并负责训练过程。如果你想直接使用最先进的模型,这个库可以是一个不错的解决方案。其限制是,它的灵活性不如基础的 PyTorch 生态系统。

还有更多…

在这个教程中,我们描述了如何使用neuralforecast训练一个特定的 Transformer 模型。但这个库包含了其他你可以尝试的 Transformers,包括以下几种:

  • Vanilla Transformer

  • TFT

  • Autoformer

  • PatchTST

你可以在以下链接查看完整的模型列表:nixtla.github.io/neuralforecast/core.html

使用 NeuralForecast 比较不同的 Transformer

NeuralForecast 包含几种深度学习方法,你可以用来解决时间序列问题。在本节中,我们将引导你通过 neuralforecast 比较不同基于 Transformer 的模型的过程。

准备工作

我们将使用与前一节相同的数据集(df 对象)。我们将验证集和测试集的大小分别设置为数据集的 10%:

val_size = int(.1 * n_time)
test_size = int(.1 * n_time)

现在,让我们来看一下如何使用 neuralforecast 比较不同的模型。

如何操作…

我们首先定义要比较的模型。在这个例子中,我们将比较一个 Informer 模型和一个基础版 Transformer,我们将模型设置如下:

from neuralforecast.models import Informer, VanillaTransformer
models = [
    Informer(h=HORIZON,
        input_size=N_LAGS,
        max_steps=1000,
        val_check_steps=10,
        early_stop_patience_steps=15),
    VanillaTransformer(h=HORIZON,
        input_size=N_LAGS,
        max_steps=1000,
        val_check_steps=10,
        early_stop_patience_steps=15),
]

每个模型的训练参数设置相同。我们可以使用 NeuralForecast 类,通过 cross_validation() 方法比较不同的模型,方法如下:

from neuralforecast.core import NeuralForecast
nf = NeuralForecast(
    models=models,
    freq='D')
cv = nf.cross_validation(df=df,
    val_size=val_size,
    test_size=test_size,
    n_windows=None)

cv 对象是比较的结果。以下是每个模型在特定时间序列中的预测样本:

图 6.7:示例时间序列中两个 Transformer 模型的预测结果

图 6.7:示例时间序列中两个 Transformer 模型的预测结果

Informer 模型似乎产生了更好的预测结果,我们可以通过计算平均绝对误差来验证这一点:

from neuralforecast.losses.numpy import mae
mae_informer = mae(cv['y'], cv['Informer'])
mae_transformer = mae(cv['y'], cv['VanillaTransformer'])

Informer 的误差为 0.42,优于 VanillaTransformer 得到的 0.53 分数。

工作原理…

在背后,cross_validation() 方法的工作原理如下。每个模型使用训练集和验证集进行训练。然后,它们在测试实例上进行评估。测试集上的预测性能为我们提供了一个可靠的估计,表示我们期望模型在实际应用中达到的性能。因此,你应该选择能最大化预测性能的模型,并用整个数据集重新训练它。

neuralforecast 库包含其他可以进行比较的模型。你也可以比较同一种方法的不同配置,看看哪种最适合你的数据。