C++11 の乱数を使う - C++ プログラミング

PROGRAM


C++11 の乱数を使う

C++11 には、乱数を生成する仕組みが新たに用意されました。

これまでも rand 関数という 0 から RAND_MAX までの疑似乱数を int 型で返す関数がありましたけど、新しく搭載された乱数ではより高度な疑似乱数や、さまざまな数学的な分布など、目的に似合う乱数を取得することができるそうです。

 

ここでは "メルセンヌ・ツイスタ" という乱数を使って、C++11 の乱数の使い方を見て行きます。

 

準備

まず、乱数を使う上で <random> ヘッダーをインクルードします。

#include <random>

そして、乱数を生成して行くことになりますが、C++11 の乱数では、乱数を生成する機能を提供する "エンジン" と、それを目的の値域に整える "ディストリビューション" という 2 つの仕組みを合わせて使用します。

 

具体的には、"エンジン" として "メルセンヌ・ツイスタ" を使用して、"ディストリビューション" ではエンジンで作られる値を "0.0 から 1.0" の範囲に整える、という感じになります。

また、疑似乱数の値は周期的で予測性や再現性があるため、その計算パターンの初期値を決める "シード" の設定も必要になります。

 

シードの初期化

まずは、乱数エンジンの初期化で使うシードを作成します。

シードの生成は少し複雑になっていますが、C++11 の乱数システムでは std::seed_seq 型に、シードの初期値として、最低 32 ビットの要素で構成された配列を指定します。

std::random_device randomDevice;

 

std::vector<uint32_t> randomSeedVector(10);

std::generate(randomSeedVector.begin(), randomSeedVector.end(), std::ref(randomDevice));

 

std::seed_seq randomSeed(randomSeedVector.begin(), randomSeedVector.end());

冒頭のシードを生成するときに使っている std::vector の準備の仕方がちょっと独特なので、詳しく見ていきます。

 

まず、最初に std::random_device というクラスを作成していますが、これは "乱数を作成する" クラスです。

これから乱数を作るエンジンを初期化しようとしているのに、そのための下準備で乱数を使っているのも変な気がしますが、どうやらこの std::random_device というクラスが作る乱数は「非決定性」という、ある値からその次の値を導き出せない値になるそうです。

コンピューターで普段扱う乱数は "疑似乱数" で、ある値を元に次の値を算出する「決定性」があります。このときの、最初の "ある値" が "シード" になります。

疑似乱数が普通とはいえ、任意の値を取りたいだけならどちらを使っても問題ありませんが、疑似乱数の方が乱数を計算するのに必要な処理時間が短く、また、均等に任意の値が出る(ようにできるなどの)メリットがあります。

 

このとき std::generate 関数は、イテレータで指定した範囲の変数を、指定したジェネレータで生成した値で埋める関数です。

指定しているジェネレータは randomDevice クラスなので、つまり非決定性の乱数で std::vector を初期化していることになります。std::vector の要素数はどのくらいが適切かは判りませんが、エンジンでは指定されただけの要素を工面して初期化してくれるらしいので、ここでは 10 個にしてみています。

こうして揃えた std::vector を std::seed_seq のコンストラクタに渡して、シードの準備は完了です。

 

エンジンを生成する

シードの準備ができたら、それを使って乱数を生成するエンジンを作成します。

std::mt19937 randomEngine(randomSeed);

今回は "メルセンヌ・ツイスタ" という乱数エンジンを使用するので、使用するエンジンは "std::mt19937" になっています。

 

これだけで、乱数エンジンの生成が完了しました。

乱数エンジンはそのまま使うこともできますが、ディストリビューションと合わせて使用すると使い勝手が良くなります。

 

ディストリビューションを使わずにエンジンだけをそのまま使いたい場合は、次のようにすることで乱数を取得できます。

int value = randomEngine();

このように、エンジンを格納した変数を () で呼び出すことで、戻り値として乱数を取り出します。

なお、このエンジンがいくつからいくつまでの乱数を返すかを知るには、それぞれ min() 関数と max() 関数を使います。

 

乱数エンジンは標準でもいくつか用意されていて、例えば次のようなものが使えるそうです。

乱数エンジン テンプレートクラス 生成値 内容
std::mt19937 std::mersenne_twister_engine uint32_t メルセンヌ・ツイスタ
std::mt19937_64 std::mersenne_twister_engine uint64_t メルセンヌ・ツイスタ
std::minstd_rand0 std::linear_congruential_engine uint32_t 線形合同法 (x' = x * 16,807 % 2,147,483,647)
std::minstd_rand std::linear_congruential_engine uint32_t 線形合同法 (x' = x * 48,271 % 2,147,483,647)
std::ranlux24_base std::subtract_with_carry_engine uint32_t キャリー付き減算アルゴリズム
std::ranlux48_base std::subtract_with_carry_engine uint64_t キャリー付き減算アルゴリズム
std::ranlux24 std::discard_block_engine uint32_t ranlux24_base エンジンが生成した値の一部を破棄して乱数を生成
std::ranlux48 std::discard_block_engine uint64_t ranlux48_base エンジンが生成した値の一部を破棄して乱数を生成
std::knuth_b std::shuffle_order_engine uint32_t minstd_rand0 エンジンが生成した値の順番を入れ替えて乱数を生成
std::random_device   unsigned int 非決定性のある乱数

このほかにも std::default_random_engine というものが用意されています。

これは、既定の乱数エンジンを使うときに使用するクラスで、どの乱数エンジンが既定になるかはコンパイラによって異なるようです。

たとえば Xcode 4.6 では std::minstd_rand が既定の乱数エンジンでしたが、Visual Studio 2012 では std::mt19937 が既定になっていました。

 

これらの乱数エンジンを構成しているクラスのもとをたどれば、大きく分けて次の 3 つのテンプレートクラスで構成されています。

乱数エンジンテンプレート 内部サイズ
sizeof(TYPE)
生成速度 乱数の質  
std::linear_congruential_engine × 1 中速 低品質 線形合同法を使った乱数エンジン(『("現在の乱数" × a + c) mod m』が次の乱数になる)
std::mersenne_twister_engine × 624 高速 高品質 メルセンヌ・ツイスタ方式を使った乱数エンジン
std::subtract_with_carry_engine × 25 低速 中品質 キャリー付き減算アルゴリズムを使った乱数エンジン

 

ディストリビューションを生成する

ディストリビューションを使うことで、乱数エンジンが生成する値を、使いやすい範囲の値に変換できます。

 

整数の乱数を使う

たとえば、乱数を 1 から 1000 までの整数にしたい場合は "uniform_int_distribution" が使えます。

std::uniform_int_distribution<int> randomIntDistribution(1, 1000);

整数を扱うディストリビューションであれば、このように std::uniform_int_distribution に続けて <> 扱うデータ型を指定して、続いてディストリビューションに付ける変数名を指定しています。

そして、コンストラクタで、扱う値の範囲を指定します。

 

これで、1 から 1000 以下までを返すディストリビューションが出来上がりました。

これにエンジンを渡すことで、そのエンジンを使って 1 から 1000 までの中の乱数を取得できます。

int value = randomIntDistribution(randomEngine);

これで、乱数を value 変数に取得することができました。

 

実数の乱数を使う

乱数を 0.0 から 1.0 未満までの実数で取得したい場合には "uniform_real_distribution" を使用します。

std::uniform_real_distribution<double> randomDoubleDistribution(0.0, 1.0);

これで、先ほどと同じエンジンを使っても、0.0 から 1.0 未満の乱数を得ることができます。

double value = randomDoubleDistribution(randomEngine);

std::uniform_int_distribution とは違って、範囲の末尾として指定した値(今回の例では 1.0)は含まれないので注意が必要です。

 

その他のディストリビューション

ディストリビューションは標準でいくつか用意されていて、たとえば次のようなものが使えるようです。

ディストリビューション 内容
std::uniform_int_distribution 離散一様分布(均等な整数を返す)
std::uniform_real_distribution 連続一様分布(均等な実数を返す)
std::bernoulli_distribution ベルヌーイ分布(確率 p で 1 を、確率 q = 1 − p で 0 をとる、離散確率分布)
std::binomial_distribution 二項分布(結果が成功か失敗のいずれかである n 回の独立な試行を行ったときの成功数で表される離散確率分布)
std::negative_binomial_distribution 負の二項分布(確率分布の一種で、二項分布の拡張)
std::geometric_distribution 幾何分布(離散型確率分布の一種)
std::poisson_distribution ポアソン分布(所与の時間間隔で発生する離散的な事象を数える特定の確率変数 X を持つ離散確率分布)
std::exponential_distribution 指数分布(連続確率分布の一種)
std::gamma_distribution ガンマ分布(連続確率分布の一種)
std::weibull_distribution ワイブル分布(物体の強度を統計的に記述する確率分布、物体の強度を統計的に記述する)
std::extreme_value_distribution 極値分布(ある分布関数にしたがって生じた大きさ n の標本のうち、x 以上 (あるいは以下) となるものの個数がどのように分布するかを表す、連続型の確率分布モデル)
std::normal_distribution 正規分布(平均値の付近に集積するようなデータの分布を表した連続的な変数に関する確率分布)
std::lognormal_distribution 対数正規分布(連続確率分布の一種)
std::chi_squared_distribution カイ二乗分布(推計統計学で最も広く利用される確率分布の一種)
std::cauchy_distribution コーシー分布(連続型確率分布の一種)
std::fisher_f_distribution F 分布(統計学および確率論で用いられる連続確率分布)
std::student_t_distribution T 分布(サンプル数が少ない場合に正規分布をとる母集団の平均を推定する問題に使用される連続確率分布)
std::discrete_distribution 離散分布で乱数を生成
std::piecewise_constant_distribution 一定の区間に分布する実数値の乱数を生成
std::piecewise_linear_distribution 定義されている区間に分布する実数値の乱数を生成

このほかにも 0.0 から 1.0 未満の範囲に変換する std::generate_canonical もあるようなのですが、Visual Studio 2012 で試した感じでは 1.0 よりも遙かに桁数の大きい値が取得できるようでした。

Xcode 4.6 では std::generate_canonical は 0.0 以上 1.0 未満を返すようです。

 

ディストリビューションとエンジンを関連付ける

乱数を得るときにディストリビューションとエンジンの両方を使わないといけないのが厄介な場合は、std::bind を使ってそれらをひとつにまとめることもできます。

std::function<int()> randomInt = std::bind(std::uniform_int_distribution<int>(1, 10), randomEngine);

このようにすることで、次のように簡単に乱数を取得できるようになります。

int value = randomInt();

このようにすれば、エンジンとディストリビューションをひとつにまとめて管理できるのと、乱数を取得するときのプログラムがシンプルになるので、何度も使いまわすようなときに便利です。

 

乱数の生成にかかる時間を計測する

ところで、C++11 ではいくつかの方法で乱数を計算することができるようになっていますが、それらの速度の違いが気になったので、次ののケースについて調べてみました。

エンジン ディストリビューション 乱数の値域 実行にかかった時間(プロセッサ時間)
std::mt19937 std::uniform_int_distribution [1-10] 780
std::mt19937 std::uniform_real_distribution [0.0-1.0) 445
std::mt19937 使用しない - 52
std::mt19937 使用せず、"(double)value / engine.max()" で 0.0 から 1.0 の範囲に変換 [0.0-1.0] 85
std::random_device 使用しない - 551
std::random_device std::uniform_real_distribution [0.0-1.0) 1078

ディストリビューションを使わない方が圧倒的に高速なのが気になりますが、ディストリビューションを使う場合は std::uniform_real_distribution の方が高速に動作するみたいでした。

ちなみに std::uniform_real_distribution で取得した乱数に、10 を乗算して切り捨ててから 1 を足して、値域を [1-10] に調整してみても、調整をしなかった [0.0-1.0) との目に見えた速度差はありませんでした。

 

また、細かい準備をしなくても使える std::random_device は、std::mt19937 と比べて圧倒的に遅い様子です。

この感じだと、std::mt19937 と std::uniform_real_distribution を使う組み合わせが何かと使いやすいかもしれないですね。


[ もどる ]