Кафедра ШІзики

Ансамблеві методи - Коли багато моделей перемагають одну

Кафедра ШІзики

Автор

35 хвилин

Час читання

12.01.2025

Дата публікації

Рівень:
Середній
Теги: #ансамбль #random-forest #gradient-boosting #xgboost #бегінг #бустинг

Ансамблеві методи: Коли багато моделей перемагають одну

Божевільна ідея племінника

В кінці Статті 8 племінник Марії мав шалену ідею:

“Тітко, а що якби замість ОДНОГО дерева ми натренували СОТНІ дерев і дали їм голосувати за прогноз?”

“Сотні? Хіба це не призведе до ще більшого перенавчання?”

“В тому й краса — це насправді зменшує перенавчання! Це називається Random Forest.”

Марія скептично подивилася. Її єдине дерево рішень спричинило катастрофу понеділка. Як БІЛЬШЕ дерев може бути краще?

Сьогодні ви відкриєте контрінтуїтивну істину: об’єднання багатьох слабких моделей створює сильний, надійний предиктор. Це сила ансамблевих методів — і вона ось-ось революціонізує піцерію Мами ML.


🤔 Мудрість натовпу

Перш ніж зануритися в код, давайте зрозуміємо ЧОМУ ансамблі працюють.

Аналогія з телевікториною

Уявіть, що ви на телевікторині зі складним питанням. У вас є дві підказки:

Варіант А: Запитати ОДНОГО експерта, який на 90% впевнений Варіант Б: Запитати 100 випадкових глядачів і взяти голосування більшості

Який би ви обрали?

Дивовижно, але Варіант Б часто виграє — навіть коли окремі глядачі точні лише на 60%! Це ефект мудрості натовпу.

Чому? Тому що індивідуальні помилки мають тенденцію компенсуватися. Якщо 60 людей вгадали правильно, а 40 помилилися, більшість (60%) все одно дає правильну відповідь. І з більшою кількістю людей точність покращується.

Від натовпу до дерев

Той самий принцип працює з деревами рішень:

Одне деревоЛіс дерев
1 “експерт”100 “голосуючих”
Перенавчається на тренувальних данихПомилки компенсуються
Нестабільне (змінюється з даними)Стабільне (усереднені прогнози)
Висока дисперсіяНизька дисперсія

Відкриття Марії: “Отже, кожне дерево може бути недосконалим, але разом вони геніальні?”

Саме так!


🎲 Бегінг: Фундамент

Бегінг (Bootstrap AGGregating) — найпростіша ансамблева техніка:

  1. Створіть багато випадкових підмножин тренувальних даних (з повторенням)
  2. Натренуйте одну модель на кожній підмножині
  3. Об’єднайте прогнози (голосування для класифікації, середнє для регресії)

Чому бегінг працює

import numpy as np

# Симуляція ефекту бегінгу
np.random.seed(42)

def single_tree_accuracy():
    """Одне дерево з 70% точністю"""
    return np.random.random() < 0.70

def forest_accuracy(n_trees=100):
    """Ліс: голосування більшості n_trees"""
    votes = [single_tree_accuracy() for _ in range(n_trees)]
    return sum(votes) / n_trees > 0.5

# Порівняння
single_results = [single_tree_accuracy() for _ in range(1000)]
forest_results = [forest_accuracy(100) for _ in range(1000)]

print(f"🌳 Точність одного дерева: {sum(single_results)/len(single_results):.1%}")
print(f"🌲 Точність лісу (100 дерев): {sum(forest_results)/len(forest_results):.1%}")

Результат:

🌳 Точність одного дерева: 71.2%
🌲 Точність лісу (100 дерев): 99.7%

Магія! Окремі дерева точні лише на 70%, але ліс досягає 99.7% точності через голосування!

Секрет різноманітності

Щоб бегінг працював, дерева мають робити різні помилки. Якщо всі дерева помиляються однаково, голосування не допоможе.

Як забезпечити різноманітність?

  1. Різні тренувальні дані: Кожне дерево бачить випадкову bootstrap вибірку
  2. Різні ознаки: Кожне розділення розглядає лише випадкову підмножину ознак

Ця комбінація робить Random Forest таким потужним.


🌲 Random Forest: Потужний ансамбль

Random Forest поєднує бегінг з рандомізацією ознак:

from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
import numpy as np
import pandas as pd

# Дані піцерії Марії
np.random.seed(42)
n = 500

df = pd.DataFrame({
    'temperature': np.random.uniform(5, 35, n),
    'is_weekend': np.random.choice([0, 1], n, p=[0.7, 0.3]),
    'is_holiday': np.random.choice([0, 1], n, p=[0.95, 0.05]),
    'hour': np.random.choice(range(10, 23), n),
})
df['is_peak'] = ((df['hour'] >= 18) & (df['hour'] <= 21)).astype(int)

df['sales'] = (
    80 - 0.8 * df['temperature'] +
    25 * df['is_weekend'] +
    45 * df['is_holiday'] +
    15 * df['is_peak'] +
    np.random.normal(0, 12, n)
).clip(30, 200)

X = df[['temperature', 'is_weekend', 'is_holiday', 'hour', 'is_peak']]
y = df['sales']

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Тренуємо Random Forest
forest = RandomForestRegressor(
    n_estimators=100,      # Кількість дерев
    max_depth=5,           # Обмеження глибини
    max_features='sqrt',   # Випадковий вибір ознак
    random_state=42
)
forest.fit(X_train, y_train)

# Оцінка
train_rmse = np.sqrt(mean_squared_error(y_train, forest.predict(X_train)))
test_rmse = np.sqrt(mean_squared_error(y_test, forest.predict(X_test)))

print(f"🌲 Результати Random Forest:")
print(f"   Train RMSE: {train_rmse:.2f} піц")
print(f"   Test RMSE:  {test_rmse:.2f} піц")
print(f"   Розрив:     {test_rmse - train_rmse:.2f} піц")

Результат:

🌲 Результати Random Forest:
   Train RMSE: 9.45 піц
   Test RMSE:  10.23 піц
   Розрив:     0.78 піц

Чудово! Розрив мінімальний — ліс добре узагальнює без перенавчання.

Інтерактивна демонстрація: Спостерігайте за голосуванням дерев

Подивіться, як окремі дерева роблять прогнози та як їхні голоси об’єднуються:

Ensemble Voting Demo / Демо голосування ансамблю

Ensemble Prediction
-
🌳 Individual Accuracy ~70%
🌲 Ensemble Accuracy -
📈 Improvement -

Ключові гіперпараметри

ПараметрЩо робитьТипові значення
n_estimatorsКількість дерев100-500
max_depthОбмеження глибини дерева5-15 (None = без обмежень)
max_featuresОзнаки на розділення’sqrt’ або ‘log2’
min_samples_leafМін. зразків у листку1-10
bootstrapВикористовувати bootstrapTrue (за замовчуванням)

Коли використовувати Random Forest

✅ Відмінно підходить для:

  • Табличних даних зі змішаними типами ознак
  • Коли потрібна важливість ознак
  • Коли важлива інтерпретованість (до певної міри)
  • Швидке прототипування з хорошою продуктивністю

❌ Менш ідеально для:

  • Дуже високовимірних даних (>10,000 ознак)
  • Коли потрібна максимальна точність (спробуйте бустинг)
  • Середовищ з обмеженою пам’яттю

📊 Важливість ознак: Що має найбільше значення?

Одна суперсила Random Forest: розуміння, які ознаки керують прогнозами.

# Отримуємо важливість ознак
importance = pd.DataFrame({
    'feature': X.columns,
    'importance': forest.feature_importances_
}).sort_values('importance', ascending=False)

print("\n🔍 Важливість ознак для піцерії Марії:")
print("=" * 50)
for idx, row in importance.iterrows():
    bar = '█' * int(row['importance'] * 40)
    print(f"{row['feature']:15s} {row['importance']:.3f} {bar}")

Результат:

🔍 Важливість ознак для піцерії Марії:
==================================================
temperature     0.425 █████████████████
is_weekend      0.245 █████████
is_peak         0.185 ███████
is_holiday      0.095 ███
hour            0.050 ██

Інсайт Марії: “Температура — найбільший фактор! Я маю зосередити маркетинг на холодних днях!”


🚀 Бустинг: Навчання на помилках

Поки бегінг будує дерева незалежно, бустинг будує їх послідовно — кожне дерево вчиться на помилках попередніх.

Інтуїція бустингу

Уявіть, що Марія тренує свій персонал:

Раунд 1: Тренуємо всіх на всіх замовленнях

  • Персонал має труднощі з п’ятничними вечірніми замовленнями

Раунд 2: Фокусуємо тренування на п’ятничних вечорах

  • Персонал покращується в п’ятниці, але має труднощі зі святами

Раунд 3: Фокусуємо тренування на святкових замовленнях

  • Персонал опановує свята

Результат: Кожен раунд націлений на найслабші місця!

Саме так працює бустинг:

Boosting: Learning from Mistakes / Бустинг: Навчання на помилках

📊 Current Error 100%
🌳 Trees Used 0
🎯 Focus Area All data

Click the steps above to see how boosting progressively improves predictions by focusing on mistakes.

Residual Errors (what each tree learns)

Gradient Boosting: Математичний погляд

Gradient Boosting будує дерева для прогнозування залишків (помилок) попередніх дерев:

from sklearn.ensemble import GradientBoostingRegressor

# Тренуємо Gradient Boosting
gb = GradientBoostingRegressor(
    n_estimators=100,       # Кількість етапів бустингу
    learning_rate=0.1,      # Розмір кроку
    max_depth=3,            # Неглибокі дерева (типово для бустингу)
    random_state=42
)
gb.fit(X_train, y_train)

train_rmse = np.sqrt(mean_squared_error(y_train, gb.predict(X_train)))
test_rmse = np.sqrt(mean_squared_error(y_test, gb.predict(X_test)))

print(f"📈 Результати Gradient Boosting:")
print(f"   Train RMSE: {train_rmse:.2f} піц")
print(f"   Test RMSE:  {test_rmse:.2f} піц")

Результат:

📈 Результати Gradient Boosting:
   Train RMSE: 8.12 піц
   Test RMSE:  9.89 піц

Як працює Gradient Boosting

  1. Початковий прогноз: Починаємо із середнього (або простої моделі)
  2. Обчислюємо залишки: Рахуємо помилки для кожного зразка
  3. Навчаємо дерево на залишках: Тренуємо дерево прогнозувати ці помилки
  4. Оновлюємо прогнози: Додаємо масштабовані прогнози дерева
  5. Повторюємо: Продовжуємо додавати дерева, що виправляють залишкові помилки

⚡ XGBoost: Переможець змагань

XGBoost (eXtreme Gradient Boosting) — оптимізована версія gradient boosting, що домінує на Kaggle змаганнях.

Чому XGBoost?

  1. Швидкість: Паралелізована побудова дерев
  2. Регуляризація: Вбудовані L1/L2 для запобігання перенавчанню
  3. Пропущені значення: Обробляє їх автоматично
  4. Масштабованість: Працює на величезних датасетах
# Встановлення: pip install xgboost
import xgboost as xgb

# Тренуємо XGBoost
xgb_model = xgb.XGBRegressor(
    n_estimators=100,
    learning_rate=0.1,
    max_depth=4,
    subsample=0.8,          # Випадкова вибірка рядків
    colsample_bytree=0.8,   # Випадкова вибірка колонок
    reg_alpha=0.1,          # L1 регуляризація
    reg_lambda=1.0,         # L2 регуляризація
    random_state=42
)
xgb_model.fit(X_train, y_train)

train_rmse = np.sqrt(mean_squared_error(y_train, xgb_model.predict(X_train)))
test_rmse = np.sqrt(mean_squared_error(y_test, xgb_model.predict(X_test)))

print(f"⚡ Результати XGBoost:")
print(f"   Train RMSE: {train_rmse:.2f} піц")
print(f"   Test RMSE:  {test_rmse:.2f} піц")

Результат:

⚡ Результати XGBoost:
   Train RMSE: 8.56 піц
   Test RMSE:  9.45 піц

Ключові параметри XGBoost

ПараметрПризначенняТипові значення
n_estimatorsКількість дерев100-1000
learning_rateРозмір кроку (eta)0.01-0.3
max_depthГлибина дерева3-10
subsampleЧастка рядків0.6-1.0
colsample_bytreeЧастка колонок0.6-1.0
reg_alphaL1 регуляризація0-1
reg_lambdaL2 регуляризація1-10

🆚 Порівняння всіх методів

Подивімося, як усі методи працюють на даних Марії:

from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
import xgboost as xgb

models = {
    '🌳 Одне дерево': DecisionTreeRegressor(max_depth=5, random_state=42),
    '🌲 Random Forest': RandomForestRegressor(n_estimators=100, max_depth=5, random_state=42),
    '📈 Gradient Boosting': GradientBoostingRegressor(n_estimators=100, learning_rate=0.1, max_depth=3, random_state=42),
    '⚡ XGBoost': xgb.XGBRegressor(n_estimators=100, learning_rate=0.1, max_depth=4, random_state=42),
}

print("\n🍕 АНСАМБЛЕВЕ ЗМАГАННЯ МАРІЇ")
print("=" * 75)
print(f"{'Метод':<25} {'Train RMSE':>12} {'Test RMSE':>12} {'Розрив':>10} {'Вердикт':>12}")
print("-" * 75)

for name, model in models.items():
    model.fit(X_train, y_train)
    train_rmse = np.sqrt(mean_squared_error(y_train, model.predict(X_train)))
    test_rmse = np.sqrt(mean_squared_error(y_test, model.predict(X_test)))
    gap = test_rmse - train_rmse

    if test_rmse < 10:
        verdict = "Відмінно"
    elif test_rmse < 12:
        verdict = "Добре"
    else:
        verdict = "Нормально"

    print(f"{name:<25} {train_rmse:>10.2f} {test_rmse:>12.2f} {gap:>10.2f} {verdict:>12}")

Результат:

🍕 АНСАМБЛЕВЕ ЗМАГАННЯ МАРІЇ
===========================================================================
Метод                      Train RMSE    Test RMSE      Розрив      Вердикт
---------------------------------------------------------------------------
🌳 Одне дерево                 10.23        11.89       1.66        Добре
🌲 Random Forest                9.45        10.23       0.78     Відмінно
📈 Gradient Boosting            8.12         9.89       1.77     Відмінно
⚡ XGBoost                      8.56         9.45       0.89     Відмінно

Результати:

  • Одне дерево: Непогано, але найвища тестова помилка
  • Random Forest: Дуже стабільний (найменший розрив), чудовий вибір за замовчуванням
  • Gradient Boosting: Найнижча тестова помилка, трохи більший розрив
  • XGBoost: Найкращий баланс точності та стабільності

🎯 Коли що використовувати?

Ось схема прийняття рішень від племінника Марії:

Обирайте Random Forest, коли:

  • Вам потрібен швидкий, надійний baseline
  • Важлива інтерпретованість та важливість ознак
  • У вас обмежений час на тюнінг
  • Дані мають багато нерелевантних ознак

Обирайте Gradient Boosting/XGBoost, коли:

  • Вам потрібна максимальна точність прогнозів
  • Є час для налаштування гіперпараметрів
  • Ви на змаганні (Kaggle тощо)
  • Дані добре підготовлені з релевантними ознаками

Дерево рішень для ансамблів 🌳

Це Kaggle змагання?
├── Так → XGBoost або LightGBM
└── Ні → Потрібні швидкі результати?
    ├── Так → Random Forest (вибір за замовчуванням)
    └── Ні → Скільки часу на тюнінг?
        ├── Обмежено → Random Forest з базовим тюнінгом
        └── Достатньо → Спробуйте все, оберіть найкраще на валідації

🛠️ Практичні поради від племінника Марії

Порада 1: Завжди починайте просто

# Почніть з Random Forest, параметри за замовчуванням
baseline = RandomForestRegressor(n_estimators=100, random_state=42)
baseline.fit(X_train, y_train)
baseline_score = np.sqrt(mean_squared_error(y_test, baseline.predict(X_test)))
print(f"Baseline RMSE: {baseline_score:.2f}")

# Оптимізуйте тільки якщо потрібно!

Порада 2: Використовуйте ранню зупинку для бустингу

# Запобігайте перенавчанню з ранньою зупинкою
xgb_model = xgb.XGBRegressor(
    n_estimators=1000,  # Велике число
    learning_rate=0.1,
    early_stopping_rounds=10,  # Зупинка якщо немає покращення
    random_state=42
)

xgb_model.fit(
    X_train, y_train,
    eval_set=[(X_test, y_test)],
    verbose=False
)

print(f"Найкраща ітерація: {xgb_model.best_iteration}")

Порада 3: Крос-валідація перед фінальним вибором

from sklearn.model_selection import cross_val_score

models = {
    'Random Forest': RandomForestRegressor(n_estimators=100, random_state=42),
    'XGBoost': xgb.XGBRegressor(n_estimators=100, learning_rate=0.1, random_state=42),
}

print("\n📊 Результати крос-валідації (5-fold):")
for name, model in models.items():
    scores = cross_val_score(model, X_train, y_train, cv=5, scoring='neg_root_mean_squared_error')
    print(f"   {name}: {-scores.mean():.2f} ± {scores.std():.2f}")

Порада 4: Налаштовуйте learning_rate та n_estimators разом

Для бустингу ці параметри взаємозалежні:

# Низький learning rate + більше дерев = схоже на високий learning rate + менше дерев
# Але низький learning rate + більше дерев зазвичай працює краще

configs = [
    (0.3, 100),   # Швидко, може перенавчитися
    (0.1, 300),   # Збалансовано
    (0.05, 600),  # Повільно, часто найкраще
]

for lr, n_est in configs:
    model = xgb.XGBRegressor(learning_rate=lr, n_estimators=n_est, max_depth=4, random_state=42)
    model.fit(X_train, y_train)
    score = np.sqrt(mean_squared_error(y_test, model.predict(X_test)))
    print(f"lr={lr}, n_est={n_est}: RMSE = {score:.2f}")

🎯 Фінальна модель Марії

Після всіх експериментів, ось продакшн-готовий ансамбль Марії:

# Продакшн модель Марії
final_model = xgb.XGBRegressor(
    n_estimators=200,
    learning_rate=0.08,
    max_depth=4,
    subsample=0.8,
    colsample_bytree=0.8,
    reg_alpha=0.1,
    reg_lambda=1.0,
    random_state=42
)

final_model.fit(X_train, y_train)

# Фінальна оцінка
train_rmse = np.sqrt(mean_squared_error(y_train, final_model.predict(X_train)))
test_rmse = np.sqrt(mean_squared_error(y_test, final_model.predict(X_test)))

print("\n🍕 ФІНАЛЬНА МОДЕЛЬ МАРІЇ")
print("=" * 50)
print(f"Train RMSE: {train_rmse:.2f} піц")
print(f"Test RMSE:  {test_rmse:.2f} піц")
print(f"Розрив:     {test_rmse - train_rmse:.2f} піц")
print(f"\n💰 Бізнес-вплив:")
print(f"   Помилка старого дерева: ±28 піц")
print(f"   Помилка нового ансамблю: ±{test_rmse:.0f} піц")
print(f"   Покращення: {(28-test_rmse)/28*100:.0f}% зменшення відходів!")

Результат:

🍕 ФІНАЛЬНА МОДЕЛЬ МАРІЇ
==================================================
Train RMSE: 8.34 піц
Test RMSE:  9.12 піц
Розрив:     0.78 піц

💰 Бізнес-вплив:
   Помилка старого дерева: ±28 піц
   Помилка нового ансамблю: ±9 піц
   Покращення: 68% зменшення відходів!

🎓 Ключові висновки

Підсумуймо, що Марія (і ви) дізналися сьогодні:

1. Ансамблі перемагають одиночні моделі

  • Бегінг: Тренуємо незалежні моделі на випадкових підмножинах, усереднюємо результати
  • Бустинг: Тренуємо послідовні моделі, кожна виправляє попередні помилки
  • Ключовий інсайт: Різноманітні помилки компенсуються

2. Random Forest = Бегінг + Рандомізація ознак

  • Швидкий, надійний, інтерпретований
  • Чудовий вибір за замовчуванням для більшості задач
  • Важливість ознак показує, що має значення

3. Gradient Boosting = Послідовна корекція помилок

  • Часто найвища точність
  • Потребує ретельного налаштування
  • XGBoost/LightGBM — оптимізовані реалізації

4. Вибір правильного методу

  • Швидкий baseline: Random Forest
  • Максимальна точність: XGBoost з тюнінгом
  • Великі датасети: LightGBM
  • Завжди: Крос-валідація перед прийняттям рішення!

5. Рекомендації з гіперпараметрів

  • Random Forest: Починайте зі 100 дерев, налаштуйте max_depth
  • Бустинг: Низький learning rate (0.05-0.1) + більше дерев
  • Обидва: Використовуйте ранню зупинку для запобігання перенавчанню

🚀 Що далі?

Піцерія Марії тепер працює гладко з її ансамблевою моделлю. Але її племінник має ще одну ідею:

“Тітко, ми використовували ознаки за замовчуванням. А що якби ми СКОНСТРУЮВАЛИ нові ознаки — як ‘днів з останнього свята’ або ‘точність прогнозу погоди’?”

“Це може зробити модель ще кращою?”

“Абсолютно! Це називається feature engineering, і часто це важливіше за сам алгоритм.”

У Статті 10: Feature Engineering ви дізнаєтеся, як створювати потужні ознаки, що перетворюють хороші моделі на чудові. Піцерія Марії скоро отримає найбільше оновлення!


🛠️ Практичне завдання

Побудуйте ансамблевий предиктор відтоку клієнтів:

Ознаки датасету:

  • tenure — місяці як клієнт (1-72)
  • monthly_charges — сума рахунку (2020-120)
  • total_charges — загальні витрати за час
  • contract_type — щомісячний, 1-рік, 2-роки
  • support_calls — кількість звернень у підтримку

Ваша місія:

  1. Натренуйте baseline дерево рішень
  2. Побудуйте Random Forest зі 100 дерев
  3. Побудуйте XGBoost модель з ранньою зупинкою
  4. Порівняйте всі три за допомогою крос-валідації
  5. Визначте топ-3 найважливіші ознаки

Критерії успіху:

  • Досягти AUC > 0.80 на тестовому наборі
  • XGBoost має перемогти Random Forest щонайменше на 1%

Ви навчилися використовувати силу багатьох моделей. Тепер ідіть будувати ліси! 🌲

Успіхів! 🎯