rand()%6がバイアスされているのはなぜですか?


109

std :: randの使用方法を読んでいるときに、このコードをcppreference.comで見つけました

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

右の表現の何が問題になっていますか?試してみたところ、完全に機能しました。


24
サイコロに使用する方が良いことに注意してくださいstd::uniform_int_distribution
Caleth

1
@Calethはい、このコードが「間違っている」理由を理解するだけでした..
yO_

15
「間違っている」を「偏っている」に変更
Cubbi 2018

3
rand()通常の実装では非常に悪いので、xkcd RNGを使用することもできます。を使用しているため、間違っていますrand()
CodesInChaos 2018

3
私はこれを書いて(まあ、コメントではなく、それは@Cubbiです)、当時考えていたのは、ピートベッカーの回答で説明されていたものです。(参考までに、これは基本的にlibstdc ++のアルゴリズムと同じですuniform_int_distribution。)
TC

回答:


136

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

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

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

極端な例として、rand()範囲内に均一に分散された値を生成するふりをします[0..6]。これらの値の余りを見ると、rand()が範囲内の値を返す場合[0..5]、余りは範囲内に均一に分散された結果を生成します[0..5]。ときにrand()戻って6、rand() % 6ちょうどかのように0を返し、rand()あなたが他の値など、多くの0の二倍と分布を得るので0を返していました。

2つ目は、の実際の問題rand() % 6です。

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

そう:

int max = 6 * ((RAND_MAX + 1u) / 6)
int value = rand();
while (value >= max)
    value = rand();

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


2
私はこのサイトの少なくとも1人の常連にこれについての論文を作成することを約束しましたが、サンプリングと拒否大きな瞬間スローする可能性があると思います。たとえば、分散を過剰に膨らませます。
バトシェバ2018

30
rand_maxが32768である場合に、この手法がもたらすバイアスのグラフを作成しました。ericlippert.com/2013/12/16/...
エリックリペット

2
@Bathsheba:一部の拒否関数がこれを引き起こす可能性があることは事実ですが、この単純な拒否は、均一IIDを別の均一IID分布に変換します。どのビットもキャリーオーバーせず、独立しているため、すべてのサンプルで同じ除去を使用するため、同一であり、均一性を示すのは簡単です。そして、一様積分確率変数の高いモーメントは、その範囲によって完全に定義されます。
MSalters

4
@MSalters:最初の文は、trueジェネレーターでは正しいですが、疑似ジェネレーターでは必ずしもtrueではありません。私が引退したら、これについて論文を書きます。
バトシェバ2018

2
@Anthonyサイコロの観点から考えます。1と3の間の乱数が必要で、標準の6面サイコロしかありません。4-6を出したら3を引くだけで得られます。しかし、代わりに1〜5の数値が必要だとしましょう。6を振ったときに5を引くと、他の数値の2倍の数の1になります。これが基本的にcppreferenceコードが行っていることです。正しいことは、6を再ロールすることです。それがピートがここでやっていることです。各数字を振る方法と同じ数の方法があるようにサイコロを分割し、偶数の除算に収まらなかったすべての数字を再ロールします
Ray

19

ここには奥行きがあります:

  1. 小型の使用uの中でRAND_MAX + 1uRAND_MAXintタイプとして定義され、多くの場合、最大の可能性がありintます。挙動はRAND_MAX + 1されるだろう未定義あなたがあふれているはずだとして、そのような場合にsignedタイプを。書き込み1uRAND_MAXtoの型変換を強制するunsignedので、オーバーフローを防ぐことができます。

  2. % 6 canの使用(ただし、std::rand私が見たすべての実装ではそうではあり ません)は、提示された代替案を超えて、さらに統計的バイアスをもたらします。このような% 6危険な例は、数生成器が低位ビットに相関プレーンを持っている場合です。たとえばrand、1970年代の有名なIBMの実装(C言語で)のように、高位ビットと低位ビットを「最後の繁栄」。さらに考慮すべき点は、6は非常に小さいということです。RAND_MAXなので、がRAND_MAX6の倍数でない場合、最小限の効果しか得られません。

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


12
% 6によって生成された個別の値の数rand()が6の倍数でない場合は常に、偏った結果が生成されます。鳩の穴の原理。確かに、バイアスがRAND_MAX6よりはるかに大きい場合、バイアスは小さくなりますが、それはあります。そして、ターゲット範囲が大きいほど、効果はもちろん大きくなります。
ピートベッカー

2
@PeteBecker:確かに、私はそれを明確にすべきです。ただし、整数除算の切り捨て効果により、サンプル範囲がRAND_MAXに近づくと、ハト穴も発生することに注意してください。
バトシェバ2018

2
@Bathshebaは、切り捨ての結果が6より大きい結果になり、操作全体が繰り返し実行されることになりませんか?
ゲルハルト

1
@ゲルハルト:正解。実際、それはまさに結果につながりx==7ます。基本的に、範囲[0, RAND_MAX]を7つのサブ範囲に分割します。6 つは同じサイズで、最後に1つの小さい範囲があります。最後の部分範囲からの結果は破棄されます。このように最後に2つの小さなサブ範囲を設定できないことは明らかです。
MSalters 2018

@MSalters:確かに。ただし、切り捨てが原因で、他の方法でも依然として問題があることに注意してください。私の仮説は、統計的な落とし穴を理解するのが難しいので、民俗は後者のためにふくよかだということです!
バトシェバ2018

13

このサンプルコードstd::randは、これが従来のカーゴカルトバルダーダッシュのケースであることを示しています。

ここにはいくつかの問題があります:

契約の人々は通常、貧しい哀れな魂が少しでもよく知らない人でも、想定すると、正確にこれらの中では考えられないだろうという用語-あるrandからサンプル均一な分布 0の整数で、1、2、...、 RAND_MAX、各呼び出しは独立したサンプルを生成ます。

最初の問題は、想定される契約、各呼び出しでの独立した一様なランダムサンプルは、実際にはドキュメントに記載されているものではないことです。実際、実装では、歴史上、最も複雑な独立性さえ提供できませんでした。 たとえば、C99§7.20.2.1 'The randfunction'は、詳しく説明していません。

このrand関数は、0〜の範囲の一連の擬似ランダム整数を計算しRAND_MAXます。

疑似ランダム性は整数ではなく関数(または関数のファミリ)のプロパティであるため、これは意味のない文ですが、ISOの官僚でさえ言語を悪用することを妨げるものではありません。結局のところ、それによって動揺する唯一の読者randは、彼らの脳細胞の腐敗を恐れてドキュメントを読むよりもよく知っています。

Cの典型的な歴史的実装は次のように機能します。

static unsigned int seed = 1;

static void
srand(unsigned int s)
{
    seed = s;
}

static unsigned int
rand(void)
{
    seed = (seed*1103515245 + 12345) % ((unsigned long)RAND_MAX + 1);
    return (int)seed;
}

これには不幸な特性があり、単一のサンプルが均一なランダムシード(の特定の値に依存しますRAND_MAX)の下で均一に分布している場合でも、連続した呼び出しで偶数と奇数の整数が交互に繰り返されます。

int a = rand();
int b = rand();

この式(a & 1) ^ (b & 1)は100%の確率で1を生成します。これは、偶数および奇数の整数でサポートされる分布の独立したランダムサンプルの場合とは異なります。このように、「より良いランダム性」というとらえどころのない獣を追いかけるために、下位ビットを破棄するべきであるというカーゴカルトが現れました。(ネタバレ注意:これは技術用語ではありません。これは、あなたが読んでいる散文の誰もが彼らが何を話しているのかわからないか、あなたが無知であると思い込んでいる必要があることを示しています。)

2番目の問題は、各呼び出しが 0、1、2、…の一様なランダム分布から独立してサンプリングしたとしてもRAND_MAX、結果はrand() % 6サイコロのように0、1、2、3、4、5に一様に分布しないことです。RAND_MAX6を法とする-1に一致しない限り、ロールします。 単純な反例:RAND_MAX= 6の場合、からrand()、すべての結果の確率は1/7になりますが、からrand() % 6は、結果0の確率は2/7になり、他のすべての結果の確率は1/7になります。 。

これを行う正しい方法は、拒否サンプリングを使用 することです。0、1、2 、…、から独立した一様なランダムサンプルを繰り返し描画し、(たとえば)結果0、1、2、sRAND_MAX拒否し((RAND_MAX + 1) % 6) - 1ます。それら、最初からやり直してください。そうでなければ、yield s % 6

unsigned int s;
while ((s = rand()) < ((unsigned long)RAND_MAX + 1) % 6)
    continue;
return s % 6;

このように、rand()私たちが受け入れる結果のセットは6で割り切れるs % 6数になり、からの可能な結果はそれぞれ、から受け入れた同じ数の結果によって得られるrand()ため、rand()均一に分散されている場合はになりsます。試行回数に制限はありませんが、予想される回数は2未満であり、試行回数に応じて成功の確率が指数関数的に増加します。

選択の成果あなたはcppreference.comでコードが作るあなたが6以下の各整数にそれらの同じ数をマッピングすることを提供し、重要ではない拒絶異なる何もについて保証されていないことを先にあるため、最初の問題のため、選択をの出力の分布または独立性、および実際には下位ビットは、「十分にランダムに見えない」パターンを示しました(次の出力が前の出力の決定的な関数であることを気にしないでください)。rand()rand()

読者のための演習:cppreference.comのコードが0、1、2 rand()、…で均一な分布を生成する場合、サイコロで均一な分布を生成することを証明しますRAND_MAX

読者のための演習:なぜ1つまたは他のサブセットを拒否したいのですか?2つのケースで各試行に必要な計算は何ですか?

3番目の問題は、シードスペースが非常に小さいため、シードが均一に分散されている場合でも、プログラムの知識と1つの結果ではあるが、シードではない敵がシードと後続の結果を容易に予測できるため、シードがそうではないように見えることです。結局ランダム。 したがって、これを暗号化に使用することさえ考えないでください。

std::uniform_int_distribution適切なランダムデバイスと人気のあるメルセンヌツイスターのようなお気に入りのランダムエンジンを使って、豪華なオーバーエンジニアリングルートとC ++ 11のクラスに行き、std::mt199374歳のいとことサイコロで遊ぶことができますが、それもできません暗号鍵の素材を生成するのに適しています。また、Mersenneツイスターも、数キロバイトの状態がCPUのキャッシュにひどいセットアップ時間をもたらし、ひどいスペースを食い尽くすので、たとえば、並列モンテカルロシミュレーションの場合でも悪いです。サブコンピューティングの再現可能なツリー; その人気はおそらくそのキャッチーな名前から主に発生します。しかし、この例のようにサイコロを転がすおもちゃに使用できます!

別のアプローチは、単純な高速鍵消去PRNGなどの小さな状態の単純な暗号化擬似乱数ジェネレーターを使用するか、自信がある場合は(たとえば、モンテカルロシミュレーションで)AES-CTRまたはChaCha20などのストリーム暗号を使用することです自然科学の研究)、国家がこれまでに妥協された場合、過去の結果を予測することに悪影響はありません。


4
「わいせつなセットアップ時間」とにかく(スレッドごとに)複数の乱数ジェネレータを使用するべきではないので、プログラムがあまり長く実行されない限り、セットアップ時間は償却されます。
JAB

2
問題のループがまったく同じ(RAND_MAX + 1 )% 6値のまったく同じ拒否サンプリングを行っていることを理解していないため、BTWに反対票を投じてください。可能な結果をどのように細分するは関係ありません。[0, RAND_MAX)受け入れられる範囲のサイズが6の倍数である限り、範囲内の任意の場所からそれらを拒否できます。地獄、どんな結果も完全x>6に拒否でき、%6もう必要ありません。
MSalters

12
私はこの答えに満足していません。暴言は良いことですが、間違った方向に進んでいます。たとえば、「より良いランダム性」は専門用語ではなく、意味がないと不平を言います。これは半分真実です。はい、それは専門用語ではありませんが、コンテキストで完全に意味のある省略表現です。そのような用語のユーザーが無知または悪意があることをほのめかすことは、それ自体、これらの事柄の1つです。「良好なランダム性」を正確に定義することは非常に難しいかもしれませんが、関数がランダム特性の良いまたは悪い結果を生成する時期を把握するのは簡単です。
Konrad Rudolph

3
私はこの答えが好きだった。それは少し怒りですが、それは良い背景情報がたくさんあります。心に留めておいてください。REALの専門家はハードウェアランダムジェネレーターのみを使用しますが、問題はそれだけ難しいことです。
Tiger4Hire 2018

10
私にとってそれは逆です。それは良い情報を含んでいますが、意見以外のものとして出くわすのはあまりにも大げさです。有用性はさておき。
リスター氏

2

私は決して経験豊富な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] << ' ';
    }
}

次に、これの出力を取得し、chisq.testR の関数を使用してカイ二乗検定を実行し、結果が予想と大幅に異なるかどうかを確認しました。このstackexchangeの質問では、カイ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回しか実行しなかったことです。

更新:私が回答を書いているときに、コンラッドルドルフが同じアプローチをとった回答を投稿しましたが、結果は大きく異なります。私は彼の答えについてコメントする評判がないので、ここで取り上げます。まず、主なことは、彼が使用するコードは、実行されるたびに乱数生成器に同じシードを使用することです。シードを変更すると、実際にはさまざまな結果が得られます。次に、シードを変更せずに試行回数を変更すると、さまざまな結果も得られます。1桁ずつ増減してみて、私の意味を確認してください。第3に、予期される値が正確でない場合に整数の切り捨てまたは丸めが行われます。それはおそらく違いを生むには十分ではありませんが、それはあります。

基本的に、要約すると、彼はたまたま自分が誤った結果を得る可能性のある正しいシードと試行回数を得ました。


引用された通路がされて:あなたの実装では、あなたの部分の誤解に起因する致命的な欠陥が含まれていない比較rand()%6ではrand()/(1+RAND_MAX)/6。むしろ、それは、剰余の単純な取得と拒否のサンプリングを比較しています(説明については他の回答を参照してください)。その結果、2番目のコードは間違っていwhileます(ループは何もしません)。統計テストにも問題があります(ロバスト性のためにテストを繰り返し実行するだけでなく、修正を実行しなかったなど)。
Konrad Rudolph

1
@KonradRudolphあなたの回答についてコメントする担当者がいないので、私のアップデートとして追加しました。また、実行ごとに設定されたシードと試行回数を使用して、誤った結果をもたらすという致命的な欠陥もあります。異なるシードで繰り返し実行した場合、それをキャッチした可能性があります。しかし、そうです、正しいwhileループは何もしませんが、その特定のコードブロックの結果も変更しません
anjama

私は実際にリピートを実行しました。シードをランダムにstd::srand(およびを使用せずに<random>)設定することは、標準に準拠する方法では非常に困難であり、その複雑さが残りのコードを損なうことを望まなかったため、意図的にシードを設定していません。また、計算には無関係です。シミュレーションで同じシーケンスを繰り返すことは完全に許容されます。もちろん、シード異なると結果も異なり、重要でないものもあります。これは、p値の定義方法に基づいて完全に予測されます。
Konrad Rudolph

1
ねえ、私は私の繰り返しでミスをしました。そして、あなたが正しい、繰り返し実行の95分位数はp = 0.05に非常に近いです。要するに、私の標準ライブラリの実装はstd::rand、ランダムシードの範囲全体で、d6の非常に優れたコイン投げシミュレーションをもたらします。
Konrad Rudolph

1
統計的有意性は物語の一部にすぎません。帰無仮説(均一に分布)と代替仮説(モジュロバイアス)があります。実際には、の選択によってインデックス付けされた代替仮説のファミリーであり、モジュロバイアスの効果サイズRAND_MAXを決定します。統計的有意性は、帰無仮説の下で誤って拒否する確率です。統計的検出力とは何ですか— 対立仮説のもとで、テストが帰無仮説を正しく拒否する確率は?rand() % 6RAND_MAX = 2 ^ 31-1のときにこの方法で検出しますか?
きしむしオッシフラゲ

2

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

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は、最低の品質で最速の速度を提供します。暗号ジェネレーターは、最低の速度で最高の品質を提供します。

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