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