用 Python 轻松实现时间序列预测:Darts 时序融合Transformer Temporal Fusion Transformer

文中内容仅限技术学习与代码实践参考,市场存在不确定性,技术分析需谨慎验证,不构成任何投资建议。

Darts

Darts 是一个 Python 库,用于对时间序列进行用户友好型预测和异常检测。它包含多种模型,从 ARIMA 等经典模型到深度神经网络。所有预测模型都能以类似 scikit-learn 的方式使用 fit()predict() 函数。该库还可以轻松地对模型进行回溯测试,将多个模型的预测结果结合起来,并将外部数据考虑在内。Darts 支持单变量和多变量时间序列和模型。基于 ML 的模型可以在包含多个时间序列的潜在大型数据集上进行训练,其中一些模型还为概率预测提供了丰富的支持。

时序融合Transformer

在本笔记本中,我们展示了两个如何使用 Darts 的 TFTModel 的示例。如果您是 darts 的新手,我们建议您先阅读快速入门笔记本。

# 如果在本地工作,修复 python 路径
from utils import fix_pythonpath_if_working_locally

fix_pythonpath_if_working_locally()
%load_ext autoreload
%autoreload 2
%matplotlib inline
import warnings

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

from darts import TimeSeries, concatenate
from darts.dataprocessing.transformers import Scaler
from darts.datasets import AirPassengersDataset, IceCreamHeaterDataset
from darts.metrics import mape
from darts.models import TFTModel
from darts.utils.likelihood_models.torch import QuantileRegression
from darts.utils.statistics import check_seasonality, plot_acf
from darts.utils.timeseries_generation import datetime_attribute_timeseries

warnings.filterwarnings("ignore")
import logging

logging.disable(logging.CRITICAL)

Temporal Fusion Transformer (TFT)

Darts 的 TFTModel 在原始 Temporal Fusion Transformer (TFT) 架构中融入了以下主要组件,如这篇论文中所述:

  • gating mechanisms: 跳过模型架构中未使用的组件

  • variable selection networks: 在每个时间步选择相关的输入变量。

  • 使用 LSTMs(长短期记忆网络)对过去和未来的输入进行时间处理

  • 多头注意力机制:捕捉长期时间依赖关系

  • 预测区间:默认情况下,生成的是分位数预测而不是确定性值

训练

TFTModel 可以使用过去和未来的协变量进行训练。它以固定大小的块(包括编码器和解码器部分)顺序进行训练:

  • 编码器:过去的输入,长度为 input_chunk_length
    • 过去的目标:必需
    • 过去的协变量:可选
  • 解码器:已知的未来输入,长度为 output_chunk_length
    • 未来的协变量:必需(如果没有可用的协变量,请考虑使用 TFTModel 的可选参数 add_encodersadd_relative_index,详见此处)

在每次迭代中,模型在解码器部分生成形状为 (output_chunk_length, n_quantiles) 的分位数预测。

预测

概率预测

默认情况下,TFTModel 使用 QuantileRegression 生成分位数概率预测。这给出了每个预测步骤中可能目标值的范围。Darts 中的大多数深度学习模型(包括 TFTModel)都支持 QuantileRegression 和其他 16 种似然性,通过在模型创建时设置 likelihood=MyLikelihood() 来产生概率预测。

为了获得有意义的结果,在预测时设置 num_samples >> 1。例如:

model.predict(*args, **kwargs, num_samples=200)

当预测时间跨度为 n 时,使用与训练期间相同大小的编码器-解码器块进行自回归生成预测。

如果 n > output_chunk_length,您必须为传递给 model.train() 的协变量提供额外的未来值。

确定性预测

要生成确定性而非概率性预测,在模型创建时将参数 likelihood 设置为 None,并将 loss_fn 设置为 PyTorch 损失函数。例如:

model = TFTModel(*args, **kwargs, likelihood=None, loss_fn=torch.nn.MSELoss())
...
model.predict(*args, **kwargs, num_samples=1)
# 开始前,我们定义一些常量
num_samples = 200

figsize = (9, 6)
lowest_q, low_q, high_q, highest_q = 0.01, 0.1, 0.9, 0.99
label_q_outer = f"{int(lowest_q * 100)}-{int(highest_q * 100)}th percentiles"
label_q_inner = f"{int(low_q * 100)}-{int(high_q * 100)}th percentiles"

航空乘客示例

这个数据集高度依赖于协变量。知道月份可以告诉我们很多关于季节成分的信息,而年份则决定了趋势成分的影响。

此外,让我们将时间索引转换为整数值,并将它们也作为协变量使用。

所有这三个协变量在未来都是已知的,可以作为 future_covariatesTFTModel 一起使用。

# 读取数据
series = AirPassengersDataset().load()

# 我们将每月乘客数转换为每月平均每日乘客数
series = series / TimeSeries.from_values(series.time_index.days_in_month)
series = series.astype(np.float32)

# 创建训练和验证集:
training_cutoff = pd.Timestamp("19571201")
train, val = series.split_after(training_cutoff)

# 对时间序列进行归一化(注意:我们避免在验证集上拟合变换器)
transformer = Scaler()
train_transformed = transformer.fit_transform(train)
val_transformed = transformer.transform(val)
series_transformed = transformer.transform(series)

# 创建年份、月份和整数索引协变量序列
covariates = datetime_attribute_timeseries(series, attribute="year", one_hot=False)
covariates = covariates.stack(
    datetime_attribute_timeseries(series, attribute="month", one_hot=False)
)
covariates = covariates.stack(
    TimeSeries.from_times_and_values(
        times=series.time_index,
        values=np.arange(len(series)),
        columns=["linear_increase"],
    )
)
covariates = covariates.astype(np.float32)

# 变换协变量(注意:我们在训练集上拟合变换器,然后可以变换整个协变量序列)
scaler_covs = Scaler()
cov_train, cov_val = covariates.split_after(training_cutoff)
scaler_covs.fit(cov_train)
covariates_transformed = scaler_covs.transform(covariates)

创建模型

如果您想生成确定性预测而不是分位数预测,可以使用 PyTorch 损失函数(即设置 loss_fn=torch.nn.MSELoss()likelihood=None)。

TFTModel 只有在提供了某些未来输入的情况下才能使用。可选参数 add_encodersadd_relative_index 可能很有用,特别是当我们没有任何未来输入可用时。它们会生成编码后的时间数据,用作未来协变量。

由于我们已经在示例中定义了未来协变量,因此它们被注释掉了。

# QuantileRegression 的默认分位数
quantiles = [
    0.01,
    0.05,
    0.1,
    0.15,
    0.2,
    0.25,
    0.3,
    0.4,
    0.5,
    0.6,
    0.7,
    0.75,
    0.8,
    0.85,
    0.9,
    0.95,
    0.99,
]
input_chunk_length = 24
forecast_horizon = 12
my_model = TFTModel(
    input_chunk_length=input_chunk_length,
    output_chunk_length=forecast_horizon,
    hidden_size=64,
    lstm_layers=1,
    num_attention_heads=4,
    dropout=0.1,
    batch_size=16,
    n_epochs=300,
    add_relative_index=False,
    add_encoders=None,
    likelihood=QuantileRegression(
        quantiles=quantiles
    ),  # 默认设置为 QuantileRegression
    # loss_fn=MSELoss(),
    random_state=42,
)

训练 TFT

接下来,我们可以将完整的 covariates 序列作为 future_covariates 参数提供给模型;模型将切片这些协变量,并仅使用其所需的部分来训练预测目标 train_transformed

my_model.fit(train_transformed, future_covariates=covariates_transformed, verbose=True)
TFTModel(hidden_size=64, lstm_layers=1, num_attention_heads=4, full_attention=False, feed_forward=GatedResidualNetwork, dropout=0.1, hidden_continuous_size=8, categorical_embedding_sizes=None, add_relative_index=False, loss_fn=None, likelihood=<darts.utils.likelihood_models.torch.QuantileRegression object at 0x7f92e0d64c70>, norm_type=LayerNorm, use_static_covariates=True, input_chunk_length=24, output_chunk_length=12, batch_size=16, n_epochs=300, add_encoders=None, random_state=42)

查看验证集上的预测

我们使用“当前”模型(即训练过程结束时的模型)进行一次性的 24 个月预测:

def eval_model(model, n, actual_series, val_series):
    pred_series = model.predict(n=n, num_samples=num_samples)

    # 绘制实际序列
    plt.figure(figsize=figsize)
    actual_series[: pred_series.end_time()].plot(label="actual")

    # 绘制带有分位数区间的预测
    pred_series.plot(
        low_quantile=lowest_q, high_quantile=highest_q, label=label_q_outer
    )
    pred_series.plot(low_quantile=low_q, high_quantile=high_q, label=label_q_inner)

    plt.title(f"MAPE: {mape(val_series, pred_series):.2f}%")
    plt.legend()

eval_model(my_model, 24, series_transformed, val_transformed)

img

回测

让我们回测我们的 TFTModel 模型,看看它在过去 3 年中以 12 个月的预测时间跨度表现如何:

backtest_series = my_model.historical_forecasts(
    series_transformed,
    future_covariates=covariates_transformed,
    start=train.end_time() + train.freq,
    num_samples=num_samples,
    forecast_horizon=forecast_horizon,
    stride=forecast_horizon,
    last_points_only=False,
    retrain=False,
    verbose=True,
)
def eval_backtest(backtest_series, actual_series, horizon, start, transformer):
    plt.figure(figsize=figsize)
    actual_series.plot(label="actual")
    backtest_series.plot(
        low_quantile=lowest_q, high_quantile=highest_q, label=label_q_outer
    )
    backtest_series.plot(low_quantile=low_q, high_quantile=high_q, label=label_q_inner)
    plt.legend()
    plt.title(f"Backtest, starting {start}, {horizon}-months horizon")
    print(
        "MAPE: {:.2f}%".format(
            mape(
                transformer.inverse_transform(actual_series),
                transformer.inverse_transform(backtest_series),
            )
        )
    )


eval_backtest(
    backtest_series=concatenate(backtest_series),
    actual_series=series_transformed,
    horizon=forecast_horizon,
    start=training_cutoff,
    transformer=transformer,
)
MAPE: 4.90%

img

月度冰淇淋销售额

让我们尝试另一个数据集。自 2004 年以来的月度冰淇淋和取暖器销售额。我们的目标是预测未来的冰淇淋销售额。首先,我们从数据中构建时间序列,并检查其周期性。

series_ice_heater = IceCreamHeaterDataset().load()

plt.figure(figsize=figsize)
series_ice_heater.plot()

print(check_seasonality(series_ice_heater["ice cream"], max_lag=36))
print(check_seasonality(series_ice_heater["heater"], max_lag=36))

plt.figure(figsize=figsize)
plot_acf(series_ice_heater["ice cream"], 12, max_lag=36)  # ~1 年季节性
(True, 12)
(True, 12)

img

<Figure size 900x600 with 0 Axes>

img

处理数据

我们再次观察到 12 个月的季节性。这次我们不会定义月度未来协变量 -> 我们让模型自己处理!

让我们定义过去的协变量。如果我们使用过去的取暖器销售数据来预测冰淇淋销售会怎样?

# 将月度销售额转换为每月平均每日销售额
converted_series = []
for col in ["ice cream", "heater"]:
    converted_series.append(
        series_ice_heater[col]
        / TimeSeries.from_values(series_ice_heater.time_index.days_in_month)
    )
converted_series = concatenate(converted_series, axis=1)
converted_series = converted_series[pd.Timestamp("20100101") :]

# 定义训练/验证截止时间
forecast_horizon_ice = 12
training_cutoff_ice = converted_series.time_index[-(2 * forecast_horizon_ice)]

# 将冰淇淋销售额作为目标,创建训练和验证集并变换数据
series_ice = converted_series["ice cream"]
train_ice, val_ice = series_ice.split_before(training_cutoff_ice)
transformer_ice = Scaler()
train_ice_transformed = transformer_ice.fit_transform(train_ice)
val_ice_transformed = transformer_ice.transform(val_ice)
series_ice_transformed = transformer_ice.transform(series_ice)

# 将取暖器销售额作为过去协变量并变换数据
covariates_heat = converted_series["heater"]
cov_heat_train, cov_heat_val = covariates_heat.split_before(training_cutoff_ice)
transformer_heat = Scaler()
transformer_heat.fit(cov_heat_train)
covariates_heat_transformed = transformer_heat.transform(covariates_heat)

创建具有自动生成的未来协变量的模型并训练

由于我们没有定义未来协变量,我们需要告诉模型自己生成未来协变量。

  • add_encoders: 可以从日期时间属性、循环重复的时间模式、索引位置和自定义索引编码函数中添加多个编码作为过去和/或未来协变量。您甚至可以添加一个处理训练、验证和预测数据适当缩放的变换器!请在此处阅读有关 TFTModel 文档的更多信息

  • add_relative_index: 为每个编码器-解码器块添加相对于预测点的缩放整数位置(如果您真的不想使用任何未来协变量,这可能很有用。位置值在所有块中保持不变,不会添加额外信息)。

我们使用 add_encoders={'cyclic': {'future': ['month']}} 来将 12 个月的季节性作为未来协变量考虑。

# 使用过去 3 年的数据作为过去输入数据
input_chunk_length_ice = 36

# 由于我们没有未来协变量,使用 `add_encoders`
my_model_ice = TFTModel(
    input_chunk_length=input_chunk_length_ice,
    output_chunk_length=forecast_horizon_ice,
    hidden_size=32,
    lstm_layers=1,
    batch_size=16,
    n_epochs=300,
    dropout=0.1,
    add_encoders={"cyclic": {"future": ["month"]}},
    add_relative_index=False,
    optimizer_kwargs={"lr": 1e-3},
    random_state=42,
)

# 使用过去协变量拟合模型
my_model_ice.fit(
    train_ice_transformed, past_covariates=covariates_heat_transformed, verbose=True
)
TFTModel(hidden_size=32, lstm_layers=1, num_attention_heads=4, full_attention=False, feed_forward=GatedResidualNetwork, dropout=0.1, hidden_continuous_size=8, categorical_embedding_sizes=None, add_relative_index=False, loss_fn=None, likelihood=None, norm_type=LayerNorm, use_static_covariates=True, input_chunk_length=36, output_chunk_length=12, batch_size=16, n_epochs=300, add_encoders={'cyclic': {'future': ['month']}}, optimizer_kwargs={'lr': 0.001}, random_state=42)

查看验证集上的预测

再次,我们使用“当前”模型(即训练过程结束时的模型)进行一次性的 24 个月预测:

n = 24
eval_model(
    model=my_model_ice,
    n=n,
    actual_series=series_ice_transformed[
        train_ice.end_time() - (2 * n - 1) * train_ice.freq :
    ],
    val_series=val_ice_transformed,
)

img

回测

让我们回测我们的 TFTModel 模型,看看它在过去 2 年中以 12 个月的预测时间跨度表现如何:

# 使用两个模型计算回测预测
last_points_only = False
backtest_series_ice = my_model_ice.historical_forecasts(
    series_ice_transformed,
    num_samples=num_samples,
    start=training_cutoff_ice,
    forecast_horizon=forecast_horizon_ice,
    stride=1 if last_points_only else forecast_horizon_ice,
    retrain=False,
    last_points_only=last_points_only,
    overlap_end=True,
    verbose=True,
)

backtest_series_ice = (
    concatenate(backtest_series_ice)
    if isinstance(backtest_series_ice, list)
    else backtest_series_ice
)
eval_backtest(
    backtest_series=backtest_series_ice,
    actual_series=series_ice_transformed[
        train_ice.start_time() - 2 * forecast_horizon_ice * train_ice.freq :
    ],
    horizon=forecast_horizon_ice,
    start=training_cutoff_ice,
    transformer=transformer_ice,
)
MAPE: 5.32%

img

可解释性

让我们尝试理解我们的 TFTModel 模型学到了什么。了解特征重要性以及模型对过去和未来输入的关注程度会很有帮助。

TFTExplainer 正是为此而生!您可以在此处找到文档。

from darts.explainability import TFTExplainer

要实例化解释器,我们有两个选项: - 传递自定义的背景序列输入,用作解释的默认输入。 - 让解释器自动从模型加载背景。只有在模型在单个目标序列上训练时才可能(如我们的情况)。

explainer = TFTExplainer(my_model_ice)

现在我们可以使用 explain() 生成解释。为此我们再次有两个选项: - 传递自定义的前景序列输入进行解释 - 不传递任何前景来解释背景

explainability_result = explainer.explain()

让我们看看特征重要性: - 编码器特征重要性:包含过去目标、过去协变量和“历史”未来协变量(输入块中的未来协变量值) - 解码器特征重要性:包含“未来”未来协变量(输出块中的未来协变量值) - 静态协变量重要性:静态变量的重要性(仅在模型使用带有静态协变量的 series 训练时显示)

explainer.plot_variable_selection(explainability_result)

img

正如预期的那样,过去冰淇淋销售的历史是编码器中最重要的特征。月份的循环编码也有助于编码器和解码器学习季节性模式。

让我们看看模型对过去和未来输入的关注程度(注意力权重)。

我们有多种绘图选项:

plot_type - “time” - 绘制所有预测步骤上聚合的注意力 - “all” - 单独绘制每个预测步骤的注意力(范围从 1output_chunk_length) - “heatmap” - 将所有预测步骤的注意力绘制为热力图

show_index_as - “relative” - 将 x 轴设置为相对于第一个预测时间步,范围从 -input_chunk_lengthoutput_chunk_length - 10 表示第一个预测时间步(由虚线突出显示)。 - “time” - 使用实际的 x 轴时间索引。虚线突出显示第一个预测时间步。

explainer.plot_attention(explainability_result, plot_type="time")

img

<Axes: title={'center': 'Attention per Horizon'}, xlabel='Index relative to first prediction point', ylabel='Attention'>

我们可以看到有趣的注意力区域: - 相对索引 -12 处的最大注意力。这表示年度季节性,这对冰淇淋销售有意义。 - 输入块开始处(-36)的高注意力:这可能既来自捕获当前输入的值范围,也来自季节性(-3 年) - 输入块末尾(-1)处的注意力更高:模型关注最近的过去 - 未来输入上的注意力(我们在下一张图中进一步查看)

explainer.plot_attention(explainability_result, plot_type="all")

img

<Axes: title={'center': 'Mean Attention'}, xlabel='Index relative to first prediction point', ylabel='Attention'>

在输出块(索引 0 - 11)上,我们看到模型仅关注每个时间跨度的过去相对值。这是因为 TFTModel 默认使用 full_attention=False。当将其设置为 True 时,模型还将关注当前和未来的输入。

explainer.plot_attention(explainability_result, plot_type="heatmap")

img

<Axes: title={'center': 'Attention Heat Map'}, xlabel='Index relative to first prediction point', ylabel='Horizon'>

我们还可以直接从 explainability_result 获取值。您可以在此处找到文档。

explainability_result.get_encoder_importance()
darts_enc_fc_cyc_month_sinheaterdarts_enc_fc_cyc_month_cosice cream
03.74.86.884.7
explainability_result.get_decoder_importance()
darts_enc_fc_cyc_month_sindarts_enc_fc_cyc_month_cos
010.289.8
explainability_result.get_static_covariates_importance()

我们还可以将注意力提取为 TimeSeries,并将其与数据一起绘制。

attention = explainability_result.get_attention().mean(axis=1)

time_intersection = train_ice_transformed.time_index.intersection(attention.time_index)

train_ice_transformed[time_intersection].plot()
attention.plot(label="mean_attention", max_nr_components=12)

img

更多信息

虽然我们只研究了一个单变量预测示例,但 TFTExplainer 可以无缝应用于多变量和/或多个 TimeSeries 用例。

风险提示与免责声明
本文内容基于公开信息研究整理,不构成任何形式的投资建议。历史表现不应作为未来收益保证,市场存在不可预见的波动风险。投资者需结合自身财务状况及风险承受能力独立决策,并自行承担交易结果。作者及发布方不对任何依据本文操作导致的损失承担法律责任。市场有风险,投资须谨慎。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

船长Q

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值