これは最近のインタビューで尋ねられた質問で、知りたいです(数値解析の理論を実際に覚えていないので、助けてください:)
浮動小数点数を累積する関数がある場合:
std::accumulate(v.begin(), v.end(), 0.0);
v
はstd::vector<float>
、 例えば。
累積する前にこれらの数値をソートする方が良いでしょうか?
どの順序で最も正確な答えが得られますか?
sorting昇順の数字は実際に数値エラーを引き起こすと思われますlessですが、残念ながら自分で証明することはできません。
PS私はこれがおそらく実世界のプログラミングとは何の関係もなく、ただ好奇心が強いだけであることを理解しています。
あなたの本能は基本的に正しいです。昇順(大規模)でのソートは通常、物事をいくらか改善します。単精度(32ビット)浮動小数点数を追加し、1 /(10億)に等しい10億の値と1に等しい1つの値がある場合を考えてみましょう。1が最初に来ると、合計が来ます1 +(1/10億)は精度の低下により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つの合計を加算することが最も正確な場合があります一緒に大きなものを追加します。浮動小数点加算の最も正確な組み合わせを見つけるのは決して簡単なことではありませんが、本当に悪いケースに対処するために、さまざまな大きさで実行中の合計の配列全体を保持し、その大きさに最も一致する合計に新しい値をそれぞれ追加できます。実行中の合計が大きすぎて大きくなり始めたら、それを次の合計に追加し、新しい合計を開始します。論理的に極端に言えば、このプロセスは、任意精度の型で合計を実行するのと同じです(したがって、あなたはそうします)。ただし、昇順または降順で加算するという単純な選択を考えると、昇順の方が適しています。
実際のプログラミングとは何らかの関係があります。それぞれが小さすぎて個々に影響を与えられない多数の値で構成される「重い」テールを誤って切り落とすと、計算が非常に悪くなる可能性があるためです。合計、または個別に合計の最後の数ビットにしか影響しない多くの小さな値からあまりにも多くの精度を捨てた場合。とにかく尾が無視できる場合は、おそらく気にしません。たとえば、そもそも少数の値を加算するだけで、合計の有効数字をいくつか使用する場合です。
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
スティーブジェソップが提供した回答の極端な例を試してみました。
#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倍を超えています。
上記のコードでdouble
sをfloat
sに変更すると、次のようになります。
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番目の回答は少し近いです)。
ダニエル・プライデンによって説明されているように、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
sをfloat
sに変更しても、次のようになります。
Kahan sum = 2.000000000000000 (difference = 0.000000000000000)
カハンが行く方法であるように思われるでしょう!
この正確な問題を解決するアルゴリズムのクラスがあります。データを並べ替えたり、並べ替えたりする必要はありません。
言い換えれば、合計はデータの1パスで実行できます。これにより、そのようなアルゴリズムは、データセットが事前に知られていない状況でも適用可能になります。データがリアルタイムで到着し、現在の合計を維持する必要がある場合。
最近の論文の要約は次のとおりです。
浮動小数点数のストリームを正確に合計するための新しいオンラインアルゴリズムを紹介します。 「オンライン」とは、アルゴリズムが一度に1つの入力のみを見る必要があり、一定のメモリのみを必要とする一方で、そのような入力の任意の長さの入力ストリームを取ることができることを意味します。 「正確」とは、アルゴリズムの内部配列の合計がすべての入力の合計と正確に等しく、返される結果が正しく丸められた合計であることを意味します。正当性の証明は、すべての入力(非正規化数であるが中間オーバーフローを法として含む)に対して有効であり、被加数または和の条件数とは無関係です。アルゴリズムは、加数ごとに漸近的に5 FLOPのみを必要とし、命令レベルの並列処理により、被加数が10,000を超える場合、明白で高速だがダムの「通常の再帰的加算」ループよりも約2倍から3倍遅いだけです。 。したがって、私たちの知る限り、それは既知のアルゴリズムの中で最も速く、最も正確で、最もメモリ効率が良いものです。実際、ハードウェアを改善しなければ、より高速なアルゴリズムや、必要なFLOPが大幅に少ないアルゴリズムがどのように存在するかを知ることは困難です。多数の被加数のアプリケーションが提供されます。
これはあなたの質問にはまったく答えませんが、賢いことは、合計を2回実行することです。1回は rounding mode "round up"で、もう1回は "round down"です。 2つの答えを比較すると、結果が/ how /不正確であることがわかります。したがって、賢い加算戦略を使用する必要がある場合。残念ながら、ほとんどの言語では、浮動小数点の丸めモードを必要なほど簡単に変更できません。これは、日常の計算で実際に役立つことを人々が知らないためです。
間隔演算 を見てください。このようにすべての計算を行い、最高値と最低値を維持します。いくつかの興味深い結果と最適化につながります。
最初に数字を昇順でソートするというSteveの答えに基づいて、さらに2つのアイデアを紹介します。
2つの数値の指数の差を決定します。この差を超えると、精度が低下しすぎると判断する場合があります。
次に、累算器の指数が次の数に対して大きすぎるまで順番に数字を加算してから、累算器を一時キューに入れ、次の数字で累算器を開始します。元のリストを使い果たすまで続けます。
一時キュー(ソート済み)を使用して、指数の差をできるだけ大きくしてプロセスを繰り返します。
指数を常に計算する必要がある場合、これは非常に遅いと思います。
私はプログラムを簡単に試しましたが、結果は1.99903でした
累積の過程で、アキュムレータはどんどん大きくなるため、累積する前に数値をソートするよりもうまくやれると思います。同様の数値が大量にある場合、すぐに精度が低下し始めます。代わりに私が提案するものは次のとおりです。
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
は最大の数値を生成するため、キュー内の数値は負ですが、最小が必要です。より多くのテンプレート引数をキューに提供することもできましたが、このアプローチはより簡単に思えます。
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;
}
精度を向上させる最も単純なsortは、昇順の絶対値でソートすることです。これにより、最小のマグニチュード値は、精度の低下を引き起こす大きなマグニチュード値と相互作用する前に蓄積またはキャンセルされる可能性があります。
とは言っても、重複しない複数の部分合計を追跡することで、より良い結果を得ることができます。以下は、テクニックを説明し、精度の証明を提示する論文です。www-2.cs.cmu.edu/ afs/cs/project/quake/public/papers/robust-arithmetic.ps
そのアルゴリズムと正確な浮動小数点加算のその他のアプローチは、単純なPython at: http://code.activestate.com/recipes/393090/ 少なくとも2つで実装されていますそれらのうち、C++に簡単に変換できます。