Главное по ML/DL, часть 2: Вопрос → Краткий ответ → Разбор → Пример кода. SVD/PCA. Bias-variance. Деревья. Бустинг

от автора

У каждого наступает момент, когда нужно быстро освежить в памяти огромный пласт информации по всему ML. Причины разные — подготовка к собеседованию, начало преподавания или просто найти вдохновение.

Времени мало, объема много, цели амбициозные — нужно научиться легко и быстро объяснять, но так же не лишая полноты!

💻 Обращу внимание, самый действенный способ разобраться и запомнить — это своими руками поисследовать задачу! Это самое важное, оно происходит в секции с кодом. Поэтому попробуйте сами решить предложенную задачку и придумать свою!

Будет здорово получить ваши задачи и в следующих выпусках разобрать!

Мы продолжаем. Обязательно испытайте себя в предыдущей [1] части!

Я считаю самый полный и простой способ заполнить все пробелы — это взять хороший экзамен и ответить на все вопросы — понятно и быстро. А что бы запомнилось лучше — решить задачку. Приступим!

Сначала попробуйте сами быстро ответить, а потом после просмотра! Стало быстрее-понятнее объяснять?

Для более полного погружения в конце приложу важные ресурсы. Делитесь своими!


11. Анализ главных компонент. Связь с SVD. Теорема Эккарта-Янга. Как применять PCA на практике.

📌 Краткий ответ

PCA (Principal Component Analysis или анализ главных компонент) — метод понижения размерности, который:

  • Решает задачу апроксимации исходной матрицы, матрицами меньшего ранга (как например ALS)

  • Ее можно решать как задачу оптимизации функции ошибки MSE или конструктивно с SVD

  • Еще одна итерпретация — предсказание исходной матрицы скалярным произведением латентных векторов юзера и товара

  • Ищет такие оси (направления), вдоль которых данные имеют максимальную дисперсию

  • Проецирует данные на первые k таких осей (компонент), сохраняя как можно больше информации (в смысле дисперсии) — почему? идем к SVD

  • Реализуется через SVD (Singular value decomposition или сингулярное разложение) — разложение матрицы на две ортонормированные [поворот] и диагональную [растяжение] (с сингулярными числами=важность/дисперсия нового признака)

  • Строки у разжатия можно перемещать (соответвенно, что финальная матрица не изменится) \Rightarrow можно оставить только самые важные признаки!

  • Теорема Эккарта-Янга — утверждает, такая апроксимация внутри своего ранга по норме Фробениуса наименьшая!

🔬 Подробный разбор

На одном дыхании можно послушать тут или почитать хендбук. Отличный английский видеоформат .

💻 Сжимаем картинку через SVD и смотрим на важность направлений

Научимся представлять картинку меньшим кол-вом памяти с помощью SVD !

import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import load_sample_image from skimage.color import rgb2gray from skimage.transform import resize  # --- Загружаем и обрабатываем картинку china = load_sample_image("china.jpg")   # встроенное изображение gray = rgb2gray(china)                   # перевод в ч/б gray = resize(gray, (256, 256), anti_aliasing=True)  # уменьшаем до квадрата  # --- SVD U, S, VT = np.linalg.svd(gray, full_matrices=False)  # --- Восстановление при разных k ks = [5, 20, 50, 100, 200] fig, axes = plt.subplots(1, len(ks) + 1, figsize=(15, 4))  # Оригинал axes[0].imshow(gray, cmap='gray') axes[0].set_title("Оригинал") axes[0].axis('off')  # При разных k for i, k in enumerate(ks):     approx = U[:, :k] @ np.diag(S[:k]) @ VT[:k, :]     axes[i + 1].imshow(approx, cmap='gray')     axes[i + 1].set_title(f"k = {k}")     axes[i + 1].axis('off')  plt.suptitle("Сжатие изображения с помощью SVD") plt.tight_layout() plt.show() 
# Расчёт накопленной доли дисперсии explained = np.cumsum(S) / np.sum(S)  # Отрисовка: сингулярные числа + накопленная доля fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))  # --- 1. Сами сингулярные числа (в лог масштабе) ax1.plot(S, marker='o') ax1.set_yscale("log") ax1.set_title("Сингулярные значения") ax1.set_xlabel("Номер компоненты") ax1.set_ylabel("Значение (log)") ax1.grid(True)  # --- 2. Накопленная доля дисперсии ax2.plot(explained, label='Cуммарная доля дисперсии') ax2.axhline(0.9, color='red', linestyle='--', label='90% информации') ax2.set_xlabel("Число компонент") ax2.set_ylabel("Накопленная доля") ax2.set_title("Сколько информации несут сингулярные значения") ax2.grid(True) ax2.legend()  plt.tight_layout() plt.show() 
💻 Проецируем многомерные данные (картинки цифр) в 2D с помощью PCA

Визуализируем датасет рукописных цифр (digits) — в нём 64 признака (8×8 картинка), и сожмём до 2 главных компонент, чтобы посмотреть, как PCA группирует похожие объекты:

from sklearn.datasets import load_digits from sklearn.decomposition import PCA import matplotlib.pyplot as plt  # --- Загружаем данные digits = load_digits() X = digits.data       # 64 признака (8x8 пикселей) y = digits.target     # метки (0-9)  # --- PCA до 2 компонент pca = PCA(n_components=2) X_pca = pca.fit_transform(X)  # --- Визуализация plt.figure(figsize=(8, 6)) scatter = plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y, cmap='tab10', s=15, alpha=0.8) plt.legend(*scatter.legend_elements(), title="Цифры", loc="best", bbox_to_anchor=(1.05, 1)) plt.title("PCA-проекция данных (64D → 2D)") plt.xlabel("1-я главная компонента") plt.ylabel("2-я главная компонента") plt.grid(True) plt.tight_layout() plt.show() 

12. Этапы обучения, валидации и тестирования модели. Проблема переобучения, способы её обнаружения.

📌 Краткий ответ

Обучение ML-модели делится на три ключевых этапа:

  1. Обучение (train) — модель подбирает параметры на обучающей выборке.

  2. Валидация (validation) — подбираем гиперпараметры на валидационной выборке.

  3. Тест (test) — финальная оценка качества, только один раз.

Бывают, что тест опускают и финальное качество берут лучшее с валидации, но это ведет к переобучению гиперпараметров.


Переобучение — это ситуация, когда модель слишком хорошо подгоняется под обучающую выборку, но плохо обобщает на новые данные.

Чтобы меньше зависеть от разбиение train/val/test используют обучение по фолдам (k-fold cross-validation) — Обучается k моделей. Датасет делиться на k частей, на k - 1 модель учиться, на оставшейся замеряется качество. Метрика усредняется.

Такой подход подход хоть устойчевее к шуму, но не всегда хочется делать бэггинг над этими k моделей. Обычно в начале EDA (exploratory data analysis или разведочный анализ) проверяют, что разбиение train/val/test подходит и учат одну модель.


Как понять, что модель начала переобучаться? (если неграмотно сохранять чекпоинты, можно остаться с переобученной моделью!)

  • Метрика на train продолжает падать, а на val начала возрастать. (Если нету графиков, то метрика на train сильно лучше, чем на val)

Как бороться?

  • Early stopping (если метрика на валидации не уменьшалась k шагов — остановиться)

  • Регуляризации — L_1/L_2, Dropout/BatchNorm, аугментация данных и тд

  • Выбросить совсем незначащие признаки (если их очень много, модель будет на них отвлекаться, может и сойдется, но дОльше!)

  • Больше данных насыпать

  • Бэггинг (Bagging, от bootstrap aggregating) — учим несколько моделей на бутсреп данных и объединяем => уменьшаем дисперсию ответа

Классическая симптомы переобучения (можно раньше времени спутать с мифическим Double Descent)

🔬 Подробный разбор

Оказывается, не стоит раньше времени останавливать обучения, может быть у вас Double Descent. Ситуация в которой лосс на валидации взлетит, а потом начнет уменьшаться до меньших значений!

Я не знаю на практике у кого такое было. Но как явления очень интересное.

Подробнее про Double Descent можно почитать 1 и 2.

💻 Пример переобучения: наблюдаем за лоссом и предсказаниями

Смотрим, что происходит при увеличением эпох с лоссом и предсказаниями, если признаковое пространство недостаточно покрыто данными.

Я специалньо взял не очень много точек (=150), потому что данные достаточно очень легко устроены/разъединяются и при большем не наблюдается эффекта переобучения.

import torch import torch.nn as nn import torch.optim as optim from sklearn.datasets import make_moons from sklearn.model_selection import train_test_split import matplotlib.pyplot as plt import numpy as np  # --- 1. Данные X, y_true = make_moons(n_samples=150, noise=0.3, random_state=42) # y_random = np.random.permutation(y_true)  # случайные метки!  # X_train, X_val, y_train, y_val = train_test_split(X, y_random, test_size=0.3, random_state=42) X_train, X_val, y_train, y_val = train_test_split(X, y_true, test_size=0.3, random_state=42)  X_train_t = torch.tensor(X_train, dtype=torch.float32) y_train_t = torch.tensor(y_train.reshape(-1, 1), dtype=torch.float32)  X_val_t = torch.tensor(X_val, dtype=torch.float32) y_val_t = torch.tensor(y_val.reshape(-1, 1), dtype=torch.float32)  # --- 2. Модель # h_size = 256 h_size = 100 model = nn.Sequential(     nn.Linear(2, h_size),     nn.ReLU(),     nn.Linear(h_size, h_size),     nn.ReLU(),     nn.Linear(h_size, 1),     nn.Sigmoid() )  loss_fn = nn.BCELoss() optimizer = optim.Adam(model.parameters(), lr=0.01)  train_losses, val_losses = [], [] snapshots = [0, 10, 30, 99]  # эпохи, для которых сохраним визуализацию  grids = np.meshgrid(     np.linspace(X[:, 0].min() - 0.5, X[:, 0].max() + 0.5, 300),     np.linspace(X[:, 1].min() - 0.5, X[:, 1].max() + 0.5, 300) ) grid_points = np.c_[grids[0].ravel(), grids[1].ravel()] grid_tensor = torch.tensor(grid_points, dtype=torch.float32)  # --- 3. Обучение snapshots_preds = []  for epoch in range(100):     model.train()     y_pred = model(X_train_t)     loss = loss_fn(y_pred, y_train_t)          optimizer.zero_grad()     loss.backward()     optimizer.step()      model.eval()     with torch.no_grad():         val_pred = model(X_val_t)         val_loss = loss_fn(val_pred, y_val_t)         if epoch in snapshots:             pred_grid = model(grid_tensor).reshape(300, 300)             snapshots_preds.append((epoch, pred_grid.numpy()))          train_losses.append(loss.item())     val_losses.append(val_loss.item())  # --- 4. График потерь plt.figure(figsize=(8, 5)) plt.plot(train_losses, label='Train Loss') plt.plot(val_losses, label='Validation Loss') plt.axvline(np.argmin(val_losses), linestyle='--', color='gray', label='Лучшая эпоха (val)') plt.title("Переобучение: валидационная ошибка начинает расти") plt.xlabel("Эпоха") plt.ylabel("Binary CrossEntropy") plt.legend() plt.grid(True) plt.tight_layout() plt.show()  # --- 5. Визуализация данных + предсказания fig, axes = plt.subplots(1, len(snapshots_preds), figsize=(16, 4)) for ax, (epoch, Z) in zip(axes, snapshots_preds):     ax.contourf(grids[0], grids[1], Z, levels=20, cmap='coolwarm', alpha=0.7)     ax.scatter(*X_train.T, c=y_train, cmap='bwr', s=15, edgecolor='k', label='Train')     ax.scatter(*X_val.T, c=y_val, cmap='bwr', marker='x', s=15, label='Val')     ax.set_title(f"Эпоха {epoch}")     ax.axis('off') plt.suptitle("Как модель обучается на шум: границы решений со временем") plt.tight_layout() plt.show() 
💻 Переобучение полинома большой степени
# Повторный запуск после сброса состояния import numpy as np import matplotlib.pyplot as plt from sklearn.preprocessing import PolynomialFeatures from sklearn.linear_model import LinearRegression from sklearn.pipeline import make_pipeline from sklearn.metrics import mean_squared_error from sklearn.model_selection import train_test_split  # --- 1. Генерируем данные np.random.seed(42) X = np.linspace(-3, 3, 10).reshape(-1, 1)  # мало точек y = np.sin(X).ravel() + 0.3 * np.random.randn(*X.shape).ravel()  X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.3, random_state=42)  # --- 2. Обучаем модели с разной степенью полинома degrees = [1, 4, 15] preds = {} val_errors = {}  x_plot = np.linspace(-3, 3, 300).reshape(-1, 1)  plt.figure(figsize=(15, 4))  for i, deg in enumerate(degrees):     model = make_pipeline(PolynomialFeatures(degree=deg), LinearRegression())     model.fit(X_train, y_train)          y_plot = model.predict(x_plot)     y_val_pred = model.predict(X_val)     val_errors[deg] = mean_squared_error(y_val, y_val_pred)          plt.subplot(1, 3, i + 1)     plt.scatter(X_train, y_train, label='Train', color='blue')     plt.scatter(X_val, y_val, label='Val', color='red', alpha=0.5)     plt.plot(x_plot, y_plot, color='green', label=f"Poly deg={deg}")     plt.title(f"deg={deg} | val MSE={val_errors[deg]:.2f}")     plt.legend()     plt.grid(True)  plt.suptitle("Сравнение моделей с разной степенью полинома") plt.tight_layout() plt.show()  val_errors # {1: 0.0623793189026051, 4: 0.16693688909602888, 15: 714985.2317858242} 

13. Стратегии валидации. Кросс-валидация. Утечки данных.

📌 Краткий ответ

Валидация — способ понять, как модель будет работать на новых данных и сравнить модели друг с другом.

Основные стратегии:

  • Hold-Out — просто делим на train/val.

  • K-Fold — учим k моделей, валидируем по очереди на каждом фолде.

  • Stratified K-Fold — сохраняем пропорции классов.

  • TimeSeriesSplit — используем прошлое, предсказываем будущее.


Data leakage (утечка данных) — модель видит информацию, которую не должна:

  • Масштабирование или выбор признаков до train/val split

  • Использование target в признаках

  • Временная утечка

Обычно модель на своих данных показывает высокую метрику, а на продовых сильно хуже.

💻 Демонстрация val утечки в train. Предварительный отбор признаков SelectKBest на train VS train + val.

Удивительно, что даже с кроссвалидацией утечка сильно влияет на финальную метрику!

import numpy as np from sklearn.datasets import make_classification from sklearn.model_selection import cross_val_score from sklearn.feature_selection import SelectKBest, f_classif from sklearn.ensemble import RandomForestClassifier from sklearn.pipeline import make_pipeline  # --- Данные: много признаков, немного информативных X, y = make_classification(     n_samples=1000, n_features=500, n_informative=30,     random_state=42, shuffle=False )  # --- ⚠️ Утечка: отбор признаков ДО cross-validation selector = SelectKBest(score_func=f_classif, k=50) X_leak = selector.fit_transform(X, y)  model = RandomForestClassifier(random_state=42) scores_leak = cross_val_score(model, X_leak, y, cv=5)  # --- ✅ Честно: отбор фич ВНУТРИ кросс-валидации pipeline = make_pipeline(     SelectKBest(score_func=f_classif, k=50),     RandomForestClassifier(random_state=42) ) scores_clean = cross_val_score(pipeline, X, y, cv=5)  print(f"⚠️ CV accuracy с утечкой:  {scores_leak.mean():.3f}") print(f"✅ CV accuracy без утечки: {scores_clean.mean():.3f}")  # ⚠️ CV accuracy с утечкой:  0.787 # ✅ CV accuracy без утечки: 0.754 

14. Компромисс смещения-дисперсии (Bias-variance trade-off). Double Descent.

📌 Краткий ответ

Это разложение ошибки (и способ ее формально определить!) (на тестовой выборке) на три компоненты: bias, variance, noise (на нее не влияем). (Текущая формула верна для MSE, но аналоги существуют и для других!)

Показывает как выбрать оптимальную модель при некоторых предположениях: при усложнении модели bias падает, variance растет. Тогда сумма их графиков U-образная \Rightarrow есть оптимум! (При Double Descent это не выполняется!)

Описание крайних ситуаций bias и variance


Теперь подробнее и по математически

Пусть:

  • y(x, \varepsilon) = f(x) + \varepsilon — целевая переменная с шумом.

\mathbb{E}[\varepsilon] = 0,\quad \mathrm{Var}[\varepsilon] = \mathbb{E}[\varepsilon^2] = \sigma^2

  • x — объект из тестовой выборки. X — обучающая выборка

  • a(x, X) — алгоритм, обученный на случайной выборке X,

Тогда среднеквадратичная ошибка (MSE) алгоритма имеет вид:

\mathbb{E}_x \mathbb{E}_{X, \varepsilon} \left[ y(x, \varepsilon) - a(x, X) \right]^2= \mathbb{E}_x \left( \underbrace{\left( \mathbb{E}_X[a(x, X)] - f(x) \right)^2}_{\text{смещение (bias)}^2}+ \underbrace{\text{Var}_X[a(x, X)]}_{\text{дисперсия (variance)}}+ \underbrace{\sigma^2}_{\text{шум}} \right)


Пояснение:

\text{bias}_X[a(x, X)] = \mathbb{E}_X[a(x, X)] - f(x)

  • смещение (bias) предсказания алгоритма в точке x, усреднённого по всем возможным обучающим выборкам, относительно истинной зависимости f

    \text{Var}_X[a(x, X)] = \mathbb{E}_X\left[(a(x, X) - \mathbb{E}_X[a(x, X)])^2\right]

  • дисперсия (variance) предсказаний алгоритма в зависимости от обучающей выборки X

\sigma^2 = \mathbb{E}_x \mathbb{E}_\varepsilon \left[ y(x, \varepsilon) - f(x) \right]^2

  • неустранимый шум в данных.

🔬 Подробный разбор

Сначала наслаждаемся MLU-explAIn и потом идем читать с выводами формул хендбук

💻 Bias–Variance разложение на настоящем примере. Множество предсказаний моделей в одном классе сложности и усреднение.

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

import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_regression from sklearn.tree import DecisionTreeRegressor from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split from sklearn.utils import resample  # --- Данные X_all, y_all = make_regression(n_samples=1000, n_features=1, noise=15, random_state=42) X_train, X_val, y_train, y_val = train_test_split(X_all, y_all, test_size=0.3, random_state=42) X_clean, y_clean = make_regression(n_samples=300, n_features=1, noise=0, random_state=42) true_model = LinearRegression().fit(X_clean, y_clean)  X_test = np.linspace(X_all.min(), X_all.max(), 200).reshape(-1, 1) true_y = true_model.predict(X_test)  # --- Bias², Variance, Noise depths = range(1, 20) n_models = 100 biases, variances, noises = [], [], [] models_by_depth = {d: [] for d in [1, 4, 10]}  for d in depths:     preds = []     for _ in range(n_models):         X_boot, y_boot = resample(X_train, y_train)         model = DecisionTreeRegressor(max_depth=d)         model.fit(X_boot, y_boot)         y_pred = model.predict(X_test)         preds.append(y_pred)         if d in models_by_depth and len(models_by_depth[d]) < 50:             models_by_depth[d].append(y_pred)     preds = np.array(preds)     mean_preds = preds.mean(axis=0)     bias2 = ((mean_preds - true_y) ** 2).mean()     var = preds.var(axis=0).mean()     noise = np.var(y_val - model.predict(X_val))  # приближённо     biases.append(bias2)     variances.append(var)     noises.append(noise)  # --- График ошибок plt.figure(figsize=(10, 6)) plt.plot(depths, biases, label='Bias²') plt.plot(depths, variances, label='Variance') plt.plot(depths, noises, label='Noise (приближённо)') plt.plot(depths, np.array(biases) + np.array(variances) + np.array(noises),          label='Total Error', linestyle='--') plt.xlabel("Глубина дерева") plt.ylabel("Ошибка") plt.title("Bias-Variance Trade-off") plt.legend() plt.grid(True) plt.tight_layout() plt.show() 

Теперь визуализируем сами модели: нарисуем все деревья на разных глубинах, их усреднение и настоящую функцию.

# --- Визуализация моделей fig, axes = plt.subplots(1, 3, figsize=(18, 5), sharey=True) for ax, d in zip(axes, [1, 4, 10]):     all_preds = np.array(models_by_depth[d])     for y_pred in all_preds:         ax.plot(X_test.ravel(), y_pred, color='gray', alpha=0.2)     ax.plot(X_test.ravel(), all_preds.mean(axis=0), color='blue', linewidth=2, label='Усреднённая модель')     ax.plot(X_test.ravel(), true_y, color='green', linestyle='--', label='f(x) (истинная)')     ax.scatter(X_train, y_train, s=10, color='black', alpha=0.6, label='Train data')     ax.set_title(f"Глубина дерева = {d}")     ax.set_xlabel("x")     ax.legend()     ax.grid(True)  axes[0].set_ylabel("y") plt.suptitle("Смещение и дисперсия на примере деревьев") plt.tight_layout() plt.show()  

И наконец — как устроены сами данные. Train/test и истинная f(x)

# Визуализация обучающих и тестовых данных на фоне регрессионной прямой plt.figure(figsize=(8, 5))  # Линия истинной функции plt.plot(X_test, true_y, color='green', linestyle='--', label='f(x) (истинная)')  # Обучающие и тестовые данные plt.scatter(X_train, y_train, color='black', s=20, alpha=0.7, label='Train') plt.scatter(X_val, y_val, color='red', s=20, alpha=0.5, label='Test')  plt.xlabel("x") plt.ylabel("y") plt.title("Генерация и распределение данных") plt.grid(True) plt.legend() plt.tight_layout() plt.show()  

15. Процедура построения дерева решений (Decision tree).

📌 Краткий ответ

Дерево решений строится итеративно, разбивая множество объектов в узле на 2 подмножества по признаку x_j и порогу t, чтобы максимально уменьшить информативность (impurity) (чем ниже, тем обьекты ближе можно представить константным значением):

Процедура разбиения узла:

  1. Перебираются все признаки x_j и возможные пороги t. Все элементы с признаком меньше порога попадают в левый узел, остальные вправо.

  2. Для каждого разбиения вычисляется:

\text{Impurity} = \frac{n_\text{left}}{n} \cdot I(D_\text{left}) + \frac{n_\text{right}}{n} \cdot I(D_\text{right})

где I(\cdot) — функция impurity:

  • Джини или энтропия (классификация)

    • \text{Gini}(p) = \sum_{k=1}^{K} p_k (1 - p_k) = 1 - \sum_{k=1}^{K} p_k^2

    • \text{Entropy}(p) = - \sum_{k=1}^{K} p_k \log_2 p_k

  • дисперсия (регрессия)

    • \text{MSE}(D) = \frac{1}{n} \sum_{i=1}^{n} (y_i - \bar{y})^2

  1. Выбирается разбиение с наименьшей информативностью

После разбиения:

  • В каждом потомке заново пересчитывается «ответ» узла:

    • мода (класс) для классификации

    • среднее значение y для регрессии

Разбиение продолжается рекурсивно, пока не выполнены условия остановки:

  • достигнута макс. глубина

  • мало объектов (min_samples_split)

  • impurity = 0

Важно: дерево строится жадно, локально оптимизируя на каждом шаге, не гарантируя глобального оптимума.

Важность для каждого признака считается так: берут изменение информативности для каждого узла и усредняется на кол-во всех разбиений.


Вопросы на подумать:

  • Почему важно информативность поправлять на размер узла?

  • Почему изменение информативности на каждом шаге не отрицательна?

  • Как эффективно перебирать пороги?

🔬 Подробный разбор
💻 Пишем свое дерево решения и сравниваем с sklearn.

tldr:

  • Видно, что качество по метрики выбиваем такое же

  • А вот, то что важность у признаков так отличается интересно.

 import numpy as np import pandas as pd from sklearn.datasets import load_breast_cancer from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score import matplotlib.pyplot as plt  # Загружаем датасет data = load_breast_cancer() X, y = data.data, data.target feature_names = data.feature_names X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)  # Свое дерево решений class Node:     def __init__(self, gini, samples, value, feature_index=None, threshold=None, left=None, right=None):         self.gini = gini         self.samples = samples         self.value = value         self.feature_index = feature_index         self.threshold = threshold         self.left = left         self.right = right  class MyDecisionTreeClassifier:     def __init__(self, max_depth=3, min_samples_split=2):         self.max_depth = max_depth         self.min_samples_split = min_samples_split         self.feature_importances_ = None         self.tree_ = None      def fit(self, X, y):         self.n_classes_ = len(set(y))         self.n_features_ = X.shape[1]         self.feature_importances_ = np.zeros(self.n_features_)         self.tree_ = self._grow_tree(X, y)          def _gini(self, y):         m = len(y)         return 1.0 - sum((np.sum(y == c) / m) ** 2 for c in np.unique(y))      def _best_split(self, X, y):         m, n = X.shape         if m <= 1:             return None, None          best_gini = 1.0         best_idx, best_thr = None, None         parent_gini = self._gini(y)          for idx in range(n):             thresholds, classes = zip(*sorted(zip(X[:, idx], y)))             num_left = [0] * self.n_classes_             num_right = np.bincount(classes, minlength=self.n_classes_)             for i in range(1, m):                 c = classes[i - 1]                 num_left[c] += 1                 num_right[c] -= 1                 gini_left = 1.0 - sum((num_left[x] / i) ** 2 for x in range(self.n_classes_))                 gini_right = 1.0 - sum((num_right[x] / (m - i)) ** 2 for x in range(self.n_classes_))                 gini = (i * gini_left + (m - i) * gini_right) / m                  if thresholds[i] == thresholds[i - 1]:                     continue                  if gini < best_gini:                     best_gini = gini                     best_idx = idx                     best_thr = (thresholds[i] + thresholds[i - 1]) / 2                     impurity_reduction = parent_gini - gini                     self.feature_importances_[idx] += impurity_reduction          return best_idx, best_thr      def _grow_tree(self, X, y, depth=0):         num_samples_per_class = [np.sum(y == i) for i in range(self.n_classes_)]         predicted_class = np.argmax(num_samples_per_class)         node = Node(             gini=self._gini(y),             samples=len(y),             value=num_samples_per_class         )          if depth < self.max_depth and len(y) >= self.min_samples_split and node.gini > 0:             idx, thr = self._best_split(X, y)             if idx is not None:                 indices_left = X[:, idx] <= thr                 X_left, y_left = X[indices_left], y[indices_left]                 X_right, y_right = X[~indices_left], y[~indices_left]                 node.feature_index = idx                 node.threshold = thr                 node.left = self._grow_tree(X_left, y_left, depth + 1)                 node.right = self._grow_tree(X_right, y_right, depth + 1)         return node      def _predict(self, inputs):         node = self.tree_         while node.left:             if inputs[node.feature_index] <= node.threshold:                 node = node.left             else:                 node = node.right         return np.argmax(node.value)      def predict(self, X):         return np.array([self._predict(inputs) for inputs in X])  # Обучение и сравнение my_tree = MyDecisionTreeClassifier(max_depth=3) my_tree.fit(X_train, y_train) y_pred_my = my_tree.predict(X_test)  sk_tree = DecisionTreeClassifier(max_depth=3, random_state=42) sk_tree.fit(X_train, y_train) y_pred_sk = sk_tree.predict(X_test)  # Сравнение результатов acc_my = accuracy_score(y_test, y_pred_my) acc_sk = accuracy_score(y_test, y_pred_sk) print(f"{acc_my=} {acc_sk=}") # acc_my=0.9590643274853801 acc_sk=0.9649122807017544   importances_df = pd.DataFrame({     "Feature": feature_names,     "MyTree": my_tree.feature_importances_ / np.sum(my_tree.feature_importances_),     "Sklearn": sk_tree.feature_importances_ }).sort_values(by="MyTree", ascending=False)  importances_df  # FeatureMyTreeSklearn # 0mean radius0.6370330.000000 # 7mean concave points0.2603520.809978 # 2mean perimeter0.0319540.000000 # 1mean texture0.0302740.025169 # 6mean concavity0.0207490.000000 # 20worst radius0.0099490.043482 # 21worst texture0.0028680.066145 # 23worst area0.0019240.040310 # 22worst perimeter0.0018330.000000 # 3mean area0.0016940.000000 # 10radius error0.0013710.000000  

16. Критерии информации. Критерии энтропии, неопределенности Джини.

📌 Краткий ответ

Смотри предыдущий пункт. Самое главное понимать, что максимум функции (информативности) достигается, когда всех обьектов поровну, а минимум все бъекты одного класса.

Критерии

График функций информативности для двух классов
💻 Визуализируем критерии информативности
import numpy as np import matplotlib.pyplot as plt  # Вероятности для одного из классов (например, класс 1) p = np.linspace(0, 1, 500)  # Энтропия (Shannon entropy) entropy = -p * np.log2(p + 1e-9) - (1 - p) * np.log2(1 - p + 1e-9)  # Джини gini = 2 * p * (1 - p)  # Визуализация plt.figure(figsize=(8, 5)) plt.plot(p, entropy, label='Entropy', linewidth=2) plt.plot(p, gini, label='Gini Impurity', linewidth=2) plt.xlabel("p (доля одного класса)") plt.ylabel("Impurity") plt.title("Сравнение критериев информации") plt.legend() plt.grid(True) plt.tight_layout() plt.show() 
Критерии

График функций информативности для двух классов

17. Ансамблевые методы. Бутстрап (bootstrap). Бэггинг (bagging). Стекинг (stacking)

📌 Краткий ответ

Ансамбли — это способ объединить множество простых моделей, чтобы получить одну, более устойчивую.

Бутстрап: процесс генерации подвыборок с помощью семплирования с возвращением.

Бэггинг (bagging, bootstrap aggregation) — обучаем несколько моделей на бутстрап-выборках и агрегируем их ответы (усреднение или голосование).

Теория вокруг Bias-variance trade-off говорит, что смещение (bias) не изменится, a дисперсия (variance) уменьшится в k раз (Если предположить независимость базовых алгоритмов). Вывод утверждения для задачи регрессии. \Rightarrow берем сложные, глубокие модели, чтобы уменьшить смещение (а дисперсию уменьшим с помощью беггинга!)

Стекинг (stacking): 1. Учим несколько (разной природы) моделей на разных фолдах, и потом на отдельном фолде учим мета модель .


Например:

Случайный лес (Random Forest) = бэггинг + метод случайных подпространств(=в каждом узле дерева выбирается случайное подмножество признаков):

Из какого кол-во признаков выбирать?

  • Много признаков \Rightarrow большая скоррелированность моделей (=маленький эффект от ансамблирования).

  • Мало признаков \Rightarrow модели слабые (=смещение увеличивается).

Практическая рекомендация — брать корень из числа всех признаков для классификации и треть признаков для регрессии.

🔬 Подробный разбор
💻 Сравнения ансамблёвых моделей
 # Импорт всех нужных библиотек from sklearn.ensemble import (     BaggingClassifier,     StackingClassifier,     RandomForestClassifier,     GradientBoostingClassifier ) from sklearn.tree import DecisionTreeClassifier from sklearn.linear_model import LogisticRegression from sklearn.svm import SVC from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score import matplotlib.pyplot as plt  # Загружаем датасет X, y = load_breast_cancer(return_X_y=True) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)  # 1. Обычное дерево tree = DecisionTreeClassifier(random_state=42) tree.fit(X_train, y_train) y_pred_tree = tree.predict(X_test) acc_tree = accuracy_score(y_test, y_pred_tree)  # 2. Бэггинг bag = BaggingClassifier(DecisionTreeClassifier(), n_estimators=100, random_state=42) bag.fit(X_train, y_train) y_pred_bag = bag.predict(X_test) acc_bag = accuracy_score(y_test, y_pred_bag)  # 3. Стекинг: дерево + SVM → логрегрессия stack = StackingClassifier(     estimators=[         ("dt", DecisionTreeClassifier()),         ("svm", SVC(probability=True))     ],     final_estimator=LogisticRegression(),     cv=5 ) stack.fit(X_train, y_train) y_pred_stack = stack.predict(X_test) acc_stack = accuracy_score(y_test, y_pred_stack)  # 4. Случайный лес rf = RandomForestClassifier(n_estimators=100, random_state=42) rf.fit(X_train, y_train) y_pred_rf = rf.predict(X_test) acc_rf = accuracy_score(y_test, y_pred_rf)  # 5. Градиентный бустинг boost = GradientBoostingClassifier(n_estimators=100, learning_rate=0.1, max_depth=3, random_state=42) boost.fit(X_train, y_train) y_pred_boost = boost.predict(X_test) acc_boost = accuracy_score(y_test, y_pred_boost)  # Визуализация результатов models = ['Decision Tree', 'Bagging', 'Stacking', 'Random Forest', 'Boosting'] accuracies = [acc_tree, acc_bag, acc_stack, acc_rf, acc_boost]  plt.figure(figsize=(10, 5)) plt.bar(models, accuracies, color=['skyblue', 'lightgreen', 'salmon', 'gold', 'orchid']) plt.ylim(0.9, 1.0) plt.ylabel("Accuracy") plt.title("Сравнение ансамблевых моделей на Breast Cancer Dataset") plt.grid(axis='y') plt.tight_layout() plt.show()  # Вывод точности всех моделей acc_tree, acc_bag, acc_stack, acc_rf, acc_boost # (0.9415204678362573, #  0.9590643274853801, #  0.9649122807017544, #  0.9707602339181286, #  0.9590643274853801)  

18. Случайный лес (Random Forest), метод случайных подпространств.

📌 Краткий ответ

Смотри прошлый билет!

💻 Поиск оптимального кол-ва случайных признаков. Несогласованность и точность моделей.

Видим, как с увеличением случайных признаков увеличивается согласованность ответов \Rightarrow модели становятся скоррелированны \Rightarrow качество бесконечно не растет
.

 # Будем оценивать дисперсию (variance) предсказаний разных деревьев # для каждого значения max_features  tree_variances = [] mean_accuracies = [] max_features_range = range(1, n_features + 1)  for mf in max_features_range:     rf = RandomForestClassifier(         n_estimators=100, max_features=mf, random_state=42, oob_score=False     )     rf.fit(X_train, y_train)          # Предсказания всех деревьев     all_preds = np.array([tree.predict(X_test) for tree in rf.estimators_])  # shape (n_estimators, n_samples)          # Среднее предсказание (majority vote)     majority_vote = np.round(np.mean(all_preds, axis=0)).astype(int)     acc = accuracy_score(y_test, majority_vote)     mean_accuracies.append(acc)          # Оценка "дисперсии" (простейшая: средняя доля несогласия между деревьями)     disagreement = np.mean(np.var(all_preds, axis=0))     tree_variances.append(disagreement)  # Визуализация fig, ax1 = plt.subplots(figsize=(10, 5))  color = 'tab:blue' ax1.set_xlabel("max_features") ax1.set_ylabel("Disagreement (Variance across trees)", color=color) ax1.plot(max_features_range, tree_variances, color=color, label="Variance across trees") ax1.tick_params(axis='y', labelcolor=color) ax1.grid(True)  ax2 = ax1.twinx() color = 'tab:green' ax2.set_ylabel("Accuracy", color=color) ax2.plot(max_features_range, mean_accuracies, color=color, linestyle='--', label="Accuracy") ax2.tick_params(axis='y', labelcolor=color)  plt.title("Variance across trees vs Accuracy (Random Forest)") fig.tight_layout() plt.show()  

19. Бустинг и градиентный бустинг (Gradient Boosting). Основная идея, производная градиента.

📌 Краткий ответ

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

  • В отличие от бэггинга (параллельные модели), здесь идет цепочка из «корректоров».

  • Итоговое предсказание — взвешенная сумма всех слабых моделей.

Градиентный бустинг (Gradient Boosting) — это частный случай, где каждая новая модель аппроксимирует антиградиент функции потерь (напр. MSE, логлосс) по предсказаниям текущего ансамбля.

🔬 Подробный разбор

📘 Общая идея бустинга

Очень круто, подробно и просто описано тут.

Имеем задачу регрессии или классификации с функцией потерь L(y, F(x)).
Хотим построить итоговую модель в виде:

F(x) = \sum_{m=1}^{M} \gamma_m h_m(x)

Каждый новый слабый алгоритм h_m(x) обучается на остатках или антиградиенте предыдущей ошибки.


Градиентный бустинг (Gradient Boosting)

На шаге m строим новую модель h_m, которая аппроксимирует антиградиент функции потерь по текущим предсказаниям:

r_i^{(m)} = - \left. \frac{\partial L(y_i, F(x_i))}{\partial F(x_i)} \right|_{F = F_{m-1}}

После этого:

  • обучаем h_m на (x_i, r_i^{(m)})

  • выбираем шаг \gamma_m

  • обновляем:

    F_m(x) = F_{m-1}(x) + \gamma_m h_m(x)

Примеры градиентов:

  • MSE (регрессия):

    \frac{\partial}{\partial F(x)} \frac{1}{2}(y - F(x))^2 = F(x) - y\Rightarrow r_i = y_i - F(x_i)

    То есть просто остатки! Совпадает с определением бустинга.

  • Log-loss (бинарная классификация):

    L(y, F) = \log(1 + e^{-yF}) \Rightarrow r_i = \frac{-y_i}{1 + e^{y_i F(x_i)}}

Почему работает?

  • Рассматриваем задачу не как минимизация расстояния между вектором предсказания и истинных значений, а с точки зрения функции потерь!

  • Итеративно минимизируем функцию потерь — как градиентный спуск в функциональном пространствеn мерном пространстве, n — размер датасета). Каждый шаг — это добавление антиградиента к аргументу функции \Rightarrow уменьшение лосса.

  • Для MSE это одно и тоже! (остаток и антиградиент равны)

Критерии

Решение задачи как минимизация расстояния предсказания и истины
Критерии

Решение задачи как минимизация функции потерь

Обращу внимание, тот и тот оба решает задачу, только второй это делает по наискорейшему направлению.

А как выбрать базовые модели? Берутся «простые» модели (с низкой дисперсией), и итеративно уменьшается смещение


Основные реализации, работающие в проде. XGBoost, LightGBM и CatBoost.
Очень важно знать важные фишки и отличия.

💻 Реализовываем свой градиентный бустинг. Сравниваемся с GradientBoostingRegressor и RandomForestRegressor.
  • Получился код, с таким же качеством как из sklearn.

  • Дерево решение проиграло бустингу.

# Полный код: сравнение градиентного бустинга и случайного леса  import numpy as np import matplotlib.pyplot as plt from sklearn.tree import DecisionTreeRegressor from sklearn.metrics import mean_squared_error from sklearn.model_selection import train_test_split from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor  # 1. Синтетические данные np.random.seed(42) X = np.linspace(0, 10, 500).reshape(-1, 1) y_true = np.sin(X).ravel() y = y_true + np.random.normal(0, 0.3, size=X.shape[0]) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)  # 2. Собственный градиентный бустинг class MyGradientBoostingRegressor:     def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3):         self.n_estimators = n_estimators         self.learning_rate = learning_rate         self.max_depth = max_depth         self.models = []         self.gammas = []      def fit(self, X, y):         self.models = []         self.gammas = []         self.init_val = np.mean(y)         F = np.full_like(y, fill_value=self.init_val, dtype=np.float64)          for m in range(self.n_estimators):             residuals = y - F             tree = DecisionTreeRegressor(max_depth=self.max_depth)             tree.fit(X, residuals)             prediction = tree.predict(X)             gamma = 1.0             F += self.learning_rate * gamma * prediction             self.models.append(tree)             self.gammas.append(gamma)      def predict(self, X):         F = np.full(X.shape[0], self.init_val)         for gamma, tree in zip(self.gammas, self.models):             F += self.learning_rate * gamma * tree.predict(X)         return F  # 3. Обучение всех моделей # Собственный бустинг my_gb = MyGradientBoostingRegressor(n_estimators=100, learning_rate=0.1, max_depth=3) my_gb.fit(X_train, y_train) y_pred_my = my_gb.predict(X_test) mse_my = mean_squared_error(y_test, y_pred_my)  # sklearn GB (обычный) sklearn_gb = GradientBoostingRegressor(n_estimators=100, learning_rate=0.1, max_depth=3, random_state=42) sklearn_gb.fit(X_train, y_train) y_pred_sklearn = sklearn_gb.predict(X_test) mse_sklearn = mean_squared_error(y_test, y_pred_sklearn)  # sklearn GB (низкий learning_rate) sklearn_gb_slow = GradientBoostingRegressor(n_estimators=300, learning_rate=0.03, max_depth=3, random_state=42) sklearn_gb_slow.fit(X_train, y_train) y_pred_slow = sklearn_gb_slow.predict(X_test) mse_slow = mean_squared_error(y_test, y_pred_slow)  # Случайный лес rf = RandomForestRegressor(n_estimators=100, max_depth=6, random_state=42) rf.fit(X_train, y_train) y_pred_rf = rf.predict(X_test) mse_rf = mean_squared_error(y_test, y_pred_rf)  # 4. Сортировка для гладких графиков sorted_idx = np.argsort(X_test.ravel()) X_plot = X_test[sorted_idx] y_true_plot = np.sin(X_plot).ravel() y_test_plot = y_test[sorted_idx] y_pred_my_plot = y_pred_my[sorted_idx] y_pred_sklearn_plot = y_pred_sklearn[sorted_idx] y_pred_slow_plot = y_pred_slow[sorted_idx] y_pred_rf_plot = y_pred_rf[sorted_idx]  # 5. Визуализация plt.figure(figsize=(12, 6)) plt.plot(X_plot, y_true_plot, label="True function sin(x)", color='green', linestyle='--') plt.plot(X_plot, y_pred_my_plot, label=f"My GB (MSE={mse_my:.4f})", color='red') plt.plot(X_plot, y_pred_sklearn_plot, label=f"sklearn GB (0.1) (MSE={mse_sklearn:.4f})", color='blue') plt.plot(X_plot, y_pred_slow_plot, label=f"sklearn GB (0.03) (MSE={mse_slow:.4f})", color='orange') plt.plot(X_plot, y_pred_rf_plot, label=f"Random Forest (MSE={mse_rf:.4f})", color='purple') plt.scatter(X_plot, y_test_plot, label="Test data", s=10, alpha=0.3) plt.legend() plt.title("Сравнение бустинга (ручной и sklearn) и случайного леса") plt.grid(True) plt.tight_layout() plt.show()  

Что дальше?

Вдумчиво разбираемся в вопросах, но коротко и понятно рассказываем суть.

Следующий этап — уметь, понимая базу, отвечать на каверзные вопросы. Поэтому сохраняем их для будущей тренировки!

Материалы


В следующей части продолжим — начнем с ввода в глубокое обучение, а закончим разбором важнейших классических архитектур. Пока подписывайтесь и делитесь своими находками!


ссылка на оригинал статьи https://habr.com/ru/articles/921190/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *