文中内容仅限技术学习与代码实践参考,市场存在不确定性,技术分析需谨慎验证,不构成任何投资建议。
Darts 是一个 Python 库,用于对时间序列进行用户友好型预测和异常检测。它包含多种模型,从 ARIMA 等经典模型到深度神经网络。所有预测模型都能以类似 scikit-learn 的方式使用 fit()
和 predict()
函数。该库还可以轻松地对模型进行回溯测试,将多个模型的预测结果结合起来,并将外部数据考虑在内。Darts 支持单变量和多变量时间序列和模型。基于 ML 的模型可以在包含多个时间序列的潜在大型数据集上进行训练,其中一些模型还为概率预测提供了丰富的支持。
模型集成
Ensembling Models
以下是对 Darts 中集成模型的简要演示。从 Quickstart Notebook 提供的示例出发,将详细说明一些高级功能与细节。
本笔记本涵盖以下主题:
- 基础与参考
- 朴素集成(Naive Ensembling)
- 确定性
- 协变量与多元序列
- 概率性
- 可学习集成(Learned Ensembling)
- 确定性
- 概率性
- Bootstrap
- 预训练集成
# 若本地运行,先修正 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
from darts.dataprocessing.transformers import Scaler
from darts.datasets import AirPassengersDataset
from darts.metrics import mape
from darts.models import (
ExponentialSmoothing,
KalmanForecaster,
LinearRegressionModel,
NaiveDrift,
NaiveEnsembleModel,
NaiveSeasonal,
RandomForestModel,
RegressionEnsembleModel,
TCNModel,
)
from darts.utils.timeseries_generation import (
datetime_attribute_timeseries as dt_attr,
)
warnings.filterwarnings("ignore")
import logging
logging.disable(logging.CRITICAL)
基础与参考
集成(Ensembling)将多个“弱”模型的预测结果组合起来,以获得更稳健、更准确的模型。
Darts 的所有集成模型均基于 stacking 技术(参考文献)。它们与其他预测模型提供相同的功能。根据所集成模型的不同,它们可以:
- 利用协变量
- 在多个序列上进行训练
- 预测多元目标
- 生成概率预测
- 以及更多…
# 使用 darts 内置的 AirPassenger 数据集
ts_air = AirPassengersDataset().load()
ts_air.plot()
<Axes: xlabel='Month'>
朴素集成
朴素集成仅对集成的预测模型生成的预测结果取平均(均值)。Darts 的 NaiveEnsembleModel
可同时接受局部模型与全局模型(以及两者的组合,但存在额外限制)。
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
注意:查看每个模型的 MAPE 后,会发现 NaiveSeasonal
单独使用时的效果实际上优于与 NaiveDrift
集成。在定义集成之前,检查单个模型的性能通常是良好实践。
在创建新的 NaiveEnsemble
前,我们将筛选模型,找出哪些模型组合表现更好。候选模型包括:
LinearRegressionModel
:经典简单模型ExponentialSmoothing
:滑动窗口模型KalmanForecaster
:基于滤波的模型RandomForestModel
:决策树模型
candidates_models = {
"LinearRegression": (LinearRegressionModel, {"lags": 12}),
"ExponentialSmoothing": (ExponentialSmoothing, {}),
"KalmanForecaster": (KalmanForecaster, {"dim_x": 12}),
"RandomForestModel": (RandomForestModel, {"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.44774
KalmanForecaster MAPE: 4.5539
RandomForestModel MAPE: 8.02264
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()
LinearRegressionModel
与 KalmanForecaster
的历史预测结果十分相似,而 ExponentialSmoothing
倾向于低估真实值,RandomForestModel
则无法捕捉峰值。为了从集成中获益,我们将选择多样性更高的 LinearRegressionModel
与 ExponentialSmoothing
。
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
与单个模型的 MAPE 分数相比,LinearRegressionModel
为 4.64008,ExponentialSmoothing
为 4.44874,集成将准确率提高至 4.04297!
使用协变量与预测多元序列
根据所用预测模型的不同,EnsembleModel
当然也可以利用协变量或预测多元序列!协变量将仅传递给支持它们的预测模型。
在下面的示例中,ExponentialSmoothing
模型不支持任何协变量,而 LinearRegressionModel
支持 future_covariates
。
ensemble = NaiveEnsembleModel([
LinearRegressionModel(lags=12, lags_future_covariates=[0]),
ExponentialSmoothing(),
])
# 将月份编码为整数并归一化
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
概率朴素集成
组合支持概率预测的模型将得到一个概率性 NaiveEnsembleModel
!我们可以轻松将上述模型调整为概率型,并在预测中获得置信区间:
ensemble_probabilistic = NaiveEnsembleModel(
forecasting_models=[
LinearRegressionModel(
lags=12,
likelihood="quantile",
quantiles=[0.05, 0.5, 0.95],
),
ExponentialSmoothing(),
]
)
# 必须传入 num_samples > 1 以获得概率预测
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")
<Axes: xlabel='time'>
可学习集成
集成也可视为监督回归问题:给定一组预测(特征),找到能够将它们组合以最小化目标误差的模型。这正是 RegressionEnsembleModel
所做的工作。主要包含三个参数:
forecasting_models
:待集成的预测模型列表。regression_train_n_points
:用于拟合“集成回归”模型(即组合预测的内部模型)的时间步数。regression_model
:可选,用于集成回归的 sklearn 兼容回归模型或 Darts 的SKLearnModel
。若未指定,则使用 Darts 的LinearRegressionModel
。使用 sklearn 模型开箱即用,但使用 Darts 回归模型可利用单个预测的任意滞后作为回归模型的输入。
一旦这些元素就绪,RegressionEnsembleModel
即可像常规预测模型一样使用:
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
与朴素集成部分最初得到的 MAPE 11.87818 相比,在两个朴素模型之上添加 LinearRegressionModel
确实提高了预测质量。
现在,让我们看看当 RegressionEnsemble
的预测模型不是朴素模型时,是否也能观察到类似提升:
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
有趣的是,尽管与依赖朴素模型的 RegressionEnsemble
(MAPE: 4.85142)相比 MAPE 有所提升,但它并未超越使用相似预测模型的 NaiveEnsemble
(MAPE: 4.04297)。
这种性能差距部分是由于留出用于训练集成 LinearRegression
的点造成的;两个预测模型(LinearRegression
与 ExponentialSmoothing
)无法访问序列的最新值,而这些值包含明显的上升趋势。
出于好奇,我们可以使用 sklearn 库中的 Ridge
回归模型来集成预测:
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_length
、stride=model.output_chunk_length
、last_points_only=False
以及 overlap_end=False
触发每个模型的历史预测,以预测目标序列的最后 regression_train_n_points
个点。
# 将 ExponentialSmoothing(局部模型)替换为 RandomForestModel(全局模型)
ensemble = RegressionEnsembleModel(
forecasting_models=[
LinearRegressionModel(lags=12),
RandomForestModel(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),
RandomForestModel(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 中的表格):
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
Bootstrap 回归集成
当 RegressionEnsembleModel
的预测模型具有概率性时,其预测的样本维度将被压缩并用作集成回归的协变量。由于集成回归模型是确定性的,生成的预测也是确定性的。
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
预训练集成
由于 NaiveEnsembleModel
与 RegressionEnsembleModel
均接受 GlobalForecastingModel
作为预测模型,因此它们可用于集成已预训练的深度学习与回归模型。请注意,仅当所有集成的预测模型均为 GlobalForecastingModel
类的实例,且在创建集成时已训练完成时,才支持此功能。
免责声明:请注意,不要使用验证期间使用的数据预训练模型,否则会引入显著偏差。
注意:TCNModel
的参数大量参考了 TCNModel 示例笔记本。
# 留出用于验证的值
train, val = ts_air.split_after(0.8)
# 对目标进行缩放
scaler = Scaler()
train = scaler.fit_transform(train)
val = scaler.transform(val)
# 使用月份作为协变量
month_series = dt_attr(ts_air.time_index, attribute="month", one_hot=True)
scaler_month = Scaler()
month_series = scaler_month.fit_transform(month_series)
# 训练一个不带任何协变量的常规线性回归
linreg_model = LinearRegressionModel(lags=24)
linreg_model.fit(train)
# 为 AirPassenger 数据集优化的参数实例化 TCN 模型
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)
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)
作为完整性检查,我们将查看各模型的单独预测:
# 单个模型预测
pred_linreg = linreg_model.predict(24)
pred_tcn = tcn_model.predict(24, verbose=False)
# 反缩放预测
pred_linreg_rescaled = scaler.inverse_transform(pred_linreg)
pred_tcn_rescaled = scaler.inverse_transform(pred_tcn)
# 绘图
ts_air[-24:].plot(label="ground truth")
pred_linreg_rescaled.plot(label="LinearRegressionModel")
pred_tcn_rescaled.plot(label="TCNModel")
plt.show()
现在我们已了解每个模型的单独性能,可以将它们集成。必须确保设置 train_forecasting_models=False
,否则集成需要先拟合才能调用 predict()
。
建议:使用 save()
方法导出模型并保留权重副本。
naive_ensemble = NaiveEnsembleModel(
forecasting_models=[tcn_model, linreg_model], train_forecasting_models=False
)
# 使用预训练模型初始化的 NaiveEnsemble 可直接调用 predict(),
# 但仍需提供 `series` 参数
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 即使预测模型已训练,也必须训练集成模型
pretrain_ensemble.fit(train)
pred_ens = pretrain_ensemble.predict(len(val))
# 反缩放预测
pred_naive_rescaled = scaler.inverse_transform(pred_naive)
pred_ens_rescaled = scaler.inverse_transform(pred_ens)
# 绘图
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
结论
对预训练的 LinearRegression
与 TCNModel
模型进行集成,使我们超越了单个模型的性能,并在这些模型的预测之上训练线性回归进一步改善了 MAPE 分数。
尽管在此小数据集上提升有限,集成是一种强大的技术,可带来令人印象深刻的结果,并被第 4 届 Makridakis 竞赛的获胜者所使用(网站,github 仓库)。
风险提示与免责声明
本文内容基于公开信息研究整理,不构成任何形式的投资建议。历史表现不应作为未来收益保证,市场存在不可预见的波动风险。投资者需结合自身财务状况及风险承受能力独立决策,并自行承担交易结果。作者及发布方不对任何依据本文操作导致的损失承担法律责任。市场有风险,投资须谨慎。