web-dev-qa-db-ja.com

すべての値の合計がdoubleの制限を超える平均を計算するための良い解決策は何ですか?

非常に大きなdoubleのセット(10 ^ 9の値)の平均を計算する必要があります。値の合計がdoubleの上限を超えているので、合計を計算する必要のない、平均を計算するためのきちんとした小さなトリックを誰かが知っていますか?

Java 1.5を使用しています。

41
Simon

平均を繰り返し計算する 。このアルゴリズムはシンプルで高速です。各値を一度だけ処理する必要があり、変数がセットの最大値より大きくなることはないため、オーバーフローは発生しません。

double mean(double[] ary) {
  double avg = 0;
  int t = 1;
  for (double x : ary) {
    avg += (x - avg) / t;
    ++t;
  }
  return avg;
}

ループ内では、avgは常に、これまでに処理されたすべての値の平均値です。つまり、すべての値が有限であれば、オーバーフローは発生しません。

170
martinus

私があなたに尋ねたい最初の問題はこれです:

  • 事前に値の数を知っていますか?

そうでない場合は、平均を計算して合計し、カウントして、除算する以外に選択肢はほとんどありません。 Doubleがこれを処理するのに十分な精度でない場合は、残念ながらDoubleを使用できません。処理できるデータ型を見つける必要があります。

一方、doが値の数を事前に知っている場合は、実際に何をしているかを確認して変更できますhowあなたはそれを行いますが、全体的な結果は保持します。

コレクションAに格納されているN個の値の平均は次のとおりです。

A[0]   A[1]   A[2]   A[3]          A[N-1]   A[N]
---- + ---- + ---- + ---- + .... + ------ + ----
 N      N      N      N               N       N

この結果のサブセットを計算するには、計算を同じサイズのセットに分割することができます。これにより、3値セットの場合にこれを行うことができます(値の数が3で割り切れる場合、別の除数が必要です)

/ A[0]   A[1]   A[2] \   / A[3]   A[4]   A[5] \   //      A[N-1]   A[N] \
| ---- + ---- + ---- |   | ---- + ---- + ---- |   \\    + ------ + ---- |
\  3      3      3   /   \  3      3      3   /   //        3       3   /
 --------------------- +  --------------------  + \\      --------------
          N                        N                        N
         ---                      ---                      ---
          3                        3                        3

同じサイズのセットが必要であることに注意してください。そうでない場合、最後のセットの数値は、その前のすべてのセットと比較して十分な値を持たないため、最終結果により大きな影響を与えます。

1〜7の数字を順番に検討します。3のセットサイズを選択すると、次の結果が得られます。

/ 1   2   3 \   / 4   5   6 \   / 7 \ 
| - + - + - | + | - + - + - | + | - |
\ 3   3   3 /   \ 3   3   3 /   \ 3 /
 -----------     -----------     ---
      y               y           y

それは与える:

     2   5   7/3
     - + - + ---
     y   y    y

すべてのセットでyが3の場合、次のようになります。

     2   5   7/3
     - + - + ---
     3   3    3

それは与える:

2*3   5*3    7
--- + --- + ---
 9     9     9

それは:

6   15   7
- + -- + -
9    9   9

合計:

28
-- ~ 3,1111111111111111111111.........1111111.........
 9

1〜7の平均は4です。明らかに、これは機能しません。上記のエクササイズを1、2、3、4、5、6、7、0、0(最後の2つのゼロに注意)で実行すると、上記の結果が得られることに注意してください。

つまり、値の数を同じサイズのセットに分割できない場合、最後のセットは、それより前のすべてのセットと同じ数の値を持っているかのようにカウントされますが、次の場合はゼロが埋め込まれますすべての欠損値。

したがって、同じサイズのセットが必要です。元の入力セットが素数の値で構成されている場合は、頑張ってください。

ここで私が心配しているのは、精度の低下です。最初に値の合計全体を保持できない場合、Doubleがこのような場合に十分な精度を提供するかどうかは完全にはわかりません。

13

私見、あなたの問題を解決する最も堅牢な方法は

  1. セットを並べ替える
  2. 合計がオーバーフローしない要素のグループに分割-それらはソートされるため、これは高速で簡単です
  3. 各グループで合計を行い、グループサイズで割ります
  4. グループの合計の合計を実行します(このアルゴリズムを再帰的に呼び出す可能性があります)-グループのサイズが等しくない場合は、グループをそのサイズで重み付けする必要があることに注意してください

このアプローチの良い点の1つは、合計する要素が非常に多い場合、および計算に使用するプロセッサ/マシンの数が多い場合に、適切にスケーリングできることです。

12
Davide

すでに提案されているより良いアプローチを使用することとは別に、計算を行うには BigDecimal を使用できます。 (覚えておいてください、それは不変です)

11
Bozho

値の潜在的な範囲を明確にしてください。

Doubleの範囲が〜= +/- 10 ^ 308であり、10 ^ 9の値を合計している場合、質問で提案されている見かけ上の範囲は10 ^ 299のオーダーの値です。

それはやや、まあ、そうではないようです...

値が本当にareである場合、通常のdoubleでは、有効な10進数で17桁しかないため、約280桁の情報を捨ててしまう前に、値の平均について考えます。

私はまた、(他の誰も持っていないので)数値のセットXについても注意します:

_mean(X) = sum(X[i] - c)  +  c
          -------------
                N
_

任意の定数cの場合。

この特定の問題では、c = min(X)mightを設定すると、加算中のオーバーフローのリスクが劇的に減少します。

問題の記述が不完全であることを謙虚に提案できますか...?

10
Alnitak

Doubleは、精度を失うことなく2の累乗で除算できます。したがって、もしあなたの唯一の問題が合計の絶対的な大きさであるなら、あなたはそれらを合計する前にあなたの数を事前にスケールすることができます。ただし、このサイズのデータ​​セットでは、小さい数値を大きい数値に追加し、小さい数値がほとんど(または完全に)無視される状況に陥るリスクが依然としてあります。

たとえば、2.2e-20を9.0e20に追加すると、数値を加算できるようにスケールを調整すると、小さい方の数値は0になるため、結果は9.0e20になります。Doubleは約17桁しか保持できず、これら2つの数値を損失なく加算するには、40桁以上が必要です。

そのため、データセットと、桁数に余裕がある精度によっては、他のことを行う必要がある場合があります。データをセットに分割すると役立ちますが、精度を維持するためのより良い方法は、おおよその平均を決定することです(この数値はすでにわかっている場合があります)。次に、合計する前に大まかな平均から各値を差し引きます。この方法では、平均からの距離を合計しているため、合計が非常に大きくなることはありません。

次に、平均デルタを取り、それを大まかな合計に追加して、正しい平均を取得します。最小と最大の差分を追跡すると、合計プロセス中にどれだけの精度が失われたかもわかります。時間が多く、非常に正確な結果が必要な場合は、反復できます。

6
John Knoeller

制限を超えない、同じサイズの数のサブセットの平均の平均を取ることができます。

6
David M

すべての値を設定サイズで割り、それを合計します

5
Alon

オプション1は、任意精度のライブラリを使用することです。これにより、上限がなくなります。

他のオプション(精度を失う)は、一度にすべてではなくグループで合計するか、合計する前に分割することです。

5
Anon.

まず、double値の内部表現をよく理解してください。ウィキペディアは良い出発点になるはずです。

次に、倍精度浮動小数点数が「値+指数」として表され、指数が2のべき乗であると考えます。最大のdouble値の制限は指数の上限であり、値の制限ではありません!したがって、すべての大きな入力数値を2の十分な大きさで除算できます。これは、十分に大きいすべての数値に対して安全です。結果を係数で再乗算して、乗算で精度が失われたかどうかを確認できます。

ここでアルゴリズムを使います

public static double sum(double[] numbers) { 
  double eachSum, tempSum;
  double factor = Math.pow(2.0,30); // about as large as 10^9
  for (double each: numbers) {
    double temp = each / factor;
    if (t * factor != each) {
      eachSum += each;
    else {
      tempSum += temp;
    }
  }
  return (tempSum / numbers.length) * factor + (eachSum / numbers.length);
}

追加の除算と乗算を心配する必要はありません。 FPUは2の累乗で行われるため、それらを完全に最適化します(比較のために、10進数の最後に数字を追加および削除することを想像してください)。

PS:さらに、精度を向上させるために Kahan summation を使用することもできます。非常に大きい数と非常に小さい数を合計したときに、Kahanの合計により精度の低下が回避されます。

3
akuhn

ですので、あまり繰り返しませんが、数値のリストは正規分布であると想定しており、オーバーフローする前に多くの数値を合計できると述べておきます。このテクニックは通常のディストリビューションではまだ機能しますが、以下で説明する期待に応えられないものがあります。

-

オーバーフローに近づくまで、食べる数を追跡しながらサブシリーズを合計し、平均をとります。これにより、平均a0とn0がカウントされます。リストがなくなるまで繰り返します。今、あなたは多くのai、niを持っているはずです。

リストの最後の一口を除いて、各aiとniは比較的接近している必要があります。あなたはリストの終わり近くにかみ傷をつけることによってそれを軽減することができます。

これらのai、niの任意のサブセットを組み合わせるには、サブセット内の任意のniを選択し(npと呼びます)、サブセット内のすべてのniをその値で除算します。組み合わせるサブセットの最大サイズは、nのほぼ一定の値です。

Ni/npは1に近いはずです。ここでsum ni/np * aiとnp /(sum ni)の倍数を合計し、sum niを追跡します。これにより、手順を繰り返す必要がある場合に、新しいni、aiの組み合わせが得られます。

繰り返す必要がある場合(つまり、ai、niペアの数は通常のniよりもはるかに大きい)、最初に1つのnレベルですべての平均を結合し、次に次のレベルで結合することにより、相対的なnサイズを一定に保つようにします。等々。

3
Carl

私は an answera question を投稿しました。その後、私の質問がこの質問よりもこの質問に適していることに気付きました。以下に再現しました。しかし、私の答えは Bozho'sAnonの組み合わせに似ていることに気づきました's

他の質問には言語にとらわれないタグが付けられていたので、含めたコードサンプルにC#を選択しました。その相対的な使いやすさとわかりやすい構文、およびこのルーチンを容易にするいくつかの機能(BCLのDivRem関数、およびイテレーター関数のサポート)の組み込み、および私自身の慣れにより、この問題に適しています。ここのOPはJavaソリューションに興味がありますが、私は効果的にそれを書くのに十分なJava流暢ではないので、誰かがこのコードのJavaへの翻訳を追加できればいいかもしれません。


ここでの数学的解のいくつかは非常に優れています。ここに簡単な技術的な解決策があります。

より大きなデータ型を使用してください。これは2つの可能性に分解されます。

  1. 高精度の浮動小数点ライブラリを使用します。 10億個の数値を平均化する必要に遭遇した人は、128ビット(またはそれ以上)の浮動小数点ライブラリを購入するためのリソース、または書き込むための頭脳力をおそらく持っています。

    私はここで欠点を理解しています。組み込み型を使用するよりも確かに遅くなります。値の数が多すぎる場合でも、オーバーフローまたはアンダーフローする可能性があります。やだやだ。

  2. 値が整数の場合、または整数に簡単にスケーリングできる場合は、整数のリストに合計を入れてください。オーバーフローしたら、単に別の整数を追加します。これは基本的に、最初のオプションの実装を単純化したものです。シンプルな (未試験) C#の例は次のとおりです

class BigMeanSet{
    List<uint> list = new List<uint>();

    public double GetAverage(IEnumerable<uint> values){
        list.Clear();
        list.Add(0);

        uint count = 0;

        foreach(uint value in values){
            Add(0, value);
            count++;
        }

        return DivideBy(count);
    }

    void Add(int listIndex, uint value){
        if((list[listIndex] += value) < value){ // then overflow has ocurred
            if(list.Count == listIndex + 1)
                list.Add(0);
            Add(listIndex + 1, 1);
        }
    }

    double DivideBy(uint count){
        const double shift = 4.0 * 1024 * 1024 * 1024;

        double rtn       = 0;
        long   remainder = 0;

        for(int i = list.Count - 1; i >= 0; i--){
            rtn *= shift;
            remainder <<= 32;
            rtn += Math.DivRem(remainder + list[i], count, out remainder);
        }

        rtn += remainder / (double)count;

        return rtn;
    }
}

私が言ったように、これはテストされていない-私が本当に平均化したい10億の値を持っていない-そのため、特にDivideBy関数でおそらく1つか2つ間違えたかもしれませんが、考え。

これは、doubleが表すことができるのと同じくらいの精度を提供し、最大2つの32ビット要素の任意の数で機能するはずです。32 -1.さらに要素が必要な場合は、count変数を展開する必要があり、DivideBy関数が複雑になりますが、読者のための演習として残しておきます。

効率に関しては、リストの他の手法を1回だけ繰り返す必要があり、1つの除算演算(まあ、それらの1つのセット)のみを実行し、ほとんどの作業を整数で行うため、ここでは他のどの手法よりも高速または高速である必要があります。 。私はそれを最適化しなかった、と私はそれが必要ならばまだ少し速くすることができるとかなり確信している。再帰的な関数呼び出しとリストのインデックス作成を廃止することから始めるとよいでしょう。繰り返しますが、読者のための演習。コードは理解しやすいように意図されています。

現在私よりもやる気のある人がコードの正しさを確認し、問題があれば修正したい場合は、ゲストになってください。


私はこのコードをテストし、いくつかの小さな修正を行いました(List<uint>コンストラクター呼び出しで括弧のペアが欠落しており、DivideBy関数の最終除算で不正な除数)。

最初に、ランダムな整数(0から2の範囲)で満たされた1000セットのランダムな長さ(1から1000の範囲)を実行してテストしました32 -1)。これらは、標準的な平均を実行することで簡単かつ迅速に精度を検証できるセットでした。

次に100でテストしました* 10の間のランダムな長さの大きなシリーズ5 と109。これらのシリーズの下限と上限もランダムに選択され、シリーズが32ビット整数の範囲内に収まるように制約されました。どのシリーズでも、結果は(lowerbound + upperbound) / 2として簡単に検証できます。

*さて、それは少し白い嘘です。約20または30回の実行に成功した後、大規模テストを中止しました。一連の長さ109 私のマシンで実行するのに1分半弱かかるので、このルーチンをテストするのに30分ほどで十分でした。

興味のある方のために、私のテストコードは以下です:

static IEnumerable<uint> GetSeries(uint lowerbound, uint upperbound){
    for(uint i = lowerbound; i <= upperbound; i++)
        yield return i;
}

static void Test(){
    Console.BufferHeight = 1200;
    Random rnd = new Random();

    for(int i = 0; i < 1000; i++){
        uint[] numbers = new uint[rnd.Next(1, 1000)];
        for(int j = 0; j < numbers.Length; j++)
            numbers[j] = (uint)rnd.Next();

        double sum = 0;
        foreach(uint n in numbers)
            sum += n;

        double avg = sum / numbers.Length;
        double ans = new BigMeanSet().GetAverage(numbers);

        Console.WriteLine("{0}: {1} - {2} = {3}", numbers.Length, avg, ans, avg - ans);

        if(avg != ans)
            Debugger.Break();
    }

    for(int i = 0; i < 100; i++){
        uint length     = (uint)rnd.Next(100000, 1000000001);
        uint lowerbound = (uint)rnd.Next(int.MaxValue - (int)length);
        uint upperbound = lowerbound + length;

        double avg = ((double)lowerbound + upperbound) / 2;
        double ans = new BigMeanSet().GetAverage(GetSeries(lowerbound, upperbound));

        Console.WriteLine("{0}: {1} - {2} = {3}", length, avg, ans, avg - ans);

        if(avg != ans)
            Debugger.Break();
    }
}
2
P Daddy

完全なデータセットの小さなセットをランダムにサンプリングすると、多くの場合「十分に良い」ソリューションになります。システム要件に基づいて、この判断を自分で行う必要があることは明らかです。サンプルサイズは非常に小さくても、かなり良い回答が得られます。これは、ランダムに選択されたサンプルの増加する数の平均を計算することにより、適応的に計算できます-平均は、一定の間隔内で収束します。

サンプリングは、二重オーバーフローの問題に対処するだけでなく、はるかに高速です。すべての問題に適用できるわけではありませんが、多くの問題に役立ちます。

2
Kevin Day

このことを考慮:

avg(n1)         : n1                               = a1
avg(n1, n2)     : ((1/2)*n1)+((1/2)*n2)            = ((1/2)*a1)+((1/2)*n2) = a2
avg(n1, n2, n3) : ((1/3)*n1)+((1/3)*n2)+((1/3)*n3) = ((2/3)*a2)+((1/3)*n3) = a3

したがって、任意のサイズのdoubleのセットの場合、これを行うことができます(これはC#で行われますが、Javaに簡単に変換できると確信しています)。

static double GetAverage(IEnumerable<double> values) {
    int i = 0;
    double avg = 0.0;
    foreach (double value in values) {
        avg = (((double)i / (double)(i + 1)) * avg) + ((1.0 / (double)(i + 1)) * value);
        i++;
    }

    return avg;
}

実際、これは次のように簡単に簡略化されます(すでにmartinusによって提供されています)。

static double GetAverage(IEnumerable<double> values) {
    int i = 1;
    double avg = 0.0;
    foreach (double value in values) {
        avg += (value - avg) / (i++);
    }

    return avg;
}

私は、値を合計してカウント(GetAverage_old)。私の入力のために、私は必要なだけランダムな正のdoubleを返すようにこのクイック関数を書きました:

static IEnumerable<double> GetRandomDoubles(long numValues, double maxValue, int seed) {
    Random r = new Random(seed);
    for (long i = 0L; i < numValues; i++)
        yield return r.NextDouble() * maxValue;

    yield break;
}

そして、ここにいくつかのテストトライアルの結果があります:

long N = 100L;
double max = double.MaxValue * 0.01;

IEnumerable<double> doubles = GetRandomDoubles(N, max, 0);
double oldWay = GetAverage_old(doubles); // 1.00535024998431E+306
double newWay = GetAverage(doubles); // 1.00535024998431E+306

doubles = GetRandomDoubles(N, max, 1);
oldWay = GetAverage_old(doubles); // 8.75142021696299E+305
newWay = GetAverage(doubles); // 8.75142021696299E+305

doubles = GetRandomDoubles(N, max, 2);
oldWay = GetAverage_old(doubles); // 8.70772312848651E+305
newWay = GetAverage(doubles); // 8.70772312848651E+305

では、10 ^ 9の値はどうでしょうか?

long N = 1000000000;
double max = 100.0; // we start small, to verify accuracy

IEnumerable<double> doubles = GetRandomDoubles(N, max, 0);
double oldWay = GetAverage_old(doubles); // 49.9994879713857
double newWay = GetAverage(doubles); // 49.9994879713868 -- pretty close

max = double.MaxValue * 0.001; // now let's try something enormous

doubles = GetRandomDoubles(N, max, 0);
oldWay = GetAverage_old(doubles); // Infinity
newWay = GetAverage(doubles); // 8.98837362725198E+305 -- no overflow

当然のことながら、このソリューションの許容度は、精度要件によって異なります。しかし、検討する価値があります。

1
Dan Tao

ロジックをシンプルに保ち、パフォーマンスを最良ではないが許容できるものに保つために、プリミティブ型と一緒にBigDecimalを使用することをお勧めします。概念は非常に単純です。プリミティブ型を使用して値を合計し、値がアンダーフローまたはオーバーフローするたびに、計算値をBigDecimalに移動し、次の合計計算のためにリセットします。もう1つ注意する必要があるのは、BigDecimalを構築するときは、常にdoubleではなくStringを使用する必要があることです。

BigDecimal average(double[] values){
    BigDecimal totalSum = BigDecimal.ZERO;
    double tempSum = 0.00;
    for (double value : values){
        if (isOutOfRange(tempSum, value)) {
            totalSum = sum(totalSum, tempSum);
            tempSum = 0.00;
        }
        tempSum += value;
    }
    totalSum = sum(totalSum, tempSum);
    BigDecimal count = new BigDecimal(values.length);
    return totalSum.divide(count);
}

BigDecimal sum(BigDecimal val1, double val2){
    BigDecimal val = new BigDecimal(String.valueOf(val2));
    return val1.add(val);
}

boolean isOutOfRange(double sum, double value){
    // because sum + value > max will be error if both sum and value are positive
    // so I adapt the equation to be value > max - sum 
    if(sum >= 0.00 && value > Double.MAX - sum){
        return true;
    }

    // because sum + value < min will be error if both sum and value are negative
    // so I adapt the equation to be value < min - sum
    if(sum < 0.00 && value < Double.MIN - sum){
        return true;
    }
    return false;
}

この概念から、結果がアンダーフローまたはオーバーフローになるたびに、その値をより大きな変数に保持します。このソリューションでは、BigDecimal計算のためにパフォーマンスが少し低下する可能性がありますが、実行時の安定性は保証されます。

0
Toomtarm Kung

累積移動平均 のセクションを確認してください

0
basszero