多时间序列、预训练模型和协变量¶
本 Notebook 是一个教程,用于介绍
在多个时间序列上训练单个模型
使用预训练模型获取训练期间未见过的时间序列的预测结果
使用协变量训练和使用模型
使用一个或多个多元时间序列训练和使用模型
首先,导入必要的库
[1]:
# fix python path if working locally
from utils import fix_pythonpath_if_working_locally
fix_pythonpath_if_working_locally()
import logging
import matplotlib.pyplot as plt
import numpy as np
import torch
from darts import concatenate
from darts.dataprocessing.transformers import Scaler
from darts.datasets import AirPassengersDataset, ElectricityDataset, MonthlyMilkDataset
from darts.metrics import mae, mape
from darts.models import (
VARIMA,
BlockRNNModel,
NBEATSModel,
RNNModel,
)
from darts.utils.callbacks import TFMProgressBar
from darts.utils.timeseries_generation import (
datetime_attribute_timeseries,
sine_timeseries,
)
logging.disable(logging.CRITICAL)
import warnings
warnings.filterwarnings("ignore")
%matplotlib inline
# for reproducibility
torch.manual_seed(1)
np.random.seed(1)
def generate_torch_kwargs():
# run torch models on CPU, and disable progress bars for all model stages except training.
return {
"pl_trainer_kwargs": {
"accelerator": "cpu",
"callbacks": [TFMProgressBar(enable_train_bar_only=True)],
}
}
读取数据¶
让我们首先读取两个时间序列——一个包含每月航空乘客数量,另一个包含每头奶牛的每月牛奶产量。这两个时间序列彼此之间没有太多关系,除了它们都是月度频率,具有明显的年度周期性和上升趋势,并且(完全巧合地)它们包含数量级可比的值。
[2]:
series_air = AirPassengersDataset().load()
series_milk = MonthlyMilkDataset().load()
series_air.plot(label="Number of air passengers")
series_milk.plot(label="Pounds of milk produced per cow")
plt.legend();

预处理¶
通常神经网络在归一化/标准化数据上效果更好。这里我们将使用 Scaler
类将我们的两个时间序列归一化到 0 到 1 之间
[3]:
scaler_air, scaler_milk = Scaler(), Scaler()
series_air_scaled = scaler_air.fit_transform(series_air)
series_milk_scaled = scaler_milk.fit_transform(series_milk)
series_air_scaled.plot(label="air")
series_milk_scaled.plot(label="milk")
plt.legend();

训练/验证集分割¶
让我们保留两个序列的最后 36 个月作为验证集
[4]:
train_air, val_air = series_air_scaled[:-36], series_air_scaled[-36:]
train_milk, val_milk = series_milk_scaled[:-36], series_milk_scaled[-36:]
全局预测模型¶
Darts 包含许多预测模型,但并非所有模型都可以在多个时间序列上进行训练。支持在多个序列上训练的模型称为 全局 模型。可以在此处(表格底部)找到全局模型的完整列表,例如:
LinearRegressionModel
BlockRNNModel
时间卷积网络 (TCNModel)
N-Beats (NBEATSModel)
TiDEModel
在下文中,我们将区分两类时间序列
目标时间序列 是我们感兴趣并希望预测(给定其历史数据)的时间序列
协变量时间序列 是可能有助于预测目标序列,但我们不感兴趣进行预测的时间序列。有时也称为 外部数据。
注意:静态协变量是时间不变的,对应于与目标时间序列分量相关的附加信息。有关此类协变量的详细信息,请参阅静态协变量 notebook。
我们进一步区分协变量序列,取决于它们是否可以提前已知
历史协变量 指的是在预测时已知其过去值的时间序列。这些通常是需要测量或观察的事物。
未来协变量 指的是在预测时已知其预测范围内未来值的时间序列。这些可以例如表示已知的未来假日或天气预报。
一些模型只使用历史协变量,另一些只使用未来协变量,还有一些模型可能同时使用两者。我们将在其他 Notebook 中更深入地探讨这个主题,但这张表格详细列出了每个模型支持的协变量。
上述所有全局模型都支持在多个序列上进行训练。此外,它们也都支持 多元序列。这意味着它们可以无缝地用于具有多个维度的时间序列;目标序列可以包含一个(通常如此)或多个维度。具有多个维度的时间序列本质上只是一个常规时间序列,其中每个时间戳上的值是向量而不是标量。
例如,BlockRNNModel、N-Beats、TCN 和 Transformer 模型遵循“块”架构。它们包含一个神经网络,该网络接收时间序列的块作为输入,并输出(预测的)未来时间序列值的块。输入维度是目标序列的维度(分量)数量,加上所有协变量的分量数量——堆叠在一起。输出维度只是目标序列的维度数量:
RNNModel
的工作方式不同,它以循环方式进行(这也是为什么它们支持未来协变量)。好消息是,作为用户,我们不必过多担心不同的模型类型和输入/输出维度。维度会根据训练数据由模型自动推断出来,对历史或未来协变量的支持只需通过 past_covariates
或 future_covariates
参数处理。
构建模型时,我们仍然需要指定两个重要参数
input_chunk_length
: 这是模型的查找窗口长度;因此,模型的每个输出都是通过读取之前的input_chunk_length
点计算得出的。output_chunk_length
: 这是内部模型产生的输出(预测)的长度。然而,“外部” Darts 模型(例如NBEATSModel
,TCNModel
等的模型)的predict()
方法可以用于更长的时间范围。在这些情况下,如果为比output_chunk_length
更长的时间范围调用predict()
,内部模型将简单地被重复调用,以自回归的方式利用其先前的输出来进行预测。如果使用了past_covariates
,则要求这些协变量要提前足够长时间已知。
单一时间序列示例¶
让我们看第一个例子。我们将构建一个 N-BEATS 模型,该模型的查找窗口为 24 点 (input_chunk_length=24
),并预测接下来的 12 点 (output_chunk_length=12
)。我们选择这些值是为了让模型一次生成一年的连续预测,同时查看过去两年。
[5]:
model_air = NBEATSModel(
input_chunk_length=24,
output_chunk_length=12,
n_epochs=200,
random_state=0,
**generate_torch_kwargs(),
)
这个模型可以像任何其他 Darts 预测模型一样使用,通过在单个时间序列上进行拟合
[6]:
model_air.fit(train_air)
[6]:
NBEATSModel(generic_architecture=True, num_stacks=30, num_blocks=1, num_layers=4, layer_widths=256, expansion_coefficient_dim=5, trend_polynomial_degree=2, dropout=0.0, activation=ReLU, input_chunk_length=24, output_chunk_length=12, n_epochs=200, random_state=0, pl_trainer_kwargs={'accelerator': 'cpu', 'callbacks': [<darts.utils.callbacks.TFMProgressBar object at 0x103d1a710>]})
与任何其他 Darts 预测模型一样,我们可以通过调用 predict()
来获得预测。请注意,在下面,我们调用的 predict()
预测范围是 36,这比模型内部的 output_chunk_length
12 要长。这在这里不是问题——如上所述,在这种情况下,内部模型将简单地以自回归的方式在其自身输出上重复调用。在这种情况下,它将被调用三次,以便三个 12 点的输出构成最终的 36 点预测——但这一切都在幕后透明地完成。
[7]:
pred = model_air.predict(n=36)
series_air_scaled.plot(label="actual")
pred.plot(label="forecast")
plt.legend()
print(f"MAPE = {mape(series_air_scaled, pred):.2f}%")
MAPE = 8.02%

训练过程(幕后)¶
那么,当我们在上面调用 model_air.fit()
时发生了什么?
为了训练内部神经网络,Darts 首先从提供的时间序列(在本例中为:series_air_scaled
)中创建输入/输出示例数据集。有几种方法可以做到这一点,并且 Darts 在 darts.utils.data
包中包含了一些不同的数据集实现。
默认情况下,NBEATSModel
将实例化一个 darts.utils.data.PastCovariatesSequentialDataset
,它只是简单地构建序列中存在的所有连续输入/输出子序列对(长度分别为 input_chunk_length
和 output_chunk_length
)。
例如,对于长度为 14 的序列,其中 input_chunk_length=4
且 output_chunk_length=2
,它看起来如下:
对于这样的数据集,长度为 N
的序列将产生 N - input_chunk_length - output_chunk_length + 1
个样本的“训练集”。在上面的玩具示例中,我们有 N=14
,input_chunk_length=4
和 output_chunk_length=2
,因此用于训练的样本数量为 K = 9。在这种情况下,一个训练 epoch 包括对所有样本的完整遍历(可能由多个 mini-batch 组成)。
请注意,不同的模型默认可能会使用不同的数据集。例如,darts.utils.data.HorizonBasedDataset
的灵感来自于 N-BEATS 论文,它生成“靠近”序列末尾的样本,甚至可能忽略序列的开头。
如果你需要控制如何从 TimeSeries
实例生成训练样本,你可以通过继承抽象类 darts.utils.data.TrainingDataset
来实现自己的训练数据集。Darts 数据集继承自 torch Dataset
,这意味着很容易实现懒加载版本,不会一次将所有数据加载到内存中。一旦你有了自己的数据集实例,你可以直接调用所有全局预测模型都支持的 fit_from_dataset()
方法。
在多个时间序列上训练模型¶
所有这些机制都可以与多个时间序列无缝地一起使用。以下是使用 input_chunk_length=4
和 output_chunk_length=2
的序列数据集在长度分别为 N 和 M 的两个序列上的样子:

这里有几点需要注意:
不同的序列不需要具有相同的长度,甚至不需要共享相同的时间戳。
实际上,它们甚至不需要具有相同的频率。
训练数据集中的样本总数将是每个序列中包含的所有训练样本的并集;因此,一个训练 epoch 现在将覆盖所有序列中的所有样本。
在航空交通和牛奶时间序列上进行训练¶
让我们看另一个例子,我们将在我们的两个时间序列(航空乘客和牛奶产量)上拟合另一个模型实例。由于使用两个(大致)相同长度的序列使训练数据集大小(大致)翻倍,我们将使用一半的 epoch 数量
[8]:
model_air_milk = NBEATSModel(
input_chunk_length=24,
output_chunk_length=12,
n_epochs=100,
random_state=0,
**generate_torch_kwargs(),
)
然后,在两个(或更多)序列上拟合模型就像将序列列表(而不是单个序列)作为参数传递给 fit()
函数一样简单
[9]:
model_air_milk.fit([train_air, train_milk])
[9]:
NBEATSModel(generic_architecture=True, num_stacks=30, num_blocks=1, num_layers=4, layer_widths=256, expansion_coefficient_dim=5, trend_polynomial_degree=2, dropout=0.0, activation=ReLU, input_chunk_length=24, output_chunk_length=12, n_epochs=100, random_state=0, pl_trainer_kwargs={'accelerator': 'cpu', 'callbacks': [<darts.utils.callbacks.TFMProgressBar object at 0x2af748f10>]})
生成序列结束之后的预测¶
现在,重要的是,在计算预测时,我们必须指定我们想要预测未来的是哪个时间序列。
之前没有这个限制。当只在一个序列上拟合模型时,模型会在内部记住这个序列,并且如果在没有 series
参数的情况下调用 predict()
,它将返回对该(唯一)训练序列的预测。一旦模型在多个序列上拟合,这就不再奏效了——在这种情况下,predict()
的 series
参数就变成了强制性的。
因此,假设我们想预测航空交通的未来。在这种情况下,我们在 predict()
函数中指定 series=train_air
,表示我们想获取 train_air
之后的时间序列的预测。
[10]:
pred = model_air_milk.predict(n=36, series=train_air)
series_air_scaled.plot(label="actual")
pred.plot(label="forecast")
plt.legend()
print(f"MAPE = {mape(series_air_scaled, pred):.2f}%")
MAPE = 7.58%

等等……这是否意味着牛奶产量有助于预测航空交通量??¶
嗯,在这个特定的例子中,使用这个模型,情况似乎是如此(至少在 MAPE 误差方面)。仔细想想,这也不是很奇怪。航空交通量深受年度季节性和上升趋势的影响。牛奶序列也展现出这两个特征,在这种情况下,它可能有助于模型捕捉它们。
请注意,这暗示了 预训练 预测模型的可能性;一次性训练模型,之后用于预测训练集中未曾出现的序列。使用我们的玩具模型,我们确实可以预测任何其他序列的未来值,即使是训练期间从未见过的序列。举例来说,假设我们想预测某个任意正弦波序列的未来。
[11]:
any_series = sine_timeseries(length=50, freq="M")
pred = model_air_milk.predict(n=36, series=any_series)
any_series.plot(label='"any series, really"')
pred.plot(label="forecast")
plt.legend()
[11]:
<matplotlib.legend.Legend at 0x2af20d5d0>

这个预测并不好(正弦波甚至没有年度季节性),但你明白了意思。
与 fit()
函数支持的方式类似,我们也可以在调用 predict()
函数时传递一个序列列表作为参数,在这种情况下它将返回一个预测序列列表。例如,我们可以一次性获取航空交通和牛奶序列的预测结果,如下所示:
[12]:
pred_list = model_air_milk.predict(n=36, series=[train_air, train_milk])
for series, label in zip(pred_list, ["air passengers", "milk production"]):
series.plot(label=f"forecast {label}")
plt.legend()
[12]:
<matplotlib.legend.Legend at 0x2b56bb9d0>

返回的两个序列分别对应于 train_air
和 train_milk
结束之后的预测。
协变量时间序列¶
到目前为止,我们一直在使用仅利用 目标 序列的历史数据来预测其未来的模型。然而,如上所述,Darts 的全局模型也支持使用 协变量 时间序列。这些是“外部数据”的时间序列,我们不一定对预测它们感兴趣,但我们仍然希望将它们作为模型的输入,因为它们可能包含有价值的信息。
构建协变量¶
让我们看一个使用我们的航空交通和牛奶序列的简单示例,我们将尝试使用年份和月份作为协变量。
[13]:
# build year and month series:
air_year = datetime_attribute_timeseries(series_air_scaled, attribute="year")
air_month = datetime_attribute_timeseries(series_air_scaled, attribute="month")
milk_year = datetime_attribute_timeseries(series_milk_scaled, attribute="year")
milk_month = datetime_attribute_timeseries(series_milk_scaled, attribute="month")
# stack year and month to obtain series of 2 dimensions (year and month):
air_covariates = air_year.stack(air_month)
milk_covariates = milk_year.stack(milk_month)
# split in train/validation sets:
air_train_covariates, air_val_covariates = air_covariates[:-36], air_covariates[-36:]
milk_train_covariates, milk_val_covariates = (
milk_covariates[:-36],
milk_covariates[-36:],
)
# scale them between 0 and 1:
scaler_covariates = Scaler()
air_train_covariates, milk_train_covariates = scaler_covariates.fit_transform([
air_train_covariates,
milk_train_covariates,
])
air_val_covariates, milk_val_covariates = scaler_covariates.transform([
air_val_covariates,
milk_val_covariates,
])
# concatenate for the full scaled series; we can feed this to model.fit()/predict() as Darts will extract the required
# covariates for you
air_covariates = concatenate([air_train_covariates, air_val_covariates])
milk_covariates = concatenate([milk_train_covariates, milk_val_covariates])
# plot the covariates:
plt.figure()
air_covariates.plot()
plt.title("Air traffic covariates (year and month)")
plt.figure()
milk_covariates.plot()
plt.title("Milk production covariates (year and month)")
[13]:
Text(0.5, 1.0, 'Milk production covariates (year and month)')


很好,对于每个目标序列(航空交通和牛奶),我们都构建了一个具有相同时间轴并包含年份和月份的协变量序列。
请注意,这里的协变量序列是多元时间序列:它们包含两个维度——一个用于年份,一个用于月份。
使用协变量进行训练¶
让我们再次回顾我们的例子,这次加入协变量。我们将在这里构建一个 BlockRNNModel
。我们激活检查点以便随时间跟踪模型在验证集上的性能。
[14]:
model_name = "BlockRNN_test"
model_pastcov = BlockRNNModel(
model="LSTM",
input_chunk_length=24,
output_chunk_length=12,
n_epochs=100,
random_state=0,
model_name=model_name,
save_checkpoints=True, # store model states: latest and best performing of validation set
force_reset=True,
**generate_torch_kwargs(),
)
现在,要使用协变量训练模型,只需将协变量(以与目标序列匹配的列表形式)作为 past_covariates
参数提供给 fit()
函数即可。该参数命名为 past_covariates
是为了提醒我们模型可以使用这些协变量的过去值来进行预测。我们可以将完整的航空交通和牛奶协变量(包括训练集和测试集)输入到 model.fit()/predict() 中,因为 Darts 会自动为你提取所需的协变量。
此外,我们传入一个验证序列以避免模型过拟合。
[15]:
model_pastcov.fit(
series=[train_air, train_milk],
past_covariates=[air_covariates, milk_covariates],
val_series=[val_air, val_milk],
val_past_covariates=[air_covariates, milk_covariates],
)
[15]:
BlockRNNModel(model=LSTM, hidden_dim=25, n_rnn_layers=1, hidden_fc_sizes=None, dropout=0.0, input_chunk_length=24, output_chunk_length=12, n_epochs=100, random_state=0, model_name=BlockRNN_test, save_checkpoints=True, force_reset=True, pl_trainer_kwargs={'accelerator': 'cpu', 'callbacks': [<darts.utils.callbacks.TFMProgressBar object at 0x2b56bb640>]})
现在我们加载在验证集上具有最佳性能的模型状态,以避免过拟合。
[16]:
model_pastcov = BlockRNNModel.load_from_checkpoint(model_name=model_name, best=True)
由于在这个例子中协变量很容易在未来已知,我们也可以定义一个 RNNModel
并使用它们作为 future_covariate
进行训练。
[17]:
model_name = "RNN_test"
model_futcov = RNNModel(
model="LSTM",
hidden_dim=20,
batch_size=8,
n_epochs=100,
random_state=0,
training_length=35,
input_chunk_length=24,
model_name=model_name,
save_checkpoints=True, # store model states: latest and best performing of validation set
force_reset=True,
**generate_torch_kwargs(),
)
model_futcov.fit(
series=[train_air, train_milk],
future_covariates=[air_covariates, milk_covariates],
val_series=[val_air, val_milk],
val_future_covariates=[air_covariates, milk_covariates],
)
[17]:
RNNModel(model=LSTM, hidden_dim=20, n_rnn_layers=1, dropout=0.0, training_length=35, batch_size=8, n_epochs=100, random_state=0, input_chunk_length=24, model_name=RNN_test, save_checkpoints=True, force_reset=True, pl_trainer_kwargs={'accelerator': 'cpu', 'callbacks': [<darts.utils.callbacks.TFMProgressBar object at 0x2b69cb9d0>]})
现在我们加载在验证集上具有最佳性能的模型状态,以避免过拟合。
[18]:
model_futurecov = RNNModel.load_from_checkpoint(model_name=model_name, best=True)
使用协变量进行预测¶
同样地,现在获取预测只需要为 BlockRNNModel
的 predict()
函数指定 past_covariates
参数即可。
[19]:
pred_cov = model_pastcov.predict(n=36, series=train_air, past_covariates=air_covariates)
series_air_scaled.plot(label="actual")
pred_cov.plot(label="forecast")
plt.legend()
[19]:
<matplotlib.legend.Legend at 0x2b6abd4b0>

请注意,这里我们调用的 predict()
的预测范围 n
大于我们训练模型时使用的 output_chunk_length
。我们能够这样做是因为尽管 BlockRNNModel
使用历史协变量,但在本例中这些协变量的未来值也是已知的,因此 Darts 能够以自回归方式计算未来 n
个时间步的预测。
对于 RNNModel
,我们可以采用类似的方法,只需将 future_covariates
提供给 predict()
函数即可。
[20]:
pred_cov = model_futcov.predict(
n=36, series=train_air, future_covariates=air_covariates
)
series_air_scaled.plot(label="actual")
pred_cov.plot(label="forecast")
plt.legend()
[20]:
<matplotlib.legend.Legend at 0x2b6ba5540>

使用协变量进行回测¶
我们也可以使用协变量对模型进行回测。例如,假设我们对评估从验证序列开始处,预测范围为 12 个月的滚动精度感兴趣。
我们从验证序列的开始 (
start=val_air.start_time()
) 开始。每个预测的长度将为
forecast_horizon=12
。下一个预测将从上一个预测之后
stride=12
个点开始。我们保留每个预测中的所有预测值 (
last_points_only=False
)。然后我们继续,直到输入数据耗尽。
最后,我们将历史预测结果连接起来,得到一个单一的连续(时间轴上)时间序列。
[21]:
backtest_pastcov = model_pastcov.historical_forecasts(
series_air_scaled,
past_covariates=air_covariates,
start=val_air.start_time(),
forecast_horizon=12,
stride=12,
last_points_only=False,
retrain=False,
verbose=True,
)
backtest_pastcov = concatenate(backtest_pastcov)
print(
f"MAPE (BlockRNNModel with past covariates) = {mape(series_air_scaled, backtest_pastcov):.2f}%"
)
backtest_futcov = model_futcov.historical_forecasts(
series_air_scaled,
future_covariates=air_covariates,
start=val_air.start_time(),
forecast_horizon=12,
stride=12,
last_points_only=False,
retrain=False,
verbose=True,
)
backtest_futcov = concatenate(backtest_futcov)
print(
f"MAPE (RNNModel with future covariates) = {mape(series_air_scaled, backtest_futcov):.2f}%"
)
MAPE (BlockRNNModel with past covariates) = 12.09%
MAPE (RNNModel with future covariates) = 10.26%
使用选定的超参数(和随机种子),使用 past_covariates
的 BlockRNNModel
(MAPE=10.48%) 似乎优于使用 future_covariates
的 RNNModel
(MAPE=15.21%)。为了更好地了解用这两个模型获得的预测结果,可以将它们并排绘制出来。
[22]:
fig, axs = plt.subplots(1, 2, figsize=(14, 6))
series_air_scaled.plot(label="actual", ax=axs[0])
backtest_pastcov.plot(label="forecast BlockRNN", ax=axs[0])
axs[0].legend()
series_air_scaled.plot(label="actual", ax=axs[1])
backtest_futcov.plot(label="forecast RNN", ax=axs[1], color="darkviolet")
axs[1].legend()
plt.show()

关于历史协变量、未来协变量及其他条件化的一些补充说明¶
目前 Darts 支持本身就是时间序列的协变量。这些协变量用作模型输入,但它们本身从不作为预测对象。协变量不一定需要与目标序列对齐(例如,它们不需要在同一时间开始)。Darts 将使用 TimeSeries
时间轴的实际时间值,以便在训练和推断时正确地联合切分目标和协变量。当然,协变量仍然需要有足够的跨度,否则 Darts 会报错。
如上所述,TCNModel
、NBEATSModel
、BlockRNNModel
、TransformerModel
使用历史协变量(如果你尝试使用 future_covariates
,它们会报错)。如果这些历史协变量碰巧未来也是已知的,那么这些模型也能以自回归方式生成 n > output_chunk_length
的预测结果(如上面 BlockRNNModel
所示)。
相反,RNNModel
使用未来协变量(如果你尝试指定 past_covariates
,它会报错)。这意味着使用此模型进行预测需要协变量在预测时间之后至少已知未来 n
个时间步的值。
历史和未来协变量(以及不同模型使用它们的方式)是一个重要但不简单的议题,我们计划在未来的 notebook(或文章)中进一步解释。
训练和预测多元时间序列¶
现在,我们不再只预测一个变量,而是希望一次预测多个变量。与多序列训练(使用两个不同的单变量数据集训练一个模型)不同,训练集由一个包含多个变量(称为 components
)观测值的单个序列组成。这些 components
通常具有相同的性质(测量相同的指标),但这并非必须如此。
即使本例未涵盖,模型也可以通过向 fit
方法提供此类多元 TimeSeries
序列来训练(当然前提是模型支持多元 TimeSeries
)。
为了说明这个例子,将使用 ElectricityDataset
(Darts 中也提供)。该数据集包含 370 个客户的电能消耗测量值(单位为 kW),采样率为 15 分钟。
[23]:
multi_serie_elec = ElectricityDataset().load()
由于这个多元序列特别大(370 个分量,140,256 个值),我们在以 1 小时频率重新采样序列之前只保留 3 个分量。最后,保留最后 168 个值(一周)以缩短训练时长。
[24]:
# retaining only three components in different ranges
retained_components = ["MT_002", "MT_008", "MT_009"]
multi_serie_elec = multi_serie_elec[retained_components]
# resampling the multivariate time serie
multi_serie_elec = multi_serie_elec.resample(freq="1H")
# keep the values for the last 5 days
multi_serie_elec = multi_serie_elec[-168:]
[25]:
multi_serie_elec.plot()
plt.show()

数据准备和推断流程¶
我们将数据集分割为训练集(6 天)和验证集(1 天)并对值进行归一化。在 Darts 中,所有模型都通过调用 fit
进行训练,并使用 predict
进行推断,因此可以定义一个简短的函数 fit_and_pred
来封装这两个步骤。
[26]:
# split in train/validation sets
training_set, validation_set = multi_serie_elec[:-24], multi_serie_elec[-24:]
# define a scaler, by default, normalize each component between 0 and 1
scaler_dataset = Scaler()
# scaler is fit on training set only to avoid leakage
training_scaled = scaler_dataset.fit_transform(training_set)
validation_scaled = scaler_dataset.transform(validation_set)
def fit_and_pred(model, training, validation):
model.fit(training)
forecast = model.predict(len(validation))
return forecast
现在,我们将使用上面定义的函数,定义并训练一个 VARIMA
模型和一个 RNNModel
。由于数据集是整数索引的,VARIMA
模型的 trend
参数必须设置为 None
,这并不是什么问题,因为在上面的图中没有明显的趋势。
[27]:
model_VARIMA = VARIMA(p=12, d=0, q=0, trend="n")
model_GRU = RNNModel(
input_chunk_length=24,
model="LSTM",
hidden_dim=25,
n_rnn_layers=3,
training_length=36,
n_epochs=200,
**generate_torch_kwargs(),
)
# training and prediction with the VARIMA model
forecast_VARIMA = fit_and_pred(model_VARIMA, training_scaled, validation_scaled)
print(f"MAE (VARIMA) = {mae(validation_scaled, forecast_VARIMA):.2f}")
# training and prediction with the RNN model
forecast_RNN = fit_and_pred(model_GRU, training_scaled, validation_scaled)
print(f"MAE (RNN) = {mae(validation_scaled, forecast_RNN):.2f}")
MAE (VARIMA) = 0.11
MAE (RNN) = 0.10
由于我们使用了 Scaler
来归一化多元序列的每个分量,我们必须记住将其缩放回原始值,以便能够正确地可视化预测值。
[28]:
forecast_VARIMA = scaler_dataset.inverse_transform(forecast_VARIMA)
forecast_RNN = scaler_dataset.inverse_transform(forecast_RNN)
labels = [f"forecast {component}" for component in retained_components]
fig, axs = plt.subplots(1, 2, figsize=(14, 6))
validation_set.plot(ax=axs[0])
forecast_VARIMA.plot(label=labels, ax=axs[0])
axs[0].set_ylim(0, 500)
axs[0].set_title("VARIMA model forecast")
axs[0].legend(loc="upper left")
validation_set.plot(ax=axs[1])
forecast_RNN.plot(label=labels, ax=axs[1])
axs[1].set_ylim(0, 500)
axs[1].set_title("RNN model forecast")
axs[1].legend(loc="upper left")
plt.show()

由于参数选择偏向于速度而不是精度,预测的质量不是很高。从原始数据集中使用更多的分量或增加训练集的大小应该会提高两个模型的精度。另一个可能的改进是通过在 VARIMA
模型中将 p
(时间滞后)设置为 24 而不是 12 来考虑数据集的日季节性,然后重新训练它。
关于使用多元序列进行训练的评论¶
之前在单变量 TimeSeries
上展示的所有功能,特别是使用协变量(历史和未来)或序列集,当然也兼容多元 TimeSeries
(只需确保所使用的模型实际支持它们)。
此外,支持多元序列的模型可能会使用不同的方法。例如,TFTModel
使用专门的模块来选择相关特征,而 NBEATSModel
将序列的分量展平为单变量序列,并依靠其全连接层来捕捉特征之间的交互。