Что такое повышение градиента?

Gradient Boosting — это метод машинного обучения, который включает в себя обучение нескольких субоптимальных моделей, основанных друг на друге, в отличие от традиционного подхода к обучению одной модели до совершенства. Это может привести к менее эффективным окончательным моделям, но ускорит процесс обучения, поскольку ни одна модель не должна быть идеальной. Как и в случае с людьми, найти «команду» несовершенных моделей намного проще (а в некоторых случаях и лучше), чем найти одну модель, которая идеально справляется с задачей.

Теория

Определения

Мы определяем комбинированный результат команды m моделей как Fₘ(X) для заданного входного вектора X. Индивидуальный выход n-й модели в команде (где n меньше или равно m) определяется как fₙ(X). Мы называем отдельные модели слабыми моделями, поскольку по отдельности они редко бывают оптимальными.

Окончательный результат

Gradient Boosting работает путем суммирования выходных данных каждого члена команды, то есть конечный результат Fₘ(X) равен сумме каждой отдельной модели fᵢ(X):

Мы можем визуализировать, как рассчитывается эта окончательная модель, используя этот простой (и плохо нарисованный) пример, в данном случае с 4 слабыми моделями. (Поскольку есть 4 слабые модели, конечный результат помечен как F₄(X)). Для примера предположим, что предполагаемый результат для этой модели с заданным значением X равен 1 — другими словами, Y = 1:

По сути, каждая модель после первой модели думает: «Что пошло не так в предыдущих моделях?» и оттуда попытки объяснить предыдущие неточности. Ни одна модель не особенно хороша в поставленной перед ней задаче, поэтому их называют слабыми моделями.

Слабые модели

Первая слабая модель обучается на исходных входных и выходных данных из набора данных — это первая итерация процесса, поэтому эта модель называется f₀(X).

Каждая последующая итерация основывается на результатах предыдущей итерации. Входные данные остаются прежними, но каждая модель после первой обучается учитывать выходные данные предыдущих итераций и исправлять их. Говоря более строго, каждая модель fₙ(X), где n ≥ 1, принимает исходный входной вектор X и обучается выводить результат:

Каждая модель обучена учитывать неточности предыдущих моделей, а это означает, что по мере добавления новых моделей в команду (т. е. по мере увеличения m) общая производительность Fₘ(X) увеличивается.

Гиперпараметры

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

Однако для постоянной скорости обучения это можно упростить до:

Эти уравнения дают нам два гиперпараметра для настройки: η — скорость обучения и m — количество слабых моделей. В зависимости от типа используемых вами моделей у вас могут быть дополнительные гиперпараметры, специфичные для настройки слабых моделей.

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

Практический пример

Набор данных

Чтобы показать Gradient Boosting в действии, давайте посмотрим на простой набор данных, который мы можем легко сгенерировать. Набор данных будет следовать функции косинуса в диапазоне [-5, 5], и мы искусственно добавим к нему некоторый шум, чтобы увидеть, как Gradient Boost работает с шумом:

import numpy as np

def gen_datapoints(length):
    xs = np.random.choice(np.linspace(-5, 5, length*10), length)
    ys = np.cos(xs) + np.random.normal(0, 0.2, length)
    return xs, ys

x_data, y_data = gen_datapoints(300)

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

Обучение моделей

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

Мы также определим функцию, которая оценивает все модели на заданном наборе входных данных.

fit_gb(x_train: ndarray, y_train: ndarray, model_count: int, learning_rate: Callable, ...)
evaluate_all_models(model_list: list, xs: ndarray, learning_rate: Callable) 

Обратите внимание, что, хотя сюда включены базовые параметры для Gradient Boosting, у вас, вероятно, будут более конкретные гиперпараметры для определения в зависимости от того, как определяются слабые модели.

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

fit_gb(x_train: ndarray, y_train: ndarray, model_count: int, learning_rate: Callable, ...):
  model_list = []
  for i in range(model_count):
    current_y_train = y_train
    # Adjust current_y_train based on previous models here
    
    model = fit(x_train, y_train, ...)
    model_list.append(model)

Метод fit(…) полностью зависит от модели, которую вы используете, поэтому вы должны определить свою модель и то, как она соответствует данным. Вам также потребуется определить функцию, которую я назвал forward(…), которая будет запускать отдельную модель на наборе данных. Функция пересылки должна следовать этому общему формату, хотя, как всегда, это будет зависеть от вашего приложения:

forward(model, x)

Настройка данных

Теперь задача состоит в том, чтобы скорректировать целевой результат каждой модели таким образом, чтобы каждая модель пыталась исправить ошибки предыдущей. Через мгновение мы определим функцию с именем Assessment_all_models(…), которая будет получать полный вывод текущего списка моделей. А пока давайте предположим, что мы уже реализовали его и используем для корректировки наших данных.

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

Мы можем реализовать это в коде следующим образом:

fit_gb(x_train: ndarray, y_train: ndarray, model_count: int, learning_rate: Callable, ...):
  model_list = []
  for i in range(model_count):
    current_y_train = y_train
    # Adjust current_y_train based on previous models here
    if i > 0:
      print(f"Evaluating {i} models...")
      previous_predictions = evaluate_all_models(model_list, x_train, learning_rate)
      current_y_train = current_y_train - previous_predictions
    
    model = fit(x_train, current_y_train, ...)
    model_list.append(model)

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

Оценка каждой модели

Чтобы закончить код, мы должны, наконец, определить Assessment_all_models(…) со следующим заголовком:

evaluate_all_models(model_list: list, input_data: ndarray, output_size: int, learning_rate: Callable)

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

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

def evaluate_all_models(model_list, input_data, output_size, learning_rate):
  total_preds = np.array([[0] * output_size] * len(input_data)) #one set of outputs per prediction
  for i in range(len(model_list)):
    current_pred = forward(model_list[i], input_data)
    total_preds += learning_rate(i) * current_pred
  total_preds = activate(total_preds)
  return total_preds

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

Мы также, наконец, используем функцию learning_rate(), которая принимает на вход индекс модели в списке, который показывает, насколько далеко в процессе Gradient Boosting находится эта модель.

Окончательный код

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

def evaluate_all_models(model_list, input_data, output_size, learning_rate):
  total_preds = np.array([[0] * output_size] * len(input_data)) #one set of outputs per prediction
  for i in range(len(model_list)):
    current_pred = forward(model_list[i], input_data)
    total_preds += learning_rate(i) * current_pred
  total_preds = activate(total_preds)
  return total_preds

def fit_gb(x_train: ndarray, y_train: ndarray, model_count: int, learning_rate: Callable, ...):
  model_list = []
  for i in range(model_count):
    current_y_train = y_train
    # Adjust current_y_train based on previous models here
    if i > 0:
      print(f"Evaluating {i} models...")
      #note that the output shape is set to 1 for this example
      #for more complex datasets, it should be set to the size of the output vector
      previous_predictions = evaluate_all_models(model_list, x_train, 1, learning_rate)
      current_y_train = current_y_train - previous_predictions
    print(f"Fitting model {i}...")
    model = fit(x_train, current_y_train, ...)
    model_list.append(model)
  return model_list

Окончательные результаты

После всего этого кодирования мы можем, наконец, запустить его и посмотреть, как он работает. При относительно небольшом обучении (~ 2–3 минуты) мы получаем довольно приличный результат, учитывая, что обучение было очень небольшим:

Имейте в виду, что сама модель довольно маленькая и не очень долго обучалась. Тем не менее, Gradient Boosting позволяет этой модели примерно соответствовать целевой функции, несмотря на то, что данные повсюду зашумлены.