std :: randの使用方法を読んでいるときに、このコードをcppreference.comで見つけました
int x = 7;
while(x > 6)
x = 1 + std::rand()/((RAND_MAX + 1u)/6); // Note: 1+rand()%6 is biased
右の表現の何が問題になっていますか?試してみたところ、完全に機能しました。
uniform_int_distribution
。)
std :: randの使用方法を読んでいるときに、このコードをcppreference.comで見つけました
int x = 7;
while(x > 6)
x = 1 + std::rand()/((RAND_MAX + 1u)/6); // Note: 1+rand()%6 is biased
右の表現の何が問題になっていますか?試してみたところ、完全に機能しました。
uniform_int_distribution
。)
回答:
には2つの問題がありますrand() % 6
(1+
どちらの問題にも影響しません)。
まず、いくつかの回答が指摘しているように、の下位ビット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();
これは問題のコードの別の実装であり、何が起こっているかをより明確に示すことを目的としています。
ここには奥行きがあります:
小型の使用u
の中でRAND_MAX + 1u
。RAND_MAX
はint
タイプとして定義され、多くの場合、最大の可能性がありint
ます。挙動はRAND_MAX + 1
されるだろう未定義あなたがあふれているはずだとして、そのような場合にsigned
タイプを。書き込み1u
はRAND_MAX
toの型変換を強制するunsigned
ので、オーバーフローを防ぐことができます。
% 6
canの使用(ただし、std::rand
私が見たすべての実装ではそうではあり ません)は、提示された代替案を超えて、さらに統計的バイアスをもたらします。このような% 6
危険な例は、数生成器が低位ビットに相関プレーンを持っている場合です。たとえばrand
、1970年代の有名なIBMの実装(C言語で)のように、高位ビットと低位ビットを「最後の繁栄」。さらに考慮すべき点は、6は非常に小さいということです。RAND_MAX
なので、がRAND_MAX
6の倍数でない場合、最小限の効果しか得られません。
結論として、最近では、その扱いやすさのため、を使用します% 6
。ジェネレータ自体によって導入されたものを超える統計異常をもたらす可能性はほとんどありません。それでも疑問がある場合は、ジェネレーターをテストして、ユースケースに適した統計特性があるかどうかを確認してください。
% 6
によって生成された個別の値の数rand()
が6の倍数でない場合は常に、偏った結果が生成されます。鳩の穴の原理。確かに、バイアスがRAND_MAX
6よりはるかに大きい場合、バイアスは小さくなりますが、それはあります。そして、ターゲット範囲が大きいほど、効果はもちろん大きくなります。
x==7
ます。基本的に、範囲[0, RAND_MAX]
を7つのサブ範囲に分割します。6 つは同じサイズで、最後に1つの小さい範囲があります。最後の部分範囲からの結果は破棄されます。このように最後に2つの小さなサブ範囲を設定できないことは明らかです。
このサンプルコードstd::rand
は、これが従来のカーゴカルトバルダーダッシュのケースであることを示しています。
ここにはいくつかの問題があります:
契約の人々は通常、貧しい哀れな魂が少しでもよく知らない人でも、想定すると、正確にこれらの中では考えられないだろうという用語-あるrand
からサンプル均一な分布 0の整数で、1、2、...、 RAND_MAX
、各呼び出しは独立したサンプルを生成します。
最初の問題は、想定される契約、各呼び出しでの独立した一様なランダムサンプルは、実際にはドキュメントに記載されているものではないことです。実際、実装では、歴史上、最も複雑な独立性さえ提供できませんでした。 たとえば、C99§7.20.2.1 'The rand
function'は、詳しく説明していません。
この
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_MAX
6を法とする-1に一致しない限り、ロールします。 単純な反例:RAND_MAX
= 6の場合、からrand()
、すべての結果の確率は1/7になりますが、からrand() % 6
は、結果0の確率は2/7になり、他のすべての結果の確率は1/7になります。 。
これを行う正しい方法は、拒否サンプリングを使用 することです。0、1、2 、…、から独立した一様なランダムサンプルを繰り返し描画し、(たとえば)結果0、1、2、s
… RAND_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::mt19937
4歳のいとことサイコロで遊ぶことができますが、それもできません暗号鍵の素材を生成するのに適しています。また、Mersenneツイスターも、数キロバイトの状態がCPUのキャッシュにひどいセットアップ時間をもたらし、ひどいスペースを食い尽くすので、たとえば、並列モンテカルロシミュレーションの場合でも悪いです。サブコンピューティングの再現可能なツリー; その人気はおそらくそのキャッチーな名前から主に発生します。しかし、この例のようにサイコロを転がすおもちゃに使用できます!
別のアプローチは、単純な高速鍵消去PRNGなどの小さな状態の単純な暗号化擬似乱数ジェネレーターを使用するか、自信がある場合は(たとえば、モンテカルロシミュレーションで)AES-CTRまたはChaCha20などのストリーム暗号を使用することです自然科学の研究)、国家がこれまでに妥協された場合、過去の結果を予測することに悪影響はありません。
(RAND_MAX + 1 )% 6
値のまったく同じ拒否サンプリングを行っていることを理解していないため、BTWに反対票を投じてください。可能な結果をどのように細分するかは関係ありません。[0, RAND_MAX)
受け入れられる範囲のサイズが6の倍数である限り、範囲内の任意の場所からそれらを拒否できます。地獄、どんな結果も完全x>6
に拒否でき、%6
もう必要ありません。
私は決して経験豊富な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.test
R の関数を使用してカイ二乗検定を実行し、結果が予想と大幅に異なるかどうかを確認しました。この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
ます(ループは何もしません)。統計テストにも問題があります(ロバスト性のためにテストを繰り返し実行するだけでなく、修正を実行しなかったなど)。
std::srand
(およびを使用せずに<random>
)設定することは、標準に準拠する方法では非常に困難であり、その複雑さが残りのコードを損なうことを望まなかったため、意図的にシードを設定していません。また、計算には無関係です。シミュレーションで同じシーケンスを繰り返すことは完全に許容されます。もちろん、シードが異なると結果も異なり、重要でないものもあります。これは、p値の定義方法に基づいて完全に予測されます。
std::rand
、ランダムシードの範囲全体で、d6の非常に優れたコイン投げシミュレーションをもたらします。
RAND_MAX
を決定します。統計的有意性は、帰無仮説の下で誤って拒否する確率です。統計的検出力とは何ですか— 対立仮説のもとで、テストが帰無仮説を正しく拒否する確率は?rand() % 6
RAND_MAX = 2 ^ 31-1のときにこの方法で検出しますか?
乱数ジェネレータは、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
は、最低の品質で最速の速度を提供します。暗号ジェネレーターは、最低の速度で最高の品質を提供します。
std::uniform_int_distribution