Надежное вычисление среднего значения двух чисел с плавающей точкой?


15

Позвольте x, yбудет два числа с плавающей точкой. Как правильно рассчитать их среднее значение?

Наивный способ (x+y)/2может привести к переполнению, когда xи yслишком велики. Я думаю, 0.5 * x + 0.5 * yможет быть лучше, но это включает в себя два умножения (что, возможно, неэффективно), и я не уверен, достаточно ли это хорошо. Есть ли способ лучше?

Другая идея, с которой я играл, это (y/2)(1 + x/y)если x<=y. Но опять же, я не уверен, как это проанализировать и доказать, что это удовлетворяет моим требованиям.

Более того, мне нужна гарантия, что вычисленное среднее будет >= min(x,y)и <= max(x,y). Как указано в ответе Дона Хэтча , возможно, лучший способ сформулировать этот вопрос таков: какова реализация среднего из двух чисел, которая всегда дает максимально возможный точный результат? То есть, если xи yявляются числами с плавающей точкой, как вычислить число с плавающей точкой, ближайшее к (x+y)/2? В этом случае вычисленное среднее автоматически >= min(x,y)и <= max(x,y). Смотрите ответ Дон Хэтч для деталей.

Примечание: мой приоритет - высокая точность. Эффективность расходуется. Однако, если есть много надежных и точных алгоритмов, я бы выбрал наиболее эффективные.


(+1) Интересный вопрос, на удивление нетривиальный.
Кирилл

1
В прошлом значения с плавающей запятой вычислялись и сохранялись в форме с более высокой точностью для промежуточных результатов. Если a + b (64-битное двойное число) дает 80-битный промежуточный результат, и это то, что делится на 2, вам не нужно беспокоиться о переполнении. Потеря точности менее очевидна.
JDługosz

Решение этой проблемы кажется относительно простым ( я добавил ответ ). Дело в том, что я программист, а не специалист по компьютерным наукам, так чего мне не хватает, что делает этот вопрос намного сложнее?
IQAndreas

Не беспокойтесь о стоимости умножения и деления на два; Ваш компилятор оптимизирует их для вас.
Федерико Полони

Ответы:


18

Я думаю, что Точность и Стабильность Численных Алгоритмов Хайама рассматривают, как можно проанализировать эти типы проблем. Смотрите Главу 2, особенно упражнение 2.8.

В этом ответе я хотел бы указать на то, что на самом деле не рассматривается в книге Хайама (в этом отношении она не очень широко известна). Если вы заинтересованы в проверке свойств простых численных алгоритмов, таких как эти, вы можете использовать возможности современных SMT-решателей ( Satisfiability Modulo Theories ), таких как z3 , используя пакет, такой как sbv в Haskell. Это несколько проще, чем с помощью карандаша и бумаги.

Предположим, мне дали , и я хотел бы знать, удовлетворяет ли z = ( x + y ) / 2 x z y . Следующий код на Haskell0ИксYZзнак равно(Икс+Y)/2ИксZY

import Data.SBV

test1 :: (SFloat -> SFloat -> SFloat) -> Symbolic SBool
test1 fun =
  do [x, y] <- sFloats ["x", "y"]
     constrain $ bnot (isInfiniteFP x) &&& bnot (isInfiniteFP y)
     constrain $ 0 .<= x &&& x .<= y
     let z = fun x y
     return $ x .<= z &&& z .<= y

test2 :: (SFloat -> SFloat -> SFloat) -> Symbolic SBool
test2 fun =
  do [x, y] <- sFloats ["x", "y"]
     constrain $ bnot (isInfiniteFP x) &&& bnot (isInfiniteFP y)
     constrain $ x .<= y
     let z = fun x y
     return $ x .<= z &&& z .<= y

позволит мне сделать это автоматически . Вот test1 funэто предположение , что для всех конечных поплавками х , у с 0 х у .ИксеUN(Икс,Y)YИкс,Y0ИксY

λ> prove $ test1 (\x y -> (x + y) / 2)
Falsifiable. Counter-example:
  x = 2.3089316e36 :: Float
  y = 3.379786e38 :: Float

Это переполняет. Предположим, теперь я беру другую формулу: Zзнак равноИкс/2+Y/2

λ> prove $ test1 (\x y -> x/2 + y/2)
Falsifiable. Counter-example:
  x = 2.3509886e-38 :: Float
  y = 2.3509886e-38 :: Float

Не работает (из - за постепенную потерю значимости: , которые могут быть неинтуитивными из - за все арифметическое существом с основанием 2).(Икс/2)×2Икс

Теперь попробуйте :Zзнак равноИкс+(Y-Икс)/2

λ> prove $ test1 (\x y -> x + (y-x)/2)
Q.E.D.

Работает! Q.E.D.Является доказательством того, что test1свойство выполняется для всех поплавков , как определено выше.

Как насчет того же, но ограниченного (вместо 0 x y )?ИксY0ИксY

λ> prove $ test2 (\x y -> x + (y-x)/2)
Falsifiable. Counter-example:
  x = -3.1300826e34 :: Float
  y = 3.402721e38 :: Float

Итак, если переполняется, как насчет z = х + ( у / 2 - х / 2 ) ?Y-ИксZзнак равноИкс+(Y/2-Икс/2)

λ> prove $ test2 (\x y -> x + (y/2 - x/2))
Q.E.D.

Таким образом, кажется, что среди формул, которые я здесь пробовал, кажется, работает (с доказательством тоже). Метод решения SMT кажется мне гораздо более быстрым способом ответа на подозрения относительно простых формул с плавающей точкой, чем анализ ошибок с плавающей точкой карандашом и бумагой.Икс+(Y/2-Икс/2)

Наконец, цель точности и стабильности часто расходится с целью производительности. Что касается производительности, я не очень понимаю, как вы можете сделать лучше, чем , тем более что компилятор все равно будет выполнять тяжелую работу по переводу этого в машинные инструкции для вас.(Икс+Y)/2

ИксИкс+(Y/2-Икс/2)YSFloatSDouble

-ffast-math(Икс+Y)/2

PPPS Я немного увлекся, глядя только на простые алгебраические выражения без условий. Дон Hatch «s формула строго лучше.


2
Оставайтесь на линии; Вы утверждали, что если x <= y (независимо от того, x> = 0 или нет), тогда x + (y / 2-x / 2) является хорошим способом сделать это? Мне кажется, что это не может быть правильно, поскольку дает неправильный ответ в следующем случае, когда ответ точно представим: x = -1, y = 1 + 2 ^ -52 (наименьшее представимое число больше 1), в этом случае ответ 2 ^ -53. Подтверждение в питоне: >>> x = -1.; y = 1.+2.**-52; print `2**-53`, `(x+y)/2.`, `x+(y/2.-x/2.)`
Дон Хэтч

2
x(x+y)/2yx,y(x+y)/2(x+y)/2

8

Во-первых, обратите внимание, что если у вас есть метод, который дает наиболее точный ответ во всех случаях, то он удовлетворит ваше необходимое условие. (Обратите внимание , что я говорю наиболее точный ответ , а не на наиболее точный ответ, так как там может быть двух победителей.) Доказательство: Если, наоборот, у вас есть точные по мере можно ответить , что это не удовлетворяет требуемому условию, что означает либо (в этом случае лучший ответ, противоречие), либо (в этом случае лучший ответ, противоречие).answer<min(x,y)<=max(x,y)min(x,y)min(x,y)<=max(x,y)<answermax(x,y)

Поэтому я думаю, что это означает, что ваш вопрос сводится к поиску наиболее точного возможного ответа. Предполагая всю арифметику IEEE754, я предлагаю следующее:

if max(abs(x),abs(y)) >= 1.:
    return x/2. + y/2.
else:
    return (x+y)/2.

Мой аргумент, что это дает наиболее точный ответ, является несколько утомительным анализом случая. Вот оно:

  • Дело max(abs(x),abs(y)) >= 1.:

    • В данном случае ни x, ни y не денормализованы: в этом случае вычисленный ответ x/2.+y/2.манипулирует теми же мантиссами и, следовательно, дает тот же самый ответ, (x+y)/2который дает вычисление , если бы мы предполагали расширенные экспоненты для предотвращения переполнения. Этот ответ может зависеть от режима округления, но в любом случае IEEE754 гарантирует, что он будет наилучшим из возможных ответов (из того факта, что вычисленное x+yгарантировано является наилучшим приближением к математическому x + y, и деление на 2 является точным в этом кейс).
    • Подраздел x денормализован (и так abs(y)>=1):

      answer = x/2. + y/2. = y/2. since abs(x/2.) is so tiny compared to abs(y/2.) = the exact mathematical value of y/2 = a best possible answer.

    • Подслучание y денормализовано (и так abs(x)>=1): аналогично.

  • Дело max(abs(x),abs(y)) < 1.:
    • В данном случае вычисляемое значение x+yявляется либо неденормированным, либо денормализованным и «четным»: хотя вычисленное значение x+yможет быть не точным, IEEE754 гарантирует, что это наилучшее возможное приближение к математическому x + y. В этом случае последующее деление на 2 в выражении (x+y)/2.является точным, поэтому вычисленный ответ (x+y)/2.является наилучшим из возможных приближений к математическому (x + y) / 2.
    • Подслучай вычисленный x+yявляется денормализованной и «нечетным»: В этом случае только один из х, у должен также быть денормализованной-and «нечетная», что означает , что другой из х, у является денормализованным с обратным знаком, и таким образом, вычисляемые x+yесть точно математическое x + y, и поэтому (x+y)/2.IEEE754 гарантирует, что вычисленное будет наилучшим приближением к математическому (x + y) / 2.

Я понимаю, что когда я сказал «денормализованный», я действительно имел в виду что-то другое - то есть числа, которые настолько близки друг к другу, как числа, то есть диапазон чисел, который примерно в два раза больше диапазона денормализованных чисел, т.е. первые 8 тиков или около того в диаграмме на en.wikipedia.org/wiki/Denormal_number . Дело в том, что «нечетные» из них являются единственными числами, для которых деление их на два не является точным. Мне нужно перефразировать эту часть ответа, чтобы прояснить это.
Дон Хэтч

еL(оп(Икс,Y))знак равнооп(Икс,Y)(1+δ)|δ|UИкс/2+Y/2(Икс+Y)/2всегда правильно округлены, отсутствуют переполнение / переполнение, все, что осталось, - это не показывать переполнения / переполнения, что легко.
Кирилл

@ Кирилл, я немного растерялся ... откуда ты? Кроме того, я не думаю, что это правда, что «деление на 2 является точным для неденормированных чисел» ... это то же самое, что я споткнулся, и, кажется, немного неловко пытаться сделать это правильно. Точное утверждение выглядит примерно так: «x / 2 является точным до тех пор, пока abs (x) как минимум вдвое больше наибольшего субнормального числа» ... ааа, неловко!
Дон Хэтч

3

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

Сильви Болдо, «Формальная проверка программ, вычисляющих среднее с плавающей точкой». В Международной конференции по формальным инженерным методам , с. 17-32. Springer, Cham, 2015. ( черновик онлайн )

(x+y)/2Икс/2+Y/2binary64С[2-+967,2970]С чтобы обеспечить наилучшую производительность для конкретного случая использования.

Это дает следующий примерный ISO-C99код:

double average (double x, double y) 
{
    const double C = 1; /* 0x1p-967 <= C <= 0x1p970 */
    return (C <= fabs (x)) ? (x / 2 + y / 2) : ((x + y) / 2);
}

В недавней последующей работе С. Болдо и соавторы показали, как добиться наилучших возможных результатов для десятичных форматов с плавающей запятой IEEE-754, используя операции плавного сложения с множественным сложением (FMA) и хорошо известную точность и точность. строительный блок удвоения (TwoSum):

Сильви Болдо, Флориан Фейсоль и Винсент Турнер, «Формально проверенный алгоритм вычисления правильного среднего десятичного числа с плавающей точкой». В 25-м Симпозиуме IEEE по компьютерной арифметике (ARITH 25) , июнь 2018 г., стр. 69-75. ( проект онлайн )


2

Хотя это может быть неэффективно с точки зрения производительности, существует очень простой способ (1) убедиться, что ни одно из чисел не превышает одно из них xили y(без переполнений), и (2) сохранить плавающую точку как "точную", так как возможно (и (3) , как дополнительный бонус, даже если используется вычитание, никакие значения никогда не будут сохранены как отрицательные числа.

float difference = max(x, y) - min(x, y);
return min(x, y) + (difference / 2.0);

На самом деле, если вы действительно хотите добиться точности, вам даже не нужно выполнять деление на месте; просто верните значения min(x, y)и differenceкоторые вы можете использовать, чтобы упростить логически или манипулировать позже.


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

@becko Да, вы бы делали по крайней мере дважды. Кроме того, приведенный вами пример может привести к неправильному ответу. Представьте себе среднее значение 2,4,9, это не то же самое, что среднее 3,9.
IQAndreas

Ты прав, моя рекурсия была неправильной. Я не уверен, как это исправить прямо сейчас, не теряя точности.
Бекко

Можете ли вы доказать, что это дает максимально точный результат? То есть, если xи yс плавающей запятой, ваше вычисление производит с плавающей запятой, ближайшей к (x+y)/2?
Бекко

1
Не будет ли это переполнением, когда x, y - наименьшее и наибольшее выразимое число?
Дон Хэтч

1

Преобразовать в более высокую точность, добавить туда значения и преобразовать обратно.

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

И это должно быть между ними, в худшем случае - только половина большего числа, если точность не достаточна.


Это подход грубой силы. Возможно, это работает, но я искал анализ, который не требовал более высокой промежуточной точности. Кроме того, можете ли вы оценить, сколько промежуточной более высокой точности требуется? В любом случае, не удаляйте этот ответ (+1), я просто не приму его в качестве ответа.
Бекко

1

Теоретически, x/2может быть вычислено вычитанием 1 из мантиссы.

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

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


0

Я думал так же, как @Roland Heath, но пока не могу комментировать, вот мое мнение:

x/2может быть вычислено вычитанием 1 из показателя степени (не мантисса, вычитание 1 из мантиссы вычитает 2^(value_of_exponent-length_of_mantissa)из общего значения).

Без ограничения общего случая, допустим x < y. (Если x > y, переименуйте переменные. Если x = y, (x+y) / 2это тривиально.)

  • Преобразование (x+y) / 2в x/2 + y/2, которое может быть выполнено двумя целочисленными вычитаниями (по одному из показателя степени)
    • Однако в зависимости от вашего представления нижняя граница показателя Если ваш показатель степени уже минимален до вычитания 1, этот метод потребует обработки специального случая. Минимальный показатель степени xбудет x/2меньше представимого (при условии, что мантисса представлена ​​неявным начальным 1).
    • Вместо того чтобы вычитать 1 из показателя степени x, сдвиньте xмантиссу вправо на единицу (и добавьте неявное ведение 1, если оно есть).
    • Вычтите 1 из показателя степени y, если он не минимален. Если он минимальный (у больше, чем х, из-за мантиссы), сдвиньте мантиссу вправо на единицу (добавьте неявное ведение 1, если оно есть).
    • Сдвиньте новую мантиссу xвправо в соответствии с показателем степени y.
    • Выполните целочисленное сложение мантисс, если только мантисса xне была полностью сдвинута. Если оба показателя были минимальными, ведущие будут переполнены, что нормально, потому что это переполнение должно снова стать неявным ведущим.
  • и одно дополнение с плавающей запятой.
    • Не могу придумать ни одного особого случая здесь; за исключением округления, которое также относится к сдвигу, описанному выше.
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.