2011-03-07

確率分布の使い方

C++0xのstd::randomには、様々な分布クラスが存在する。一体どうやって使い分ければいいのか。ここでは、ゲームにたとえて考えてみる。

もっとも簡単な分布は、一様分布(Uniform distributions)である。これは、a ≦ i ≦ b, の範囲の値iを、それぞれ等しい確率で返す分布である。

ゲームでいえば、サイコロやルーレットなどの実装に使えるだろう。

// 六面サイコロの実装
int main()
{
    std::mt19937 rng ;
    // 一様分布
    // 0から5までの数字を等しい確率で返す分布
    std::uniform_int_distribution<> dice(0, 5) ;

    int a[6] = { } ; // 六面サイコロの出た目の回数を記録する配列

    // 600回サイコロを振る
    for ( int i = 0 ; i != 600 ; ++i )
    {
        ++a[dice(rng)] ; // サイコロを振る
    }

    // 600回サイコロを振った結果、どの目も100前後になる
    for ( int i = 0 ; i != 6 ; ++i )
    {
        std::cout << i + 1 << "=" << a[i] << std::endl ;
    }
}

ベルヌーイ分布(Bernoulli distributions)は、ある確率pでtrueを、確率1 - pでfalseを返す分布である。

ゲームでいえば、10%の確率で攻撃が当たるとか、30%の確率でアイテムを落とすなどといった場合に使える。trueの場合はアタリで、falseの場合はハズレである。

// 30%の確率でアイテムを落とすコード
int main()
{
    std::mt19937 rng ;

    // ベルヌーイ分布
    // 30%の確率でtrueを返す分布
    // 30%の確率でアイテムを落とす
    std::bernoulli_distribution drop_rate( 0.3 ) ;


    int n = 0 ;
    for ( int i = 0 ; i != 1000 ; ++i )
    {
        if ( drop_rate(rng) ) // アイテムを落としたかどうか計算
            ++n ;
    }

    // 1000回の可能性のうち、アイテムは、300回前後落とされている
    std::cout << n << std::endl ;
}

ポアソン分布(Poisson distributions)は、単位時間あたりの事象の発生の平均回数が与えられている場合に、確率的に、何回発生したのかを求める分布である。

ゲームでいえば、ストラテジーゲームで、一年で1000人の敵を倒せる武将が、10日で何人の敵を倒せるかという確率を計算するのに使える。

// 一年で1000人の敵を倒せる武将は、10日で何人の敵を倒せるのか確率的に求める
int main()
{
    std::mt19937 rng ;

    // 一年で1000人の敵を倒せる武将が、10日で倒せる人数の平均を求める
    // 1000人 / 365日 * 10日
    double rate = 1000 / 365 * 10 ; 

    // ポアソン分布
    std::poisson_distribution<> score( rate ) ;

    // 最初の十日間で倒した人数
    std::cout << score(rng) << std::endl ;
    // 次の十日間で倒した人数
    std::cout << score(rng) << std::endl ;
    // その次の十日間で倒した人数
    std::cout << score(rng) << std::endl ;
}

正規分布(Normal distributions)は、統計の初歩でも出てくる、だいぶ馴染み深い分布であると思う。

ゲームでいえば、ゲーム内において、大小様々な木々を動的に生成するときに、できるだけ大きさの差を自然にしたいときに使えるだろう。

サンプリング分布(Sampling distributions)はある個数のうちのランダムなサンプルがいくつか与えられている場合に、全体の個数の統計的な情報を得るための分布である。

この分布は、出る目の確率が偏っている、イカサマなサイコロを実装するのに使える。

// イカサマなサイコロの実装
int main()
{
    std::mt19937 rng ;

    // 1の目がでる確率が50%、その他の目のでる確率がそれぞれ10%の、イカサマなサイコロ
    std::discrete_distribution<> cheat_dice = { 0.5, 0.1, 0.1, 0.1, 0.1, 0.1 } ;

    int a[6] = { } ; // 六面サイコロの出た目の回数を記録する配列

    // 600回サイコロを振る
    for ( int i = 0 ; i != 600 ; ++i )
    {
        ++a[cheat_dice(rng)] ; // サイコロを振る
    }

    // 1の目が300前後、その他の目が60前後振られている
    for ( int i = 0 ; i != 6 ; ++i )
    {
        std::cout << i + 1 << "=" << a[i] << std::endl ;
    }
}

ちなみに現実では、第二次世界大戦中、ドイツの戦車の生産台数を割り出すのに使われている。これは本当の話である。当時連合国側は、捕獲したドイツ戦車の連番のシリアルナンバーから、ドイツの戦車の生産台数を割り出したのだ。詳しくは、WikipediaのGerman tank problemを参照。また、以前にこのブログでも取り上げている話題だ。

問題は、これらの分布クラスに、いくつか派生版があるということだ。一体どのように使えるのか、数学の知識のない私には、よく分からない。

一様分布 - Wikipedia
ベルヌーイ分布 - Wikipedia
ポアソン分布 - Wikipedia
正規分布 - Wikipedia
Sampling distribution - Wikipedia, the free encyclopedia

2 comments:

Anonymous said...

サンプリング分布について、説明は合ってますが名称がおかしいと思います。

このような分布はCategorical distributionまたはDiscrete distributionといいます。非常に基本的な分布であるにもかかわらず、なぜか日本語名は無いようです。

そのためか、この分布は多項分布(Multinomial distribution)と呼ばれることもありますが、実際には多項分布の特殊ケース(パラメータ n が1の場合)です。

江添亮 said...

この分野は全くの門外漢なのですが、なぜか一般的な日本語の名称が見つからなかったのです。
ランダムなサンプル個体から全体を推定するなんて、非常に面白いことです。
ましてや、ドイツの戦車の生産台数を推定する逸話は、日本語に翻訳される価値があるほど面白い話だと思うのですが。