疑似乱数生成器の内部状態を保存する方法
解説
C++の<random>ヘッダには、メルセンヌツイスターなどの疑似乱数生成器が用意されています。疑似乱数生成器は、初期値として用いる値であるseedから、指定されたアルゴリズムに基づいた乱数のように見える数列を生成します。生成器は内部状態を保持しており、呼び出されるたびに、その状態に基づいて疑似乱数を生成するとともに、内部状態を更新します。
内部状態を保存することはシリアライズ、復元することはデシリアライズと呼ばれます。計算を途中で中断する時に内部状態を保存し、再開する時に復元すれば、中断せずに計算を続けた場合と同じ乱数列を生成できます。
<random>ヘッダに定義されている疑似乱数生成器には、内部状態を比較・保存・復元するための非メンバ関数として、operator==、operator<<、operator>>が用意されています。
operator<<
operator>>
operator==
- 2つの生成器の内部状態が等しいかを比較する。等しければ、その後に生成される乱数列も一致する。
乱数エンジンに分布クラスを組み合わせると、特定の分布に従う乱数を生成できます。
例えば、乱数エンジンから得た値を使って、指定した範囲の一様な実数乱数を生成するuniform_real_distributionや、平均μ、標準偏差σの正規分布にしたがう乱数を生成するnormal_distributionのようなクラスがあります。
分布クラスには、分布の設定に加えて、生成履歴に依存する内部状態を持つものもあります。例えば、normal_distributionは、Marsaglia polar methodを用いているので、一度に計算した値の一部を次回の呼び出し用に保持する実装をしています。
そのため、分布クラスを使う場合は、乱数エンジンと分布オブジェクトの両方を保存・復元することで、同じ標準ライブラリの実装で続きの乱数列を再現できます。分布オブジェクトの保存・復元にも、operator<<とoperator>>を使用できます。
sample code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
#include <iostream>
#include <fstream>
#include <random>
using namespace std;
int main() {
//seedの設定
random_device seed_gen; //seedを用意
mt19937 rand_gen1(seed_gen()); //生成器を初期化
normal_distribution<double> dist1(0.0, 1.0); // 平均0、標準偏差1
// 乱数を11個生成して内部状態を進める
for (int i=0;i<11;++i){
dist1(rand_gen1);
}
// mt19937の内部状態と分布を保存
ofstream ofs("random_state.txt");
ofs << rand_gen1 << '\n' << dist1 << endl;
ofs.close();
// mt19937の内部状態と分布を読み込み->rand_gen2
mt19937 rand_gen2;
normal_distribution<double> dist2;
ifstream ifs("random_state.txt");
ifs >> rand_gen2 >> dist2;
ifs.close();
//rand_gen1とrand_gen2、dist1とdist2が等しいかどうかを確認
//正しければtrue、違えばfalse
cout << boolalpha << (rand_gen1 == rand_gen2) << endl;
cout << boolalpha << (dist1 == dist2) << endl;
// 続きの乱数を生成し、値が一致することを確認
for (int i = 0; i < 5; ++i) {
const double x = dist1(rand_gen1);
const double y = dist2(rand_gen2);
cout << x << ' ' << y << endl;
}
}
|
参考文献