Примеры неточностей с плавающей точкой
Как вы объясните неточность с плавающей запятой новым программистам и неспециалистам, которые все еще думают, что компьютеры бесконечно мудры и точны?
У вас есть любимый пример или анекдот, который кажется гораздо лучше, чем точное, но сухое объяснение?
Как этому учат на уроках информатики?
7 ответов
Есть две основные ловушки, с которыми люди сталкиваются с числами с плавающей запятой.
Проблема масштаба. У каждого числа FP есть экспонента, которая определяет общую "шкалу" числа, чтобы вы могли представлять либо очень маленькие, либо действительно большие значения, хотя количество цифр, которые вы можете посвятить этому, ограничено. Добавление двух чисел разного масштаба иногда приводит к "съедению" меньшего числа, поскольку нет способа вписать его в более крупный масштаб.
PS> $a = 1; $b = 0.0000000000000000000000001 PS> Write-Host a=$a b=$b a=1 b=1E-25 PS> $a + $b 1
В качестве аналогии для этого случая можно представить большой бассейн и чайную ложку воды. Оба имеют очень разные размеры, но по отдельности вы можете легко понять, насколько они примерно. Однако, если вы наливаете чайную ложку в бассейн, у вас все еще остается бассейн с водой.
(Если люди, изучающие это, имеют проблемы с экспоненциальной нотацией, можно также использовать значения
1
а также100000000000000000000
или так.)Тогда есть проблема двоичного и десятичного представления. Число как
0.1
не может быть представлен точно с ограниченным количеством двоичных цифр. Некоторые языки маскируют это, хотя:PS> "{0:N50}" -f 0.1 0.10000000000000000000000000000000000000000000000000
Но вы можете "усилить" ошибку представления, многократно сложив числа:
PS> $sum = 0; for ($i = 0; $i -lt 100; $i++) { $sum += 0.1 }; $sum 9,99999999999998
Я не могу придумать хорошую аналогию, чтобы правильно объяснить это, хотя. Это в основном та же проблема, почему вы можете представлять 1/3 только приблизительно в десятичной дроби, потому что для получения точного значения вам нужно бесконечно повторять 3 в конце десятичной дроби.
Точно так же двоичные дроби хороши для представления половин, четвертей, восьмых и т. Д., Но такие вещи, как десятая, будут давать бесконечно повторяющийся поток двоичных цифр.
Тогда есть другая проблема, хотя большинство людей не сталкиваются с этим, если они не делают огромное количество числовых вещей. Но тогда те уже знают о проблеме. Поскольку многие числа с плавающей запятой являются лишь приближениями к точному значению, это означает, что для данного приближения f действительного числа r может быть бесконечно много действительных чисел r 1, r 2,..., которые отображаются в точно такое же приближение, Эти числа лежат в определенном интервале. Допустим, что r min - это минимально возможное значение r, которое приводит к тому, что f и r max - максимально возможное значение r, для которого это выполняется, тогда вы получили интервал [ r min, r max ], где любое число в этом интервале может быть ваш фактический номер р.
Теперь, если вы выполняете вычисления для этого числа - сложение, вычитание, умножение и т. Д. - вы теряете точность. Каждое число является лишь приблизительным, поэтому вы фактически выполняете вычисления с интервалами. Результатом является также интервал, и ошибка аппроксимации только увеличивается, увеличивая интервал. Вы можете получить одно число из этого расчета. Но это всего лишь одно число из интервала возможных результатов, принимая во внимание точность ваших исходных операндов и потерю точности из-за вычислений.
Подобные вещи называются интервальной арифметикой, и, по крайней мере, для меня это было частью нашего курса математики в университете.
Покажите им, что система base-10 страдает точно такой же проблемой.
Попробуйте представить 1/3 как десятичное представление в базе 10. Вы не сможете сделать это точно.
Поэтому, если вы напишите "0.3333", у вас будет достаточно точное представление для многих вариантов использования.
Но если вы вернете это обратно к дроби, вы получите "3333/10000", что отличается от "1/3".
Другие дроби, такие как 1/2, могут быть легко представлены конечным десятичным представлением в base-10: "0.5"
Теперь base-2 и base-10 страдают по существу одной и той же проблемой: у обоих есть некоторые числа, которые они не могут точно представить.
Хотя у base-10 нет проблем с представлением 1/10 как "0.1" в base-2, вам нужно бесконечное представление, начинающееся с "0.000110011..".
Как это для объяснения непрофессионалу. Один из способов, которым компьютеры представляют числа, - это подсчет дискретных единиц. Это цифровые компьютеры. Для целых чисел, тех, которые не имеют дробной части, современные цифровые компьютеры считают степени двух: 1, 2, 4, 8.,,, Значение места, двоичные цифры, бла, бла, бла. Для дробных чисел цифровые компьютеры считают обратные степени двух: 1/2, 1/4, 1/8, ... Проблема в том, что многие числа не могут быть представлены суммой конечного числа этих обратных степеней. Использование большего количества значений места (больше битов) повысит точность представления этих "проблемных" чисел, но никогда не получит его точно, потому что оно имеет только ограниченное количество битов. Некоторые числа не могут быть представлены бесконечным числом битов.
Snooze...
Итак, вы хотите измерить объем воды в контейнере, и у вас есть только 3 мерных стакана: полная, полстакана и четверть стакана. После подсчета последней полной чашки, скажем, осталась одна треть чашки. Тем не менее, вы не можете измерить это, потому что оно точно не заполняет любую комбинацию доступных чашек. Он не заполняет половину стакана, а перелив четверти стакана слишком мал, чтобы что-либо заполнить. Итак, у вас есть ошибка - разница между 1/3 и 1/4. Эта ошибка усугубляется, когда вы комбинируете ее с ошибками других измерений.
Еще один пример на С
printf (" %.20f \n", 3.6);
невероятно дает
+3,60000000000000008882
В питоне:
>>> 1.0 / 10
0.10000000000000001
Объясните, как некоторые дроби не могут быть представлены точно в двоичном виде. Так же, как некоторые дроби (например, 1/3) не могут быть точно представлены в базе 10.
Вот мое простое понимание.
Проблема: значение 0,45 не может быть точно представлено с плавающей точкой и округлено до 0,450000018. Это почему?
Ответ: значение int 45 представлено двоичным значением 101101. Чтобы сделать значение 0,45, было бы точно, если бы вы могли взять 45 x 10^-2 (= 45 / 10^2.) Но это невозможно, потому что Вы должны использовать базу 2 вместо 10.
Таким образом, ближайший к 10^2 = 100 будет 128 = 2^7. Общее количество битов, которое вам нужно, составляет 9: 6 для значения 45 (101101) + 3 бита для значения 7 (111). Тогда значение 45 х 2^-7 = 0,3515625. Теперь у вас серьезная проблема с неточностью. 0.3515625 не близко к 0,45.
Как мы можем исправить эту неточность? Ну, мы могли бы изменить значения 45 и 7 на что-то другое.
Как насчет 460 х 2^-10 = 0,44921875. Теперь вы используете 9 бит для 460 и 4 бита для 10. Тогда это немного ближе, но все же не так близко. Однако, если ваше первоначальное желаемое значение было 0,44921875, вы получите точное совпадение без аппроксимации.
Таким образом, формула для вашего значения будет X = A x 2^B. Где A и B - целочисленные значения, положительные или отрицательные. Очевидно, что чем выше могут быть числа, тем выше будет ваша точность, однако, поскольку вы знаете, что число битов для представления значений A и B ограничено. Для числа с плавающей точкой у вас есть общее число 32. Double имеет 64, а Decimal имеет 128.
Симпатичный кусок числовой странности может наблюдаться, если преобразовать 9999999.4999999999 в float
и обратно к double
, Результат сообщается как 10000000, даже если это значение, очевидно, ближе к 9999999, и хотя 9999999.499999999 правильно округляет до 9999999.