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

Я считаю самый полный и простой способ заполнить все пробелы — это взять хороший экзамен и ответить на все вопросы — понятно и быстро. А что запомнилось решить задачку. Приступим!
Сначала попробуйте сами быстро ответить, а потом после просмотра! Стало быстрее-понятнее объяснять?
Для более полного погружения в конце приложу важные ресурсы. Делитесь своими!
📚 Глава 1: Модели, метрики и формула Байеса
0. Задача обучения с учителем. Регрессия, Классификация
📌 Краткий ответ
-
Обучение с учителем — это постановка задачи, при которой каждый объект обучающей выборки снабжён целевым значением
, и модель обучается приближать отображение
.
-
Регрессия: если
(например, цена, температура).
-
Классификация: если
, то есть класс или категория (например, диагноз, категория изображения).
🔬 Подробный разбор
Общая постановка задачи
В обучении с учителем задана обучающая выборка из пар
где — вектор признаков,
— целевая переменная. Требуется построить алгоритм
, минимизирующий ошибку предсказания.
Регрессия
Если или
, задача называется регрессией.
Модель должна предсказывать численное значение. Типичные функции потерь:
-
Mean Squared Error (MSE)
-
Mean Absolute Error (MAE)
Примеры:
-
Прогнозирование цены недвижимости
-
Оценка спроса на товар
Классификация
Если — задача классификации.
В простейшем случае — бинарная классификация (например, «да/нет»).
При
— многоклассовая. Также существует 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, расширения для многоклассовой классификации
📌 Краткий ответ
Для задачи бинарной классификации () можно построить матрицу ошибок и по ним посчитать метрики:

|
Метрика |
Формула |
Смысл |
|---|---|---|
|
Accuracy |
|
Общая доля правильных предсказаний |
|
Balanced Accuracy |
|
Усреднённая точность по классам при дисбалансе |
|
Precision |
|
Доля верных положительных предсказаний |
|
Recall |
|
Доля найденных положительных среди всех реальных |
|
F1(b)-score |
|
Баланс между 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 |
|
Среднеквадратичная ошибка. Наказывает большие ошибки сильнее. |
|
MAE |
|
Средняя абсолютная ошибка. Интерпретируется в исходных единицах. |
|
R² score |
|
На сколько лучше константного предсказания(=среднее при минимизации 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) на веса. При нормальном, получаем
регуляризацию, при лаплассе
.
🔬 Формальные выводы
Детальнее можно узнать в конце тетрадке.
✍ MLE и линейная регрессия
Предполагаем, что целевая переменная yᵢ имеет нормальное распределение с центром в xᵢᵀw и дисперсией σ²:
Тогда правдоподобие всей выборки:
Берём логарифм:
Раскрываем сумму:
→ максимизация логарифма правдоподобия эквивалентна минимизации:
✅ Вывод:
Метод максимального правдоподобия (MLE) для линейной регрессии приводит к функции потерь MSE — среднеквадратичной ошибке.
Регуляризация (например, Ridge(=
)) возникает при добавлении априорного распределения на веса — это уже MAP, не MLE.
✍ MLE и логистическая регрессия
В бинарной классификации целевая переменная yᵢ ∈ {0, 1} — для отклонения используем распределние Бернули.
Предполагаем:
Правдоподобие всей выборки:
Логарифм правдоподобия:
✅ Вывод:
Максимизация логарифма правдоподобия ⇔ минимизация log-loss (логистической функции потерь)
✍ Что меняется с регуляризацией: MAP (Maximum A Posteriori)
Добавим априорное распределение на параметры:
-
L2-регуляризация ⇔ априорное
w ∼ 𝒩(0, λ⁻¹I) -
L1-регуляризация ⇔ априорное
w ∼ Laplace(0, b)
MAP-оценка:
→ Это и есть MLE + регуляризация:
-
MLE⇔ логлосс -
MAP⇔ логлосс + регуляризация
💻 Баесовский вывод двух нормальных распределений
Задача
Пусть параметр неизвестен и:
-
Prior:
-
Likelihood:
— наблюдение, связанное с
Найти posterior
📐 Шаг 1: формула Байеса
По определению:
Логарифмируем обе части:
🧮 Шаг 2: подставляем нормальные распределения
-
Prior:
-
Likelihood (в терминах
, фиксируя
):
📉 Шаг 3: складываем логарифмы
Это — квадратичная функция по , то есть логарифм нормального распределения. Следовательно, сам posterior — тоже нормальный:
✅ Вывод: параметры апостериорного распределения
Отрисуем!
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) и P(xᵢ | 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:
-
Аналитически:
-
Теорема Гаусса-Маркова: это наилучшая линейная несмещённая оценка при стандартных предположениях (BLUE)
-
При больших данных: используется градиентный спуск
🔬 Подробный разбор
📌 Постановка задачи
У нас есть:
-
— матрица признаков;
-
— целевая переменная;
-
— веса модели.
Цель: минимизировать среднеквадратичную ошибку:
📌 Вывод аналитического решения
Выпишем градиент по :
Приравниваем к нулю:
Раскрываем скобки:
Предполагая, что обратима:
📌 Теорема Гаусса-Маркова (формулировка)
Если:
-
модель линейна по параметрам:
;
-
ошибки
имеют нулевое среднее;
-
одинаковую дисперсию
;
-
некоррелированы между собой;
→ тогда (из нормального уравнения) — наилучшая линейная несмещённая оценка (BLUE).
Термин BLUE (Best Linear Unbiased Estimator) — это сокращение:
-
Linear (линейная):
Оценка— это линейная функция от
:
где
-
Unbiased (несмещённая):
Среднее значение оценки совпадает с истинным параметром: -
Best (наилучшая):
Из всех линейных и несмещённых оценок,имеет наименьшую дисперсию:
💻 Аналитическое и градиентное решение поиска весов линейной модели + график
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):
-
L1-регуляризация (Lasso):
-
L2 сглаживает и уменьшает веса
-
L1 приводит к разреженным решениям (обнуляет ненужные признаки)
🔬 Подробный разбор
— Ridge,
— Lasso, Elastic Net — комбинация.
📌 Вероятностная интерпретация
Добавление регуляризатора эквивалентно введению априорного распределения на параметры (подробнее в 3 вопросе о MLE):
-
L2 = Gaussian prior:
-
L1 = Laplace prior:
→ То есть регуляризация = байесовская MAP-оценка, если мы знаем prior на веса.
Почему при зануляются веса?
Очень популярный и важный вопрос! Изобразим уровни потерь по отдельности двух частей лосса!
Предпложим противное. Пусть опитимум пересечения и он не на осях координат. Из-за выпуклости двух фигур, найдется пересечения внутри, а по нему можно уже подняться вверх, сохранив ошибку по
и уменьшить MSE ! Второй заумный аргумент .

Упрощение 2-аргумента: две фигуры выпуклые и имеют единственную касательную (помимо в точках на осях!), тогда в точке касания можно провести разделяющую прямую!
А это значит, что оси у элипса у 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 и минимизации логистических потерь.
📌 Краткий ответ
Логистическая регрессия — это модель предсказывающая вероятность класса :
P(y = 1 | x) = \sigma(x^\top w), \quad \sigma(z) = \frac{1}{1 + e^{-z}}
MLE: максимизация логарифма правдоподобия ⇔ минимизация log-loss (обычно для бинарной классификации) = cross-entropy-loss (обычно для мультиклассовой классификации)
или
| обычно,
— ровно одна 1 и остальные нули.
🔬 Подробный разбор
📌 Вывод: MLE для логистической регрессии
Обозначим:
Правдоподобие:
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)
Подставляем :
И получаем логистическую функцию потерь:
\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 — 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): обучаем
бинарных моделей «класс vs остальные», выбираем класс с макс. откликом.
-
One-vs-One (OvO): обучаем
моделей по парам классов, предсказание — по большинству голосов.
-
При предсказании инферяться все модели!
-
В One-vs-One не учитываются вероятности лишь факт победы.
🔬 Подробный разбор
📌 One-vs-Rest (OvR)
Обучение:
-
бинарных моделей
, каждая отличает класс
от остальных
Предсказание:
-
Вычисляем отклики
-
Выбираем класс с максимальным значением:
Если модель выдаёт вероятности , выбираем по ним.
📌 One-vs-One (OvO)
Обучение:
-
Строим классификаторы для всех пар классов:
Всего
моделей.
Предсказание:
-
Каждый классификатор голосует:
-
Считаем число голосов за каждый класс
-
Итог:
Голоса — дискретные, уверенность моделей не используется.
💻 Сравнение 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).
Он решает задачу максимизации отступа, то есть делает так, чтобы:
-
все объекты лежали как можно дальше от границы (реализуется ядром, скалярным произведением сонаправленностью <
,
>)
-
и при этом допускались ошибки для равномерного отступа (через мягкие штрафы) (реализуется добавлением зазора=константы 1 — M).
-
ядра можно брать разные — не обязательно линейное
Функция потерь (hinge-loss) устроена так, чтобы:
-
не штрафовать объекты с отступом
,
-
и наказывать только те, которые «лезут» в буферную зону или ошибаются (=либо неверные, либо неуверенно правильные!):
Чем, же это лучше чем просто линейная регрессия в классификации? А тем, что SVM решает задачу разделить данные, а линейная регресия старается провести через них!
🔬 Подробный разбор
📌 Модель
Классификатор:
Отступ (margin):
Чем больше , тем выше уверенность в классификации.
📌 Целевая функция (hinge loss)
SVM минимизирует:
-
Первый член — штраф за малый отступ (ошибки или «почти ошибки»)
-
Второй — регуляризация (контроль за нормой
)
📌 Ядровой трюк (kernel trick)
Вместо линейного , используем:
→ Не нужно явно строить , а граница может быть нелинейной.
📌 Примеры ядер
|
Ядро |
Формула |
|---|---|
|
Линейное |
|
|
Полиномиальное |
|
|
RBF (Гаусс) |
|
✅ Свойства допустимого ядра (ядровой функции)
Функция — допустимое ядро, если оно:
-
Симметрична:
-
Положительно полуопределённая (PSD):
Для любыхи любых весов
выполняется:
Это значит: матрица Грама
на любом наборе точек — положительно полуопределённая.
Почему это важно?
Если — валидное ядро, то по теореме Мерсера:
для некоторого отображения в (возможно бесконечномерное) пространство признаков.
→ Это делает метод SVM с ядром линейным в этом скрытом пространстве, без явного вычисления .
💻 Сравнения разных 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

Что дальше?

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