Численные методы
Погрешности и арифметика с плавающей точкой
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 форматы:**
| Формат | Знак | Экспонента | Мантисса | Всего бит |
|---|---|---|---|---|
| float32 | 1 | 8 | 23 | 32 |
| float64 | 1 | 11 | 52 | 64 |
Число = (-1)^sign × 2^(exp - bias) × (1 + mantissa) bias = 127 (float32) или 1023 (float64)
| Значение | Знак | Экспонента | Мантисса | Тип |
|---|---|---|---|---|
| +0 | 0 | 00...0 | 00...0 | Ноль |
| -0 | 1 | 00...0 | 00...0 | Ноль (отрицательный) |
| +∞ | 0 | 11...1 | 00...0 | Бесконечность |
| NaN | 0 | 11...1 | ≠ 0 | Not a Number |
| Нормализованное | 0/1 | 00...1 - 11...0 | любая | Обычное число |
| Денормализованное | 0/1 | 00...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)$.
| Тип | float32 | float64 |
|---|---|---|
| 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$ тиков.