web-dev-qa-db-ja.com

なぜ乱数ジェネレーターを使用しているときにモジュロバイアスがあると人々が言うのですか?

私はこの質問をたくさん尋ねましたが、それに対する本当の具体的な答えを見たことはありません。そこで、C++のRand()のような乱数ジェネレーターを使用するときに「モジュロバイアス」が正確に存在する理由を人々が理解できるように、ここに投稿します。

266
user1413793

Rand()は、0とRand_MAXの間の自然数を選択する擬似乱数ジェネレーターです。これは、cstdlibで定義された定数です(詳細については、この article を参照してください) Rand())。

たとえば、0と2の間の乱数を生成する場合はどうなりますか?説明のために、Rand_MAXが10で、Rand()%3を呼び出して0から2の間の乱数を生成することにします。ただし、Rand()%3は、0〜2の数値を同じ確率で生成しません。

Rand()が0、3、6、または9を返す場合Rand()%3 == 0。したがって、P(0) = 4/11

Rand()が1、4、7、または10を返す場合Rand()%3 == 1。したがって、P(1) = 4/11

Rand()が2、5、または8を返す場合Rand()%3 == 2。したがって、P(2) = 3/11

これは、0〜2の数値を同じ確率で生成しません。もちろん、小さな範囲ではこれは最大の問題ではないかもしれませんが、大きな範囲ではこれは分布をゆがめ、小さな数字に偏らせる可能性があります。

それで、Rand()%nは等しい確率で0からn-1までの範囲の数値を返しますか? Rand_MAX%n == n - 1の場合。この場合、以前の仮定Rand()は0からRand_MAXの間の数値を等しい確率で返すので、nのモジュロクラスも等しく分散されます。

それでは、この問題をどのように解決しますか?おおまかな方法​​は、希望する範囲の数値を取得するまで乱数を生成し続けることです。

int x; 
do {
    x = Rand();
} while (x >= n);

ただし、nの値が低い場合は、範囲内で値を取得する機会がn/Rand_MAXしかないため、非効率的です。したがって、平均でRand()Rand_MAX/n呼び出す必要があります。 。

より効率的な式のアプローチは、Rand_MAX - Rand_MAX % nのようなnで割り切れる長さの大きな範囲を取り、範囲内にあるものを取得するまで乱数を生成し続け、次にモジュラスを取得することです。

int x;

do {
    x = Rand();
} while (x >= (Rand_MAX - Rand_MAX % n));

x %= n;

nの値が小さい場合、Rand()を複数回呼び出す必要はほとんどありません。


引用された作品とさらに読む:


376
user1413793

ランダムを選択し続けることは、偏りを取り除く良い方法です。

更新

nで割り切れる範囲のxを検索すると、コードを高速化できます。

// Assumptions
// Rand() in [0, Rand_MAX]
// n in (0, Rand_MAX]

int x; 

// Keep searching for an x in a range divisible by n 
do {
    x = Rand();
} while (x >= Rand_MAX - (Rand_MAX % n)) 

x %= n;

上記のループは非常に高速で、平均して1回の繰り返しが必要です。

36

@ user1413793は問題について正しいです。 1つ指摘する点を除いて、これ以上は説明しません。はい、nの小さな値とRand_MAXの大きな値の場合、モジュロバイアスは非常に小さくなります。ただし、バイアスを誘導するパターンを使用すると、乱数を計算するたびにバイアスを考慮し、さまざまな場合に異なるパターンを選択する必要があります。そして、あなたが間違った選択をした場合、それがもたらすバグは微妙であり、ユニットテストがほとんど不可能です。適切なツール(arc4random_uniformなど)を使用するだけの場合と比べて、それは余分な作業であり、それほど作業ではありません。より多くの作業を行い、より悪いソリューションを取得することは、特にほとんどのプラットフォームで毎回正しく実行することが簡単な場合、ひどいエンジニアリングです。

残念ながら、ソリューションの実装はすべて間違っているか、本来よりも効率が悪いです。 (各ソリューションには問題を説明するさまざまなコメントがありますが、解決するために修正されたソリューションはありません。)これは、カジュアルなアンサーシーカーを混乱させる可能性が高いため、ここで既知の適切な実装を提供します。

繰り返しになりますが、最善の解決策は、それを提供するプラットフォームで arc4random_uniform を使用するか、プラットフォームで同様の範囲のソリューション(Javaで Random.nextInt など)を使用することです。それはあなたにコード費用なしで正しいことをします。これはほとんどの場合、正しい呼び出しです。

arc4random_uniformがない場合は、オープンソースのパワーを使用して、より広い範囲のRNGの上に実装されている方法を正確に確認できます(この場合はar4randomですが、他のRNGの上でも同様のアプローチが機能します) 。

OpenBSDの実装 は次のとおりです。

/*
 * Calculate a uniformly distributed random number less than upper_bound
 * avoiding "modulo bias".
 *
 * Uniformity is achieved by generating new random numbers until the one
 * returned is outside the range [0, 2**32 % upper_bound).  This
 * guarantees the selected random number will be inside
 * [2**32 % upper_bound, 2**32) which maps back to [0, upper_bound)
 * after reduction modulo upper_bound.
 */
u_int32_t
arc4random_uniform(u_int32_t upper_bound)
{
    u_int32_t r, min;

    if (upper_bound < 2)
        return 0;

    /* 2**32 % x == (2**32 - x) % x */
    min = -upper_bound % upper_bound;

    /*
     * This could theoretically loop forever but each retry has
     * p > 0.5 (worst case, usually far better) of selecting a
     * number inside the range we need, so it should rarely need
     * to re-roll.
     */
    for (;;) {
        r = arc4random();
        if (r >= min)
            break;
    }

    return r % upper_bound;
}

同様のことを実装する必要がある人のために、このコードに関する最新のコミットコメントに注目する価値があります。

Arc4random_uniform()を変更して2**32 % upper_bound'' as -upper_bound%upper_boundを計算します」。コードを簡素化し、ILP32アーキテクチャとLP64アーキテクチャの両方で同じようにします。また、64ビットの剰余の代わりに32ビットの剰余を使用することにより、LP64アーキテクチャでわずかに高速化します。

Jorden Verwerがtech @ ok deraadtについて指摘しました。 djmやottoに異議はありません

Java実装も簡単に見つけることができます(前のリンクを参照)。

public int nextInt(int n) {
   if (n <= 0)
     throw new IllegalArgumentException("n must be positive");

   if ((n & -n) == n)  // i.e., n is a power of 2
     return (int)((n * (long)next(31)) >> 31);

   int bits, val;
   do {
       bits = next(31);
       val = bits % n;
   } while (bits - val + (n-1) < 0);
   return val;
 }
17
Rob Napier

定義

モジュロバイアスは、モジュロ演算を使用して出力セットを入力セットのサブセットに減らす際の固有のバイアスです。一般に、出力セットのサイズが入力セットのサイズの約数ではないときにモジュロ演算を使用する場合のように、入力セットと出力セット間のマッピングが均等に分散されない場合は常にバイアスが存在します。

このバイアスは、数値をビットの文字列(0と1)で表す計算では、避けるのが特に困難です。ランダム性の真にランダムなソースを見つけることも非常に困難ですが、この議論の範囲を超えています。 この回答の残りの部分では、真にランダムなビットの無制限のソースが存在すると仮定します。

問題の例

これらのランダムビットを使用して、サイコロ(0〜5)のシミュレーションを検討してみましょう。 6つの可能性があるので、6を表すのに十分なビット、つまり3ビットが必要です。残念ながら、3つのランダムビットは8つの可能な結果を​​もたらします。

000 = 0, 001 = 1, 010 = 2, 011 = 3
100 = 4, 101 = 5, 110 = 6, 111 = 7

6を法とする値を取ることにより、結果セットのサイズを正確に6に減らすことができますが、これはmodulo bias問題を提示します:110 0を生成し、111は1を生成します。このダイがロードされます。

潜在的なソリューション

アプローチ0:

理論的には、ランダムなビットに頼るのではなく、1日中サイコロを振ってデータベースに結果を記録し、各結果を1回だけ使用する小さな軍隊を雇うことができます。これは見た目と同じくらい実用的であり、とにかく真にランダムな結果をもたらすことはほとんどありません(意図したしゃれ).

アプローチ1:

モジュラスを使用する代わりに、素朴で数学的に正しい解決策は、110および111を生成する結果を破棄し、3つの新しいビットで再試行することです。残念ながら、これは各ロールで25%の可能性があり、各ロールを含むロールを再ロールする必要があることを意味しますそれ自体です。これは、最も些細な用途を除いて、明らかに非実用的です。

アプローチ2:

より多くのビットを使用します。3ビットの代わりに4を使用します。これにより、16の可能な結果が得られます。もちろん、結果が5より大きい場合はいつでも再ローリングすると事態が悪化し(10/16 = 62.5%)、単独では役に立ちません。

2 * 6 = 12 <16であるため、12未満の結果を安全に取得し、6を法としてその結果を均等に分散できます。他の4つの結果は破棄し、前のアプローチと同様に再ロールする必要があります。

最初は良さそうですが、数学を確認しましょう。

4 discarded results / 16 possibilities = 25%

この場合、1余分なビットは役に立たなかったまったく!

その結果は残念ですが、5ビットでもう一度試してみましょう。

32 % 6 = 2 discarded results; and
2 discarded results / 32 possibilities = 6.25%

明確な改善ですが、多くの実際のケースでは十分ではありません。良いニュースは、ビットを追加しても、破棄および再ロールが必要になる可能性が増えることはありませんです。これはサイコロだけでなく、すべての場合に当てはまります。

ただし、1ビットを追加しても何も変わらないことがあります。実際、ロールを6ビットに増やした場合、確率は6.25%のままです。

これにより、さらに2つの質問があります。

  1. 十分なビットを追加すると、破棄の確率が減少するという保証がありますか?
  2. 一般的な場合、何ビットで十分ですか

一般的な解決策

ありがたいことに、最初の質問に対する答えはイエスです。 6の問題は、2 ^ x mod 6が2と4の間でフリップすることです。これは偶然にも互いに2の倍数であるため、偶数のx> 1に対して

[2^x mod 6] / 2^x == [2^(x+1) mod 6] / 2^(x+1)

したがって、6はルールではなく例外です。同じ方法で2のべき乗を生成するより大きなモジュラスを見つけることは可能ですが、最終的にこれはラップアラウンドする必要があり、破棄の確率は減少します。

それ以上の証拠を提供せずに、一般的に必要なビット数の2倍を使用すると、破棄される可能性が小さく、通常は重要ではありません。

コンセプトの証明

以下は、OpenSSLのlibcrypoを使用してランダムバイトを提供するプログラムの例です。コンパイルするときは、ほとんどの人が利用できるはずの-lcryptoでライブラリにリンクしてください。

#include <iostream>
#include <assert.h>
#include <limits>
#include <openssl/Rand.h>

volatile uint32_t dummy;
uint64_t discardCount;

uint32_t uniformRandomUint32(uint32_t upperBound)
{
    assert(Rand_status() == 1);
    uint64_t discard = (std::numeric_limits<uint64_t>::max() - upperBound) % upperBound;
    uint64_t randomPool = Rand_bytes((uint8_t*)(&randomPool), sizeof(randomPool));

    while(randomPool > (std::numeric_limits<uint64_t>::max() - discard)) {
        Rand_bytes((uint8_t*)(&randomPool), sizeof(randomPool));
        ++discardCount;
    }

    return randomPool % upperBound;
}

int main() {
    discardCount = 0;

    const uint32_t MODULUS = (1ul << 31)-1;
    const uint32_t ROLLS = 10000000;

    for(uint32_t i = 0; i < ROLLS; ++i) {
        dummy = uniformRandomUint32(MODULUS);
    }
    std::cout << "Discard count = " << discardCount << std::endl;
}

MODULUSおよびROLLSの値を試して、ほとんどの条件下で実際に何回の再ロールが発生するかを確認することをお勧めします。懐疑的な人は、計算された値をファイルに保存し、分布が正常に見えることを確認することもできます。

12
Jim Wood

モジュロの使用には、通常2つの不満があります。

  • 1つはすべてのジェネレーターに有効です。制限のあるケースでは見やすいです。ジェネレータのRand_MAXが2(C標準に準拠していない)であり、値として0または1のみが必要な場合、モジュロを使用すると、(ジェネレータが0および2を生成するときに)2倍の頻度で0が生成されますgenerate 1(ジェネレーターが1を生成するとき)。これは、ジェネレーター値から目的の値へのマッピングが何であれ、値をドロップしないとすぐに真になることに注意してください。

  • ある種のジェネレーターは、少なくともいくつかのパラメーターについて、他よりも下位ビットのランダム性が低くなっていますが、悲しいことに、それらのパラメーターには他の興味深い特性があります(Rand_MAXを2のべき乗よりも小さくすることができます)。問題はよく知られており、長い間ライブラリ実装はおそらく問題を回避します(たとえば、C標準のサンプルRand()実装はこの種類のジェネレータを使用しますが、下位16ビットをドロップします)が、文句を言う人もいますそれに不運があるかもしれません

のようなものを使用する

int alea(int n){ 
 assert (0 < n && n <= Rand_MAX); 
 int partSize = 
      n == Rand_MAX ? 1 : 1 + (Rand_MAX-n)/(n+1); 
 int maxUsefull = partSize * n + (partSize-1); 
 int draw; 
 do { 
   draw = Rand(); 
 } while (draw > maxUsefull); 
 return draw/partSize; 
}

0からnの間の乱数を生成すると、両方の問題が回避されます(Rand_MAX == INT_MAXでオーバーフローが回避されます)

ところで、C++ 11は、リダクションおよびRand()以外のジェネレーターに標準的な方法を導入しました。

9
AProgrammer

マークのソリューション(受け入れられているソリューション)はほぼ完璧です。

int x;

do {
    x = Rand();
} while (x >= (Rand_MAX - Rand_MAX % n));

x %= n;

16年3月25日23:16に編集

マーク・アメリー39k21170211

ただし、Rand_MAX(RM)がNの倍数(N =有効な有効な結果の数)の1より小さいシナリオでは、1つの有効な結果セットを破棄する警告があります。

すなわち、「破棄された値の数」(D)がNに等しい場合、それらは実際には有効なセット(V)であり、無効なセット(I)ではありません。

マークのソリューションを使用すると、次の場合に値が破棄されます:X => RM-RM%N

EG: 

Ran Max Value (RM) = 255
Valid Outcome (N) = 4

When X => 252, Discarded values for X are: 252, 253, 254, 255

So, if Random Value Selected (X) = {252, 253, 254, 255}

Number of discarded Values (I) = RM % N + 1 == N

 IE:

 I = RM % N + 1
 I = 255 % 4 + 1
 I = 3 + 1
 I = 4

   X => ( RM - RM % N )
 255 => (255 - 255 % 4) 
 255 => (255 - 3)
 255 => (252)

 Discard Returns $True

上記の例でわかるように、X(初期関数から取得する乱数)の値が252、253、254、または255の場合、これらの4つの値が有効な戻り値のセットを構成していても破棄します。

IE:破棄された値のカウント(I N(有効な結果の数)の場合、戻り値の有効なセットは元の関数によって破棄されます。)==

値NとRMの差をDとして記述すると、つまり:

D = (RM - N)

その後、Dの値が小さくなると、この方法による不要な再ロールの割合は、自然の乗法ごとに増加します。 (Rand_MAXが素数と等しくない場合、これは有効な懸念事項です)

例えば:

RM=255 , N=2 Then: D = 253, Lost percentage = 0.78125%

RM=255 , N=4 Then: D = 251, Lost percentage = 1.5625%
RM=255 , N=8 Then: D = 247, Lost percentage = 3.125%
RM=255 , N=16 Then: D = 239, Lost percentage = 6.25%
RM=255 , N=32 Then: D = 223, Lost percentage = 12.5%
RM=255 , N=64 Then: D = 191, Lost percentage = 25%
RM=255 , N= 128 Then D = 127, Lost percentage = 50%

必要なRerollsの割合が増加するため、NがRMに近づくにつれて、コードを実行するシステムの制約と検索される値に応じて、多くの異なる値でこれが有効な懸念事項になる可能性があります。

これを無効にするために、ここに示すように簡単な修正を行うことができます:

 int x;

 do {
     x = Rand();
 } while (x > (Rand_MAX - ( ( ( Rand_MAX % n ) + 1 ) % n) );

 x %= n;

これは、モジュラスを使用して最大値を定義することの追加の特性を考慮した、より一般的な式のバージョンを提供します。

Nの乗数であるRand_MAXに小さい値を使用する例

元のバージョンをマーク:

Rand_MAX = 3, n = 2, Values in Rand_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3.
When X >= (Rand_MAX - ( Rand_MAX % n ) )
When X >= 2 the value will be discarded, even though the set is valid.

一般化バージョン1:

Rand_MAX = 3, n = 2, Values in Rand_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3.
When X > (Rand_MAX - ( ( Rand_MAX % n  ) + 1 ) % n )
When X > 3 the value would be discarded, but this is not a vlue in the set Rand_MAX so there will be no discard.

さらに、NがRand_MAXの値の数である場合。この場合、Rand_MAX = INT_MAXでない限り、N = Rand_MAX +1を設定できます。

ループ単位では、N = 1を使用するだけで、Xの値はすべて受け入れられますが、最終的な乗数のIFステートメントを挿入します。しかし、おそらく、n = 1で関数が呼び出されたときに1を返す正当な理由があるコードがあります...

したがって、n = Rand_MAX + 1にしたい場合、通常はDiv 0エラーを提供する0を使用する方がよい場合があります。

一般化バージョン2:

int x;

if n != 0 {
    do {
        x = Rand();
    } while (x > (Rand_MAX - ( ( ( Rand_MAX % n ) + 1 ) % n) );

    x %= n;
} else {
    x = Rand();
}

これらのソリューションは両方とも、RM + 1がnの積である場合に発生する不必要に破棄された有効な結果に関する問題を解決します。

2番目のバージョンは、Rand_MAXに含まれる値の可能な合計セットと等しくするためにnが必要な場合のエッジケースシナリオもカバーしています。

両方の修正されたアプローチは同じであり、有効な乱数を提供し、破棄された値を最小化する必要性に対するより一般的なソリューションを可能にします。

繰り返すには:

マークの例を拡張する基本的な一般的な解決策:

 int x;

 do {
     x = Rand();
 } while (x > (Rand_MAX - ( ( ( Rand_MAX % n ) + 1 ) % n) );

 x %= n;

Rand_MAX + 1の1つの追加シナリオを許可する拡張一般ソリューション= n:

int x;

if n != 0 {
    do {
        x = Rand();
    } while (x > (Rand_MAX - ( ( ( Rand_MAX % n ) + 1 ) % n) );

    x %= n;
} else {
    x = Rand();
}
9
Ben Personick