машинное обучение
July 24, 2021

Тюнинг CatBoost - градиентного бустинга от Яндекс

Как настроить один из лучших инструментов градиентного бустинга - CatBoost. Рассмотрим на примере.

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

При этом формула этой взаимосвязи следующая (data_m - имя датафрейма):

data_m['расходы'] = 2*np.sqrt(data_m['зарплата'])+0.5*data_m['сумма_помощи']

Задача - использовать CatBoost для предсказания расходов. Посмотрим, сможет ли алгоритм справиться с поставленной задачей.

Перед использованием рассмотрим параметры модели (подробнее читай здесь):

iterations (синонимы num_boost_round, n_estimators, num_trees) - максимальное количество деревьев, используемых в модели (по умолчанию 1000). Значение может быть ниже заданного, если используются другие параметры, накладывающие ограничение на количество деревьев;

learning_rate - скорость обучения;

random_seed - инициализирующее значение для используемого генератора случайных чисел, чтобы обеспечить повторямость эксперимента;

l2_leaf_reg (reg_lambda) - коэффициент регуляризации функции потерь;

depth (max_depth) - глубина дерева (максимальное значение - 16);

min_data_in_leaf (min_child_samples) - минимальное количество узлов в листе;

max_leaves (num_leaves) - максимальное количество листьев в дереве;

subsample - часть исходной выборки, отбираемая для обучения каждого дерева;

colsample_bylevel - доля признаков, используемая для отбора на каждом сплите;

class_weights - веса классов в моделях классификации. Используется для устранения несбалансированности (например, вес positive=количество_negative/количество_positive);

auto_class_weights - устраняет дисбаланс автоматически (значения - 'Balanced', 'SqrtBalanced');

scale_pos_weight - задает вес для положительного класса;

Среди параметров scale_pos_weight, auto_class_weights, class_weights одновременно используется только один.

early_stopping_rounds - устанавливает количество итераций для останова, если на их протяжении метрика качества не улучшалась по сравнению с оптимальной;

use_best_model - если истина, в результате обучения с ранней остановкой будет возвращаться модель, полученная на итерации (количеством деревьев) с лучшей метрикой на валидационной выборке;

loss_function - функция потерь, которую модель минимизирует в процессе обучения (подмножество из списка);

eval_metric - валидационная метрика, используемая для ранней остановки (в зависимости от задачи ссылки здесь);

custom_metric - отслеживаемые метрики (список доступен здесь). Лучшие метрики в процессе обучения можно вернуть методом get_best_score;

classes_count - количество классов для многоклассовой классификации;

task_type - используемое для вычислений устройство ('CPU,'GPU');

thread_count - число используемых для обучения потоков (по умолчанию = -1, что означает - все ядра процессора);

cat_features - список наименований категориальных признаков;

verbose - объем выводимой информации (False - подавляем сообщения).

Теперь разделим данные на обучающую и тестовую выборки, обучим регрессор с параметрами по умолчанию, применим для предсказания тестовой выборки и подсчитаем метрики качества:

X_tr, X_ts, y_tr, y_ts = train_test_split(data_m[['зарплата', 'сумма_помощи']],
                                          data_m['расходы'], test_size=0.2)
reg = CatBoostRegressor(verbose = False)

reg.fit(X_tr, y_tr)
y_pr = reg.predict(X_ts)

metrics = {}
metrics['mape'] = mean_absolute_percentage_error(y_ts, y_pr)
metrics['mae'] = mean_absolute_error(y_ts, y_pr)

Сравните тестовые данные и предсказанные моделью:

Сохранение и загрузка модели

Модель можно сохранять в файл и загружать из файла:

Сохранение

reg.save_model('model')

Загрузка

reg = CatBoostRegressor()
reg.load_model("model")

Ниже представлен код для генерации использованного в статье набора данных:

import pandas as pd
import numpy as np
import string
from catboost import CatBoostRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error, mean_absolute_percentage_error

np.random.seed(0)
letters = np.array(list(string.ascii_uppercase))
digits = np.arange(0,10)
people_num = 10000
towns_num = 200 # количество городов, нацело делит people_num для удобства
# людям соответствуют по три случайные буквы алфавита (имя, фамилия)
names = np.random.choice(letters, size=(people_num,3))
# зарплата от 20 до 50 тыс. рублей, случайное равномерное распределение
salary = np.random.uniform(20,500, size=people_num)
# индекс города - по семь цифр из алфавита, их меньше чем людей,
# так как предполагаем одинаковые города для разных людей
loc_ind_p = np.random.choice(digits, size=(towns_num,7))
loc_ind = np.tile(loc_ind_p, (people_num//towns_num,1))
# склеим буквы имен и фамилий, а также
# индексы городов
names = pd.DataFrame(names, dtype='str').apply(lambda x:' '.join(x),axis=1)
loc_ind = pd.DataFrame(loc_ind, dtype='str').apply(lambda x:''.join(x),axis=1)
data = pd.DataFrame({'ФИО':names, 'индекс_города':loc_ind, 
                         'зарплата':salary})
data['зарплата'] = np.round(data['зарплата'],2)
helps = np.random.uniform(5,10, size = towns_num)
town_help = pd.DataFrame({'индекс_города':pd.DataFrame(loc_ind_p, dtype='str')
                                          .apply(lambda x:''.join(x), axis=1),
                          'сумма_помощи':helps})
town_help['сумма_помощи'] = np.round(town_help['сумма_помощи'],2)
data_m = pd.merge(data, town_help, left_on = 'индекс_города', 
                    right_on='индекс_города')
# перемешиваем данные
data_m = data_m.sample(frac=1).reset_index(drop=True)
data_m['расходы'] = 2*np.sqrt(data_m['зарплата'])+\
                        0.5*data_m['сумма_помощи']