Математика глубокого обучения

Численная устойчивость

logsumexp, стабильный softmax, температура — приёмы, без которых формулы не работают

Шаг 79 из 117 · ~30 мин

Катастрофическое сокращение

Из прошлого урока: шаг сетки пропорционален величине числа. Отсюда первое следствие — вычитание близких величин теряет значащие цифры.

Пример, который стоит увидеть, потому что он ломает и в двойной точности. Посчитаем дисперсию набора [108, 108+1, 108+2][10^8,\ 10^8+1,\ 10^8+2] двумя способами:

формуларезультат
E[x2]E[x]2\mathbb{E}[x^2] - \mathbb{E}[x]^222
1n(xixˉ)2\frac1n\sum (x_i - \bar{x})^20.66670.6667

Верный ответ — 2/32/3. Наивная формула ошибается втрое, и не в fp32, а в fp64: она вычитает два числа порядка 101610^{16}, различающиеся на единицу, и от результата остаётся мусор.

Отсюда общее правило: , а не после. Тот же принцип объясняет, почему в блоке 3 формула дисперсии через отклонения от среднего — не педантизм, а необходимость.

1ni(xixˉ)2вместо1nixi2xˉ2\htmlData{k=cancel}{\frac{1}{n}\sum_i (x_i - \bar{x})^2} \quad\text{вместо}\quad \htmlData{k=naive}{\frac{1}{n}\sum_i x_i^2 - \bar{x}^2}

logsumexp

Второй источник беды — экспонента. e1000e^{1000} переполняется, e1000e^{-1000} обнуляется, и оба случая встречаются в логитах постоянно.

logiezi = m+logiezim,m=maxizi\log \sum_i e^{z_i} \ = \ m + \log \sum_i e^{z_i - m}, \qquad m = \max_i z_i

Тождество проверяется вынесением eme^m за скобку. Но численно два выражения совершенно разные:

zzнаивносо сдвигом
[1000,1001][1000, 1001]\infty1001.3131001.313
[1000,1001][-1000, -1001]-\infty999.687-999.687

После сдвига наибольший показатель равен нулю, то есть e0=1e^0 = 1 — переполнение невозможно по построению. Остальные слагаемые не больше единицы, и если какое-то обнулилось, оно и было пренебрежимо мало.

Второй случай — обнуление, и он опаснее: наивная формула даёт -\infty, что дальше превращается в nan при первом же вычитании. Первый хотя бы шумит явно.

Стабильный softmax

Ровно тот же приём:

softmax(z)i=ezimjezjm\text{softmax}(z)_i = \frac{e^{z_i - m}}{\sum_j e^{z_j - m}}

И у softmax есть свойство, которое делает это законным: инвариантность к сдвигу. Прибавление константы ко всем логитам не меняет результат, потому что ezi+ce^{z_i + c} сокращается в числителе и знаменателе.

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

сдвинуть все логиты 0
температура T 1
сумма вероятностей
1
максимум
60.9%

Температура и затухание градиента

Температура делит логиты: softmax(z/T)\text{softmax}(z / T). При T0T \to 0 распределение стремится к argmax, при TT \to \infty — к равномерному.

Практически важна не сама температура, а то, что происходит с градиентом. Якобиан softmax содержит pi(1pi)p_i(1 - p_i), и когда распределение схлопывается, эта величина обращается в нуль:

TTмаксимальная вероятностьмасштаб градиента
110.6440.6442.31012.3 \cdot 10^{-1}
0.50.50.8650.8651.21011.2 \cdot 10^{-1}
0.10.11\approx 14.51054.5 \cdot 10^{-5}
0.020.021\approx 11.910221.9 \cdot 10^{-22}

Последняя строка — не «маленький градиент», а его отсутствие: 102210^{-22} неотличимо от нуля даже в fp32. Отсюда прямая связь с уроком про внимание: масштабирование на 1/d1/\sqrt{d} нужно именно затем, чтобы логиты не оказались в этой области.

Приёмы списком

проблемарешение
logez\log \sum e^{z} переполняетсявычесть максимум
logsoftmax\log \text{softmax}log_softmax, а не log(softmax(x))
log(1+x)\log(1 + x) при малом xxlog1p
ex1e^x - 1 при малом xxexpm1
произведение вероятностейсумма логарифмов (блок 3, урок 120)
дисперсияцентрировать до квадрата, или алгоритм Уэлфорда
log(1σ(x))\log(1 - \sigma(x))через logsigmoid(-x)
накопление длинной суммыfp32-накопитель даже при bf16-данных

Вторая строка стоит отдельного внимания. log(softmax(x)) вычисляет экспоненты, нормирует, затем берёт логарифм — и если какая-то вероятность обнулилась, получается -\infty. log_softmax считает zilogsumexp(z)z_i - \text{logsumexp}(z) напрямую: ни одной экспоненты в опасной области, ни одного логарифма от нуля. Именно поэтому в PyTorch кросс-энтропия принимает логиты, а не вероятности — она внутри вызывает log_softmax, и передача уже нормированных вероятностей ломает эту защиту.

Накопители

Последний приём, который часто забывают. Из прошлого урока: в bf16 прибавление 10410^{-4} к единице не меняет ничего, потому что эпсилон равен 7.81037.8 \cdot 10^{-3}.

Значит, суммируя тысячу малых чисел в bf16, вы потеряете почти все: как только частичная сумма подрастёт, следующие слагаемые начнут округляться в неё бесследно. Поэтому:

  • накопители держат в fp32, даже когда данные в bf16. Матричные ядра так и устроены: умножения в bf16, накопление в fp32;
  • мастер-копия весов в fp32 — по той же причине, а не ради точности вычислений;
  • для очень длинных сумм существует суммирование Кэхана, восстанавливающее потерянные младшие биты.

Здесь полезно вспомнить формат-виджет: сравните эпсилон bf16 и fp32 и прикиньте, сколько слагаемых нужно, чтобы разница стала заметной.

раскладка битов

всего бит
16
эпсилон
7.81e-3
максимум
3.390e+38
мин. нормальное
1.18e-38
десятичных цифр
2.4
Эпсилон 7.8e-3. Прибавление 1e-4 к числу порядка единицы не изменит ничего: поправка меньше половины шага сетки и округлится бесследно. Именно поэтому шаг оптимизатора нельзя применять прямо к bf16-весам.

Последнее наблюдение из этого сравнения полезно само по себе: у bf16 и fp32 одинаковая экспонента. bf16 — это fp32 с отрезанной мантиссой, поэтому преобразование между ними сводится к отбрасыванию или дописыванию нулей, без всякой обработки диапазона. Это одна из причин, по которой bf16 удобен аппаратно.

Источники

Проверки

0 из 2
  1. Приёмы устойчивости

    Отметьте все верные утверждения о численной устойчивости.

  2. logsumexp со сдвигом

    Реализуйте logsumexp_facts(z) для вектора логитов. Верните [naive_finite, lse, max_prob, entropy]:

    • naive_finite1.0, если наивная сумма iezi\sum_i e^{z_i} получилась конечной и строго положительной, иначе 0.0. В Python переполнение math.exp бросает OverflowError — перехватите его; в JavaScript получится Infinity;
    • lse = m+logiezimm + \log\sum_i e^{z_i - m}, где m=maxizim = \max_i z_iустойчивая формула;
    • max_prob — наибольшая вероятность softmax, посчитанная как ezilsee^{z_i - \texttt{lse}};
    • entropy — энтропия этого распределения в битах.

    Обратите внимание, что lse считается устойчиво всегда, независимо от того, что показал naive_finite: первое число сообщает, провалилась ли наивная формула, а не как считать правильную.

    Полезное свойство: pi=ezilsep_i = e^{z_i - \text{lse}} — это и есть softmax, потому что lse\text{lse} равен логарифму знаменателя. Отдельно нормировать не нужно.

    функция logsumexp_facts

    Загрузка редактора…

    Ctrl/⌘ + Enter