ЛР3
Градиентный бустинг и синус

Задание 3.1 и 3.2 · 3 балла за класс

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

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

Бустинг
Последовательность моделей, каждая исправляет ошибки суммы предыдущих.
Пенёк, неглубокое дерево
Дерево глубины 1-3. Само по себе слабое, зато не переобучается.
f₀
Начальный прогноз: одна константа для всех, у нас среднее y.
Остатки r
Антиградиент потерь. Для MSE это 2(y - f): удвоенная текущая ошибка.
ρ (ро)
Длина шага: с каким множителем прибавить новое дерево.
На пальцах. Художник рисует портрет. Первый слой: просто пятно среднего цвета. Каждый следующий слой исправляет, где сейчас не так: тут темнее, там светлее. Каждый слой грубый (пенёк), но вместе выходит точный портрет.

Алгоритм

1. f = f₀ = среднее y 2. повторить M раз: r = -градиент(y, f) (для MSE: 2(y - f)) h = дерево, обученное предсказывать r по x ρ = лучшее из [-1, -0.99, ..., 1] по потерям L(y, f + ρ·h) (или ρ = 1) f = f + ρ·h 3. прогноз = f₀ + ρ₁h₁ + ρ₂h₂ + ... + ρ_M h_M

Код

class GradientTreeBoosting:
    def __init__(self, loss='MSE', iterations=10, const_p=False, **tree_kwargs):
        self.loss, self.loss_gradient = losses[loss]
        self.iterations = iterations
        self.const_p = const_p
        self.tree_kwargs = tree_kwargs
        self.trees = []
        self.rhos = []
        self.train_loss = []
  1. losses[loss] из словаря берём пару (функция потерь, её градиент) по названию 'MSE', 'MAE' или 'lq'.
  2. const_p True: ρ всегда 1. False: ρ подбирается каждый шаг.
  3. **tree_kwargs всё остальное (max_depth=3, random_state=42) уходит в DecisionTreeRegressor.
  4. trees, rhos список обученных деревьев и их шагов, это и есть модель. train_loss история ошибки для графика.
    def fit(self, X, y):
        y = np.asarray(y, dtype=float)
        self.f0 = np.mean(y)
        f = np.full(len(y), self.f0)
        self.trees, self.rhos, self.train_loss = [], [], []
        rho_grid = np.linspace(-1, 1, 201)
        for _ in range(self.iterations):
            residuals = -self.loss_gradient(y, f)
            tree = DecisionTreeRegressor(**self.tree_kwargs).fit(X, residuals)
            h = tree.predict(X)
            if self.const_p:
                rho = 1.0
            else:
                rho = rho_grid[np.argmin(self.loss(y, f + rho_grid[:, None] * h))]
            f = f + rho * h
            self.trees.append(tree)
            self.rhos.append(rho)
            self.train_loss.append(self.loss(y, f))
        return self
  1. np.full(len(y), self.f0) массив, где у всех объектов прогноз = среднее.
  2. np.linspace(-1, 1, 201) 201 кандидат ρ с шагом 0.01: условие просит брать ρ из чисел от -1 до 1.
  3. residuals = -self.loss_gradient(y, f) антиградиент: куда двигать прогноз каждого объекта.
  4. DecisionTreeRegressor(...).fit(X, residuals) дерево учится предсказывать не y, а остатки.
  5. rho_grid[:, None] * h матрица 201 × n: строка k это прогноз f + ρₖ·h. self.loss(...) благодаря axis=-1 даёт 201 число, np.argmin номер лучшего. Всё без цикла по ρ.
  6. f = f + rho * h обновляем прогноз.
    def predict(self, X):
        f = np.full(len(X), self.f0)
        for tree, rho in zip(self.trees, self.rhos):
            f = f + rho * tree.predict(X)
        return f
  1. Начинаем с константы и по очереди прибавляем вклад каждого дерева, как требует условие («последовательно применить»).

Синус

Точки синуса с шумом, прогноз бустинга и одного дерева
Синие точки: данные. Зелёные ступени: одно дерево глубины 3 (только 8 разных значений). Оранжевые: бустинг из 10 таких деревьев, почти точно идёт по синусу.
МодельMSE
одно дерево глубины 30.236
бустинг, ρ подбирается0.026
бустинг, ρ = 10.524 (хуже одного дерева!)

Подбор ρ каждый раз выбрал ρ = 0.5. Ошибка 0.026 близка к шуму: 0.2² = 0.04, лучше на этих точках почти нельзя.

Попробуй руками

Тот же бустинг деревьями глубины 3 на синусе. Нажимай «Шаг» и смотри, как жёлтая линия приближается к синусу. Потом переключи режим на «ρ = 1» и сравни: линия начинает метаться.

Нижний график: текущая ошибка y - f каждой точки (оранжевые) и то, что выучит следующее дерево (зелёная ступенчатая линия). С каждым шагом ошибки прижимаются к нулю.

На чём учится каждое новое дерево бустинга?

Вопросы на защите

Почему деревья в бустинге неглубокие?

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

Чем бустинг отличается от леса?

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

Где цикл по ρ?

Его нет: потери для всех 201 значений ρ считаются одной матричной операцией, потом argmin.

Почему ρ всегда 0.5?

Дерево учится на 2(y - f), то есть на удвоенной ошибке. Чтобы исправить ошибку ровно, нужно взять половину: ρ = 0.5.

Одна фразаБустинг начинает со среднего и добавляет деревья, обученные на антиградиенте; шаг ρ выбирается из [-1, 1] одной матричной операцией; на синусе 10 деревьев дают MSE 0.026 против 0.236 у одного дерева.