Численные методы

Погрешности и арифметика с плавающей точкой

25 февраля 1991 года. Дахран, Саудовская Аравия. Ракета Patriot не перехватила Scud. Погибли 28 американских военных. Причина: число $0.1$ не представимо точно в двоичном float. За 100 часов работы системы ошибка округления накопилась до 0.34 секунды смещения по времени - это 500 метров промаха в пространстве. Patriot missile bug стал учебным примером того, почему численные методы - это не академия. Это жизни.

  • **Нейросети (IEEE-754)**: каждый forward pass обучается на float32. Смешанная точность (float16 + float32) экономит вдвое памяти GPU при потере <0.1% accuracy. Накопленные ошибки округления влияют на сходимость при обучении миллиардных моделей.
  • **Finite element в авиастроении**: ANSYS и Nastran решают системы миллионов уравнений. Накопление ошибок float64 за тысячи шагов - вопрос безопасности фюзеляжа, не академического интереса.
  • **GPS-навигация**: координата вычисляется интегрированием IMU-данных. Дрейф float-ошибок за час полёта - десятки метров. Именно поэтому GPS нужна внешняя коррекция.
  • **Банки**: стандарт ISO 20022 требует Decimal, не float. 0.1 + 0.2 ≠ 0.3 в float64 - при миллиарде транзакций это реальные убытки.
  • **Компиляторы и JIT**: GCC и LLVM переупорядочивают float-операции для скорости. Это легально по IEEE-754 - но может изменить результат. Поэтому `-ffast-math` по умолчанию выключен.

Floating Point

Компьютер работает с 64 битами. Вещественных чисел между 0 и 1 - несчётно бесконечно. Значит, большинство из них **невозможно представить точно**. Числа с плавающей точкой (floating point) - компромисс: широкий диапазон ($10^{-308}$ до $10^{308}$), но ограниченная точность (16 значащих цифр). Именно этот компромисс в 1991 году стоил 28 жизней.

**Floating point** - представление вещественного числа в виде: x = ±m × 2^e где m - мантисса (значащие цифры), e - экспонента (масштаб). Аналогия: научная нотация 6.022 × 10²³ - мантисса 6.022, экспонента 23. **Проблема:** только числа вида k/2ⁿ представимы точно. 0.1 = 1/10 - бесконечная двоичная дробь!

Точно представимы только дроби вида $a/2^n$: 0.5, 0.25, 0.125, 0.375. Числа 0.1, 0.2, 0.3, 1/3 - бесконечные двоичные дроби, которые обрезаются до 52 бит мантиссы. Ошибка одной операции - $\sim 10^{-16}$, почти невидимая. Но за 100 часов при тактовой частоте 10 раз в секунду - это $3.6 \times 10^6$ операций. Ошибки суммируются. Patriot получил смещение 0.34 секунды.

Patriot хранил время как целое число тиков, умноженное на $1/10$ секунды. Но $1/10 = 0.1$ - бесконечная двоичная дробь. В 24-битной арифметике системы ошибка составляла $9.5 \times 10^{-8}$ секунды на тик. За $100 \times 3600 \times 10 = 3{,}600{,}000$ тиков накопилось $0.34$ секунды. Scud летит со скоростью $\sim 1700$ м/с. $0.34 \times 1700 \approx 578$ метров промаха. Это не выдумка - это официальный отчёт GAO 1992 года.

Почему 0.1 + 0.2 ≠ 0.3 в floating point?

IEEE 754

IEEE 754 - стандарт 1985 года, используемый практически всеми процессорами от Cortex-M до A100. Число хранится в трёх полях: знак (1 бит), экспонента (масштаб), мантисса (значащие цифры). Именно этот стандарт определяет, что $0.1 + 0.2 = 0.30000000000000004$ в Python, JavaScript, C, Java, Rust - во всех языках одинаково.

**IEEE 754 форматы:**

ФорматЗнакЭкспонентаМантиссаВсего бит
float32182332
float641115264

Число = (-1)^sign × 2^(exp - bias) × (1 + mantissa) bias = 127 (float32) или 1023 (float64)

ЗначениеЗнакЭкспонентаМантиссаТип
+0000...000...0Ноль
-0100...000...0Ноль (отрицательный)
+∞011...100...0Бесконечность
NaN011...1≠ 0Not a Number
Нормализованное0/100...1 - 11...0любаяОбычное число
Денормализованное0/100...0≠ 0Очень малое (≈ 0)

Особые значения: **+∞** и **-∞** (переполнение при делении на ноль), **NaN** (0/0, $\sqrt{-1}$ - не число), **-0** (да, $-0 = +0$, но $1/(-0) = -\infty$). Денормализованные числа - числа вблизи нуля с пониженной точностью. В нейросетях NaN при обучении означает взрыв градиента: `loss = nan` - первый симптом слишком большого learning rate.

Диапазон float64: от $\approx 5 \times 10^{-324}$ до $\approx 1.8 \times 10^{308}$. Точность: ~15-17 значащих десятичных цифр. Числа $10^{15} + 1$ и $10^{15}$ неразличимы в float64 (разница меньше machine epsilon). Именно поэтому наивный счётчик времени Patriot на 24-битном float потерял точность - масштаб числа вырос, относительная точность упала.

Сколько значащих десятичных цифр обеспечивает float64?

Rounding

Каждая арифметическая операция с float порождает ошибку округления. Одна операция - $\le \varepsilon/2$ относительной ошибки. Это гарантирует IEEE 754. Проблема начинается при цепочках: миллион операций - потенциально $\sim 10^6 \cdot \varepsilon/2 \approx 10^{-10}$ накопленной ошибки. Всё зависит от того, складываются ли ошибки или компенсируют друг друга.

**Абсолютная погрешность:** |x̃ − x|, где x̃ - приближённое, x - точное значение. **Относительная погрешность:** |x̃ − x| / |x| (при x ≠ 0). **Machine epsilon (ε)** - наименьшее число, для которого fl(1 + ε) ≠ 1: - float32: ε ≈ 1.19 × 10⁻⁷ - float64: ε ≈ 2.22 × 10⁻¹⁶ Смысл: относительная ошибка любой правильно округлённой операции ≤ ε/2.

**Гарантия IEEE 754:** каждая базовая операция (+, -, ×, ÷, √) даёт **правильно округлённый** результат - как если бы вычислялось с бесконечной точностью, затем округлялось до ближайшего float. Одна операция = ошибка $\le \varepsilon/2$. Но алгоритм Кахана показывает: правильная перестановка шагов позволяет суммировать $n$ чисел с ошибкой $O(\varepsilon)$ вместо $O(n \varepsilon)$.

Типfloat32float64
Machine epsilon≈ 1.2 × 10⁻⁷≈ 2.2 × 10⁻¹⁶
Значащие десятичные цифры~7~16
Диапазон≈ 10⁻³⁸ - 10³⁸≈ 10⁻³⁰⁸ - 10³⁰⁸
Типичное использованиеGPU, ML inferenceНаучные вычисления
Размер4 байта8 байт

Чему равно выражение 1.0 + 1e-17 в float64 арифметике?

Cancellation

**Catastrophic cancellation** (катастрофическая потеря значимости) - самая опасная ловушка floating point. При вычитании двух почти равных чисел значащие цифры взаимно уничтожаются, оставляя только шум округления. Именно поэтому наивная формула корней квадратного уравнения работает для $b^2 \gg 4ac$, но теряет точность при $b^2 \approx 4ac$.

**Catastrophic cancellation:** при x ≈ y вычисление x − y теряет точность. Пример: x = 1.000000000000001, y = 1.000000000000000 Точный результат: 10⁻¹⁵ float64 результат: может содержать только 1-2 верных цифры вместо 16 **Правило:** количество потерянных цифр ≈ −log₁₀(|x−y| / |x|)

Борьба с cancellation - **алгебраическая перестановка формулы**. Вместо $(1+x) - 1$ при малом $x$ используйте $x$ напрямую. Вместо $\sqrt{x+1} - \sqrt{x}$ - умножьте на сопряжённое: $\frac{1}{\sqrt{x+1}+\sqrt{x}}$. Вместо наивной формулы корней - формулу Виета: $x_1 x_2 = c/a$. NumPy предоставляет `expm1`, `log1p`, `hypot` - специально для случаев, где наивные формулы накапливают ошибку.

Floating point - не «сломанная» арифметика. Это инженерный компромисс, обеспечивающий диапазон от $10^{-308}$ до $10^{308}$ с 16 значащими цифрами в 8 байтах. Для 99% задач этого достаточно. Но 1% - это Patriot 1991, это нестабильное обучение нейросетей, это ошибки в финансовом ПО. Знание ловушек - не академизм. Это компетенция, отделяющая инженера от пользователя калькулятора.

double precision (float64) достаточно для любых вычислений

float64 с ~16 значащими цифрами недостаточен для финансовых вычислений (нужен Decimal), длинных цепочек операций (нужна компенсация) и некоторых научных задач (нужен quad precision или arbitrary precision)

В финансах ошибка в $0.01$ цента при миллиарде транзакций = $10{,}000$ долларов потерь. Банки используют `Decimal` (фиксированная точка по основанию 10). В науке: климатические модели решают $10^9$ уравнений за $10^4$ шагов - ошибки накапливаются до уровня сигнала. Quad precision (float128) и arbitrary precision (mpmath, GMP) - для задач, где float64 недостаточно. PyTorch обучает на float32, верифицирует на float64 - это намеренный выбор.

Какой из следующих вычислений наиболее подвержен catastrophic cancellation?

Ключевые идеи

  • **Floating point:** $x = \pm m \times 2^e$; только числа вида $k/2^n$ представимы точно. $0.1$ - бесконечная двоичная дробь
  • **IEEE 754:** sign + exponent + mantissa; float64 = 52 бита мантиссы = ~16 значащих десятичных цифр
  • **Machine epsilon:** $\varepsilon \approx 2.2 \times 10^{-16}$ для float64; относительная ошибка любой правильно округлённой операции $\le \varepsilon/2$
  • **Catastrophic cancellation:** вычитание близких чисел уничтожает значащие цифры. Patriot missile bug - это буквально оно
  • **Алгоритм Кахана:** компенсированное суммирование - хранит потерянные биты в переменной. Numpy использует его внутри `np.sum`
  • **Callback**: Patriot 1991 - 28 погибших из-за накопленной ошибки 0.1 в binary float. Каждая нейросеть в мире работает на IEEE-754

Связанные темы

Понимание floating point - фундамент для всех численных методов:

  • Интерполяция — Ошибки floating point ограничивают точность интерполяционных полиномов
  • Сплайны — Сплайны более устойчивы к ошибкам округления, чем полиномы высокой степени

Вопросы для размышления

  • Нейросети обучаются на float32, а inference часто на float16 или int8. Почему переход на меньшую точность не разрушает предсказания? Что такое quantization-aware training?
  • Python decimal.Decimal решает проблему $0.1 + 0.2 \ne 0.3$, но работает медленнее float64 в 50-100 раз. Когда это оправдано? Почему банки используют именно Decimal?
  • Patriot missile ошибся на $0.34$ секунды за 100 часов из-за ошибки представления $0.1$. Посчитайте вручную: машинный такт системы был $1/10$ секунды. Какова относительная ошибка float32 для $0.1$? Накопите её за $100 \times 3600 \times 10$ тиков.

Связанные уроки

  • calc-03-limits-intro
Погрешности и арифметика с плавающей точкой

0

1

Войти