У каждого наступает момент, когда нужно быстро освежить в памяти огромный пласт информации по всему ML. Причины разные — подготовка к собеседованию, начало преподавания или просто найти вдохновение.
Времени мало, объема много, цели амбициозные — нужно научиться легко и быстро объяснять, но так же не лишая полноты!
💻 Обращу внимание, самый действенный способ разобраться и запомнить — это своими руками поисследовать задачу! Это самое важное, оно происходит в секции с кодом. Поэтому попробуйте сами решить предложенную задачку и придумать свою!
Будет здорово получить ваши задачи и в следующих выпусках разобрать!
Мы продолжаем. Обязательно испытайте себя в предыдущей [1] части!

Я считаю самый полный и простой способ заполнить все пробелы — это взять хороший экзамен и ответить на все вопросы — понятно и быстро. А что бы запомнилось лучше — решить задачку. Приступим!
Сначала попробуйте сами быстро ответить, а потом после просмотра! Стало быстрее-понятнее объяснять?
Для более полного погружения в конце приложу важные ресурсы. Делитесь своими!
11. Анализ главных компонент. Связь с SVD. Теорема Эккарта-Янга. Как применять PCA на практике.
📌 Краткий ответ
PCA (Principal Component Analysis или анализ главных компонент) — метод понижения размерности, который:
-
Решает задачу апроксимации исходной матрицы, матрицами меньшего ранга (как например ALS)
-
Ее можно решать как задачу оптимизации функции ошибки MSE или конструктивно с SVD
-
Еще одна итерпретация — предсказание исходной матрицы скалярным произведением латентных векторов юзера и товара

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

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

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

-
Теорема Эккарта-Янга — утверждает, такая апроксимация внутри своего ранга по норме Фробениуса наименьшая!
🔬 Подробный разбор
На одном дыхании можно послушать тут или почитать хендбук. Отличный английский видеоформат .
💻 Сжимаем картинку через 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-модели делится на три ключевых этапа:
-
Обучение (train) — модель подбирает параметры на обучающей выборке.
-
Валидация (validation) — подбираем гиперпараметры на валидационной выборке.
-
Тест (test) — финальная оценка качества, только один раз.
Бывают, что тест опускают и финальное качество берут лучшее с валидации, но это ведет к переобучению гиперпараметров.
Переобучение — это ситуация, когда модель слишком хорошо подгоняется под обучающую выборку, но плохо обобщает на новые данные.
Чтобы меньше зависеть от разбиение train/val/test используют обучение по фолдам (k-fold cross-validation) — Обучается моделей. Датасет делиться на
частей, на
модель учиться, на оставшейся замеряется качество. Метрика усредняется.
Такой подход подход хоть устойчевее к шуму, но не всегда хочется делать бэггинг над этими
моделей. Обычно в начале EDA (exploratory data analysis или разведочный анализ) проверяют, что разбиение train/val/test подходит и учат одну модель.
Как понять, что модель начала переобучаться? (если неграмотно сохранять чекпоинты, можно остаться с переобученной моделью!)
-
Метрика на train продолжает падать, а на val начала возрастать. (Если нету графиков, то метрика на train сильно лучше, чем на val)
Как бороться?
-
Early stopping (если метрика на валидации не уменьшалась k шагов — остановиться)
-
Регуляризации —
/
, Dropout/BatchNorm, аугментация данных и тд
-
Выбросить совсем незначащие признаки (если их очень много, модель будет на них отвлекаться, может и сойдется, но дОльше!)
-
Больше данных насыпать
-
Бэггинг (Bagging, от bootstrap aggregating) — учим несколько моделей на бутсреп данных и объединяем => уменьшаем дисперсию ответа
Классическая симптомы переобучения (можно раньше времени спутать с мифическим Double Descent)

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

💻 Пример переобучения: наблюдаем за лоссом и предсказаниями
Смотрим, что происходит при увеличением эпох с лоссом и предсказаниями, если признаковое пространство недостаточно покрыто данными.
Я специалньо взял не очень много точек (=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 — учим
моделей, валидируем по очереди на каждом фолде.
-
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-образная есть оптимум! (При Double Descent это не выполняется!)

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

Теперь подробнее и по математически
Пусть:
-
— целевая переменная с шумом.
-
— объект из тестовой выборки.
— обучающая выборка
-
— алгоритм, обученный на случайной выборке
,
Тогда среднеквадратичная ошибка (MSE) алгоритма имеет вид:
Пояснение:
-
смещение (bias) предсказания алгоритма в точке
, усреднённого по всем возможным обучающим выборкам, относительно истинной зависимости
-
дисперсия (variance) предсказаний алгоритма в зависимости от обучающей выборки
-
неустранимый шум в данных.
🔬 Подробный разбор
Сначала наслаждаемся 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 и истинная
# Визуализация обучающих и тестовых данных на фоне регрессионной прямой 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 подмножества по признаку и порогу
, чтобы максимально уменьшить информативность (impurity) (чем ниже, тем обьекты ближе можно представить константным значением):
Процедура разбиения узла:
-
Перебираются все признаки
и возможные пороги
. Все элементы с признаком меньше порога попадают в левый узел, остальные вправо.
-
Для каждого разбиения вычисляется:
где — функция impurity:
-
Джини или энтропия (классификация)
-
-
дисперсия (регрессия)
-
-
Выбирается разбиение с наименьшей информативностью
После разбиения:
-
В каждом потомке заново пересчитывается «ответ» узла:
-
мода (класс) для классификации
-
среднее значение
для регрессии
-
Разбиение продолжается рекурсивно, пока не выполнены условия остановки:
-
достигнута макс. глубина
-
мало объектов (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 раз (Если предположить независимость базовых алгоритмов). Вывод утверждения для задачи регрессии. берем сложные, глубокие модели, чтобы уменьшить смещение (а дисперсию уменьшим с помощью беггинга!)
Стекинг (stacking): 1. Учим несколько (разной природы) моделей на разных фолдах, и потом на отдельном фолде учим мета модель .
Например:
Случайный лес (Random Forest) = бэггинг + метод случайных подпространств(=в каждом узле дерева выбирается случайное подмножество признаков):
Из какого кол-во признаков выбирать?
-
Много признаков
большая скоррелированность моделей (=маленький эффект от ансамблирования).
-
Мало признаков
модели слабые (=смещение увеличивается).
Практическая рекомендация — брать корень из числа всех признаков для классификации и треть признаков для регрессии.
🔬 Подробный разбор
💻 Сравнения ансамблёвых моделей
# Импорт всех нужных библиотек 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), метод случайных подпространств.
📌 Краткий ответ
Смотри прошлый билет!
💻 Поиск оптимального кол-ва случайных признаков. Несогласованность и точность моделей.
Видим, как с увеличением случайных признаков увеличивается согласованность ответов модели становятся скоррелированны
качество бесконечно не растет
.
# Будем оценивать дисперсию (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, логлосс) по предсказаниям текущего ансамбля.
🔬 Подробный разбор
📘 Общая идея бустинга
Очень круто, подробно и просто описано тут.
Имеем задачу регрессии или классификации с функцией потерь .
Хотим построить итоговую модель в виде:
Каждый новый слабый алгоритм обучается на остатках или антиградиенте предыдущей ошибки.
Градиентный бустинг (Gradient Boosting)
На шаге строим новую модель
, которая аппроксимирует антиградиент функции потерь по текущим предсказаниям:
После этого:
-
обучаем
на
-
выбираем шаг
-
обновляем:
Примеры градиентов:
-
MSE (регрессия):
То есть просто остатки! Совпадает с определением бустинга.
-
Log-loss (бинарная классификация):
Почему работает?
-
Рассматриваем задачу не как минимизация расстояния между вектором предсказания и истинных значений, а с точки зрения функции потерь!
-
Итеративно минимизируем функцию потерь — как градиентный спуск в функциональном пространстве (в
мерном пространстве,
— размер датасета). Каждый шаг — это добавление антиградиента к аргументу функции
уменьшение лосса.
-
Для 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()

Что дальше?

Вдумчиво разбираемся в вопросах, но коротко и понятно рассказываем суть.
Следующий этап — уметь, понимая базу, отвечать на каверзные вопросы. Поэтому сохраняем их для будущей тренировки!
Материалы
-
Сам список вопросов взял с одного из экзаменов
girafe.ai ~ Deep Learning School -
Хендбуки Яндекс — становятся все больше и больше. Местами может быть избыточно.
-
SelfEdu — мегакрутой. Также, который вдохновлялся (и я тоже!) Сергеем Николенко.
-
MLU-EXPLAIN — на досуге можно залипнуть в интерактив.
-
Классический и легко написанный учебник.
-
Забыл одно из самых важных — LLM очень помогают набросать код и проверить гипотезу
В следующей части продолжим — начнем с ввода в глубокое обучение, а закончим разбором важнейших классических архитектур. Пока подписывайтесь и делитесь своими находками!
ссылка на оригинал статьи https://habr.com/ru/articles/921190/
Добавить комментарий