模型集成

以下是对Darts中集成模型的简要演示。从 快速入门笔记本 提供的示例开始,将详细介绍一些高级功能和细微差别。

本笔记本涵盖了以下主题:

[1]:
# fix python path if working locally
from utils import fix_pythonpath_if_working_locally

fix_pythonpath_if_working_locally()
[2]:
%load_ext autoreload
%autoreload 2
%matplotlib inline
import matplotlib.pyplot as plt

from darts.models import (
    ExponentialSmoothing,
    KalmanForecaster,
    LinearRegressionModel,
    NaiveDrift,
    NaiveEnsembleModel,
    NaiveSeasonal,
    RandomForest,
    RegressionEnsembleModel,
    TCNModel,
)
from darts.metrics import mape
from darts.datasets import AirPassengersDataset
from darts.utils.timeseries_generation import (
    datetime_attribute_timeseries as dt_attr,
)
from darts.dataprocessing.transformers import Scaler

import warnings

warnings.filterwarnings("ignore")

import logging

logging.disable(logging.CRITICAL)

基础与参考

集成方法结合了多个“弱”模型的预测,以获得一个更稳健和准确的模型。

Darts 的所有集成模型都依赖于 堆叠技术 (参考)。它们提供了与其他预测模型相同的功能。根据集成的模型,它们可以:

  • 利用协变量

  • 在多系列上进行训练

  • 预测多变量目标

  • 生成概率预测

  • 以及更多…

[3]:
# using the AirPassenger dataset, directly available in darts
ts_air = AirPassengersDataset().load()
ts_air.plot()
[3]:
<Axes: xlabel='Month'>
../_images/examples_19-EnsembleModel-examples_4_1.png

朴素集成

朴素集成简单地取集成预测模型生成的预测的平均值(均值)。Darts的 NaiveEnsembleModel 接受本地和全局预测模型(以及两者的组合,有一些额外的限制)。

[4]:
naive_ensemble = NaiveEnsembleModel(
    forecasting_models=[NaiveSeasonal(K=12), NaiveDrift()]
)

backtest = naive_ensemble.historical_forecasts(ts_air, start=0.6, forecast_horizon=3)

ts_air.plot(label="series")
backtest.plot(label="prediction")
print("NaiveEnsemble (naive) MAPE:", round(mape(backtest, ts_air), 5))
NaiveEnsemble (naive) MAPE: 11.87818
../_images/examples_19-EnsembleModel-examples_6_1.png

注意:在查看每个模型的MAPE后,人们会注意到 NaiveSeasonal 实际上在单独使用时比与 NaiveDrift 集成时表现更好。在定义集成之前,检查单个模型的性能通常是一个好的做法。

在创建新的 NaiveEnsemble 之前,我们将筛选模型以确定哪些模型能够很好地协同工作。候选模型包括: - LinearRegressionModel :经典且简单的模型 - ExponentialSmoothing :移动窗口模型 - KalmanForecaster :基于滤波器的模型 - RandomForest :决策树模型

[5]:
candidates_models = {
    "LinearRegression": (LinearRegressionModel, {"lags": 12}),
    "ExponentialSmoothing": (ExponentialSmoothing, {}),
    "KalmanForecaster": (KalmanForecaster, {"dim_x": 12}),
    "RandomForest": (RandomForest, {"lags": 12, "random_state": 0}),
}

backtest_models = []

for model_name, (model_cls, model_kwargs) in candidates_models.items():
    model = model_cls(**model_kwargs)
    backtest_models.append(
        model.historical_forecasts(ts_air, start=0.6, forecast_horizon=3)
    )
    print(f"{model_name} MAPE: {round(mape(backtest_models[-1], ts_air), 5)}")
LinearRegression MAPE: 4.64008
ExponentialSmoothing MAPE: 4.44874
KalmanForecaster MAPE: 4.5539
RandomForest MAPE: 8.02264
[6]:
fix, axes = plt.subplots(2, 2, figsize=(9, 6))
for ax, backtest, model_name in zip(
    axes.flatten(),
    backtest_models,
    list(candidates_models.keys()),
):
    ts_air[-len(backtest) :].plot(ax=ax, label="ground truth")
    backtest.plot(ax=ax, label=model_name)

    ax.set_title(model_name)
    ax.set_ylim([250, 650])
plt.tight_layout()
../_images/examples_19-EnsembleModel-examples_9_0.png

使用 LinearRegressionModelKalmanForecaster 获得的历史预测看起来非常相似,而 ExponentialSmoothing 往往低估了真实值,RandomForest 未能捕捉到峰值。为了从集成中获益,我们将优先考虑多样性,并继续使用 LinearRegressionModelExponentialSmoothing

[7]:
ensemble = NaiveEnsembleModel(
    forecasting_models=[LinearRegressionModel(lags=12), ExponentialSmoothing()]
)

backtest = ensemble.historical_forecasts(ts_air, start=0.6, forecast_horizon=3)

ts_air[-len(backtest) :].plot(label="series")
backtest.plot(label="prediction")
plt.ylim([250, 650])
print("NaiveEnsemble (v2) MAPE:", round(mape(backtest, ts_air), 5))
NaiveEnsemble (v2) MAPE: 4.04297
../_images/examples_19-EnsembleModel-examples_11_1.png

与单个模型的MAPE得分相比,LinearRegressionModel 为4.64008,ExponentialSmoothing 为4.44874,集成模型将准确性提高到了4.04297!

使用协变量 & 预测多元序列

根据使用的预测模型,EnsembleModel 当然也可以利用协变量或预测多变量序列!协变量将仅传递给支持它们的预测模型。

在下面的例子中,ExponentialSmoothing 模型不支持任何协变量,而 LinearRegressionModel 模型支持 future_covariates

[8]:
ensemble = NaiveEnsembleModel(
    [LinearRegressionModel(lags=12, lags_future_covariates=[0]), ExponentialSmoothing()]
)

# encoding the months as integer, normalised
future_cov = dt_attr(ts_air.time_index, "month", add_length=12) / 12
backtest = ensemble.historical_forecasts(
    ts_air, future_covariates=future_cov, start=0.6, forecast_horizon=3
)

ts_air[-len(backtest) :].plot(label="series")
backtest.plot(label="prediction")
plt.ylim([250, 650])
print("NaiveEnsemble (w/ future covariates) MAPE:", round(mape(backtest, ts_air), 5))
NaiveEnsemble (w/ future covariates) MAPE: 4.07502
../_images/examples_19-EnsembleModel-examples_13_1.png

概率性朴素集成

结合支持概率预测的模型,结果是一个概率性的 NaiveEnsembleModel!我们可以轻松调整上述使用的模型,使其具有概率性,并在我们的预测中获得置信区间:

[9]:
ensemble_probabilistic = NaiveEnsembleModel(
    forecasting_models=[
        LinearRegressionModel(
            lags=12,
            likelihood="quantile",
            quantiles=[0.05, 0.5, 0.95],
        ),
        ExponentialSmoothing(),
    ]
)

# must pass num_samples > 1 to obtain a probabilistic forecasts
backtest = ensemble_probabilistic.historical_forecasts(
    ts_air, start=0.6, forecast_horizon=3, num_samples=100
)

ts_air[-len(backtest) :].plot(label="ground truth")
backtest.plot(label="prediction")
[9]:
<Axes: xlabel='time'>
../_images/examples_19-EnsembleModel-examples_15_1.png

学习集成

集成也可以被视为一个监督回归问题:给定一组预测(特征),找到一个模型来组合它们,以最小化目标上的误差。这就是 RegressionEnsembleModel 所做的。主要三个参数是:

  • forecasting_models 是我们想要进行集成预测的预测模型列表。

  • regression_train_n_points 是用于拟合“集成回归”模型(即,结合预测的内部模型)的时间步数。

  • regression_model 是可选的,可以是与 sklearn 兼容的回归模型或用于集成回归的 Darts RegressionModel。如果未指定,则使用 Darts 的 LinearRegressionModel。使用 sklearn 模型可以开箱即用,但使用 Darts 的回归模型可以潜在地使用各个预测的任意滞后作为回归模型的输入。

一旦这些元素就位,RegressionEnsembleModel 就可以像常规的预测模型一样使用:

[10]:
ensemble_model = RegressionEnsembleModel(
    forecasting_models=[NaiveSeasonal(K=12), NaiveDrift()],
    regression_train_n_points=12,
)

backtest = ensemble_model.historical_forecasts(ts_air, start=0.6, forecast_horizon=3)

ts_air.plot()
backtest.plot()

print("RegressionEnsemble (naive) MAPE:", round(mape(backtest, ts_air), 5))
RegressionEnsemble (naive) MAPE: 4.85142
../_images/examples_19-EnsembleModel-examples_17_1.png

naive ensembling section 开始时获得的 11.87818 的 MAPE 相比,在两个 naive 模型之上添加一个 LinearRegressionModel 确实提高了预测的质量。

现在,让我们看看当 RegressionEnsemble 预测模型不是那么简单时,是否能观察到类似的增益:

[11]:
ensemble = RegressionEnsembleModel(
    forecasting_models=[LinearRegressionModel(lags=12), ExponentialSmoothing()],
    regression_train_n_points=12,
)

backtest = ensemble.historical_forecasts(ts_air, start=0.6, forecast_horizon=3)

ts_air.plot(label="series")
backtest.plot(label="prediction")

print("RegressionEnsemble (v2) MAPE:", round(mape(backtest, ts_air), 5))
RegressionEnsemble (v2) MAPE: 4.63334
../_images/examples_19-EnsembleModel-examples_19_1.png

有趣的是,尽管与依赖于简单模型的 RegressionEnsemble 相比,MAPE 有所提高(MAPE: 4.85142),但它并未超越使用类似预测模型的 ``NaiveEnsemble``(MAPE: 4.04297)。

这种性能差距部分是由于为训练集成 LinearRegression 而设置的点;两个预测模型(LinearRegressionExponentialSmoothing)无法访问包含明显上升趋势的序列的最新值。

出于好奇,我们可以使用sklearn库中的``Ridge``回归模型来集成预测:

[12]:
from sklearn.linear_model import Ridge

ensemble = RegressionEnsembleModel(
    forecasting_models=[LinearRegressionModel(lags=12), ExponentialSmoothing()],
    regression_train_n_points=12,
    regression_model=Ridge(),
)

backtest = ensemble.historical_forecasts(ts_air, start=0.6, forecast_horizon=3)

print("RegressionEnsemble (Ridge) MAPE:", round(mape(backtest, ts_air), 5))
RegressionEnsemble (Ridge) MAPE: 6.46803

在这种特定情况下,使用带有正则化项的回归模型会恶化预测结果,但在其他情况下,它可能会改善预测结果。

使用历史预测进行训练

当预测的数值大于它们的 output_chunk_length 时,GlobalForecastingModels 依赖于自回归(使用它们自己的输出作为输入)来预测远期的数值。然而,随着预测的时间戳远离观测的末尾,预测的质量可能会显著下降。在 RegressionEnsemble 的回归模型训练期间,预测模型为实际已知并可用的地面真实时间戳生成预测,从而可以使用 historical_forecasts 而不是 predict()

这可以通过 train_using_historical_forecasts=True 来激活。

在底层,集成模型将使用 forecast_horizon=model.output_chunk_lengthstride=model.output_chunk_lengthlast_points_only=Falseoverlap_end=False 为每个模型触发历史预测,以预测目标序列的最后 regression_train_n_points 个点。

[13]:
# replacing the ExponentialSmoothing (local) with RandomForest (global)
ensemble = RegressionEnsembleModel(
    forecasting_models=[
        LinearRegressionModel(lags=12),
        RandomForest(lags=12, random_state=0),
    ],
    regression_train_n_points=12,
    train_using_historical_forecasts=False,
)
backtest = ensemble.historical_forecasts(ts_air, start=0.6, forecast_horizon=3)

ensemble_hist_fct = RegressionEnsembleModel(
    forecasting_models=[
        LinearRegressionModel(lags=12),
        RandomForest(lags=12, random_state=0),
    ],
    regression_train_n_points=12,
    train_using_historical_forecasts=True,
)
backtest_hist_fct = ensemble_hist_fct.historical_forecasts(
    ts_air, start=0.6, forecast_horizon=3
)

print("RegressionEnsemble (no hist_fct) MAPE:", round(mape(backtest, ts_air), 5))
print("RegressionEnsemble (hist_fct) MAPE:", round(mape(backtest_hist_fct, ts_air), 5))
RegressionEnsemble (no hist_fct) MAPE: 5.7016
RegressionEnsemble (hist_fct) MAPE: 5.12017

如预期,使用带有预测模型的历史预测来训练回归模型,可以产生更好的预测结果。

概率回归集成

为了具有概率性,RegressionEnsembleModel 必须有一个概率性的集成回归模型(参见 README 中的表格):

[14]:
ensemble = RegressionEnsembleModel(
    forecasting_models=[LinearRegressionModel(lags=12), ExponentialSmoothing()],
    regression_train_n_points=12,
    regression_model=LinearRegressionModel(
        lags_future_covariates=[0], likelihood="quantile", quantiles=[0.05, 0.5, 0.95]
    ),
)

backtest = ensemble.historical_forecasts(
    ts_air, start=0.6, forecast_horizon=3, num_samples=100
)

ts_air[-len(backtest) :].plot(label="ground truth")
backtest.plot(label="prediction")

print("RegressionEnsemble (probabilistic) MAPE:", round(mape(backtest, ts_air), 5))
RegressionEnsemble (probabilistic) MAPE: 5.15071
../_images/examples_19-EnsembleModel-examples_25_1.png

引导回归集成

RegressionEnsembleModel 的预测模型是概率性的时,它们的预测样本维度会被减少并作为集成回归的协变量使用。由于集成回归模型是确定性的,因此生成的预测也是确定性的。

[15]:
ensemble = RegressionEnsembleModel(
    forecasting_models=[
        LinearRegressionModel(
            lags=12, likelihood="quantile", quantiles=[0.05, 0.5, 0.95]
        ),
        ExponentialSmoothing(),
    ],
    regression_train_n_points=12,
    regression_train_num_samples=100,
    regression_train_samples_reduction="median",
)

backtest = ensemble.historical_forecasts(ts_air, start=0.6, forecast_horizon=3)

ts_air[-len(backtest) :].plot(label="ground truth")
backtest.plot(label="prediction")

print("RegressionEnsemble (bootstrap) MAPE:", round(mape(backtest, ts_air), 5))
RegressionEnsemble (bootstrap) MAPE: 5.10138
../_images/examples_19-EnsembleModel-examples_27_1.png

预训练集成

由于 NaiveEnsembleModelRegressionEnsembleModel 都接受 GlobalForecastingModel 作为预测模型,它们可以用于集成预训练的深度学习和回归模型。请注意,此功能仅在所有集成的预测模型都是 GlobalForecastingModel 类的实例并且在创建集成时已经训练的情况下才受支持。

免责声明 : 注意不要使用验证期间的数据对模型进行预训练,因为这会引入相当大的偏差。

注意 : TCNModel 的参数很大程度上受到 TCNModel 示例笔记本 的启发。

[16]:
# holding out values for validation
train, val = ts_air.split_after(0.8)

# scaling the target
scaler = Scaler()
train = scaler.fit_transform(train)
val = scaler.transform(val)

# use the month as a covariate
month_series = dt_attr(ts_air.time_index, attribute="month", one_hot=True)
scaler_month = Scaler()
month_series = scaler_month.fit_transform(month_series)

# training a regular linear regression, without any covariates
linreg_model = LinearRegressionModel(lags=24)
linreg_model.fit(train)

# instanciating a TCN model with parameters optimized for the AirPassenger dataset
tcn_model = TCNModel(
    input_chunk_length=24,
    output_chunk_length=12,
    n_epochs=500,
    dilation_base=2,
    weight_norm=True,
    kernel_size=5,
    num_filters=3,
    random_state=0,
)
tcn_model.fit(train, past_covariates=month_series)
[16]:
TCNModel(kernel_size=5, num_filters=3, num_layers=None, dilation_base=2, weight_norm=True, dropout=0.2, input_chunk_length=24, output_chunk_length=12, n_epochs=500, random_state=0)

作为健全性检查,我们将单独查看模型的预测:

[17]:
# individual model forecasts
pred_linreg = linreg_model.predict(24)
pred_tcn = tcn_model.predict(24, verbose=False)

# scaling them back
pred_linreg_rescaled = scaler.inverse_transform(pred_linreg)
pred_tcn_rescaled = scaler.inverse_transform(pred_tcn)

# plotting
ts_air[-24:].plot(label="ground truth")
pred_linreg_rescaled.plot(label="LinearRegressionModel")
pred_tcn_rescaled.plot(label="TCNModel")
plt.show()
../_images/examples_19-EnsembleModel-examples_31_0.png

既然我们已经对这些模型的个体表现有了很好的了解,我们可以将它们集成。我们必须确保设置 train_forecasting_models=False,否则在调用 predict() 之前需要先拟合集成模型。

建议 : 使用 save() 方法导出您的模型并保留一份您的权重副本。

[18]:
naive_ensemble = NaiveEnsembleModel(
    forecasting_models=[tcn_model, linreg_model], train_forecasting_models=False
)
# NaiveEnsemble initialized with pre-trained models can call predict() directly,
# the `series` argument must however be provided
pred_naive = naive_ensemble.predict(len(val), train)

pretrain_ensemble = RegressionEnsembleModel(
    forecasting_models=[tcn_model, linreg_model],
    regression_train_n_points=24,
    train_forecasting_models=False,
    train_using_historical_forecasts=False,
)
# RegressionEnsemble must train the ensemble model, even if the forecasting models are already trained
pretrain_ensemble.fit(train)
pred_ens = pretrain_ensemble.predict(len(val))

# scaling back the predictions
pred_naive_rescaled = scaler.inverse_transform(pred_naive)
pred_ens_rescaled = scaler.inverse_transform(pred_ens)

# plotting
plt.figure(figsize=(8, 5))
scaler.inverse_transform(val).plot(label="ground truth")
pred_naive_rescaled.plot(label="pre-trained NaiveEnsemble")
pred_ens_rescaled.plot(label="pre-trained RegressionEnsemble")
plt.ylim([250, 650])

# MAPE
print("LinearRegression MAPE:", round(mape(pred_linreg_rescaled, ts_air), 5))
print("TCNModel MAPE:", round(mape(pred_tcn_rescaled, ts_air), 5))
print("NaiveEnsemble (pre-trained) MAPE:", round(mape(pred_naive_rescaled, ts_air), 5))
print(
    "RegressionEnsemble (pre-trained) MAPE:", round(mape(pred_ens_rescaled, ts_air), 5)
)
LinearRegression MAPE: 3.91311
TCNModel MAPE: 4.70491
NaiveEnsemble (pre-trained) MAPE: 3.82837
RegressionEnsemble (pre-trained) MAPE: 3.61749
../_images/examples_19-EnsembleModel-examples_33_1.png

结论

结合预训练的 LinearRegressionTCNModel 模型使我们能够超越单一模型,并且在这些模型之上训练线性回归进一步提高了MAPE分数。

如果在这个小数据集上的收益仍然有限,集成是一种强大的技术,可以产生令人印象深刻的结果,并且在第4届Makridakis竞赛的获胜者中得到了显著应用(网站github仓库)。