У каждого наступает момент, когда нужно быстро освежить в памяти огромный пласт информации по всему 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, деревья, ансамбли, бустинг, и глубокое обучение. Пока подписывайтесь и делитесь своими находками!
2er6e1
Уже на пункте 0 споткнулся о такие недостатки в изложении:
x - это объекты или векторы?
f - это отображение или алгоритм?
"модель обучается приближать отображение" - какая-то каша. Наверное так лучше было написать: "требуется найти модель, которая будет производить отображение". и, наверное, этот поиск будет вестись методом приближений. ?
После чего дальше читать желание пропало.