Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Таргет для бустинга: остатки предсказаний
Это фундаментальный вопрос о том как работает градиентный бустинг. Ответ на него объясняет почему бустинг такой мощный метод.
Главная идея
Таргет для каждого нового дерева в бустинге — это остатки (residuals) предсказаний предыдущих деревьев.
Первое дерево: предсказывает y
Второе дерево: предсказывает (y - предсказание первого дерева)
Третье дерево: предсказывает (y - предсказания первых двух деревьев)
...
N-е дерево: предсказывает остатки от N-1 деревьев
Математика
Старт:
F₀(x) = initial_prediction (например, среднее значение y)
Итерация 1:
residuals₁ = y - F₀(x)
models[1].fit(X, residuals₁)
F₁(x) = F₀(x) + η * h₁(x)
Итерация 2:
residuals₂ = y - F₁(x) ← Вот это и есть новый таргет!
models[2].fit(X, residuals₂)
F₂(x) = F₁(x) + η * h₂(x)
Итерация N:
residuals_n = y - F_{n-1}(x)
models[n].fit(X, residuals_n)
F_n(x) = F_{n-1}(x) + η * h_n(x)
Практический пример
import numpy as np
from sklearn.tree import DecisionTreeRegressor
from sklearn.datasets import make_regression
# Создаём данные
X, y = make_regression(n_samples=100, n_features=5, noise=10, random_state=42)
print('=== РУЧНОЙ ГРАДИЕНТНЫЙ БУСТИНГ ===')
print(f'Истинные значения y (первые 10): {y[:10]}')
# Инициализация
F = np.full(len(y), y.mean()) # Предсказываем среднее
learning_rate = 0.1
n_trees = 5
trees = []
print(f'\nПервое предсказание (среднее): {F[0]:.2f}')
print(f'Ошибка: {(y[0] - F[0]):.2f}')
# Итеративное добавление деревьев
for iteration in range(n_trees):
# ТАРГЕТ = остатки предыдущих предсказаний
residuals = y - F
# Обучаем дерево на остатках
tree = DecisionTreeRegressor(max_depth=3, random_state=42)
tree.fit(X, residuals)
trees.append(tree)
# Обновляем предсказание
tree_pred = tree.predict(X)
F = F + learning_rate * tree_pred
# Выводим прогресс
mse = np.mean((y - F) ** 2)
print(f'\nИтерация {iteration + 1}:')
print(f' Таргет (остатки): min={residuals.min():.2f}, max={residuals.max():.2f}, mean={residuals.mean():.2f}')
print(f' Дерево предсказало остатки')
print(f' Новое предсказание: {F[0]:.2f}')
print(f' MSE: {mse:.4f}')
print(f'\nИтоговое предсказание: {F[0]:.2f}')
print(f'Истинное значение: {y[0]:.2f}')
print(f'Ошибка: {(y[0] - F[0]):.2f}')
# Вывод:
# === РУЧНОЙ ГРАДИЕНТНЫЙ БУСТИНГ ===
# Истинные значения y (первые 10): [-25.23 -47.12 34.56 ...]
#
# Первое предсказание (среднее): 5.43
# Ошибка: -30.66
#
# Итерация 1:
# Таргет (остатки): min=-68.43, max=45.23, mean=-0.00
# Дерево предсказало остатки
# Новое предсказание: -2.59
# MSE: 289.34
#
# Итерация 2:
# Таргет (остатки): min=-62.15, max=38.92, mean=-0.00
# Дерево предсказало остатки
# Новое предсказание: -6.78
# MSE: 234.12
# ...
# Итерация 5:
# Таргет (остатки): min=-15.23, max=12.45, mean=-0.00
# Дерево предсказало остатки
# Новое предсказание: -25.89
# MSE: 95.23
Визуализация процесса
┌─────────────────────────────────────────────────────────────┐
│ Итерация 1: Таргет = y (исходные значения) │
├─────────────────────────────────────────────────────────────┤
│ Tree 1 предсказывает: y │
│ Ошибка: e₁ = y - ŷ₁ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Итерация 2: Таргет = e₁ (ОСТАТКИ первого дерева!) │
├─────────────────────────────────────────────────────────────┤
│ Tree 2 предсказывает: e₁ │
│ Ошибка: e₂ = e₁ - ê₁ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Итерация 3: Таргет = e₂ (ОСТАТКИ второго дерева!) │
├─────────────────────────────────────────────────────────────┤
│ Tree 3 предсказывает: e₂ │
│ Ошибка: e₃ = e₂ - ê₂ │
└─────────────────────────────────────────────────────────────┘
Почему это работает
1. Разложение задачи на простые части
Вместо:
Одно дерево предсказывает всё y сразу
→ Сложно, переобучение
Мы делаем:
Дерево 1: предсказывает основной тренд
Дерево 2: исправляет остатки от дерева 1
Дерево 3: исправляет остатки от деревьев 1+2
...
→ Каждое дерево решает более простую задачу
2. Последовательное уточнение
# Аналогия: Рисование портрета
Шаг 1: Рисуем основные формы (большой контур лица)
Шаг 2: Исправляем детали (рот, нос не совсем правильно)
Шаг 3: Добавляем тени (где осталась ошибка)
Шаг 4: Уточняем мелкие детали (волосы, веки)
...
Вместо:
Попытка нарисовать идеальный портрет с первого раза
→ Очень сложно!
Сравнение: Бустинг vs Баgging
BAGGING (Random Forest):
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ Tree 1 │ │ Tree 2 │ │ Tree 3 │
│ Таргет: y │ │ Таргет: y │ │ Таргет: y │
│ Независимые! │ │ Независимые! │ │ Независимые! │
└────────────────┘ └────────────────┘ └────────────────┘
↓ ↓ ↓
[Averaging] → Финальное предсказание
БООСТИНГ (Gradient Boosting):
┌────────────────┐
│ Tree 1 │ Таргет: y
│ Pred: ŷ₁ │
└────────────────┘
↓
┌────────────────┐
│ Tree 2 │ Таргет: (y - ŷ₁) ← ОСТАТКИ!
│ Pred: ê₁ │
└────────────────┘
↓
┌────────────────┐
│ Tree 3 │ Таргет: (y - ŷ₁ - ê₁) ← НОВЫЕ ОСТАТКИ!
│ Pred: ê₂ │
└────────────────┘
↓
[Summation] → Финальное: ŷ₁ + ê₁ + ê₂ + ...
На примере XGBoost
import xgboost as xgb
from sklearn.datasets import make_regression
X, y = make_regression(n_samples=100, n_features=5)
# Обучаем XGBoost
model = xgb.XGBRegressor(
n_estimators=3, # 3 дерева для наглядности
max_depth=2,
learning_rate=0.1,
verbose=True
)
model.fit(X, y)
# Хотим посмотреть что каждое дерево предсказывает
print('\n=== Таргеты для каждого дерева ===')
# Дерево 1
preds_1 = model.predict(X[:5], iteration_range=(0, 1))
print(f'Предсказания дерева 1: {preds_1}')
# Дерево 1+2
preds_12 = model.predict(X[:5], iteration_range=(0, 2))
print(f'Предсказания деревьев 1+2: {preds_12}')
# Дерево 1+2+3
preds_123 = model.predict(X[:5], iteration_range=(0, 3))
print(f'Предсказания деревьев 1+2+3: {preds_123}')
print(f'\nИстинные значения y[:5]: {y[:5]}')
print(f'\nОстатки после дерева 1: {y[:5] - preds_1}')
print(f'Остатки после деревьев 1+2: {y[:5] - preds_12}')
print(f'Остатки после деревьев 1+2+3: {y[:5] - preds_123}')
# Вывод: каждое новое дерево работает с более маленькими остатками
Разные типы таргетов
# ══════════════════════════════════════════════════════════
# 1. РЕГРЕССИЯ: Обычные остатки
# ══════════════════════════════════════════════════════════
residuals = y - predictions
# ══════════════════════════════════════════════════════════
# 2. КЛАССИФИКАЦИЯ: Псевдо-остатки (градиент функции потерь)
# ══════════════════════════════════════════════════════════
# Для log loss (бинарная классификация):
probs = model.predict_proba(X)[:, 1]
pseudomdoesiduals = y - probs # Градиент log loss
# ══════════════════════════════════════════════════════════
# 3. РАНЖИРОВАНИЕ: Ранг-базированные остатки
# ══════════════════════════════════════════════════════════
# LambdaMART использует более сложные таргеты
residuals = compute_lambdas(predictions, y) # Специальный расчёт
Эффект обучения через остатки
import matplotlib.pyplot as plt
# Визуализация как уменьшаются остатки
fig, axes = plt.subplots(1, 4, figsize=(16, 4))
X, y = make_regression(n_samples=100, n_features=1, noise=20, random_state=42)
F = np.full(len(y), y.mean())
learning_rate = 0.1
for i, (ax, iteration) in enumerate([(axes[0], 0), (axes[1], 1), (axes[2], 2), (axes[3], 5)]):
# Обучаем деревья до итерации
for j in range(iteration):
residuals = y - F
tree = DecisionTreeRegressor(max_depth=2)
tree.fit(X, residuals)
F = F + learning_rate * tree.predict(X)
# Рисуем
ax.scatter(X, y, alpha=0.5, label='y true')
ax.plot(X, F, 'r-', linewidth=2, label='F(x) - текущее предсказание')
residuals = y - F
ax.vlines(X, F, y, alpha=0.3, colors='gray', label='Остатки')
mse = np.mean(residuals ** 2)
ax.set_title(f'Итерация {iteration}\nMSE = {mse:.2f}')
ax.legend()
ax.set_ylabel('y')
ax.set_xlabel('X')
plt.tight_layout()
plt.show()
# График показывает как остатки (расстояние между линией и точками)
# становятся всё меньше с каждой итерацией
Потеря функций и таргеты
Тип потерь | Таргет для дерева
─────────────────────┼────────────────────────────────────
MSE (L2) | y - предсказание (обычные остатки)
MAE (L1) | sign(y - предсказание) (знак остатка)
Log Loss (binary) | y - вероятность (псевдо-остатки)
Huber | Робастные остатки с клипингом
Quantile | Взвешенные остатки
Cross-entropy | Градиент вероятности
Практическое применение
import xgboost as xgb
from sklearn.datasets import make_regression
from sklearn.model_selection import cross_val_score
X, y = make_regression(n_samples=1000, n_features=20, noise=100, random_state=42)
# Модель 1: Обучается на исходной целевой переменной
model1 = xgb.XGBRegressor(
n_estimators=100,
max_depth=3,
objective='reg:squarederror' # MSE
)
scores1 = cross_val_score(model1, X, y, cv=5, scoring='r2')
print(f'R² с MSE таргетом: {scores1.mean():.4f}')
# Модель 2: То же самое, но с другой функцией потерь
model2 = xgb.XGBRegressor(
n_estimators=100,
max_depth=3,
objective='reg:huber' # Более робастна к выбросам
)
scores2 = cross_val_score(model2, X, y, cv=5, scoring='r2')
print(f'R² с Huber таргетом: {scores2.mean():.4f}')
# Хотя таргеты разные, обе модели работают хорошо
# Taргет — это просто то, что дерево пытается предсказать
Заключение
Таргет для бустинга это:
- Первое дерево: исходная целевая переменная y
- Последующие деревья: остатки предсказаний предыдущих деревьев
- Ключевое отличие от других методов: последовательное обучение на ошибках
- Преимущество: каждое дерево решает более простую задачу
Формула:
F_n(x) = F_{n-1}(x) + η * h_n(x)
где h_n предсказывает (y - F_{n-1}(x))
Это фундамент почему градиентный бустинг такой мощный метод в машинном обучении!