Ошибка округления в среднем

У меня есть некоторые проблемы с ошибками округления в C++. Если мне нужно вычислить среднее значение двух поплавков a а также bтогда почему так лучше делать a+0.5*(b-a) чем (a+b)/2? Я не могу понять, почему должны быть какие-то различия в двух способах его вычисления.

2 ответа

Решение

Ваша формула верна в случае, если вы вычисляете среднее число многих чисел. В этом случае вы можете сделать следующее:

μn = 1 / nΣxi

но здесь при добавлении 101-го числа вам нужно будет добавить x101 к μ100, где μ100 может быть довольно большим по сравнению с x101, и поэтому вы потеряете некоторую точность. Чтобы избежать этой проблемы, вам может понравиться это:

μ101 = μ100 + 1 / n (x101 - μ100)

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

Возможно, вы захотите прочитать статью Численно устойчивые вычисления арифметических средних

Давайте посмотрим, как числа представлены в IEEE с плавающей точкой. Рассмотрим C++ float:

Интервал [1,2] идет с шага 2-23, поэтому вы можете представлять числа 1 + n * 2-23, где n принадлежит {0,..., 223}.

Интервал [2j, 2j + 1] соответствует значению [1,2], но умножается на 2j.

Чтобы увидеть, как теряется точность, вы можете запустить эту программу:

#include <iostream>
#include <iomanip>
int main() {
    float d = pow(2,-23);
    std::cout << d << std::endl;
    std::cout << std::setprecision(8) << d + 1 << std::endl;
    std::cout << std::setprecision(8) << d + 2 << std::endl; // the precision has been lost
    system("pause");
}

Выход

1.19209e-07
1.0000001
2

[Отказ от ответственности: этот ответ предполагает формат и семантику IEEE 754. В частности, мы предполагаем, что float это формат IEEE 754 binary32, в котором мы используем режим округления по умолчанию для округления до четности, и что промежуточные выражения не вычисляются с расширенной точностью - например, потому что FLT_EVAL_METHOD является 0.]

Вот одна из возможных причин предпочитать a + 0.5 * (b-a) если a а также b очень большие и имеют тот же знак, то промежуточное количество a + b в выражении 0.5 * (a + b) может переполниться, давая либо бесконечный результат, либо исключение с плавающей точкой. По сравнению, a + 0.5 * (b - a) не переполнится в этой ситуации.

Однако это небольшое преимущество следует сопоставить со следующим:

  • a + 0.5 * (b - a) требует трех операций с плавающей точкой; 0.5 * (a + b) требуется только два.
  • в случаях, когда a + b не переполняется, 0.5 * (a + b) всегда дает правильно округленный ответ: то есть дает наилучшее возможное приближение к фактическому среднему значению, учитывая ограничения представимости целевого типа. (Это не совсем очевидно, но не трудно доказать: либо a + b больше по величине, чем в два раза наименьшая нормальная, в этом случае сумма правильно округляется и умножение на 0.5 является точным, или a + b вычисляется точно, а затем умножение на 0.5 правильно округлено. В любом случае, не более одной из двух арифметических операций может привести к ошибке.) Но a + 0.5 * (b - a) не всегда будет давать правильно округленное среднее, и на самом деле может быть много миллионов язв по ошибке. Рассмотрим случай, когда a = -1.0 а также b = 1.0 + 2^-23, затем a + 0.5 * (b - a) дает 0.0, Правильное среднее значение 2^-24,
  • выражение a + 0.5 * (b - a) может также переполниться, если a а также b очень большие с противоположными знаками, а не с тем же знаком. В этой ситуации 0.5 * (a + b) не переполнится.
  • a + 0.5 * (b - a) (очень немного) менее читабельно, чем 0.5 * (a + b); читателю нужно немного подумать, чтобы понять, что он делает.

Учитывая вышесказанное, трудно поддержать общую рекомендацию a + 0.5 * (b - a) следует использовать вместо 0.5 * (a + b),

Другие вопросы по тегам