Вероятности, softmax и правдоподобие

⏱ 40 мин · 🧩 заданий: 7

Зачем вероятности

Хорошая модель не просто говорит «это кошка», а говорит «кошка с вероятностью 92%, собака 7%, птица 1%». Вероятности позволяют модели выражать уверенность, а нам — построить правильную функцию потерь. Нам понадобится совсем немного теории.

Распределения

Распределение вероятностей — список возможных исходов и их вероятностей. Вероятности неотрицательны и в сумме дают 1.

  • Дискретное: конечное число исходов. Бросок кубика: шесть исходов по 1/6. Ответ классификатора: 10 цифр со своими вероятностями.
  • Непрерывное: исход — любое число (рост, ошибка измерения). Описывается плотностью, вероятность попасть в интервал — площадь под ней.

Математическое ожидание (среднее) — взвешенное вероятностями среднее исходов: \mathbb{E}[X] = \sum_i p_i x_i. Дисперсия — средний квадрат отклонения от среднего: \text{Var}[X] = \mathbb{E}[(X - \mathbb{E}X)^2], а стандартное отклонение \sigma = \sqrt{\text{Var}} — типичный разброс.

import numpy as np
import vis

rng = np.random.default_rng(0)
rolls = rng.integers(1, 7, size=10_000)
print("среднее бросков:", rolls.mean(), "(теория 3.5)")
print("дисперсия:", rolls.var().round(3), "(теория 35/12 ≈ 2.917)")
faces, counts = np.unique(rolls, return_counts=True)
vis.bar([str(k) for k in faces], counts / len(rolls), title="Частоты исходов кубика", fmt=".3f")

Нормальное распределение

Самое важное непрерывное распределение — нормальное (гауссово) \mathcal{N}(\mu, \sigma^2), «колокол» со средним \mu и разбросом \sigma:

p(x) = \frac{1}{\sigma\sqrt{2\pi}} \exp\left(-\frac{(x - \mu)^2}{2\sigma^2}\right)

Сумма многих мелких независимых случайностей почти всегда распределена нормально (центральная предельная теорема), поэтому так ведут себя шумы и ошибки измерений. В нейросетях нормальное распределение используют для начальной инициализации весов.

import numpy as np
import vis

x = np.linspace(-5, 5, 300)
pdf = lambda x, mu, s: np.exp(-(x - mu) ** 2 / (2 * s ** 2)) / (s * np.sqrt(2 * np.pi))
vis.line(x, {"μ = 0, σ = 1": pdf(x, 0, 1), "μ = 0, σ = 2": pdf(x, 0, 2), "μ = 2, σ = 0.5": pdf(x, 2, 0.5)},
         title="Нормальное распределение", xlabel="x")

samples = np.random.default_rng(0).normal(0, 1, 100_000)
print(f"доля в пределах 1σ: {(np.abs(samples) < 1).mean():.3f}, 2σ: {(np.abs(samples) < 2).mean():.3f}, 3σ: {(np.abs(samples) < 3).mean():.4f}")

Softmax: от оценок к вероятностям

Последний слой классификатора выдаёт C произвольных чисел — логитов z_1, \dots, z_C (по одному на класс). Они могут быть отрицательными и не суммируются в 1. Чтобы получить распределение, применяют softmax:

\text{softmax}(z)_k = \frac{e^{z_k}}{\sum_{j=1}^{C} e^{z_j}}
  1. Экспонента делает все числа положительными и сохраняет порядок: больший логит → большая вероятность.
  2. Деление на сумму нормирует: вероятности в сумме дают 1.

Двигайте логиты и температуру:

  • Разница логитов важнее их величины: прибавьте ко всем одно число — вероятности не изменятся.
  • Температура T: \text{softmax}(z / T). При T \to 0 распределение становится «острым» (почти весь вес у максимума), при больших T — равномерным. Языковые модели используют температуру, чтобы регулировать «креативность» при генерации текста.

Устойчивое вычисление

e^{1000} не помещается во float — наивная формула сломается. Но мы знаем, что сдвиг всех логитов не меняет ответ. Вычтем максимум: тогда наибольшая экспонента равна e^0 = 1, и переполнения нет.

import numpy as np

def softmax_naive(z):
    e = np.exp(z)
    return e / e.sum()

def softmax(z):
    e = np.exp(z - z.max())
    return e / e.sum()

z = np.array([2.0, 1.0, 0.1])
print(softmax(z).round(3), softmax(z).sum())
big = np.array([1000.0, 1001, 1002])
print("наивно:", softmax_naive(big))       # nan — переполнение
print("устойчиво:", softmax(big).round(3))

Сигмоида — это softmax для двух классов

Для двух классов с логитами (z, 0): \text{softmax} = \frac{e^z}{e^z + 1} = \frac{1}{1 + e^{-z}} = \sigma(z). Поэтому в бинарной классификации модель выдаёт одно число, а вероятность получают сигмоидой.

Правдоподобие

Как оценить, хороша ли модель, которая выдаёт вероятности? Посмотрим, какую вероятность она дала тому, что произошло на самом деле. Если на картинке «3», а модель дала классу «3» вероятность 0.9 — хорошо; если 0.01 — плохо.

Правдоподобие (likelihood) модели на наборе данных — вероятность, которую модель приписывает всем правильным ответам сразу. Объекты независимы, поэтому вероятности перемножаются:

\mathcal{L}(\theta) = \prod_{i=1}^{N} p_\theta(y_i \mid x_i)

Метод максимального правдоподобия: подобрать параметры \theta так, чтобы правдоподобие было максимальным — модель должна считать реальные данные как можно более вероятными.

Произведение тысяч вероятностей исчезает в нуль, поэтому берут логарифм (максимум от этого не сдвигается), а чтобы получилась задача минимизации, — со знаком минус. Получается отрицательное лог-правдоподобие:

-\ln \mathcal{L}(\theta) = -\sum_{i=1}^{N} \ln p_\theta(y_i \mid x_i)

Классификация → кросс-энтропия

Если модель выдаёт вероятности через softmax, то -\ln p_\theta(y_i \mid x_i) — минус логарифм вероятности правильного класса. Среднее по объектам — это кросс-энтропия, главная функция потерь для классификации:

L_{CE} = -\frac{1}{N}\sum_{i=1}^{N} \ln \text{softmax}(z_i)_{y_i}
import numpy as np
import vis

for p in (0.99, 0.9, 0.5, 0.1, 0.01):
    print(f"вероятность правильного класса {p:<5} → потеря {-np.log(p):.3f}")

p = np.linspace(0.005, 1, 200)
vis.line(p, -np.log(p), title="Кросс-энтропия одного объекта", xlabel="вероятность правильного класса", ylabel="−ln p")

Уверенная правильная модель почти ничего не платит, неуверенная (0.5) платит 0.69, а уверенно ошибающаяся — очень много. Если модель угадывает случайно среди C классов, потеря равна \ln C (для 10 цифр это 2.30) — полезный ориентир: обучение должно начинаться примерно с этого значения.

Регрессия → MSE

Для регрессии предположим, что правильный ответ — это предсказание модели плюс нормальный шум: y = \hat y + \varepsilon, \varepsilon \sim \mathcal{N}(0, \sigma^2). Подставим плотность нормального распределения в отрицательное лог-правдоподобие:

-\ln p(y \mid x) = \frac{(y - \hat y)^2}{2\sigma^2} + \text{const}

Минимизировать это — то же самое, что минимизировать средний квадрат ошибки. MSE — не произвольный выбор, а максимальное правдоподобие при гауссовом шуме.

Итог

  • Распределение: вероятности исходов, сумма 1. Среднее и дисперсия описывают центр и разброс. Нормальное распределение — «колокол» \mathcal{N}(\mu, \sigma^2).
  • Softmax превращает логиты в вероятности: e^{z_k} / \sum_j e^{z_j}. Для устойчивости вычитают максимум; температура регулирует «остроту».
  • Максимальное правдоподобие: модель должна приписывать реальным данным наибольшую вероятность. Удобно минимизировать отрицательный логарифм.
  • Классификация → кросс-энтропия -\ln p_{\text{правильный}}; регрессия с гауссовым шумом → MSE.

Практика решено 0 из 7

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Ввод ответа

Чему равно математическое ожидание (среднее) броска честного шестигранного кубика?

Задание 2 Один вариант ответа

Логиты модели — [2, 2, 2]. Что выдаст softmax?

Задание 3 Один вариант ответа

Что произойдёт с softmax, если ко всем логитам прибавить 100?

Задание 4 Ввод ответа

Модель дала правильному классу вероятность 0.25. Чему равна кросс-энтропия на этом объекте (натуральный логарифм)? Округлите до двух знаков.

Задание 5 Один вариант ответа

Почему для классификации минимизируют −log вероятности правильного класса, а не просто максимизируют саму вероятность?

Задание 6 Программа на Python

Устойчивый softmax. Напишите softmax(z) для матрицы логитов z формы (N, C): softmax по каждой строке. Вычтите из каждой строки её максимум перед экспонентой — тогда не будет переполнения даже при логитах порядка 1000.

Какие тесты проверяют код (2)
p = softmax(np.array([[1.0, 2, 3], [0, 0, 0]]))
assert np.shape(p) == (2, 3)
assert np.allclose(p.sum(axis=1), 1), "Каждая строка должна суммироваться в 1"
assert np.allclose(p[1], 1 / 3)
assert np.allclose(p[0], np.exp([1, 2, 3]) / np.exp([1, 2, 3]).sum())

p = softmax(np.array([[1000.0, 1001, 1002]]))
assert np.all(np.isfinite(p)), "Переполнение: вычтите максимум строки"
assert np.allclose(p, softmax(np.array([[0.0, 1, 2]])))

Задание 7 Программа на Python

Кросс-энтропия. Напишите cross_entropy(logits, targets): logits — (N, C), targets — вектор правильных классов длины N. Верните среднее по объектам -\ln p_{\text{правильный}} (обычный float). Используйте устойчивый log-softmax: \ln \text{softmax}(z)_k = z_k - m - \ln\sum_j e^{z_j - m}, где m = \max_j z_j.

Какие тесты проверяют код (3)
assert abs(cross_entropy(np.zeros((4, 4)), np.array([0, 1, 2, 3])) - np.log(4)) < 1e-12, "При равных логитах потеря = ln C"

z = np.array([[2.0, 1, 0.1], [0.5, 2.5, 0.3]])
p = np.exp(z) / np.exp(z).sum(1, keepdims=True)
expected = -(np.log(p[0, 0]) + np.log(p[1, 2])) / 2
assert abs(cross_entropy(z, np.array([0, 2])) - expected) < 1e-12

v = cross_entropy(np.array([[1000.0, 0]]), np.array([1]))
assert np.isfinite(v) and abs(v - 1000) < 1e-9, "Нужна устойчивая формула через log-softmax"

Войдите, чтобы отмечать пройденные уроки.