源代码

LightGBMCV

 LightGBMCV (freq:Union[int,str], lags:Optional[Iterable[int]]=None, lag_t
             ransforms:Optional[Dict[int,List[Union[Callable,Tuple[Callabl
             e,Any]]]]]=None,
             date_features:Optional[Iterable[Union[str,Callable]]]=None,
             num_threads:int=1, target_transforms:Optional[List[Union[mlfo
             recast.target_transforms.BaseTargetTransform,mlforecast.targe
             t_transforms._BaseGroupedArrayTargetTransform]]]=None)

创建 LightGBM CV 对象。

类型默认值详细信息
freqUnionPandas offset 别名,例如 ‘D’、‘W-THU’,或表示时间序列频率的整数。
lags可选None用作特征的目标变量的滞后值。
lag_transforms可选None目标变量滞后值及其变换的映射。
date_features可选None根据日期计算的特征。可以是 pandas 日期属性,或将日期作为输入的函数。
num_threadsint1计算特征时使用的线程数。
target_transforms可选None在计算特征之前应用于目标变量,并在预测步骤后恢复的变换。

示例

这里展示了 M4 数据集中仅 4 个时间序列的示例。如果您想在所有序列上自行运行,可以参考此 Notebook

import random

from datasetsforecast.m4 import M4, M4Info
from fastcore.test import test_eq, test_fail
from mlforecast.target_transforms import Differences
from nbdev import show_doc

from mlforecast.lag_transforms import SeasonalRollingMean
group = 'Hourly'
await M4.async_download('data', group=group)
df, *_ = M4.load(directory='data', group=group)
df['ds'] = df['ds'].astype('int')
ids = df['unique_id'].unique()
random.seed(0)
sample_ids = random.choices(ids, k=4)
sample_df = df[df['unique_id'].isin(sample_ids)]
sample_df
unique_iddsy
86796H196111.8
86797H196211.4
86798H196311.1
86799H196410.8
86800H196510.6
325235H413100499.0
325236H413100588.0
325237H413100647.0
325238H413100741.0
325239H413100834.0
info = M4Info[group]
horizon = info.horizon
valid = sample_df.groupby('unique_id').tail(horizon)
train = sample_df.drop(valid.index)
train.shape, valid.shape
((3840, 3), (192, 3))

LightGBMCV 的作用是模仿 LightGBM 的 cv 函数,在该函数中,多个 Booster 同时在数据的不同分区上进行训练,即每次对所有分区执行一次 boosting 迭代。这允许按迭代估算误差,因此如果我们将此与早期停止结合使用,我们可以找到使用所有数据训练最终模型的最佳迭代次数,甚至可以使用这些单独模型的预测来计算集成结果。

为了准确估算模型的预测性能,我们计算整个测试周期的预测值并计算相应的指标。由于此步骤可能会减慢训练速度,因此可以使用 `eval_every` 参数来控制此行为。例如,如果 `eval_every=10`(默认值),则每 10 次 boosting 迭代,我们将计算完整窗口的预测值并报告误差。

我们还有早期停止参数

  • early_stopping_evals:在没有改进的情况下,应进行多少次完整窗口评估以停止训练?
  • early_stopping_pct:在这些 early_stopping_evals 次评估中,我们希望达到的最小百分比改进是多少,以便继续训练?

这使得 LightGBMCV 类成为快速测试模型不同配置的好工具。考虑以下示例,我们将尝试找出哪些特征可以提高模型的性能。我们首先只使用滞后项。

static_fit_config = dict(
    n_windows=2,
    h=horizon,
    params={'verbose': -1},
    compute_cv_preds=True,
)
cv = LightGBMCV(
    freq=1,
    lags=[24 * (i+1) for i in range(7)],  # one week of lags
)

源代码

LightGBMCV.fit

 LightGBMCV.fit (df:pandas.core.frame.DataFrame, n_windows:int, h:int,
                 id_col:str='unique_id', time_col:str='ds',
                 target_col:str='y', step_size:Optional[int]=None,
                 num_iterations:int=100,
                 params:Optional[Dict[str,Any]]=None,
                 static_features:Optional[List[str]]=None,
                 dropna:bool=True, keep_last_n:Optional[int]=None,
                 eval_every:int=10,
                 weights:Optional[Sequence[float]]=None,
                 metric:Union[str,Callable]='mape',
                 verbose_eval:bool=True, early_stopping_evals:int=2,
                 early_stopping_pct:float=0.01,
                 compute_cv_preds:bool=False,
                 before_predict_callback:Optional[Callable]=None,
                 after_predict_callback:Optional[Callable]=None,
                 input_size:Optional[int]=None)

同时训练 Booster 并在完整的预测窗口上评估它们的性能。

类型默认值详细信息
dfDataFrame长格式的时间序列数据。
n_windowsint要评估的窗口数。
hint预测范围。
id_colstrunique_id标识每个时间序列的列。
time_colstrds标识每个时间步长的列,其值可以是时间戳或整数。
target_colstry包含目标变量的列。
step_size可选None每个交叉验证窗口之间的步长。如果为 None,则等于 h
num_iterationsint100要运行的最大 boosting 迭代次数。
params可选None传递给 LightGBM Booster 的参数。
static_features可选None静态特征的名称,预测时将重复使用。
dropnaboolTrue丢弃由变换产生的包含缺失值的行。
keep_last_n可选None为预测步骤保留每个时间序列的最后这么多条记录。如果您的特征允许,可以节省时间和内存。
eval_everyint10在完整预测窗口上评估之前,训练的 boosting 迭代次数。
weights可选None用于乘以每个窗口指标的权重。如果为 None,则所有窗口具有相同的权重。
metricUnionmape用于评估模型性能和执行早期停止的指标。
verbose_evalboolTrue打印每次评估的指标。
early_stopping_evalsint2在没有改进的情况下,运行的最大评估次数。
early_stopping_pctfloat0.01early_stopping_evals 次评估中,指标值的最小百分比改进。
compute_cv_predsboolFalse找到最佳迭代次数后,计算每个窗口的预测值。
before_predict_callback可选None在计算预测值之前对特征调用的函数。
此函数将接收传递给模型进行预测的输入 DataFrame,并应返回具有相同结构的 DataFrame。
时间序列标识符位于索引上。
after_predict_callback可选None在更新目标变量之前对预测值调用的函数。
此函数将接收包含预测值的 pandas Series,并应返回另一个具有相同结构的 Series。
时间序列标识符位于索引上。
input_size可选None每个窗口中每个时间序列的最大训练样本数。如果为 None,将使用扩展窗口。
返回值List(boosting 轮次,指标值) 元组列表。
hist = cv.fit(train, **static_fit_config)
[LightGBM] [Info] Start training from score 51.745632
[10] mape: 0.590690
[20] mape: 0.251093
[30] mape: 0.143643
[40] mape: 0.109723
[50] mape: 0.102099
[60] mape: 0.099448
[70] mape: 0.098349
[80] mape: 0.098006
[90] mape: 0.098718
Early stopping at round 90
Using best iteration: 80

通过设置 compute_cv_preds,我们可以获得每个模型在其对应验证折叠上的预测值。

cv.cv_preds_
unique_iddsyBooster窗口
0H19686515.515.5229240
1H19686615.114.9858320
2H19686714.814.6679010
3H19686814.414.5145920
4H19686914.214.0357930
187H41395659.077.2279051
188H41395758.080.5896411
189H41395853.053.9868341
190H41395938.036.7497861
191H41396046.036.2812251

我们训练的各个模型都已保存,因此调用 predict 会返回每个训练好的模型的预测值。


源代码

LightGBMCV.predict

 LightGBMCV.predict (h:int,
                     before_predict_callback:Optional[Callable]=None,
                     after_predict_callback:Optional[Callable]=None,
                     X_df:Optional[pandas.core.frame.DataFrame]=None)

使用每个训练好的 Booster 计算预测值。

类型默认值详细信息
hint预测范围。
before_predict_callback可选None在计算预测值之前对特征调用的函数。
此函数将接收传递给模型进行预测的输入 DataFrame,并应返回具有相同结构的 DataFrame。
时间序列标识符位于索引上。
after_predict_callback可选None在更新目标变量之前对预测值调用的函数。
此函数将接收包含预测值的 pandas Series,并应返回另一个具有相同结构的 Series。
时间序列标识符位于索引上。
X_df可选None包含未来外部特征的 DataFrame。应包含 id 列和时间列。
返回值DataFrame每个时间序列和时间步长的预测值,每个窗口一列。
preds = cv.predict(horizon)
preds
unique_iddsBooster0Booster1
0H19696115.67025215.848888
1H19696215.52292415.697399
2H19696314.98583215.166213
3H19696414.98583214.723238
4H19696514.56215214.451092
187H413100470.69524265.917620
188H413100566.21658062.615788
189H413100663.89657367.848598
190H413100746.92279750.981950
191H413100845.00654142.752819

我们可以对这些预测值进行平均并进行评估。

def evaluate_on_valid(preds):
    preds = preds.copy()
    preds['final_prediction'] = preds.drop(columns=['unique_id', 'ds']).mean(1)
    merged = preds.merge(valid, on=['unique_id', 'ds'])
    merged['abs_err'] = abs(merged['final_prediction'] - merged['y']) / merged['y']
    return merged.groupby('unique_id')['abs_err'].mean().mean()
eval1 = evaluate_on_valid(preds)
eval1
0.11036194712311806

现在,由于这些时间序列是按小时计算的,也许我们可以尝试通过计算第 168 (24 * 7) 个差分来去除每日季节性,即减去一周前同一时间的数值,因此我们的目标变量将是 zt=ytyt168z_t = y_{t} - y_{t-168}。特征将从此目标变量计算,预测时将自动重新应用。

cv2 = LightGBMCV(
    freq=1,
    target_transforms=[Differences([24 * 7])],
    lags=[24 * (i+1) for i in range(7)],
)
hist2 = cv2.fit(train, **static_fit_config)
[LightGBM] [Info] Start training from score 0.519010
[10] mape: 0.089024
[20] mape: 0.090683
[30] mape: 0.092316
Early stopping at round 30
Using best iteration: 10
assert hist2[-1][1] < hist[-1][1]

不错!我们在更少的迭代次数下获得了更好的分数。让我们看看这种改进是否也能体现在验证集上。

preds2 = cv2.predict(horizon)
eval2 = evaluate_on_valid(preds2)
eval2
0.08956665504570135
assert eval2 < eval1

太好了!现在也许我们可以尝试一些滞后变换。我们将尝试季节性滚动平均值,它对“每个季节”的值进行平均,也就是说,如果我们设置 season_length=24window_size=7,那么我们将对每周同一小时的值进行平均。

cv3 = LightGBMCV(
    freq=1,
    target_transforms=[Differences([24 * 7])],
    lags=[24 * (i+1) for i in range(7)],
    lag_transforms={
        48: [SeasonalRollingMean(season_length=24, window_size=7)],
    },
)
hist3 = cv3.fit(train, **static_fit_config)
[LightGBM] [Info] Start training from score 0.273641
[10] mape: 0.086724
[20] mape: 0.088466
[30] mape: 0.090536
Early stopping at round 30
Using best iteration: 10

看来这也有帮助!

assert hist3[-1][1] < hist2[-1][1]

这是否反映在验证集上?

preds3 = cv3.predict(horizon)
eval3 = evaluate_on_valid(preds3)
eval3
0.08961279023129345

不错!mlforecast 也支持日期特征,但在本例中,我们的时间列由整数构成,因此这里没有太多可能性。如您所见,这使您可以更快地迭代并更好地估算模型的预测性能。

如果您正在进行超参数调优,能够运行几次迭代、评估性能并确定特定配置是否有前景以及是否应丢弃,这将非常有用。例如,optuna 提供了剪枝器 (pruners),您可以根据当前分数调用它来决定是否应丢弃该试验。现在我们将展示如何做到这一点。

由于 CV 需要一些设置,例如 LightGBM 数据集和内部特征,因此我们提供了 setup 方法。


源代码

LightGBMCV.setup

 LightGBMCV.setup (df:pandas.core.frame.DataFrame, n_windows:int, h:int,
                   id_col:str='unique_id', time_col:str='ds',
                   target_col:str='y', step_size:Optional[int]=None,
                   params:Optional[Dict[str,Any]]=None,
                   static_features:Optional[List[str]]=None,
                   dropna:bool=True, keep_last_n:Optional[int]=None,
                   weights:Optional[Sequence[float]]=None,
                   metric:Union[str,Callable]='mape',
                   input_size:Optional[int]=None)

初始化内部数据结构,以便迭代训练 Booster。在调用 partial_fit 之前使用此方法。

类型默认值详细信息
dfDataFrame长格式的时间序列数据。
n_windowsint要评估的窗口数。
hint预测范围。
id_colstrunique_id标识每个时间序列的列。
time_colstrds标识每个时间步长的列,其值可以是时间戳或整数。
target_colstry包含目标变量的列。
step_size可选None每个交叉验证窗口之间的步长。如果为 None,则等于 h
params可选None传递给 LightGBM Booster 的参数。
static_features可选None静态特征的名称,预测时将重复使用。
dropnaboolTrue丢弃由变换产生的包含缺失值的行。
keep_last_n可选None为预测步骤保留每个时间序列的最后这么多条记录。如果您的特征允许,可以节省时间和内存。
weights可选None用于乘以每个窗口指标的权重。如果为 None,则所有窗口具有相同的权重。
metricUnionmape用于评估模型性能和执行早期停止的指标。
input_size可选None每个窗口中每个时间序列的最大训练样本数。如果为 None,将使用扩展窗口。
返回值LightGBMCV带有用于 partial_fit 的内部数据结构的 CV 对象。
cv4 = LightGBMCV(
    freq=1,
    lags=[24 * (i+1) for i in range(7)],
)
cv4.setup(
    train,
    n_windows=2,
    h=horizon,
    params={'verbose': -1},
)
LightGBMCV(freq=1, lag_features=['lag24', 'lag48', 'lag72', 'lag96', 'lag120', 'lag144', 'lag168'], date_features=[], num_threads=1, bst_threads=8)

拥有此对象后,我们可以调用 partial_fit 只训练一些迭代并返回预测窗口的分数。


源代码

LightGBMCV.partial_fit

 LightGBMCV.partial_fit (num_iterations:int,
                         before_predict_callback:Optional[Callable]=None,
                         after_predict_callback:Optional[Callable]=None)

训练 Booster 若干迭代次数。

类型默认值详细信息
num_iterationsint要运行的 boosting 迭代次数
before_predict_callback可选None在计算预测值之前对特征调用的函数。
此函数将接收传递给模型进行预测的输入 DataFrame,并应返回具有相同结构的 DataFrame。
时间序列标识符位于索引上。
after_predict_callback可选None在更新目标变量之前对预测值调用的函数。
此函数将接收包含预测值的 pandas Series,并应返回另一个具有相同结构的 Series。
时间序列标识符位于索引上。
返回值float训练 num_iterations 次迭代后的加权指标。
score = cv4.partial_fit(10)
score
[LightGBM] [Info] Start training from score 51.745632
0.5906900462828166

这与我们第一个示例中的第一次评估结果相同。

assert hist[0][1] == score

现在我们可以使用此分数来决定此配置是否有前景。如果需要,我们可以再训练一些迭代。

score2 = cv4.partial_fit(20)

这现在等于我们第一个示例中的第三个指标,因为这次我们训练了 20 次迭代。

assert hist[2][1] == score2

使用自定义指标

内置指标是 MAPE 和 RMSE,它们按时间序列计算,然后对所有时间序列进行平均。如果您想做一些不同的事情或完全使用不同的指标,您可以定义自己的指标,如下所示

def weighted_mape(
    y_true: pd.Series,
    y_pred: pd.Series,
    ids: pd.Series,
    dates: pd.Series,
):
    """Weighs the MAPE by the magnitude of the series values"""
    abs_pct_err = abs(y_true - y_pred) / abs(y_true)
    mape_by_serie = abs_pct_err.groupby(ids).mean()
    totals_per_serie = y_pred.groupby(ids).sum()
    series_weights = totals_per_serie / totals_per_serie.sum()
    return (mape_by_serie * series_weights).sum()
_ = LightGBMCV(
    freq=1,
    lags=[24 * (i+1) for i in range(7)],
).fit(
    train,
    n_windows=2,
    h=horizon,
    params={'verbose': -1},
    metric=weighted_mape,
)
[LightGBM] [Info] Start training from score 51.745632
[10] weighted_mape: 0.480353
[20] weighted_mape: 0.218670
[30] weighted_mape: 0.161706
[40] weighted_mape: 0.149992
[50] weighted_mape: 0.149024
[60] weighted_mape: 0.148496
Early stopping at round 60
Using best iteration: 60