web-dev-qa-db-ja.com

Rand()%6が偏っているのはなぜですか?

Std :: Randの使い方を読むとき、このコードは cppreference.com にあります。

int x = 7;
while(x > 6) 
    x = 1 + std::Rand()/((Rand_MAX + 1u)/6);  // Note: 1+Rand()%6 is biased

右側の表現の何が問題になっていますか試してみて、それは完璧に動作します。

105
yO_

Rand() % 6には2つの問題があります(1+はどちらの問題にも影響しません)。

最初に、いくつかの回答が指摘したように、Rand()の下位ビットが適切に一様でない場合、剰余演算子の結果も一様ではありません。

第二に、Rand()によって生成された個別の値の数が6の倍数ではない場合、残りは高い値よりも低い値を生成します。たとえRand()が完全に分散された値を返したとしても同じです。

極端な例として、Rand()[0..6]の範囲の一様に分布した値を生成するとしましょう。これらの値の余りを見てみると、Rand()[0..5]の範囲の値を返すとき、残りは[0..5]の範囲の一様分布の結果を生成します。 Rand()が6を返すとき、Rand() % 6が0を返すように、Rand()は0を返します。したがって、0の2倍の分布を得ることができます。

二つ目はRand() % 6本当の問題です。

この問題を回避する方法は、値を破棄して、不均一な重複を生成することです。 Rand_MAX以下の最大の6の倍数を計算し、Rand()がその倍数以上の値を返すときはいつでもそれを拒否し、必要な回数だけ再度Rand()を呼び出します。

そう:

int max = 6 * ((Rand_MAX + 1u) / 6)
int value = Rand();
while (value >= max)
    value = Rand();

これは問題のコードの異なる実装で、何が起こっているのかをより明確に示すことを目的としています。

136
Pete Becker

ここに隠された深さがあります。

  1. Rand_MAX + 1uで小さいuを使用しています。 Rand_MAXint型として定義されており、多くの場合、可能な限り最大のintです。あなたがsigned型をオーバーフローさせるような場合には、Rand_MAX + 1の振る舞いは未定義になります。 1uを書くと、Rand_MAXからunsignedへの型変換が強制されるため、オーバーフローは回避されます。

  2. % 6の使用はを可能にします(しかし私が見たstd::Randのすべての実装ではそうではありません)提示された代替方法の上にそしてそれを超えて追加の統計的バイアスを導入する。 % 6が危険であるそのような事例は、Randのかなり有名なIBMの実装(C言語での)のように、数値ジェネレータが下位ビットに相関プレーンを持っているケースです。最後の繁栄 "。さらなる考慮は、6が非常に小さいということです。 Rand_MAXなので、Rand_MAXが6の倍数でない場合は最小限の効果しかありませんが、おそらくそうではありません。

結論として、最近では、その扱いやすさのために、私は% 6を使います。ジェネレータ自体によってもたらされるものを超えて統計的な異常をもたらすことはありそうもありません。それでも疑問がある場合は、ジェネレータをテストして、ユースケースに適した統計的性質があるかどうかを確認します。

19
Bathsheba

私は経験豊富なC++ユーザーではありませんが、std::Rand()/((Rand_MAX + 1u)/6)1+std::Rand()%6より偏っていないという他の答えが実際に当てはまるかどうかに興味を持っていました。それで私は両方の方法の結果を表にするためにテストプログラムを書きました(私はC++を時代の中で書いていません、それをチェックしてください)。コードを実行するためのリンクはこちら にあります 。次のようにも再現されています。

// Example program
#include <cstdlib>
#include <iostream>
#include <ctime>
#include <string>

int main()
{
    std::srand(std::time(nullptr)); // use current time as seed for random generator

    // Roll the die 6000000 times using the supposedly unbiased method and keep track of the results

    int results[6] = {0,0,0,0,0,0};

    // roll a 6-sided die 20 times
    for (int n=0; n != 6000000; ++n) {
        int x = 7;
        while(x > 6) 
            x = 1 + std::Rand()/((Rand_MAX + 1u)/6);  // Note: 1+Rand()%6 is biased

        results[x-1]++;
    }

    for (int n=0; n !=6; n++) {
        std::cout << results[n] << ' ';
    }

    std::cout << "\n";


    // Roll the die 6000000 times using the supposedly biased method and keep track of the results

    int results_bias[6] = {0,0,0,0,0,0};

    // roll a 6-sided die 20 times
    for (int n=0; n != 6000000; ++n) {
        int x = 7;
        while(x > 6) 
            x = 1 + std::Rand()%6;

        results_bias[x-1]++;
    }

    for (int n=0; n !=6; n++) {
        std::cout << results_bias[n] << ' ';
    }
}

次に、この結果を出力し、Rのchisq.test関数を使用してカイ二乗検定を実行し、結果が予想と著しく異なるかどうかを確認します。このスタック交換の質問では、カイ2乗検定を使用してダイの公平性をテストする方法について詳しく説明します。 ダイが正しいかどうかをテストするにはどうすればよいですか? 。これがいくつかの実行の結果です。

> ?chisq.test
> unbias <- c(100150, 99658, 100319, 99342, 100418, 100113)
> bias <- c(100049, 100040, 100091, 99966, 100188, 99666 )

> chisq.test(unbias)

Chi-squared test for given probabilities

data:  unbias
X-squared = 8.6168, df = 5, p-value = 0.1254

> chisq.test(bias)

Chi-squared test for given probabilities

data:  bias
X-squared = 1.6034, df = 5, p-value = 0.9008

> unbias <- c(998630, 1001188, 998932, 1001048, 1000968, 999234 )
> bias <- c(1000071, 1000910, 999078, 1000080, 998786, 1001075   )
> chisq.test(unbias)

Chi-squared test for given probabilities

data:  unbias
X-squared = 7.051, df = 5, p-value = 0.2169

> chisq.test(bias)

Chi-squared test for given probabilities

data:  bias
X-squared = 4.319, df = 5, p-value = 0.5045

> unbias <- c(998630, 999010, 1000736, 999142, 1000631, 1001851)
> bias <- c(999803, 998651, 1000639, 1000735, 1000064,1000108)
> chisq.test(unbias)

Chi-squared test for given probabilities

data:  unbias
X-squared = 7.9592, df = 5, p-value = 0.1585

> chisq.test(bias)

Chi-squared test for given probabilities

data:  bias
X-squared = 2.8229, df = 5, p-value = 0.7273

私が行った3回の実行では、両方の方法のp値は、有意性を検定するために使用された典型的なアルファ値(0.05)よりも常に大きかった。つまり、どちらも偏っているとは考えられません。興味深いことに、偏りのないとされる方法は一貫して低いp値を持ち、これは実際にはより偏っている可能性があることを示しています。警告は私がたった3回走ったということです。

更新:私が私の答えを書いている間に、Konrad Rudolphは同じアプローチをとるが非常に異なった結果を得る答えを投稿しました。私は彼の答えについてコメントする評判がないので、ここでそれに取り組むつもりです。まず第一に、彼が使用するコードは実行されるたびに乱数ジェネレータに同じシードを使用するということです。種を変えると、実際にはさまざまな結果が得られます。次に、種を変えずに試行回数を変えれば、さまざまな結果が得られます。私が何を意味するのか見るためには、桁の大きさを増減してみてください。第三に、期待値がそれほど正確ではない場合には、整数の切り捨てまたは四捨五入が行われます。違いを生むにはおそらく十分ではありませんが、それだけです。

基本的に、要約すれば、彼はちょうど間違った結果を得ているかもしれないという正しい種と試行回数を得ただけでした。

2
anjama

乱数発生器は、2進数のストリームを処理するものと考えることができます。ジェネレータは、それをチャンクにスライスすることによってストリームを数値に変換します。 std:Rand関数が32767のRand_MAXで動作している場合、各スライスで15ビットを使用しています。

0から32767までの数のモジュールを取ると、5462の0と1が、5461の2、3、4、および5のモジュールしかないことがわかります。したがって、結果は偏っています。 Rand_MAX値が大きいほど、偏りは少なくなりますが避けられません。

偏っていないのは[0 ..(2 ^ n)-1]の範囲の数です。 3つのビットを抽出し、それらを0..7の範囲の整数に変換し、6と7を棄却することで、(理論的に)0..5の範囲のより良い数を生成できます。

ビットストリーム内のすべてのビットが、ストリーム内のどこにあるか、または他のビットの値に関係なく、「0」または「1」になる可能性が等しいことを願います。これは実際には非常に困難です。ソフトウェアPRNGのさまざまな実装では、速度と品質の間でさまざまな妥協点があります。 std::Randのような線形合同生成器は、最低の品質を実現するために最速の速度を提供します。暗号化ジェネレータは最低速度で最高品質を提供します。

2
Simon G.