Как неправильно настроенная базовая оценка подрывает числовую стабильность прогнозов модели в разных версиях XGBoost

Специалисты по машинному обучению уже давно слышали поговорку «Мусор на входе, мусор на выходе». Это подчеркивает важность того, чтобы любые усилия по моделированию основывались на наборе данных, который хорошо изучен и подходит для решения поставленной задачи. Не менее важным аспектом моделирования является необходимость понимания нюансов гиперпараметров алгоритмов моделирования.

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

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

В этом посте мы углубимся в XGBoost, популярный пакет моделирования с открытым исходным кодом, который реализует алгоритм машины повышения градиента (GBM), и сосредоточимся на конкретном гиперпараметре base_score. В частности, мы представим сценарий, в котором модель, обученная с неправильно указанной базовой оценкой, в конечном итоге дает заметно разные прогнозы для двух соседних версий пакета XGBoost.

К концу этого поста вы узнаете:

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

Цикл прогнозирования XGBoost и роль базовой оценки

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

Как только модель будет обучена и готова генерировать прогнозы, алгоритм сначала суммирует прогнозируемые остатки по отдельным деревьям, а затем добавляет базовую оценку в конце, чтобы получить окончательное прогнозируемое значение. В псевдокоде этот процесс может быть выражен в простом цикле for, как показано ниже для одного наблюдения.

predicted_value = 0
for tree in trees:
    # Predicted residual is found by first locating the leaf to
    # which the observation of interest belongs, and then looking 
    # up the numerical value for said leaf
    predicted_residual = …
    predicted_value += predicted_residual
# Add base score at the end to get final prediction
predicted_value += base_score

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

# Alternatively, set running sum equal to base score at the start
predicted_value = base_score
for tree in trees:
    predicted_residual = …
    predicted_value += predicted_residual

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

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

Влияние базовой оценки на числовую стабильность прогнозов модели

На высоком уровне вот что мы будем делать:

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

Для постановки эксперимента нам нужно создать две виртуальные среды, установить XGBoost 1.2.0 в одну и 1.3.0 в другую. Один из способов добиться этого — использовать диспетчер среды/пакетов conda. Подробности см. в официальной документации conda.

Данные обучения полностью синтетические. В частности, данные состоят из:

  • 1000 наблюдений.
  • Одна целевая переменная плюс пять признаков, все шесть из которых являются случайно сгенерированными числами от нуля до единицы.
import numpy as np
# Set random seed for reproducibility
np.random.seed(2021)
data = np.random.rand(1000, 6)
# Save to disk for later use
np.savetxt('data.csv', data)

Мы обучим модель в XGBoost 1.2.0 и сохраним подобранную модель вместе с прогнозами модели на данных обучения для последующего использования.

# Execute this snippet in xgboost 1.2.0 environment
import xgboost as xgb
y, x = data[:, 0], data[:, 1:]
# Set hyperparameters for model training
hyperparams = {
    'base_score': 1000,
    'n_estimators': 500,
    'max_depth': 5,
    'learning_rate': .05
}
# Train model and save
model = xgb.XGBRegressor(**hyperparams)
model.fit(x, y)
model.save_model('model_v120.bin')
# Generate predictions on training data and save
predicted_v120 = model.predict(x)
np.savetxt('predicted_v120.csv', predicted_v120)

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

Затем мы загрузим подобранный объект модели в XGBoost 1.3.0 и снова сгенерируем прогнозы на обучающих данных.

# Execute this snippet in xgboost 1.3.0 environment
data = np.loadtxt('data.csv')
x = data[:, 1:]
# Load previously trained model
model = xgb.XGBRegressor()
model.load_model('model_v120.bin')
predicted_v130 = model.predict(x)
predicted_v120 = np.loadtxt('predicted_v120.csv')

Теперь у нас есть два набора предсказаний модели, оба из которых генерируются одним и тем же базовым объектом модели на одних и тех же обучающих данных. Единственная разница заключается в используемой версии XGBoost — 1.2.0 или 1.3.0.

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

Max absolute percent difference: 0.82%
1.2.0: 0.05175781
1.3.0: 0.05133431

Как видно выше, дельта между двумя наборами прогнозов может достигать 0,82%. Хотя мы не обязательно ожидаем, что прогнозы будут точно идентичными (возможно, из-за ошибок округления с плавающей запятой), наблюдаемое расхождение намного больше, чем ожидалось; дельта такой величины — это не то, что мы можем просто списать на мел как незначительную без дальнейшего исследования. В частности, если использование модели связано с процедурой управления, как это часто бывает на крупных предприятиях, это несоответствие наверняка вызовет недоумение у отдела управления рисками.

Давайте повторно обучим модель с другой базовой оценкой и посмотрим, повлияет ли это каким-либо образом на дельту. Действительно, изменение базовой оценки на 0,5 (значение по умолчанию в XGBoost) приводит к тому, что прогнозы модели почти идентичны для двух версий XGBoost.

Max percent difference: 0.00%
1.2.0: 0.00867423
1.3.0: 0.00867402

Мы попробуем несколько других базовых оценок и оценим их влияние на максимальную разницу. Как показано в таблице ниже, существует положительная корреляция между базовой оценкой и величиной максимальной разницы. Возьмем, к примеру, экстремальное значение 25 000. Если мы обучим модель с этой базовой оценкой и сравним прогнозы модели в двух версиях XGBoost, они могут отличаться друг от друга на целых 30%.

Можно ли сделать вывод, что XGBoost дает ненадежные прогнозы модели, как только мы переходим на более новую версию пакета? Нет. Как мы вскоре увидим, наша конкретная схема моделирования такова, что влияние аномально большого базового показателя будет усилено.

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

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

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

Как неправильно указанная базовая оценка подрывает числовую стабильность

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

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

# Start running sum at base score. New behavior introduced in 1.3.0
predicted_value = base_score
for tree in trees:
    predicted_residual = …
    predicted_value += predicted_residual

Предположим, что прогнозируемый остаток от самого первого дерева равен -50,0341012. Мы ожидаем, что predicted_value будет равно 949,9658988 после первой итерации цикла.

1000.0–50.0341012 = 949.9658988

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

1000.0
 -50.0341012
    ↓
   1.0          * 10³ Convert to scientific notation
  -5.00341012   * 10¹
    ↓
   1.0          * 10³
  -0.0500341012 * 10³ Ensure the same exponent
    ↓
   0.9499658988 * 10³
    ↓
 949.9658988

То, что в конечном итоге происходит в реальности, немного отличается.

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

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

  0.9499658988 * 10³
   ↓
  0.9499659xxx * 10³ Last 3 digits discarded
   ↓
949.9659             Less precision as a result

Напротив, если мы начнем вычисление суммы с нуля — поведение в XGBoost 1.2.0 — каждое последующее сложение/вычитание в цикле for будет выполняться более точным образом, используя все доступные биты без преждевременного отбрасывания замыкающих цифр.

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

Как исправить числовую нестабильность, вызванную неправильно указанным базовым счетом

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

Наиболее надежное исправление — повторное обучение модели с правильно выбранной базовой оценкой — той, которая соизмерима со шкалой целевой переменной.

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

def v120_predict_replica(model, x):
    """Replica of the prediction logic in xgboost version 1.2.0.
    Specifically, base score will be added _at the end_ of the 
    prediction loop, rather than at the start. This is achieved via  
    setting `base_margin` to be zero, which forces the base score to 
    be ignored.
https://xgboost.readthedocs.io/en/latest/prediction.html#base-
margin
    """
    float32 = np.float32 # xgboost uses 32-bit floats
    base_margin = np.zeros(len(x), dtype=float32)
    dmatrix = xgb.DMatrix(x, base_margin=base_margin)
    base_score = model.get_xgb_params()[‘base_score’]
    booster = model.get_booster()
    predicted_value = booster.predict(dmatrix) + float32(base_score)
    return predicted_value

Тем не менее, это не следует рассматривать как постоянное исправление — это просто временная мера, позволяющая выиграть нам больше времени, поскольку мы планируем более надежное решение по переобучению модели.

Выводы

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

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

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

Благодарности

Этот пост является кульминацией предыдущей работы и вклада моих коллег из Capital One (в алфавитном порядке):

Ссылки и ресурсы

ЗАЯВЛЕНИЕ О РАСКРЫТИИ ИНФОРМАЦИИ: © 2022 Capital One. Мнения принадлежат конкретному автору. Если в этом посте не указано иное, Capital One не связана и не поддерживается ни одной из упомянутых компаний. Все используемые или демонстрируемые товарные знаки и другая интеллектуальная собственность являются собственностью соответствующих владельцев.