Правдоподобие, softmax и cross-entropy
Как языковая модель превращает свои «сырые» оценки в вероятности и почему её учат минимизировать именно cross-entropy — с ручным счётом на маленьких примерах.
Зачем это нужно
Заголовок раздела «Зачем это нужно»На последнем шаге любая большая языковая модельбольшая языковая модель (БЯМ, LLM)EN large language model (LLM)В китайском «большая модель» («да мосин») — разговорное общее название для LLM и вообще foundation models. выдаёт вектор чисел — по одному на каждый токен словаря. У Qwen это около 152 тысяч чисел, у DeepSeek-V3 — около 129 тысяч. Эти числа называются логитамилогитEN logitСтрого — логарифм шансов log(p / (1 − p)). В нейросетях логитами называют любые выходы до softmax или сигмоиды. (logits; вы встречали их в уроке 1.0.3). Они могут быть любыми: отрицательными, огромными, нулевыми.
Чтобы выбрать следующий токен, логиты нужно превратить в вероятности — в категориальное распределение из прошлого урока. Это делает функция softmax. А чтобы модель училась, нужно измерить, насколько её вероятности расходятся с правдой. Это делает перекрёстная энтропияперекрёстная энтропияEN cross-entropy. На этих двух функциях стоит обучение каждой языковой модели в мире, от GPT-2 до DeepSeek-V4.
flowchart LR A["Контекст:<br/>«Карп прыгает через»"] --> B[Трансформер] B --> C["Логиты<br/>z ∈ ℝ^V"] C --> D[softmax] D --> E["Вероятности<br/>p(«врата») = 0.71<br/>p(«реку») = 0.12 …"] E --> F["Loss = −log p(правильный токен)"]
В уроке 1.0.3 вы уже прочитали формулы softmax и cross-entropy «по символам». Теперь разберёмся, почему они именно такие и как считать их так, чтобы компьютер не сломался на больших числах.
Интуиция: softmax
Заголовок раздела «Интуиция: softmax»Softmax делает три вещи:
- Делает все числа положительными — с помощью экспоненты (она всегда больше нуля, урок 1.0.1).
- Нормирует их, чтобы сумма была равна 1.
- Усиливает различия: если логит больше на 1, вероятность больше в раза.
Читаем: «вероятность -го токена — это в степени его логита, делённое на сумму таких экспонент по всем токенам словаря». Знаменатель один на всех — он и делает сумму вероятностей равной 1.
Пример по шагам. Логиты .
| Шаг | Токен 1 | Токен 2 | Токен 3 | |
|---|---|---|---|---|
| логит | 1 | 2 | 3 | |
| экспонента | 2.718 | 7.389 | 20.086 | сумма 30.193 |
| вероятность | 0.090 | 0.245 | 0.665 | сумма 1 |
Самый большой логит забирает большую часть вероятности, но остальным тоже кое-что достаётся. Проверьте свойство 3: и — соседние логиты отличаются на 1, вероятности — в раз.
Та же формула в коде — «школьная» версия из урока 1.0.3:
import math
def softmax(z): exps = [math.exp(v) for v in z] # шаг 1: e^{z_i} — все числа стали положительными s = sum(exps) # знаменатель: сумма экспонент, считаем один раз return [e / s for e in exps] # шаг 2: делим каждую на сумму
p = softmax([1.0, 2.0, 3.0])print([round(x, 3) for x in p], round(sum(p), 3)) # [0.09, 0.245, 0.665] 1.0Эта версия правильная, но на больших логитах ломается. Как именно и как это чинят — в разделе «Ловушка: переполнение».
Метод максимального правдоподобия
Заголовок раздела «Метод максимального правдоподобия»Откуда вообще берутся «правильные» вероятности? Начнём с самого простого случая. Допустим, мы подбросили монету 10 раз и получили 7 орлов и 3 решки. Какая вероятность орла «самая правдоподобная»?
Правдоподобие (likelihood) — это вероятность увидеть именно наши данные, если вероятность орла равна . Броски независимы (урок 1.3.1), поэтому вероятности перемножаются: семь раз и три раза :
Подставим несколько значений:
| 0.3 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | |
|---|---|---|---|---|---|---|
| 0.000075 | 0.00098 | 0.00179 | 0.00222 | 0.00168 | 0.00048 |
Максимум — при , ровно доле орлов в данных. Это и есть метод максимального правдоподобия (MLE): выбрать параметры, при которых наблюдённые данные наиболее вероятны. Компьютер найдёт максимум перебором:
ps = [i / 100 for i in range(1, 100)] # p = 0.01, 0.02, …, 0.99best = max(ps, key=lambda p: p ** 7 * (1 - p) ** 3) # p с наибольшим правдоподобиемprint(best) # 0.7(Если хочется строго: в максимуме производная равна нулю, урок 1.2.1. Производная равна , и она обращается в ноль при , то есть при .)
Зачем логарифм. Правдоподобие текста — произведение тысяч вероятностей токенов (цепное правило вероятностей из урока 1.3.1), и оно быстро становится машинным нулём (урок 1.0.2). Логарифм превращает произведение в сумму и не меняет точку максимума: тоже максимален при , потому что логарифм — растущая функция. А поскольку оптимизаторы привыкли минимизировать (градиентный спуск идёт вниз, урок 1.0.3), ставят минус:
Читаем: «loss при параметрах — это минус сумма по всем позициям логарифмов вероятности, которую модель дала настоящему токену , зная все предыдущие токены ». Это отрицательное лог-правдоподобие (NLLотрицательное лог-правдоподобие (NLL)EN negative log-likelihood (NLL)−log P(данные | модель), обычно усреднённое по токенам. Для языковой модели совпадает с cross-entropy.) текста. Языковую модель учат именно так: для каждой позиции в тексте берём вероятность, которую модель дала настоящему следующему токену, логарифмируем, ставим минус и усредняем.
Числа. Пример из урока 1.3.1: после «Карп» модель дала слову «прыгает» вероятность 0.2, затем слову «через» — 0.5, затем слову «врата» — 0.71. Минус логарифмы: , , . Сумма 2.645, среднее по трём токенам — 0.882. Самый большой вклад дал самый «неожиданный» для модели токен.
import mathprobs = [0.2, 0.5, 0.71] # вероятности настоящих токеновnll = -sum(math.log(p) for p in probs) # минус сумма логарифмовprint(round(nll, 3), round(nll / len(probs), 3)) # 2.645 0.882 — сумма и среднее на токенCross-entropy — то же самое, но с другого угла
Заголовок раздела «Cross-entropy — то же самое, но с другого угла»перекрёстная энтропияперекрёстная энтропияEN cross-entropy между «правильным» распределением и предсказанием модели :
В языковой модели правильный ответ один (следующий токен ), то есть — это one-hot кодированиеone-hot кодирование (унитарный код)EN one-hot encodingВектор длины V из нулей с единицей в позиции номера токена. Все one-hot векторы попарно ортогональны. вектор: единица на месте , нули на остальных (урок 1.0.3). Тогда от суммы остаётся одно слагаемое:
Это ровно отрицательное лог-правдоподобие правильного токена. Пример из урока 1.0.3: логиты дают вероятности ; если правильный — первый токен, то .
В коде обучения LLM на PyTorch вы всегда видите F.cross_entropy(logits, targets). Эта функция принимает логиты,
а не вероятности, и номера правильных токенов; softmax и логарифм она делает сама, устойчивым способом, который
мы разберём через пару разделов.
Как читать значение loss:
| Вероятность правильного токена | Loss |
|---|---|
| 1.0 (модель уверена и права) | 0 |
| 0.5 | 0.69 |
| 0.1 | 2.30 |
| 1 / 152 000 (полное незнание) | 11.93 |
Perplexity (перплексияперплексияEN perplexity) — это просто (урок 1.0.2). При loss = 2.3 perplexity ≈ 10: модель в среднем «колеблется между 10 равновероятными вариантами».
Ловушка: переполнение
Заголовок раздела «Ловушка: переполнение»Логиты в настоящих моделях бывают большими, а в пониженной точности переполнение наступает намного раньше: в FP16 экспонента переполняется уже при (урок 1.0.2). Возьмём и попробуем «школьный» softmax. Самая большая экспонента не помещается во float64 (предел около , урок 1.0.2), и результат зависит от того, чем считать:
import mathmath.exp(1000) # OverflowError: math range error — чистый Python падает сразуimport numpy as np # запускайте отдельно: ошибка выше остановила бы программуz = np.array([1000.0, 1001.0, 1002.0])e = np.exp(z) # RuntimeWarning: overflow encountered in expprint(e) # [inf inf inf]print(e / e.sum()) # [nan nan nan] — inf / inf = nan, «не число»NumPy не падает, а молча выдаёт nan, и дальше nan расползается по всем вычислениям. Это хуже явной ошибки.
Спасает простое свойство: softmax не меняется, если ко всем логитам прибавить (или вычесть) одну и ту же константу :
Читаем: (правило степеней из урока 1.0.2), и общий множитель есть и в числителе, и в каждом слагаемом знаменателя — он сокращается. Если взять , то самая большая экспонента равна , а остальные меньше 1 — переполнения нет.
Числа. Для берём и получаем сдвинутые логиты . Их экспоненты — безобидные числа, а ответ совпадает с примером : эти логиты отличаются от наших на одну и ту же константу 999.
Для cross-entropy есть ещё одна ловушка — . Возьмём и правильный второй токен. После сдвига
его вероятность равна , а во float64 округляется до 0.0. Вероятность стала нулём,
и math.log(0.0) падает с ValueError: math domain error (урок 1.0.2). Поэтому логарифм softmax считают сразу, не
вычисляя саму вероятность, через log-sum-exp:
Читаем: — «логарифм суммы экспонент» , посчитанный безопасно: максимум вынесли наружу, а под логарифмом осталась сумма чисел от 0 до 1, причём хотя бы одно из них равно 1. Поэтому под логарифмом никогда не бывает нуля. Откуда формула: (логарифм частного — разность логарифмов, урок 1.0.2), а — тот же вынос за скобку.
Числа. Для : , под логарифмом , значит . Loss для правильного второго токена: — огромный, но конечный, и обучение может его уменьшать. Для первого токена: .
Величину называют log-softmax — это логарифмы вероятностей, посчитанные без самих
вероятностей. Для log-softmax равен , и cross-entropy для первого токена —
просто минус первое число, 0.417. В PyTorch это F.log_softmax; вы встретите её в уроке 1.3.3.
Почему именно cross-entropy: градиент «p минус ответ»
Заголовок раздела «Почему именно cross-entropy: градиент «p минус ответ»»Обучение двигает логиты по градиенту loss (урок 1.2.3). У связки «softmax + cross-entropy» он получается удивительно простым. Производная loss по логиту равна
где , а — one-hot вектор правильного ответа. Читаем: «вероятность, которую модель дала токену, минус то, что должно было быть (1 для правильного, 0 для остальных)». Вывод — несколько строк цепного правила из урока 1.2.2, он есть в разделе 3.4 d2l (ссылка в источниках).
Числа. , правильный — первый токен, . Градиент: . Шаг градиентного спуска идёт против градиента: логит правильного токена растёт, остальные падают — тем сильнее, чем больше вероятности им досталось по ошибке. Каждая компонента лежит между −1 и 1, поэтому градиент не «взрывается», даже когда модель уверенно ошибается. Сравните с производной из урока 1.2.1: по самой вероятности толчок при равен −100, а по логиту — не больше 1 по модулю. Softmax и логарифм «гасят» друг друга, и обучение получается устойчивым. Это одна из главных причин, почему классификаторы и LLM учат именно на cross-entropy.
Проверим формулу центральной разностью из урока 1.2.1 (логиты маленькие, поэтому «школьный» softmax здесь безопасен):
import math
def softmax(z): exps = [math.exp(v) for v in z] s = sum(exps) return [e / s for e in exps]
def ce(z, t): return -math.log(softmax(z)[t]) # cross-entropy «в лоб»
z, t = [2.0, 1.0, 0.1], 0p = softmax(z)analytic = [p[i] - (1 if i == t else 0) for i in range(len(z))] # p − y
h = 1e-5numeric = []for i in range(len(z)): zp = list(z); zp[i] += h # копия z, i-й логит сдвинут вверх zm = list(z); zm[i] -= h # и вниз numeric.append((ce(zp, t) - ce(zm, t)) / (2 * h)) # центральная разность
print([round(g, 3) for g in analytic]) # [-0.341, 0.242, 0.099]print([round(g, 3) for g in numeric]) # [-0.341, 0.242, 0.099]Практика
Заголовок раздела «Практика»Реализуйте устойчивые softmax и cross_entropy на чистом Python (без NumPy). Тесты проверят и правильность,
и устойчивость к огромным логитам вроде [1000.0, 1000.0].
import math
def softmax(logits):
"""Превращает список логитов в список вероятностей (сумма = 1).
Пример: softmax([1.0, 2.0, 3.0]) → [0.090, 0.245, 0.665] (с округлением).
Должна работать и для огромных чисел, например [1000.0, 1000.0] → [0.5, 0.5]:
перед экспонентой вычтите из всех логитов максимум (урок 1.3.2, «Ловушка: переполнение»).
"""
# TODO: m = max(logits); экспоненты от (z − m); их сумма; каждую экспоненту делим на сумму
...
def cross_entropy(logits, target):
"""Cross-entropy для одного примера: -log(softmax(logits)[target]).
target — индекс правильного класса (токена), считается с нуля.
Пример: cross_entropy([0.0, 0.0, 0.0, 0.0], 2) → ln 4 ≈ 1.386 (все 4 варианта равновероятны).
Считайте через logsumexp: m + log(Σ exp(z − m)) − logits[target], иначе на [1000.0, 0.0]
вероятность округлится до 0.0 и log(0) упадёт с ValueError.
"""
# TODO: ваш код
...
# Проверьте себя кнопкой «Запустить»:
print(softmax([1.0, 2.0, 3.0])) # ≈ [0.0900, 0.2447, 0.6652]
print(cross_entropy([0.0, 0.0, 0.0, 0.0], 2)) # ≈ 1.3863
Проверь себя
Заголовок раздела «Проверь себя»- Softmax превращает логиты в вероятности: экспонента делает числа положительными, деление на сумму — нормирует. Логит больше на 1 — вероятность больше в раз. Для устойчивости вычитаем максимум: softmax от сдвига не меняется.
- Метод максимального правдоподобия выбирает параметры, при которых данные наиболее вероятны. Для LLM это значит минимизировать отрицательное лог-правдоподобие, то есть cross-entropy .
- — так loss считают без переполнения и без .
- Градиент cross-entropy по логитам — : простой, ограниченный и устойчивый.
- Loss свежей модели ≈ . Perplexity = . На масштабе DeepSeek-V3 экономят точность почти везде, кроме вероятностей.
Дальше: энтропия, KL-дивергенция и perplexity. KL понадобится для RLHF, GRPO и дистилляции.