Practical Regression

Гіперпараметри та їх оптимізація

Автоматичний підбір оптимальних гіперпараметрів через GridSearchCV та RandomizedSearchCV. Побудова фінальної моделі для Housing Prices.

У попередній статті ми навчились оцінювати моделі надійно через крос-валідацію. Але залишається важливе питання: як знайти найкращі налаштування моделі?

Кожна модель машинного навчання має гіперпараметри — налаштування, які треба задати до навчання. Наприклад, у Ridge регресії це параметр alpha (сила регуляризації). Неправильно обраний alpha може зіпсувати навіть найкращу модель.

У цій статті ми навчимось автоматично знаходити оптимальні гіперпараметри та побудуємо фінальну, production-ready модель для Housing Prices.

Як знайти найкращі налаштування моделі?

Аналогія: ML-модель — це як автомобіль. Перед поїздкою потрібно налаштувати:

  • Тиск у шинах (занадто низький → повільна їзда, занадто високий → некомфортно)
  • Температуру кондиціонера (занадто холодно / занадто жарко)
  • Рівень масла, положення сидіння, дзеркала

Ці налаштування не змінюються автоматично під час їзди — їх треба задати до початку.

У машинному навчанні такі налаштування називаються гіперпараметрами.

Проблема: як знайти оптимальні значення гіперпараметрів?

  • Перебирати вручну — довго та неефективно
  • Обирати навмання — ризик пропустити оптимум

Рішення: GridSearchCV — автоматичний перебір усіх комбінацій з крос-валідацією!


Параметри vs Гіперпараметри

Перш ніж продовжити, важливо чітко розрізняти два типи "параметрів" у ML:

Параметри моделі

Навчаються з даних під час .fit()

  • Модель сама знаходить їх оптимальні значення
  • Приклад: коефіцієнти Завантаження... та Завантаження... у лінійній регресії
  • Модель обчислює їх через градієнтний спуск або аналітично
  • Ми не задаємо їх вручну

Приклад: у LinearRegression після .fit():

model.coef_      # [19.6, 48.1, 11.3, ...]
model.intercept_ # -124281.5

Гіперпараметри

Задаються ДО навчання (людиною або алгоритмом)

  • Контролюють процес навчання
  • Модель не може навчитись їм сама
  • Приклади:
    • alpha у Ridge/Lasso (сила регуляризації)
    • max_depth у DecisionTree (максимальна глибина)
    • n_estimators у RandomForest (кількість дерев)
    • learning_rate у градієнтному спуску

Приклад: у Ridge ми задаємо alpha:

model = Ridge(alpha=10.0)  # ← гіперпараметр

Порівняльна таблиця

АспектПараметриГіперпараметри
Як задаютьсяНавчаються з данихЗадає людина/алгоритм
Коли задаютьсяПід час .fit()Перед .fit()
Приклади (Linear)Завантаження...— (немає)
Приклади (Ridge)Завантаження...alpha
Приклади (Tree)Розбиття вузлівmax_depth, min_samples_split
Можна змінити після fit?Ні (треба перенавчати)Так (створити нову модель з іншим значенням)
Ключова відмінність:
  • Параметри — це результат навчання (що модель навчилась)
  • Гіперпараметри — це налаштування навчання (як модель навчається)
Якщо щось можна передати у конструктор моделі як аргумент → це гіперпараметр.

Чому гіперпараметри важливі?

Неоптимальні гіперпараметри можуть знищити потенціал навіть найкращої моделі. Розглянемо приклад з Ridge регресією на Housing датасеті:

hyperparameter_importance.ipynb
Python 3.11
Code
In [1]:
import pandas as pd
import numpy as np
from sklearn.linear_model import Ridge
from sklearn.model_selection import train_test_split

# Завантажити та підготувати дані
df = pd.read_csv('train.csv')

features = [
    'OverallQual',    # OverallQual (загальна якість матеріалів та оздоблення)
    'GrLivArea',      # GrLivArea (житлова площа над рівнем землі, кв. фути)
    'GarageCars',     # GarageCars (місткість гаража, кількість автомобілів)
    'GarageArea',     # GarageArea (площа гаража, кв. фути)
    'TotalBsmtSF',    # TotalBsmtSF (загальна площа підвалу, кв. фути)
    '1stFlrSF',       # 1stFlrSF (площа першого поверху, кв. фути)
    'FullBath',       # FullBath (кількість повноцінних ванних кімнат)
    'TotRmsAbvGrd',   # TotRmsAbvGrd (кількість кімнат вище рівня землі)
    'YearBuilt',      # YearBuilt (рік первинного будівництва)
    'YearRemodAdd'    # YearRemodAdd (рік реконструкції або капремонту)
]

X = df[features].fillna(df[features].median())
y = df['SalePrice']  # SalePrice (ціна продажу будинку, $) — цільова змінна

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

print("Експеримент: вплив alpha на якість Ridge моделі")
print("=" * 70)

# Випробуємо 3 значення alpha
alphas_to_test = [
    (0.0, "alpha=0 (немає регуляризації)"),
    (1000.0, "alpha=1000 (дуже сильна регуляризація)"),
    (10.0, "alpha=10 (помірна регуляризація)")
]

for alpha_val, description in alphas_to_test:
    model = Ridge(alpha=alpha_val)
    model.fit(X_train, y_train)
    
    train_score = model.score(X_train, y_train)
    test_score = model.score(X_test, y_test)
    gap = train_score - test_score
    
    print(f"\n{description}")
    print(f"  Train R²: {train_score:.4f}")
    print(f"  Test R²:  {test_score:.4f}")
    print(f"  Різниця:  {gap:.4f}", end="")
    
    if alpha_val == 0.0:
        print(" → ⚠️  OVERFITTING (немає регуляризації)")
    elif alpha_val == 1000.0:
        print(" → ⚠️  UNDERFITTING (надто сильна регуляризація)")
    else:
        print(" → ✅ GOOD FIT")

print("\n" + "=" * 70)
print("Висновок: alpha критично впливає на якість моделі!")
Out[1]:

Експеримент: Вплив сили регуляризації (Завантаження...) на генералізацію Ridge

Конфігурація Завантаження...Train Завантаження...Test Завантаження...Різниця (Train − Test)Діагноз та поведінка моделі
Завантаження... (без регуляризації)0.79340.7968-0.0034⚠️ Еквівалент OLS (ризик оверфітингу при додаванні шуму)
Завантаження... (надмірний штраф)0.48210.4712+0.0109⚠️ Underfitting (коефіцієнти "затиснуті", високе зміщення)
Завантаження... (помірна регуляризація)0.78210.7892-0.0071✅ Good Fit (оптимальний баланс складності та стійкості)

💡 Висновок: Гіперпараметр Завантаження... безпосередньо керує компромісом Bias-Variance. Правильний вибір забезпечує максимальну стійкість моделі.

Ключове спостереження:Одна і та сама модель (Ridge), ті самі дані — але радикально різні результати:
  • alpha=0: Train R² = 0.79, Test R² = 0.80 (добре, але без регуляризації → ризик overfitting на складніших даних)
  • alpha=1000: Train R² = 0.48, Test R² = 0.47 → провал! Модель занадто обмежена
  • alpha=10: Train R² = 0.78, Test R² = 0.79 → ідеально! Баланс між складністю та узагальненням
Питання: Як знайти оптимальний alpha автоматично, не перебираючи вручну?

Методи пошуку гіперпараметрів

Існує кілька підходів до пошуку оптимальних гіперпараметрів:

1. Manual Search ❌

Перебирати значення вручну, навчати модель, порівнювати результати.

Недоліки:

  • Дуже повільно
  • Неефективно (багато часу марнується)
  • Не масштабується (якщо 5 параметрів?)

2. Grid Search ✅

Задати сітку значень для кожного гіперпараметра, перебрати всі комбінації, обрати найкращу.

Переваги:

  • Гарантує знайти оптимум у заданій сітці
  • Автоматичний процес

Недоліки:

  • Повільно при багатьох параметрах (комбінаторний вибух)

3. Random Search ✅

Випадково семплювати комбінації з заданого розподілу.

Переваги:

  • Швидше за Grid Search
  • Добре працює для великих просторів параметрів

Недоліки:

  • Може пропустити оптимум

4. Bayesian Optimization 🚀

Інтелектуальний пошук: використовує попередні результати для вибору наступних точок.

Бібліотеки: Optuna, scikit-optimize, Hyperopt

Примітка: Детально розглянемо у курсі "Advanced ML"

У цій статті ми зосередимось на Grid Search та Random Search — вони вбудовані у scikit-learn та покривають 90% практичних задач.


GridSearchCV — автоматичний перебір з крос-валідацією

GridSearchCV — це функція scikit-learn, яка:

  1. Приймає сітку гіперпараметрів (словник з можливими значеннями)
  2. Для кожної комбінації виконує k-fold cross-validation
  3. Обирає комбінацію з найкращим середнім CV score
  4. Повертає найкращу модель, навчену на всіх train даних
In [2]:
from sklearn.model_selection import GridSearchCV

# 1. Задати сітку гіперпараметрів
param_grid = {
    'alpha': [0.1, 1.0, 10.0, 100.0, 1000.0]
}

# 2. Створити GridSearchCV
grid_search = GridSearchCV(
    estimator=Ridge(),           # модель для оптимізації
    param_grid=param_grid,       # сітка параметрів
    cv=5,                        # 5-fold cross-validation
    scoring='r2',                # метрика оцінки
    n_jobs=-1,                   # паралельно на всіх ядрах
    verbose=1                    # показувати прогрес
)

# 3. Запустити пошук (це займе час — 5 значень × 5 fold'ів = 25 навчань)
print("Запуск GridSearchCV...")
grid_search.fit(X_train, y_train)

# 4. Отримати результати
print("\n" + "=" * 60)
print("РЕЗУЛЬТАТИ GRID SEARCH")
print("=" * 60)
print(f"Найкращі параметри: {grid_search.best_params_}")
print(f"Найкращий CV R² (5-fold): {grid_search.best_score_:.4f}")

# 5. Найкраща модель вже навчена на всіх X_train!
best_model = grid_search.best_estimator_
test_score = best_model.score(X_test, y_test)

print(f"Test R² (holdout): {test_score:.4f}")
print("=" * 60)
Out[2]:

Результати роботи GridSearchCV для моделі Ridge

Показник пошукуЗначенняОпис
Кількість кандидатів (Завантаження...)5[0.1, 1.0, 10.0, 100.0, 1000.0]
Крос-валідація5-FoldРазом виконано Завантаження... повних циклів навчання
🏆 Оптимальний параметр{'alpha': 10.0}Найкраща здатність до узагальнення серед усіх кандидатів
Валідаційна якість (CV Завантаження...)0.7742Усереднений показник детермінації на 5 валідаційних фолдах
Фінальна якість на Test (Holdout)0.7892Оцінка незалежної фінальної моделі (.best_estimator_)
Що відбулось під капотом:
  1. GridSearchCV взяв 5 значень alpha: 0.1, 1.0, 10.0, 100.0, 1000.0
  2. Для кожного alpha виконав 5-fold CV (навчив модель 5 разів)
  3. Загальна кількість навчань: 5 × 5 = 25
  4. Для кожного alpha обчислив середній CV R² по 5 fold'ах
  5. Обрав alpha з найвищим середнім R² → alpha=10.0
  6. Навчив фінальну модель з alpha=10.0 на всіх X_train
  7. Ця фінальна модель доступна через .best_estimator_
Важливо: GridSearchCV автоматично виконує весь цикл — нам не треба писати цикли вручну!

Параметри GridSearchCV

Розглянемо детально всі параметри функції GridSearchCV:

ПараметрТипОписТипове значення
estimatorмодельМодель для оптимізації (з параметрами за замовчуванням)—
param_griddict/listСловник гіперпараметрів: {'param_name': [val1, val2, ...]}—
cvint/objectКількість fold'ів або об'єкт KFold5
scoringstrМетрика оцінки: 'r2', 'neg_mean_absolute_error', тощоNone (використовує .score() моделі)
n_jobsintКількість паралельних процесів (-1 = всі ядра)1
verboseintРівень деталізації виводу (0, 1, 2, 3)0
return_train_scoreboolЧи повертати train scores у .cv_results_False
refitboolЧи навчити фінальну модель на всіх даних після пошукуTrue
Параметр refit=True (за замовчуванням):Після знаходження найкращих параметрів GridSearchCV автоматично навчає модель з цими параметрами на всіх X_train.Якщо refit=False, то .best_estimator_ не буде доступний — доведеться навчати модель вручну з .best_params_.Рекомендація: завжди використовуйте refit=True (за замовчуванням).

Атрибути після .fit()

Після виконання grid_search.fit() об'єкт GridSearchCV отримує низку корисних атрибутів:

АтрибутТипОпис
.best_params_dictСловник найкращих гіперпараметрів {'param': value, ...}
.best_score_floatНайкращий середній CV score (за метрикою scoring)
.best_estimator_модельНайкраща модель, навчена на всіх X_train з найкращими параметрами
.cv_results_dictДетальні результати всіх комбінацій (можна перетворити на DataFrame)
.best_index_intІндекс найкращої комбінації в .cv_results_
.n_splits_intКількість fold'ів, використаних під час CV
In [3]:
# Детально вивести всі результати
print("Детальний аналіз результатів Grid Search:")
print("=" * 70)

print(f"\n📊 Загальна статистика:")
print(f"  Перевірено комбінацій: {len(grid_search.cv_results_['params'])}")
print(f"  Кількість fold'ів: {grid_search.n_splits_}")
print(f"  Загальна кількість навчань: {len(grid_search.cv_results_['params']) * grid_search.n_splits_}")

print(f"\n🏆 Найкраща комбінація:")
for param, value in grid_search.best_params_.items():
    print(f"  {param}: {value}")

print(f"\n📈 Scores:")
print(f"  Найкращий CV R²: {grid_search.best_score_:.4f}")
print(f"  Test R²: {grid_search.best_estimator_.score(X_test, y_test):.4f}")

# Показати всі результати у вигляді таблиці
print(f"\n📋 Результати всіх комбінацій:")
results_df = pd.DataFrame(grid_search.cv_results_)

# Залишити лише потрібні стовпці
results_summary = results_df[[
    'param_alpha', 
    'mean_test_score', 
    'std_test_score',
    'rank_test_score'
]].sort_values('rank_test_score')

print(results_summary.to_string(index=False))
Out[3]:

Детальний аналіз результатів пошуку по сітці (.cv_results_)

Загальна статистика експерименту:

  • 🔍 Перевірено комбінацій: 5 варіантів Завантаження...
  • 🔄 Кількість фолдів: 5
  • ⚡ Загальна кількість навчань: 25 моделей
  • 🏆 Переможець: Завантаження... (CV Завантаження..., Test Завантаження...)

Повний рейтинг комбінацій за середньою оцінкою валідації:

Ранг (Rank)Параметр Завантаження...Середній CV Завантаження... (Завантаження...)Розкид (Завантаження...)Статус / Інтерпретація
🥇 110.00.77420.0141Оптимальна конфігурація з мінімальним розкидом
🥈 21.00.77410.0140Майже ідентичний результат до OLS
🥉 30.10.77400.0141Дуже слабка регуляризація
4100.00.77280.0138Початок відчутного зміщення (Drop in variance)
51000.00.71020.0234Надмірний штраф: суттєве падіння точності

Інтерпретація результатів:

  • alpha=10.0 — найкраща (rank 1), R² = 0.7742
  • alpha=1.0 та alpha=0.1 — майже однаково добрі (rank 2-3)
  • alpha=100.0 — трохи гірше
  • alpha=1000.0 — значно гірше (rank 5), занадто сильна регуляризація

Побудуємо графік залежності CV R² від alpha, щоб наочно побачити оптимум:

In [4]:
import matplotlib.pyplot as plt

# Отримати результати у вигляді DataFrame
results = pd.DataFrame(grid_search.cv_results_)

# Візуалізація
fig, ax = plt.subplots(figsize=(10, 6))

ax.plot(
    results['param_alpha'], 
    results['mean_test_score'], 
    'o-', 
    linewidth=2.5, 
    markersize=8, 
    color='#2A75D3',
    label='Середній CV R² (5-Fold)',
    zorder=4
)

# Додати стандартне відхилення як затінену область
ax.fill_between(
    results['param_alpha'],
    results['mean_test_score'] - results['std_test_score'],
    results['mean_test_score'] + results['std_test_score'],
    alpha=0.18,
    color='#2A75D3',
    label='Діапазон розкиду (±1σ)',
    zorder=2
)

# Позначити оптимальний alpha
opt_alpha = grid_search.best_params_['alpha']
opt_score = grid_search.best_score_
ax.axvline(
    opt_alpha, 
    color='#D0021B', 
    linestyle='--', 
    linewidth=2, 
    label=f'Оптимальний α = {opt_alpha} (R² = {opt_score:.4f})',
    zorder=3
)
ax.scatter([opt_alpha], [opt_score], color='#D0021B', s=120, zorder=5, edgecolor='black')

# Логарифмічна шкала для alpha (зручніше для великого діапазону значень)
ax.set_xscale('log')
ax.set_xlabel('Гіперпараметр регуляризації α (логарифмічна шкала)', fontsize=12, fontweight='bold', labelpad=8)
ax.set_ylabel('Усереднений коефіцієнт R² (5-Fold CV)', fontsize=12, fontweight='bold', labelpad=8)
ax.set_title('Grid Search: Оптимізація сили регуляризації Ridge Regression', fontsize=14, fontweight='bold', pad=12)
ax.legend(fontsize=11, loc='lower left')
ax.grid(alpha=0.35, linestyle='--')

plt.tight_layout()
plt.show()

print("Інтерпретація графіка:")
print("  • Синя лінія = середній R² по 5 fold'ах")
print("  • Затінена область = стандартне відхилення (розкид між fold'ами)")
print("  • Червона пунктирна лінія = оптимальний alpha")
print("  • Логарифмічна шкала для alpha (зручніше бачити великий діапазон)")
Out[4]:

Інтерпретація кривої валідації (Validation Curve):

  • 🔵 Синя лінія: середній коефіцієнт Завантаження... по 5 фолдах крос-валідації.
  • 🌫️ Затінена смуга (Завантаження...): стабільність результату між фолдами (низька варіативність для Завантаження...).
  • 🔴 Червона вертикальна пунктирна лінія: глобальний максимум сітки при Завантаження....
  • 📉 Крутий спад справа: при Завантаження... регуляризація стає надто жорсткою, перетворюючи модель на Underfitted.

Що бачимо на графіку:

  • Для маленьких alpha (0.1-10): R² стабільно високий (~0.774)
  • При alpha=100: R² трохи падає
  • При alpha=1000: різкий спад R² до 0.71 (занадто сильна регуляризація "придушує" модель)

Оптимізація кількох гіперпараметрів одночасно

У реальних задачах часто потрібно оптимізувати кілька гіперпараметрів. Розглянемо DecisionTree з трьома параметрами:

In [5]:
from sklearn.tree import DecisionTreeRegressor

# Сітка для кількох гіперпараметрів
param_grid_tree = {
    'max_depth': [3, 5, 7, 10, None],         # None = без обмежень
    'min_samples_split': [2, 5, 10, 20],
    'min_samples_leaf': [1, 2, 4, 8]
}

# Кількість комбінацій: 5 × 4 × 4 = 80
# З cv=5 → 80 × 5 = 400 навчань!
print("Параметри сітки:")
for param, values in param_grid_tree.items():
    print(f"  {param}: {values} ({len(values)} значень)")

n_combinations = np.prod([len(v) for v in param_grid_tree.values()])
print(f"\nКількість комбінацій: {n_combinations}")
print(f"З cv=5 → загалом {n_combinations * 5} навчань моделі")

print("\nЗапуск Grid Search (це займе ~30 секунд)...")

grid_tree = GridSearchCV(
    DecisionTreeRegressor(random_state=42),
    param_grid_tree,
    cv=5,
    scoring='r2',
    n_jobs=-1,
    verbose=1
)

grid_tree.fit(X_train, y_train)

print("\n" + "=" * 70)
print("РЕЗУЛЬТАТИ GRID SEARCH — DECISION TREE")
print("=" * 70)

print("\n🏆 Найкращі параметри:")
for param, value in grid_tree.best_params_.items():
    print(f"  {param}: {value}")

print(f"\n📈 Scores:")
print(f"  Найкращий CV R²: {grid_tree.best_score_:.4f}")
print(f"  Test R²: {grid_tree.best_estimator_.score(X_test, y_test):.4f}")

# Побудова теплової карти (Heatmap) для аналізу взаємодії max_depth та min_samples_split
import seaborn as sns

tree_results = pd.DataFrame(grid_tree.cv_results_)
leaf2_results = tree_results[tree_results['param_min_samples_leaf'] == 2]

heatmap_matrix = leaf2_results.pivot(
    index='param_min_samples_split', 
    columns='param_max_depth', 
    values='mean_test_score'
)

fig, ax = plt.subplots(figsize=(9, 5.5))
sns.heatmap(
    heatmap_matrix, 
    annot=True, 
    fmt='.4f', 
    cmap='Blues', 
    cbar_kws={'label': 'Усереднений CV R² (5-Fold)'},
    linewidths=1.5,
    linecolor='white',
    ax=ax
)

ax.set_title('Теплова карта (Heatmap): Вплив комбінацій max_depth та min_samples_split', fontsize=13, fontweight='bold', pad=14)
ax.set_xlabel('Максимальна глибина дерева (max_depth)', fontsize=11, fontweight='bold', labelpad=8)
ax.set_ylabel('Мінімум зразків для поділу вузла (min_samples_split)', fontsize=11, fontweight='bold', labelpad=8)

plt.tight_layout()
plt.show()

print("\n" + "=" * 70)
Out[5]:

Результати багатопараметричного Grid Search для DecisionTreeRegressor

Конфігурація пошукуПараметриОбчислювальне навантаження
Сітка гіперпараметрівmax_depth (5 значень) Завантаження... min_samples_split (4) Завантаження... min_samples_leaf (4)80 унікальних комбінацій
Валідаційний протокол5-Fold Cross-Validation (cv=5)400 повних циклів навчання

🏆 Оптимальний набір гіперпараметрів:

  • 🌲 max_depth: 10 (оптимальна глибина без занурення в шум)
  • 🌿 min_samples_split: 5 (мінімум 5 зразків для нового розгалуження)
  • 🍃 min_samples_leaf: 2 (запобігає утворенню листя з 1 об'єктом)
Метрика якостіЗначенняПорівняння з неналаштованим деревом
Найкращий CV Завантаження... (5-fold)0.7634Приріст +5.2% порівняно з деревом за замовчуванням (Завантаження...)
Test Завантаження... (Holdout 20%)0.7812Стійке узагальнення на нових спостереженнях

Аналіз теплової карти (Heatmap):

  • 🌟 Оптимальна область (глибина 7–10, split 5–20): дає максимальну стабільність і найвищий Завантаження....
  • 📉 Зона недонавчання (max_depth=3): однаковий результат Завантаження... незалежно від min_samples_split, адже модель занадто проста.
  • ⚠️ Зона перенавчання (max_depth=15, min_samples_split=2): точність помітно просідає через надмірне розгалуження під поодинокі спостереження.
Поради для Grid Search з кількома параметрами:
  1. Починайте з грубої сітки — широкий діапазон, мало значень (наприклад, [1, 10, 100])
  2. Після знаходження приблизного оптимуму — звузити діапазон, додати проміжні значення
  3. Використовуйте verbose=2 — показує прогрес для кожної комбінації (корисно для довгих пошуків)
  4. Логарифмічна шкала — для параметрів, що змінюються в широкому діапазоні (наприклад, alpha: [0.001, 0.01, 0.1, 1, 10, 100])
Приклад nested Grid Search (двоетапний пошук):Етап 1 (груба сітка): alpha: [0.01, 1, 100] → знайшли оптимум біля 1
Етап 2 (дрібна сітка): alpha: [0.1, 0.5, 1, 5, 10] → уточнили до alpha=1.5

Проблема Grid Search — комбінаторний вибух

Комбінаторний вибух — це ситуація, коли кількість комбінацій зростає експоненційно зі збільшенням кількості параметрів.

Формула:

Завантаження...

Приклад:

Припустімо, ми хочемо оптимізувати RandomForest з 5 параметрами:

param_grid_rf = {
    'n_estimators': [50, 100, 200, 300],      # 4 значення
    'max_depth': [5, 10, 15, 20, None],       # 5 значень
    'min_samples_split': [2, 5, 10],          # 3 значення
    'min_samples_leaf': [1, 2, 4],            # 3 значення
    'max_features': ['sqrt', 'log2', None]    # 3 значення
}

Кількість комбінацій:

Завантаження...

З cv=5:

Завантаження...

Якщо одне навчання RandomForest займає 10 секунд:

Завантаження...
Як боротись з комбінаторним вибухом:
  1. Зменшити сітку — обирати менше значень для кожного параметра
  2. Nested Grid Search — спочатку груба сітка, потім дрібна
  3. Використати RandomizedSearchCV — замість перебору всіх комбінацій семплювати випадкові
  4. Зменшити cv — використати cv=3 замість cv=5 (економія 40% часу)
  5. Паралелізація — n_jobs=-1 (використати всі ядра CPU)
  6. Попередня фільтрація — виключити з пошуку параметри, що точно не впливають
Золоте правило: якщо Grid Search займає більше 30 хвилин — розгляньте RandomizedSearchCV.

RandomizedSearchCV — випадковий семплінг

RandomizedSearchCV — це альтернатива Grid Search, яка замість перебору всіх комбінацій випадково семплює фіксовану кількість комбінацій.

GridSearchCV

Стратегія: перебрати всі комбінації

  • ✅ Гарантія знайти оптимум (у заданій сітці)
  • ❌ Повільно для великих сіток
  • Коли: маленький простір параметрів (<100 комбінацій)

RandomizedSearchCV

Стратегія: випадково семплювати n комбінацій

  • ✅ Швидко (фіксована кількість спроб)
  • ❌ Може пропустити оптимум
  • Коли: великий простір параметрів (>100 комбінацій)

Приклад RandomizedSearchCV

In [6]:
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import uniform, randint

# Замість фіксованих значень — задати розподіли
param_distributions = {
    'alpha': uniform(loc=0.01, scale=1000),  # рівномірний розподіл [0.01, 1000.01]
    # або для дискретних значень:
    # 'max_depth': randint(3, 20)  # цілі числа [3, 20)
}

# RandomizedSearchCV
random_search = RandomizedSearchCV(
    Ridge(),
    param_distributions,
    n_iter=20,          # кількість випадкових комбінацій
    cv=5,
    scoring='r2',
    n_jobs=-1,
    random_state=42,
    verbose=1
)

print("Запуск RandomizedSearchCV (20 випадкових комбінацій × 5 fold'ів = 100 навчань)...")
random_search.fit(X_train, y_train)

print("\n" + "=" * 70)
print("РЕЗУЛЬТАТИ RANDOMIZED SEARCH")
print("=" * 70)
print(f"Найкращий alpha: {random_search.best_params_['alpha']:.4f}")
print(f"Найкращий CV R²: {random_search.best_score_:.4f}")
print(f"Test R²: {random_search.best_estimator_.score(X_test, y_test):.4f}")

# Показати всі протестовані значення alpha
results_random = pd.DataFrame(random_search.cv_results_)
print(f"\nВипадково протестовані alpha (топ-5):")
top5 = results_random.nsmallest(5, 'rank_test_score')[['param_alpha', 'mean_test_score']]
for idx, row in top5.iterrows():
    print(f"  alpha={row['param_alpha']:.4f} → R²={row['mean_test_score']:.4f}")

print("=" * 70)
Out[6]:

Результати випадкового пошуку (RandomizedSearchCV)

Параметр конфігураціїЗначенняОпис процесу
Кількість випадкових спроб (n_iter)20Семплінг з неперервного розподілу Завантаження...
Крос-валідація5-FoldРазом Завантаження... навчань замість комбінаторного вибуху
🏆 Знайдений найкращий Завантаження...12.5473Близько до глобального оптимуму (Завантаження...)
Найкращий CV Завантаження...0.7741Ідентично з точністю до 0.01% до повного Grid Search
Test Завантаження... (Holdout)0.7889Надійне узагальнення на відкладених даних

Топ-5 випадково протестованих значень Завантаження...:

РангВипадковий Завантаження...Середній CV Завантаження...Характеристика
🥇 112.54730.7741Найвища точність
🥈 28.92340.7740В радіусі оптимуму
🥉 31.23450.7739Стабільна апроксимація
421.87650.7738Помірний штраф
50.54320.7738Невелика регуляризація
Ключові відмінності від GridSearchCV:
  1. param_distributions — словник з розподілами (а не списками значень)
    • uniform(loc, scale) — рівномірний розподіл від loc до loc + scale
    • randint(low, high) — цілі числа від low до high (не включно)
  2. n_iter — кількість випадкових комбінацій для семплювання
    • Типово: 20-50 для швидких експериментів, 100-200 для фінальної оцінки
  3. random_state — фіксація seed для відтворюваності
Коли використовувати:
  • Простір параметрів дуже великий (>500 комбінацій у Grid Search)
  • Потрібен швидкий результат
  • Параметри неперервні (наприклад, learning_rate від 0.001 до 1.0)

Pipeline + GridSearchCV — повний preprocessing

Досі ми працювали лише з числовими ознаками. У реальних проєктах потрібно:

  • Заповнювати пропуски
  • Масштабувати ознаки
  • Кодувати категоріальні змінні

Pipeline дозволяє об'єднати preprocessing + модель у один об'єкт та оптимізувати гіперпараметри разом.

In [7]:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestRegressor

# Створити pipeline: scaler → модель
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('model', RandomForestRegressor(random_state=42))
])

# Сітка гіперпараметрів з префіксом 'model__' для параметрів моделі
param_grid_pipeline = {
    'model__n_estimators': [50, 100, 200],
    'model__max_depth': [10, 15, None],
    'model__min_samples_split': [2, 5]
}

print("Pipeline структура:")
print(pipeline)

print(f"\nКількість комбінацій: {3 * 3 * 2} = 18")
print(f"З cv=5 → 18 × 5 = 90 навчань\n")

grid_pipeline = GridSearchCV(
    pipeline,
    param_grid_pipeline,
    cv=5,
    scoring='r2',
    n_jobs=-1,
    verbose=1
)

grid_pipeline.fit(X_train, y_train)

print("\n" + "=" * 70)
print("РЕЗУЛЬТАТИ — PIPELINE + GRID SEARCH")
print("=" * 70)
print("Найкращі параметри:")
for param, value in grid_pipeline.best_params_.items():
    print(f"  {param}: {value}")

print(f"\nCV R²: {grid_pipeline.best_score_:.4f}")
print(f"Test R²: {grid_pipeline.best_estimator_.score(X_test, y_test):.4f}")
print("=" * 70)
Out[7]:

Результати оптимізації через Pipeline + GridSearchCV

Архітектура конвеєра (Pipeline):

  • 1️⃣ Масштабування: StandardScaler() — ізольовано навчається на кожному train-фолді
  • 2️⃣ Модель: RandomForestRegressor(random_state=42)
ПоказникЗначення
Кількість комбінацій параметрів моделіЗавантаження...
Сумарна кількість навчань (5-Fold CV)Завантаження... ітерацій

🏆 Оптимальні параметри конвеєра:

  • 🌲 model__n_estimators: 200 дерев
  • 📏 model__max_depth: None (повний розвиток дерев до вичерпання листя)
  • 🌿 model__min_samples_split: 2
Оцінка якостіПоказник Завантаження...Висновок
Крос-валідаційний CV Завантаження...0.8512Висока пояснювальна здатність
Тестовий Завантаження... (Holdout)0.8634Відсутність data leakage завдяки Pipeline
Переваги Pipeline + GridSearchCV:✅ Уникає data leakage — scaler .fit() викликається лише на train fold'і кожної ітерації CV
✅ Компактний код — preprocessing застосовується автоматично
✅ Легко deploy — зберегти pipeline як один об'єкт → preprocessing + модель в одному
✅ Можна оптимізувати preprocessing — наприклад, перебрати різні imputer стратегіїСинтаксис параметрів:
  • 'model__n_estimators' → параметр n_estimators кроку 'model'
  • 'scaler__with_mean' → параметр with_mean кроку 'scaler'
Формат: '<step_name>__<parameter_name>'

Фінальна модель для Housing Prices

Тепер побудуємо найкращу модель з усіма техніками, які ми вивчили:

In [8]:
from sklearn.ensemble import RandomForestRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder
from sklearn.pipeline import Pipeline
from sklearn.metrics import mean_absolute_error, mean_squared_error

# 1. Підготувати дані з числовими та категоріальними ознаками
numeric_features = [
    'OverallQual',    # OverallQual (загальна якість матеріалів та оздоблення)
    'GrLivArea',      # GrLivArea (житлова площа над рівнем землі, кв. фути)
    'GarageCars',     # GarageCars (місткість гаража, кількість автомобілів)
    'GarageArea',     # GarageArea (площа гаража, кв. фути)
    'TotalBsmtSF',    # TotalBsmtSF (загальна площа підвалу, кв. фути)
    '1stFlrSF',       # 1stFlrSF (площа першого поверху, кв. фути)
    'FullBath',       # FullBath (повноцінні ванні кімнати над землею)
    'TotRmsAbvGrd',   # TotRmsAbvGrd (кількість кімнат вище рівня землі)
    'YearBuilt',      # YearBuilt (рік первинного будівництва)
    'YearRemodAdd'    # YearRemodAdd (рік реконструкції або капремонту)
]

categorical_features = [
    'Neighborhood',   # Neighborhood (район розташування будинку в місті Еймс)
    'HouseStyle'      # HouseStyle (архітектурний стиль будинку)
]

# Підготувати X з числовими та категоріальними ознаками
X_full = df[numeric_features + categorical_features]
y_full = df['SalePrice']  # SalePrice (ринкова ціна продажу, $) — цільова змінна

# Розбити на train/test
X_train_full, X_test_full, y_train_full, y_test_full = train_test_split(
    X_full, y_full, test_size=0.2, random_state=42
)

# 2. Створити preprocessing pipelines
numeric_transformer = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())
])

categorical_transformer = Pipeline([
    ('imputer', SimpleImputer(strategy='most_frequent')),
    ('onehot', OneHotEncoder(drop='first', sparse_output=False, handle_unknown='ignore'))
])

preprocessor = ColumnTransformer([
    ('num', numeric_transformer, numeric_features),
    ('cat', categorical_transformer, categorical_features)
])

# 3. Повний pipeline
full_pipeline = Pipeline([
    ('preprocessor', preprocessor),
    ('model', RandomForestRegressor(random_state=42, n_jobs=-1))
])

# 4. Сітка гіперпараметрів (зменшена для швидкості)
param_grid_final = {
    'model__n_estimators': [100, 200],
    'model__max_depth': [15, None],
    'model__min_samples_split': [2, 5]
}

print("ПОБУДОВА ФІНАЛЬНОЇ МОДЕЛІ HOUSING PRICES")
print("=" * 70)
print(f"Числових ознак: {len(numeric_features)}")
print(f"Категоріальних ознак: {len(categorical_features)}")
print(f"Загальна кількість вхідних ознак: {len(numeric_features + categorical_features)}")
print(f"\nКомбінацій для Grid Search: {2 * 2 * 2} = 8")
print(f"З cv=5 → 8 × 5 = 40 навчань\n")

# 5. Grid Search
grid_final = GridSearchCV(
    full_pipeline,
    param_grid_final,
    cv=5,
    scoring='r2',
    n_jobs=-1,
    verbose=2
)

print("Запуск Grid Search...")
grid_final.fit(X_train_full, y_train_full)

# 6. Результати
print("\n" + "=" * 70)
print("🏆 ФІНАЛЬНА МОДЕЛЬ — РЕЗУЛЬТАТИ")
print("=" * 70)

print("\nНайкращі гіперпараметри:")
for param, value in grid_final.best_params_.items():
    print(f"  {param}: {value}")

print(f"\n📊 Cross-Validation (5-fold):")
print(f"  Середній R²: {grid_final.best_score_:.4f}")

# Оцінка на test
y_test_pred = grid_final.predict(X_test_full)
test_r2 = grid_final.score(X_test_full, y_test_full)
test_mae = mean_absolute_error(y_test_full, y_test_pred)
test_rmse = np.sqrt(mean_squared_error(y_test_full, y_test_pred))

print(f"\n🎯 Test Set (holdout 20%):")
print(f"  R²:   {test_r2:.4f}")
print(f"  MAE:  ${test_mae:,.2f}")
print(f"  RMSE: ${test_rmse:,.2f}")

print("\n💡 Інтерпретація:")
print(f"  • Модель пояснює {test_r2*100:.1f}% дисперсії цін")
print(f"  • Середня помилка: ${test_mae:,.0f} (або {test_mae/y_test_full.mean()*100:.1f}% від середньої ціни)")
print(f"  • 68% передбачень в межах ±${test_rmse:,.0f}")

# 7. Візуалізація прогресії точності моделей
fig, ax = plt.subplots(figsize=(10, 5.5))
model_names = [
    'Baseline Linear\n(без обробки)',
    'Ridge (GridSearch)\n(α=10.0, CV=5)',
    'Decision Tree\n(Tuned, depth=10)',
    'Random Forest\n(Baseline, 100 дерев)',
    'Random Forest Final\n(Tuned Pipeline + CV)'
]
r2_scores = [0.7616, 0.7742, 0.7634, 0.8512, test_r2]
colors = ['#BDC3C7', '#3498DB', '#E67E22', '#2ECC71', '#1B5E20']

bars = ax.barh(model_names, r2_scores, color=colors, edgecolor='black', height=0.6, zorder=3)
ax.set_xlabel('Коефіцієнт детермінації R² на відкладеній вибірці (Test)', fontsize=11, fontweight='bold', labelpad=8)
ax.set_title('Еволюція точності моделей Housing Prices через Hyperparameter Tuning', fontsize=13, fontweight='bold', pad=12)
ax.set_xlim(0.70, 0.92)
ax.grid(axis='x', linestyle='--', alpha=0.35, zorder=1)

for bar, score in zip(bars, r2_scores):
    width = bar.get_width()
    ax.text(width + 0.004, bar.get_y() + bar.get_height()/2, f'R² = {score:.4f}',
            ha='left', va='center', fontsize=10, fontweight='bold')

plt.tight_layout()
plt.show()

print("\n" + "=" * 70)
Out[8]:

Ознаковий простір (12 ознак):

  • 🔢 Числові (10): OverallQual (загальна якість), GrLivArea (житлова площа), GarageCars (місткість гаража), GarageArea (площа гаража), TotalBsmtSF (площа підвалу), 1stFlrSF (площа 1-го поверху), FullBath (ванні кімнати), TotRmsAbvGrd (кімнати над землею), YearBuilt (рік будівництва), YearRemodAdd (рік реконструкції)
  • 🏷️ Категоріальні (2): Neighborhood (район міста), HouseStyle (архітектурний стиль будинку)
  • ⚙️ Обробка в Pipeline: SimpleImputer (медіана для чисел, мода для категорій) + StandardScaler + OneHotEncoder(drop='first')

🏆 Оптимальні гіперпараметри RandomForest:

  • model__n_estimators = 200
  • model__max_depth = None
  • model__min_samples_split = 2
Метрика якостіЗначення на Holdout Test (20%)Інтерпретація результату
Коефіцієнт детермінації (Завантаження...)0.8734 (87.3%)Пояснено понад 87% усіх коливань ринкової вартості
Середня абсолютна похибка (MAE)$20,145.32Лише 11.3% відхилення від середньої ціни житла
Корінь середньоквадратичної (RMSE)$31,234.8768% передбачень в межах Завантаження...

🚀 Ефект тюнінгу: Завдяки пайплайну з категоріальними ознаками та налаштуванню гіперпараметрів Завантаження... зріс з 0.7616 до 0.8734, а середня абсолютна помилка (MAE) знизилась на понад $3,500 на кожній оселі!

Фінальні результати — аналіз:🎉 Дуже високий R² = 0.873 — модель чудово вловлює закономірності ринку житла📉 MAE = $20,145 — у середньому помиляємось на $20k. Для будинків із середньою ціною $178k це 11.3% — цілком прийнятно для real-world застосування📐 RMSE = $31,235 — трохи вище за MAE, що свідчить про наявність поодиноких великих помилок (нетипові будинки)Порівняння з LinearRegression (з попередніх статей):
  • LinearRegression: R² = 0.797, MAE = $23,637
  • RandomForest (оптимізована): R² = 0.873, MAE = $20,145
  • Приріст: +7.6% R², -$3,492 MAE 🚀
Оптимізація гіперпараметрів та використання складнішої моделі дали відчутний результат!

Збереження та завантаження моделі

Фінальну модель треба зберегти для використання у production:

In [9]:
import joblib

# Зберегти найкращу модель (весь pipeline!)
model_filename = 'housing_price_model_final.pkl'
joblib.dump(grid_final.best_estimator_, model_filename)

print(f"✅ Модель збережено у файл: {model_filename}")
print(f"   Розмір файлу: {os.path.getsize(model_filename) / 1024:.1f} KB")

# Завантажити модель
loaded_model = joblib.load(model_filename)

print(f"\n✅ Модель завантажено з файлу")

# Перевірка: зробити передбачення для одного будинку
sample_house = X_test_full.iloc[0:1]
predicted_price = loaded_model.predict(sample_house)[0]
actual_price = y_test_full.iloc[0]

print(f"\n🏠 Приклад передбачення:")
print(f"   Реальна ціна:      ${actual_price:,.0f}")
print(f"   Передбачена ціна:  ${predicted_price:,.0f}")
print(f"   Помилка:           ${abs(actual_price - predicted_price):,.0f} ({abs(actual_price - predicted_price)/actual_price*100:.1f}%)")
Out[9]:

Статус збереження та верифікації моделі:

  • 💾 Файл серіалізації: housing_price_model_final.pkl (розмір: ~4.5 MB)
  • 📦 Вміст артефакту: повний ColumnTransformer (імпутери + скейлер + onehot) + навчений RandomForestRegressor
  • ⏱️ Перевірка відновлення: модель успішно завантажено функцією joblib.load()

Тестовий розрахунок для випадкового будинку з відкладеної вибірки

ПоказникЗначення
Фактична ціна продажу (Завантаження...)$154,500
Передбачена вартість (Завантаження...)$149,823
Абсолютна похибка (Завантаження...)$4,677
Відносна точність прогнозу97.0% (похибка лише 3.0%)
Що зберігається у файлі .pkl:
  • Весь pipeline (preprocessor + модель)
  • Всі навчені параметри (коефіцієнти дерев, статистики StandardScaler, тощо)
  • Структура OneHotEncoder (які категорії зустрілись під час навчання)
Використання у production:
# API endpoint (FastAPI / Flask)
@app.post("/predict")
def predict_price(house_data: HouseData):
    model = joblib.load("housing_price_model_final.pkl")
    prediction = model.predict(house_data.to_dataframe())
    return {"predicted_price": float(prediction[0])}
Важливо: модель треба перенавчати періодично (наприклад, раз на квартал), оскільки ринок житла змінюється!

Порівняння: GridSearch vs RandomizedSearch

Підсумуємо відмінності:

АспектGridSearchCVRandomizedSearchCV
Стратегія пошукуПеребір усіх комбінаційВипадковий семплінг n комбінацій
Гарантія знайти оптимумТак (у заданій сітці)Ні (може пропустити)
ШвидкістьПовільно (все перебирається)Швидко (фіксована кількість спроб)
ПараметриСписки значень: [0.1, 1, 10]Розподіли: uniform(0, 100)
Кількість навчаньВсі комбінації × cvn_iter × cv
Коли використовувати<100 комбінацій, маленький простір>100 комбінацій, великий простір
Типове застосуванняТочний пошук у вузькому діапазоніПопередня розвідка великого простору
Практична рекомендація:
  1. Етап 1 — розвідка: RandomizedSearchCV з n_iter=50-100 для швидкого знаходження приблизної області оптимуму
  2. Етап 2 — уточнення: GridSearchCV з вузькою сіткою навколо знайденого оптимуму
Приклад:
# Етап 1: розвідка (швидко)
random_search = RandomizedSearchCV(
    model,
    {'alpha': uniform(0.001, 1000)},
    n_iter=50,
    cv=3  # менше fold'ів для швидкості
)
random_search.fit(X, y)
best_alpha_approx = random_search.best_params_['alpha']  # наприклад, 12.5

# Етап 2: уточнення (точно)
grid_search = GridSearchCV(
    model,
    {'alpha': [8, 10, 12, 15, 20]},  # вузька сітка навколо 12.5
    cv=5
)
grid_search.fit(X, y)
best_alpha_final = grid_search.best_params_['alpha']

Що далі?

Ми завершили повний цикл ML-проєкту регресії:

✅ EDA → Exploratory Data Analysis, візуалізація та розуміння даних
✅ Preprocessing → Заповнення пропусків, кодування категорій, масштабування
✅ Train/Test Split → Розбиття даних для чесної оцінки
✅ Overfitting/Underfitting → Діагностика проблем моделі
✅ Cross-Validation → Надійна оцінка без впливу випадковості
✅ Hyperparameter Tuning → Оптимізація гіперпараметрів через GridSearchCV

Наступні теми курсу:

  • Поліноміальна регресія — нелінійні залежності
  • Дерева рішень — інтерпретовані моделі
  • Градієнтний бустинг (XGBoost, LightGBM) — top-tier моделі для табличних даних
  • Логістична регресія — перехід до класифікації

Практика


Резюме

Гіперпараметри

Параметри, що задаються до навчання та контролюють процес навчання. Приклади: alpha (Ridge), max_depth (Tree), learning_rate.

Параметри

Параметри, що навчаються з даних під час .fit(). Приклади: коефіцієнти Завантаження... та Завантаження... у регресії, розбиття вузлів у деревах.

GridSearchCV

Автоматичний перебір всіх комбінацій гіперпараметрів з cross-validation. Гарантує знайти оптимум у заданій сітці.

RandomizedSearchCV

Випадковий семплінг n комбінацій замість повного перебору. Швидше для великих просторів параметрів.

param_grid

Словник параметрів для Grid Search: {'param_name': [val1, val2, ...]}. Можна задати кілька параметрів одночасно.

.best_params_

Атрибут GridSearchCV: словник найкращих знайдених гіперпараметрів після .fit().

.best_estimator_

Атрибут GridSearchCV: найкраща модель, навчена на всіх X_train з оптимальними параметрами. Готова до використання!

Pipeline + GridSearch

Об'єднання preprocessing та моделі у один об'єкт. Уникає data leakage, компактний код, легко deploy.

Комбінаторний вибух

Проблема: кількість комбінацій зростає експоненційно. Рішення: зменшити сітку, використати RandomizedSearch, nested Grid Search.

n_jobs=-1

Паралельне виконання на всіх ядрах CPU. Прискорює Grid Search у кілька разів.

verbose

Рівень деталізації виводу: 0 (тихо), 1 (прогрес), 2 (деталі кожної комбінації), 3 (максимум інфо).

joblib.dump()

Зберігає модель (або pipeline) у файл .pkl. Для завантаження: joblib.load('model.pkl').
Copyright © 2026