Перейти к содержимому
Ступень 22.4 Практический ML на табличных данных

Градиентный бустинг изнутри

Бустинг как градиентный спуск в пространстве функций: остатки и p − y как антиградиенты, шаг Ньютона в листьях, регуляризованная цель XGBoost, гиперпараметры LightGBM на числах, отличия трёх библиотек, важность признаков и SHAP.

УРОКstage-2/2-4-tabular-practice/03-gradient-boosting.mdx2 чУрок 3 из 4
После урока вы сможете
  • Вывести, на что учится очередное дерево для MSE и для log-loss, и посчитать значение листа шагом Ньютона
  • Прочитать регуляризованную цель XGBoost и посчитать вес листа и выигрыш от разбиения с λ и γ
  • Настроить learning_rate, число деревьев (ранней остановкой), num_leaves, min_data_in_leaf и сабсэмплинг в LightGBM и объяснить, что делает каждый
  • Отличать важность по числу разбиений, по gain, перестановочную важность и SHAP и знать их ловушки

В уроке 2.3.1 вы собрали бустинг из «пней» на остатках и узнали, что XGBoost, LightGBM и CatBoost — стандарт для таблиц. Чтобы вызвать fit, этого хватает. Но у LightGBM больше сотни параметров, и без понимания механики их подбирают наугад. Сегодня откроем капот: что делает дерево, когда мы предсказываем вероятность; откуда в XGBoost вторые производные и что значат reg_lambda и min_split_gain; как learning_rate, num_leaves и сабсэмплинг меняют качество — на числах; как не обмануться важностью признаков. Заодно встретим старого знакомого: p − y — градиент cross-entropy по логитам, как в каждой LLM (урок 1.3.2).

Градиентный спуск в пространстве функций

Заголовок раздела «Градиентный спуск в пространстве функций»

Напомним рецепт из урока 2.3.1: F0F_0 — константа, затем каждое новое дерево hmh_m учится на остатках yi−Fm−1(xi)y_i - F_{m-1}(x_i), и Fm=Fm−1+νhmF_m = F_{m-1} + \nu h_m, где ν\nu — темп обучения (shrinkage).

Теперь посмотрим на это глазами урока 1.4.1. Пусть в train nn примеров. Соберём предсказания модели на них в вектор F=(F(x1),…,F(xn))\mathbf{F} = (F(x_1), \dots, F(x_n)) — это nn чисел, которые мы можем двигать. Функция потерь — обычная функция этого вектора: L(F)=∑iL(yi,Fi)\mathcal{L}(\mathbf{F}) = \sum_i L(y_i, F_i). Её градиент по F\mathbf{F} — вектор из nn чисел:

gi=∂L(yi,Fi)∂Fi.g_i = \frac{\partial L(y_i, F_i)}{\partial F_i}.

Читаем: «насколько вырастет потеря на примере ii, если чуть-чуть увеличить предсказание для него». Шаг градиентного спуска был бы Fi←Fi−νgiF_i \leftarrow F_i - \nu g_i. Но такой шаг определён только в nn точках train, а нам нужно предсказывать для любых новых xx. Поэтому мы обучаем дерево приближать антиградиент −gi-g_i по признакам xix_i и делаем шаг деревом: оно обобщает «куда двигаться» на всё пространство признаков. Это и есть идея Фридмана (2001): градиентный бустингградиентный бустингEN gradient boosting (GBDT)Реализации — XGBoost, LightGBM, CatBoost. — градиентный спуск, где каждый шаг — новая функция.

Функция потерь L(y,F)L(y, F)Антиградиент −g-g — на что учится дерево
MSE: 12(y−F)2\frac12 (y - F)^2остаток y−Fy - F
MAE: ∣y−F∣\lvert y - F \rvertзнак остатка: +1+1 или −1-1
log-loss (бинарная cross-entropy), FF — логитy−py - p, где p=σ(F)p = \sigma(F)

Для MSE остатки — это просто антиградиент; вот почему бустинг из урока 2.3.1 был градиентным. Для MAE дерево учится на знаках: каждый пример тянет с силой 1, и выброс не перетягивает модель (устойчивость MAE, урок 2.2.3).

Для бинарной классификации бустинг предсказывает логит FF, а вероятность получается сигмоидой p=σ(F)p = \sigma(F), как в логистической регрессии (урок 2.2.2). Функция потерь — бинарная cross-entropy, и её производная по логиту — та самая p−yp - y из уроков 1.3.2 и 2.2.2. Значит, дерево учится на y−py - p: «насколько вероятность не дотягивает до ответа».

На числах. Пример класса 1, текущий логит F=0F = 0: p=0.5p = 0.5, дерево получит цель 1−0.5=0.51 - 0.5 = 0.5. Пример класса 0 с F=2F = 2: p=0.881p = 0.881, цель −0.881-0.881 — сильный толчок вниз, модель уверенно ошибается. Пример класса 1 с F=−3F = -3: p=0.047p = 0.047, цель 0.9530.953. Для KK классов на каждом раунде строят KK деревьев, по одному на класс, и дерево класса kk учится на yk−pky_k - p_k — градиенте softmax-cross-entropy.

Дерево, обученное на антиградиент, задаёт структуру — какие примеры попадают в один лист. А значение листа можно выбрать точнее: то число ww, которое сильнее всего уменьшает потерю на примерах этого листа. Для MSE это среднее остатков (как у обычного регрессионного дерева). Для log-loss точного ответа формулой нет, и берут один шаг метода Ньютонаметод НьютонаEN Newton's methodШаг оптимизации с учётом кривизны: приблизить функцию параболой g·w + ½·h·w² и прыгнуть в её минимум w = −g/h. В бустинге так считают значения листьев..

Идея Ньютона: градиентный спуск знает только наклон gg и не знает, насколько далеко идти. Если знать ещё кривизну — вторую производную hh — функцию можно приблизить параболой gw+12hw2g w + \frac12 h w^2 и сразу прыгнуть в её минимум w=−g/hw = -g / h. Крутая парабола (большое hh) — шаг короче, пологая — длиннее. Для log-loss hi=pi(1−pi)h_i = p_i (1 - p_i), и значение листа

w=∑i∈лист(yi−pi)∑i∈листpi(1−pi).w = \frac{\sum_{i \in \text{лист}} (y_i - p_i)}{\sum_{i \in \text{лист}} p_i (1 - p_i)}.

Читаем: «сумма антиградиентов, делённая на сумму кривизн». На числах. В лист попали 4 примера с метками (1,1,1,0)(1, 1, 1, 0), у всех пока p=0.5p = 0.5 (F=0F = 0). Числитель 0.5+0.5+0.5−0.5=1.00.5 + 0.5 + 0.5 - 0.5 = 1.0, знаменатель 4⋅0.25=1.04 \cdot 0.25 = 1.0, значение листа — 1.0. Идеальный ответ для этого листа — логит доли единиц ln⁡3/41/4=ln⁡3≈1.10\ln \frac{3/4}{1/4} = \ln 3 \approx 1.10: Ньютон за один шаг почти попал, а среднее остатков дало бы всего 1.0/4=0.251.0 / 4 = 0.25. Так устроен GradientBoostingClassifier в scikit-learn, а XGBoost и LightGBM обобщают эту идею.

Тяньци Чэнь пошёл дальше и сделал вторые производные частью выбора разбиений. Цель на шаге mm — потеря плюс штраф за сложность нового дерева ff с TT листьями и весами листьев wjw_j:

∑iL(yi,Fm−1(xi)+f(xi))+γT+12λ∑j=1Twj2.\sum_i L\big(y_i, F_{m-1}(x_i) + f(x_i)\big) + \gamma T + \tfrac12 \lambda \sum_{j=1}^{T} w_j^2.

Первое слагаемое раскладывают в ряд Тейлора до второго порядка: L(yi,F+f)≈L(yi,F)+gif+12hif2L(y_i, F + f) \approx L(y_i, F) + g_i f + \frac12 h_i f^2 (та же парабола, что у Ньютона). Пусть GjG_j и HjH_j — суммы gig_i и hih_i по примерам листа jj. Тогда для каждого листа задача — минимизировать параболу Gjw+12(Hj+λ)w2G_j w + \frac12 (H_j + \lambda) w^2, и её минимум

wj∗=−GjHj+λ.w_j^* = -\frac{G_j}{H_j + \lambda}.

Это шаг Ньютона, у которого знаменатель увеличен на λ\lambda: L2-штраф из урока 2.1.2 прижимает значения листов к нулю. Подставив w∗w^* обратно, получают формулу выигрыша от разбиения узла на левую и правую части:

Gain=12[GL2HL+λ+GR2HR+λ−(GL+GR)2HL+HR+λ]−γ.\mathrm{Gain} = \frac12 \left[ \frac{G_L^2}{H_L + \lambda} + \frac{G_R^2}{H_R + \lambda} - \frac{(G_L + G_R)^2}{H_L + H_R + \lambda} \right] - \gamma.

Читаем: «насколько качество двух детей лучше качества родителя, минус плата γ\gamma за лишний лист». Если выигрыш отрицательный, узел не делят.

На числах. Те же 4 примера с p=0.5p = 0.5: для класса 1 g=p−y=−0.5g = p - y = -0.5, для класса 0 g=0.5g = 0.5, у всех h=0.25h = 0.25. В узле G=−1.0G = -1.0, H=1.0H = 1.0. При λ=1\lambda = 1 вес листа w∗=1.0/(1+1)=0.5w^* = 1.0 / (1 + 1) = 0.5 — вдвое меньше, чем без штрафа. Разобьём узел на {1,1}\{1, 1\} и {1,0}\{1, 0\}: GL=−1.0G_L = -1.0, HL=0.5H_L = 0.5, GR=0G_R = 0, HR=0.5H_R = 0.5.

Gain=12[1.01.5+0−1.02.0]−γ≈0.083−γ.\mathrm{Gain} = \tfrac12 \left[ \tfrac{1.0}{1.5} + 0 - \tfrac{1.0}{2.0} \right] - \gamma \approx 0.083 - \gamma.

При γ=0.1\gamma = 0.1 выигрыш отрицательный, и разбиение не делается: на четырёх примерах оно не стоит лишнего листа.

Так читаются параметры XGBoost: reg_lambda — это λ\lambda, gamma (в LightGBM min_split_gain) — это γ\gamma, min_child_weight — минимальная сумма HH в листе. Для MSE hi=1h_i = 1, и это просто минимальное число примеров. Посмотрите после этого раздела видео StatQuest «XGBoost Part 1: Regression» (26 минут, ссылка в источниках): тот же вывод выигрыша и роль λ\lambda и γ\gamma на четырёх точках, с картинками.

Возьмём синтетику Фридмана — классический тест для бустинга: y=10sin⁡(πx0x1)+20(x2−0.5)2+10x3+5x4y = 10 \sin(\pi x_0 x_1) + 20 (x_2 - 0.5)^2 + 10 x_3 + 5 x_4 плюс шум с σ=1\sigma = 1. Признаков 10, из них x5…x9x_5 \dots x_9 — чистый шум. 2000 строк на обучение, 1000 на валидацию. Стандартное отклонение yy около 5, а ниже RMSE 1.0 не опуститься: это уровень шума. Все модели — LightGBM с ранней остановкой (терпение 100 деревьев) и, если в таблице не сказано иное, с num_leaves = 8 и learning_rate = 0.05, как в коде из практики.

learning_rateЛучшее число деревьевRMSE на валидацииВремя, с
0.31921.3310.10
0.12801.2230.12
0.038411.2000.29
0.0121781.2130.88

Кривые RMSE на валидации в зависимости от числа деревьев в логарифмической шкале для learning_rate 0.3, 0.1 и 0.03; чем меньше шаг, тем позже кривая опускается и тем ниже её минимум; после минимума ошибка медленно растёт

Большой шаг быстро спускается и рано начинает переобучаться: после 192-го дерева ошибка на валидации растёт. Маленький шаг идёт дольше, но приходит ниже. Дальше 0.03 выигрыша почти нет (0.01 даже чуть хуже из-за шума валидации), а время растёт пропорционально числу деревьев. Практическое правило: learning_rate 0.03–0.1, число деревьев — ранней остановкой, никакого перебора n_estimators.

num_leaves248163164128
RMSE на валидации1.7861.1811.2001.2391.2891.3621.368

Пни (num_leaves = 2) не могут выразить взаимодействие x0x1x_0 x_1: каждое дерево смотрит на один признак, и сумма пней — сумма функций от отдельных признаков. Уже 4 листа ловят взаимодействие. Дальше деревья переобучаются: на 2000 строк по умолчанию 31 лист — это много. num_leaves (или max_depth) — главный рычаг сложности, рядом с ним min_data_in_leaf (минимум примеров в листе, по умолчанию 20) и lambda_l2.

Сабсэмплинг — идея случайного леса внутри бустинга (Фридман назвал это стохастическим градиентным бустингом): каждое дерево видит случайную долю строк (bagging_fraction с bagging_freq = 1) и признаков (feature_fraction). Деревья становятся разнообразнее, переобучение — слабее:

Строк / признаков на дерево100% / 100%80% / 100%80% / 80%50% / 50%
RMSE на валидации (num_leaves = 8)1.2001.1941.1761.187

Выигрыш скромный, но бесплатный. Порядок настройки, который работает почти всегда: learning_rate = 0.05 и ранняя остановка → num_leaves и min_data_in_leaf → сабсэмплинг и lambda_l2 → в конце уменьшить learning_rate и снова найти число деревьев. Как перебирать комбинации честно — тема следующего урока.

Гистограммы. Перебирать все пороги дорого: для каждого признака нужно отсортировать nn значений. LightGBM (а также XGBoost в режиме tree_method="hist", с версии 2.0 он выбран по умолчанию) один раз разбивает каждый признак на корзины — по умолчанию до 255 (в XGBoost — до 256) — и дальше перебирает только границы корзин, суммируя gg и hh по корзинам. Это гистограмма из урока 0.3.3, только вместо числа точек в корзине — суммы градиентов. Сколько корзин нужно:

max_bin71563255
RMSE на валидации1.6411.3101.1981.200

С 63 корзин потери точности нет. Отличие вашего бустинга от LightGBM с пнями в задании ниже — тоже от гистограмм.

Рост по листьям. XGBoost по умолчанию растит дерево «по уровням»: делит все узлы текущей глубины. LightGBM растит по листьямрост дерева по листьямEN leaf-wise tree growthСтратегия LightGBM: каждый раз делить один лист с наибольшим выигрышем. Альтернатива — рост по уровням (XGBoost по умолчанию).: каждый раз делит один лист с наибольшим выигрышем, где бы он ни был.

flowchart LR
subgraph L1["По уровням (XGBoost): глубина 2 — 4 листа"]
  A1["корень"] --> B1["узел"]
  A1 --> C1["узел"]
  B1 --> D1["лист"]
  B1 --> E1["лист"]
  C1 --> F1["лист"]
  C1 --> G1["лист"]
end
subgraph L2["По листьям (LightGBM): 4 листа, делим самый выгодный"]
  A2["корень"] --> B2["узел"]
  A2 --> C2["лист"]
  B2 --> D2["узел"]
  B2 --> E2["лист"]
  D2 --> F2["лист"]
  D2 --> G2["лист"]
end
При том же числе листьев рост по листьям уходит глубже там, где это выгоднее

При одинаковом числе листьев рост по листьям обычно точнее, но легче переобучается на маленьких данных: поэтому у LightGBM главный параметр — num_leaves, а не глубина. Ещё два ускорения из статьи LightGBM: GOSS (в выборку для дерева берут все примеры с большими градиентами и случайную долю остальных) и EFB (склеивание взаимоисключающих разреженных признаков, например one-hot столбцов, в один).

XGBoostLightGBMCatBoost
АвторыТяньци Чэнь (DMLC), 2014–2016Microsoft Research Asia, 2017Яндекс, 2017
Деревьяпо уровням, режим histпо листьямсимметричные: один вопрос на весь уровень
Особенностьрегуляризованная цель второго порядкагистограммы, GOSS, EFB — скоростьordered boosting, категории «из коробки»
Пропуски NaNдля каждого разбиения учится, в какую ветку их слатьтак жепо умолчанию считает пропуск меньше любого значения
Категориальные признакиесть поддержкаесть поддержкасильнейшая: ordered target statistics

Пропуски — приятная особенность всех трёх. XGBoost и LightGBM при обучении пробуют отправить строки с NaN и налево, и направо и запоминают лучшее направление; CatBoost по умолчанию ставит их левее всех значений. Заполнять пропуски для бустинга не обязательно. CatBoost, напомним из урока 2.3.1, борется с утечкой целевой переменной: target statistics для категорий и остатки для бустинга считаются только по «прошлым» примерам случайной перестановки — та же идея out-of-fold, что в target encoding из прошлого урока.

Когда модель обучена, первый вопрос заказчика: «на что она смотрит?». Обучим LightGBM на тех же данных (код — в практике) и сравним способы ответить.

важность признаковважность признаковEN feature importanceSplit — число разбиений по признаку, gain — суммарное уменьшение потери, permutation — рост ошибки на валидации при перемешивании признака. Описывает модель, а не причинность. бывает разной:

  • split — сколько раз признак использовался в разбиениях;
  • gain — насколько суммарно уменьшилась потеря от этих разбиений;
  • перестановочная (permutation) — насколько вырастет ошибка на валидации, если перемешать значения признака и разрушить его связь с ответом.
Признакx0x_0x1x_1x2x_2x3x_3x4x_4x5…x9x_5 \dots x_9 (шум)
split, разбиений742671534502400243–283
gain, доля0.2240.2270.0840.3650.091по 0.002
рост RMSE при перестановке2.472.561.142.961.12около 0

По числу разбиений шумные признаки выглядят лишь вдвое слабее полезного x4x_4: их можно делить сотни раз с крошечной пользой. По gain и перестановке видно правду. Вторая ловушка — коррелированные признаки: если добавить почти точную копию x3x_3, gain поделится между ними (0.204 и 0.163 вместо 0.365), и каждый будет выглядеть вдвое менее важным. Третья — важность описывает модель, а не мир: «модель опирается на признак» не значит «признак вызывает ответ».

Важность — одно число на признак для всей модели. А часто нужно объяснить конкретный прогноз: почему этому клиенту отказали? Ответ даёт теория игр. Представим, что признаки — игроки, которые вместе «заработали» отклонение прогноза от среднего, и разделим выигрыш честно. Значение ШеплиSHAP (значения Шепли)EN SHAP (Shapley additive explanations)Вклад каждого признака в конкретный прогноз по теории игр: базовое значение плюс сумма вкладов равно прогнозу. Для деревьев — точный алгоритм TreeSHAP. признака — его средний вклад по всем порядкам, в которых игроки могли присоединяться к команде.

На числах. Два признака. Средний прогноз (никто не известен) — 2; известен только первый — 6; только второй — 3; оба — 9. Вклад первого, если он пришёл первым: 6−2=46 - 2 = 4; если вторым: 9−3=69 - 3 = 6. Значение Шепли — среднее: φ1=5\varphi_1 = 5. Для второго: 3−2=13 - 2 = 1 и 9−6=39 - 6 = 3, φ2=2\varphi_2 = 2. Проверка: 2+5+2=92 + 5 + 2 = 9 — базовое значение плюс вклады равно прогнозу. Это свойство аддитивности держится всегда.

Для dd признаков порядков d!d!, но для деревьев есть точный быстрый алгоритм TreeSHAP (Лундберг и соавторы). В LightGBM он встроен: booster.predict(X, pred_contrib=True), отдельная библиотека не нужна. Для первой строки валидации базовое значение 14.41, вклады признаков от −1.89 до 0.87, сумма 9.73 — ровно прогноз модели. Средний модуль SHAP по строкам — ещё один, самый надёжный из перечисленных способ ранжировать признаки.

Напишите векторизованный поиск лучшего пня по всем признакам, бустинг на пнях для MSE и функцию, которая снимает кривую RMSE на валидации у LightGBM. Данные — синтетика Фридмана из 5 признаков (400 строк train, 200 validation). Тесты проверят, что ваш пень совпадает с медленным перебором, бустинг опускает RMSE с 4.9 ниже 1.95, кривая LightGBM с num_leaves = 2 почти повторяет вашу, а 4 листа обгоняют пни.

Новое в поиске пня — формула суммы квадратов отклонений через суммы: для mm чисел ∑(ri−rˉ)2=∑ri2−(∑ri)2/m\sum (r_i - \bar r)^2 = \sum r_i^2 - (\sum r_i)^2 / m. После сортировки признака суммы слева от каждого порога даёт np.cumsum, и SSE всех n−1n - 1 порогов считается одной строкой без цикла.

Бустинг на пнях против LightGBMnumpy, lightgbm · нужен интернет7 тестов
import lightgbm as lgb
import numpy as np


def make_data(seed=0):
    """Синтетика Фридмана: y = 10·sin(π·x0·x1) + 20·(x2 − 0.5)² + 10·x3 + 5·x4 + шум (σ = 1).

    600 точек: первые 400 — train, остальные 200 — validation.
    """
    rng = np.random.default_rng(seed)
    X = rng.uniform(0, 1, (600, 5))
    y = 10 * np.sin(np.pi * X[:, 0] * X[:, 1]) + 20 * (X[:, 2] - 0.5) ** 2 + 10 * X[:, 3] + 5 * X[:, 4]
    y = y + rng.normal(0, 1, 600)
    return X[:400], y[:400], X[400:], y[400:]


def rmse(y, pred):
    return float(np.sqrt(np.mean((np.asarray(y) - np.asarray(pred)) ** 2)))


def best_stump(X, r):
    """Лучший «пень» по MSE для целей r: перебрать ВСЕ признаки j и пороги t.

    Пень отвечает left, если X[:, j] <= t, иначе right; left и right — средние r в своих листьях.
    Пороги — середины между соседними РАЗЛИЧНЫМИ значениями признака (как в уроке 2.3.1).
    Лучший — с минимальной суммой квадратов ошибок; при равенстве — первый найденный
    (меньший номер признака, затем меньший порог).
    Вернуть кортеж (j, t, left, right).

    Подсказка для скорости: отсортируйте признак, тогда суммы r и r² слева от каждого порога
    даёт np.cumsum, и SSE всех порогов считается одной формулой без цикла по порогам.
    """
    # TODO: ваш код
    ...


def predict_stump(stump, X):
    """Предсказание пня (готово)."""
    j, t, left, right = stump
    return np.where(X[:, j] <= t, left, right)


def boost_stumps(X_tr, y_tr, X_val, y_val, n_rounds=200, lr=0.1):
    """Градиентный бустинг на пнях для MSE.

    F0 = среднее y_tr (одинаковое для train и val). На каждом раунде:
    остатки r = y_tr − F_tr (антиградиент MSE) → пень best_stump(X_tr, r) →
    F_tr += lr · пень(X_tr), F_val += lr · пень(X_val).
    Вернуть список длины n_rounds: RMSE на валидации после каждого раунда.
    """
    # TODO: ваш код
    ...


def lgbm_curve(X_tr, y_tr, X_val, y_val, n_rounds=200, lr=0.1, num_leaves=2):
    """То же в LightGBM: список RMSE на валидации после каждого раунда.

    params = {"objective": "regression", "metric": "rmse", "learning_rate": lr,
              "num_leaves": num_leaves, "verbose": -1, "num_threads": 1}
    Обучите lgb.train(params, lgb.Dataset(X_tr, y_tr), num_boost_round=n_rounds,
    valid_sets=[lgb.Dataset(X_val, y_val)], callbacks=[lgb.record_evaluation(res)]),
    где res = {} — словарь, куда LightGBM запишет историю: res["valid_0"]["rmse"].
    """
    # TODO: ваш код
    ...


# «Запустить»: сравниваем кривые
X_tr, y_tr, X_val, y_val = make_data()
print("бейзлайн (всегда среднее):", round(rmse(y_val, np.full(len(y_val), y_tr.mean())), 3))
own = boost_stumps(X_tr, y_tr, X_val, y_val)
if own is not None:
    print("свой бустинг, RMSE на раундах 1/10/50/100/200:", [round(own[i], 3) for i in (0, 9, 49, 99, 199)])
for leaves in (2, 4):
    curve = lgbm_curve(X_tr, y_tr, X_val, y_val, num_leaves=leaves)
    if curve is not None:
        print(f"LightGBM, num_leaves={leaves}:", [round(float(curve[i]), 3) for i in (0, 9, 49, 99, 199)])

LightGBM с ранней остановкой, важностью и SHAP (локально)

Заголовок раздела «LightGBM с ранней остановкой, важностью и SHAP (локально)»

Код, которым получены таблицы урока. Установка: pip install lightgbm.

import numpy as np
import lightgbm as lgb
rng = np.random.default_rng(1)
X = rng.uniform(0, 1, (3000, 10)) # 10 признаков, из них x5…x9 — чистый шум
y = (10 * np.sin(np.pi * X[:, 0] * X[:, 1]) + 20 * (X[:, 2] - 0.5) ** 2
+ 10 * X[:, 3] + 5 * X[:, 4] + rng.normal(0, 1, 3000))
X_tr, y_tr, X_val, y_val = X[:2000], y[:2000], X[2000:], y[2000:]
params = {"objective": "regression", "metric": "rmse", "learning_rate": 0.05,
"num_leaves": 8, "verbose": -1, "seed": 0,
"num_threads": 1} # на 2000 строк один поток быстрее; на больших данных уберите
booster = lgb.train(params, lgb.Dataset(X_tr, y_tr), num_boost_round=5000,
valid_sets=[lgb.Dataset(X_val, y_val)],
callbacks=[lgb.early_stopping(100, verbose=False)]) # стоп, если 100 деревьев без улучшения
pred = booster.predict(X_val) # по умолчанию — с лучшим числом деревьев
print("лучшее число деревьев:", booster.best_iteration)
print("RMSE на валидации:", round(float(np.sqrt(np.mean((y_val - pred) ** 2))), 3))
split = booster.feature_importance("split") # сколько раз признак использовали в разбиениях
gain = booster.feature_importance("gain") # суммарное уменьшение loss от этих разбиений
print("split:", split)
print("gain, доли:", np.round(gain / gain.sum(), 3))
contrib = booster.predict(X_val, pred_contrib=True) # SHAP: (n, 10 + 1), последний столбец — базовое значение
print("SHAP первой строки:", np.round(contrib[0], 2))
print("сумма вкладов:", round(float(contrib[0].sum()), 2), " прогноз:", round(float(pred[0]), 2))
лучшее число деревьев: 600
RMSE на валидации: 1.2
split: [742 671 534 502 400 265 283 243 277 283]
gain, доли: [0.224 0.227 0.084 0.365 0.091 0.002 0.002 0.002 0.002 0.002]
SHAP первой строки: [-0.06 0.87 -1.47 -1.89 -1.87 -0.08 -0.07 -0.03 -0.03 -0.06 14.41]
сумма вкладов: 9.73 прогноз: 9.73

Числа могут слегка отличаться в другой версии LightGBM. Поменяйте num_leaves на 2 и 64, learning_rate на 0.3 и сравните с таблицами. Перестановочную важность даёт sklearn.inspection.permutation_importance, графики SHAP — библиотека shap.

Проверь себя6 вопросов
1 Бинарная классификация, log-loss. Пример класса 0, текущий логит F=0F = 0. На какое значение будет учиться следующее дерево для этого примера (антиградиент)?
2 В лист попали 2 примера: y=(1,1)y = (1, 1), у обоих p=0.5p = 0.5. Чему равно значение листа шагом Ньютона (без регуляризации)?
3 В узле G=−4G = -4, H=3H = 3. Каким будет оптимальный вес листа XGBoost при λ=1\lambda = 1?
4 Какие утверждения о гиперпараметрах верны?Можно выбрать несколько ответов
5 Шумный признак получил много разбиений (split), но почти нулевой gain. Чему верить?
6 Базовое значение SHAP равно 10, вклады трёх признаков строки: 2, −3 и 0.5. Каков прогноз модели для этой строки?
  • Градиентный бустинг — градиентный спуск в пространстве функций: каждое дерево приближает антиградиент потери по предсказаниям. Для MSE это остатки, для log-loss — y−py - p, тот же градиент, что у softmax-cross-entropy в LLM.
  • Структуру дерева задаёт антиградиент, а значения листьев выбирают шагом Ньютона: ∑(y−p)/∑p(1−p)\sum (y - p) / \sum p(1-p). XGBoost делает вторые производные частью цели: w∗=−G/(H+λ)w^* = -G/(H + \lambda), разбиение — только если выигрыш больше γ\gamma.
  • Главные рычаги LightGBM: learning_rate (0.03–0.1) и ранняя остановка, num_leaves и min_data_in_leaf, сабсэмплинг строк и признаков. Гистограммы с 63–255 корзинами почти не теряют точности.
  • XGBoost — регуляризованная цель второго порядка, LightGBM — гистограммы и рост по листьям, CatBoost — ordered boosting и категории. Все три сами справляются с NaN.
  • Важность по числу разбиений обманывает; смотрите gain, перестановочную важность и SHAP, а SHAP-вклады строки в сумме с базой дают её прогноз.

Как бустинг работает ранжировщиком в настоящей системе — ленте рекомендаций маркетплейса, — в уроке 11.2.3.

Дальше: проект с честной валидацией — Pipeline без утечек, стратегии кросс-валидации, подбор гиперпараметров и одна-единственная проверка на отложенном тесте.