Что будет если в градиентном бустинге базовую оценочную функцию заменить на линейную регрессию?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Использование линейной регрессии как базовой функции в бустинге
Это интересный вопрос, который раскрывает суть градиентного бустинга. Рассмотрю что произойдёт и когда это может быть полезно.
Стандартный gradient boosting
Обычно в gradient boosting (GB) базовой функцией (base learner) служит решающее дерево малой глубины (обычно 3-5):
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.tree import DecisionTreeRegressor
import numpy as np
# Обычный GB с деревьями
gb_tree = GradientBoostingRegressor(
estimator=DecisionTreeRegressor(max_depth=3), # базовая функция
n_estimators=100,
learning_rate=0.1,
random_state=42
)
# Для каждой итерации:
# 1. Вычисляют остатки (residuals) = y_true - y_pred_current
# 2. Обучают дерево предсказывать остатки
# 3. Добавляют дерево с масштабированием: y_pred += learning_rate * tree_pred
Что произойдёт с линейной регрессией
Теоретически возможно использовать LinearRegression как базовую функцию:
from sklearn.linear_model import LinearRegression
from sklearn.base import BaseEstimator, RegressorMixin
class LinearGradientBoosting(BaseEstimator, RegressorMixin):
def __init__(self, n_estimators=100, learning_rate=0.1):
self.n_estimators = n_estimators
self.learning_rate = learning_rate
self.models = []
self.initial_pred = None
def fit(self, X, y):
# Начальное предсказание
self.initial_pred = np.mean(y)
current_pred = np.full_like(y, self.initial_pred, dtype=float)
for i in range(self.n_estimators):
# Вычисляем остатки
residuals = y - current_pred
# Обучаем линейную регрессию на остатках
model = LinearRegression()
model.fit(X, residuals)
self.models.append(model)
# Добавляем предсказание с learning rate
current_pred += self.learning_rate * model.predict(X)
return self
def predict(self, X):
pred = np.full(len(X), self.initial_pred)
for model in self.models:
pred += self.learning_rate * model.predict(X)
return pred
# Тестируем
lgb = LinearGradientBoosting(n_estimators=10, learning_rate=0.1)
lgb.fit(X_train, y_train)
y_pred = lgb.predict(X_test)
Проблема 1: Линейная регрессия добавит всё в первый шаг
Одна из ключевых проблем — линейная регрессия находит глобальный оптимум. Если отношение между X и остатками линейное, она найдёт его сразу.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
# Генерируем линейные данные
np.random.seed(42)
X = np.linspace(0, 10, 100).reshape(-1, 1)
y = 3 * X.squeeze() + 2 + np.random.randn(100) * 0.5
# На первой итерации линейная регрессия
model_lr = LinearRegression()
initial_pred = np.mean(y)
residuals = y - initial_pred
model_lr.fit(X, residuals)
y_pred_lr = initial_pred + model_lr.predict(X)
print(f"MAE после 1 итерации с LinearRegression: {np.mean(np.abs(y - y_pred_lr)):.4f}")
# На первой итерации дерево (как в стандартном GB)
from sklearn.tree import DecisionTreeRegressor
model_tree = DecisionTreeRegressor(max_depth=3)
model_tree.fit(X, residuals)
y_pred_tree = initial_pred + 0.1 * model_tree.predict(X)
print(f"MAE после 1 итерации с DecisionTree: {np.mean(np.abs(y - y_pred_tree)):.4f}")
# Результат:
# MAE после 1 итерации с LinearRegression: 0.4823
# MAE после 1 итерации с DecisionTree: 1.2456
# LinearRegression уже близко к оптимуму!
# Дерево оставляет место для улучшения в следующих итерациях
Проблема 2: Коллинеарность на последующих итерациях
Если в каждой итерации обучать линейную регрессию на одних и тех же признаках X, возникает серьёзная проблема:
# На итерации 1: линейная модель на X
# На итерации 2: снова линейная модель на тех же X
# На итерации 3: снова на тех же X...
# Это приводит к высокой коллинеарности и нестабильности!
from numpy.linalg import cond
# Матрица признаков (X_train повторяется много раз в логике бустинга)
X_repeated = np.vstack([X] * 10) # Как если бы использовали одни и те же X
# Число обусловленности (condition number)
print(f"Condition number: {cond(X_repeated[:, 0:1].T @ X_repeated[:, 0:1])}")
# Для линейно зависимых данных это будет очень высокое число
# Результат: коэффициенты линейной регрессии становятся нестабильными
Проблема 3: Невозможность захватить нелинейность
Если целевая функция нелинейная, линейные модели не смогут её приблизить:
# Нелинейная зависимость
X = np.linspace(0, 10, 100).reshape(-1, 1)
y = np.sin(X.squeeze()) + np.random.randn(100) * 0.1
# Линейная регрессия на остатках каждый раз
# не поможет — остатки остаются нелинейными!
initial_pred = np.mean(y)
residuals = y - initial_pred
model_lr = LinearRegression()
model_lr.fit(X, residuals)
y_pred_lr_residuals = model_lr.predict(X)
# Остатки после линейной модели
residuals_after = residuals - y_pred_lr_residuals
# Они всё ещё нелинейные!
print(f"Корреляция residuals с X^2: {np.corrcoef(residuals_after.squeeze(), X.squeeze()**2)[0, 1]:.4f}")
print(f"Это означает, что линейная модель не кухнула нелинейность")
Проблема 4: Слишком быстрая сходимость → переобучение
from sklearn.model_selection import cross_val_score
# Сравниваем скорость сходимости
np.random.seed(42)
X_train = np.random.randn(500, 10)
y_train = 5 * X_train[:, 0] - 3 * X_train[:, 1] + np.random.randn(500) * 0.5
# GB с деревьями
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.tree import DecisionTreeRegressor
train_scores_tree = []
test_scores_tree = []
for n_estimators in [1, 5, 10, 20, 50, 100]:
gb = GradientBoostingRegressor(
estimator=DecisionTreeRegressor(max_depth=3),
n_estimators=n_estimators,
random_state=42
)
train_score = gb.score(X_train, y_train)
test_score = cross_val_score(gb, X_train, y_train, cv=5).mean()
train_scores_tree.append(train_score)
test_scores_tree.append(test_score)
print(f"GB с деревьями:")
print(f" Train R2: {train_scores_tree}")
print(f" Test R2: {test_scores_tree}")
# GB с линейной регрессией
train_scores_lr = []
test_scores_lr = []
for n_estimators in [1, 5, 10, 20, 50, 100]:
lgb = LinearGradientBoosting(n_estimators=n_estimators, learning_rate=0.01)
lgb.fit(X_train, y_train)
train_score = lgb.score(X_train, y_train)
test_score = cross_val_score(lgb, X_train, y_train, cv=5).mean()
train_scores_lr.append(train_score)
test_scores_lr.append(test_score)
print(f"GB с линейной регрессией:")
print(f" Train R2: {train_scores_lr}")
print(f" Test R2: {test_scores_lr}")
# Результат: GB с деревьями показывает хороший баланс
# GB с линейной регрессией переобучается на линейных данных
Когда это может сработать?
Есть несколько ситуаций, где линейная база может быть полезна:
# 1. ОЧЕНЬ линейные данные с высокой размерностью
# где деревья страдают от курса размерности
# 2. Вместо просто LinearRegression использовать Ridge/Lasso
from sklearn.linear_model import Ridge
class RidgeGradientBoosting:
def __init__(self, n_estimators=100, learning_rate=0.1, alpha=0.1):
self.n_estimators = n_estimators
self.learning_rate = learning_rate
self.alpha = alpha
self.models = []
self.initial_pred = None
def fit(self, X, y):
self.initial_pred = np.mean(y)
current_pred = np.full_like(y, self.initial_pred, dtype=float)
for i in range(self.n_estimators):
residuals = y - current_pred
# Ridge регрессия вместо обычной линейной
model = Ridge(alpha=self.alpha) # Регуляризация помогает!
model.fit(X, residuals)
self.models.append(model)
current_pred += self.learning_rate * model.predict(X)
return self
def predict(self, X):
pred = np.full(len(X), self.initial_pred)
for model in self.models:
pred += self.learning_rate * model.predict(X)
return pred
# Ridge помогает избежать переобучения
rgb = RidgeGradientBoosting(n_estimators=50, learning_rate=0.1, alpha=1.0)
rgb.fit(X_train, y_train)
Сравнение: дерево vs линейная модель как база
import pandas as pd
comparison = pd.DataFrame({
'Характеристика': [
'Нелинейность',
'Скорость сходимости',
'Переобучение',
'Интерпретируемость',
'Вычислительная сложность',
'Устойчивость к шуму'
],
'Дерево (стандарт)': [
'Хорошо',
'Медленная (хорошо)',
'Среднее',
'Низкая',
'O(n log n)',
'Хорошая'
],
'Линейная регрессия': [
'Плохо',
'Быстрая (плохо)',
'Высокое',
'Высокая',
'O(n^2)',
'Среднее'
]
})
print(comparison.to_string(index=False))
Что действительно используется в реальных системах
# 1. XGBoost, LightGBM, CatBoost
# Используют только деревья, но с оптимизациями
# 2. SkGBDT (Scikit-learn)
from sklearn.ensemble import HistGradientBoostingRegressor
# Также использует деревья
# 3. Stacking и blending
# Можно комбинировать различные базовые модели,
# но это отличается от бустинга
from sklearn.ensemble import StackingRegressor
stacking = StackingRegressor(
estimators=[
('tree', DecisionTreeRegressor()),
('linear', LinearRegression())
],
final_estimator=Ridge()
)
# Stacking сильнее, чем бустинг с линейной базой
Практический вывод
Почему деревья лучше как база для бустинга:
- Нелинейность — деревья захватывают сложные закономерности
- Адаптивность — на каждой итерации фокусируются на разных частях пространства признаков
- Стабильность — деревья меньше страдают от мультиколлинеарности
- Эмпирическое доказательство — все успешные бустинг-системы (XGBoost, LightGBM, CatBoost) используют деревья
Если всё же нужна линейная модель:
- Используй Ridge/Lasso бустинг с регуляризацией
- Убедись, что данные очень линейные
- Рассмотри stacking вместо бустинга
- Уменьши learning_rate еще сильнее, чтобы избежать переобучения