这是我最近的一次采访中问的一个问题,我想知道(我实际上不记得数值分析的理论,所以请帮助我:)
如果我们有一些函数,它会累积浮点数:
std::accumulate(v.begin(), v.end(), 0.0);
v
是一个std::vector<float>
,例如。
在累加它们之前对这些数字进行排序会更好吗?
哪个命令会给出最准确的答案?
我怀疑排序依次递增的数字实际上使数值误差少,但不幸的是我不能证明它自己。
PS我确实意识到这可能与现实世界的编程无关,只是感到好奇。
这是我最近的一次采访中问的一个问题,我想知道(我实际上不记得数值分析的理论,所以请帮助我:)
如果我们有一些函数,它会累积浮点数:
std::accumulate(v.begin(), v.end(), 0.0);
v
是一个std::vector<float>
,例如。
在累加它们之前对这些数字进行排序会更好吗?
哪个命令会给出最准确的答案?
我怀疑排序依次递增的数字实际上使数值误差少,但不幸的是我不能证明它自己。
PS我确实意识到这可能与现实世界的编程无关,只是感到好奇。
Answers:
您的直觉基本上是正确的,按升序排列(数量级)通常可以使事情有所改善。考虑以下情况:我们要添加单精度(32位)浮点数,并且有10亿个值等于1 /(10亿个),并且有一个值等于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%输出)是不准确的。您要解决的特定问题将告诉您前者是否足够好。
实际上,您可以玩的技巧更多,而不仅仅是按顺序添加它们。如果您有很多非常小的值,中等数量的中间值和少量大值,那么首先将所有小值相加,然后分别将中值相加,然后将这两个总数相加,可能是最准确的方法一起添加大的 找到最精确的浮点加法组合并不是一件容易的事,但是要处理非常糟糕的情况,您可以将整个运行总计保持在不同的幅度,将每个新值添加到最匹配其幅度的总和,当运行的总计开始变得太大而无法达到其幅度时,请将其添加到下一个总计中,然后开始一个新的总计。从逻辑上讲,此过程等效于以任意精度类型执行总和(因此,d)。但是考虑到按升序或降序数量级的简单选择,升序是更好的选择。
它确实与实际编程有关,因为在某些情况下,如果您不小心砍掉了由大量值组成的“沉重”尾巴,那么每个尾巴都太小而无法单独影响,则计算可能会变得非常错误。总和,或者如果您放弃了很多单独影响总和的最后几位的小值而导致的精度过高。如果尾巴可以忽略不计,那么您可能不在乎。例如,如果您刚开始只将少量值相加,而只使用了一些重要的和。
您可能还应该知道还有一种针对这种累加运算设计的算法,称为Kahan Summation。
根据维基百科,
的Kahan的求和算法(也称为补偿求和)显著降低通过添加有限精度浮点数的一个序列获得的总的数值误差,相对于明显的方法。这是通过保留单独的运行补偿(用于累积小误差的变量)来完成的。
在伪代码中,算法为:
function kahanSum(input) var sum = input[1] var c = 0.0 //A running compensation for lost low-order bits. for i = 2 to input.length y = input[i] - c //So far, so good: c is zero. t = sum + y //Alas, sum is big, y small, so low-order digits of y are lost. c = (t - sum) - y //(t - sum) recovers the high-order part of y; subtracting y recovers -(low part of y) sum = t //Algebraically, c should always be zero. Beware eagerly optimising compilers! next i //Next time around, the lost low part will be added to y in a fresh attempt. return sum
sum
,几乎可以使精度提高一倍c
。它可以简单地扩展到N个变量。
-ffast-math
GCC)。
sum, c, t, y
会有所帮助。您还需要添加sum -= c
在之前return sum
。
我在史蒂夫·杰索普(Steve Jessop)提供的答案中尝试了极端的例子。
#include <iostream>
#include <iomanip>
#include <cmath>
int main()
{
long billion = 1000000000;
double big = 1.0;
double small = 1e-9;
double expected = 2.0;
double sum = big;
for (long i = 0; i < billion; ++i)
sum += small;
std::cout << std::scientific << std::setprecision(1) << big << " + " << billion << " * " << small << " = " <<
std::fixed << std::setprecision(15) << sum <<
" (difference = " << std::fabs(expected - sum) << ")" << std::endl;
sum = 0;
for (long i = 0; i < billion; ++i)
sum += small;
sum += big;
std::cout << std::scientific << std::setprecision(1) << billion << " * " << small << " + " << big << " = " <<
std::fixed << std::setprecision(15) << sum <<
" (difference = " << std::fabs(expected - sum) << ")" << std::endl;
return 0;
}
我得到以下结果:
1.0e+00 + 1000000000 * 1.0e-09 = 2.000000082740371 (difference = 0.000000082740371)
1000000000 * 1.0e-09 + 1.0e+00 = 1.999999992539933 (difference = 0.000000007460067)
第一行中的错误比第二行中的错误大十倍以上。
如果在上面的代码中将double
s 更改为s float
,则会得到:
1.0e+00 + 1000000000 * 1.0e-09 = 1.000000000000000 (difference = 1.000000000000000)
1000000000 * 1.0e-09 + 1.0e+00 = 1.031250000000000 (difference = 0.968750000000000)
这两个答案都无法接近2.0(但第二个答案稍微接近)。
使用double
Daniel Pryden所述的Kahan求和(带有s):
#include <iostream>
#include <iomanip>
#include <cmath>
int main()
{
long billion = 1000000000;
double big = 1.0;
double small = 1e-9;
double expected = 2.0;
double sum = big;
double c = 0.0;
for (long i = 0; i < billion; ++i) {
double y = small - c;
double t = sum + y;
c = (t - sum) - y;
sum = t;
}
std::cout << "Kahan sum = " << std::fixed << std::setprecision(15) << sum <<
" (difference = " << std::fabs(expected - sum) << ")" << std::endl;
return 0;
}
我得到的正是2.0:
Kahan sum = 2.000000000000000 (difference = 0.000000000000000)
即使在上面的代码中将double
s 更改为s,我也会float
得到:
Kahan sum = 2.000000000000000 (difference = 0.000000000000000)
看来Kahan是要走的路!
double
都还不错:-)请注意,这并不算不好十亿分之十的精度相加会损失精度,因为它有52个有效位,而IEEE float
只有24个,并且会。
c
以包含比下一个被求和大得多的值。这意味着总和要比主要总和小得多,因此必须有很多这样的总和。尤其是double
算术运算。
有一类算法可以解决这个确切的问题,而无需对数据进行排序或重新排序。
换句话说,求和可以一次通过数据。这也使得这样的算法适用于事先不知道数据集的情况,例如,如果数据是实时到达的并且需要保持运行总和。
这是最近一篇论文的摘要:
我们提出一种新颖的在线算法,用于精确求和浮点数流。“在线”是指该算法一次只需要看一个输入,并且可以采用这种输入的任意长度的输入流,而只需要恒定的内存。“精确”是指算法内部数组的总和与所有输入的总和完全相等,并且返回的结果是正确舍入的总和。正确性证明对所有输入均有效(包括非归一化的数字,但取模中间溢出),并且与求和数或总和的条件数无关。该算法渐近每次请求只需要5个FLOP,并且由于指令级并行性的运行速度比明显的速度慢2--3倍,当求和数大于10,000时,快速但笨拙的“常规递归求和”循环。因此,据我们所知,它是已知算法中最快,最准确和最有效的内存。确实,很难看到如果不对硬件进行改进就可以存在一种更快的算法或需要更少FLOP的算法。提供了大量求证的申请。
来源:算法908:浮点流的在线精确求和。
我认为您可以比对数字进行累加更好,因为在累加过程中,累加器会越来越大。如果您有大量相似的数字,您将很快失去精度。这是我的建议:
while the list has multiple elements
remove the two smallest elements from the list
add them and put the result back in
the single element in the list is the result
当然,使用优先级队列而不是列表,该算法将是最有效的。C ++代码:
template <typename Queue>
void reduce(Queue& queue)
{
typedef typename Queue::value_type vt;
while (queue.size() > 1)
{
vt x = queue.top();
queue.pop();
vt y = queue.top();
queue.pop();
queue.push(x + y);
}
}
司机:
#include <iterator>
#include <queue>
template <typename Iterator>
typename std::iterator_traits<Iterator>::value_type
reduce(Iterator begin, Iterator end)
{
typedef typename std::iterator_traits<Iterator>::value_type vt;
std::priority_queue<vt> positive_queue;
positive_queue.push(0);
std::priority_queue<vt> negative_queue;
negative_queue.push(0);
for (; begin != end; ++begin)
{
vt x = *begin;
if (x < 0)
{
negative_queue.push(x);
}
else
{
positive_queue.push(-x);
}
}
reduce(positive_queue);
reduce(negative_queue);
return negative_queue.top() - positive_queue.top();
}
队列中的数字为负,因为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 ++类中)。将数字添加到数组时,仅添加具有相同指数的数字(直到找到一个空插槽并存储该数字)。当需要求和时,将数组从最小到最大求和以最小化截断。单精度示例:
/* clear array */
void clearsum(float asum[256])
{
size_t i;
for(i = 0; i < 256; i++)
asum[i] = 0.f;
}
/* add a number into array */
void addtosum(float f, float asum[256])
{
size_t i;
while(1){
/* i = exponent of f */
i = ((size_t)((*(unsigned int *)&f)>>23))&0xff;
if(i == 0xff){ /* max exponent, could be overflow */
asum[i] += f;
return;
}
if(asum[i] == 0.f){ /* if empty slot store f */
asum[i] = f;
return;
}
f += asum[i]; /* else add slot to f, clear slot */
asum[i] = 0.f; /* and continue until empty slot */
}
}
/* return sum from array */
float returnsum(float asum[256])
{
float sum = 0.f;
size_t i;
for(i = 0; i < 256; i++)
sum += asum[i];
return sum;
}
双精度示例:
/* clear array */
void clearsum(double asum[2048])
{
size_t i;
for(i = 0; i < 2048; i++)
asum[i] = 0.;
}
/* add a number into array */
void addtosum(double d, double asum[2048])
{
size_t i;
while(1){
/* i = exponent of d */
i = ((size_t)((*(unsigned long long *)&d)>>52))&0x7ff;
if(i == 0x7ff){ /* max exponent, could be overflow */
asum[i] += d;
return;
}
if(asum[i] == 0.){ /* if empty slot store d */
asum[i] = d;
return;
}
d += asum[i]; /* else add slot to d, clear slot */
asum[i] = 0.; /* and continue until empty slot */
}
}
/* return sum from array */
double returnsum(double asum[2048])
{
double sum = 0.;
size_t i;
for(i = 0; i < 2048; i++)
sum += asum[i];
return sum;
}
您的浮标应以双精度添加。这将为您提供比其他任何技术都更高的精度。为了获得更高的精度和更快的速度,您可以创建四个和,然后将它们相加。
如果要添加双精度数,请使用长双精度数作为总和-但是,这只会在长双精度实际上具有比双精度更高的精度的实现中起作用(通常为x86,PowerPC取决于编译器设置)。