Фаза 01 · урок 13
Численная устойчивость
Цель урока: Ваша модель обучается три часа, а затем функция потерь становится NaN. Вы добавляете оператор вывода. На шаге 9 000 логиты в порядке. На шаге 9 001 они равны inf . К шагу 9 002 каждый градиент становится nan , и обучение прекращается.
Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.
Содержание урока
- Цели обучения
- Проблема
- Концепция
- IEEE 754: как компьютеры хранят вещественные числа
- Почему 0.1 + 0.2 != 0.3
- Катастрофическая потеря точности
- Переполнение и антипереполнение
- Приём log-sum-exp
- Почему softmax требует приёма вычитания максимума
- NaN и Inf: обнаружение и предотвращение
- Численная проверка градиента
- Обучение со смешанной точностью
- bfloat16 и float16: почему bfloat16 выигрывает для обучения
- Ограничение градиента
- Нормализующие слои как численные стабилизаторы
- Распространённые численные ошибки ML
- Соберите это
- Шаг 1: продемонстрируйте пределы точности плавающей запятой
- Шаг 2: реализуйте наивный и устойчивый softmax
- Шаг 3: реализуйте устойчивый log-sum-exp
- Шаг 4: реализуйте устойчивую кросс-энтропию
- Шаг 5: проверка градиента
- Используйте это
- Моделирование смешанной точности
- Ограничение градиента
- Обнаружение NaN/Inf
- Выпустите это
- Упражнения
- Ключевые термины
- Дополнительное чтение
Числа с плавающей запятой — это негерметичная абстракция. Она ударит вас во время обучения, и вы не заметите, откуда это пришло.
Тип: Практика Язык: Python Предварительные требования: Фаза 1, уроки 01–04 Время: ~120 минут
Цели обучения
- Реализовать численно устойчивые softmax и log-sum-exp с помощью приёма вычитания максимума
- Выявлять переполнение, антипереполнение и катастрофическую потерю точности в вычислениях с плавающей запятой
- Сверять аналитические градиенты с численными, используя центрированные конечные разности
- Объяснить, почему для обучения bfloat16 предпочтительнее float16 и как масштабирование функции потерь предотвращает антипереполнение градиентов
Проблема
Ваша модель обучается три часа, а затем функция потерь становится NaN. Вы добавляете оператор вывода. На шаге 9 000 логиты в порядке. На шаге 9 001 они равны inf. К шагу 9 002 каждый градиент становится nan, и обучение прекращается.
Или: модель дообучается до конца, но точность на 2% хуже заявленной в статье. Вы проверяете всё. Архитектура совпадает. Гиперпараметры совпадают. Данные совпадают. Проблема в том, что в статье использовали float32, а вы — float16 без правильного масштабирования. Тридцать два бита накопленной ошибки округления незаметно съели вашу точность.
Или: вы реализуете функцию потерь кросс-энтропии с нуля. Она работает на небольших логитах. Когда логиты превышают 100, она возвращает inf. Softmax переполнился, потому что exp(100) больше, чем может представить float32. Каждый ML-фреймворк решает это двухстрочным приёмом. Вы не знали, что такой приём существует.
Численная устойчивость — не теоретическая забота. Это различие между успешным запуском обучения и запуском, который тихо завершается ошибкой. В конечном счёте каждая серьёзная ошибка ML, которую вам придётся отлаживать, сводится к числам с плавающей запятой.
Концепция
IEEE 754: как компьютеры хранят вещественные числа
Компьютеры хранят вещественные числа как значения с плавающей запятой согласно стандарту IEEE 754. У float три части: бит знака, экспонента и мантисса (значащая часть).
Разметка Float32 (всего 32 бита):
[1 знак] [8 экспонента] [23 мантисса]
Значение = (-1)^sign * 2^(exponent - 127) * 1.mantissa
Мантисса определяет точность (сколько значащих цифр). Экспонента определяет диапазон (насколько большим или малым может быть число).
Формат Биты Экспонента Мантисса Десятичных цифр Диапазон (примерно)
float64 64 11 52 ~15-16 +/- 1.8e308
float32 32 8 23 ~7-8 +/- 3.4e38
float16 16 5 10 ~3-4 +/- 65,504
bfloat16 16 8 7 ~2-3 +/- 3.4e38
float32 даёт примерно 7 десятичных цифр точности. Это означает, что он различает 1.0000001 и 1.0000002, но не 1.00000001 и 1.00000002. После 7 цифр всё превращается в шум округления.
float16 даёт примерно 3 цифры. Наибольшее число, которое он может представить, — 65 504. Это тревожно мало для ML, где логиты, градиенты и активации регулярно превышают это значение.
bfloat16 — ответ Google на проблему диапазона float16. У него та же 8-битная экспонента, что и у float32 (тот же диапазон, до 3.4e38), но только 7 бит мантиссы (меньшая точность, чем у float16). Для обучения нейросетей диапазон важнее точности, поэтому bfloat16 обычно побеждает.
Почему 0.1 + 0.2 != 0.3
Число 0.1 невозможно точно представить в двоичной плавающей запятой. В системе счисления по основанию 2 это периодическая дробь:
0.1 в двоичном виде = 0.0001100110011001100110011... (повторяется бесконечно)
Float32 обрезает её до 23 бит мантиссы. Сохранённое значение приблизительно равно 0.100000001490116. Аналогично, 0.2 сохраняется приблизительно как 0.200000002980232. Их сумма равна 0.300000004470348, а не 0.3.
В Python:
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
Это важно для ML, потому что:
- Сравнения потерь, например
if loss < threshold, могут давать неверный ответ. - Накопление множества малых значений (обновления градиента за тысячи шагов) отклоняется от истинной суммы.
- Контрольные суммы и тесты воспроизводимости не проходят, если сравнивать float с
==.
Исправление: никогда не сравнивайте float с помощью ==. Используйте abs(a - b) < epsilon или math.isclose().
Катастрофическая потеря точности
Когда вы вычитаете два почти равных числа с плавающей запятой, значащие цифры сокращаются, и на месте ведущих цифр оказывается шум округления.
a = 1.0000001 (хранится как 1.00000011920929 в float32)
b = 1.0000000 (хранится как 1.00000000000000 в float32)
Истинная разность: 0.0000001
Вычисленная: 0.00000011920929
Относительная ошибка: 19.2%
Это относительная ошибка 19% от одного вычитания. В ML это происходит всякий раз, когда вы:
- Вычисляете дисперсию данных с большим средним:
E[x^2] - E[x]^2, когда E[x] велико - Вычитаете почти равные логарифмы вероятностей
- Вычисляете градиенты конечными разностями при слишком маленьком epsilon
Исправление: перестраивайте формулы, чтобы не вычитать большие, почти равные числа. Для дисперсии используйте алгоритм Уэлфорда или сначала центрируйте данные. Для логарифмов вероятностей работайте в логарифмическом пространстве на всём протяжении вычислений.
Переполнение и антипереполнение
Переполнение происходит, когда результат слишком велик, чтобы его представить. Антипереполнение происходит, когда он слишком мал (ближе к нулю, чем наименьшее представимое положительное число).
Границы Float32:
Максимум: 3.4028235e+38
Минимальное положительное (нормальное): 1.175e-38
Минимальное положительное (денормализованное): 1.401e-45
Переполнение: всё > 3.4e38 становится inf
Антипереполнение: всё < 1.4e-45 становится 0.0
Функция exp() — основной источник переполнения в ML:
exp(88.7) = 3.40e+38 (едва помещается в float32)
exp(89.0) = inf (переполнение)
exp(-87.3) = 1.18e-38 (едва выше антипереполнения)
exp(-104) = 0.0 (антипереполнение до нуля)
Функция log() создаёт проблему с другой стороны:
log(0.0) = -inf
log(-1.0) = nan
log(1e-45) = -103.3 (нормально)
log(1e-46) = -inf (вход антипереполнился до 0, затем log(0) = -inf)
В ML exp() появляется в softmax, sigmoid и вычислениях вероятностей. log() появляется в кросс-энтропии, логарифмических правдоподобиях и KL-дивергенции. Комбинация log(exp(x)) без правильных приёмов — минное поле.
Приём log-sum-exp
Вычислять log(sum(exp(x_i))) напрямую численно опасно. Если хотя бы один x_i велик, exp(x_i) переполняется. Если все x_i очень отрицательны, каждый exp(x_i) антипереполняется до нуля, а log(0) равен -inf.
Приём: вычесть максимальное значение перед возведением в экспоненту.
log(sum(exp(x_i))) = max(x) + log(sum(exp(x_i - max(x))))
Почему это работает: после вычитания max(x) наибольшая экспонента равна exp(0) = 1. Переполнение невозможно. Хотя бы один член суммы равен 1, поэтому сумма не меньше 1, а log(1) = 0. Антипереполнение до -inf также невозможно.
Доказательство:
log(sum(exp(x_i)))
= log(sum(exp(x_i - c + c))) (прибавляем и вычитаем c)
= log(sum(exp(x_i - c) * exp(c))) (exp(a+b) = exp(a)*exp(b))
= log(exp(c) * sum(exp(x_i - c))) (выносим exp(c))
= c + log(sum(exp(x_i - c))) (log(a*b) = log(a) + log(b))
Положите c = max(x), и переполнение устранено.
Этот приём встречается в ML повсюду:
- Нормализация softmax
- Вычисление функции потерь кросс-энтропии
- Суммирование логарифмов вероятностей в последовательностных моделях
- Смесь гауссиан
- Вариационный вывод
Почему softmax требует приёма вычитания максимума
Softmax преобразует логиты в вероятности:
softmax(x_i) = exp(x_i) / sum(exp(x_j))
Без приёма логиты [100, 101, 102] вызывают переполнение:
exp(100) = 2.69e43
exp(101) = 7.31e43
exp(102) = 1.99e44
sum = 2.99e44
Они переполняют float32 (максимум ~3.4e38)? Нет, 2.69e43 < 3.4e38? На самом деле:
exp(88.7) уже находится на пределе float32.
exp(100) = inf в float32.
С приёмом вычтем max(x) = 102:
exp(100 - 102) = exp(-2) = 0.135
exp(101 - 102) = exp(-1) = 0.368
exp(102 - 102) = exp(0) = 1.000
sum = 1.503
softmax = [0.090, 0.245, 0.665]
Вероятности идентичны. Вычисление безопасно. Это не оптимизация. Это требование корректности.
NaN и Inf: обнаружение и предотвращение
nan (Not a Number, «не число») и inf (бесконечность) вирусно распространяются через вычисления. Один nan в обновлении градиента делает вес nan, который делает nan каждым последующий вывод. Обучение прекращается за один шаг.
Как появляется inf:
exp()от большого положительного числа- Деление на ноль:
1.0 / 0.0 - Переполнение
float32при накоплении
Как появляется nan:
0.0 / 0.0inf - infinf * 0sqrt()от отрицательного числаlog()от отрицательного числа- Любая арифметика с уже существующим
nan
Обнаружение:
import math
math.isnan(x) # True if x is nan
math.isinf(x) # True if x is +inf or -inf
math.isfinite(x) # True if x is neither nan nor inf
Стратегии предотвращения:
- Ограничивайте входы
exp():exp(clamp(x, -80, 80)) - Добавляйте epsilon к знаменателям:
x / (y + 1e-8) - Добавляйте epsilon внутрь
log():log(x + 1e-8) - Используйте устойчивые реализации (log-sum-exp, устойчивый softmax)
- Ограничивайте градиенты, чтобы предотвратить взрыв весов
- Во время отладки проверяйте
nan/infпосле каждого прямого прохода
Численная проверка градиента
Аналитические градиенты (из обратного распространения) могут содержать ошибки. Численная проверка градиентов подтверждает их, вычисляя градиенты конечными разностями.
Формула центрированной разности:
df/dx ~= (f(x + h) - f(x - h)) / (2h)
Она имеет точность O(h^2), что намного лучше прямой разности (f(x+h) - f(x)) / h, имеющей точность только O(h).
Выбор h: если он слишком велик, приближение неверно. Если слишком мал, катастрофическая потеря точности разрушает ответ. Обычно используют h = 1e-5–1e-7.
Проверка: вычислите относительную разность между аналитическим и численным градиентами.
relative_error = |grad_analytical - grad_numerical| / max(|grad_analytical|, |grad_numerical|, 1e-8)
Практические правила:
- relative_error < 1e-7: идеально, градиент корректен
- relative_error < 1e-5: приемлемо, вероятно корректен
- relative_error > 1e-3: что-то не так
- relative_error > 1: градиент полностью неверен
Всегда проверяйте градиенты, реализуя новый слой или функцию потерь. PyTorch предоставляет для этого torch.autograd.gradcheck().
Обучение со смешанной точностью
Современные GPU имеют специализированное оборудование (Tensor Cores), выполняющее матричные умножения float16 в 2–8 раз быстрее float32. Обучение со смешанной точностью использует это:
1. Хранить основную копию весов в float32
2. Прямой проход в float16 (быстро)
3. Вычислять функцию потерь в float32 (предотвращает переполнение)
4. Обратный проход в float16 (быстро)
5. Масштабировать градиенты до float32
6. Обновлять основные веса float32
Проблема чистого обучения float16: градиенты часто очень малы (1e-8 или меньше). Float16 антипереполняет всё ниже ~6e-8 до нуля. Модель перестаёт учиться, потому что все обновления градиентов равны нулю.
Решение — масштабирование функции потерь:
1. Умножить loss на большой множитель (например, 1024)
2. Обратный проход вычисляет градиенты (loss * 1024)
3. Все градиенты в 1024 раза больше (выведены выше антипереполнения float16)
4. Разделить градиенты на 1024 до обновления весов
5. Итоговый эффект: то же обновление, но без антипереполнения
Динамическое масштабирование функции потерь автоматически подбирает коэффициент. Начните с большого значения (65536). Если градиенты переполняются до inf, уменьшите его вдвое. Если N шагов проходят без переполнения, удвойте его.
bfloat16 и float16: почему bfloat16 выигрывает для обучения
float16: [1 sign] [5 exponent] [10 mantissa]
bfloat16: [1 sign] [8 exponent] [7 mantissa]
У float16 больше точность (10 бит мантиссы против 7), но ограниченный диапазон (максимум ~65 504). У bfloat16 меньше точность, но тот же диапазон, что и у float32 (максимум ~3.4e38).
Для обучения нейросетей:
- Активации и логиты регулярно превышают 65 504 во время всплесков обучения. float16 переполняется; bfloat16 справляется.
- Для float16 требуется масштабирование функции потерь, но для bfloat16 оно обычно не нужно, поскольку его диапазон покрывает спектр величин градиентов.
- bfloat16 — простое усечение float32: отбросьте нижние 16 бит мантиссы. Преобразование тривиально и не затрагивает экспоненту.
float16 предпочтителен для инференса, где значения ограничены и точность важнее. bfloat16 предпочтителен для обучения, где важнее диапазон. Поэтому TPU и современные GPU NVIDIA (A100, H100) имеют нативную поддержку bfloat16.
Ограничение градиента
Взрывающиеся градиенты возникают, когда градиенты экспоненциально растут через множество слоёв (это распространено в RNN, глубоких сетях и трансформерах). Один большой градиент может испортить все веса за один шаг.
Два типа ограничения:
Ограничение по значению: независимо ограничивает каждый элемент градиента.
grad = clamp(grad, -max_val, max_val)
Просто, но может изменить направление вектора градиента.
Ограничение по норме: масштабирует весь вектор градиента так, чтобы его норма не превышала порог.
if ||grad|| > max_norm:
grad = grad * (max_norm / ||grad||)
Сохраняет направление градиента. Именно это делает torch.nn.utils.clip_grad_norm_(). Это стандартный выбор.
Типичные значения: max_norm=1.0 для трансформеров, max_norm=0.5 для RL, max_norm=5.0 для более простых сетей.
Ограничение градиента — не костыль. Это защитный механизм. Без него единственный выброс в батче может породить градиент, достаточно большой, чтобы разрушить недели обучения.
Нормализующие слои как численные стабилизаторы
Пакетную нормализацию, нормализацию слоя и RMS-нормализацию обычно представляют как регуляризаторы, помогающие обучению сходиться. Они также являются численными стабилизаторами.
Без нормализации активации могут экспоненциально расти или уменьшаться по слоям:
Слой 1: значения в [0, 1]
Слой 5: значения в [0, 100]
Слой 10: значения в [0, 10,000]
Слой 50: значения в [0, inf]
Нормализация центрирует и масштабирует активации в каждом слое:
LayerNorm(x) = (x - mean(x)) / (std(x) + epsilon) * gamma + beta
epsilon (обычно 1e-5) предотвращает деление на ноль, когда все активации одинаковы. Обучаемые параметры gamma и beta позволяют сети восстановить нужный ей масштаб.
Это удерживает значения в численно безопасном диапазоне по всей сети, предотвращая и переполнение на прямом проходе, и взрыв градиентов на обратном.
Распространённые численные ошибки ML
Ошибка: функция потерь становится NaN после нескольких эпох. Причина: логиты стали слишком большими, softmax переполнился. Или скорость обучения слишком высока и веса разошлись. Исправление: используйте устойчивый softmax (вычитание максимума), уменьшите скорость обучения, добавьте ограничение градиента.
Ошибка: функция потерь застряла на log(num_classes). Причина: модель выдаёт почти равномерные вероятности. Часто это означает исчезающие градиенты или то, что модель вообще не обучается. Исправление: проверьте корректность меток данных, функцию потерь и наличие «мёртвых» ReLU.
Ошибка: точность валидации ниже ожидаемой на 1–3%. Причина: смешанная точность без правильного масштабирования функции потерь. Антипереполнение градиентов молча обнуляет малые обновления. Исправление: включите динамическое масштабирование функции потерь или перейдите на bfloat16.
Ошибка: нормы градиентов некоторых слоёв равны 0.0. Причина: «мёртвые» нейроны ReLU (все входы отрицательны) или антипереполнение float16. Исправление: используйте LeakyReLU или GELU, масштабирование градиентов, проверьте инициализацию весов.
Ошибка: модель работает на одном GPU, но даёт другие результаты на другом.
Причина: недетерминированный порядок накопления чисел с плавающей запятой. Параллельные GPU-редукции суммируют в разных порядках на разном оборудовании, а сложение чисел с плавающей запятой не ассоциативно.
Исправление: принимайте небольшие различия (1e-6) или задайте torch.use_deterministic_algorithms(True) и примите потерю скорости.
Ошибка: exp() возвращает inf при вычислении функции потерь.
Причина: необработанные логиты переданы в exp() без приёма вычитания максимума.
Исправление: используйте torch.nn.functional.log_softmax(), который реализует log-sum-exp внутри.
Ошибка: обучение расходится после перехода с float32 на float16. Причина: float16 не может представить величины градиента ниже 6e-8 или активации выше 65 504. Исправление: используйте смешанную точность с масштабированием функции потерь (AMP) или bfloat16.
logsumexp-stability
Соберите это
Шаг 1: продемонстрируйте пределы точности плавающей запятой
print("=== Floating Point Precision ===")
print(f"0.1 + 0.2 = {0.1 + 0.2}")
print(f"0.1 + 0.2 == 0.3? {0.1 + 0.2 == 0.3}")
print(f"Difference: {(0.1 + 0.2) - 0.3:.2e}")
Шаг 2: реализуйте наивный и устойчивый softmax
import math
def softmax_naive(logits):
exps = [math.exp(z) for z in logits]
total = sum(exps)
return [e / total for e in exps]
def softmax_stable(logits):
max_logit = max(logits)
exps = [math.exp(z - max_logit) for z in logits]
total = sum(exps)
return [e / total for e in exps]
safe_logits = [2.0, 1.0, 0.1]
print(f"Naive: {softmax_naive(safe_logits)}")
print(f"Stable: {softmax_stable(safe_logits)}")
dangerous_logits = [100.0, 101.0, 102.0]
print(f"Stable: {softmax_stable(dangerous_logits)}")
# softmax_naive(dangerous_logits) would return [nan, nan, nan]
Шаг 3: реализуйте устойчивый log-sum-exp
def logsumexp_naive(values):
return math.log(sum(math.exp(v) for v in values))
def logsumexp_stable(values):
c = max(values)
return c + math.log(sum(math.exp(v - c) for v in values))
safe = [1.0, 2.0, 3.0]
print(f"Naive: {logsumexp_naive(safe):.6f}")
print(f"Stable: {logsumexp_stable(safe):.6f}")
large = [500.0, 501.0, 502.0]
print(f"Stable: {logsumexp_stable(large):.6f}")
# logsumexp_naive(large) returns inf
Шаг 4: реализуйте устойчивую кросс-энтропию
def cross_entropy_naive(true_class, logits):
probs = softmax_naive(logits)
return -math.log(probs[true_class])
def cross_entropy_stable(true_class, logits):
max_logit = max(logits)
shifted = [z - max_logit for z in logits]
log_sum_exp = math.log(sum(math.exp(s) for s in shifted))
log_prob = shifted[true_class] - log_sum_exp
return -log_prob
logits = [2.0, 5.0, 1.0]
true_class = 1
print(f"Naive: {cross_entropy_naive(true_class, logits):.6f}")
print(f"Stable: {cross_entropy_stable(true_class, logits):.6f}")
Шаг 5: проверка градиента
def numerical_gradient(f, x, h=1e-5):
grad = []
for i in range(len(x)):
x_plus = x[:]
x_minus = x[:]
x_plus[i] += h
x_minus[i] -= h
grad.append((f(x_plus) - f(x_minus)) / (2 * h))
return grad
def check_gradient(analytical, numerical, tolerance=1e-5):
for i, (a, n) in enumerate(zip(analytical, numerical)):
denom = max(abs(a), abs(n), 1e-8)
rel_error = abs(a - n) / denom
status = "OK" if rel_error < tolerance else "FAIL"
print(f" param {i}: analytical={a:.8f} numerical={n:.8f} "
f"rel_error={rel_error:.2e} [{status}]")
def f(params):
x, y = params
return x**2 + 3*x*y + y**3
def f_grad(params):
x, y = params
return [2*x + 3*y, 3*x + 3*y**2]
point = [2.0, 1.0]
analytical = f_grad(point)
numerical = numerical_gradient(f, point)
check_gradient(analytical, numerical)
Используйте это
Моделирование смешанной точности
import struct
def float32_to_float16_round(x):
packed = struct.pack('f', x)
f32 = struct.unpack('f', packed)[0]
packed16 = struct.pack('e', f32)
return struct.unpack('e', packed16)[0]
def simulate_bfloat16(x):
packed = struct.pack('f', x)
as_int = int.from_bytes(packed, 'little')
truncated = as_int & 0xFFFF0000
repacked = truncated.to_bytes(4, 'little')
return struct.unpack('f', repacked)[0]
Ограничение градиента
def clip_by_norm(gradients, max_norm):
total_norm = math.sqrt(sum(g**2 for g in gradients))
if total_norm > max_norm:
scale = max_norm / total_norm
return [g * scale for g in gradients]
return gradients
grads = [10.0, 20.0, 30.0]
clipped = clip_by_norm(grads, max_norm=5.0)
print(f"Original norm: {math.sqrt(sum(g**2 for g in grads)):.2f}")
print(f"Clipped norm: {math.sqrt(sum(g**2 for g in clipped)):.2f}")
print(f"Direction preserved: {[c/clipped[0] for c in clipped]} == {[g/grads[0] for g in grads]}")
Обнаружение NaN/Inf
def check_tensor(name, values):
has_nan = any(math.isnan(v) for v in values)
has_inf = any(math.isinf(v) for v in values)
if has_nan or has_inf:
print(f"WARNING {name}: nan={has_nan} inf={has_inf}")
return False
return True
check_tensor("good", [1.0, 2.0, 3.0])
check_tensor("bad", [1.0, float('nan'), 3.0])
check_tensor("ugly", [1.0, float('inf'), 3.0])
См. code/numerical.py — там находятся полные реализации со всеми продемонстрированными граничными случаями.
Выпустите это
Этот урок создаёт:
code/numerical.pyс устойчивыми softmax, log-sum-exp, кросс-энтропией, проверкой градиентов и моделированием смешанной точностиoutputs/prompt-numerical-debugger.mdдля диагностики NaN/Inf и численных проблем во время обучения
Эти устойчивые реализации снова появятся в фазе 3 при построении цикла обучения и в фазе 4 при реализации механизмов внимания.
Упражнения
-
Катастрофическая потеря точности. Вычислите дисперсию [1000000.0, 1000001.0, 1000002.0] с помощью наивной формулы
E[x^2] - E[x]^2в float32. Затем вычислите её с помощью онлайн-алгоритма Уэлфорда. Сравните ошибки с истинной дисперсией (0.6667). -
Поиск точности. Найдите наименьшее положительное значение float32
x, при котором1.0 + x == 1.0в Python. Это машинный эпсилон. Убедитесь, что он совпадает сnumpy.finfo(numpy.float32).eps. -
Граничные случаи log-sum-exp. Проверьте вашу функцию
logsumexp_stableна: (a) всех равных значениях, (b) одном значении, намного большем остальных, (c) всех очень отрицательных значениях (-1000). Убедитесь, что она выдаёт верные результаты там, где наивная версия не справляется. -
Проверка градиента слоя нейросети. Реализуйте один линейный слой
y = Wx + bи его аналитический обратный проход. Используйтеnumerical_gradient, чтобы проверить корректность для матрицы весов 3x2. -
Эксперимент с масштабированием функции потерь. Смоделируйте обучение с float16: создайте случайные градиенты в диапазоне [1e-9, 1e-3], преобразуйте в float16 и измерьте, какая доля становится нулевой. Затем примените масштабирование функции потерь (умножьте на 1024), преобразуйте в float16, масштабируйте обратно и снова измерьте долю нулей.
Ключевые термины
| Термин | Что обычно говорят | Что это на самом деле означает |
|---|---|---|
| IEEE 754 | «Стандарт float» | Международный стандарт, определяющий двоичные форматы чисел с плавающей запятой, правила округления и специальные значения (inf, nan). Его реализуют все современные CPU и GPU. |
| Машинный эпсилон | «Предел точности» | Наименьшее значение e, такое что 1.0 + e != 1.0 в заданном формате float. Для float32 это примерно 1.19e-7. |
| Катастрофическая потеря точности | «Потеря точности при вычитании» | При вычитании почти равных чисел с плавающей запятой значащие цифры сокращаются, и в результате доминирует шум округления. |
| Переполнение | «Слишком большое число» | Результат превышает максимальное представимое значение и становится inf. exp(89) переполняет float32. |
| Антипереполнение | «Слишком маленькое число» | Результат ближе к нулю, чем наименьшее представимое положительное значение, и становится 0.0. exp(-104) антипереполняет float32. |
| Приём log-sum-exp | «Сначала вычтите максимум» | Вычисление log(sum(exp(x))) через вынесение exp(max(x)), предотвращающее переполнение и антипереполнение. Используется в softmax, кросс-энтропии и математике логарифмов вероятностей. |
| Устойчивый softmax | «Softmax, который не взрывается» | Вычитание max(logits) перед возведением в экспоненту. Численно идентичный результат без возможности переполнения. |
| Проверка градиента | «Проверьте свой backprop» | Сравнение аналитических градиентов из обратного распространения с численными градиентами конечных разностей для поиска ошибок реализации. |
| Смешанная точность | «Прямой проход float16, обратный float32» | Использование float меньшей точности для критичных к скорости операций и большей точности для численно чувствительных операций. Типичное ускорение — 2–3x. |
| Масштабирование функции потерь | «Предотвратите антипереполнение градиента» | Умножение функции потерь на большую константу перед обратным распространением, чтобы градиенты оставались в представимом диапазоне float16, и деление на ту же константу перед обновлением весов. |
| bfloat16 | «Мозговая плавающая точка» | 16-битный формат Google с 8 битами экспоненты (тот же диапазон, что у float32) и 7 битами мантиссы (меньшая точность, чем у float16). Предпочтителен для обучения. |
| Ограничение градиента | «Ограничьте норму градиента» | Масштабирование вектора градиента так, чтобы его норма не превышала порог. Предотвращает порчу весов взрывающимися градиентами. |
| NaN | «Не число» | Специальное значение float из неопределённых операций (0/0, inf-inf, sqrt(-1)). Распространяется через всю последующую арифметику. |
| Inf | «Бесконечность» | Специальное значение float из-за переполнения или деления на ноль. Может сочетаться с другими значениями и создавать NaN (inf - inf, inf * 0). |
| Численный градиент | «Грубая производная» | Приближение производной вычислением f(x+h) и f(x-h) и делением на 2h. Медленно, но надёжно для проверки. |
Дополнительное чтение
- What Every Computer Scientist Should Know About Floating-Point Arithmetic (Goldberg 1991) — исчерпывающий эталонный источник: плотный, но полный
- Mixed Precision Training (Micikevicius et al., 2018) — статья NVIDIA, представившая масштабирование функции потерь для float16
- AMP: Automatic Mixed Precision (PyTorch docs) — практическое руководство по смешанной точности в PyTorch
- bfloat16 format (Google Cloud TPU docs) — почему Google выбрала этот формат для TPU
- Kahan Summation (Wikipedia) — алгоритм уменьшения ошибки округления при суммировании чисел с плавающей запятой
Источник
Оригинал статьи (зафиксированная ревизия)
Навигация
← Назад: 01.12 — Операции с тензорами ↑ Фаза 1 — Математические основы · Полный каталог Вперёд: 01.14 — Нормы и расстояния