首个可解释的深度学习时间序列预测模型-N-BEATS

原创 学AI必备的 人工智能学习指南 2024年08月17日 15:18 湖南

图片

图片

N-BEATS架构

在时间序列预测领域,长期以来 深度学习和Transformer模型 并未能显著超越其他模型的表现。

回顾 Makridakis M-竞赛,胜出方案往往依赖于统计模型

直至M4竞赛,冠军方案要么是纯粹的统计模型,要么是机器学习与统计模型的混合体。

而纯机器学习方法仅勉强超越竞赛基线,然而,这一局面在2020年由Oreshkin等人发表的一篇论文中得到了改变。

他们提出了 N-BEATS,一种前景广阔的纯深度学习方法。

该模型不仅击败了M4竞赛的冠军方案,还首次实现了 纯深度学习对成熟统计方法的超越。

N-BEATS,全称为“Neural Basis Expansion Analysis for Interpretable Time Series”,“面向可解释时间序列的神经基础扩展分析”。

图片

本文将带大家深入解析N-BEATS的架构,内容通俗易懂。

还将展示如何让这种深度学习方法变得可解释,并教大家如何在Python中轻松实现N-BEATS模型,并调整其超参数。

图片

为了让大家能更好的学习,我可以把我学习期间收集的时间序列资料、LLM大语言模型资料一起分享给大家

图片

图片

       

在探讨N-BEATS架构之前,让我们先了解其核心理念。

N-BEATS的核心功能在于神经基础扩展,基础扩展是一种数据增强方法,通过扩展特征集来建模非线性关系。

听起来有点抽象?让我们通过下面这个例子来解释。

假设我们的目标值y与某个特征x存在某种关系,我们想用线性模型来表示这种关系。

在一维空间中,这将得到一个线性关系(如下图左侧所示)。

然而,如果特征和目标之间并非线性关系,那么这种模型就毫无用处。

那么,我们能做些什么呢?

答案是扩展特征集,我们可以将原始特征的二次值添加到特征集中,得到[x, x²]

这样,我们就从一维空间进入了二维空间,因为现在我们有了两个特征而不是一个。

然后,我们可以在二维空间中拟合一个线性模型,从而得到一个二次多项式模型(如下图右侧所示)。

图片

图片

一维空间中的线性回归(左)与二维空间中的线性回归(右)对比。通过基扩展,我们能够更好地将数据拟合到模型中。

这就是基础扩展的全部内容。

我们基于原始特征添加新特征来扩展特征集,在这个例子中,我们使用了二次多项式扩展,我们将每个原始特征的二次值添加到特征集中。

最常见的基础扩展方法是多项式基础扩展,但除此之外,还有许多其他方法,如分箱、分段线性样条、自然三次样条、对数或平方等。

N-BEATS模型会自行决定使用哪种基础扩展方法,在训练过程中,模型会尝试找到最适合数据的基础扩展方法。

我们让模型自行完成这项工作,这就是它被称为“神经基础扩展”的原因。

N-BEATS模型是如何工作的呢?

N-BEATS模型具有以下架构:

图片

N-BEATS架构概览

这里包含了很多内容,图片可能看起来有些复杂。但N-BEATS背后的思想其实很直接。

我们可以观察到两点:

首先,模型将时间序列分为回顾期和预测期。

模型利用回顾期来进行预测,回顾期的长度通常是预测期长度的两到六倍。

其次,N-BEATS模型(右侧黄色矩形)由多层堆叠组成,每个堆叠又由多层块组成。

每个块具有类似分叉的结构(左侧蓝色矩形),块中的一个分支产生回溯预测,另一个分支根据某些块输入数据产生预测。

预测包含了对未见值的预测。而回溯预测则展示了模型对输入数据的拟合情况。

那么,我们如何从输入中得到回溯预测和预测呢?

首先,模型将输入通过一个具有四层的全连接神经网络(MLP)。

MLP为回溯预测和预测生成扩展系数θ,这些扩展系数流入两个分支,一个用于回溯预测,一个用于预测

在每个分支中,我们执行基础扩展,这就是“神经基础扩展”实际发生的地方。

如上图所示,N-BEATS将堆叠中的各个块连接起来。

由于每个块都会返回回溯预测和预测,因此会发生两件事:首先,模型将每个块的部分预测相加,以产生堆叠的预测;其次,模型从块的输入中移除回溯预测。

因此,每个块只接收前一个块的残差。

模型只将前一个块未捕获的信息传递给下一个块,每个块都试图仅近似输入信号的一部分,专注于局部模式。

然后,N-BEATS模型将多个堆叠分层

与堆叠中的块类似,除了第一个堆叠外,每个堆叠都是基于前一个堆叠的残差进行训练的。

每个堆叠都会学习之前未捕获的全局模式,最终预测是堆叠预测的总和,提供了层次化的分解。

我们可以看到,N-BEATS采用了一种双重残差堆叠方法。

回溯预测和预测分别产生向后和向前的残差,块和堆叠的分层架构导致了这些残差的堆叠。

通过双重残差堆叠,N-BEATS能够重现统计模型的机制。

N-BEATS的优势

相比其他深度学习方法,N-BEATS使我们能够构建具有可解释性结果的非常深的神经网络。

此外,训练速度更快,该模型不包含任何循环或自注意力层,其双重残差堆叠促进了更流畅的梯度反向传播。

与经典的时间序列预测方法相比,我们无需进行任何特征工程。

无需识别时间序列特有的特征,如季节性和趋势,N-BEATS会为我们完成这些工作。

这使得模型易于使用,我们可以快速上手。

此外,该模型还具备零样本迁移学习的能力。

深度学习架构如何变得可解释?

我上面描述的N-BEATS通用版本并不可解释,模型可以学习的基函数和网络的深度没有限制,因此我们不知道模型学到了什么,以及这些是否是时间序列特有的成分,如趋势。

如何获得可解释性?

我们有一个技巧,通过限制模型的深度和模型可以学习的基扩展函数来获得可解释性。

例如,在时间序列预测中,我们经常使用趋势和季节性。

我们可以强制模型仅学习这两个特征。

首先,我们通过仅使用两个堆叠来限制模型的深度。第一个堆叠学习趋势,第二个堆叠学习季节性。

接着,我们可以通过提取每个堆叠的部分预测来解读模型的结果。

最后我们必须强制模型仅学习趋势和季节性,为此,我们需要引入特定于问题的归纳偏置。

我们通过将基扩展函数设置为特定函数形式来实现这一点。为此,我们将每个块中的最后一层替换为一个函数,使用多项式基来确定趋势,使用傅里叶基来确定季节性。

图片

通用版N-BEATS的基本架构(左)与可解释版N-BEATS的架构(右)


使用N-BEATS进行预测示例

现在,我们已经了解了N-BEATS的工作原理,让我们将该模型应用于预测任务。

我们将预测德国未来两周的批发电力价格,数据由Ember在“欧洲批发电力价格”下以CC-BY-4.0许可证提供。

我们将使用Nixtla的neuralforecast库中的N-BEATS实现,该库使我们能够轻松地应用N-BEATS。

图片

德国电力批发价格预测

请注意,我并不是在尝试获得尽可能准确的预测,而是展示如何应用N-BEATS。

好的,让我们开始吧。

首先,导入我们需要的所有库和数据集。

import matplotlib.pyplot as pltimport numpy as npimport pandas as pdfrom neuralforecast import NeuralForecastfrom sklearn.metrics import mean_absolute_error
# %% Import dataprice_data = pd.read_csv("data/Germany.csv")price_data = price_data[["Datetime (UTC)", "Price (EUR/MWhe)"]]
price_data["Datetime (UTC)"] = pd.to_datetime(price_data["Datetime (UTC)"])price_data = price_data.loc[price_data["Datetime (UTC)"]>="2019-01-01"]price_data.sort_values("Datetime (UTC)", inplace=True)
# convert data into Nixtla's time series formatprice_data.rename(columns={"Price (EUR/MWhe)": "y", "Datetime (UTC)": "ds"}, inplace=True)price_data["unique_id"] = 1
plt.plot(price_data["ds"], price_data["y"])

Nixtla有一个所有模型都期望的时间序列格式,即包含时间戳(ds列)、目标值(y列)和唯一标识符(unique_id列)的DataFrame。

unique_id允许我们同时在不同时间序列上训练模型。

这里我们省略了详细的数据探索步骤。

但我们可以看到两个季节性成分:

  • 日季节性:早晨和傍晚时段电价较高,因为这两个时段的电力消耗通常较高。

  • 周季节性:工作日电价高于周末,因为工作日的电力消耗通常较高。

下一步是将数据集拆分为训练集和测试集,由于我想预测未来两周的消费情况,因此我使用数据集中的最后两周作为测试集。

我在使用Pandas的Datetime时遇到了一些问题,因此我将日期时间转换为了时间戳。

# %% Train-Test Splitforecast_horizon = 24 * 14 # 2 weeks
Y_train_df = price_data.iloc[:-forecast_horizon] Y_test_df = price_data.iloc[-forecast_horizon:] 
Y_train_df.reset_index(inplace=True, drop=True)Y_test_df.reset_index(inplace=True, drop=True)
_, ax = plt.subplots(1, figsize=(10,3))ax.plot(Y_train_df["ds"], Y_train_df["y"], label="train")ax.plot(Y_test_df["ds"], Y_test_df["y"], label="test")ax.set_ylabel("price in EUR/MWh")ax.legend()
# convert datetime to timestampY_train_df["ds"] = Y_train_df["ds"].astype(int) // 10**9

最后,创建一个函数来绘制结果。

# %% Plotting functiondef make_comparison_plot(y_pred, y_true):
    mae = mean_absolute_error(y_pred=y_pred, y_true=y_true)    print(f"MAE: {mae}")
    fig, ax = plt.subplots(2, figsize=(10, 4))
    ax[0].plot(Y_test_df["ds"], y_true, label="true")    ax[0].plot(Y_test_df["ds"], y_pred, label="prediction")    ax[0].grid()    ax[0].legend()    ax[0].set_title(f"prediction vs. true (mean-absolute-error: {mae:.2f})")    ax[0].set_ylabel("price in EUR/MWh")    ax[0].set_xticklabels(ax[0].get_xticklabels(), rotation=45, ha="right")
    ax[1].scatter(y_pred, y_true)    max_val = np.min([y_pred.max(), y_true.max()])    min_val = np.min([y_pred, y_true])    ax[1].plot([min_val, max_val], [min_val, max_val], color="r")    ax[1].grid()    ax[1].set_ylabel("actual price in EUR/MWh")    ax[1].set_xlabel("predicted price in EUR/MWh")
    fig.tight_layout()

基线模型

为了完整性,我们将从一个简单的模型作为基线开始。

我们将使用季节性朴素基线模型,因为消费显示出不同的季节性模式。

我们将使用Nixtla的statsforecast库中提供的SeasonalNaive模型。

在这里,我们将训练集中的最后一周数据作为我们的预测结果。

# %% Baseline modelfrom statsforecast.models import SeasonalNaive
model = SeasonalNaive(season_length=24 * 7)model = model.fit(y=Y_train_df["y"])y_pred_seasonal_naive = model.predict(h=forecast_horizon)
make_comparison_plot(y_pred=y_pred_seasonal_naive["mean"], y_true=Y_test_df["y"])

结果看起来相当不错

基线模型给出的平均绝对误差(MAE)为17.84,在测试集的第一周,基线模型的准确率非常高。

然而,随着我们离最后一个已知值越来越远,准确率逐渐降低。

图片

季节性朴素基线模型的预测结果

如果我们能在基线模型上投入更多时间,或许能取得更好的效果。

不过,对于我们的应用场景来说,这个基线模型应该已经足够好了。

训练N-BEATS模型

接下来,我们训练第一个N-BEATS模型,实现过程非常直接。

我们初始化N-BEATS模型,定义预测期和回溯期,在这个例子中,使用了两周的回溯期。

然后,我们有一些自定义选项,可以通过选择堆叠类型、块的数量、多层感知机(MLP)层的大小、激活函数等来定制模型;

通过选择损失函数、学习率、批量大小等来定制训练过程,还可以对输入数据进行缩放处理。

# %% N-BEATS modelfrom neuralforecast.models import NBEATS
model = NBEATS(    h=forecast_horizon,    input_size=2 * forecast_horizon,    stack_types=["seasonality"],    activation="ReLU",    num_lr_decays=0,    scaler_type="robust",    max_steps=100,    val_check_steps=10,    early_stop_patience_steps=2,)
fcst = NeuralForecast(models=[model], freq=3600)fcst.fit(df=Y_train_df, val_size=2 * forecast_horizon)y_pred_nbeats = fcst.predict()
make_comparison_plot(y_pred=y_pred_nbeats["NBEATS"], y_true=Y_test_df["y"])

初始化模型后,我们需要用neuralforecast类来封装它,这个类提供了我们训练模型和后续进行预测所需的方法。

此外,该类还允许我们通过向models参数传递模型列表来训练不同的模型。

最后,我们可以使用fit()方法来训练模型,并对下一个月进行预测。

结果显示,N-BEATS模型的表现略优于基线模型,其MAE降至17.5,而基线模型的MAE为17.84。

图片

N-BEATS模型的预测结果

调整 N-BEATS 模型的超参数

接下来,我们对N-BEATS模型的超参数进行调优,与其随意调整以寻找好的超参数,不如直接运行超参数优化。

这并不复杂,Nixtla为我们提供了AutoNBEATS模型,用于进行超参数调优。

我们可以选择ray或optuna作为后端,并使用默认的超参数配置或创建自定义配置。

在初始化AutoNBEATS模型时定义我们的选择即可,这与运行NBEATS模型的唯一区别在于此。其他步骤保持不变。

在这个例子中,我将使用Optuna和一个自定义配置文件。

# %% AutoNBEATS - Hyperparameter optimizationimport optunafrom neuralforecast.auto import AutoNBEATSfrom neuralforecast.losses.pytorch import MAE

def config(trial):    return {        "scaler_type": trial.suggest_categorical("scaler_type", ["minmax", "robust", "standard"]),        "stack_types": trial.suggest_categorical(            "stack_types", [["identity"], ["trend"], ["seasonality"], ["trend", "seasonality"]]        ),        "activation": trial.suggest_categorical("activation", ["ReLU", "LeakyReLU"]),        "num_lr_decays": trial.suggest_int("num_lr_decays", 0, 3),        "input_size": trial.suggest_int("input_size", 2, 6),        "random_seed": trial.suggest_int("random_seed", 1, 20),        "batch_size": trial.suggest_categorical("batch_size", [32, 64, 128, 256]),        "windows_batch_size": trial.suggest_categorical(            "windows_batch_size", [128, 256, 512, 1024]        ),        "max_steps": 100,    }
model = AutoNBEATS(    h=forecast_horizon,    loss=MAE(),    config=config,    search_alg=optuna.samplers.TPESampler(),    backend="optuna",    num_samples=20,)
fcst = NeuralForecast(models=[model], freq=3600)fcst.fit(df=Y_train_df, val_size=2 * forecast_horizon)
# make prediction with best modely_pred_autonbeats = fcst.predict()
make_comparison_plot(y_pred=y_pred_autonbeats["AutoNBEATS"], y_true=Y_test_df["y"])

超参数调优后,运行predict()方法将使用最佳模型进行预测。

我们发现,“优化”后的N-BEATS模型的准确率(MAE为22.64)反而低于基线模型和未优化的N-BEATS模型。

但是,在超参数调优过程中使用不同的搜索空间进行更多尝试可能会得到不同的结果。

图片

AutoNBEATS模型的预测结果:这些结果显示了超参数调优实验中表现最佳的模型。

如果我们不向模型传递配置文件,AutoNBEATS将使用默认配置。

如果我们只想从默认配置中更改少数几个参数,可以运行上述代码片段。

# Use your own config or AutoNBEATS.default_configdefault_config = AutoNBEATS.get_default_config(h=forecast_horizon, backend="optuna")
def adapted_config(trial):    config = {**default_config(trial)}    config.update(        {            "max_steps": 100,            "scaler_type": "robust",        }    )    return config

如果你想查看超参数调优的结果,可以从模型的results属性中获取。

# show results of hyperparameter tuning runsresults = fcst.models[0].results.trials_dataframe()

结合外生变量的N-BEATS模型

我们只需使用Nixtla的NBEATSx模型,并定义要使用哪些外生变量。

例如,我们可以将一天中的小时数传递给模型,因为存在日季节性。

# %% N-BEATSxfrom neuralforecast.models import NBEATSx
model = NBEATSx(    h=forecast_horizon,    input_size=2 * forecast_horizon,    futr_exog_list=["weekday"],    stack_types=["seasonality"],    activation="ReLU",    num_lr_decays=0,    scaler_type="robust",    max_steps=100,    val_check_steps=10,    early_stop_patience_steps=2,)
fcst = NeuralForecast(models=[model], freq=3600)
Y_train_df["weekday"] = pd.to_datetime(Y_train_df["ds"]).dt.weekdayfutr_df = Y_test_df.copy()futr_df["weekday"] = futr_df["ds"].dt.weekdayfutr_df["ds"] = futr_df["ds"].astype(int) // 10**9
fcst.fit(df=Y_train_df, val_size=2 * forecast_horizon)y_pred_nbeatsx = fcst.predict(futr_df=futr_df)
make_comparison_plot(y_pred=y_pred_nbeatsx["NBEATSx"], y_true=Y_test_df["y"])

将星期几作为外生变量后,MAE降至19.58,但是,尝试不同的外生变量(如电力消耗或发电量预测)或不同的超参数可能会进一步提高准确率。

图片

使用N-BEATS模型结合外生变量的预测:这里展示了在N-BEATS模型中引入外生变量后的预测结果。

关于示例的最后说明

在所有示例中,我们用了一种非常静态的方法,即我们只在测试集上进行一次预测。

但假设我们为一家拥有发电单元的公司工作,该公司希望知道如何规划其单元的运行以最大化收入。

为此,他们希望每天早上都能获得更新的预测,所以我们必须每天运行模型。

但是,我们该如何管理呢?在上面的例子中,我们只预测了训练集之后紧接着的几周。

每当我们需要进行新的预测时,都需要重新训练模型,N-BEATS模型依赖于回溯期,因此我们必须使用最新的可用数据更新数据集,并重新训练模型。

在我们的例子中,就是使用最后一天的批发价格。

那么,我们是否也需要重新进行超参数调优呢?不是必须的

因为调优过程可能需要很长时间,并且计算成本高昂,相反,我们只需在训练集上进行一次超参数调优,并在每天重新训练模型时使用相同的超参数。

如果发现模型性能随时间下降过多,我们可以重新进行超参数调优。

总结

通过这篇文章大家可以学会以下知识:

  • 对N-BEATS模型的工作原理有了非常深入的理解

  • 能够在实际中运用N-BEATS模型

  • 在进行超参数调优时,能够调整模型的内部结构

如果大家想更深入地了解N-BEATS模型,可以查看N-BEATS原文。

论文地址:https://arxiv.org/pdf/1905.10437

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐