Машинное обучение Математика для ML: линейная алгебра, анализ, вероятности
0%

Математика для ML: линейная алгебра, анализ, вероятности

Математика для ML: линейная алгебра, анализ, вероятности

Есть распространённая иллюзия: «математика в ML не нужна, есть же scikit-learn». Пока всё работает — иллюзия держится. Ломается она в конкретных, очень типичных ситуациях:

  • модель линейной регрессии выдаёт коэффициенты порядка 1e13 с противоположными знаками;
  • градиентный спуск «застревает», и непонятно — это плохой learning rate, плохие признаки или баг;
  • логистическая регрессия выдаёт nan на трети батчей;
  • ROC-AUC на отложенной выборке 0.91, а через две недели в проде — 0.63, и никто не может сказать, значимо это или шум;
  • PCA «съел» 99% дисперсии одной компонентой, и эта компонента — просто признак «выручка в рублях».

Каждый из этих пунктов — не про библиотеки, а про математику: обусловленность матрицы, геометрию линии уровня, численную устойчивость, доверительные интервалы, влияние масштаба на ковариацию. Эта статья — не курс матанализа, а рабочий минимум: тот объём, который реально используется каждый день, объяснённый до уровня «понимаю, зачем и вижу в коде».

Карта трека, типы задач и терминология постановки — в статье Машинное обучение: карта трека, типы задач и постановка.

Карта: что именно нужно и зачем

Дальше — три больших блока в этом порядке, потому что они складываются в одну историю: линейная алгебра описывает данные и модель, анализ описывает обучение, вероятность описывает, что мы вообще оптимизируем и насколько верим результату.


Часть 1. Линейная алгебра: язык данных

Вектор — это описание объекта

Один объект (клиент, транзакция, изображение, товар) после подготовки признаков — это вектор $x \in \mathbb{R}^d$: упорядоченный набор из $d$ чисел. Порядок фиксирован: третья координата всегда «возраст», седьмая всегда «средний чек». Это соглашение и есть «признаковое пространство».

Датасет из $n$ объектов — матрица $X \in \mathbb{R}^{n \times d}$: строки — объекты, столбцы — признаки. Это единственное соглашение, которое нужно держать в голове, читая любую формулу ML: строка — объект, столбец — признак.

Ключевая мысль: как только объекты стали точками в пространстве, у нас появляются геометрические понятия — расстояние, угол, проекция, размерность. И почти каждый классический алгоритм ML — это геометрическая идея:

Алгоритм Геометрическая суть
k-NN ближайшие точки по метрике
Линейная регрессия проекция $y$ на подпространство столбцов $X$
SVM гиперплоскость с максимальным зазором
k-means минимизация суммы квадратов расстояний до центроидов
PCA поворот осей вдоль направлений максимальной дисперсии

Скалярное произведение, нормы, косинус

Скалярное произведение $\langle a, b \rangle = \sum_i a_i b_i = |a|,|b|\cos\theta$ — рабочая лошадка всего ML. Оно измеряет «сонаправленность». Линейная модель $\hat{y} = \langle w, x\rangle + b$ буквально спрашивает: «насколько объект сонаправлен с вектором весов?».

Нормы задают, что значит «далеко»:

  • $|x|_ 1 = \sum |x_i|$ — манхэттенская; порождает разреженность в L1-регуляризации (Lasso);
  • $|x|_ 2 = \sqrt{\sum x_i^2}$ — евклидова; порождает сжатие весов в L2 (Ridge);
  • $|x|_ \infty = \max |x_i|$ — используется в оценках робастности и adversarial-атаках.

Почему L1 даёт нули, а L2 — нет: шар L1 имеет «углы» на осях координат, и линия уровня функции потерь чаще всего касается его именно в углу, где часть координат равна нулю. Шар L2 гладкий — касание в общем положении происходит в точке без нулей. Подробнее — в статье про переобучение и регуляризацию.

Косинусная близость $\cos\theta = \frac{\langle a,b\rangle}{|a||b|}$ игнорирует длину вектора. Это важно для текстов и эмбеддингов: документ из 5000 слов и его пересказ из 200 слов имеют разную длину TF-IDF-вектора, но почти одинаковое направление.

import numpy as np

def cosine_similarity(A: np.ndarray, B: np.ndarray) -> np.ndarray:
    """Косинусная близость всех пар строк A и B. A: (n, d), B: (m, d) -> (n, m)."""
    # Нормируем строки; eps защищает от деления на ноль для нулевых векторов
    A_n = A / (np.linalg.norm(A, axis=1, keepdims=True) + 1e-12)
    B_n = B / (np.linalg.norm(B, axis=1, keepdims=True) + 1e-12)
    return A_n @ B_n.T          # O(n*m*d) времени, O(n*m) памяти

Обратите внимание на сложность: n*m пар. Для миллиона объектов матрица попарных сходств не поместится в память — отсюда весь ANN-поиск (HNSW, IVF-PQ) в рекомендательных системах.

Матрица как преобразование и SVD

Второй взгляд на матрицу: $A$ — это функция, переводящая вектор $x$ в вектор $Ax$. Любое линейное преобразование раскладывается в три простых шага — поворот, растяжение по осям, ещё один поворот. Это и есть сингулярное разложение $A = U\Sigma V^\top$.

Геометрия SVD: единичная окружность превращается в эллипс

Что из этого следует практически:

  • Сингулярные числа $\sigma_i$ — во сколько раз растягивается пространство вдоль соответствующего направления. Их квадраты, делённые на $n-1$, — это дисперсии главных компонент центрированных данных. PCA — это буквально SVD центрированной матрицы $X$.
  • Ранг — число ненулевых $\sigma_i$, то есть реальная размерность данных. Если один признак равен сумме двух других, ранг меньше числа столбцов, матрица $X^\top X$ вырождена, и у нормального уравнения нет единственного решения.
  • Число обусловленности $\kappa(A) = \sigma_{\max}/\sigma_{\min}$ — насколько ошибка во входе усиливается на выходе. При $\kappa \sim 10^{10}$ и float64 (около 16 значащих цифр) вы теряете 10 цифр точности — отсюда и коэффициенты 1e13 из вступления.
import numpy as np

rng = np.random.default_rng(0)
n = 500
x1 = rng.normal(size=n)
x2 = rng.normal(size=n)
x3 = x1 + x2 + 1e-8 * rng.normal(size=n)   # почти линейно зависимый признак
X = np.column_stack([x1, x2, x3])

s = np.linalg.svd(X, compute_uv=False)
print(s)                       # последнее сингулярное число ~1e-7
print(s[0] / s[-1])            # kappa ~ 1e8 — красный флаг мультиколлинеарности
print(np.linalg.matrix_rank(X))  # 2, хотя столбцов 3

Практическое правило: считайте np.linalg.cond(X) на подготовленной матрице признаков. $\kappa < 10^3$ — спокойно; $10^3..10^6$ — стоит добавить регуляризацию; $> 10^6$ — ищите дублирующие признаки, one-hot без drop_first, признаки в разных единицах.

Проекция и метод наименьших квадратов

Линейная регрессия геометрически — это проекция. У нас есть вектор ответов $y \in \mathbb{R}^n$ и подпространство, натянутое на столбцы $X$ (все возможные $Xw$). Мы ищем ближайшую точку этого подпространства к $y$. Условие ближайшести: вектор ошибки $y - Xw$ перпендикулярен подпространству, то есть $X^\top (y - Xw) = 0$. Отсюда нормальное уравнение:

$$ X^\top X, w = X^\top y $$

Решать его «в лоб» через inv(X.T @ X) — классическая ошибка: обусловленность $X^\top X$ равна $\kappa(X)^2$, вы возводите проблему в квадрат. Правильно — через QR-разложение или SVD, что и делают библиотеки.

import numpy as np

def fit_ols(X: np.ndarray, y: np.ndarray) -> np.ndarray:
    """МНК устойчиво: lstsq использует SVD, а не обращение X^T X."""
    w, *_ = np.linalg.lstsq(X, y, rcond=None)   # O(n*d^2) времени, O(n*d) памяти
    return w

def fit_ridge(X: np.ndarray, y: np.ndarray, alpha: float) -> np.ndarray:
    """Ridge: alpha*I делает матрицу невырожденной и снижает kappa."""
    d = X.shape[1]
    A = X.T @ X + alpha * np.eye(d)
    return np.linalg.solve(A, X.T @ y)          # solve, а не inv

Здесь виден и смысл L2-регуляризации без всякой статистики: прибавляя $\alpha I$, мы поднимаем все сингулярные числа на $\alpha$, и $\kappa$ падает с $\sigma_1/\sigma_d$ до $(\sigma_1+\alpha)/(\sigma_d+\alpha)$. Ridge — это в первую очередь лекарство от вырожденности, и только во вторую — от переобучения.

Сложность точных методов: $O(nd^2)$ по времени при $n \gg d$. Для $d \sim 10^5$ (разреженные текстовые признаки) это неприемлемо — поэтому переходят к итерационным методам, о которых вторая часть.


Часть 2. Анализ и оптимизация: механизм обучения

Производная, градиент, направление спуска

Производная — скорость изменения. Градиент $\nabla f(w)$ — вектор частных производных; его ключевое свойство: он указывает направление наискорейшего роста функции, а его длина равна скорости роста в этом направлении. Значит, $-\nabla f$ — направление наискорейшего убывания. Отсюда весь градиентный спуск:

$$ w_{t+1} = w_t - \eta \nabla f(w_t) $$

Гессиан $H = \nabla^2 f$ — матрица вторых производных, описывает кривизну. Её собственные числа — кривизны вдоль главных направлений, а $\kappa(H) = \lambda_{\max}/\lambda_{\min}$ — та самая обусловленность, которая определяет скорость сходимости.

Влияние обусловленности на траекторию градиентного спуска

Это и есть математическое объяснение, почему масштабирование признаков обязательно для всего, что учится градиентом (линейные модели, SVM, нейросети), и не нужно деревьям: для квадратичной потери гессиан пропорционален $X^\top X$, и разброс масштабов столбцов напрямую раздувает $\kappa$.

Оценки сходимости (для гладкой $f$ с константой Липшица градиента $L$):

Случай Скорость Шагов до точности ε
Выпуклая, гладкая $O(1/t)$ $O(1/\varepsilon)$
Сильно выпуклая ($\mu > 0$) линейная, $(1 - \mu/L)^t$ $O(\kappa \log(1/\varepsilon))$
Невыпуклая, гладкая $\min_t |\nabla f|^2 = O(1/t)$ гарантий глобальности нет
Nesterov momentum, сильно выпуклая ускоренная $O(\sqrt{\kappa}\log(1/\varepsilon))$

Замена $\kappa$ на $\sqrt{\kappa}$ — не косметика: при $\kappa = 10^4$ это разница между 10 000 и 100 итерациями.

Выпуклость — почему одни модели «всегда сходятся», а другие нет

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

  • Линейная регрессия с MSE, логистическая регрессия с логлоссом, SVM с hinge-loss, Lasso/Ridge — выпуклы. Результат воспроизводим с точностью до численного шума.
  • Нейросети, k-means, матричные факторизации — невыпуклы. Результат зависит от seed, инициализации и порядка данных. Именно поэтому k-means запускают с n_init=10.

Правило цепочки и вычислительный граф

Всё обучение современных моделей держится на правиле цепочки: $\frac{\partial}{\partial x} f(g(x)) = f’(g(x)) \cdot g’(x)$. Модель — это композиция функций, и градиент по любому параметру считается протаскиванием производной назад по графу вычислений.

Обратите внимание на красивое сокращение: произведение двух «страшных» множителей $\frac{a-y}{a(1-a)} \cdot a(1-a)$ даёт просто $a - y$. Именно поэтому в коде логистической регрессии градиент выглядит как X.T @ (p - y) / n — сигмоида и кросс-энтропия «созданы друг для друга». То же верно для softmax + категориальная кросс-энтропия и для линейной модели + MSE. Это не совпадение: все три пары — следствие того, что распределения принадлежат экспоненциальному семейству, а функция потерь — его логарифмическое правдоподобие.

Реализация автодифференцирования в тридцать строк — лучший способ понять backprop:

import numpy as np

class Var:
    """Скалярный узел вычислительного графа с обратным распространением."""
    def __init__(self, value: float, parents=(), backward_fn=lambda: None):
        self.value = value
        self.grad = 0.0
        self._parents = parents
        self._backward_fn = backward_fn

    def __add__(self, other):
        out = Var(self.value + other.value, (self, other))
        def _back():
            self.grad += out.grad          # производная суммы по каждому слагаемому = 1
            other.grad += out.grad
        out._backward_fn = _back
        return out

    def __mul__(self, other):
        out = Var(self.value * other.value, (self, other))
        def _back():
            self.grad += other.value * out.grad   # правило произведения
            other.grad += self.value * out.grad
        out._backward_fn = _back
        return out

    def sigmoid(self):
        s = 1.0 / (1.0 + np.exp(-self.value))
        out = Var(s, (self,))
        out._backward_fn = lambda: setattr(self, "grad", self.grad + s * (1 - s) * out.grad)
        return out

    def backward(self):
        # Топологическая сортировка графа: узел обрабатываем после всех его потребителей
        order, seen = [], set()
        def visit(v):
            if id(v) in seen:
                return
            seen.add(id(v))
            for p in v._parents:
                visit(p)
            order.append(v)
        visit(self)
        self.grad = 1.0
        for v in reversed(order):
            v._backward_fn()

# Проверка: f = (w * x + b), затем сигмоида; сверим с аналитическим градиентом
w, x, b = Var(0.5), Var(2.0), Var(-0.3)
y = (w * x + b).sigmoid()
y.backward()
s = y.value
print(w.grad, x.value * s * (1 - s))   # совпадает: df/dw = x * σ'(z)

Сложность backprop: один обратный проход стоит примерно столько же, сколько прямой, — $O(|E|)$ по числу рёбер графа, а память $O(|V|)$, потому что нужно хранить активации. Отсюда весь gradient checkpointing в больших моделях: меняем память на пересчёт.

От GD к SGD, momentum и Adam

Полный градиент по всей выборке стоит $O(nd)$ на шаг. При $n = 10^8$ это неприемлемо, поэтому берут стохастическую оценку по мини-батчу: она несмещённая, а её дисперсия падает как 1 $/B$ при размере батча $B$. Отсюда практическое правило «увеличил батч в 4 раза — можно увеличить learning rate примерно в 2 раза» (линейное правило масштабирования даёт множитель, пропорциональный $B$, для больших моделей — см. Goyal et al., 2017, https://arxiv.org/abs/1706.02677).

Три идеи, которые лечат три разные болезни:

  • Momentum ($v_{t+1} = \beta v_t + g_t$): накапливает движение вдоль устойчивого направления и гасит зигзаг поперёк «оврага» — прямое лекарство от большой $\kappa$.
  • Adaptive rates (AdaGrad/RMSProp): делят шаг на корень из накопленного квадрата градиента, выравнивая эффективный learning rate по координатам — лекарство от разного масштаба признаков и разреженности.
  • Adam = momentum + RMSProp + коррекция смещения. Дефолт для нейросетей; для выпуклых задач с плотными признаками честный L-BFGS часто быстрее и точнее.

Метод Ньютона ($w \leftarrow w - H^{-1}g$) сходится квадратично, но требует $O(d^3)$ на обращение гессиана и $O(d^2)$ памяти. При $d = 10^6$ это невозможно, поэтому в проде живут квазиньютоновские методы (L-BFGS хранит $m \approx 10$ последних пар векторов, $O(md)$ памяти) и диагональные аппроксимации вроде Adam. Кстати, XGBoost/LightGBM в бустинге — это по сути метод Ньютона в функциональном пространстве: они используют и первую, и вторую производную функции потерь (grad и hess).


Часть 3. Вероятности: откуда берутся функции потерь

Базовый словарь

  • Случайная величина — функция из исходов в числа. В ML: $X$ — признаки, $Y$ — целевая.
  • Матожидание $\mathbb{E}\lbrack X\rbrack $ — «центр масс» распределения; линейно всегда: $\mathbb{E}[aX + bY] = a\mathbb{E}\lbrack X\rbrack + b\mathbb{E}[Y]$, даже для зависимых величин.
  • Дисперсия $\mathrm{Var}(X) = \mathbb{E}[(X - \mathbb{E}X)^2]$; складывается только для некоррелированных величин — это ровно то, на чём стоит бэггинг.
  • Ковариация $\mathrm{Cov}(X,Y)$ и ковариационная матрица $\Sigma = \frac{1}{n-1}X_c^\top X_c$ для центрированной $X_c$. PCA — это её собственное разложение.
  • Условная вероятность $P(A \mid B)$ и независимость $P(A,B) = P(A)P(B)$ — фундамент наивного Байеса.

Важное различие, на котором ломаются собеседования: некоррелированность ≠ независимость. Корреляция Пирсона ловит только линейную связь. Для $X \sim U(-1,1)$ и $Y = X^2$ корреляция равна нулю, а зависимость идеальная. Проверяйте нелинейные связи взаимной информацией или Spearman.

Формула Байеса и почему она везде

$$ P(H \mid D) = \frac{P(D \mid H), P(H)}{P(D)} $$

Читается так: апостериорное убеждение = правдоподобие данных × априорное убеждение, нормированное. Классический численный пример, объясняющий провалы моделей на редких классах: тест на болезнь с чувствительностью 99% и специфичностью 99%, распространённость болезни 0.1%. При положительном тесте

$$ P(\text{болен} \mid +) = \frac{0.99 \cdot 0.001}{0.99 \cdot 0.001 + 0.01 \cdot 0.999} \approx 0.09 $$

Девять процентов, а не 99. Это ровно та ловушка, из-за которой accuracy бессмысленна на несбалансированных данных, а precision при редком позитивном классе всегда низок — см. оценку моделей.

Правдоподобие: MSE и логлосс не выдуманы, а выведены

Мы предполагаем вероятностную модель данных и ищем параметры, при которых наблюдаемые данные максимально правдоподобны:

$$ \hat{\theta}_ {MLE} = \arg\max_\theta \prod_i p(y_i \mid x_i, \theta) = \arg\min_\theta \Big(-\sum_i \log p(y_i \mid x_i, \theta)\Big) $$

Логарифм берут по двум причинам: произведение $10^6$ вероятностей мгновенно обнуляется в float64, а сумма логарифмов — нет; и сумму удобнее дифференцировать.

Эта диаграмма — самый полезный объект во всей статье. Из неё следуют вещи, которые иначе выглядят магией:

  • MSE — не «естественная» метрика, а следствие гипотезы о гауссовом шуме с постоянной дисперсией. Если ошибка мультипликативная (выручка, время отклика) — логарифмируйте таргет или берите Gamma/Tweedie loss, иначе модель систематически недооценивает хвост.
  • L2-регуляризация — это MAP-оценка с гауссовым априором на веса, L1 — с лапласовым. Параметр alpha — обратная дисперсия априора: «насколько сильно я заранее уверен, что веса малы».
  • Для счётчиков (число заказов, кликов) MSE плоха, потому что предполагает постоянную дисперсию, а у пуассоновских данных дисперсия равна среднему.

Энтропия, кросс-энтропия, KL

Энтропия $H(p) = -\sum p_i \log p_i$ — средняя «неожиданность», минимальное среднее число бит на сообщение. Кросс-энтропия $H(p,q) = -\sum p_i \log q_i$ — сколько бит мы тратим, кодируя истинное $p$ по нашей модели $q$. Разница — дивергенция Кульбака–Лейблера:

$$ D_{KL}(p | q) = H(p,q) - H(p) \ge 0 $$

Отсюда: минимизация кросс-энтропии = минимизация KL между истинным и модельным распределением (энтропия данных от нас не зависит). А information gain в решающих деревьях — это уменьшение энтропии при разбиении. KL несимметрична: $D_{KL}(p|q) \neq D_{KL}(q|p)$, поэтому для мониторинга дрейфа признаков в проде чаще берут симметричные меры — Jensen–Shannon или PSI.

Оценивание: почему одна цифра метрики — это не результат

Выборочное среднее — случайная величина. По ЦПТ при больших $n$ его распределение близко к нормальному со стандартной ошибкой $\sigma/\sqrt{n}$. Отсюда — доверительный интервал, и отсюда же жёсткий вывод: ROC-AUC 0.91 на выборке из 300 объектов и ROC-AUC 0.89 — это одно и то же число.

Универсальный инструмент, не требующий формул для конкретной метрики, — бутстрэп:

import numpy as np
from sklearn.metrics import roc_auc_score

def bootstrap_ci(y_true, y_score, n_boot=2000, alpha=0.05, seed=0):
    """Перцентильный бутстрэп-интервал для любой метрики. O(n_boot * стоимость метрики)."""
    rng = np.random.default_rng(seed)
    y_true, y_score = np.asarray(y_true), np.asarray(y_score)
    n = len(y_true)
    stats = []
    for _ in range(n_boot):
        idx = rng.integers(0, n, n)            # выборка с возвращением
        if len(np.unique(y_true[idx])) < 2:    # AUC не определён на одном классе
            continue
        stats.append(roc_auc_score(y_true[idx], y_score[idx]))
    lo, hi = np.percentile(stats, [100 * alpha / 2, 100 * (1 - alpha / 2)])
    return float(np.mean(stats)), float(lo), float(hi)

Правило, которое стоит сделать частью код-ревью: любая метрика в отчёте идёт с интервалом. Иначе команда неделями «улучшает» модель внутри доверительного интервала.

Смещение и дисперсия оценки — тот же язык, что и разложение ошибки bias–variance в статье про переобучение:

$$ \mathbb{E}[(y - \hat{f}(x))^2] = \underbrace{(\mathbb{E}\hat{f} - f)^2}_ {\text{bias}^2}

  • \underbrace{\mathrm{Var}(\hat{f})}_ {\text{variance}} + \underbrace{\sigma^2}_ {\text{шум}} $$

И бэггинг здесь — прямое следствие формулы дисперсии суммы: усреднение $M$ моделей с попарной корреляцией $\rho$ даёт дисперсию $\rho\sigma^2 + \frac{1-\rho}{M}\sigma^2$. Отсюда вся конструкция случайного леса: случайные подпространства признаков нужны не «для разнообразия вообще», а чтобы уменьшить именно $\rho$ — второй член и так гасится числом деревьев, а первый без декорреляции не уходит никогда.


Численные ловушки, которые ломают прод

  1. Переполнение в softmax/сигмоиде. np.exp(1000) = inf. Лечение — вычитание максимума и трюк log-sum-exp:
import numpy as np

def softmax_stable(z: np.ndarray) -> np.ndarray:
    """Численно устойчивый softmax по последней оси."""
    z = z - z.max(axis=-1, keepdims=True)   # сдвиг не меняет результат, но убирает переполнение
    e = np.exp(z)
    return e / e.sum(axis=-1, keepdims=True)

def logsumexp(z: np.ndarray, axis=-1) -> np.ndarray:
    m = z.max(axis=axis, keepdims=True)
    return (m + np.log(np.exp(z - m).sum(axis=axis, keepdims=True))).squeeze(axis)
  1. log(0) в кросс-энтропии. Если модель выдала ровно 0 или 1, логлосс — бесконечность. Библиотеки клиппируют вероятности в $[\epsilon, 1-\epsilon]$; в своём коде считайте логлосс сразу из логитов (binary_cross_entropy_with_logits), а не из вероятностей.
  2. Катастрофическая потеря значимости. «Наивная» формула дисперсии $\mathbb{E}[X^2] - (\mathbb{E}X)^2$ на данных вида $10^9 \pm 1$ даёт отрицательную дисперсию. Используйте алгоритм Уэлфорда или двухпроходную формулу.
  3. Сравнение float на равенство при поиске порогов, дедупликации, проверке сходимости — только через np.isclose с явным atol.
  4. Накопление в float32. Сумма миллиона чисел в float32 теряет точность; np.sum использует попарное суммирование, ручной цикл — нет.

Типичные ошибки

  • Игнорирование обусловленности. Огромные коэффициенты линейной модели с противоположными знаками — это не «важные признаки», это вырожденность. Смотрите np.linalg.cond, VIF, корреляционную матрицу.
  • Оптимизация без масштабирования признаков. Признак «доход в рублях» рядом с признаком «доля 0..1» превращает линии уровня в узкий овраг и убивает сходимость.
  • MSE на всём подряд. Для положительных величин с тяжёлым хвостом (деньги, длительности) — логарифм таргета, MAE, квантильная или Tweedie loss.
  • Путаница «доказано» и «не отвергнуто». $p > 0.05$ означает «не хватило данных отвергнуть», а не «эффекта нет».
  • Множественные сравнения. 20 признаков × 5 гипотез при $\alpha = 0.05$ дают почти гарантированную «значимую» находку. Поправка Холма или контроль FDR (Benjamini–Hochberg).
  • Вера в единственную цифру метрики без доверительного интервала и без учёта дисперсии по фолдам кросс-валидации.
  • Забытая нормировка на $n-1$ при оценке дисперсии по выборке (np.var по умолчанию делит на $n$, ddof=0) — на маленьких выборках это заметное смещение.

Как это выглядит в проде

  • Feature store и мониторинг дрейфа. Сравнение распределения признака сегодня и на обучении — это KL/JS/PSI и тест Колмогорова–Смирнова. Порог PSI > 0.2 — типичный триггер переобучения модели; см. MLOps.
  • Калибровка вероятностей. Модель может хорошо ранжировать (высокий AUC) и при этом выдавать вероятности, которые нельзя подставлять в формулу ожидаемой выручки. Platt scaling — это логистическая регрессия над логитом, isotonic — монотонная регрессия; обе оцениваются через Brier score, который есть MSE в вероятностном пространстве.
  • A/B-тесты. Размер выборки считается заранее из MDE, дисперсии и мощности: $n \approx \frac{2(z_{1-\alpha/2} + z_{1-\beta})^2 \sigma^2}{\Delta^2}$. Дисперсию снижают CUPED — линейной поправкой на предпериодную ковариату, что есть просто проекция из части 1.
  • Разреженная линейная алгебра. Матрицы «пользователь × товар» имеют плотность ~0.01%; CSR-формат и разреженный ALS/SVD — обязательная часть рекомендательного стека.
  • Квантизация и смешанная точность. bfloat16 имеет тот же диапазон экспоненты, что float32, но 8 бит мантиссы — поэтому обучение в нём устойчиво, а накопление градиентов всё равно ведут в float32.

Минимальный чек-лист перед обучением модели

  1. Посмотрел X.shape, типы, масштабы столбцов.
  2. Посчитал np.linalg.cond(X) и корреляции — нет ли дублей и мультиколлинеарности.
  3. Отмасштабировал признаки, если модель градиентная или метрическая.
  4. Выбрал функцию потерь из распределения таргета, а не по привычке.
  5. Проверил, что нет inf/nan после первых батчей, и что loss падает.
  6. Отчитался метрикой с доверительным интервалом, а не одной цифрой.

Источники

Мини-итог

Линейная алгебра отвечает на вопрос «что такое данные и модель»: объекты — точки, признаки — координаты, модель — преобразование, а вырожденность матрицы — источник половины необъяснимых багов. Анализ отвечает на вопрос «как модель учится»: градиент задаёт направление, кривизна и обусловленность — скорость, выпуклость — гарантии. Вероятность отвечает на вопрос «что мы вообще минимизируем и насколько верим числу»: функция потерь — это минус логарифм правдоподобия, регуляризация — априорное распределение, а метрика без доверительного интервала — не результат.

Если из всей статьи запомнить три вещи: считайте cond(X), масштабируйте признаки перед градиентными методами, выводите функцию потерь из распределения таргета.

Что дальше

Данные и признаки: очистка, кодирование, масштабирование, утечки — переходим от математического языка к тому, как реально готовится матрица $X$: пропуски, категории, масштабирование (теперь вы знаете, почему оно критично), и главный источник «слишком хороших» метрик — утечки данных.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов