Математика для 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$.
Что из этого следует практически:
- Сингулярные числа $\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$ — второй член и так гасится числом деревьев, а первый без декорреляции не уходит никогда.
Численные ловушки, которые ломают прод
- Переполнение в 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)
log(0)в кросс-энтропии. Если модель выдала ровно 0 или 1, логлосс — бесконечность. Библиотеки клиппируют вероятности в $[\epsilon, 1-\epsilon]$; в своём коде считайте логлосс сразу из логитов (binary_cross_entropy_with_logits), а не из вероятностей.- Катастрофическая потеря значимости. «Наивная» формула дисперсии $\mathbb{E}[X^2] - (\mathbb{E}X)^2$ на данных вида $10^9 \pm 1$ даёт отрицательную дисперсию. Используйте алгоритм Уэлфорда или двухпроходную формулу.
- Сравнение float на равенство при поиске порогов, дедупликации, проверке
сходимости — только через
np.iscloseс явнымatol. - Накопление в
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.
Минимальный чек-лист перед обучением модели
- Посмотрел
X.shape, типы, масштабы столбцов. - Посчитал
np.linalg.cond(X)и корреляции — нет ли дублей и мультиколлинеарности. - Отмасштабировал признаки, если модель градиентная или метрическая.
- Выбрал функцию потерь из распределения таргета, а не по привычке.
- Проверил, что нет
inf/nanпосле первых батчей, и что loss падает. - Отчитался метрикой с доверительным интервалом, а не одной цифрой.
Источники
- Gilbert Strang. Introduction to Linear Algebra + курс MIT 18.06 — https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/
- Deisenroth, Faisal, Ong. Mathematics for Machine Learning — бесплатный PDF, https://mml-book.github.io/
- Boyd, Vandenberghe. Convex Optimization — бесплатный PDF, https://web.stanford.edu/~boyd/cvxbook/
- Trefethen, Bau. Numerical Linear Algebra — обусловленность и устойчивость, канонический текст.
- Bishop. Pattern Recognition and Machine Learning — https://www.microsoft.com/en-us/research/publication/pattern-recognition-machine-learning/
- Hastie, Tibshirani, Friedman. The Elements of Statistical Learning — бесплатный PDF, https://hastie.su.domains/ElemStatLearn/
- Kingma, Ba. Adam: A Method for Stochastic Optimization — https://arxiv.org/abs/1412.6980
- Baydin et al. Automatic Differentiation in Machine Learning: a Survey — https://arxiv.org/abs/1502.05767
- Документация NumPy по линейной алгебре — https://numpy.org/doc/stable/reference/routines.linalg.html
- Andrej Karpathy. micrograd — 100 строк автодифференцирования, https://github.com/karpathy/micrograd
Мини-итог
Линейная алгебра отвечает на вопрос «что такое данные и модель»: объекты — точки, признаки — координаты, модель — преобразование, а вырожденность матрицы — источник половины необъяснимых багов. Анализ отвечает на вопрос «как модель учится»: градиент задаёт направление, кривизна и обусловленность — скорость, выпуклость — гарантии. Вероятность отвечает на вопрос «что мы вообще минимизируем и насколько верим числу»: функция потерь — это минус логарифм правдоподобия, регуляризация — априорное распределение, а метрика без доверительного интервала — не результат.
Если из всей статьи запомнить три вещи: считайте cond(X), масштабируйте признаки перед
градиентными методами, выводите функцию потерь из распределения таргета.
Что дальше
Данные и признаки: очистка, кодирование, масштабирование, утечки — переходим от математического языка к тому, как реально готовится матрица $X$: пропуски, категории, масштабирование (теперь вы знаете, почему оно критично), и главный источник «слишком хороших» метрик — утечки данных.