Gradient boosting — це, мабуть, найпотужніший інструмент для табличних даних, який у нас є на сьогодні. Якщо ви працювали з будь-яким Kaggle-змаганням за останні років п'ять, ви точно бачили ці три назви: XGBoost, LightGBM та CatBoost. Вони скрізь — і в змаганнях, і в продакшені. Але коли приходить час обрати щось конкретне для свого проєкту, починається головний біль. У цьому гайді я спробую розкласти все по поличках — порівняємо фреймворки на реальному коді з актуальними версіями 2026 року, і я поділюсь власними спостереженнями з досвіду.
Що таке gradient boosting і чому він працює
Якщо коротко — gradient boosting будує купу слабких дерев рішень одне за одним, де кожне наступне дерево намагається виправити помилки попередніх. Це принципово відрізняється від, скажімо, випадкового лісу, де дерева ростуть паралельно і нічого не знають одне про одного.
З математичної сторони все зводиться до мінімізації функції втрат через додавання нових дерев у напрямку антиградієнта. Звучить складно, але на практиці це дає неймовірну гнучкість — один і той самий підхід працює для класифікації, регресії, ранжування і ще багато чого. Саме тому gradient boosting став таким універсальним інструментом.
Огляд фреймворків: XGBoost 3.2, LightGBM 4.6 та CatBoost 1.2.10
XGBoost 3.2 — перевірений часом лідер
XGBoost створив Тяньці Чен аж у 2014 році, і з того часу він встиг стати чимось на кшталт золотого стандарту. Зараз актуальна версія — XGBoost 3.2.0 (лютий 2026). Ось що він вміє:
- Побудова дерев по рівнях (level-wise) — на кожному кроці розбиваються всі вузли одного рівня. Дерева виходять збалансовані, глибина контрольована — все як ми любимо.
- L1 (Lasso) і L2 (Ridge) регуляризація — вбудований захист від перенавчання, не треба нічого додатково вигадувати.
- Автоматична обробка пропущених значень — XGBoost сам знаходить оптимальний напрямок для вузлів із пропусками. Чесно, це одна з тих речей, які економлять купу часу на препроцесингу.
- Зовнішня пам'ять (External Memory) — у версії 3.0+ серйозно переробили підтримку зовнішньої пам'яті, тепер можна працювати з даними до терабайтів.
- Рекодер категоріальних даних — у версії 3.1 нарешті додали збереження категорій у навченій моделі та автоматичне перекодування під час інференсу.
LightGBM 4.6 — швидкість та ефективність
LightGBM — це дитя Microsoft, яке з'явилось у 2017 році і одразу змінило правила гри у плані швидкості. Актуальна версія — LightGBM 4.6.0 (лютий 2025), версія 4.6.0.99 зараз у розробці. Цікавий момент: у березні 2026 проєкт мігрував з Microsoft/LightGBM до lightgbm-org/LightGBM, хоча команда мейнтейнерів залишилася та сама.
- Побудова дерев по листях (leaf-wise) — замість того щоб чесно розбивати всі вузли рівня, LightGBM шукає листок із найбільшим зменшенням помилки і розщеплює саме його. Дерева виходять глибші, але ефективніші. На практиці це дає дуже помітний буст.
- GOSS (Gradient-based One-Side Sampling) — розумна вибірка екземплярів із більшими градієнтами. Менше даних обробляється, а якість майже не страждає.
- EFB (Exclusive Feature Bundling) — об'єднання взаємовиключних ознак для зменшення розмірності. Хитра оптимізація, але працює.
- Гістограмний підхід — безперервні ознаки дискретизуються в бакети, що різко знижує використання пам'яті і прискорює обчислення.
- GPU-підтримка з коробки — починаючи з версії 4.4, звичайна pip-інсталяція автоматично включає OpenCL-підтримку GPU. Не треба нічого збирати вручну — просто ставиш і їдеш.
CatBoost 1.2.10 — найкращий для категоріальних даних
CatBoost від Яндекса (теж 2017 рік — хороший був рік для бустингу). Актуальна версія — CatBoost 1.2.10 (лютий 2026). Зізнаюсь, я довго недооцінював CatBoost, поки не спробував його на проєкті з купою категоріальних фіч — і був приємно здивований.
- Симетричні (збалансовані) дерева — на кожному рівні всі вузли розбиваються за однією і тією ж умовою. Це дуже прискорює інференс і зменшує перенавчання.
- Вбудована обробка категоріальних ознак — CatBoost використовує Ordered Target Statistics замість one-hot encoding. Якщо у вас фіча з тисячами категорій (назви міст, товарів тощо), це просто рятує.
- Мінімальне налаштування гіперпараметрів — серйозно, CatBoost часто дає пристойні результати навіть якщо ви просто створите модель і натиснете fit(). Для прототипування — безцінна річ.
- Підтримка Polars — у версії 1.2.9+ додали підтримку Polars DataFrame. Якщо ви вже перейшли на Polars (а варто хоча б спробувати), це дуже зручно.
- Прискорення до 10x — багатопотокова ініціалізація даних із numpy float32 масивів і виправлення неефективних циклів у Cython.
Порівняльна таблиця: XGBoost vs LightGBM vs CatBoost
| Критерій | XGBoost 3.2 | LightGBM 4.6 | CatBoost 1.2.10 |
|---|---|---|---|
| Стратегія побудови дерев | По рівнях (level-wise) | По листях (leaf-wise) | Симетричні дерева |
| Швидкість навчання | Середня | Найшвидша (до 11–15x швидше за XGBoost) | Швидша за XGBoost |
| Використання пам'яті | Вище | Низьке (гістограми) | Середнє |
| Категоріальні ознаки | Підтримка з 2.0+ | Часткова підтримка | Найкраща вбудована підтримка |
| Якість «з коробки» | Потребує тюнінгу | Потребує тюнінгу | Відмінна без тюнінгу |
| GPU-навчання | CUDA | OpenCL / CUDA | CUDA |
| Зовнішня пам'ять | Повна підтримка (до ТБ) | Базова | Ні |
| Швидкість інференсу | Швидка | Швидка | Найшвидша |
| Ліцензія | Apache 2.0 | MIT | Apache 2.0 |
Практичний приклад: класифікація на датасеті
Ну добре, досить теорії. Давайте подивимось на код. Порівняємо всі три фреймворки на задачі бінарної класифікації — візьмемо класичний Breast Cancer зі scikit-learn (так, він маленький, але для демонстрації підходить ідеально).
Встановлення бібліотек
pip install xgboost lightgbm catboost scikit-learn pandas numpy
Підготовка даних
import numpy as np
import pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score
import time
# Завантаження даних
data = load_breast_cancer()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = data.target
# Розбиття на тренувальну та тестову вибірки
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
XGBoost — навчання та оцінка
Починаємо з дідуся бустингу. Тут все досить стандартно:
import xgboost as xgb
# Створення та навчання моделі XGBoost
xgb_model = xgb.XGBClassifier(
n_estimators=300,
max_depth=6,
learning_rate=0.1,
subsample=0.8,
colsample_bytree=0.8,
eval_metric='logloss',
random_state=42
)
start = time.time()
xgb_model.fit(X_train, y_train)
xgb_time = time.time() - start
xgb_pred = xgb_model.predict(X_test)
xgb_proba = xgb_model.predict_proba(X_test)[:, 1]
print(f"XGBoost — Accuracy: {accuracy_score(y_test, xgb_pred):.4f}")
print(f"XGBoost — F1: {f1_score(y_test, xgb_pred):.4f}")
print(f"XGBoost — ROC AUC: {roc_auc_score(y_test, xgb_proba):.4f}")
print(f"XGBoost — Час навчання: {xgb_time:.4f} сек")
LightGBM — навчання та оцінка
Тепер LightGBM. Зверніть увагу на verbose=-1 — без цього він буде засипати вас логами на кожній ітерації (повірте, це дратує).
import lightgbm as lgb
# Створення та навчання моделі LightGBM
lgb_model = lgb.LGBMClassifier(
n_estimators=300,
max_depth=6,
learning_rate=0.1,
subsample=0.8,
colsample_bytree=0.8,
verbose=-1,
random_state=42
)
start = time.time()
lgb_model.fit(X_train, y_train)
lgb_time = time.time() - start
lgb_pred = lgb_model.predict(X_test)
lgb_proba = lgb_model.predict_proba(X_test)[:, 1]
print(f"LightGBM — Accuracy: {accuracy_score(y_test, lgb_pred):.4f}")
print(f"LightGBM — F1: {f1_score(y_test, lgb_pred):.4f}")
print(f"LightGBM — ROC AUC: {roc_auc_score(y_test, lgb_proba):.4f}")
print(f"LightGBM — Час навчання: {lgb_time:.4f} сек")
CatBoost — навчання та оцінка
І нарешті CatBoost. Мінімум параметрів — і це не лінь, а свідомий вибір. CatBoost справді непогано працює з дефолтами.
from catboost import CatBoostClassifier
# Створення та навчання моделі CatBoost
cb_model = CatBoostClassifier(
iterations=300,
depth=6,
learning_rate=0.1,
verbose=0,
random_state=42
)
start = time.time()
cb_model.fit(X_train, y_train)
cb_time = time.time() - start
cb_pred = cb_model.predict(X_test)
cb_proba = cb_model.predict_proba(X_test)[:, 1]
print(f"CatBoost — Accuracy: {accuracy_score(y_test, cb_pred):.4f}")
print(f"CatBoost — F1: {f1_score(y_test, cb_pred):.4f}")
print(f"CatBoost — ROC AUC: {roc_auc_score(y_test, cb_proba):.4f}")
print(f"CatBoost — Час навчання: {cb_time:.4f} сек")
Робота з категоріальними ознаками
Ось тут починається найцікавіше. Обробка категоріальних ознак — це та річ, де різниця між фреймворками відчувається найбільше. Я якось витратив пів дня на one-hot encoding для фічі з 50 000 категорій (назви товарів), а потім просто передав сирі категорії в CatBoost і отримав кращий результат за 10 хвилин. Було і смішно, і боляче одночасно.
CatBoost — нативна підтримка категорій
import pandas as pd
from catboost import CatBoostClassifier, Pool
# Створення датасету з категоріальними ознаками
df = pd.DataFrame({
'city': ['Kyiv', 'Lviv', 'Odesa', 'Kharkiv', 'Kyiv', 'Lviv'] * 100,
'category': ['A', 'B', 'C', 'A', 'B', 'C'] * 100,
'value': np.random.randn(600),
'target': np.random.randint(0, 2, 600)
})
cat_features = ['city', 'category']
# CatBoost автоматично обробляє категоріальні ознаки
train_pool = Pool(
data=df.drop('target', axis=1),
label=df['target'],
cat_features=cat_features
)
model = CatBoostClassifier(iterations=100, verbose=0)
model.fit(train_pool)
LightGBM — підтримка через dtype
LightGBM теж вміє працювати з категоріями, але треба явно вказати тип. Не складно, але додатковий крок.
import lightgbm as lgb
# LightGBM підтримує категоріальні ознаки через тип category
df_lgb = df.copy()
for col in cat_features:
df_lgb[col] = df_lgb[col].astype('category')
lgb_model = lgb.LGBMClassifier(n_estimators=100, verbose=-1)
lgb_model.fit(
df_lgb.drop('target', axis=1),
df_lgb['target'],
categorical_feature=cat_features
)
XGBoost — підтримка категорій через enable_categorical
XGBoost додав нативну підтримку категорій пізніше за інших, і це видно — потрібно і тип змінити, і прапорець увімкнути, і tree_method='hist' поставити. Працює, але кроків більше.
import xgboost as xgb
# XGBoost підтримує категоріальні ознаки з версії 2.0+
df_xgb = df.copy()
for col in cat_features:
df_xgb[col] = df_xgb[col].astype('category')
xgb_model = xgb.XGBClassifier(
n_estimators=100,
enable_categorical=True,
tree_method='hist'
)
xgb_model.fit(df_xgb.drop('target', axis=1), df_xgb['target'])
Налаштування гіперпараметрів за допомогою Optuna
Якщо ви хочете вичавити максимум із моделі, без тюнінгу гіперпараметрів не обійтись. Optuna — мій улюблений інструмент для цього (раніше використовував GridSearch, але після Optuna повертатись не хочеться). Ось приклад для XGBoost, але адаптувати під LightGBM чи CatBoost — справа десяти хвилин.
import optuna
from sklearn.model_selection import cross_val_score
import xgboost as xgb
def objective(trial):
params = {
'n_estimators': trial.suggest_int('n_estimators', 100, 1000),
'max_depth': trial.suggest_int('max_depth', 3, 10),
'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3, log=True),
'subsample': trial.suggest_float('subsample', 0.6, 1.0),
'colsample_bytree': trial.suggest_float('colsample_bytree', 0.6, 1.0),
'min_child_weight': trial.suggest_int('min_child_weight', 1, 10),
'reg_alpha': trial.suggest_float('reg_alpha', 1e-8, 10.0, log=True),
'reg_lambda': trial.suggest_float('reg_lambda', 1e-8, 10.0, log=True),
'eval_metric': 'logloss',
'random_state': 42
}
model = xgb.XGBClassifier(**params)
scores = cross_val_score(model, X_train, y_train, cv=5, scoring='roc_auc')
return scores.mean()
# Запуск оптимізації
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=100)
print(f"Найкращі параметри: {study.best_params}")
print(f"Найкращий ROC AUC: {study.best_value:.4f}")
Важливість ознак: порівняння підходів
Розуміння того, які фічі реально впливають на результат — це не просто для красивих графіків. Це інструмент для відбору ознак і, що важливіше, для розуміння вашої предметної області. Буває, модель показує, що якась фіча, на яку ви не звертали уваги, виявляється найважливішою. Такі моменти — одне з найприємніших відчуттів у роботі з даними.
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
# XGBoost — важливість ознак
xgb_importance = xgb_model.feature_importances_
top_idx = np.argsort(xgb_importance)[-10:]
axes[0].barh(range(10), xgb_importance[top_idx])
axes[0].set_yticks(range(10))
axes[0].set_yticklabels(X.columns[top_idx])
axes[0].set_title('XGBoost')
# LightGBM — важливість ознак
lgb_importance = lgb_model.feature_importances_
top_idx = np.argsort(lgb_importance)[-10:]
axes[1].barh(range(10), lgb_importance[top_idx])
axes[1].set_yticks(range(10))
axes[1].set_yticklabels(X.columns[top_idx])
axes[1].set_title('LightGBM')
# CatBoost — важливість ознак
cb_importance = cb_model.get_feature_importance()
top_idx = np.argsort(cb_importance)[-10:]
axes[2].barh(range(10), cb_importance[top_idx])
axes[2].set_yticks(range(10))
axes[2].set_yticklabels(X.columns[top_idx])
axes[2].set_title('CatBoost')
plt.tight_layout()
plt.savefig('feature_importance_comparison.png', dpi=150)
plt.show()
Early stopping: коли зупинити навчання
Early stopping — це одна з тих речей, яку варто використовувати завжди. Серйозно, завжди. Він і від перенавчання захищає, і час економить. Кожен фреймворк реалізує його трохи по-різному, але ідея одна: якщо модель перестала покращуватись на валідаційній вибірці — зупиняємось.
# XGBoost з early stopping
xgb_model = xgb.XGBClassifier(
n_estimators=1000,
learning_rate=0.1,
early_stopping_rounds=50,
eval_metric='logloss'
)
xgb_model.fit(
X_train, y_train,
eval_set=[(X_test, y_test)],
verbose=False
)
print(f"XGBoost зупинився на ітерації: {xgb_model.best_iteration}")
# LightGBM з early stopping через callbacks
lgb_model = lgb.LGBMClassifier(n_estimators=1000, learning_rate=0.1, verbose=-1)
lgb_model.fit(
X_train, y_train,
eval_set=[(X_test, y_test)],
callbacks=[lgb.early_stopping(50), lgb.log_evaluation(0)]
)
print(f"LightGBM зупинився на ітерації: {lgb_model.best_iteration_}")
# CatBoost з early stopping
cb_model = CatBoostClassifier(
iterations=1000,
learning_rate=0.1,
early_stopping_rounds=50,
verbose=0
)
cb_model.fit(X_train, y_train, eval_set=(X_test, y_test))
print(f"CatBoost зупинився на ітерації: {cb_model.best_iteration_}")
Ансамблювання: об'єднуємо силу трьох фреймворків
А тепер фішка, яка виграє змагання. Якщо подивитись на топові рішення Kaggle, дуже часто там не одна модель, а ансамбль із кількох. Найпростіший варіант — просто усереднити ймовірності. Звучить примітивно, але працює напрочуд добре.
# Усереднення прогнозів (soft voting)
ensemble_proba = (xgb_proba + lgb_proba + cb_proba) / 3
ensemble_pred = (ensemble_proba >= 0.5).astype(int)
print(f"Ансамбль — Accuracy: {accuracy_score(y_test, ensemble_pred):.4f}")
print(f"Ансамбль — F1: {f1_score(y_test, ensemble_pred):.4f}")
print(f"Ансамбль — ROC AUC: {roc_auc_score(y_test, ensemble_proba):.4f}")
Але якщо хочете піти далі — є стекінг. Ідея в тому, щоб навчити метамодель (зазвичай щось просте, типу логістичної регресії) на прогнозах базових моделей. По суті, ви даєте метамоделі зрозуміти, кому з трьох бустингів коли варто довіряти більше.
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_predict
# Генерація метаознак через крос-валідацію
xgb_meta = cross_val_predict(
xgb.XGBClassifier(n_estimators=300, eval_metric='logloss'),
X_train, y_train, cv=5, method='predict_proba'
)[:, 1]
lgb_meta = cross_val_predict(
lgb.LGBMClassifier(n_estimators=300, verbose=-1),
X_train, y_train, cv=5, method='predict_proba'
)[:, 1]
cb_meta = cross_val_predict(
CatBoostClassifier(iterations=300, verbose=0),
X_train, y_train, cv=5, method='predict_proba'
)[:, 1]
# Навчання метамоделі
meta_X_train = np.column_stack([xgb_meta, lgb_meta, cb_meta])
meta_model = LogisticRegression()
meta_model.fit(meta_X_train, y_train)
# Прогноз на тестових даних
meta_X_test = np.column_stack([xgb_proba, lgb_proba, cb_proba])
stack_proba = meta_model.predict_proba(meta_X_test)[:, 1]
stack_pred = meta_model.predict(meta_X_test)
print(f"Стекінг — ROC AUC: {roc_auc_score(y_test, stack_proba):.4f}")
Коли обирати який фреймворк: практичні рекомендації
Окей, давайте чесно — «найкращого» фреймворку не існує. Все залежить від контексту. Але ось мої рекомендації, засновані на досвіді.
Обирайте XGBoost, якщо:
- Працюєте з надвеликими даними і вам реально потрібна зовнішня пам'ять — XGBoost 3.x тягне датасети до терабайтів, і це не маркетинг.
- Потрібна максимальна гнучкість і, скажімо, інтеграція зі Spark для розподіленого навчання.
- Ваша команда вже знає XGBoost — він найкраще документований, найбільше відповідей на Stack Overflow, і це теж має значення.
Обирайте LightGBM, якщо:
- Швидкість навчання — пріоритет. LightGBM у 11-15 разів швидший за XGBoost на великих датасетах. Це не перебільшення.
- У вас обмежені ресурси пам'яті — гістограмний підхід реально економить RAM.
- Потрібно швидко ітерувати і проганяти багато експериментів. Коли навчання однієї моделі займає секунди замість хвилин, це змінює весь робочий процес.
Обирайте CatBoost, якщо:
- У ваших даних багато категоріальних ознак — CatBoost обробляє їх нативно, і зазвичай робить це краще, ніж ваш ручний encoding.
- Хочете хороші результати без годин тюнінгу гіперпараметрів. Для прототипів і MVP — ідеальний варіант.
- Швидкість інференсу критична (наприклад, продакшн-сервіс із жорсткими вимогами до latency) — симетричні дерева дають найшвидше прогнозування.
Інтеграція зі scikit-learn пайплайнами
Окремий плюс всіх трьох фреймворків — вони повністю сумісні з scikit-learn API. Це означає, що ви можете вставити їх у Pipeline, використовувати з GridSearchCV, cross_val_score і всім іншим звичним інструментарієм. Ось короткий приклад:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.compose import ColumnTransformer
import lightgbm as lgb
# Визначення числових ознак
numeric_features = X.columns.tolist()
preprocessor = ColumnTransformer(
transformers=[
('num', StandardScaler(), numeric_features)
]
)
# Створення пайплайну
pipeline = Pipeline([
('preprocessor', preprocessor),
('classifier', lgb.LGBMClassifier(n_estimators=200, verbose=-1))
])
# Крос-валідація
scores = cross_val_score(pipeline, X, y, cv=5, scoring='roc_auc')
print(f"ROC AUC (CV): {scores.mean():.4f} ± {scores.std():.4f}")
Часті питання (FAQ)
Що краще для початківців: XGBoost, LightGBM чи CatBoost?
Я б рекомендував починати з CatBoost. Мінімум налаштувань, хороший результат з коробки, автоматична обробка категорій — все це дозволяє зосередитись на розумінні даних, а не на підборі параметрів. Потім, коли відчуєте себе впевненіше, варто освоїти LightGBM (для швидкості) і XGBoost (для гнучкості). Але чесно? В якийсь момент вам знадобляться всі три.
Чи можна використовувати XGBoost, LightGBM і CatBoost разом?
Не просто можна — це стандартна практика на Kaggle. Ансамблювання прогнозів трьох фреймворків (averaging або stacking) майже завжди дає кращий результат, ніж будь-яка окрема модель. Причина проста: кожен фреймворк будує дерева по-різному і знаходить різні патерни в даних. Різноманітність у ансамблі — це сила.
Який фреймворк найшвидший на GPU?
Для GPU-навчання LightGBM і XGBoost зазвичай показують найкращу швидкість із CUDA. CatBoost теж підтримує GPU і особливо ефективний, коли у вас багато категоріальних ознак. А от на CPU найшвидший інференс — у CatBoost, завдяки симетричній архітектурі дерев. Тут все залежить від того, що саме вам важливіше: швидкість навчання чи швидкість прогнозування.
Як обрати між gradient boosting та нейронними мережами?
Просте правило: якщо ваші дані лежать у таблиці — починайте з gradient boosting. Для табличних даних він зазвичай перевершує нейромережі і за якістю, і за швидкістю навчання, і за інтерпретованістю. Нейронки краще підходять для зображень, тексту, аудіо — тобто неструктурованих даних. Так, є TabNet і подібні архітектури, але на практиці вони рідко б'ють добре налаштований бустинг (принаймні поки що).
Чи потрібна нормалізація даних для gradient boosting?
Ні, і це одна з приємних особливостей дерев рішень. На відміну від лінійних моделей чи нейронних мереж, gradient boosting не потребує нормалізації чи стандартизації ознак. Дерева розбивають дані за пороговими значеннями, тому масштаб ознак взагалі не має значення. Одна фіча в діапазоні 0-1, інша в 0-1000000 — бустингу все одно.