应该添加浮点数以获得最精确的结果?

时间:2011-07-14 19:44:59

标签: c++ floating-point precision

这是我在最近的一次采访中被问到的一个问题,我想知道(我实际上并不记得数值分析的理论,所以请帮助我:)

如果我们有一些积累浮点数的函数:

std::accumulate(v.begin(), v.end(), 0.0);
例如,

vstd::vector<float>

  • 在积累这些数字之前对它们进行排序会更好吗?

  • 哪种顺序能给出最准确的答案?

我怀疑排序数字会按升序实际上使数字错误更少,但遗憾的是我无法证明这一点。

P.S。我确实意识到这可能与现实世界的编程无关,只是好奇。

11 个答案:

答案 0 :(得分:108)

你的直觉基本上是正确的,按升序排序(数量级)通常会有所改善。考虑我们添加单精度(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 }}。所有6个订单给出的结果在输入中最大幅度值的范围内是准确的(0.0000001%输出),但是对于其中4个,结果在真实解决方案的范围内是不准确的(100%输出)。您正在解决的特殊问题将告诉您前者是否足够好。

事实上,你可以发挥更多技巧,而不仅仅是按排序顺序添加它们。如果你有很多非常小的值,中等数量的中等值,以及少量的大值,那么首先将所有小值相加可能是最准确的,然后分别总计中等值,添加这两个总数然后加上大的。找到最准确的浮点加法组合并不是一件容易的事情,但是为了应对非常糟糕的情况,你可以保持不同大小的整个运行总数,将每个新值添加到与其幅度最匹配的总数中,当一个运行总计开始变得太大而不是它的大小时,将它添加到下一个总计并开始一个新的。从它的逻辑极端来看,这个过程相当于以任意精度类型执行求和(所以你要这样做)。但考虑到以递增或递减的顺序添加的简单选择,升序是更好的选择。

它确实与现实世界的编程有一定关系,因为在某些情况下,如果你不小心砍掉由大量值组成的“重”尾巴,你的计算可能会出现严重错误,每个值都太小单独影响总和,或者如果你从很多小值中抛弃太多精度,这些小值只会影响总和的最后几位。如果尾巴可以忽略不计,你可能不在乎。例如,如果您只是首先将少量值加在一起,而您只使用了总和的几个有效数字。

答案 1 :(得分:87)

还有一种算法设计用于这种累积操作,称为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

答案 2 :(得分:34)

我在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更改为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(但第二个答案稍微接近)。

使用Daniel Pryden描述的Kahan求和(double 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更改为float s,我也会得到:

Kahan sum  = 2.000000000000000    (difference = 0.000000000000000)

似乎Kahan是要走的路!

答案 3 :(得分:14)

有一类算法可以解决这个问题,无需对数据进行排序或重新排序

换句话说,求和可以在数据上一次完成。这也使得这种算法适用于预先不知道数据集的情况,例如,如果数据实时到达并且需要维持运行总和。

以下是最近一篇论文的摘要:

  

我们提出了一种新颖的在线算法,用于精确求和流   浮点数。 “在线”我们指的是算法   需要一次只能看到一个输入,并且可以随意进行   这些输入的长度输入流,同时只需要常数   记忆。 “确切”是指我们内部数组的总和   算法完全等于所有输入的总和,以及   返回的结果是正确舍入的总和。正确性的证明   适用于所有输入(包括非标准化数字但模数   中间溢出),并且与加数的数量无关   或者总和的条件数。该算法渐近需要   每个summand只有5个FLOP,并且由于指令级并行性   比明显的,快速但愚蠢的只慢约2-3倍   当命令数为时,“普通递归求和”循环   大于10,000。因此,据我们所知,它是最快,最多的   在已知算法中准确且大多数存储器有效。确实,它   很难看出一个更快的算法或一个需要的算法   没有硬件改进,可以存在明显更少的FLOP。   提供了大量加数的应用程序。

来源:Algorithm 908: Online Exact Summation of Floating-Point Streams

答案 4 :(得分:2)

在Steve首先按升序排序数字的答案的基础上,我将介绍另外两个想法:

  1. 决定两个数字的指数差异,在这两个数字之上你可能会认为你会失去太多精确度。

  2. 然后按顺序添加数字直到累加器的指数对于下一个数字来说太大,然后将累加器放到临时队列中并使用下一个数字启动累加器。继续,直到您用完原始列表。

  3. 您使用临时队列(已对其进行排序)以及可能更大的指数差异重复此过程。

    我认为如果你必须一直计算指数,这将会很慢。

    我快速完成了一个程序,结果是1.99903

答案 5 :(得分:2)

我认为你可以做的比在积累数字之前对数字进行排序更好,因为在累积过程中,累加器变得越来越大。如果你有大量相似的数字,你将很快开始失去精确度。以下是我建议的内容:

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产生最大数字,但我们想要最小。我本可以为队列提供更多模板参数,但这种方法似乎更简单。

答案 6 :(得分:2)

这并不能完全回答你的问题,但聪明的做法是两次运行,一次用rounding mode“向上”,一次用“向下”。比较两个答案,你知道/结果是多少/不准确,如果你需要使用更聪明的求和策略。不幸的是,大多数语言并没有像它应该那样容易地改变浮点舍入模式,因为人们不知道它在日常计算中实际上是有用的。

看看Interval arithmetic,你可以在这里做所有数学运算,保持最高和最低值。它会带来一些有趣的结果和优化。

答案 7 :(得分:0)

提高准确性的最简单的排序是按升序绝对值排序。这使得最小幅度值有可能在与较大幅度值相互作用之前累积或消除,这会导致精度损失。

也就是说,你可以通过跟踪多个非重叠的部分和来做得更好。这是一篇描述该技术并提出准确性证明的论文:www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps

精确浮点求和的算法和其他方法在简单的Python中实现:http://code.activestate.com/recipes/393090/其中至少有两个可以简单地转换为C ++。

答案 8 :(得分:0)

对于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;
}

答案 9 :(得分:-1)

你的花车应该加倍精确。这将为您提供比任何其他技术更多的精确度。为了更精确和更快的速度,您可以创建四个总和,并在最后添加它们。

如果要添加双精度数,请使用long double作为总和 - 但是,这只会在long double实际上具有精度高于double(通常为x86,PowerPC,具体取决于编译器设置)的实现中产生积极影响。

答案 10 :(得分:-1)

关于排序,在我看来,如果你期望取消,那么数字应该以降序的数量级添加,而不是提升。例如:

(( - 1 + 1)+ 1e-20)将给出1e-20

((1e-20 + 1) - 1)将给出0

在第一个方程中,两个大数被抵消,而在第二个方程中,1e-20项在加到1时会丢失,因为没有足够的精度来保留它。

此外,pairwise summation对于总结大量数字非常不错。