Математика с плавающей точкой нарушена? - PullRequest
2646 голосов
/ 26 февраля 2009

Рассмотрим следующий код:

0.1 + 0.2 == 0.3  ->  false
0.1 + 0.2         ->  0.30000000000000004

Почему возникают эти неточности?

Ответы [ 30 ]

8 голосов
/ 29 декабря 2016

Ради интереса я поиграл с представлением чисел с плавающей точкой, следуя определениям из Стандарта C99, и написал код ниже.

Код печатает двоичное представление чисел с плавающей точкой в ​​3 отдельных группах

SIGN EXPONENT FRACTION

и после этого он печатает сумму, которая при суммировании с достаточной точностью покажет значение, которое действительно существует в аппаратном обеспечении.

Таким образом, когда вы пишете float x = 999..., компилятор преобразует это число в битовое представление, напечатанное функцией xx, так что сумма, напечатанная функцией yy, будет равна данному числу.

В действительности эта сумма является лишь приблизительной. Для числа 999 999 999 компилятор вставит в битовое представление числа с плавающей точкой число 1 000 000 000

После кода я присоединяю консольный сеанс, в котором я вычисляю сумму терминов для обеих констант (минус PI и 999999999), которые действительно существуют в аппаратном обеспечении, вставленные туда компилятором.

#include <stdio.h>
#include <limits.h>

void
xx(float *x)
{
    unsigned char i = sizeof(*x)*CHAR_BIT-1;
    do {
        switch (i) {
        case 31:
             printf("sign:");
             break;
        case 30:
             printf("exponent:");
             break;
        case 23:
             printf("fraction:");
             break;

        }
        char b=(*(unsigned long long*)x&((unsigned long long)1<<i))!=0;
        printf("%d ", b);
    } while (i--);
    printf("\n");
}

void
yy(float a)
{
    int sign=!(*(unsigned long long*)&a&((unsigned long long)1<<31));
    int fraction = ((1<<23)-1)&(*(int*)&a);
    int exponent = (255&((*(int*)&a)>>23))-127;

    printf(sign?"positive" " ( 1+":"negative" " ( 1+");
    unsigned int i = 1<<22;
    unsigned int j = 1;
    do {
        char b=(fraction&i)!=0;
        b&&(printf("1/(%d) %c", 1<<j, (fraction&(i-1))?'+':')' ), 0);
    } while (j++, i>>=1);

    printf("*2^%d", exponent);
    printf("\n");
}

void
main()
{
    float x=-3.14;
    float y=999999999;
    printf("%lu\n", sizeof(x));
    xx(&x);
    xx(&y);
    yy(x);
    yy(y);
}

Вот сеанс консоли, в котором я вычисляю реальное значение числа с плавающей запятой, которое существует в аппаратных средствах. Я использовал bc, чтобы напечатать сумму терминов, выведенных основной программой. Эту сумму можно вставить в python repl или что-то подобное.

-- .../terra1/stub
@ qemacs f.c
-- .../terra1/stub
@ gcc f.c
-- .../terra1/stub
@ ./a.out
sign:1 exponent:1 0 0 0 0 0 0 fraction:0 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 1 0 0 0 0 1 1
sign:0 exponent:1 0 0 1 1 1 0 fraction:0 1 1 0 1 1 1 0 0 1 1 0 1 0 1 1 0 0 1 0 1 0 0 0
negative ( 1+1/(2) +1/(16) +1/(256) +1/(512) +1/(1024) +1/(2048) +1/(8192) +1/(32768) +1/(65536) +1/(131072) +1/(4194304) +1/(8388608) )*2^1
positive ( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
-- .../terra1/stub
@ bc
scale=15
( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
999999999.999999446351872

Вот и все. Значение 999999999 на самом деле

999999999.999999446351872

Вы также можете проверить с помощью bc, что -3.14 также возмущен. Не забудьте установить коэффициент scale в bc.

Отображаемая сумма - это то, что находится внутри оборудования. Значение, которое вы получите путем вычисления, зависит от установленного вами масштаба. Я установил коэффициент scale равным 15. Математически с бесконечной точностью кажется, что это 1 000 000 000.

5 голосов
/ 20 декабря 2017

Другой способ взглянуть на это: используются 64 бита для представления чисел. Как следствие, не может быть более 2 ** 64 = 18,446,744,073,709,551,616 различных чисел.

Однако Math говорит, что между 0 и 1 уже существует бесконечно много десятичных знаков. IEE 754 определяет кодировку для эффективного использования этих 64 битов для гораздо большего числа номеров плюс NaN и +/- Infinity, поэтому между точно представленными пробелами есть промежутки. числа, заполненные только приблизительными числами.

К сожалению, 0,3 сидит в промежутке.

4 голосов
/ 22 декабря 2017

Поскольку этот поток немного расширился до общего обсуждения текущих реализаций с плавающей запятой, я бы добавил, что существуют проекты по устранению их проблем.

Взгляните, например, на https://posithub.org/, который демонстрирует тип числа, называемый posit (и его предшественник unum), который обещает предложить лучшую точность с меньшим количеством битов. Если мое понимание верно, это также устраняет проблемы в этом вопросе. Довольно интересный проект, человек за ним математик это Доктор. Джон Густафсон . Все это с открытым исходным кодом, со многими актуальными реализациями на C / C ++, Python, Julia и C # (https://hastlayer.com/arithmetics).

3 голосов
/ 20 декабря 2018

Представьте, что вы работаете в базовой десятке, скажем, с 8 цифрами точности. Вы проверяете, есть ли

1/3 + 2 / 3 == 1

и узнайте, что это возвращает false. Зачем? Ну, а реальные числа у нас есть

1/3 = 0,333 .... и 2/3 = 0,666 ....

Обрезая до восьми десятичных знаков, мы получаем

0.33333333 + 0.66666666 = 0.99999999

, что, конечно, отличается от 1.00000000 ровно 0.00000001.


Ситуация для двоичных чисел с фиксированным числом битов в точности аналогична. В качестве действительных чисел имеем

1/10 = 0,0001100110011001100 ... (база 2)

и

1/5 = 0,0011001100110011001 ... (база 2)

Если бы мы урезали их, скажем, до семи бит, то мы получили бы

0.0001100 + 0.0011001 = 0.0100101

в то время как с другой стороны,

3/10 = 0,01001100110011 ... (база 2)

, который, усеченный до семи битов, равен 0.0100110, и они различаются ровно 0.0000001.


Точная ситуация немного более тонкая, потому что эти цифры обычно хранятся в научной записи. Так, например, вместо того, чтобы хранить 1/10 как 0.0001100, мы можем сохранить его как что-то вроде 1.10011 * 2^-4, в зависимости от того, сколько битов мы выделили для показателя степени и мантиссы. Это влияет на то, сколько цифр точности вы получите для своих расчетов.

В результате из-за этих ошибок округления вы по существу никогда не захотите использовать == для чисел с плавающей запятой. Вместо этого вы можете проверить, меньше ли абсолютное значение их разности, чем некоторое фиксированное небольшое число.

3 голосов
/ 08 августа 2018

Начиная с Python 3.5 , вы можете использовать функцию math.isclose() для проверки приблизительного равенства:

>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False
2 голосов
/ 07 мая 2019

На самом деле все довольно просто. Когда у вас есть система 10-й базы (как наша), она может выражать только дроби, которые используют основной множитель базы. Первичные множители 10 равны 2 и 5. Таким образом, 1/2, 1/4, 1/5, 1/8 и 1/10 могут быть выражены чисто, потому что все знаменатели используют простые множители 10. Напротив, 1 / 3, 1/6 и 1/7 - все повторяющиеся десятичные дроби, потому что их знаменатели используют простой множитель 3 или 7. В двоичном (или базовом 2) единственном простом множителе является 2. Таким образом, вы можете выражать только те дроби, которые содержат только 2 как главный фактор. В двоичном коде 1/2, 1/4, 1/8 все будет выражено чисто в десятичном виде. В то время как 1/5 или 1/10 будут повторять десятичные дроби. Таким образом, 0,1 и 0,2 (1/10 и 1/5), в то время как чистые десятичные дроби в системе Base 10, являются повторяющимися десятичными знаками в системе Base 2, в которой работает компьютер. Когда вы выполняете математику с этими повторяющимися десятичными знаками, вы получаете остатки которые переносятся, когда вы преобразуете двоичное (двоичное) число компьютера в более удобочитаемое число 10.

С https://0.30000000000000004.com/

2 голосов
/ 31 мая 2018

Другой вопрос был назван дубликатом к этому:

В C ++, почему результат cout << x отличается от значения, которое показывает отладчик для x?

x в вопросе является float переменной.

Один пример будет

float x = 9.9F;

Отладчик показывает 9.89999962, вывод операции cout равен 9.9.

Ответом оказывается то, что точность cout по умолчанию для float равна 6, поэтому округляется до 6 десятичных цифр.

См. здесь для справки

2 голосов
/ 20 апреля 2018

Math.sum (javascript) .... вид замены оператора

.1 + .0001 + -.1 --> 0.00010000000000000286
Math.sum(.1 , .0001, -.1) --> 0.0001

Object.defineProperties(Math, {
    sign: {
        value: function (x) {
            return x ? x < 0 ? -1 : 1 : 0;
            }
        },
    precision: {
        value: function (value, precision, type) {
            var v = parseFloat(value), 
                p = Math.max(precision, 0) || 0, 
                t = type || 'round';
            return (Math[t](v * Math.pow(10, p)) / Math.pow(10, p)).toFixed(p);
        }
    },
    scientific_to_num: {  // this is from https://gist.github.com/jiggzson
        value: function (num) {
            //if the number is in scientific notation remove it
            if (/e/i.test(num)) {
                var zero = '0',
                        parts = String(num).toLowerCase().split('e'), //split into coeff and exponent
                        e = parts.pop(), //store the exponential part
                        l = Math.abs(e), //get the number of zeros
                        sign = e / l,
                        coeff_array = parts[0].split('.');
                if (sign === -1) {
                    num = zero + '.' + new Array(l).join(zero) + coeff_array.join('');
                } else {
                    var dec = coeff_array[1];
                    if (dec)
                        l = l - dec.length;
                    num = coeff_array.join('') + new Array(l + 1).join(zero);
                }
            }
            return num;
         }
     }
    get_precision: {
        value: function (number) {
            var arr = Math.scientific_to_num((number + "")).split(".");
            return arr[1] ? arr[1].length : 0;
        }
    },
    diff:{
        value: function(A,B){
            var prec = this.max(this.get_precision(A),this.get_precision(B));
            return +this.precision(A-B,prec);
        }
    },
    sum: {
        value: function () {
            var prec = 0, sum = 0;
            for (var i = 0; i < arguments.length; i++) {
                prec = this.max(prec, this.get_precision(arguments[i]));
                sum += +arguments[i]; // force float to convert strings to number
            }
            return Math.precision(sum, prec);
        }
    }
});

идея состоит в том, чтобы использовать Math вместо операторов, чтобы избежать ошибок с плавающей запятой

Math.diff(0.2, 0.11) == 0.09 // true
0.2 - 0.11 == 0.09 // false

также обратите внимание, что Math.diff и Math.sum автоматически определяют точность для использования

Math.sum принимает любое количество аргументов

1 голос
/ 02 октября 2018

Это было задуман как ответ на этот вопрос - который был закрыт как дубликат этого вопроса, в то время как я ставил вместе этот ответ, так что теперь я не могу публиковать его там ... поэтому я буду публиковать здесь вместо этого!


Резюме вопроса:

На листе 10^-8/1000 и 10^-11 оцениваются как равно , тогда как в VBA они не соответствуют.

На листе номера по умолчанию соответствуют научным обозначениям.

Если вы измените ячейки на числовой формат ( Ctrl + 1 ), равный Number с 15 десятичными точками, вы получите:

=10^-11 returns 0.000000000010000
=10^(-8/1000) returns 0.981747943019984

Таким образом, они определенно не одинаковы ... один - примерно ноль, а другой - примерно 1.

Excel не был предназначен для работы с чрезвычайно маленькими числами - по крайней мере, не со стандартной установкой. Существуют надстройки, помогающие повысить точность чисел.


Excel был разработан в соответствии со стандартом IEEE для двоичной арифметики с плавающей точкой ( IEEE 754 ). Стандарт определяет, как числа с плавающей точкой сохраняются и вычисляются. Стандарт IEEE 754 широко используется, поскольку он позволяет хранить числа с плавающей запятой в разумных пределах и вычисления могут выполняться относительно быстро.

Преимущество представления с плавающей точкой перед фиксированной точкой состоит в том, что оно может поддерживать более широкий диапазон значений. Например, представление с фиксированной запятой, которое имеет 5 десятичных цифр с десятичной запятой, расположенной после третьей цифры, может представлять числа 123.34, 12.23, 2.45 и т. Д., Тогда как представление с плавающей запятой с точностью до 5 цифр может представляют собой 1,2345, 12345, 0,00012345 и т. д. Аналогично, представление с плавающей точкой также позволяет выполнять вычисления в широком диапазоне величин при сохранении точности. Например,

img


Другие ссылки:

0 голосов
/ 22 апреля 2019

Десятичные дроби, такие как 0.1, 0.2 и 0.3, не представлены точно в двоично-кодированных типах с плавающей запятой. Сумма аппроксимаций для 0.1 и 0.2 отличается от аппроксимации, использованной для 0.3, отсюда и ложность 0.1 + 0.2 == 0.3, как можно более четко увидеть здесь:

#include <stdio.h>

int main() {
    printf("0.1 + 0.2 == 0.3 is %s\n", 0.1 + 0.2 == 0.3 ? "true" : "false");
    printf("0.1 is %.23f\n", 0.1);
    printf("0.2 is %.23f\n", 0.2);
    printf("0.1 + 0.2 is %.23f\n", 0.1 + 0.2);
    printf("0.3 is %.23f\n", 0.3);
    printf("0.3 - (0.1 + 0.2) is %g\n", 0.3 - (0.1 + 0.2));
    return 0;
}

Выход:

0.1 + 0.2 == 0.3 is false
0.1 is 0.10000000000000000555112
0.2 is 0.20000000000000001110223
0.1 + 0.2 is 0.30000000000000004440892
0.3 is 0.29999999999999998889777
0.3 - (0.1 + 0.2) is -5.55112e-17

Чтобы эти вычисления были оценены более надежно, вам необходимо использовать десятичное представление для значений с плавающей запятой. Стандарт C не определяет такие типы по умолчанию, но как расширение, описанное в Техническом отчете . Типы _Decimal32, _Decimal64 и _Decimal128 могут быть доступны в вашей системе (например, gcc поддерживает их на выбранных целях , но clang не поддерживает их на OS / X).

...