交叉验证

实施交叉验证以评估历史数据上的模型

时间序列交叉验证是一种评估模型在历史数据上表现的方法。它通过定义一个滑动窗口,跨越过去的观测值,并预测随后的时间段来进行工作。它与标准交叉验证的不同之处在于保持数据的时间顺序,而不是随机划分数据。

这种方法通过考虑多个时间段,可以更好地估计我们模型的预测能力。当只使用一个窗口时,它类似于标准的训练-测试划分,其中测试数据是最后一组观测值,而训练集由早期数据组成。

下图展示了时间序列交叉验证的工作原理。

在本教程中,我们将解释如何在 NeuralForecast 中执行交叉验证。

大纲: 1. 安装 NeuralForecast

  1. 加载并绘制数据

  2. 使用交叉验证训练多个模型

  3. 评估模型并为每个系列选择最佳模型

  4. 绘制交叉验证结果

本指南假定您对 neuralforecast 有基本的了解。要访问最小示例,请访问 快速入门

以下代码行用于使交叉验证的输出将系列的ID作为一列而不是索引。

import os
os.environ['NIXTLA_ID_AS_COL'] = '1' 

1. 安装 NeuralForecast

%%capture
!pip install neuralforecast

2. 加载并绘制数据

我们将使用pandas从M4预测比赛加载每小时数据集,该数据集已存储在parquet文件中以提高效率。

import pandas as pd
Y_df = pd.read_parquet('https://datasets-nixtla.s3.amazonaws.com/m4-hourly.parquet')
Y_df.head()
unique_id ds y
0 H1 1 605.0
1 H1 2 586.0
2 H1 3 586.0
3 H1 4 559.0
4 H1 5 511.0

neuralforecast 的输入应该是一个长格式的数据框,包含三列:unique_iddsy

  • unique_id(字符串、整数或类别):每个时间序列的唯一标识符。

  • ds(整数或时间戳):整数索引时间或格式为 YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS 的时间戳。

  • y(数值型):要预测的目标变量。

该数据集包含414个独特的时间序列。为了减少总执行时间,我们将只使用前10个。

uids = Y_df['unique_id'].unique()[:10] # 选择10个ID以加快示例运行速度
Y_df = Y_df.query('unique_id in @uids').reset_index(drop=True)

为了绘制系列数据,我们将使用 utilsforecast.plotting 中的 plot_series 方法。utilsforecastneuralforecast 的依赖项,因此它应该已经安装。

from utilsforecast.plotting import plot_series
plot_series(Y_df)

3. 使用交叉验证训练多个模型

我们将使用 cross-validation 方法训练来自 neuralforecast 的不同模型,以决定哪个模型在历史数据上表现最佳。为此,我们需要导入 NeuralForecast 类以及我们想要比较的模型。

from neuralforecast import NeuralForecast 
from neuralforecast.auto import MLP, NBEATS, NHITS

在本教程中,我们将使用neuralforecastMPLNBEATSNHITS模型。

首先,我们需要创建一个模型列表,然后实例化NeuralForecast类。对于每个模型,我们将定义以下超参数:

  • h:预测范围。这里,我们将使用与M4比赛相同的范围,即提前48步。

  • input_size:模型用于进行预测的历史观察值(滞后)的数量。在这种情况下,它将是预测范围的两倍。

  • loss:要优化的损失函数。在这里,我们将使用来自neuralforecast.losses.pytorch的多量分位损失(MQLoss)。

多分位损失(MQLoss)是每个目标分位的分位损失之和。单个分位的分位损失衡量模型对实际分布特定分位的预测效果,基于分位值对高估和低估进行不对称处罚。更多细节请参见 这里

虽然对于每个模型可以定义其他超参数,但为了本教程的目的,我们将使用默认值。有关每个模型超参数的更多信息,请查阅相应的文档。

from neuralforecast.losses.pytorch import MQLoss

horizon = 48 

models = [MLP(h=horizon, input_size=2*horizon, loss=MQLoss()), 
          NBEATS(h=horizon, input_size=2*horizon, loss=MQLoss()), 
          NHITS(h=horizon, input_size=2*horizon, loss=MQLoss()),]

nf = NeuralForecast(models=models, freq=1)
Seed set to 1
Seed set to 1
Seed set to 1

cross_validation 方法接受以下参数:

  • df: 按照第2节中描述格式的数据框。

  • n_windows (int): 要评估的窗口数量。默认值为1,此处我们将使用3。

  • step_size (int): 产生预测的连续窗口之间的步长。在这个例子中,我们将step_size设置为horizon以产生不重叠的预测。以下图示展示了基于step_size参数和模型的预测视野h产生预测的方式。在这个图示中,step_size=2h=4

  • refit (bool或int): 是否为每个交叉验证窗口重新训练模型。如果为False,则在开始时训练模型,然后用于预测每个窗口。如果为正整数,则每refit窗口重新训练模型。默认值为False,但这里我们将refit设置为1,以便在每个窗口后使用截止时间之前的时间戳数据重新训练模型。
%%capture
cv_df = nf.cross_validation(Y_df, n_windows=3, step_size=horizon, refit=1)

值得一提的是,neuralforecastcross_validation方法的默认版本与其他库不同,后者通常在每个窗口开始时重新训练模型。默认情况下,它只训练模型一次,然后在所有窗口上使用这些模型生成预测,从而减少总执行时间。对于需要重新训练模型的场景,可以使用refit参数来指定模型应在多少个窗口之后重新训练。

cv_df.head()
unique_id ds cutoff MLP-median MLP-lo-90 MLP-lo-80 MLP-hi-80 MLP-hi-90 NBEATS-median NBEATS-lo-90 NBEATS-lo-80 NBEATS-hi-80 NBEATS-hi-90 NHITS-median NHITS-lo-90 NHITS-lo-80 NHITS-hi-80 NHITS-hi-90 y
0 H1 605 604 627.988586 534.398438 557.919495 706.230042 729.458984 622.185120 582.681030 598.783203 648.851318 653.490173 632.352661 579.588623 581.992249 641.553772 663.227234 622.0
1 H1 606 604 567.843018 473.527313 503.057220 652.319458 702.797974 544.922485 489.482178 518.790222 572.878967 585.352661 553.002197 495.692871 504.136749 595.143494 616.993591 558.0
2 H1 607 604 521.304260 412.619751 446.281494 598.887146 630.079163 496.092041 439.469055 450.099365 524.822876 523.129578 496.740112 449.902313 453.967438 539.902588 560.253723 513.0
3 H1 608 604 488.616760 393.635559 403.897003 557.288452 600.689697 464.539612 409.801483 425.563171 483.017029 497.362915 455.020721 416.871094 420.002441 500.547150 507.826721 476.0
4 H1 609 604 464.543854 339.080414 367.943787 536.813965 558.877808 444.119019 384.093872 409.416290 470.489075 474.619446 432.959076 389.460663 397.191345 465.580536 491.252747 449.0

cross-validation 方法的输出是一个数据框,其中包括以下列:

  • unique_id: 每个时间序列的唯一标识符。

  • ds: 时间戳或时间索引。

  • cutoff: 在该交叉验证窗口中使用的最后时间戳或时间索引。

  • "model": 包含模型点预测(中位数)和预测区间的列。默认情况下,在使用 MQLoss 时包含 80% 和 90% 的预测区间。

  • y: 实际值。

4. 评估模型并为每个序列选择最佳模型

为了评估模型的点预测,我们将使用均方根误差(RMSE),其定义为实际值与预测值之间平方差的均值的平方根。

为了方便,我们将使用 utilsforecast 中的 evaluatermse 函数。

from utilsforecast.evaluation import evaluate
from utilsforecast.losses import rmse 

evaluate 函数接受以下参数:

  • df:要评估的包含预测的数据框。

  • metrics(列表):要计算的指标。

  • models(列表):要评估的模型名称。默认值为 None,这将使用在删除 id_coltime_coltarget_col 后的所有列。

  • id_col(字符串):标识序列的唯一 ID 的列。默认值为 unique_id

  • time_col(字符串):带有时间戳或时间索引的列。默认值为 ds

  • target_col(字符串):带有目标变量的列。默认值为 y

请注意,如果我们使用 models 的默认值,则需要从交叉验证数据框中排除 cutoff 列。

evaluation_df = evaluate(cv_df.loc[:, cv_df.columns != 'cutoff'], metrics=[rmse])

对于每个唯一的 ID,我们将选择具有最低 RMSE 的模型。

evaluation_df['best_model'] = evaluation_df.drop(columns=['metric', 'unique_id']).idxmin(axis=1)
evaluation_df
unique_id metric MLP-median NBEATS-median NHITS-median best_model
0 H1 rmse 44.453354 48.828117 47.718341 MLP-median
1 H10 rmse 24.375221 18.887296 17.938162 NHITS-median
2 H100 rmse 165.888534 211.220029 200.504549 MLP-median
3 H101 rmse 375.096472 201.191102 149.309690 NHITS-median
4 H102 rmse 475.430266 321.459725 331.499041 NBEATS-median
5 H103 rmse 8552.597224 9091.500057 8169.006459 NHITS-median
6 H104 rmse 187.017183 194.206979 148.196734 NHITS-median
7 H105 rmse 349.070196 304.997747 312.518832 NBEATS-median
8 H106 rmse 196.937493 361.771205 357.442132 MLP-median
9 H107 rmse 211.585091 236.404925 241.229147 MLP-median

我们可以汇总结果以查看每个模型获胜的次数。

summary_df = evaluation_df.groupby(['metric', 'best_model']).size().sort_values().to_frame()
summary_df = summary_df.reset_index()
summary_df.columns = ['metric', 'model', 'num. of unique_ids']
summary_df
metric model num. of unique_ids
0 rmse NBEATS-median 2
1 rmse MLP-median 4
2 rmse NHITS-median 4

有了这些信息,我们现在知道在历史数据中哪个模型对每个序列的表现最好。

5. 绘制交叉验证结果

为了可视化交叉验证结果,我们将再次使用 plot_series 方法。我们需要将交叉验证输出中的 y 列重命名,以避免与原始数据框重复。我们还将排除 cutoff 列,并使用 max_insample_length 参数仅绘制最后 300 个观测值,以便更好地进行可视化。

cv_df.rename(columns = {'y' : 'actual'}, inplace = True) # 重命名实际值 
plot_series(Y_df, cv_df.loc[:, cv_df.columns != 'cutoff'], max_insample_length=300)

为了进一步阐明交叉验证的概念,我们将绘制在每个截止点生成的预测,系列的 unique_id='H1'。由于我们设定了 n_windows=3,因此有三个截止点。在这个例子中,我们使用了 refit=1,因此每个模型在每个窗口中都使用截止时间之前及包含截止时间的数据重新训练。此外,由于 step_size 等于预测范围,生成的预测是非重叠的。

cutoff1, cutoff2, cutoff3 = cv_df['cutoff'].unique()
plot_series(Y_df, cv_df[cv_df['cutoff'] == cutoff1].loc[:, cv_df.columns != 'cutoff'], ids=['H1']) # 使用ids参数选择特定系列

plot_series(Y_df, cv_df[cv_df['cutoff'] == cutoff2].loc[:, cv_df.columns != 'cutoff'], ids=['H1'])

plot_series(Y_df, cv_df[cv_df['cutoff'] == cutoff3].loc[:, cv_df.columns != 'cutoff'], ids=['H1'])

Give us a ⭐ on Github