Лучший алгоритм для предотвращения потери точности?
недавнее домашнее задание, которое я получил, просит нас взять выражения, которые могут создать потерю точности при выполнении в компьютере, и изменить их так, чтобы избежать этой потери.
к сожалению, направления для этого не очень понятно. Наблюдая за различными выполняемыми примерами, я знаю, что есть определенные методы этого: использование рядов Тейлора, использование сопряжений, если задействованы квадратные корни, или поиск общего знаменателя, когда вычитаются две дроби.
тем не менее, у меня возникли проблемы с замечанием, когда именно произойдет потеря точности. До сих пор единственное, что я знаю наверняка, это то, что когда вы вычитаете два числа, которые близки к одному и тому же, потеря точности происходит, так как цифры высокого порядка значительны, и вы теряете их от округления.
мой вопрос в том, какие другие общие ситуации я должен искать, и что считается "хорошими" методами приближается к ним?
например, вот одна проблема:
f(x) = tan(x) − sin(x) when x ~ 0
каков наилучший и худший алгоритм для оценки этого из этих трех вариантов:
(a) (1/ cos(x) − 1) sin(x),
(b) (x^3)/2
(c) tan(x)*(sin(x)^2)/(cos(x) + 1).
Я понимаю, что когда x близок к нулю, tan(x) и sin(x) почти одинаковы. Я не понимаю, как и почему любой из этих алгоритмов лучше или хуже подходит для решения проблемы.
4 ответов
обычно используется еще одно эмпирическое правило: при добавлении длинного ряда чисел начинайте добавлять от чисел, близких к нулю, и заканчивайте самыми большими числами.
объяснить, почему это хорошо, abit сложно. когда вы добавляете небольшие числа к большим числам, есть вероятность, что они будут полностью отброшены, потому что они меньше, чем самая низкая цифра в текущей мантиссе большого числа. возьмем к примеру такую ситуацию:
a = 1,000,000;
do 100,000,000 time:
a += 0.01;
если это 0.01 меньше, чем самая низкая цифра мантиссы, тогда цикл ничего не делает, и конечный результат a == 1,000,000 но если вы сделаете это так:
a = 0;
do 100,000,000 time:
a += 0.01;
a += 1,000,000;
чем низкое число медленно растет, и вы, скорее всего, получите что-то близкое к a == 2,000,000, что является правильным ответом.
Это, конечно, экстремальный пример, но я надеюсь, что вы поймете эту идею.
IEEE 754 является стандартом с плавающей запятой, обычно реализуемым современными процессорами. Полезно понять основы этого, так как это дает вам много интуиции о том, что не делать. Упрощенное объяснение этого заключается в том, что компьютеры хранят числа с плавающей запятой в чем-то вроде научной нотации base-2 с фиксированным числом цифр (бит) для экспоненты и для мантисса. Это означает, что чем больше абсолютное значение числа, тем менее точно он может быть представлен. Для 32-битных поплавков в IEEE 754 половина возможных битовых шаблонов представляет от -1 до 1, хотя числа до 10^38 представимы с 32-битным поплавком. Для значений больше 2^24 (приблизительно 16.7 миллионов) 32-битный float не может точно представлять все целые числа.
что это означает для вас, что вы вообще хотите избежать следующий:
- имеющие промежуточные значения большие, когда окончательный ответ ожидается.
-
добавление / вычитание малых чисел в / из больших чисел. Например, если вы написали что-то вроде:
for (float index = 17000000; index
этот цикл никогда не завершится, потому что 17,000,000 + 1 округляется до 17,000,000. Если у вас что-то например:
float foo = 10000000 - 10000000.0001
значение foo будет 0, а не -0.0001 из-за ошибки округления.
мой вопрос в том, что некоторые другие общие ситуации, которые я должен искать и что считается 'хорошо' методы сближения с ними?
есть несколько способов, которыми вы можете иметь серьезную или даже катастрофическую потерю точности.
наиболее важной причиной является то, что числа с плавающей запятой имеют ограниченное количество цифр, например.дублеры имеют 53 бита. Это означает, что если у вас есть "ненужные" цифры, которые не являются частью решения, но должны быть сохраненный, вы теряете точность.
например (мы используем десятичные типы для демонстрации):
2.598765000000000000000000000100 -
2.598765000000000000000000000099
интересной частью является ответ 100-99 = 1. Как 2.598765 равна в обоих случаях не изменяет результат, но тратит 8 цифр. Гораздо хуже, потому что компьютер не делает знайте, что цифры бесполезны, он вынужден хранить его и запихивает 21 нуль после него, трата на все 29 цифр. К сожалению, нет способа обойти его для различий, но есть и другие случаи, например exp (x)-1, который является функцией, очень часто встречающейся в физике.
функция exp около 0 почти линейна, но она обеспечивает 1 в качестве ведущей цифры. Так с 12 значимых цифр exp(0.001)-1 = 1.00100050017 - 1 = 1.00050017 e-3
Если мы используем вместо функции expm1 (), используйте ряд Тейлора:
1 + x +x^2/2 +x^3/6 ... -1 =
x +x^2/2 +x^3/6 =: expm1 (x)
expm1 (0.001) = 1.00500166667 e-3
намного лучше.
вторая проблема-это функции с очень крутым наклоном, такие как касательная x вблизи pi/2. tan (11) имеет наклон 50000, что означает, что любое небольшое отклонение, вызванное ошибками округления раньше будет усилено фактором 50000 ! Или у вас есть особенности, если, например, результат приближается к 0/0, это означает, что он может иметь любое значение.
в обоих случаях вы создаете заменить функция, simplying исходной функции. Бесполезно выделять различные подходы к решению, потому что без обучения вы просто не "увидите" проблему в первую очередь.
очень хорошая книга для изучения и обучения: Forman S. Acton: Real Computing made real
еще одна вещь, чтобы избежать вычитания чисел, которые почти равны, так как это также может привести к повышенной чувствительности к ошибке округления. Для значений около 0 cos(x) будет близок к 1, поэтому 1/cos (x) - 1 является одним из тех вычитаний, которых вы хотели бы избежать, если это возможно, поэтому я бы сказал, что (a) следует избегать.