Как школьная алгебра за 7 класс спасает softmax от затухания градиентов

Прокомментировать Просмотры: 4

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

Оба этих казуса напрямую затрагивают привычные нам функции softmax и cross-entropy.

22 октября 2026 года исследователи из Google DeepMind — Джонатан Баррон (Jonathan T. Barron), Дэн Гнанапрагасам (Dan Gnanapragasam) и Майкл Тшаннен (Michael Tschannen) — выпустили препринт A Fix for Softmax and Cross-Entropy. Авторы детально исследовали два вычислительных изъяна (в публикации они получили названия Softmax Collapse и Softmax Cancellation), обнаруженных в базовых библиотеках вроде PyTorch и JAX, и презентовали патчи, полностью сохраняющие изначальную математическую логику.

Означает ли это, что все эти годы нейросети обучались некорректно? Разумеется, нет. Тем не менее кейс крайне любопытный: порой для кардинального изменения динамики обучения достаточно иначе записать банальную школьную дробь. Давайте обратимся к цифрам, графикам и фрагментам кода.

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

Что мы детально разберем:

Начнем с базового кейса: учим алгоритм отличать кота от собаки

Допустим, наша модель занимается классификацией изображений. На вход подается снимок кота. В тренировочном датасете эталонная метка известна заранее: кот.

На текущем этапе модель еще может ошибаться, но наша цель — скорректировать её параметры w таким образом, чтобы вероятность верного класса росла, а штраф (loss) минимизировался.

Сперва нейросеть выдает пару сырых чисел — логиты z_{\text{кот}} и z_{\text{собака}}.

Преобразование Softmax переводит их в вероятности q_{\text{кот}} и q_{\text{собака}}, в сумме дающие единицу. Следом функция cross-entropy рассчитывает итоговый loss — меру штрафа.

Для нашего конкретного кадра это отрицательный логарифм вероятности кота: чем ближе этот показатель к единице, тем меньше штрафные санкции.

Затем запускается обратный проход.

Мы вычисляем градиент — вектор частных производных функции потерь, отражающий степень изменения штрафа при микроскопической модификации каждого логита.

Опираясь на эти производные и архитектуру самой нейросети, мы находим градиенты по весам, после чего оптимизатор производит обновление коэффициентов w.

Сами логиты формируются на основе изображения и текущих весов; мы не обучаем изолированно два фиктивных числа «кот» и «собака». Однако любая неточность в производных по логитам неизбежно транслируется дальше, искажая корректировку всей сети.

Схема 1. Прямой проход генерирует вероятности и уровень штрафа. Обратный проход вычисляет производные, на базе которых оптимизатор модифицирует веса модели.

Именно в этот момент можно корректно сформулировать главный вопрос статьи: насколько точно вычислительная машина рассчитывает данные производные, если модель демонстрирует абсолютную уверенность в ответе?

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

В бинарном частном случае мы получаем хорошо знакомую сигмоиду

Обозначим разницу логитов как m. Вероятность кота выражается через сигмоидальную функцию:

m=z_{\text{кот}}-z_{\text{собака}},\qquad{}q_{\text{кот}}=\frac{1}{1+e^{-m}}=\sigma(m).

В строгой математике вторая вероятность равна q_{\text{собака}}=1-q_{\text{кот}}. Если m=0, шансы классов равны; чем сильнее растет m, тем увереннее модель склоняется в пользу кота. В классической логистической регрессии эта разница определяется через признаки и весовые коэффициенты. В глубокой сети перед логитами могут стоять сотни промежуточных слоев, но финальный этап вычислений остается идентичным.

Поскольку верным ответом является кот, формула потерь принимает вид:

\mathcal{L}=-\ln q_{\text{кот}}=\ln(1+e^{-m}).

Производная по m равна q_{\text{кот}} - 1. Логит кота входит в m с положительным знаком, а логит собаки — с отрицательным. Отсюда формируются две составляющие градиента по логитам:

g_{\text{кот}}=q_{\text{кот}}-1,\qquad{}g_{\text{собака}}=1-q_{\text{кот}}=q_{\text{собака}}.

Возьмем стандартный умеренный пример: логиты [2, 0], то есть m=2. На выходе получаем вероятности порядка [0.881; 0.119], уровень потерь около 0.127 и градиент [−0.119; +0.119].

Знаки имеют вполне интуитивную интерпретацию. Если при прочих равных слегка завысить логит кота, штраф снизится, поэтому производная отрицательна. Если же увеличить логит собаки, штраф возрастет — производная положительна. При стандартном градиентном спуске мы движемся против градиента. На этом этапе никаких вычислительных аномалий не наблюдается.

А где же обещанная защита от переполнения?

Она создана для предотвращения совершенно иных проблем. Например, логиты [1020, 1000] дают ровно такую же разницу в 20 единиц, что и [20, 0]. Однако попытка напрямую вычислить e^{1020} и e^{1000} неминуемо приведет к переполнению разрядной сетки float32: вместо конечных чисел возникнут бесконечности, а последующая нормировка выдаст NaN.

Общепринятый трюк — предварительное вычитание максимума из всех логитов перед экспоненцированием. Из набора [1020, 1000] мы получаем [0, −20], а экспоненты составят 1 и примерно 2.06·10⁻⁹. Обе величины прекрасно укладываются в рамки float32.

Почему это допустимо? Мы делим каждую экспоненту на один и тот же коэффициент пропорциональности, благодаря чему их относительные доли в общей сумме остаются неизменными. Для двух классов формула выглядит так:

\frac{e^{z_{\text{кот}}}}{e^{z_{\text{кот}}}+e^{z_{\text{собака}}}}= \frac{e^{z_{\text{кот}}-z_{\max}}}{e^{z_{\text{кот}}-z_{\max}}+e^{z_{\text{собака}}-z_{\max}}}.

Схема 2. Смещение [1020, 1000] → [0, −20] сохраняет корректность вероятностей и позволяет рассчитать экспоненты без экстремального переполнения.

Общая проблематика уже освещалась на SE7ENе в материале «Откуда в обучении берётся nan: численная нестабильность в ML и почему всё считают в логарифмах» за авторством экспертов OTUS. Там основное внимание уделено переполнениям, возникновению NaN и переходу к логарифмическим вычислениям.

В нашем тестовом сценарии [20, 0] никакой угрозы переполнения не существует: даже e^{20} без проблем помещается в тип float32. Мы всё равно применяем стандартную защиту с вычитанием максимума, однако она абсолютно бессильна против другого фундаментального изъяна — нивелирования микроскопической прибавки на фоне единицы.

Первая аномалия: исчезновение одной из компонент градиента

Представим, что наша модель стала ещё более самоуверенной: логиты составляют [20, 0], правильным ответом по-прежнему числится кот. Зафиксируем малый относительный вес второго класса через параметр r:

r=e^{-m}=e^{-20}\approx2{,}061\cdot10^{-9}.

Тогда вероятность q_{\text{кот}}=1/(1+r) невероятно близка к единице, но с точки зрения математики всё же меньше неё. Это означает, что штраф остаётся строго положительным, а градиент ещё не обнулился. Теоретически мы ждем две составляющие: примерно [−2.06·10⁻⁹; +2.06·10⁻⁹].

Давайте проанализируем стандартный расчет первой компоненты:

g_{\text{кот}}=\frac{1}{1+r}-1.

Формат float32 оперирует примерно семью значащими десятичными знаками. Сама величина r сохраняется корректно. Однако сумма 1+r округляется до единицы: добавка ничтожно мала на фоне единицы. В результате мы получаем 1/1-1=0. Проблема кроется не в нехватке диапазона для хранения крошечного числа, а в потере его веса при суммировании с крупной величиной.

Вероятность собаки Softmax вычисляет отдельно в виде r/(1+r). Она продолжает оставаться равной примерно r, в то время как вероятность кота уже округлилась до единицы. Отсюда и берется асимметрия: одна составляющая стерта, а вторая уцелела.

Решение этой проблемы заимствовано из школьного курса алгебры. Приведем дроби к единому знаменателю:

\frac{1}{1+r}-1=\frac{1-(1+r)}{1+r}=-\frac{r}{1+r}.

Теперь миниатюрное число оказывается в числителе. Даже если знаменатель округлится до единицы, итоговое значение будет эквивалентно -r. Математика осталась прежней, но результат машинного вычисления кардинально изменился.

Схема 3. Традиционная запись теряет r в операции 1 + r, в итоге давая ноль. Эквивалентная дробь −r / (1 + r) успешно сохраняет крошечную отрицательную компоненту.

Протестируем это на Python

Мы четко понимаем, что именно тестируем и какой результат ожидаем увидеть. В нашем скрипте первый класс соответствует коту, второй — собаке; target = 0. Инструкция shifted = logits - logits.max() реализует ту самую страховку от переполнения. Затем вычисляются вероятности через softmax, а из вероятности правильного класса вычитается единица:

import numpy as np

logits = np.array([20.0, 0.0], dtype=np.float32)

# Стандартная схема softmax с защитой от переполнения.
shifted = logits - logits.max()
e = np.exp(shifted)
probs = e / e.sum()

# Производные cross-entropy по логитам, target = 0.
grad = probs.copy()
grad[0] -= np.float32(1.0)

print(f"Вероятности: {probs}")
print(f"Градиент:    {grad}")
print(f"Сумма градиентов: {grad.sum():.6e}")
Вероятности: [1.0000000e+00 2.0611537e-09]
Градиент:    [0.0000000e+00 2.0611537e-09]
Сумма градиентов: 2.061154e-09

Рис. 1. Для логитов [20, 0] корректный градиент включает две противоположные компоненты. При обычном расчете в float32 отрицательная составляющая исчезает, а положительная сохраняется. Собственный численный пример; цифры округлены для читаемости.

Для кота ожидалось значение порядка −2.06·10⁻⁹, а на выходе получился ровно ноль. У собаки положительная компонента уцелела. Перед нами классический пример Softmax Collapse. Исчезновение одной составляющей не означает немедленную остановку обучения всей нейросети: сигналы от остальных классов продолжают поступать в обратном направлении, но они уже искажены.

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

Данный тест демонстрирует особенности вычислительного конвейера NumPy. Поведение вашего бэкенда в PyTorch или JAX требует профилирования в вашей среде выполнения. А вопрос о том, способны ли столь микроскопические погрешности заметно влиять на процесс обучения, мы обсудим чуть ниже на основе данных авторов.

Если классов больше двух

В реальных задачах вместо собаки могут выступать тысячи альтернативных классов. Обозначим правильный класс индексом t. Его логит — z_t, вероятность — q_t, а производная потерь по логиту — g_t. Совокупный относительный вес всех конкурирующих классов теперь вычисляется суммированием:

r=\sum_{j\ne t}e^{z_j-z_t}.

Формулы остаются неизменными: q_t=1/(1+r) и g_t = -\frac{r}{1+r}. Для построения графиков удобно выразить r через запас уверенности (margin) m:

m=z_t-\log\sum_{j\ne t}e^{z_j},\qquad r=e^{-m}.

В бинарном сценарии мы получаем прежнюю разницу двух логитов. При наличии множества классов отрыв рассчитывается относительно всей массы конкурентов одновременно, а не только по отношению к ближайшему сопернику. Чем выше m, тем меньше показатель r и тем ближе система подходит к проблемной зоне.

Где пролегает критическая точка срыва

Тип данных float32 выделяет 23 явно хранимых бита под дробную часть плюс один неявный ведущий бит для нормализованных чисел. Шаг дискретизации между единицей и следующим представимым числом составляет 2^{-23}. При округлении до ближайшего прибавки, меньшие чем 2^{-24}, бесповоротно теряются.

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

m_{\text{cliff}}\approx(b+1)\ln 2,

где b — количество явно выделенных битов мантиссы.

Формат вычислений

Битов в мантиссе

Порог срыва

bfloat16

7

≈ 5.54

float16

10

≈ 7.62

float32

23

≈ 16.63

float64

52

≈ 36.74

Здесь указаны пороги арифметики конкретной операции. Использование весов в формате bfloat16 вовсе не гарантирует, что функции Softmax и Loss обсчитываются в нём же: механизмы смешанной точности часто выполняют чувствительные расчеты в float32. Кроме того, данная таблица не отменяет необходимости профилирования библиотечного ядра.

Как потеря градиента выглядит графически

Сопоставим традиционную формулу 1/(1+r)-1 с уже изученной устойчивой дробью -r/(1+r). По оси абсцисс отложим рост уверенности модели, а по оси ординат — модуль градиента для правильного класса.

Рис. 2. Зависимость модуля градиента верного класса от маржи m. В классической схеме float32 градиент обнуляется приблизительно при m = 16.6. Устойчивый вариант продолжает плавно уменьшаться синхронно с эталоном. Машинный ноль вынесен отдельно: в логарифмическом масштабе он не имеет конечной координаты.

Проверим ряд контрольных точек по графику:

import numpy as np

m = np.array([10, 15, 16, 17, 20, 30], dtype=np.float32)
one = np.float32(1.0)
r = np.exp(-m)

usual = one / (one + r) - one
fixed = -r / (one + r)

for margin, a, b in zip(m, usual, fixed):
    print(f"m={margin:2.0f}: usual={a: .6e}, fixed={b: .6e}")
m=10: usual=-4.541874e-05, fixed=-4.539786e-05
m=15: usual=-3.576279e-07, fixed=-3.059022e-07
m=16: usual=-1.192093e-07, fixed=-1.125352e-07
m=17: usual= 0.000000e+00, fixed=-4.139937e-08
m=20: usual= 0.000000e+00, fixed=-2.061154e-09
m=30: usual= 0.000000e+00, fixed=-9.357623e-14

Бесконечной точности это, разумеется, не гарантирует. Если экспонента окажется слишком малой для выбранного типа данных, произойдет underflow — округление до нуля у нижней границы представимых чисел. Предлагаемый патч устраняет преждевременную потерю сигнала при сложении с единицей, однако он не способен расширить аппаратный диапазон чисел процессора или ускорителя.

Вторая аномалия: Softmax Cancellation

Теперь слегка изменим условия задачи. В процессе дистилляции знаний (knowledge distillation) мы тренируем модель-ученика имитировать предсказания замороженной модели-учителя. Предположим, учитель выдает для фотографии 70% уверенности в коте и 30% в собаке. Наша задача — заставить ученика воспроизвести именно это распределение вероятностей, а не обязательно доводить вероятность кота до абсолютной единицы.

Здесь p_k — вероятность класса у учителя, q_k — у ученика, а z_{S,k} — логит ученика. Вместо жесткой разметки мы задействуем «мягкие» метки (soft labels). Функция потерь принимает следующий вид:

\mathcal{L}=-\sum_k p_k\log q_k.

А её производная по логиту ученика выглядит так:

\frac{\partial\mathcal{L}}{\partial z_{S,k}}=q_k-p_k.

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

Тем не менее вероятности могут вычисляться по разным алгоритмам: например, для учителя — через деление экспонент, а для градиента ученика — через экспоненту от результата log_softmax. В строгой математике результаты тождественны, но из-за округлений младшие биты могут расходиться.

Для численного тестирования возьмем три класса и идентичные логиты учителя и ученика [2, 1, 0].

import numpy as np

z = np.array([2.0, 1.0, 0.0], dtype=np.float32)
shifted = z - z.max()
e = np.exp(shifted)

p = e / e.sum()                         # путь через softmax
q = np.exp(shifted - np.log(e.sum()))   # через log_softmax

noise = q - p
print(noise)
print(f"Норма: {np.linalg.norm(noise):.6e}")
[ 0.0000000e+00  1.4901161e-08 -1.4901161e-08]
Норма: 2.107342e-08

Логиты совпали тютелька в тютельку, а посчитанный градиент — нет. Это не означает, что компьютер способен сгенерировать ненулевой итог из операции x - x: здесь происходит вычитание чисел, которые уже успели микроскопически разойтись из-за применения различных вычислительных веток.

Рис. 3. Логиты учителя составляют [2, 1, 0], а ученика — [2, 1, δ]. По горизонтали отложено минимально представимое в float32 расхождение логитов; по вертикали — норма градиента. По мере сближения моделей погрешность стандартного расчета начинает доминировать в результате. Точное математическое совпадение показано изолированно. Масштаб ошибки зависит от данных, разрядности и особенностей реализации.

Предложенный патч: предварительное сравнение логитов

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

Шаг 1. Находим расхождение между учеником и учителем.

\delta_k=z_{S,k}-z_{T,k}.

Шаг 2. Вычитаем среднее расхождение, взвешенное вероятностями учителя. Полученные промежуточные величины обозначим как d_k:

d_k=\delta_k-\sum_i p_i\delta_i.

Шаг 3. Рассчитываем миниатюрную поправку к вероятности. Для удобства обозначим общий нормирующий член буквой c:

c=\operatorname{log1p}\left(\sum_i p_i\operatorname{expm1}(d_i)\right),q_k-p_k=p_k\operatorname{expm1}(d_k-c).

Функция expm1(x) вычисляет выражение e^x-1, а log1p(x) возвращает \log(1+x). Эти специализированные математические примитивы сохраняют точность при малых значениях аргумента x, тогда как прямой расчет с явным добавлением или вычитанием единицы безвозвратно теряет значащие разряды.

В строгой математике мы получаем ровно ту же разность q_k-p_k. При идеальном совпадении логитов все \delta равны нулю, следом обнуляются d и c. Финальная операция сработает как expm1(0), выдав чистый машинный ноль.

Тест исправленного алгоритма при идентичных логитах: короткий листинг на NumPy
import numpy as np

teacher = np.array([2.0, 1.0, 0.0], dtype=np.float32)
student = teacher.copy()

e = np.exp(teacher - teacher.max())
p = e / e.sum()

delta = student - teacher
d = delta - np.sum(p * delta)
c = np.log1p(np.sum(p * np.expm1(d)))
gradient = p * np.expm1(d - c)

print(gradient)  # [0. 0. 0.]

Это локальная демонстрация для близких логитов, а не готовый пайплайн обучения. При значительных расхождениях потребуются дополнительные меры стабилизации; полноценную реализацию лучше подсмотреть в первоисточнике.

Почему даже незначительная погрешность способна влиять на обучение

Может показаться: кому есть дело до градиента порядка одной стомиллионной доли? Однако величина шага оптимизации определяется не только самим градиентом, но и структурой оптимизатора.

В качестве примера возьмем алгоритм Adam, который нормирует накопленный сигнал относительно оценки его недавнего масштаба:

w_{n+1} = w_n - \eta \frac{\widehat{m}_n}{\sqrt{\widehat{v}_n}+\epsilon}

Здесь w обозначает параметры сети, \eta — скорость обучения, а \widehat m_n вместе с \widehat v_n выступают скользящими оценками первого и второго моментов градиента с поправкой на смещение. Малый параметр \epsilon страхует от деления на ноль. Индекс n обозначает номер итерации.

В условиях Softmax Collapse искаженный градиент по логитам распространяется по сети, влияя на производные по весам. Последствия зависят от конкретной архитектуры, остальных примеров в батче и текущего состояния оптимизатора. Один лишь искусственный пример на двух числах не доказывает фатального изменения динамики обучения — здесь требуются масштабные эксперименты.

Авторы связывают подобную асимметрию с эффектом Slingshot («рогатка»): после длительных периодов низких потерь в процессе обучения неожиданно возникают резкие скачки loss. В тестах исследователей замена численной схемы полностью устраняет эти всплески и меняет дальнейшую траекторию оптимизации.

На иллюстрации ниже сопоставляются два экспериментальных режима. Weight decay — это регуляризация, плавно уменьшающая веса модели; в верхнем эксперименте она задействована, а в нижнем отключена.

Рис. 4. Оригинальные графики Figure 2b,c из работы Баррона, Гнанапрагасама и Тшаннена с русскоязычными пояснениями. Слева отражен уровень потерь, справа — тестовая точность. Сверху: сложение по модулю 31, weight decay λ = 1, Adam ε = 10⁻⁸. Снизу: (a² + b²) mod 97, λ = 0, ε = 10⁻¹⁶. Расчеты выполнены в float32, сделано пять запусков с различными случайными сидами. Термин Epoch обозначает эпохи, Step — шаги оптимизации, а c₃₂ ≈ 16.6 соответствует порогу срыва. Источник: секция 4 препринта, CC BY 4.0. Перед нами исходные эмпирические данные авторов, а не наша симуляция процесса обучения.

Это гораздо более весомый результат, чем пара изолированных цифр в скрипте. Тем не менее он не доказывает, что абсолютно любой внезапный всплеск loss в любой современной языковой модели обусловлен исключительно softmax. В статье рассмотрены конкретные архитектуры и задачи; экстраполяция выводов на вашу собственную систему требует независимого тестирования.

Как переписать функцию потерь для сохранения миниатюрных величин

Для единичного примера с правильным классом t функцию кросс-энтропии можно математически выразить через всех конкурентов. Первым шагом вычисляем:

a=\operatorname{logsumexp}_{j\ne t}(z_j-z_t).

Здесь logsumexp означает «логарифм суммы экспонент», реализованный численного устойчивым методом. Следом определяем loss:

\mathcal{L} = \operatorname{softplus}(a)

Функция softplus(a) представляет собой стабильную программную реализацию \log(1+e^a). Миниатюрную прибавку больше не нужно явно прибавлять к единице стандартной операцией, рискуя потерять её до вычисления логарифма.

Ниже представлена учебная реализация для нашего частного случая: правильный класс обладает максимальным логитом. Код возвращает и потери, и явный градиент по логитам для последующей валидации. Данный фрагмент не заменяет библиотечный механизм autograd, не покрывает все возможные вариации cross-entropy и не решает задачи дистилляции.

Проверенный пример на NumPy для ситуации с уверенным верным ответом
import numpy as np

def confident_hard_ce(z, target):
    z = np.asarray(z)
    if z.ndim != 1 or z.size < 2 or z.dtype not in (np.float32, np.float64):
        raise ValueError("Требуется 1D-массив float32/float64, K >= 2")
    if not np.isfinite(z).all():
        raise ValueError("Все логиты должны быть конечными числами")
    if not isinstance(target, (int, np.integer)) or not 0 <= target < z.size:
        raise ValueError("target должен быть целым в диапазоне от 0 до K - 1")
    target = int(target)
    if z[target] != z.max():
        raise ValueError("В рамках этого примера целевой класс должен быть лидером")

    other = np.arange(z.size) != target
    d = z[other] - z[target]
    if not np.isfinite(d).all():
        raise ValueError("Разности вышли за пределы допустимого диапазона формата")

    log_r = np.logaddexp.reduce(d)       # log(sum(exp(d)))
    zero = np.zeros((), dtype=z.dtype)
    loss = np.logaddexp(zero, log_r)    # softplus(log_r)

    grad = np.zeros_like(z)
    grad[other] = np.exp(d - loss)
    grad[target] = -grad[other].sum()  # избегаем вычисления q_t - 1
    return loss, grad

z = np.array([20.0, 0.0], dtype=np.float32)
loss, grad = confident_hard_ce(z, target=0)
print(loss)        # 2.0611537e-09
print(grad)        # [-2.0611537e-09  2.0611537e-09]
print(grad.sum())  # 0.0

Вызов np.logaddexp(a, b) производит устойчивое вычисление логарифма от суммы двух экспонент. Метод reduce последовательно агрегирует весь массив конкурентов. Градиенты конкурирующих классов вычисляются на основе малых экспонент, а градиент целевого класса формируется как их инвертированная сумма.

Ограничение на максимальное значение целевого логита введено намеренно. Данный фрагмент иллюстрирует конкретный частный режим работы; для обработки произвольных ответов, батчей, весовых коэффициентов классов и автоматического дифференцирования необходим полноценный фреймворк.

На странице проекта авторы опубликовали репозиторий с кодом на JAX и интерактивным демо. Для внедрения подобных патчей в продакшен я бы начинал с глубокого профилирования вычислительного конвейера и оценки его поведения в целевой экосистеме, а не с простого копирования одной дроби в рабочий кодовая база.

Неужели этого никто не замечал раньше?

Замечали. Более того, публикация DeepMind ссылается на ряд предшествующих работ.

Так, Прието с соавторами на конференции ICLR 2025 исследовали феномен Softmax Collapse и презентовали альтернативную функцию StableMax, модифицирующую само преобразование. В рассматриваемой же статье авторы сохраняют канонические функции Softmax и Cross-Entropy в неизменном виде, оптимизируя исключительно порядок выполнения арифметических операций.

Кроме того, Лю с коллегами в мае 2026 года изучали нарушение свойства нулевой суммы градиентов и его корреляцию с эффектом Slingshot. Еще раньше вопросы вычислительной устойчивости softmax и log-sum-exp систематически разбирали Бланчард и Хайэм.

Новизна свежей работы заключается в точечном исправлении двух конкретных дефектов и глубоком анализе их влияния на динамику обучения. Это далеко не первое открытие факта, что компьютер округляет действительные числа с некоторой погрешностью.

Стоит ли сломя голову бежать переписывать код в продакшене?

Я бы посоветовал начать с измерений и профилирования. Пристального внимания заслуживают сценарии дистилляции знаний с практически сливающимися распределениями, задачи с высокой уверенностью предсказаний, а также затяжное обучение моделей после полного заучивания обучающей выборки. В последнем случае нередко наблюдается эффект grokking: способность к обобщению проявляется спустя огромное число эпох после достижения 100% точности на train-датасете.

Однако ни колоссальный объем словаря LLM, ни используемый формат весов сами по себе не дают ответа на вопрос о наличии проблемы в вашем пайплайне. Требуется точное понимание распределения маржинальных отрывов и реальной разрядности чувствительных операций.

Прием Label smoothing (сглаживание меток) принудительно снижает давление в сторону бесконечной уверенности, заставляя целевой класс принимать вероятность чуть ниже единицы. Это способно уменьшить вероятность попадания в критическую зону, но само по себе не исправляет изъян в формуле. Изменение температуры тоже влияет на уверенность: при фиксированных логитах высокая температура сглаживает распределение, а низкая делает его более резким. Универсальной панацеей это не является.

Практическую проверку я бы выстроил по следующему алгоритму:

  1. Подать на вход уверенный правильный ответ — например, [20, 0] в формате float32. Зафиксировать потери, все компоненты градиента и их суммарное значение.

  2. Для сценария дистилляции подать на вход модели-ученику и замороженному учителю одинаковые логиты. Убедиться в нулевом значении градиента, а не кросс-энтропии.

  3. Протестировать поведение системы на предельно близких логитах. Сопоставить результаты с расчетами повышенной точности, проконтролировав сохранение микродобавки уже на входе.

  4. Задокументировать версии используемых библиотек, тип железа и фактический dtype операций. Отдельно протестировать нужные режимы смешанной точности (mixed precision) и компиляции графов.

  5. При внедрении патча сравнить метрики качества, стабильность и время обучения в идентичных условиях. Более точная математическая формула далеко не всегда оказывается быстрее оптимизированного библиотечного ядра.

Заключение

Лично меня в этой истории привлекает сочетание масштабности контекста и потрясающей простоты сути. Мы обсуждаем фронтирные нейросети из миллиардов параметров, а ключ к устранению серьезного математического изъяна кроется в элементарном преобразовании школьной дроби.

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

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

А доводилось ли вам сталкиваться с необъяснимыми всплесками loss или дрейфом модели-ученика при дистилляции? Особенно интересны воспроизводимые кейсы с указанием версий библиотек, железа и форматов вычислений.

Источники и воспроизводимость

Все пять примеров на NumPy протестированы в среде CPU: Python 3.12.1, NumPy 2.5.1, с явным заданием float32. Три схемы призваны наглядно разъяснить логику вычислений. Графики 1–3 созданы авторами статьи как самостоятельные иллюстрации, а на графике 4 использованы результаты оригинального исследования с надлежащим указанием авторства.

 

Источник

Похожие статьи

Поиск по играм, новостям и статьям…

Введите не менее двух символов

Введите не менее двух символов