Экстремальное повышение градиента, или XGBoost, приобрело огромную популярность в последние годы для решения широкого круга задач, связанных с табличными данными. Благодаря своей способности обрабатывать пропущенные значения, выбирать функции и выполнять параллельную обработку, XGBoost стал лучшим выбором среди специалистов по обработке и анализу данных и специалистов по машинному обучению.

Этот пост в блоге будет посвящен созданию расширенных пользовательских целевых функций; мы обсудим несколько передовых методов их реализации и интеграции в процесс настройки гиперпараметров.

XGBoost и целевые функции

Этот пост не будет охватывать весь механизм XGBoost или углубляться в его математику. Тем не менее, стоит упомянуть одну концепцию, заключающуюся в том, как XGBoost использует свою определенную функцию потерь. По сути, XGBoost использует аппроксимацию функции потерь, основанную на расширении Тейлора до второй производной. Затем при минимизации аппроксимированных потерь соответствующие уравнения, необходимые для построения деревьев, могут быть выполнены с использованием первой и второй производных, также известных как градиент и гессиан соответственно. Не волнуйтесь, в этом посте мы покажем, как использовать градиент и гессиан. (перейдите здесь, чтобы узнать больше о механизме XGBoost).

Библиотека XGBoost поставляется с несколькими встроенными целевыми функциями, каждая из которых предназначена для разных вариантов использования. Например, цель reg:squarederror обычно используется для задач регрессии. Цель binary:logistic, с другой стороны, используется для задач бинарной классификации (дополнительные целевые функции можно найти в документации XGBoost).

Зачем использовать встроенные целевые функции? Ну, во-первых, их можно легко определить в конфигурации гиперпараметров модели XGBoost. При этом, если мы решим использовать встроенную целевую функцию, мы должны убедиться, что выбранная цель подходит для нашей задачи прогнозирования. В противном случае обучение не даст очень полезной модели.

Самый большой недостаток использования встроенных целей заключается в том, что они не всегда способны решать сложные задачи. Иногда бизнес-требования могут потребовать от нас сосредоточиться на определенных KPI, что делает встроенную цель неподходящей.

Одним из способов преодоления таких проблем является создание пользовательской целевой функции, которая соответствует требуемым бизнес-потребностям. Возможность настраивать целевую функцию XGBoost делает его очень полезным инструментом для решения уникальных и сложных задач, используя при этом мощь и простоту использования всей библиотеки XGBoost.

Относится ли настройка гиперпараметров к нашей целевой функции?

Ответ положительный. XGBoost имеет большое количество гиперпараметров, которые можно настроить для оптимизации производительности модели, таких как скорость обучения, максимальная глубина деревьев и сила регуляризации. Правильный выбор может улучшить предсказательную силу модели и уменьшить переоснащение.

Но как насчет пользовательских целевых функций? Ну, в некоторых случаях целевые функции могут содержать свои собственные гиперпараметры, которые необходимо настроить. Одним из примеров является цель «reg:tweedie», которая имеет параметр «tweedie_variance_power», управляющий степенью дисперсии распределения. Хотя «tweedie_variance_power» является встроенным параметром, который можно легко определить с помощью словаря параметров модели, при построении пользовательской цели с новыми пользовательскими параметрами все становится сложнее.

Возможность настраивать эти пользовательские целевые функции позволяет специалистам по данным адаптировать свои модели к своим конкретным потребностям и оптимизировать производительность для своих конкретных случаев использования. В следующих разделах мы объясним, как настроить новые гиперпараметры, которые мы добавили в нашу пользовательскую целевую функцию, тем самым сделав наши модели XGBoost лучше, чем когда-либо!

Реализация пользовательской целевой функции

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

Чтобы реализовать это как целевую функцию, нам нужно будет вычислить первую и вторую производные квадрата члена ошибки относительно y_pred:

import numpy as np
import xgboost as xgb
from typing import Tuple

def my_squared_error(y_pred:np.ndarray,
                     dtrain:xgb.DMatrix) -> Tuple[np.ndarray, np.ndarray]:
    y_true = dtrain.get_label()
    grad = 2*(y_pred - y_true)
    hess = np.repeat(2, y_true.shape[0])
    return grad, hess

grad представляет производную первого порядка квадрата ошибки по отношению к y_pred и hess — производная второго порядка.

Несколько вещей, которые следует иметь в виду в отношении целевой функции:

  1. Порядок аргументов функции должен оставаться таким, как показано в коде, начиная с вектора y_pred, за которым следует dtrain. который представляет собой DMatrix данных поезда.
  2. Функция должна возвращать два аргумента: производную первого порядка члена потерь (градиент) и производную второго порядка (гессе).
  3. Обратите внимание на знак градиентов и гессиана в соответствии с положением вектора y_pred в члене ошибки.
  4. Переменные grad и hess – это векторы, длина которых равна количеству выборок в данных поезда.

Чтобы обучить модель XGBoost с нашей реализацией целевой функции, мы можем передать нашу функцию в метод обучения через аргумент «obj» следующим образом:

params = {
    "max_depth":5,
    "eta":0.15,
    "eval_metric":"rmse"
}

model = xgb.train(params=params,
                  dtrain=dtrain,
                  num_boost_round=100,
                  obj=my_squared_error)

Давайте посмотрим на следующий синтезированный пример, где целевой переменной является ценность жизни пользователей (LTV) в долларах США ($). Подгонка нашей модели к этим данным привела к следующему графику:

Приведенный выше график показывает, что наша собственная реализация целевой функции квадрата ошибки работала, как и ожидалось. При попытке минимизировать MSE прогнозы в значительной степени симметрично разбросаны по 45-градусной линии без какого-либо видимого успеха в прогнозировании пользователей с высоким LTV. Это было довольно просто, верно?

Вывод пользовательской цели на новый уровень

Теперь давайте усложним задачу. Предполагая, что подгонки простого регрессора к нашим данным недостаточно, мы хотим построить целевую функцию, соответствующую определенным бизнес-потребностям. Например, давайте определим «ценного пользователя» как пользователя, который потратил больше определенной суммы в долларах. Наша цель — максимально избежать недооценки потенциального дохода для этих пользователей.

Мы можем написать уравнение нашего нового асимметричного пользовательского проигрыша следующим образом:

Прежде чем перейти к реализации целевой функции, важно отметить два важных аспекта:

  1. В целевую функцию добавлены две новые переменные в дополнение к вектору y_pred и объекту dtrain. Первая переменная — tau, — это пороговое значение, определяющее «ценного пользователя». Эта переменная настраивается и определяется на основе бизнес-логики. Вторая переменная — дельта — уровень «наказания» за недооценку «ценных пользователей». Дельта — это новый гиперпараметр, и его значение следует определять на основе процедуры настройки гиперпараметра.
  2. Новые переменные не могут быть переданы в функцию при текущей настройке, поскольку пользовательская целевая функция должна оставаться с представленной ранее структурой. Аргумент «obj» метода train XGBoost после вызова будет ожидать объект Callable с точной структурой, как указано выше.

Чтобы преодолеть эту проблему, мы будем использовать функции Python Closures и обернем реализацию градиента и гессиана другой функцией, которая принимает в качестве аргументов дополнительные переменные.

def my_assymetric_error_wrapper(tau, delta):
    def my_assymetric_error(y_pred, dtrain):
        y_true = dtrain.get_label()
        error = (y_pred - y_true)
        grad = np.where(((y_true>tau)&(error<0)), delta*2*error, 2*error)
        hess = np.where(((y_true>tau)&(error<0)), delta*2, 2)
        return grad, hess
    return my_assymetric_error

Теперь мы можем легко использовать нашу новую асимметричную пользовательскую цель в методе обучения XGBoost, в то время как значения tau и delta можно установить вне функции. Этот метод позволяет либо настроить параметр (tau), либо настроить его (дельта). Для примера установим tau=8$ и delta=10:

params = {
    "max_depth":5,
    "eta":0.15,
    "eval_metric":"rmse"
}

model = xgb.train(params=params,
                      dtrain=dtrain,
                      num_boost_round=150,
                      obj = my_assymetric_error_wrapper(tau=8, delta=10))

Теперь обучение будет основываться на нашей новой пользовательской логике целей с использованием определенных значений tau и delta. Подгонка модели к нашим данным привела к следующему:

Мы видим, что новая асимметричная пользовательская цель хорошо справилась с задачей (насколько это возможно) избежать недооценки ценных пользователей (доход › 8 долл. США), в то же время ведя себя почти так же, как «squarederror» цель для остальных из них.

Настройте новый гиперпараметр

Следуя нашему предыдущему примеру, мы можем понять, что tau — это настраиваемый параметр, который можно определить на основе некоторой бизнес-логики. С другой стороны, определить значение дельты намного сложнее, так как оно больше похоже на гиперпараметр модели. Поэтому параметр delta необходимо настраивать вместе с другими гиперпараметрами модели.

Но стоит ли нам беспокоиться о том, что delta — это новый гиперпараметр и он не является частью встроенных параметров библиотеки XGBoost? Вовсе нет, так как мы использовали замыкания Python для реализации нашей новой пользовательской целевой функции, теперь мы можем передать любое значение нашему новому параметру. Таким образом, мы можем использовать эту возможность в процедуре настройки гиперпараметров и оптимизировать производительность нашей модели.

В этом сообщении в блоге мы решили использовать Optuna, который представляет собой среду оптимизации гиперпараметров с открытым исходным кодом. Он предоставляет несколько алгоритмов выборки и обрезки и поддерживает различные платформы машинного и глубокого обучения. Одним из ключевых преимуществ Optuna является то, что она имеет широкие возможности настройки, позволяя пользователям определять свои собственные области поиска, целевые функции и стратегии оптимизации. Optuna также поддерживает параллельные и распределенные вычисления, что делает ее масштабируемой и способной решать крупномасштабные задачи оптимизации гиперпараметров.

Пользовательская целевая функция + Оптуна

Первым шагом в работе с Optuna является реализация цели оптимизации, которая является основной логикой процесса оптимизации. Он состоит из трех основных компонентов:

  1. Установка пространства выборки гиперпараметров. Здесь мы можем определить, какие гиперпараметры мы хотим настроить, и пространство их значений. В нашем случае, помимо настройки встроенных гиперпараметров XGBoost, мы установим пространство выборки для нашего нового гиперпараметра delta. Обратите внимание, что мы не определили параметр delta в параметре XGBoost, поскольку это не встроенный гиперпараметр, и он передается в модель через пользовательскую цель. функция.
  2. Обучение модели XGBoost. Здесь мы начнем обучение, используя нашу новую асимметричную пользовательскую цель, где параметр дельта изменяется в зависимости от процесса оптимизации.
  3. Определение показателя оценки. Эта оценка будет использоваться для определения наилучшего варианта процесса настройки.

Мы снова будем использовать замыкания Python, чтобы обернуть цель другой функцией, которая получит в качестве аргументов набор для обучения и проверки, а также настраиваемую переменную нашей новой пользовательской цели XGBoost, tau .

def objective_wrapper(dtrain, dval, tau):
    def objective(trial):
        # 1. setting parameters sampling space
        param = {
            "subsample" : trial.suggest_float("subsample", 0.0, 1.0),
            "max_depth" : trial.suggest_int("max_depth", 1, 9),
            "eta" : trial.suggest_float("eta", 0.005, 0.35),
            "gamma" : trial.suggest_float("gamma", 1, 10.0),
        }
        
        # our new hyper-parameter
        delta = trial.suggest_int("delta", 1, 50, step=5)
        
        # 2. model training using the custom objective
        model = xgb.train(param,
                          dtrain,
                          num_boost_round = 100,
                          obj = my_assymetric_error_wrapper(tau=tau,
                                                            delta=delta))
        
        # 3. calculate score on validation set
        preds = model.predict(dval)
        labels = dval.get_label()
        mse = np.mean((preds-labels)**2)
        return mse
    return objective

Теперь, когда целевая функция оптимизации построена, мы можем инициализировать исследование Optuna. При инициализации исследования есть возможность выбрать конкретный пробоотборник и секаторы, ну и конечно задать направление оптимизации. Для простоты мы будем использовать пробоотборник TPE по умолчанию, и, поскольку мы внедрили MSE в качестве нашей оценочной оценки, мы установим направление оптимизации на «минимизировать». Мы установили tau=8$ и оптимизировали для 10 проб:

import optuna

study = optuna.create_study(sampler=optuna.samplers.TPESampler(seed=42),
                            direction='minimize')
study.optimize(objective_wrapper(dtrain, dval, tau=8), n_trials=10)

Результаты процесса настройки будут выглядеть примерно так:

[I 2023-02-15 09:17:36,163] A new study created in memory with name: demo
[I 2023-02-15 09:17:37,556] Trial 0 finished with value: 0.8846970796585083 and parameters: {'subsample': 0.3745401188473625, 'max_depth': 9, 'eta': 0.25753790992493475, 'gamma': 6.387926357773329, 'delta': 6}. Best is trial 0 with value: 0.8846970796585083.
[I 2023-02-15 09:17:37,960] Trial 1 finished with value: 2.7566282749176025 and parameters: {'subsample': 0.15599452033620265, 'max_depth': 1, 'eta': 0.3038307702923526, 'gamma': 6.41003510568888, 'delta': 36}. Best is trial 0 with value: 0.8846970796585083.
[I 2023-02-15 09:17:38,749] Trial 2 finished with value: 5.54156494140625 and parameters: {'subsample': 0.020584494295802447, 'max_depth': 9, 'eta': 0.2921927110761455, 'gamma': 2.9110519961044856, 'delta': 6}. Best is trial 0 with value: 0.8846970796585083.
[I 2023-02-15 09:17:39,232] Trial 3 finished with value: 1.1226681470870972 and parameters: {'subsample': 0.18340450985343382, 'max_depth': 3, 'eta': 0.18604096891312205, 'gamma': 4.887505167779041, 'delta': 11}. Best is trial 0 with value: 0.8846970796585083.
[I 2023-02-15 09:17:39,716] Trial 4 finished with value: 1.5384970903396606 and parameters: {'subsample': 0.6118528947223795, 'max_depth': 2, 'eta': 0.10578990374465026, 'gamma': 4.297256589643226, 'delta': 21}. Best is trial 0 with value: 0.8846970796585083.
[I 2023-02-15 09:17:40,218] Trial 5 finished with value: 0.8311933279037476 and parameters: {'subsample': 0.7851759613930136, 'max_depth': 2, 'eta': 0.182410881252696, 'gamma': 6.331731119758382, 'delta': 1}. Best is trial 5 with value: 0.8311933279037476.
[I 2023-02-15 09:17:40,709] Trial 6 finished with value: 2.2779664993286133 and parameters: {'subsample': 0.6075448519014384, 'max_depth': 2, 'eta': 0.02744279957992143, 'gamma': 9.539969835279999, 'delta': 46}. Best is trial 5 with value: 0.8311933279037476.
[I 2023-02-15 09:17:41,295] Trial 7 finished with value: 1.5023095607757568 and parameters: {'subsample': 0.8083973481164611, 'max_depth': 3, 'eta': 0.03869687933220243, 'gamma': 7.158097238609412, 'delta': 21}. Best is trial 5 with value: 0.8311933279037476.
[I 2023-02-15 09:17:41,870] Trial 8 finished with value: 1.3249372243881226 and parameters: {'subsample': 0.12203823484477883, 'max_depth': 5, 'eta': 0.016864039784750345, 'gamma': 9.18388361870904, 'delta': 11}. Best is trial 5 with value: 0.8311933279037476.
[I 2023-02-15 09:17:42,435] Trial 9 finished with value: 0.8856847286224365 and parameters: {'subsample': 0.662522284353982, 'max_depth': 3, 'eta': 0.18442346730634473, 'gamma': 5.920392514089517, 'delta': 6}. Best is trial 5 with value: 0.8311933279037476.

Вы можете видеть, что наряду со встроенными гиперпараметрами, такими как «max_depth» и «eta», наш новый гиперпараметр дельта был настроен, так как были оценены различные его возможные значения.

Дальнейшее изучение выходных журналов показывает, что выбранное значение дельта в лучшем испытании (испытании № 5) равно 1. Это означает, что мы не применяем никаких дополнительных «наказание» за недооценку ценных пользователей. Как это возможно? Что ж, это подводит меня к следующему моменту: при использовании пользовательской цели, которая следует определенной бизнес-логике, важно согласовать с ней нашу оценку. Здесь, поскольку мы использовали MSE в качестве оценочного показателя в процессе настройки, имеет смысл выбрать значение delta равное 1, поскольку оно преобразует нашу пользовательскую целевую функцию обратно в обычный «squarederror», предназначенный для минимизации MSE. Разве это не круто?

Таким образом, важно определить оценку оценки, которая отражает ту же бизнес-логику, что и пользовательская целевая функция. Помимо оценки процесса настройки, параметр eval_metric XGBoost также должен следовать той же бизнес-логике, поскольку он влияет на механизм early_stopping процесса обучения. Как мы можем сделать это? Он будет сохранен для будущих сообщений в блоге.

Теперь вы готовы создать свою собственную замечательную пользовательскую целевую функцию и настроить все ее новые гиперпараметры!

Рекомендации



Что делает «XGBoost таким экстремальным?
Полное руководство по внутренней работе XGBoostmedium.com»