最も正確な結果を得るには、フロートをどの順序で追加する必要がありますか?


105

これは私が最近のインタビューで尋ねられた質問であり、知りたいです(私は実際には数値解析の理論を覚えていませんので、私を助けてください:)

浮動小数点数を累積する関数がある場合:

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

vstd::vector<float>、たとえば、。

  • それらを蓄積する前にこれらの数値をソートする方が良いでしょうか?

  • どの順序が最も正確な答えを与えるでしょうか?

数値を昇順で並べ替えると、実際には数値誤差が少なくなると思いますが、残念ながら自分では証明できません。

PS私はこれがおそらく実世界のプログラミングとは何の関係もないことに気づき、好奇心旺盛なだけだと思います。


17
これは実際には、実際のプログラミングと関係があります。ただし、多くのアプリケーションは、計算が「かなり近い」限り、計算の絶対的な最高の精度を実際には考慮していません。エンジニアリングアプリケーション?とっても大事。医療アプリケーション?とっても大事。大規模な統計?精度はやや低くても問題ありません。
Zéychin

18
あなたが実際に知っていて、詳細にあなたの推論を説明するページを指すことができる場合を除いて、答えないでください。私たちの周りを飛んでいる浮動小数点数についてはすでに多くのがらくたがあり、それに追加したくありません。あなたが知っていると思うなら。やめる。あなたが知っていると思うだけなら、あなたはおそらく間違っているからです。
マーティンヨーク

4
@Zéychin「エンジニアリングアプリケーション?非常に重要。医療アプリケーション?非常に重要。」??? 私は:)あなたが真実を知っていたならば、あなたは驚かれると思います
BЈовић

3
@Zeychin絶対エラーは無関係です。重要なのは相対誤差です。100分の1ラジアンが0.001%の場合、誰が気にするのでしょうか。
BЈовић

3
私はこれを本当にお勧めします。「すべてのコンピューター科学者が浮動小数点について知っておくべきこと」perso.ens-lyon.fr/jean-michel.muller/goldberg.pdf
Mohammad Alaggan

回答:


108

あなたの本能は基本的に正しいです。昇順(大きさ)で並べ替えると、通常は多少改善されます。単精度(32ビット)浮動小数点数を追加し、1 /(10億)に等しい10億の値と1に等しい1つの値がある場合を考えます。1が最初に来る場合、合計は精度が低下するため、1 +(1/10億)は1になるため、1になります。各追加は、合計にはまったく影響しません。

小さい値が最初に来る場合、それらは少なくとも何かに合計されますが、それでも私はそれらの2 ^ 30を持っていますが、2 ^ 25またはそれ以降は、それぞれが個別に合計に影響を与えていない状況に戻りますもう。だから私はまだもっとトリックが必要になるでしょう。

これは極端なケースですが、一般に、大きさが非常に異なる2つの値を追加するよりも、類似した大きさの2つの値を追加する方が正確です。数値を並べ替えることで、類似した等級の値をグループ化し、昇順でそれらを追加することにより、小さい値に、より大きい数値の等級に累積的に到達する「チャンス」を与えます。

それでも、負の数が関係している場合、このアプローチを「裏切る」ことは簡単です。合計する3つの値を考えます{1, -1, 1 billionth}。算術的に正しい合計は1 billionthですが、最初の加算に小さな値が含まれる場合、最終的な合計は0になります。6つの可能な次数のうち、「正しい」のは2つだけです- {1, -1, 1 billionth}{-1, 1, 1 billionth}。6つの次数はすべて、入力の最大値のスケール(0.0000001%アウト)で正確な結果を提供しますが、それらの4つについては、真の解のスケール(100%アウト)では結果が不正確です。あなたが解決している特定の問題は、前者が十分かどうかを教えてくれます。

実際、並べ替えた順序で追加するよりもはるかに多くのトリックをプレイできます。非常に小さな値が多数あり、中間値の数が多く、大きな値の数が少ない場合は、最初に小さな値をすべて合計し、次に中間値を個別に合計して、これら2つの合計を追加するのが最も正確です。一緒に大きなものを追加します。浮動小数点加算の最も正確な組み合わせを見つけることは決して簡単なことではありませんが、本当に悪い場合に対処するには、実行中の合計の配列全体を異なる大きさに保ち、その大きさに最も一致する合計に新しい値をそれぞれ追加します。そして、現在の合計がその大きさに対して大きすぎるようになったら、それを次の合計に追加して、新しい合計を開始します。このプロセスは、論理的に極端に言えば、和を任意精度型で実行することと同じです(つまり、dはそれを行います)。しかし、昇順または降順で追加するという単純化した選択を考えると、昇順の方が適しています。

実際のプログラミングとはある程度関係があります。多数の値で構成された「重い」尾を誤って切り落とすと、計算結果が非常に悪くなり、それぞれが小さすぎて個別に影響を及ぼさない場合があるためです。合計、または個別に合計の最後の数ビットにのみ影響する多くの小さな値からあまりにも多くの精度を捨てた場合 とにかく尾が無視できる場合は、おそらく気にしません。たとえば、最初に少数の値を加算するだけで、合計の有効数字をいくつか使用している場合です。


8
説明のための+1。通常、加算は数値的に安定しているため(減算や除算とは異なり)、直感に反します。
Konrad Rudolph

2
@Konrad、数値的には安定しているかもしれませんが、オペランドの大きさが異なると正確ではありません:)
MSN

3
@ 6502:大きさの順にソートされているため、-1が最後になります。合計の真の値が1であれば、それで問題ありません。3つの値を合計すると、1/10億、1、-1となり、0になります。この時点で、興味深い実用的な質問に答える必要があります-のスケールで正確な答えが必要ですか?真の合計、または最大値のスケールで正確な答えだけが必要ですか?一部の実用的なアプリケーションでは、後者で十分ですが、それ以外の場合は、より洗練されたアプローチが必要です。量子物理学はくりこみを使用します。
スティーブジェソップ

8
この単純なスキームを使い続ける場合は、絶対値が最も小さい2つの数値を常に加算し、その和をセットに再挿入します。(そうですね、おそらくここではマージソートが最適です。以前に合計された数値を含む配列の部分を部分合計の作業領域として使用できます。)
Neil

2
@Kevin Panko:単純なバージョンは、単精度浮動小数点数が24桁の2進数を持ち、その最大値が数値の最大セットビットです。したがって、大きさが2 ^ 24を超えて異なる2つの数値を加算すると、小さい方の値が完全に失われ、大きさが少ししか異なる場合は、小さい方の対応するビット数の精度が失われます。数。
スティーブジェソップ2011

88

この種の累積演算用に設計されたアルゴリズム、Kahan Summationもあります。

ウィキペディアによると、

カハンの加算アルゴリズム(としても知られている補償総和は)かなり明白なアプローチに比べ、有限精度浮動小数点数の配列を付加した合計数値誤差を減少させます。これは、別個の実行中の補償(小さなエラーを蓄積する変数)を維持することによって行われます。

疑似コードでは、アルゴリズムは次のとおりです。

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

3
このスレッドへの素敵な追加+1。これらのステートメントを「熱心に最適化」するコンパイラは禁止する必要があります。
Chris A.

1
それはほとんど2つの合算変数を使用することにより、精度を倍増するための簡単な方法だsumc大きさが異なります。簡単にN個の変数に拡張できます。
MSalters

2
@ChrisA。カウントするすべてのコンパイラでこれを明示的に制御できます(-ffast-mathGCC 経由など)。
Konrad Rudolph

6
@Konrad Rudolphは、これで可能な最適化であることを指摘してくれてありがとう-ffast-math。このディスカッションとこのリンクから学んだことは、数値の精度を重視する場合はおそらく使用を避けるべき-ffast-mathですが、CPUに依存している可能性があるが、正確な数値計算を重視しない多くのアプリケーション(たとえば、ゲームプログラミングなど)では)、-ffast-math使用するのが妥当です。したがって、私は強く発言された「禁止された」コメントを賞賛したいと思います。
クリスA.

倍精度変数を使用するsum, c, t, yと役立ちます。またsum -= c、前に追加する必要がありreturn sumます。
G.コーエン

34

スティーブ・ジェソップの答えの中で、極端な例を試してみました。

#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)

最初の行のエラーは2番目の行の10倍以上大きくなっています。

上記のコードでdoublesをfloats に変更すると、次のようになります。

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に近づいていません(2番目の回答は少し近いです)。

doubleDaniel 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)

上記のコードでdoublesをfloats に変更しても、次のようになります。

Kahan sum  = 2.000000000000000    (difference = 0.000000000000000)

カハンが行くべき道のようです!


私の「大きな」値は1に等しく、1e9ではありません。サイズの昇順に追加あなたの第二の答え、数学的に正確である(1億、プラス億十億、1億1である)、けれどもより運によって任意の方法のいずれかの一般的な健全性:-)注double悪いを受けません有効ビット数が52であるため、10億分の1を加算すると精度が低下しますが、IEEEにfloatは24 ビットしかありません。
スティーブジェソップ2011

@Steve、私のエラー、お詫び。サンプルコードをあなたが意図したものに更新しました。
アンドリュースタイン

4
カハンの精度はまだ限られていますが、キラーケースを構築するには、メインの合計とエラーアキュムレータの両方に、次の合計cよりもはるかに大きな値を含める必要があります。これは、加数がメインの合計よりもはるかに小さく、非常に小さいことを意味します。そのため、合計するには非常に多くの合計が必要になります。特にdouble算術で。
Steve Jessop、2011

14

この正確な問題を解決するアルゴリズムのクラスがあり、データを並べ替えたり、並べ替えたりする必要はありません

言い換えると、合計はデータの1回のパスで実行できます。これにより、データが事前にわからない場合、たとえばデータがリアルタイムで到着し、現在の合計を維持する必要がある場合にも、このようなアルゴリズムを適用できます。

ここに最近の論文の要約があります:

浮動小数点数のストリームを正確に合計する新しいオンラインアルゴリズムを紹介します。「オンライン」とは、アルゴリズムが一度に1つの入力のみを参照する必要があり、一定のメモリのみを必要としながら、そのような入力の任意の長さの入力ストリームを取得できることを意味します。「正確」とは、アルゴリズムの内部配列の合計がすべての入力の合計と正確に等しく、返される結果が正しく丸められた合計であることを意味します。正しさの証明は、すべての入力(正規化されていない数値を含むが、中間のオーバーフローを法とする)に対して有効であり、加数の数や合計の条件数とは無関係です。アルゴリズムは漸近的に1つの命令あたり5つのFLOPのみを必要とし、命令レベルの並列処理により、明白な実行よりも約2〜3倍遅いだけです。加数の数が10,000を超える場合、高速だがダムの「通常の再帰的加算」ループ。したがって、私たちの知る限り、これは既知のアルゴリズムの中で最も速く、最も正確で、最もメモリ効率の高いものです。実際、ハードウェアを改善せずに、より高速なアルゴリズムや、必要なFLOPが大幅に少ないアルゴリズムがどのように存在するかを確認することは困難です。多数の加数のアプリケーションが提供されます。

出典:Algorithm 908:Online Exact Summation of Floating-Point Streams


1
@インバース:実店舗のライブラリはまだ残っています。または、オンラインでPDFを購入すると、5〜15ドルかかります(ACMメンバーかどうかによって異なります)。最後に、DeepDyveは紙を$ 2.99で24時間貸すことを提案しているようです(DeepDyveを初めて使用する場合は、無料トライアルの一部として無料で入手できる場合もあります):deepdyve.com/lp/acm /…
NPE

2

最初に数値を昇順にソートするというスティーブの答えに基づいて、さらに2つのアイデアを紹介します。

  1. 2つの数値の指数の違いを決定します。これを超えると、精度が低下しすぎると判断する可能性があります。

  2. 次に、アキュムレータの指数が次の数に対して大きすぎるまで順番に数を追加し、次にアキュムレータを一時キューに入れ、次の数からアキュムレータを開始します。元のリストがなくなるまで続けます。

一時的なキュー(並べ替え済み)を使用して、指数の差がさらに大きくなる可能性があるプロセスを繰り返します。

常に指数を計算する必要がある場合、これはかなり遅くなると思います。

プログラムをすぐに試したところ、結果は1.99903でした。


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は、最大の数が得られるためですが、最小の数が必要です。キューにより多くのテンプレート引数を提供することもできましたが、このアプローチはより単純に思えます。


2

これはあなたの質問に完全には答えませんが、賢いことは、合計を2回実行することです。1回は丸めモード切り上げ」で、もう1回は「切り捨て」で行います。2つの回答を比較すると、結果が/ how /不正確であることがわかっているため、より巧妙な合計戦略を使用する必要があります。残念ながら、ほとんどの言語では浮動小数点の丸めモードを簡単に変更することができません。これは、日常の計算で実際に役立つことを人々が知らないためです。

このようなすべての計算を行うInterval演算を見てみましょう。移動しながら最高値と最低値を維持します。それはいくつかの興味深い結果と最適化につながります。


0

精度を向上させる最も簡単なソートは、昇順の絶対値でソートすることです。これにより、最小のマグニチュード値は、精度の低下を引き起こす大きなマグニチュード値と相互作用する前に、蓄積またはキャンセルされる可能性があります。

とはいえ、重複しない複数の部分合計を追跡することで、より良い結果が得られます。テクニックを説明し、正確性を証明する論文を次に示します。www-2.cs.cmu.edu/ afs / cs / project / quake / public / papers / robust-arithmetic.ps

そのアルゴリズムと正確な浮動小数点の合計に対する他のアプローチは、単純なPythonのhttp://code.activestate.com/recipes/393090/で実装されてい ます。 そのうちの少なくとも2つは簡単にC ++に変換できます。


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

これは、1971年Malcolmの方法、あるいは、DemmelとHidaの指数を使用するバリアント(「アルゴリズム3」)のように聞こえます。あなたのようなキャリーベースのループを行う別のアルゴリズムがありますが、現時点では見つかりません。
ZachB

@ZachB-コンセプトは、リンクされたリストのボトムアップマージソートに似ています。これも小さな配列を使用し、array [i]は2 ^ iノードのリストを指します。これがどのくらい前に戻るのかわかりません。私の場合、それは1970年代の自己発見でした。
rcgldr

-1

フロートは倍精度で追加する必要があります。これにより、他のどの手法よりも高い精度が得られます。もう少し精度と速度を上げるには、たとえば4つの合計を作成し、最後に合計します。

倍精度の数値を追加する場合は、合計にlong doubleを使用します。ただし、これはlong doubleが実際にdoubleよりも精度が高い実装(通常はx86、コンパイラーの設定に応じてPowerPC)でのみ効果があります。


1
「これにより、他のどの手法よりも精度が高くなります」正確な総和の使用方法を説明した最初の遅い回答から1年以上経過して回答が得られることをご存知ですか?
Pascal Cuoq 2014

「ロングダブル」タイプはひどいので、使用しないでください。
ジェフ

-1

ソートに関しては、キャンセルが予想される場合は降順で数字を追加する必要があるようです昇順ではなくです。例えば:

((-1 + 1)+ 1e-20)は1e-20を返します

だが

((1e-20 + 1)-1)は0を返します

最初の方程式では2つの大きな数がキャンセルされますが、2番目の方程式では、1e-20項を1に追加すると失われます。これは、それを保持するのに十分な精度がないためです。

また、ペアごとの合計は、たくさんの数を合計するのにかなりまともです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.