Это был вопрос, который мне задали во время моего недавнего интервью, и я хочу знать (на самом деле я не помню теорию численного анализа, поэтому, пожалуйста, помогите мне :)
Если у нас есть какая-то функция, которая накапливает числа с плавающей запятой:
std::accumulate(v.begin(), v.end(), 0.0);
v
это std::vector<float>
, например.
Не лучше ли отсортировать эти числа перед накоплением?
Какой приказ даст наиболее точный ответ?
Я подозреваю , что сортировка чисел в порядке возрастания будет на самом деле сделать числовую ошибку меньше , но , к сожалению , я не могу доказать это сам.
PS Я понимаю, что это, вероятно, не имеет ничего общего с программированием в реальном мире, просто из любопытства.
c++
floating-point
precision
Йиппи-Ки-Яй
источник
источник
Ответы:
Ваш инстинкт в основном прав, сортировка по возрастанию (по величине) обычно несколько улучшает ситуацию. Рассмотрим случай, когда мы добавляем числа с плавающей запятой одинарной точности (32 бита), и есть 1 миллиард значений, равных 1 / (1 миллиард), и одно значение, равное 1. Если 1 идет первой, тогда будет сумма. к 1, поскольку 1 + (1/1 миллиарда) равно 1 из-за потери точности. Каждое добавление не влияет на общую сумму.
Если сначала идут маленькие значения, они, по крайней мере, будут суммировать что-то, хотя даже тогда у меня их 2 ^ 30, тогда как после 2 ^ 25 или около того я возвращаюсь в ситуацию, когда каждое в отдельности не влияет на общую больше нет. Так что мне еще понадобятся трюки.
Это крайний случай, но в целом сложение двух значений одинаковой величины более точное, чем добавление двух значений очень разных величин, поскольку таким образом вы «отбрасываете» меньшее количество бит точности при меньшем значении. Сортируя числа, вы группируете значения одинаковой величины вместе и добавляя их в порядке возрастания, вы даете маленьким значениям «шанс» кумулятивного достижения величины больших чисел.
Тем не менее, если речь идет об отрицательных числах, этот подход легко «перехитрить». Рассмотрим три значения для суммирования
{1, -1, 1 billionth}
. Арифметически правильная сумма равна1 billionth
, но если мое первое добавление включает крошечное значение, тогда моя окончательная сумма будет равна 0. Из 6 возможных заказов только 2 являются «правильными» -{1, -1, 1 billionth}
и{-1, 1, 1 billionth}
. Все 6 порядков дают результаты, которые точны в масштабе самого большого значения на входе (0,0000001% выхода), но для 4 из них результат неточен в масштабе истинного решения (выход 100%). Конкретная проблема, которую вы решаете, скажет вам, достаточно ли первое или нет.Фактически, вы можете сыграть гораздо больше трюков, чем просто добавить их в отсортированном порядке. Если у вас много очень маленьких значений, среднее количество средних значений и небольшое количество больших значений, тогда может быть наиболее точным сначала сложить все маленькие, а затем отдельно суммировать средние значения, сложить эти две суммы вместе, затем сложите большие. Совсем нетривиально найти наиболее точную комбинацию добавлений с плавающей запятой, но чтобы справиться с действительно плохими случаями, вы можете сохранить целый массив текущих итогов с разными величинами, добавлять каждое новое значение к итоговому результату, который лучше всего соответствует его величине, и когда текущая сумма начинает становиться слишком большой для своей величины, добавьте ее к следующей сумме и начните новую. В своем логическом пределе этот процесс эквивалентен вычислению суммы в типе произвольной точности (так что вы я сделаю это). Но, учитывая упрощенный выбор добавления в порядке возрастания или убывания, возрастание является лучшим вариантом.
Это имеет какое-то отношение к программированию в реальном мире, поскольку в некоторых случаях ваши вычисления могут сильно ошибиться, если вы случайно отрежете «тяжелый» хвост, состоящий из большого количества значений, каждое из которых слишком мало, чтобы индивидуально повлиять на него. сумма, или если вы отбрасываете слишком большую точность из множества небольших значений, которые по отдельности влияют только на последние несколько бит суммы. В тех случаях, когда хвост в любом случае незначителен, вам, вероятно, все равно. Например, если вы сначала складываете только небольшое количество значений и используете только несколько значащих цифр суммы.
источник
Существует также алгоритм, предназначенный для такого рода операции накопления, называемый суммированием Кахана , о котором вам, вероятно, следует знать.
Согласно Википедии,
источник
sum
иc
различающихся величиной. Его можно тривиально расширить до N переменных.-ffast-math
GCC).-ffast-math
. Из этого обсуждения и этой ссылки я узнал , что если вы заботитесь о числовой точности, вам, вероятно, следует избегать использования-ffast-math
этого во многих приложениях, где вы можете быть привязаны к процессору, но не заботитесь о точных численных вычислениях (например, программирование игр ),-ffast-math
разумно использовать. Таким образом, я хотел бы внести поправки в свой строго сформулированный «запрещенный» комментарий.sum, c, t, y
. Вам также нужно добавитьsum -= c
доreturn sum
.Я попробовал крайний пример в ответе Стива Джессопа.
Получил следующий результат:
Ошибка в первой строке более чем в десять раз больше во второй.
Если я изменю
double
s наfloat
s в приведенном выше коде, я получу:Ни один из ответов даже не близок к 2,0 (но второй чуть ближе).
Используя суммирование Кахана (с
double
s), как описано Дэниелом Прайденом:Получаю ровно 2.0:
И даже если я изменю
double
s наfloat
s в приведенном выше коде, я получу:Казалось бы, Кахан - это правильный путь!
источник
double
это не плохо потеря точности при сложении миллиардных долей, поскольку он имеет 52 значащих бита, тогда как IEEEfloat
имеет только 24 и будет.c
содержали значения, намного превышающие следующее слагаемое. Это означает, что слагаемое намного, намного меньше, чем основная сумма, поэтому их должно быть очень много, чтобы складывать много. Особенно сdouble
арифметикой.Существует класс алгоритмов, которые решают именно эту проблему без необходимости сортировки или иного изменения порядка данных .
Другими словами, суммирование может быть выполнено за один проход по данным. Это также делает такие алгоритмы применимыми в ситуациях, когда набор данных не известен заранее, например, если данные поступают в реальном времени и необходимо поддерживать текущую сумму.
Вот отрывок из недавней статьи:
Источник: Алгоритм 908: точное суммирование потоков с плавающей запятой в режиме онлайн .
источник
Основываясь на ответе Стива о первой сортировке чисел в порядке возрастания, я бы представил еще две идеи:
Определите разницу в показателе степени двух чисел, выше которой вы можете решить, что потеряете слишком много точности.
Затем сложите числа по порядку, пока показатель аккумулятора не станет слишком большим для следующего числа, затем поместите аккумулятор во временную очередь и запустите аккумулятор со следующего числа. Продолжайте, пока не исчерпаете исходный список.
Вы повторяете процесс с временной очередью (отсортировав ее) и, возможно, с большей разницей в экспоненте.
Я думаю, что это будет довольно медленно, если вам придется постоянно вычислять экспоненты.
Я быстро попробовал программу, и результат был 1.99903.
источник
Я думаю, вы можете сделать лучше, чем сортировать числа перед их накоплением, потому что в процессе накопления накопитель становится все больше и больше. Если у вас много одинаковых чисел, вы быстро потеряете точность. Вот что я бы предложил вместо этого:
Конечно, этот алгоритм будет наиболее эффективным с приоритетной очередью вместо списка. Код на C ++:
Водитель:
Числа в очереди отрицательны, потому что
top
дает наибольшее число, но нам нужно наименьшее . Я мог бы предоставить очереди больше аргументов шаблона, но этот подход кажется более простым.источник
Это не совсем ответ на ваш вопрос, но можно сделать умный ход - вычислить сумму дважды: один раз в режиме округления «в большую сторону» и один раз в режиме «в меньшую сторону». Сравните два ответа, и вы знаете / насколько / неточны ваши результаты, и, следовательно, вам нужно использовать более умную стратегию суммирования. К сожалению, в большинстве языков изменение режима округления с плавающей запятой не так просто, как должно быть, потому что люди не знают, что это действительно полезно в повседневных вычислениях.
Взгляните на интервальную арифметику, где вы выполняете все подобные математические операции, сохраняя при этом самые высокие и самые низкие значения. Это приводит к интересным результатам и оптимизациям.
источник
Самая простая сортировка , повышающая точность, - это сортировка по возрастанию абсолютного значения. Это позволяет наименьшим значениям величин иметь возможность накапливаться или отменяться перед взаимодействием с более крупными значениями величин, которые могут вызвать потерю точности.
Тем не менее, вы можете добиться большего, отслеживая несколько неперекрывающихся частичных сумм. Вот документ с описанием техники и представлением доказательства точности: www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps
Этот алгоритм и другие подходы к точному суммированию с плавающей запятой реализованы на простом Python по адресу: http://code.activestate.com/recipes/393090/ По крайней мере два из них можно тривиально преобразовать в C ++.
источник
Для IEEE 754 одинарной или двойной точности или чисел известного формата другой альтернативой является использование массива чисел (переданного вызывающей стороной или в классе для C ++), индексированного по экспоненте. При добавлении чисел в массив добавляются только числа с одинаковым показателем степени (до тех пор, пока не будет найден пустой слот и число не будет сохранено). Когда запрашивается сумма, массив суммируется от наименьшего к наибольшему, чтобы минимизировать усечение. Пример одинарной точности:
пример двойной точности:
источник
Ваши поплавки должны быть добавлены с двойной точностью. Это даст вам больше точности, чем любой другой метод. Для большей точности и большей скорости вы можете создать, скажем, четыре суммы и сложить их в конце.
Если вы добавляете числа с двойной точностью, используйте long double для суммы - однако это будет иметь положительный эффект только в реализациях, где long double на самом деле имеет большую точность, чем double (обычно x86, PowerPC в зависимости от настроек компилятора).
источник
Что касается сортировки, мне кажется, что если вы ожидаете отмены, тогда числа следует складывать в порядке убывания, а не по возрастанию. Например:
((-1 + 1) + 1e-20) даст 1e-20
но
((1e-20 + 1) - 1) даст 0
В первом уравнении два больших числа исключаются, тогда как во втором член 1e-20 теряется при добавлении к 1, так как для его сохранения недостаточно точности.
Кроме того, попарное суммирование вполне подходит для суммирования большого количества чисел.
источник