概要 まず擬似乱数の生成について少し考えてみましょう。乱数生成を正しく行うことは困難です。これには多くの問題がありますが、根本的な原因はもちろん、コンピュータアルゴリズム自身がランダムでないことです。その他には、"ランダム"の定義に関わる問題、ジェネレータの品質の検定に関わる問題、アルゴリズムを正しく効率的に実装する問題もあります。歴史的にシミュレーション作業を混乱させてきたのは、擬似乱数生成です。このライブラリでは、適正な乱数生成ジョブを行うことだけでなく、それが働く方法やそれらの欠点を説明しようと考えています。また、乱数生成についてさらに詳しく学びたい人向けに、参考文献を紹介したファイル、$(SWARMDOCS)/refbook/random/extra/SOURCES.for.0.7]があります。"$(SWARMDOCS)"は、swarmdocs-1.4.tar.gzを展開したディレクトリ、つまりこのドキュメンテーションを生成したswarmdocsのホームディレクトリです。この分野ではKnuthが主な照会先になりますが、かなり古いため、ここで使用されるジェネレータのほとんどは説明されていません。 |
以下は<random.h>にインポートされる他のヘッダファイルです。
#import <defobj.h> #import <random/generators.h> #import <random/distributions.h> #import <random/RandomDefs.h> #import <random/RandomVars.h> |
基本的なオブジェクトサポートを提供するために、defobjライブラリインターフェイスがインクルードされます。RandomDefs.hには、ライブラリで使用されるいくつかのCプリプロセッサマクロとtypedefが含まれます。
このリファレンスガイドはジェネレータと分布のオブジェクト定義を含み、また、各プロトコルの"このプロトコルに採用されているプロトコル"セクションを通じて、継承構造をエンコードします。(sub-)プロトコル名をクリックするだけで、それがどのメソッドを実装しているかが分かります(ここで、Objective-Cの参考書のプロトコルセクションを復習したくなるかもしれません!)。
解説されるプロトコルでは、最終的にはCREATABLEから継承するあらゆるプロトコルが、プログラムで使用できるオブジェクトを定義します(これはSwarm DefObj装置の一部です)。言い換えれば、'InternalState'が普通のプロトコル(メソッド定義のリスト)であるのに対し、'ACGgen'はプロトコルとそのプロトコルを実装するクラスの両方を表す名前です(myGen = [ACGgen create: ....]と記述すればACGジェネレータが作成できます)。同様に'GammaDist'も、プロトコルとそのプロトコルを実装するクラスの両方を定義します。
すべてのジェネレータと分布は、最終的にSwarmObjectから継承します。
/random/docsディレクトリにある2つのファイル、'methods.SimpleGenerators.h'と'methods.SplitGenerators.h'は、2種類の最も一般的なジェネレータ用に実装された一連の完全なメソッドを示しています。
1.0.2 -> 1.0.3. 注意:新しいrandomライブラリと古いものでは、動作が異なります。つまり、1.0.2リリースで提供されたrandomライブラリを使用したアプリケーションは、破損することになります。しかし、これらのアプリケーションの新しいrandomライブラリへの移行はまったく難しくありません。これは、最終バージョンを持つ標準セットに忠実であるように大きな努力がなされており、下位互換の鍵がいくつか与えられているからです。
基本的にrandomライブラリは、擬似乱数アルゴリズムそのものと、シミュレートする分布に擬似乱数アルゴリズムの出力を変換する機能という2つの部分で構成されます。擬似乱数アルゴリズムは、このドキュメンテーションを通じてジェネレータと呼ばれます。また、擬似乱数は真の乱数ジェネレータがSwarmに実装されていなくとも、多くの場合は"擬似"という単語を抜かして乱数と呼びます。
主筆: Sven Thommsen |
<sthomme@humsci.auburn.edu> |
乱数ジェネレータの利用上の注意 |
random 0.75 |
1997-12-08 |
// ------------------------------------------------
//
// Random v. 0.75 (Swarm 1.0.4)
//
// 乱数ジェネレータの利用上の注意
//
// Sven Thommesen <sthomme@humsci.auburn.edu>
//
// 1997-12-08
//
// ------------------------------------------------
v. 0.6上の変更点
最終バージョンのRandom以降、ウェブや文献にある多数の新しいジェネレータが追加されています。現在は、合計36のジェネレータが定義されています! これらの中には、実に長い周期を持つもの、極めて高速なもの、古いジェネレータより統計的によい特性を持っているものがあります。
'分割'ジェネレータという新種のジェネレータは、L'EcuyerのC2LCGXgenやC4LCGXgenジェネレータという枠の中で紹介されてきたものです。
'分割'ジェネレータは、その周期をサブ周期に分割して素早いアクセスができる長周期ジェネレータです。我々は、A個の'仮想ジェネレータ'を持つこととしてジェネレータを構成します。この仮想ジェネレータはそれぞれ、長さ2^wのサブセグメントを2^v個アドレスできます。これらのパラメータ(A,v,w)は、ジェネレータが作成されるときにユーザが選択(C4LCGXgenのデフォルト値はA=128, v=31, w=41)できます。この利点は、サブセグメントが統計的に独立した乱数列として働くことです。
-getUnsignedSampleに加えて、ジェネレータは、範囲[0.0,1.0)の浮動小数点出力を提供するようになりました。これらのメソッドの形は:
-(float) getFloatSample; // 1つのunsigned値を使用
-(double) getThinDoubleSample; // 1つのunsigned値を使用
-(double) getDoubleSample; // 2つのunsigned値を使用
-(long double) getLongDoubleSample; // 2つのunsigned値を使用
最後のメソッドでは、long doubleの長さはマシンによって異なるため、アーキテクチャを越えた移殖はできません。
ジェネレータは、単一のシード、あるいは長さがジェネレータに依存するシードのベクトルを使って開始できるようになりました(PMMLCGは1つのシードに1つの整数が必要なだけですが、MT19937では624個の整数が要求されます)。
ジェネレータは、開始したシード値を記憶するようになりました。また、提供された変量の個数(つまり、それらが扱った-getUnsignedSampleへの呼び出し数)もカウントされます。
DEFAULTSEED、DEFAULTSEED1、DEFAULTSEED2、DEFAULTSEED3、DEFAULTSEED4と定義される、いくつかの任意のシード値があります。また、デフォルトのジェネレータ'randomGenerator'がスタートしたときの値を返すFIRSTSEED値があります。
NEXTSEEDマクロは、インラインLCGを使いFIRSTSEEDで開始した決定論的なシード値のシーケンスを作成します。RANDOMSEEDマクロは、プログラムの時刻に依存するため呼び出されるたびに異なります。さらに、STARTSEEDがあります。これはデフォルトではNEXTSEEDに一致しますが、コマンドラインパラメータ'--varyseed'でプログラムを開始したときはRANDOMSEEDに一致します。
ジェネレータには、新しい作成メソッド'+createWithDefaults: aZone'が与えられました。このメソッドはジェネレータを作成し、STARTSEEDでそれを初期化します。分割ジェネレータでは、A、v、wにデフォルト値を取得します。
v. 0.6以降の変更点
ジェネレータクラスは、それらの最後がすべて'-gen'で終わるように名前が変わりました。実行するためには、単純な検索と置換であなたのコードを修正してください。
(あるいは、あなたは新しいジェネレータをどれか1つ試したい?)
SWBgenのバグが修正され、ACGとSCGのコードも変更されました。
-verifySelfメソッドが無くなりました。/random/testR0にあるテストプログラムを参照してください。
'getState:'メソッドは'putStateInto: (void *) buffer'と命名され、'setState:'メソッドは'setStateFrom: (void *) buffer'となりました。あなたのコードを検索、置換してこの変更に対応してください。
注意:また、これらのメソッドは、保存されたデータのサイズを維持するように多少変更されています。その結果、v. 0.6オブジェクトが保存した'setStateFrom'をv. 0.7ジェネレータに使うことはできません。
次のリリースでは、このような変更はほとんどないはずです。
利用に関する注意
----------------
まず'単純'なジェネレータを説明した上で、'分割'ジェネレータが'単純'なそれとどのように異なるかを論じます。
以下の説明ではPSWBgenを例として使いますが、他のジェネレータに置き換えて読むことができます。
注意:'my'で始まる名前は、どれもあなたが自分のプログラムで定義した変数を指します。
単純なジェネレータ:
------------------
ジェネレータを作成する方法は3通りあります。
(a) 不精な方法
myGenerator = [ PSWBgen createWithDefaults: [self getZone] ];
オブジェクトを割り当て、--varyseedが指定されていないときはNEXTSEED、指定されていればRANDOMSEEDに等しいSTARTSEEDを使ってそれを初期化します。
(b) 単一のシード値を使う方法
myGenerator = [ PSWBgen create: [self getZone]
setStateFromSeed: mySeed ];
オブジェクトを割り当て、あなたが指定したシード値を使ってそれを初期化します。オブジェクトがその状態を満たすのにシード値のベクトルを要求する場合、このメソッドはインラインPMMLCGジェネレータを使って残りの必要な値を生成します。
その後は、以下のようにしてジェネレータの初期化にどんなシード値が使用されたかを知ることができます。
myUnsigned = [ myGenerator getInitialSeed ];
さらに、以下を呼び出せば、有効な最大シード値が分かります。
myUnsigned = [ myGenerator getMaxSeedValue ];
(v. 0.7で定義されたジェネレータでは、この値はすべて2^32-1です。シードに0は認められません)
以下のメソッドを使うと、いつでもジェネレータの状態をリセットできます。
[ myGenerator setStateFromSeed: mySeedValue ];
(c) シード値のベクトルを使う方法
コンパイル時に以下の固定配列を定義したと仮定します。
unsigned int mySeedVector [vectorLength];
そうすれば、以下のようにできます。
myGenerator = [ PSWBgen create: [self getZone]
setStateFromSeeds: mySeedVector ];
次のように尋ねれば、シード値がいくつ必要かが分かります。
myUnsigned = [ myGenerator lengthOfSeedVector ];
(言うまでもなく、まずこれをなすオブジェクトを首尾よく作成しなければなりません。たとえば、createWithDefaultsを使います!)
次に、以下の方法でシードベクトルを動的に割り当てます。
unsigned int *mySeedVector;
mySeedVector = [[self getZone] alloc: [ myGenerator lengthOfSeedVector]];
オブジェクトの初期化に使われたシード値のベクトルを知るには、次のようにします。
unsigned int *myVector;
myVector = [ myGenerator getInitialSeeds ];
また、次の方法で特定のジェネレータに認められる最大シード値が調べられます。
myVector = [ myGenerator getMaxSeedValues ];
(これらの値はジェネレータごとに異なります。あるジェネレータのすべてのベクトル要素に等しい値は認められません。また、シードに0は認められません)
注意:上記の2つの呼び出しで、myVector変数はジェネレータに対して内部的な配列を指すようにセットされます。値を保存したいときは、静的あるいは動的な空間をあなたのプログラムに割り当て、forループを使ってmyVector[i]からmyAllocatedVector[i]にデータをコピーします。
以下のメソッドを使えば、ジェネレータの状態はいつでもリセットできます。
[ myGenerator setStateFromSeeds: (unsigned *) mySeedVector ];
これはまた、currentCount変数を0にリセットします。
注意:ジェネレータの状態をシードのベクトルからセットする場合、以下のような呼び出しは値0(不正なシード)を返します。
myUnsignedValue = [ myGenerator getInitialSeed ];
一方、単一のシード値でジェネレータを初期化する場合、以下の呼び出しはその単一シードを使って取得する出力に等しい出力を生み出すシードベクトルを返すことになります。
mySeedVector = [ myGenerator getInitialSeeds ];
(d) 対称値
以下のようにセットすれば、ジェネレータは対称値を提供できます。
[ myGenerator setAntithetic: myBooleanValue ];
これがセットされると、-getUnsignedSampleはxの代わりに(unsignedMax-x)を、浮動小数点メソッドはyに代えて(1.0 - y)を返すようになります。
以下を呼び出せば、このフラグがセットされているか確かめることができます。
myBooleanValue = [ myGenerator getAntithetic ];
(e) ジェネレータの出力
以下を呼び出すと、ジェネレータから連続する疑似乱数が取得できます。
myUnsignedValue = [ myGenerator getUnsignedSample ];
以下を尋ねれば、上述の戻り値の最大値が分かります。
myUnsignedValue = [ myGenerator getUnsignedMax ];
(返される最小値は常に0です)
逆に、範囲[0.0,1.0)で浮動小数点出力が欲しいときは、以下のどれかを呼び出します。
// 1つのunsigned値を使って仮数を埋める。
myFloatValue = [ myGenerator getFloatSample ];
myDoubleValue = [ myGenerator getThinDoubleSample ];
// 2つのunsigned値を使って仮数を埋める。
myDoubleValue = [ myGenerator getDoubleSample ];
myLongDoubleValue = [ myGenerator getLongDoubleSample ];
注意:long doubleのサイズはアーキテクチャによって異なり、精度が変化するため、最後のメソッドは移植性がありません。
最後に、以下の方法でいくつの変量が作成されたかカウントを取得できます。
myLongLongInt = [ myGenerator getCurrentCount ];
(currentCountはunsigned long long int型で、カウントは2^64までです)
分割ジェネレータ:
-----------------
'分割'ジェネレータは、作成時に(A,v,w)の構成を指定する必要があります。
myGenerator = [ C4LCGXgen create: [self getZone]
setA: 64 setv: 20 setw: 76
setStateFromSeed: mySeedValue ];
myGenerator = [ C4LCGXgen create: [self getZone]
setA: 32 setv: 25 setw: 60
setStateFromSeeds: (unsigned *) mySeedVector ];
(どちらのケースでも、唯一の制限はA * 2^v * 2^wがジェネレータの周期、つまりC2LCGXでは2^60、C4LCGXでは2^120より小さくなければならないことです)
出力を取得するには、乱数を取得したい'仮想'ジェネレータを次の中から指定しなければなりません。
myUnsignedValue = [ myGenerator getUnsignedSample: 12 ];
myFloatValue = [ myGenerator getFloatSample: myVirtualGenerator ];
myDoubleValue = [ myGenerator getThinDoubleSample: someUnsignedValue ];
myDoubleValue = [ myGenerator getDoubleSample: 32 ];
myLongDoubleValue = [ myGenerator getLongDoubleSample: 0 ];
注意:仮想ジェネレータは、0からA-1の番号がつけられます
現在までに生成された変量のカウントは、以下のように取得します。
myLongLongInt = [ myGenerator getCurrentCount: myVirtualGenerator ];
myLongLongInt = [ myGenerator getCurrentSegment: myVirtualGenerator ];
後者の呼び出しは、現在仮想ジェネレータが数値を引き出しているセグメント番号を示します。
これらのメソッド以外は、'分割'ジェネレータも上述の'単純'なジェネレータも同じです。
これに加えて、'分割'ジェネレータには仮想ジェネレータを管理する次のメソッドがあります:
// すべての仮想ジェネレータを、最初のセグメントの開始位置に置く。
[ myGenerator initAll ]; // 作成時自動的になされる。
// すべての仮想ジェネレータを、現在のセグメントの開始位置に戻す。
[ myGenerator restartAll ];
// すべての仮想ジェネレータを、次のセグメントの開始位置に置く。
[ myGenerator advanceAll ];
// すべての仮想ジェネレータを、指定したセグメントの開始位置に置く。
[ myGenerator jumpAllToSegment: myLongLongIntValue ];
個々の仮想ジェネレータをアドレスすることもできます。
[ myGenerator initGenerator: myVgen ];
[ myGenerator restartGenerator: myVgen ];
[ myGenerator advanceGenerator: myVgen ];
[ myGenerator jumpGenerator: myVgen toSegment: myLongLongIntValue ];
InternalStateメソッドは、単純なジェネレータと分割ジェネレータで共通です。
// オブジェクトの状態データ(のほとんど)をストリームにプリントする。
[ myNormalDist describe: myStream ];
myStreamストリームは以下のように作成できます。
id myStream = [ OutStream create: [self getZone] setFileStream: stdout ]; あるいは
id myStream = [ OutStream create: [self getZone] setFileStream: stderr ];
// オブジェクトの(クラス)名を取得する。
myString = [ myNormalDist getName ];
// putStateInto/setStateFromが使うオブジェクトの'魔法の数(magic number)'を取得する。
myUnsigned = [ myNormalDist getMagic ];
状態の保存とリセット
以下のメソッドを使うと、ジェネレータの内部状態の保存とリストアができます。
// putStateInto/setStateFromに必要なメモリバッファのサイズを取得する。
myUnsigned = [ myGenerator getStateSize ];
// ジェネレータの状態データを、メモリバッファに取得する。
[ myGenerator putStateInto: myBuffer ];
// ジェネレータの状態を、メモリバッファのデータからセットする。
[ myGenerator setStateFrom: myBuffer ];
たとえば、次のようなデータ定義があるとします。
FILE * myFile;
const char * myFileName = "MyGenFile.bin"; // その他何でも
int stateSizeG;
id stateBufG;
int status;
以下のコードは、オブジェクトの状態をディスクに保存する方法です。
(ディスクファイルのエラーを処理するため、処理中止あるいはエラーメッセージのプリントのどちらかのコードを付け加える必要があります)
// 必要とするバッファの大きさを調べる。
stateSizeG = [ myGenerator getStateSize ];
// メモリをバッファに割り当てる。
stateBufG = [[self getZone] alloc: stateSizeG];
// 状態データをバッファに置くようにジェネレータに要求する。
[ myGenerator putStateInto: (void *) stateBufG ];
// 出力用のディスクファイルを開く。
myFile = fopen(myFileName, "w");
if (myFile == NULL) { }; // オープンエラー。ディスク一杯かアクセス権なし。
// 状態バッファをバイナリ形式でディスクに書き込む。
status = fwrite(stateBufG, stateSizeG, 1, myFile);
if (status < 1) { }; // 書き込みエラー。ディスク一杯?
// ファイルを閉じる。
status = fclose(myFile);
if (status) { }; // ファイルクローズのエラー?
// バッファに割り当てたメモリを解放する。
[[self getZone] free: stateBufG];
// あるいはテスト目的で、単にゼロをバッファデータにセット。
// memset(stateBufG, 0, stateSizeG);
以下のコードは、オブジェクトの状態をディスクファイルからセットする方法を示します。
// 必要なバッファの大きさを調べる。
stateSizeG = [ myGenerator getStateSize ];
// メモリをバッファに割り当てる。
stateBufG = [[self getZone] alloc: stateSizeG];
// 入力用のディスクファイルを開く。
myFile = fopen(myFileName, "r");
if (myFile == NULL) { }; // オープンエラー。ファイルが見つからない。
// 状態データをメモリバッファに読み込む。
status = fread(stateBufG, stateSizeG, 1, myFile);
if (status < 1) { }; // 読み込みエラー
// ファイルを閉じる。
status = fclose(myFile);
if (status) { }; // ファイルクローズエラー
// バッファデータから状態をセットするようジェネレータに要求する。
[ myGenerator setStateFrom: (void *) stateBufG ];
// バッファに割り当てたメモリを解放する。
[[self getZone] free: stateBufG];
ジェネレータの検定
v. 0.6以降、MarsagliaのDiehard検定やENT検定を使って、我々は実装したジェネレータの基本的な統計的検定をいくつか行ってきました。
検定結果、周期、状態サイズ、実行時間といった検定結果は、/random/docs/doc.quality.generatorsのドキュメントにまとめられています。これらのデータを使って、あなたのシミュレーションに適したジェネレータを選択してください。
検定結果(要約)
a)検定により、SCG、LCGという古いジェネレータは品質が低く、避けるべきであることが分かりました。
b)lagged-Fibonacciベースのジェネレータ(ACG、SWB、PSWB)は、格子構造を使って動作しなければならないという理由で、Diehardの'Birthday spacings test'はすべて合格しません。これらのジェネレータは、条件つきでのみ推薦されます。
c)残りの32ビットジェネレータ(つまり、32ビットunsigned intをすべて満たすジェネレータ)はすべての検定に合格しており、今のところ推薦されます。
(注意:多くの検定に合格していても、ある検定で悪い結果が出ればそのジェネレータが良いとは証明されません!)
d)どの31ビットジェネレータも、同じ一連の検定に合格しません。'固定した'ビットを出力に持つジェネレータが、これらのいくつかを不合格にしています。私の頭の中では、これらの結果をMarsaglia教授とともに解釈し終えるまで、すべての31ビットジェネレータを'推薦'カテゴリに入れておきます。
ただし、警告:PMMLCGジェネレータは検定に合格しますが、それらの周期は非常に短いため(2^31未満)、利用するのは`おもちゃ程度の'シミュレーションにとどめておかなければなりません。ジェネレータが一巡し、その数値列が反復することを望む人はいません!
いずれにせよ、L'Ecuyer教授は彼が開発したC4LCGXとC2MRG3ジェネレータと、MatsumotoのTT800を推薦し(まだモンスターMT19937がリリースされていませんでした)、Marsaglia教授は彼の桁上がり乗積ジェネレータ(MWCA、MWCB、C3MWC、RWC2、RWC8="母体")を推薦します。
提供済みユーティリティオブジェクト
<random/random.m>には以下のオブジェクトが定義されており、今すぐあなたのプログラムのどこからでもアクセスできます。
id <MT19937> randomGenerator;
id <UniformIntegerDist> uniformIntRand;
id <UniformUnsignedDist> uniformUnsRand;
id <UniformDoubleDist> uniformDblRand;
3つの分布オブジェクトが乱数を導くジェネレータは、2^19937(10^6001)の周期を持つ極めて高速なMT19937です。
主筆: Sven Thommsen |
<sthomme@humsci.auburn.edu> |
分布オブジェクトの利用上の注意 |
random 0.7 |
1997-09-01 |
// --------------------------------------------
//
// Random v. 0.7 (Swarm 1.0.3)
//
// 分布オブジェクトの利用上の注意
//
// Sven Thommesen <sthomme@humsci.auburn.edu>
//
// 1997-09-01
//
// --------------------------------------------
v. 0.6上の変更点
新たにBernoulliDistという分布クラスが加えられました。BernoulliDistは、与えられた確率を使ってバイナリ値(yes/true/1)を返します(以前のRandomBitDistは、公平なコイン投げ、つまり50%の確率に固定されていました)。
分布クラスに新しいcreateメソッド'+createWithDefaults:aZone'が与えられました。このメソッドは分布オブジェクトを作成し、新しいジェネレータオブジェクトを作成してそのジェネレータの使用を独占します。それぞれの分布クラスは、別々に割り当てられるデフォルトのジェネレータクラスを持つことになります。これらのジェネレータは、STARTSEEDで初期化されます。デフォルトのSTARTSEEDは固定値DEFAULTSEEDに一致しますが、コマンドラインパラメータ`--varyseed'を使ってあなたのプログラムを開始すれば、様々なRANDOMSEEDに等しくなります。
すべての分布は、新しい'分割'ジェネレータと相互作用するコードを持っています。
UniformIntegerDistとUniformUnsignedDistは、パラメータminValueをmaxValueに等しくセットできるようになりました。このようにセットすると常にその値が返されます。
UniformDoubleDistもこれを認めます。[x、x)という設定は数学的に疑わしいのですが...
NormalDistとLogNormalDistは、分散ゼロを指定できるようになりました。この場合の戻り値はそれぞれ平均値とexp(平均値)です。
v. 0.6以降の変更点
分布クラスは、すべて名前が'Dist'で終わるように改名されました。実行するためには、あなたのコード上で単純な検索と置換を行ってください。
v. 0.6にあった分布オブジェクトの'固定'と'非固定'の区別はかなり緩和されました。いつでもデフォルトパラメータのセットとリセットができ、たとえ異なるデフォルトパラメータがセットされていたとしても、与えられたパラメータを使った変量の呼び出しが作成できるようになりました。
uniform(0,1)の浮動小数点値の生成は、分布オブジェクトからジェネレータオブジェクトに移りました。したがって、uniform(0,1) doubleだけが必要なときは、分布は必要なくジェネレータから取得できます。
注意:v. 0.6の方式とは違い、ジェネレータは2つの32-bit unsigned値からdoubleの仮数を埋めます。したがって、新しいバージョンの出力は多少違います。
LogNormalDistのバグは修正されました。
'getState:'メソッドは'putStateInto: (void *) buffer'、'setState:'メソッドは'setStateFrom: (void *) buffer'と名づけられました。あなたのコードを検索、置換すれば、この変更に対応できます。
注意:また、これらのメソッドは、保存されたデータのサイズを維持するように多少変更されています。その結果、v. 0.7の分布にv. 0.6オブジェクトが保存した'setStateFrom'を使うことはできません。
次のリリースでは、このような変更はほとんどないはずです。
利用に関する注意
----------------
それぞれの分布オブジェクトには、作成時に乱数ジェネレータを割り当てなければならず、オブジェクトの作成後にジェネレータの再割り当てはできません。複数の分布オブジェクトを1つのジェネレータに接続できるため、結果的には、交互に差し込む方法でそのジェネレータから出力を引き出すことができます。また、分布オブジェクトごとに新しいジェネレータを作成することもできます。
それぞれの分布は、独自のパラメータを1,2個持っており、以下の2種類の方法でこれらのパラメータを扱うことができます。(1) 一連のデフォルトパラメータ値を割り当て、それらのパラメータを使って分布から値を引き出します。(2) あるいは、デフォルトパラメータを割り当てなくても構いません。その場合は、呼び出しごとに必要な(おそらく異なる)パラメータを指定することになります。デフォルトパラメータはいつでもセットやリセットができ、異なるデフォルトがセットされていても、指定したパラメータ値を使って変量を呼び出すことができます。
ジェネレータオブジェクトと同じように、分布オブジェクトもそれらの内部状態を保存し、後にリストアすることができます。
以下の説明ではNormalDistを例として使いますが、他の分布に置き換えて読むことができます。
注意:'my'で始まる名前は、どれもあなたのプログラムで定義した変数を指します。
以下のいずれかの方法で、分布を作成します。
(a) 不精な方法
myNormalDist = [ NormalDist createWithDefaults: [self getZone]];
このメソッドは、デフォルトパラメータセットなしで、分布オブジェクトとそれに接続する新しいジェネレータオブジェクトを作成します。ジェネレータオブジェクトはSTARTSEEDを使って初期化されます(上述の説明を参照)。この目的のため、異なる分布クラスは異なるジェネレータを使います。
(b) デフォルトパラメータを使わず、単純なジェネレータを使う方法
myNormalDist = [ NormalDist create: [self getZone]
setGenerator: mySimpleGenerator ];
もちろん、まず`単純'なジェネレータを指すように'myGenerator'をセットしておかなければなりません。注意:分布オブジェクトの作成後は、分布に異なるジェネレータを割り当てることはできません。
以下のようにすれば、分布と同時にジェネレータが作成できます。
myNormalDist = [ NormalDist create: [self getZone]
setGenerator: [TT800gen create: [self getZone]
setStateFromSeed: 34453] ];
(c) デフォルトパラメータを使わず、分割ジェネレータを使う方法
myNormalDist = [ NormalDist create: [self getZone]
setGenerator: mySplitGenerator
setVirtualGenerator: 7 ];
あるいは、以下のようになるかもしれません。
myNormalDist = [ NormalDist create: [self getZone]
setGenerator: [C4LCGXgen createWithDefaults: [self getZone]]
setVirtualGenerator: 99 ];
分割ジェネレータは仮想ジェネレータ(乱数列)の集合とみなすことができ、分布オブジェクトはこれらの1つに'接続'されなければなりません。分布オブジェクトが作成された後で、ジェネレータや仮想ジェネレータを再割り当てすることはできません。
上述の方法(a)-(c)はすべて、分布オブジェクトから確率変量を獲得するとき、以下のようにパラメータを指定する必要があります。
myDouble = [ myNormalDist getSampleWithMean: 3.3 withVariance: 1.7];
呼び出しごとに異なるパラメータを使うことができます(デフォルトパラメータがすでに設定されていても、この呼び出しを使うことができます)。
(d) デフォルトパラメータを使い、単純なジェネレータを使う方法
myNormalDist = [ NormalDist create: [self getZone]
setGenerator: mySimpleGenerator
setMean: 7.6 setVariance: 1.2 ];
(e) デフォルトパラメータを使い、分割ジェネレータを使う方法
myNormalDist = [ NormalDist create: [self getZone]
setGenerator: mySplitGenerator
setVirtualGenerator: 33
setMean: 3.2 setVariance: 2.1 ];
これらの場合は、パラメータを指定しなくても乱数が取得できます。
myDouble = [ myNormalDist getDoubleSample ];
デフォルトパラメータがすでに設定されていても、パラメータは指定できます。
(もちろん、異なる分布は異なるパラメータを持ちます。たとえば、RandomBitDistオブジェクトにパラメータはありませんが、Uniformには上限と下限パラメータ、NormalDistやLogNormalDistには平均値と分散のパラメータがあります。また、ExponentialDistは平均値のみ、GammaDistはアルファとベータをパラメータに使います。利用できる固有のメソッドについては、random/distributions.hを参照してください)
(f)デフォルトパラメータは、以下の方法でいつでもリセットできます。
[ myNormalDist setMean: 3.3 setVariance: 2.2 ];
(g) 現在のパラメータの値は、以下のように取得します。
// デフォルトパラメータ
myDouble1 = [ myNormalDist getMean ];
myDouble2 = [ myNormalDist getVariance ];
myDouble3 = [ myNormalDist getStdDev ];
// ジェネレータオブジェクトへのポイントを取得する。
myOtherGenerator = [ myNormalDist getGenerator ];
// 仮想ジェネレータの数を取得する(分割ジェネレータが使われている場合)。
myUnsignedValue = [ myNormalDist getVirtualGenerator];
// デフォルトパラメータがセットされているか確認する。
myBoolean = [ myNormalDist getOptionsInitialized ];
// そのオブジェクトが今までいくつの変量を提供したかを確認する。
// (カウンタは2^64までのunsigned long long int)
myLongLongInt = [ myNormalDist getCurrentCount ];
(h) 次の方法で変量カウンタやその他の状態変数をリセットできます。
[ myNormalDist reset ];
ほとんどの場合は、[ myGenerator setStateFromSeed: mySeedValue ];を使い、接続したジェネレータも一緒にリセットします。
(i) 最後に、以下のようにしてInternalStateプロトコルメソッドを獲得します。
// オブジェクトの状態データ(のほとんど)をストリームにプリントする。
[ myNormalDist describe: myStream ];
myStreamストリームは以下のように作成できます。
id myStream = [ OutStream create: [self getZone] setFileStream: stdout ]; あるいは
id myStream = [ OutStream create: [self getZone] setFileStream: stderr ];
// オブジェクトの(クラス)名を取得する。
myString = [ myNormalDist getName ];
// putStateInto/setStateFromが使うオブジェクトの'魔法の数(magic number)'を取得する。
myUnsigned = [ myNormalDist getMagic ];
以下のメソッドを使うと、分布オブジェクトの内部状態の保存とリストアができます。
// putStateInto/setStateFromに必要なメモリバッファのサイズを取得する。
myUnsigned = [ myNormalDist getStateSize ];
// 分布の状態データを、メモリバッファに取得する。
[ myNormalDist putStateInto: myBuffer ];
// 分布の状態を、メモリバッファのデータからセットする。
[ myNormalDist setStateFrom: myBuffer ];
たとえば、次のようなデータ定義があるとします。
FILE * myFile;
const char * myFileName = "MyDistFile.bin"; // その他何でも
int stateSizeD;
id stateBufD;
int status;
以下のコードは、オブジェクトの状態をディスクに保存する方法です。
(ディスクファイルのエラーを処理するため、処理中止あるいはエラーメッセージのプリントのどちらかのコードを付け加える必要があります)
// 必要とするバッファの大きさを調べる。
stateSizeD = [ myNormalDist getStateSize ];
// メモリをバッファに割り当てる。
stateBufD = [[self getZone] alloc: stateSizeD];
// 状態データをバッファに置くよう分布オブジェクトに要求する。
[ myNormalDist putStateInto: (void *) stateBufD ];
// 出力用のディスクファイルを開く。
myFile = fopen(myFileName, "w");
if (myFile == NULL) { }; // オープンエラー。ディスク一杯かアクセス権なし。
// 状態バッファをバイナリ形式でディスクに書き込む。
status = fwrite(stateBufD, stateSizeD, 1, myFile);
if (status < 1) { }; // 書き込みエラー。ディスク一杯?
// ファイルを閉じる。
status = fclose(myFile);
if (status) { }; // ファイルクローズのエラー?
// バッファに割り当てたメモリを解放する。
[[self getZone] free: stateBufD];
// あるいはテスト目的で、単にゼロをバッファデータにセット。
// memset(stateBufD, 0, stateSizeD);
以下のコードは、オブジェクトの状態をディスクファイルからセットする方法を示します。
// 必要なバッファの大きさを調べる。
stateSizeD = [ myNormalDist getStateSize ];
// メモリをバッファに割り当てる。
stateBufD = [[self getZone] alloc: stateSizeD];
// 入力用のディスクファイルを開く。
myFile = fopen(myFileName, "r");
if (myFile == NULL) { }; // オープンエラー。ファイルが見つからない。
// 状態データをメモリバッファに読み込む。
status = fread(stateBufD, stateSizeD, 1, myFile);
if (status < 1) { }; // 読み込みエラー
// ファイルを閉じる。
status = fclose(myFile);
if (status) { }; // ファイルクローズエラー
// バッファデータから状態をセットするよう分布オブジェクトに要求する。
[ myNormalDist setStateFrom: (void *) stateBufD ];
// バッファに割り当てたメモリを解放する。
[[self getZone] free: stateBufD];
提供済みユーティリティオブジェクト
<random/random.m>には以下のオブジェクトが定義されており、今すぐあなたのプログラムのどこからでもアクセスできます。
id <MT19937> randomGenerator;
id <UniformIntegerDist> uniformIntRand;
id <UniformUnsignedDist> uniformUnsRand;
id <UniformDoubleDist> uniformDblRand;
3つの分布オブジェクトが乱数を導くジェネレータは、2^19937(10^6001)の周期を持つ極めて高速なMT19937です。
利用できません。
利用できません。
このライブラリもそうですが、Swarmライブラリインターフェイスについて議論されたことは過去にまったくありません。このドキュメントでは、インターフェイスに関する議論に含まれるいくつかの論点をそのまま掲載することで、合理的な決定がいくらか提供できればと考えています(これらの議論には実装の問題と重なる部分がありますが、ここで重要視しているのはシミュレーションで有効な擬似乱数を使用することに関連した問題の複雑さを理解してもらうことです)。
論点.
ジェネレータやそれに関連する様々なクラスは? http://www.sela.co.il:8080/swtrain/new/shai/tgp.psを参照。
推奨されないジェネレータをインクルードする?
ジェネレータのシード vs. 状態?
"状態"とは何? その処理方法は?
特別な方法で自身のコンフィギュレーションを報告するオブジェクトを使ったデバッグサポートに関する大きな問題。及び、オブジェクト状態の報告とオブジェクトの保存にそれがどう関連するか。
個別の分布型にクラスを分ける? (たとえば、double、integer, unsigned :: 連続/離散)
ジェネレータ出力から分布出力への変換に伴う問題。
サポートすべき精度は?
分布が使うジェネレータは、分布を通じて可視であるべき?
効率性!
どんな分布を提供する?
どんなジェネレータを提供する?
開いた区間 vs. 閉じた区間?
デフォルトジェネレータ、シード、状態をプログラムする?
共同プログラミング? (a.k.a. Softwareプロセス!)
手短な作成メソッドは?
フェーズ最適化の潜在性は?
乱数列、同期の限界周期、適切なシミュレーション vs. コードデバッグ(これが最もホットな問題)
ライブラリの互換性
これらは単に問題を並べただけです。それぞれの問題が解決し、その選択根拠を説明した後に、(時間が有れば)いくらかまた追加されるはずです。ただし、それは骨の折れる仕事です。
ここで示したコードは、効率的で適度に安全なジェネレータをいくつか実装する方法を示しています。アルゴリズムは参考文献[$(SWARMDOCS)/refbook/random/extra/SOURCES.for.0.7を参照]をもとにしています。これらのアルゴリズムは、可能な限り正確に実装し、いくつか単純な検定がなされてきましたが、どれかのアルゴリズムの品質が悪かったり、誤って実装されている可能性は常にあります。
最良の結果を得るためには、ライブラリのユーザは範囲を限定した何らかの方法で、これらのジェネレータを自分で検定しなければなりません。この簡単な方法として、ジェネレータのあるクラス(たとえばPMMLCG)で1回、次に他のクラス(たとえばSWB)で1回というように、実験を2度実行する方法があります。結果が著しく異なれば、あなたはジェネレータを疑わなければなりません。しかしそうでなければ、いや、それでもジェネレータは不完全かもしれません。
Swarmのftpサイトには、tarballという乱数ライブラリの検定プログラムがあります。SwarmTests-0.7.tar.gzをダウンロードしてください。
このリリースで提供されたジェネレータは、George MarsagliaのDiehard検定だけでなく、John Walkerのエントロピー検定(ENT)を使って統計的にテストされています。これらの検定結果は$(SWARMDOCS)/refbook/random/extra/doc.quality.generatorsドキュメント、その他のジェネレータの性質については$(SWARMDOCS)/refbook/random/extra/generators.tableを参照してください。また、$(SWARMDOCS)/refbook/random/extra/CHOOSING.A.GENERATORには、あなたのシミュレーションでジェネレータを選択する方法について、いくつかの注意事項を説明しました。
ENT検定は、Swarmウェブサイトにあるtarball検定プログラムに含まれています。Diehard検定は著作権が保護されているため含まれていませんが http://www.hku.hk/internet/randomCD.htmlからダウンロードできます。
なお、分布オブジェクトの統計的な検定はなされていません
ドキュメンテーションと実装の現状
このライブラリは、Sven Thommesenが寄贈したRandomライブラリVersion0.75です。Version0.6はNelson Minarのオリジナルrandomライブラリを再実装し、多くの変更や新しいインターフェイスを加えたものでした。このバージョンにはより多くのジェネレータと分布が追加され、インターフェイスもいくらか変更されています。
十分には検定されておらず、実装は少し不安定かもしれません。また、Svenは検定プログラム一式を寄贈しました。我々のウェブサイトでこれを利用できるようにする予定です。我々はジェネレータと分布は正しく実装されているものとかなりの確信を持っていますが、いかなる擬似乱数生成ライブラリを使っても、そこから獲得する結果は厳密に調査すべきです。