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

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

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

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

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

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

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

? Глава 1: Модели, метрики и формула Байеса

0. Задача обучения с учителем. Регрессия, Классификация

? Краткий ответ
  • Обучение с учителем — это постановка задачи, при которой каждый объект обучающей выборки снабжён целевым значением y, и модель обучается приближать отображение f(x) \approx y.

  • Регрессия: если y \in \mathbb{R} (например, цена, температура).

  • Классификация: если y \in \{1, \dots, K\}, то есть класс или категория (например, диагноз, категория изображения).

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

Общая постановка задачи

В обучении с учителем задана обучающая выборка из пар

(x_i, y_i), \quad i = 1, \dots, n,

где x_i \in \mathbb{R}^d — вектор признаков, y_i — целевая переменная. Требуется построить алгоритм f(x), минимизирующий ошибку предсказания.


Регрессия

Если y_i \in \mathbb{R} или \mathbb{R}^k, задача называется регрессией.
Модель должна предсказывать численное значение. Типичные функции потерь:

  • Mean Squared Error (MSE)

  • Mean Absolute Error (MAE)

Примеры:

  • Прогнозирование цены недвижимости

  • Оценка спроса на товар


Классификация

Если y_i \in \{1, \dots, K\} — задача классификации.
В простейшем случае — бинарная классификация (например, "да/нет").
При

K > 2многоклассовая. Также существует multi-label классификация, когда одному объекту соответствуют несколько меток.

Модель выдает либо вероятности по классам (soft), либо сразу метку (hard). Часто оптимизируют logloss или используют surrogate-функции.

Примеры:

  • Распознавание рукописных цифр (0–9)

  • Классификация e-mail как "спам / не спам"

? Отрисовываем предсказания линейной и логистической регресии

Заглянем чуть дальше и покажем, пример решения задачи регресии (линейной регрессией) и классификации (логистической регрессией)

from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.datasets import make_regression, make_classification

# --- Регрессия ---
X_reg, y_reg = make_regression(n_samples=100, n_features=2, noise=0.1, random_state=43) # [100, 2], [100]
reg = LinearRegression().fit(X_reg, y_reg)

# --- Классификация ---
X_clf, y_clf = make_classification(n_samples=100, n_features=2, n_classes=2, n_redundant=0, random_state=43) # [100, 2], [100]
clf = LogisticRegression().fit(X_clf, y_clf)


# --- Отрисовка ---
import matplotlib.pyplot as plt
import numpy as np

# Создаем фигуру с двумя подграфиками
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))

# --- Регрессия ---
# Создаем сетку точек для линии регрессии
x_grid = np.linspace(X_reg[:, 0].min(), X_reg[:, 0].max(), 100).reshape(-1, 1)
# Добавляем второй признак (среднее значение) # отрисовать только 1 признак можем => по второму усредним! 
# так делать очень плохо! но для игрушечного примера - ок!
x_grid_full = np.column_stack([x_grid, np.full_like(x_grid, X_reg[:, 1].mean())])
y_pred = reg.predict(x_grid_full)

# Визуализация регрессии
ax1.scatter(X_reg[:, 0], y_reg, alpha=0.5, label='Данные')
ax1.plot(x_grid, y_pred, 'r-', label='Линия регрессии')
ax1.set_title('Регрессия')
ax1.set_xlabel('Признак 1')
ax1.set_ylabel('Целевая переменная')
ax1.legend()

# --- Классификация ---
# Создаем сетку точек для границы принятия решений
x_min, x_max = X_clf[:, 0].min() - 0.5, X_clf[:, 0].max() + 0.5
y_min, y_max = X_clf[:, 1].min() - 0.5, X_clf[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02),
                     np.arange(y_min, y_max, 0.02))

# Предсказываем классы для всех точек сетки
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)

# Визуализация классификации
ax2.contourf(xx, yy, Z, alpha=0.3, cmap='viridis')
ax2.contour(xx, yy, Z, [0.5], colors='red', linewidths=2)

scatter = ax2.scatter(X_clf[:, 0], X_clf[:, 1], c=y_clf, cmap='viridis', alpha=0.5)
ax2.set_title('Классификация')
ax2.set_xlabel('Признак 1')
ax2.set_ylabel('Признак 2')
ax2.legend(*scatter.legend_elements(), title="Классы")

plt.tight_layout()
plt.show()

1. Метрики классификации: accuracy, balanced accuracy, precision, recall, f1-score, ROC-AUC, расширения для многоклассовой классификации

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

Для задачи бинарной классификации (y \in {0, 1}) можно построить матрицу ошибок и по ним посчитать метрики:

Метрика

Формула

Смысл

Accuracy

Accuracy = (TP + TN) / (TP + TN + FP + FN)

Общая доля правильных предсказаний

Balanced Accuracy

0.5 * (TPR + TNR) = 0.5 * (TP / (TP + FN) + TN / (TN + FP))

Усреднённая точность по классам при дисбалансе

Precision

TP / (TP + FP)

Доля верных положительных предсказаний

Recall

TP / (TP + FN)

Доля найденных положительных среди всех реальных

F1(b)-score

2 * P * R / (P + R) = 2 / (1/P + 1/R) = [b=1] = (b^2 + 1) / (b^2 r^-1 + r^-1)

Баланс между precision и recall

AUC

Доля правильно упорядоченных пар среди (Negative, Positive)

Площадь под ROC-кривой (TPR (y) vs FPR (x) при разных порогах)

Легче запомнить, как TPR = recall позитивного класса, а FPR = 1 - recall негативного класса !

Как по мне самое простое и полезное переформулировка - это доля правильно упорядоченных пар среди (Negative, Positive)

  • Самый плохой случай - AUC=0.5 иначе можно реверснуть!

  • Лучшая метрика AUC=1


Для многоклассовой классификации - считаем для каждого класса one-vs-rest матрицу ошибок. Далее либо микро-усредняем (суммируем компоненты и считаем метрку) или макро-усреднение (по классам считаем и усредняем)

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

Очень подробно расписано здесь!

Обратите внимание так же на:

  • Recall@k, Precision@k

  • Average Precision

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

? Визуализируем AUC ROC

from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.dummy import DummyClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt

# --- 1. Синтетические, "грязные" данные ---
X, y = make_classification(
    n_samples=1000,
    n_features=20,
    n_informative=5,
    n_redundant=4,
    n_classes=2,
    weights=[0.75, 0.25],  # дисбаланс классов
    flip_y=0.1,            # 10% меток шумные
    class_sep=0.8,         # классы частично пересекаются
    random_state=42
)

# --- 2. Деление на train/test ---
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=42)

# --- 3. Логистическая регрессия ---
model = LogisticRegression(max_iter=1000).fit(X_tr, y_tr)
y_prob = model.predict_proba(X_te)[:, 1]
fpr_model, tpr_model, _ = roc_curve(y_te, y_prob)
auc_model = roc_auc_score(y_te, y_prob)

# --- 4. Dummy-классификатор (стратегия stratified) ---
dummy = DummyClassifier(strategy='stratified', random_state=42).fit(X_tr, y_tr)
y_dummy_prob = dummy.predict_proba(X_te)[:, 1]
fpr_dummy, tpr_dummy, _ = roc_curve(y_te, y_dummy_prob)
auc_dummy = roc_auc_score(y_te, y_dummy_prob)

# --- 5. Визуализация ROC-кривых ---
plt.figure(figsize=(8, 6))
plt.plot(fpr_model, tpr_model, label=f"Logistic Regression (AUC = {auc_model:.2f})")
plt.plot(fpr_dummy, tpr_dummy, linestyle='--', label=f"Dummy Stratified (AUC = {auc_dummy:.2f})")
plt.plot([0, 1], [0, 1], 'k:', label="Random Guess (AUC = 0.50)")

plt.xlabel("False Positive Rate (FPR)")
plt.ylabel("True Positive Rate (TPR)")
plt.title("ROC-кривая: логистическая регрессия vs случайный классификатор")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()

2. Метрики регрессии: MSE, MAE, R²

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

Метрика

Формула

Смысл

MSE

mean((y_true - y_pred) ** 2)

Среднеквадратичная ошибка. Наказывает большие ошибки сильнее.

MAE

mean(abs(y_true - y_pred))

Средняя абсолютная ошибка. Интерпретируется в исходных единицах.

R² score

1 - MSE_model / MSE_const

На сколько лучше константного предсказания(=среднее при минимизации MSE) . От 0 до 1 (может быть < 0 при плохой модели).

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

MSE (Mean Squared Error)
Наиболее распространённая функция потерь. Ошибки возводятся в квадрат, что делает метрику чувствительной к выбросам.
MSE = mean((y - ŷ) ** 2)

MAE (Mean Absolute Error)
Абсолютное отклонение между предсказаниями и истиной. Менее чувствительна к выбросам(=робастнее), хорошо интерпретируется (в тех же единицах, что и целевая переменная).
MAE = mean(|y - ŷ|)

Huber Loss — гибрид между MSE и MAE: локально квадратичный штраф, дальше линейный.

R² (коэффициент детерминации)
Показывает, какую часть дисперсии целевой переменной объясняет модель.
R² = 1 - (MSE_model / MSE_const)
Где MSE_const — ошибка наивной модели, предсказывающей среднее.

? Сравниваем функции ошибок
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
from sklearn.datasets import make_regression
from sklearn.linear_model import LinearRegression, HuberRegressor
from sklearn.model_selection import train_test_split

# Данные
X, y = make_regression(n_samples=500, noise=15, random_state=42)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=42)

# --- Линейная регрессия ---
model_lr = LinearRegression().fit(X_tr, y_tr)
y_pred_lr = model_lr.predict(X_te)

print("=== Linear Regression ===")
print("MSE:", mean_squared_error(y_te, y_pred_lr))
print("MAE:", mean_absolute_error(y_te, y_pred_lr))
print("R²:", r2_score(y_te, y_pred_lr))

# --- Huber-регрессия ---
model_huber = HuberRegressor().fit(X_tr, y_tr)
y_pred_huber = model_huber.predict(X_te)

print("\n=== Huber Regressor ===")
print("MSE:", mean_squared_error(y_te, y_pred_huber))
print("MAE:", mean_absolute_error(y_te, y_pred_huber))
print("R²:", r2_score(y_te, y_pred_huber))
=== Linear Regression ===
MSE: 334.45719591398216
MAE: 14.30958669001259
R²: 0.988668164971938

=== Huber Regressor ===
MSE: 367.2515287731075
MAE: 15.169297076822216
R²: 0.9875570512797974

Эти метрики - они могут быть как лосс функциями, так и бизнесс метриками! Какой лосс минимизировать, нужно понять какая целевая бизнес метрика!

import numpy as np
import matplotlib.pyplot as plt

# Ошибки (residuals)
errors = np.linspace(-2, 2, 400)

# MSE: квадратичные потери
mse_loss = errors ** 2

# MAE: абсолютные потери
mae_loss = np.abs(errors)

# Huber loss
delta = 1.0
huber_loss = np.where(
    np.abs(errors) <= delta,
    0.5 * errors ** 2,
    delta * (np.abs(errors) - 0.5 * delta)
)

# Визуализация
plt.figure(figsize=(8, 6))
plt.plot(errors, mse_loss, label='MSE Loss', color='red')
plt.plot(errors, mae_loss, label='MAE Loss', color='blue')
plt.plot(errors, huber_loss, label='Huber Loss (δ = 1.0)', color='green')
plt.xlabel("Ошибка (residual)")
plt.ylabel("Значение функции потерь")
plt.title("Сравнение MSE, MAE и Huber Loss")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()

3. Оценка максимального правдоподобия (MLE), связь с регрессией и классификацией

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

MLE (Maximum Likelihood Estimation) — метод оценки параметров, при котором максимизируется вероятность наблюдаемых данных.

Целевая переменная (и параметры модели) рассматриваются как слуайные величины.

Фиксируем класс моделей (например линейные) и ищем максимально правдоподобную модель среди них (=> нужно определить вероятность наблюдаемого семпла и при выводк воспользоваться независимостью семплов)!

  • В линейной регрессии (при нормальном шуме) MLE ⇔ минимизация MSE

  • В логистической регрессии MLE ⇔ минимизация логлосса

  • Регуляризация вносит априорные предположения (MAP) на веса. При нормальном, получаем L_2 регуляризацию, при лаплассе L_1.

? Формальные выводы

Детальнее можно узнать в конце тетрадке.

✍ MLE и линейная регрессия

Предполагаем, что целевая переменная yᵢ имеет нормальное распределение с центром в xᵢᵀw и дисперсией σ²:

y_i \sim \mathcal{N}(x_i^\top w, \sigma^2)

Тогда правдоподобие всей выборки:

p(y | X, \theta) = \prod_i \mathcal{N}(y_i | x_i^\top w, \sigma^2)

Берём логарифм:

\log p(y | X, \theta) = \sum_i \log \left( \frac{1}{\sqrt{2\pi} \sigma} \exp\left( -\frac{(y_i - x_i^\top w)^2}{2\sigma^2} \right) \right)

Раскрываем сумму:

= \sum_i \left( -\frac{1}{2} \log(2\pi) - \log \sigma - \frac{(y_i - x_i^\top w)^2}{2\sigma^2} \right)

→ максимизация логарифма правдоподобия эквивалентна минимизации:

\sum_i (y_i - x_i^\top w)^2

✅ Вывод:

Метод максимального правдоподобия (MLE) для линейной регрессии приводит к функции потерь MSE — среднеквадратичной ошибке.

Регуляризация (например, Ridge(=L_2)) возникает при добавлении априорного распределения на веса — это уже MAP, не MLE.


✍ MLE и логистическая регрессия

В бинарной классификации целевая переменная yᵢ ∈ {0, 1} - для отклонения используем распределние Бернули.

Предполагаем:

P(y_i = 1 | x_i, w) = \sigma(x_i^\top w) = \frac{1}{1 + e^{-x_i^\top w}}

Правдоподобие всей выборки:

L(w) = \prod_i \left[ \sigma(x_i^\top w) \right]^{y_i} \cdot \left[ 1 - \sigma(x_i^\top w) \right]^{1 - y_i}

Логарифм правдоподобия:

\log L(w) = \sum_i \left[ y_i \log \sigma(x_i^\top w) + (1 - y_i) \log (1 - \sigma(x_i^\top w)) \right]

✅ Вывод:

Максимизация логарифма правдоподобия ⇔ минимизация log-loss (логистической функции потерь)


✍ Что меняется с регуляризацией: MAP (Maximum A Posteriori)

Добавим априорное распределение на параметры:

  • L2-регуляризация ⇔ априорное w ∼ ?(0, λ⁻¹I)

  • L1-регуляризация ⇔ априорное w ∼ Laplace(0, b)

MAP-оценка:

\log P(w | X, y) ∝ \log L(w) + \log P(w)

→ Это и есть MLE + регуляризация:

  • MLE ⇔ логлосс

  • MAP ⇔ логлосс + регуляризация

? Баесовский вывод двух нормальных распределений

Задача

Пусть параметр w неизвестен и:

  • Prior: w \sim \mathcal{N}(\mu_0, \sigma_0^2)

  • Likelihood: y \sim \mathcal{N}(\mu_1, \sigma_1^2) — наблюдение, связанное с w

Найти posterior P(w \mid y)

? Шаг 1: формула Байеса

По определению:

P(w | y) \propto P(y | w) \cdot P(w)

Логарифмируем обе части:

\log P(w | y) = \log P(y | w) + \log P(w) + \text{const}

? Шаг 2: подставляем нормальные распределения

  1. Prior:

\log P(w) = -\frac{1}{2\sigma_0^2} (w - \mu_0)^2 + \text{const}
  1. Likelihood (в терминах w, фиксируя y = \mu_1):

\log P(y | w) = -\frac{1}{2\sigma_1^2} (w - \mu_1)^2 + \text{const}

? Шаг 3: складываем логарифмы

\log P(w | y) = -\frac{1}{2\sigma_0^2} (w - \mu_0)^2 - \frac{1}{2\sigma_1^2} (w - \mu_1)^2 + \text{const}

Это — квадратичная функция по w, то есть логарифм нормального распределения. Следовательно, сам posterior — тоже нормальный:

P(w | y) = \mathcal{N}(\mu_{\text{post}}, \sigma_{\text{post}}^2)

✅ Вывод: параметры апостериорного распределения

\sigma_{\text{post}}^2 = \left( \frac{1}{\sigma_0^2} + \frac{1}{\sigma_1^2} \right)^{-1}\mu_{\text{post}} = \sigma_{\text{post}}^2 \left( \frac{\mu_0}{\sigma_0^2} + \frac{\mu_1}{\sigma_1^2} \right)

Отрисуем!

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm

# Ось параметра w
w = np.linspace(-5, 5, 500)

# Заданные параметры
mu0, sigma0 = 0, 1     # prior: N(0, 1)
mu1, sigma1 = 2, 1     # likelihood: N(2, 1)

# Распределения
prior = norm.pdf(w, loc=mu0, scale=sigma0)
likelihood = norm.pdf(w, loc=mu1, scale=sigma1)

# Постериорное распределение — аналитически
sigma_post_sq = 1 / (1/sigma0**2 + 1/sigma1**2)
mu_post = sigma_post_sq * (mu0/sigma0**2 + mu1/sigma1**2)
posterior = norm.pdf(w, loc=mu_post, scale=np.sqrt(sigma_post_sq))

# Визуализация
plt.figure(figsize=(10, 6))
plt.plot(w, prior, label=f"Prior N({mu0}, {sigma0**2})", color='green')
plt.plot(w, likelihood, label=f"Likelihood N({mu1}, {sigma1**2})", color='blue')
plt.plot(w, posterior, label=f"Posterior N({mu_post:.2f}, {sigma_post_sq:.2f})", color='red')

plt.axvline(mu0, color='green', linestyle=':')
plt.axvline(mu1, color='blue', linestyle=':')
plt.axvline(mu_post, color='red', linestyle='--', label=f"MAP = {mu_post:.2f}")

plt.title("Байесовский вывод: Posterior = Prior × Likelihood")
plt.xlabel("w")
plt.ylabel("Плотность")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()

На самом деле, даже для двух многмерных нормальных распределений с разными дисперсиями - верно следующее, их апостериальное распределение тоже нормальное!

4. Наивный байесовский классификатор

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

Наивный байесовский классификатор предполагает, что все признаки условно независимы при фиксированном классе:

P(y | x_1, ..., x_d) ∝ P(y) \cdot \prod_{i=1}^d P(x_i | y)

Обучение: оцениваем P(y) и P(xᵢ | y) по каждому признаку.
Работает быстро, устойчив к малым выборкам, можно задавать разные распределения.

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

В логарифмической форме:

\log P(y | x) ∝ \log P(y) + \sum_i \log P(x_i | y)

Можно использовать:

  • Гауссовское распределениеGaussianNB

  • Ядерную оценку плотности (KDE) — сглаженные вероятности

  • Экспоненциальное, Лапласовское и др.

Преимущество подхода: можно подставлять разные распределения под разные признаки.

? Наивный Баейс на практике с разными распределениями признаков (KDE)

Полная тетрадка тут.

Для простоты будем работать не со всеми 4 признаками, а с двумя!

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KernelDensity
from sklearn.metrics import accuracy_score
from sklearn.naive_bayes import GaussianNB
from scipy.special import logsumexp

# --- 1. Загрузка и подготовка данных
iris = load_iris()
X = iris.data[:, [2, 3]]  # два признака: длина и ширина лепестка
y = iris.target
feature_names = np.array(iris.feature_names)[[2, 3]]
class_names = iris.target_names

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# --- 2. Обёртка KDE
class KDEWrapper:
    def __init__(self, data):
        self.kde = KernelDensity(kernel='gaussian', bandwidth=0.2).fit(data[:, None])

    def logpdf(self, x):
        return self.kde.score_samples(x[:, None])
    

#  --- 3. NaiveBayes из тетрадки
class NaiveBayes:
    def fit(self, X, y, sample_weight=None, distributions=None):
        self.unique_labels = np.unique(y)
        if distributions is None:
            distributions = [KDEWrapper] * X.shape[1]
        assert len(distributions) == X.shape[1]
        self.conditional_feature_distributions = {}
        for label in self.unique_labels:
            dists = []
            for i in range(X.shape[1]):
                dists.append(distributions[i](X[y == label, i]))
            self.conditional_feature_distributions[label] = dists
        self.prior_label_distibution = {l: np.mean(y == l) for l in self.unique_labels}

    def predict_log_proba(self, X):
        log_proba = np.zeros((X.shape[0], len(self.unique_labels)))
        for i, label in enumerate(self.unique_labels):
            for j in range(X.shape[1]):
                log_proba[:, i] += self.conditional_feature_distributions[label][j].logpdf(X[:, j])
            log_proba[:, i] += np.log(self.prior_label_distibution[label])
        log_proba -= logsumexp(log_proba, axis=1)[:, None]
        return log_proba

    def predict(self, X):
        return self.unique_labels[np.argmax(self.predict_log_proba(X), axis=1)]
# --- 4. Обучение моделей
model_kde = NaiveBayes()
model_kde.fit(X_train, y_train, distributions=[KDEWrapper, KDEWrapper])
y_pred_kde = model_kde.predict(X_test)

model_gnb = GaussianNB()
model_gnb.fit(X_train, y_train)
y_pred_gnb = model_gnb.predict(X_test)

print("KDE NB Accuracy:", accuracy_score(y_test, y_pred_kde))
print("GaussianNB Accuracy:", accuracy_score(y_test, y_pred_gnb))

# KDE NB Accuracy: 1.0
# GaussianNB Accuracy: 1.0
# --- 5. Визуализация KDE-плотностей
def plot_kde_and_gaussian_densities(X_data, y_data):
    fig, axes = plt.subplots(1, 2, figsize=(12, 4))
    
    # Обучим GaussianNB — он сам оценит параметры
    gnb = GaussianNB()
    gnb.fit(X_data, y_data)

    for i in range(X_data.shape[1]):
        ax = axes[i]
        for label in np.unique(y_data):
            x_vals = X_data[y_data == label, i]
            grid = np.linspace(x_vals.min() * 0.9, x_vals.max() * 1.1, 500)

            # --- KDE
            kde = KernelDensity(kernel='gaussian', bandwidth=0.2).fit(x_vals[:, None])
            ax.plot(grid, np.exp(kde.score_samples(grid[:, None])), label=f'{class_names[label]} (KDE)', linestyle='-')

            # --- Gauss via GaussianNB
            mu = gnb.theta_[label, i]
            sigma = np.sqrt(gnb.var_[label, i])
            ax.plot(grid, norm.pdf(grid, mu, sigma), label=f'{class_names[label]} (Gauss)', linestyle='--')


        ax.set_title(f'Плотности для {feature_names[i]}')
        ax.set_xlabel('Значение признака')
        ax.set_ylabel('Плотность')
        ax.legend()
        ax.grid()

    plt.tight_layout()
    plt.show()

# --- 6. Визуализация границ решений
def plot_decision_boundary(X_data, y_data, model, title):
    x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
    y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
    xx, yy = np.meshgrid(np.linspace(x_min, x_max, 300),
                         np.linspace(y_min, y_max, 300))
    grid = np.c_[xx.ravel(), yy.ravel()]
    Z = model.predict(grid).reshape(xx.shape)

    plt.figure(figsize=(8, 6))
    plt.contourf(xx, yy, Z, alpha=0.3, cmap='Accent')
    plt.contour(xx, yy, Z, levels=np.arange(0, 4), colors='k', linewidths=0.5)
    for label in np.unique(y_train):
        plt.scatter(X_data[y_data == label, 0], X_data[y_data == label, 1],label=class_names[label], s=40)
    plt.xlabel(feature_names[0])
    plt.ylabel(feature_names[1])
    plt.title(title)
    plt.legend()
    plt.grid(True)
    plt.tight_layout()
    plt.show()

# train
plot_kde_and_gaussian_densities(X_train, y_train)
plot_decision_boundary(X_train, y_train, model_kde, "Наивный Байес с KDE")
plot_decision_boundary(X_train, y_train, model_gnb, "GaussianNB (Гауссовский Наивный Байес)")
# test
plot_kde_and_gaussian_densities(X_test, y_test)
plot_decision_boundary(X_test, y_test, model_kde, "Наивный Байес с KDE")
plot_decision_boundary(X_test, y_test, model_gnb, "GaussianNB (Гауссовский Наивный Байес)")

5. Метод ближайших соседей

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

Метод k ближайших соседей (k-NN) — это ленивый классификатор, который:

  • не обучается явно

  • при предсказании ищет k ближайших объектов в обучающей выборке

  • голосует за класс большинства (или усредняет — в регрессии).

Работает по метрике (например, евклидовой), чувствителен к масштабу и шуму.

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

При классификации:

ŷ(x) = argmax_c ∑ I(yᵢ = c) для xᵢ ∈ N_k(x)

Плюсы:

  • не требует обучения,

  • хорошо работает на небольших данных.

Минусы:

  • не масштабируется (хранит всё),

  • чувствителен к размерности и шуму,

  • требует нормализации признаков.

Гиперпараметры:

  • k — число соседей (подбирается на валидации),

  • metric — метрика расстояния (евклидово, косинусное и т.д.)

? Пишем свой KNN и сравниваемся с библиотечным на MNIST
import numpy as np
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
from scipy.stats import mode

# --- 1. Загружаем данные
digits = load_digits()
X = digits.data
y = digits.target

# Масштабирование
X = StandardScaler().fit_transform(X)

# Разделение
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=42)

# --- 2. Своя реализация k-NN (евклидовая метрика)
class MyKNN:
    def __init__(self, n_neighbors=5):
        self.k = n_neighbors

    def fit(self, X, y):
        self.X_train = X
        self.y_train = y

    def predict(self, X):
        predictions = []
        for x in X:
            dists = np.linalg.norm(self.X_train - x, axis=1)
            nearest = np.argsort(dists)[:self.k]
            labels = self.y_train[nearest]
            pred = mode(labels, keepdims=False).mode 
            predictions.append(pred)
        return np.array(predictions)

# --- 3. Обучение и сравнение
# sklearn
sk_knn = KNeighborsClassifier(n_neighbors=5)
sk_knn.fit(X_tr, y_tr)
y_pred_sk = sk_knn.predict(X_te)
acc_sk = accuracy_score(y_te, y_pred_sk)

# наш
my_knn = MyKNN(n_neighbors=5)
my_knn.fit(X_tr, y_tr)
y_pred_my = my_knn.predict(X_te)
acc_my = accuracy_score(y_te, y_pred_my)

assert np.isclose(acc_sk, acc_my, rtol=1e-6), 'Точности не совпдают!'
print(f"{acc_sk=} {acc_my=}")
# acc_sk=0.9777777777777777 acc_my=0.9777777777777777

? Глава 2: Почему линейная модель — это не просто прямая

6. Линейная регрессия. Формулировка задачи для случая функции потерь MSE. Аналитическое решение. Теорема Гаусса-Маркова. Градиентный подход в линейной регрессии.

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

Линейная регрессия минимизирует MSE:

L(w) = \|Xw - y\|^2
  • Аналитически: \hat{w} = (X^\top X)^{-1} X^\top y

  • Теорема Гаусса-Маркова: это наилучшая линейная несмещённая оценка при стандартных предположениях (BLUE)

  • При больших данных: используется градиентный спуск

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

? Постановка задачи

У нас есть:

  • X \in \mathbb{R}^{n \times d} — матрица признаков;

  • y \in \mathbb{R}^n — целевая переменная;

  • w \in \mathbb{R}^d — веса модели.

Цель: минимизировать среднеквадратичную ошибку:

L(w) = \|Xw - y\|^2 = (Xw - y)^\top (Xw - y)

? Вывод аналитического решения

Выпишем градиент по w:

\nabla_w L(w) = 2 X^\top (Xw - y)

Приравниваем к нулю:

X^\top (Xw - y) = 0

Раскрываем скобки:

X^\top X w = X^\top y

Предполагая, что X^\top X обратима:

\hat{w} = (X^\top X)^{-1} X^\top y

? Теорема Гаусса-Маркова (формулировка)

Если:

  1. модель линейна по параметрам: y = Xw + \varepsilon;

  2. ошибки \varepsilon имеют нулевое среднее;

  3. одинаковую дисперсию \text{Var}(\varepsilon) = \sigma^2 I;

  4. некоррелированы между собой;

→ тогда \hat{w} (из нормального уравнения) — наилучшая линейная несмещённая оценка (BLUE).

Термин BLUE (Best Linear Unbiased Estimator) — это сокращение:

  1. Linear (линейная):
    Оценка \hat{w} — это линейная функция от y:

    \hat{w} = A y

    где A = (X^\top X)^{-1} X^\top

  2. Unbiased (несмещённая):
    Среднее значение оценки совпадает с истинным параметром:

    \mathbb{E}[\hat{w}] = w
  3. Best (наилучшая):
    Из всех линейных и несмещённых оценок, \hat{w} имеет наименьшую дисперсию:

    \text{Var}(\hat{w}) \text{ минимальна среди всех линейных несмещённых оценок}
? Аналитическое и градиентное решение поиска весов линейной модели + график
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_regression
from sklearn.metrics import mean_squared_error

# --- Данные
X_raw, y = make_regression(n_samples=300, n_features=1, noise=15, random_state=42)
X = np.hstack([X_raw, np.ones((X_raw.shape[0], 1))])  # добавим bias

# --- Аналитическое решение
w_analytic = np.linalg.inv(X.T @ X) @ X.T @ y
y_pred_analytic = X @ w_analytic

# --- Градиентный спуск
w = np.zeros(X.shape[1])
lr = 0.01
losses = []

for _ in range(1000):
    grad = 2 * X.T @ (X @ w - y) / len(y)
    w -= lr * grad
    losses.append(mean_squared_error(y, X @ w))

y_pred_gd = X @ w

# --- Сравнение
print("MSE (аналитика):", mean_squared_error(y, y_pred_analytic))
print("MSE (градиент):", mean_squared_error(y, y_pred_gd))

# --- Визуализация
plt.figure(figsize=(10, 5))

# 1. Предсказания
plt.subplot(1, 2, 1)
plt.scatter(X_raw, y, s=20, alpha=0.6, label='Данные')
plt.plot(X_raw, y_pred_analytic, label='Аналитическое решение', color='green')
plt.plot(X_raw, y_pred_gd, label='Градиентный спуск', color='red', linestyle='--')
plt.title("Сравнение решений")
plt.xlabel("X")
plt.ylabel("y")
plt.legend()
plt.grid()

# 2. Потери во времени
plt.subplot(1, 2, 2)
plt.plot(losses, label="MSE (градиент)")
plt.title("Сходимость градиента")
plt.xlabel("Итерации")
plt.ylabel("MSE")
plt.grid()
plt.tight_layout()
plt.show()
# MSE (аналитика): 230.84267462302407
# MSE (градиент): 230.84267462302407

7. Регуляризация в линейных моделях: L_1 ,L_2 их свойства. Вероятностная интерпретация.

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

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

  • L2-регуляризация (Ridge):

    \text{Loss}(w) = \text{MSE}(w) + \lambda \|w\|_2^2
  • L1-регуляризация (Lasso):

    \text{Loss}(w) = \text{MSE}(w) + \lambda \|w\|_1
  • L2 сглаживает и уменьшает веса

  • L1 приводит к разреженным решениям (обнуляет ненужные признаки)

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

L_2 - Ridge, L_1 - Lasso, Elastic Net - комбинация.

? Вероятностная интерпретация

Добавление регуляризатора эквивалентно введению априорного распределения на параметры (подробнее в 3 вопросе о MLE):

  • L2 = Gaussian prior:

    P(w) \sim \mathcal{N}(0, \sigma^2 I)\Rightarrow \text{MAP} \propto \log L(w) - \lambda \|w\|_2^2
  • L1 = Laplace prior:

    P(w) \sim \text{Laplace}(0, b)\Rightarrow \text{MAP} \propto \log L(w) - \lambda \|w\|_1

→ То есть регуляризация = байесовская MAP-оценка, если мы знаем prior на веса.


Почему при зануляются веса?

Очень популярный и важный вопрос! Изобразим уровни потерь по отдельности двух частей лосса!

Предпложим противное. Пусть t^* опитимум пересечения и он не на осях координат. Из-за выпуклости двух фигур, найдется пересечения внутри, а по нему можно уже подняться вверх, сохранив ошибку по L_1 и уменьшить MSE ! Второй заумный аргумент .

Упрощение 2-аргумента: две фигуры выпуклые и имеют единственную касательную (помимо L_1 в точках на осях!), тогда в точке касания можно провести разделяющую прямую!

А это значит, что оси у элипса у MSE параллельны фиксированному направлению, а вероятность таких направлений (на одну размерность меньше=) равна нулю!

? Сравниваем веса моделей с L1 и L2

Обучим две модельки с разными регулизаторами на данных с 8 из 10 шумных признаками. В идеали избавиться(иметь вес ноль) от всех неинформативных признаков!

# Генерируем данные: 2 полезных признака + 8 шумовых

from sklearn.linear_model import Ridge, Lasso
from sklearn.metrics import mean_squared_error
import numpy as np
import matplotlib.pyplot as plt

# --- Параметры
n_samples = 100
n_features = 10
n_informative = 2  # только два признака "полезные"

# --- Генерация данных
np.random.seed(42)
X = np.random.randn(n_samples, n_features)
true_coefs = np.zeros(n_features)
true_coefs[:n_informative] = [3, -2]  # только первые 2 признака значимы

# Целевая переменная с шумом
y = X @ true_coefs + np.random.normal(0, 1.0, size=n_samples)

# --- Обучение моделей
ridge = Ridge(alpha=1.0)
lasso = Lasso(alpha=0.1)

ridge.fit(X, y)
lasso.fit(X, y)

# --- Сравнение весов
x_idx = np.arange(n_features)
plt.figure(figsize=(10, 4))
plt.stem(x_idx, true_coefs, linefmt="gray", markerfmt="go", basefmt=" ", label="True")
plt.stem(x_idx, ridge.coef_, linefmt="b-", markerfmt="bo", basefmt=" ", label="Ridge")
plt.stem(x_idx, lasso.coef_, linefmt="r-", markerfmt="ro", basefmt=" ", label="Lasso")
plt.xticks(ticks=x_idx)
plt.title("L1 vs L2: 2 информативных признака, остальные шум")
plt.xlabel("Индекс признака")
plt.ylabel("Вес")
plt.legend()
plt.tight_layout()
plt.show()

# --- Подсчёт зануленных весов
ridge_zeros = np.sum(np.abs(ridge.coef_) < 1e-4)
lasso_zeros = np.sum(np.abs(lasso.coef_) < 1e-4)
print(f"{ridge_zeros=}, f{lasso_zeros=}")
# ridge_zeros=np.int64(0), flasso_zeros=np.int64(5)

8. Логистическая регрессия. Эквивалентность подходов MLE и минимизации логистических потерь.

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

Логистическая регрессия — это модель предсказывающая вероятность класса y = 1:

P(y = 1 | x) = \sigma(x^\top w), \quad \sigma(z) = \frac{1}{1 + e^{-z}}

MLE: максимизация логарифма правдоподобия ⇔ минимизация log-loss (обычно для бинарной классификации) = cross-entropy-loss (обычно для мультиклассовой классификации)

\log L(w) = \sum_i y_i \log p_i + (1 - y_i) \log(1 - p_i)

или

\mathcal{L}(W) = - \sum_{i=1}^{n} \sum_{k=1}^{K} p_{i, k} \cdot \log \hat{p}_{i,k} | обычно, p_{i_{fix}, k} - ровно одна 1 и остальные нули.

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

? Вывод: MLE для логистической регрессии

Обозначим:

  • y_i \in \{0, 1\}

  • p_i = \sigma(x_i^\top w)

Правдоподобие:

L(w) = \prod_i p_i^{y_i} (1 - p_i)^{1 - y_i}

Логарифмируем:

\log L(w) = \sum_i y_i \log p_i + (1 - y_i) \log(1 - p_i)

Подставляем p_i = \sigma(x_i^\top w):

И получаем логистическую функцию потерь:

\mathcal{L}(w) = - \sum_i \left[
  y_i \log \sigma(x_i^\top w) + (1 - y_i) \log(1 - \sigma(x_i^\top w))
\right]
? Почему сырая линейка плоха в классификации? А лог-рег хорош?

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

Но дальше мы узнаем о SVM, и увидим что не обязательно приводить выход модели в диапозон [0, 1] !

# Пример: класс 0 сконцентрирован в одном месте, класс 1 — сильно растянут вправо

# Класс 0 — 50 точек около x = 0
X0 = np.random.normal(loc=0, scale=0.5, size=(50, 1))
y0 = np.zeros(50)

# Класс 1 — 50 точек с растущим x (от 10 до 500)
# X1 = np.linspace(5, 25, 10).reshape(-1, 1)
X1 = np.linspace(10, 500, 10).reshape(-1, 1)
y1 = np.ones(10)

# Объединяем
X_all = np.vstack([X0, X1])
y_all = np.concatenate([y0, y1])

# Обучаем модели
linreg = LinearRegression().fit(X_all, y_all)
logreg = LogisticRegression().fit(X_all, y_all)

# Предсказания на сетке
x_grid = np.linspace(-2, 30, 500).reshape(-1, 1)
lin_preds = linreg.predict(x_grid)
log_probs = logreg.predict_proba(x_grid)[:, 1]

# Визуализация
plt.figure(figsize=(10, 5))
plt.scatter(X0, y0, color='blue', label='Класс 0 (скученный)', alpha=0.7)
plt.scatter(X1, y1, color='orange', label='Класс 1 (удалённый)', alpha=0.9)
plt.plot(x_grid, lin_preds, color='green', linestyle='--', label='Linear Regression')
plt.plot(x_grid, log_probs, color='black', label='Logistic Regression')
plt.xlabel("x")
plt.ylabel("Предсказание / Вероятность")
plt.title("Линейная vs логистическая регрессия при удалённых объектах класса 1")
plt.ylim(-0.1, 1.1)
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()

9. Многоклассовая классификация. Один-против-одного, один-против-всех, их свойства.

? Краткий ответ
  • One-vs-Rest (OvR): обучаем K бинарных моделей «класс vs остальные», выбираем класс с макс. откликом.

  • One-vs-One (OvO): обучаем \frac{K(K-1)}{2} моделей по парам классов, предсказание — по большинству голосов.

  1. При предсказании инферяться все модели!

  2. В One-vs-One не учитываются вероятности лишь факт победы.

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

? One-vs-Rest (OvR)

Обучение:

  • K бинарных моделей f_k(x), каждая отличает класс k от остальных

Предсказание:

  • Вычисляем отклики f_0(x), f_1(x), \dots, f_{K-1}(x)

  • Выбираем класс с максимальным значением:

    \hat{y} = \arg\max_k f_k(x)

Если модель выдаёт вероятности P(y = k \mid x), выбираем по ним.

? One-vs-One (OvO)

Обучение:

  • Строим классификаторы для всех пар классов:

    f_{i,j}(x) \text{ обучается на классах } i \text{ и } j

    Всего \frac{K(K-1)}{2} моделей.

Предсказание:

  • Каждый классификатор голосует: f_{i,j}(x) \in \{i, j\}

  • Считаем число голосов за каждый класс

  • Итог:

    \hat{y} = \arg\max_k \text{(кол-во голосов за } k)

Голоса — дискретные, уверенность моделей не используется.

? Сравнение One-vs-Rest и One-vs-One на Iris
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.multiclass import OneVsRestClassifier, OneVsOneClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, confusion_matrix
import numpy as np
import pandas as pd
from IPython.display import display

# --- 1. Данные
X, y = load_iris(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=0)

# --- 2. Модель
base_model = LogisticRegression(max_iter=1000)

# --- 3. One-vs-Rest (OvR)
clf_ovr = OneVsRestClassifier(base_model).fit(X_tr, y_tr)
y_pred_ovr = clf_ovr.predict(X_te)

# --- 4. One-vs-One (OvO)
clf_ovo = OneVsOneClassifier(base_model).fit(X_tr, y_tr)
y_pred_ovo = clf_ovo.predict(X_te)

# --- 5. Confusion matrices
cm_ovr = confusion_matrix(y_te, y_pred_ovr)
cm_ovo = confusion_matrix(y_te, y_pred_ovo)

print("Confusion Matrix (OvR):")
print(cm_ovr)

print("\nConfusion Matrix (OvO):")
print(cm_ovo)

# --- 6. Разбиения для OvR
ovr_split = pd.DataFrame({
    'Класс': list(range(len(clf_ovr.estimators_))),
    'Положительных': [(y_tr == k).sum() for k in range(len(clf_ovr.estimators_))],
    'Отрицательных': [(y_tr != k).sum() for k in range(len(clf_ovr.estimators_))],
    'Всего': [len(y_tr)] * len(clf_ovr.estimators_)
})

print("\nOvR — разбивка по классам:")
display(ovr_split)

# --- 7. Разбиения для OvO
ovo_pairs = [(est.classes_[0], est.classes_[1]) for est in clf_ovo.estimators_]

ovo_data = []
for a, b in ovo_pairs:
    count_a = np.sum(y_tr == a)
    count_b = np.sum(y_tr == b)
    total = count_a + count_b
    ovo_data.append({
        'Пара классов': f"{a} vs {b}",
        f"#{a}": count_a,
        f"#{b}": count_b,
        'Суммарно': total
    })

ovo_split = pd.DataFrame(ovo_data)

print("\nOvO — разбивка по парам классов:")
display(ovo_split)

# --- 8. Accuracy summary
print(f"\nOvR Accuracy: {accuracy_score(y_te, y_pred_ovr):.3f}")
print(f"OvO Accuracy: {accuracy_score(y_te, y_pred_ovo):.3f}")

Видно что датасет достаточно хороший игрушечный, тут и данные разделены хорошо и по классам все сбалансированно (и классов немного).

10. Метод опорных векторов. Задача оптимизации для SVM. Трюк с ядром. Свойства ядра.

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

SVM (support vector machine) — это алгоритм решает задачу классификации ("линейная классификация"), который ищет гиперплоскость, максимально разделяющую классы с зазором (margin).

Он решает задачу максимизации отступа, то есть делает так, чтобы:

  • все объекты лежали как можно дальше от границы (реализуется ядром, скалярным произведением сонаправленностью <y, \hat{y}>)

  • и при этом допускались ошибки для равномерного отступа (через мягкие штрафы) (реализуется добавлением зазора=константы 1 - M).

  • ядра можно брать разные - не обязательно линейное

Функция потерь (hinge-loss) устроена так, чтобы:

  • не штрафовать объекты с отступом \ge 1,

  • и наказывать только те, которые «лезут» в буферную зону или ошибаются (=либо неверные, либо неуверенно правильные!):

\mathcal{L}(w) = \frac{1}{n} \sum \max(0, 1 - y_i (w^\top x_i + b)) + \frac{\lambda}{2} \|w\|^2

Чем, же это лучше чем просто линейная регрессия в классификации? А тем, что SVM решает задачу разделить данные, а линейная регресия старается провести через них!

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

? Модель

Классификатор:

\hat{y}(x) = \text{sign}(w^\top x + b)

Отступ (margin):

M_i = y_i (w^\top x_i + b)

Чем больше M_i, тем выше уверенность в классификации.


? Целевая функция (hinge loss)

SVM минимизирует:

\mathcal{L}(w) = \frac{1}{n} \sum_{i=1}^n \max(0, 1 - y_i(w^\top x_i + b)) + \frac{\lambda}{2} \|w\|^2
  • Первый член — штраф за малый отступ (ошибки или «почти ошибки»)

  • Второй — регуляризация (контроль за нормой w)


? Ядровой трюк (kernel trick)

Вместо линейного x \cdot x', используем:

K(x, x') = \langle \phi(x), \phi(x') \rangle

→ Не нужно явно строить \phi(x), а граница может быть нелинейной.


? Примеры ядер

Ядро

Формула

Линейное

K(x, x') = x^\top x'

Полиномиальное

K(x, x') = (x^\top x' + c)^d

RBF (Гаусс)

K(x, x') = \exp(-\gamma |x - x'|^2)

✅ Свойства допустимого ядра (ядровой функции)

Функция K(x, x') — допустимое ядро, если оно:

  1. Симметрична:

    K(x, x') = K(x', x)
  2. Положительно полуопределённая (PSD):
    Для любых x_1, \dots, x_n \in \mathbb{R}^d и любых весов \alpha_1, \dots, \alpha_n \in \mathbb{R} выполняется:

    \sum_{i=1}^n \sum_{j=1}^n \alpha_i \alpha_j K(x_i, x_j) \ge 0

    Это значит: матрица Грама K на любом наборе точек — положительно полуопределённая.

Почему это важно?

Если K — валидное ядро, то по теореме Мерсера:

K(x, x') = \langle \phi(x), \phi(x') \rangle

для некоторого отображения \phi(\cdot) в (возможно бесконечномерное) пространство признаков.

→ Это делает метод SVM с ядром линейным в этом скрытом пространстве, без явного вычисления \phi(x).

? Сравнения разных SVM ядер: linear vs poly vs RBF
from sklearn.datasets import make_classification
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import matplotlib.pyplot as plt
import numpy as np

# --- 1. Данные
X, y = make_classification(n_samples=300, n_features=2, n_redundant=0,
                           n_clusters_per_class=1, class_sep=1.0, random_state=42)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=0)

# --- 2. Модели
clf_linear = SVC(kernel='linear', C=1).fit(X_tr, y_tr)
clf_rbf = SVC(kernel='rbf', gamma=1, C=1).fit(X_tr, y_tr)
clf_poly = SVC(kernel='poly', degree=3, C=1).fit(X_tr, y_tr)


# --- 3. Визуализация
def plot_decision_boundary(model, X, y, ax, title):
    h = 0.02
    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                         np.arange(y_min, y_max, h))
    Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)

    ax.contourf(xx, yy, Z, alpha=0.3, cmap=plt.cm.coolwarm)
    ax.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.coolwarm, edgecolors='k')
    ax.set_title(title)
    ax.set_xlabel("x₁")
    ax.set_ylabel("x₂")
    ax.grid(True)

# --- 4. Графики
fig, axes = plt.subplots(1, 3, figsize=(12, 5))
plot_decision_boundary(clf_linear, X_te, y_te, axes[0], "Линейный SVM")
plot_decision_boundary(clf_poly, X_te, y_te, axes[1], "Полиномиальный 3-й степени SVM")
plot_decision_boundary(clf_rbf, X_te, y_te, axes[2], "RBF SVM")
plt.tight_layout()
plt.show()


y_pred_linear = clf_linear.predict(X_te)
y_pred_poly = clf_poly.predict(X_te)
y_pred_rbf = clf_rbf.predict(X_te)

# Accuracy
acc_linear = accuracy_score(y_te, y_pred_linear)
acc_poly = accuracy_score(y_te, y_pred_poly)
acc_rbf = accuracy_score(y_te, y_pred_rbf)
print(f"Linear SVM Accuracy: {acc_linear:.3f}")
print(f"Polynomial SVM Accuracy: {acc_poly:.3f}")
print(f"RBF SVM Accuracy: {acc_rbf:.3f}")
# Linear SVM Accuracy: 0.944
# Polynomial SVM Accuracy: 0.911
# RBF SVM Accuracy: 0.967

Что дальше?

Учимся быстро и понятно рассказывать. Для этого проговариваем много раз. Рассказываем друзьям, либо записываем себе и слушаем.

Пока готовимся, сохраняем каверзные вопросы, на которые непросто дать верные/легкии ответы. Сначала основное, потом остальное!

Материалы


В следующей части продолжим — будут PCA, Bias–variance tradeoff, деревья, ансамбли, бустинг, и глубокое обучение. Пока подписывайтесь и делитесь своими находками!

Комментарии (2)


  1. 2er6e1
    15.06.2025 05:29

    Уже на пункте 0 споткнулся о такие недостатки в изложении:

    x - это объекты или векторы?

    f - это отображение или алгоритм?

    "модель обучается приближать отображение" - какая-то каша. Наверное так лучше было написать: "требуется найти модель, которая будет производить отображение". и, наверное, этот поиск будет вестись методом приближений. ?

    После чего дальше читать желание пропало.