машинное обучение
July 19, 2022

Кросс-валидация и отбор количества итераций в LightGBM

Рассмотрим вопрос на примере датасета о прогнозе выживаемости пассажиров "Титаника". Загрузим набор данных:

import pandas as pd
import numpy as np
SEED=0
np.random.seed(SEED)

df = pd.read_csv("https://s3.amazonaws.com/h2o-public-test-data/smalldata/gbm_test/titanic.csv")
df = df.astype({it:'category' for it in df.select_dtypes(include='object').columns})

df = df.sample(frac=1).reset_index(drop=True)
df.head()

Сначала вспомним, как осуществить ранний останов без кросс-валидации. Для этого следует сформировать два датасета (Dataset) и вызвать метод train с параметрами, включающими измеряемые метрики, тренировочный и валидационный датасеты (valid_sets), функцию раннего останова (добавляется в список в параметре callbacks) с указанием количества итераций, в течение которых отслеживается факт улучшения метрики (stopping_rounds). Если задано несколько валидационных метрик, то параметр first_metric_only=True в callback функции указывает, что выбор итерации будет происходит не с учетом их всех, а только первой:

import lightgbm as lgb
tr_idx, val_idx = np.split(np.arange(df.shape[0]), [round(0.8*df.shape[0])])

train_data = lgb.Dataset(df.drop(columns='survived').iloc[tr_idx], 
                         label = df['survived'].iloc[tr_idx])
valid_data = lgb.Dataset(df.drop(columns='survived').iloc[val_idx], 
                         label = df['survived'].iloc[val_idx], reference=train_data)

params = {'metric':['auc', 'binary_logloss'], 'objective': 'binary'}

clf = lgb.train(params, train_data, num_boost_round=100, valid_sets=[valid_data], 
                callbacks=[lgb.early_stopping(stopping_rounds=5, first_metric_only=True)])
clf

Лучшая итерация и значения метрик хранятся в атрибутах best_iteration, best_score объекта lightgbm.basic.Booster:

clf.best_iteration, clf.best_score

Для кросс-валидации используется функция cv, которая принимает аналогичные параметры, только вместо двух датасетов один (так как сама осуществляет разбиение) и стратегию разбиений (количество фолдов или их индексы), дополнительные параметры здесь:

from sklearn.model_selection import KFold
sp = KFold(n_splits=3, shuffle=True, random_state=SEED)

train_data = lgb.Dataset(df.drop(columns='survived'), label = df['survived'])


cv_res = lgb.cv(params, train_data, 300, folds=sp, shuffle=False, seed = None,
            callbacks=[lgb.early_stopping(stopping_rounds=5, first_metric_only=True)],
            return_cvbooster=True)

Лучшую итерацию можно получить из атрибута best_iteration CVBooster:

cv_res['cvbooster'].best_iteration

либо из результатов кросс-валидации:

print(f"Номер лучшей итерации (с 0) - {np.array(cv_res['auc-mean']).argmax()}, \
максимальное качество - {np.array(cv_res['auc-mean']).max()}")

Задав номер лучшей итерации, проведем кросс-валидацию вручную и сверим качество:

from sklearn.metrics import roc_auc_score

auc_l = []
data_all = lgb.Dataset(df.drop(columns='survived'), label = df['survived'])

for idx_tr, idx_val in sp.split(df):
    X_tr, X_val = df.drop(columns='survived').iloc[idx_tr], df.drop(columns='survived').iloc[idx_val]
    y_tr, y_val = df['survived'].iloc[idx_tr], df['survived'].iloc[idx_val]
    
    train_data = lgb.Dataset(X_tr, label = y_tr, reference=data_all)

    params = {'metric':['auc', 'binary_logloss'], 'objective': 'binary'}

    clf = lgb.train(params, train_data, num_boost_round=cv_res['cvbooster'].best_iteration)
    auc_l.append(roc_auc_score(y_val, clf.predict(X_val)))
    
print(np.mean(auc_l))

Обратите внимание на параметр reference=data_all при формировании train_data. Он нужен для того, чтобы бинаризация переменных при поиске оптимальной границы осуществлялась по всему массиву данных. Именно так происходит в функции cv. В противном случае бинаризация идет только по тренировочному датасету, и результаты совпадать не будут.

Не пропустите ничего интересного и подписывайтесь на страницы канала в других социальных сетях:

Яндекс Дзен

Telegram