擬似乱数とは、プログラムにより生成する「乱数っぽく見える数」のことだ。 本物の乱数(自然乱数)とは完全に予測不能な数のことで、 これは厳密に言えば、プログラムでは絶対に生成することはできない。 (自然乱数を得る手段としては、回路の熱雑音などの物理現象を利用する方法などがある。)
擬似乱数は計算(多くの場合は漸化式)によって作るため、 計算の初期値がわかれば予測が可能である。 予測可能であることは乱数の性質に反するが、シミュレーションを行う場合などには都合がよい。 またゲームでも、ランダムに弾が発射されるようなシューティングゲームのリプレイを再生する場合などに、 乱数を再現する必要性が出てくる。
擬似乱数を生成する方法は様々なものが考えられている。主な方法の名称を以下に示す。 この記事でとりあえず言いたいことは、XorShiftが手ごろでいいね!ということである。
■ 線形合同法(最も古典的)
■ 足し算法、引き算法(たまに見かける)
■ メルセンヌ・ツイスタ(現時点では最強)
■ XorShift(速度・性能ともに手ごろ)
★ ロジスティック写像を用いる方法(まだ研究段階?)
最もシンプルで、古くから色々なところで使われている方法。 例えばVisualC++におけるrand()では、以下のような動作をするらしい。 (参考サイト: 「良い乱数・悪い乱数」)
VisualC++の乱数(線形合同法)
static long x = 1;
int rand() { x = x * 214013 + 2531011; return (int)(x >> 16) & 32767; }
void srand( long s ){ x = s; }
xの初期値だとか214013だとかのマジックナンバーは、開発環境によって変わる。 ここで注意すべきは、rand()で乱数を発生させると、0~32767の乱数しか得られないということ。 乱数の周期がどうこう言う前に、精度が15bitしかないので、 0.0~1.0に正規化したとしても、とてもシミュレーションなどには向かない。 筆者はこの乱数の最大値(これはRAND_MAXとしてstdlib.hに定義されている) はもっと大きな値だと思っていたので、 研究で遺伝的アルゴリズムを組む時に思うような結果が得られず、悩んだことがある。
以上の理由から、乱数が重要になる場面ではrand()ではなく、ちゃんとした乱数を選んで使う必要がある。
また、線形合同法の欠点は、その性質上奇数と偶数が交互に現れてしまう (もしくはどちらかが出続ける)ということである。このようなわかりやすい規則性は、 擬似乱数と言えど好ましくない。 例えばすごろくゲームを作るときに、毎回奇数と偶数が交互に出るような場面を想像すればわかりやすいだろう。 なお、VC++のrand()は上位ビットをシフトして使っているため、奇数と偶数が交互、とまではならない。
ちなみに、筆者の拙作ゲーム「GENETOS」のプログラムでは、 Ver.0.60までの乱数は以下のような方法で生成している。 線形合同法に、ちょっと小細工を加えたものである。
GENETOSの乱数(線形合同法+α)
unsigned int CommonUtil::random( int mod ){
randomSeed *= 1103515245;
randomSeed += 12345;
if( ((randomSeed >> 3) & 1) == 1 ) randomSeed++; //偶数奇数が不規則っぽくなるといいなぁ
if( randomSeed % 17 == 1 ) randomSeed++;
return randomSeed % mod;
}
(1103515245と12345という値は、古いCコンパイラで使われていたものらしい。) 先に言っておくと、この方法は好ましくない。 偶数奇数が交互に現れるというのが嫌な感じがしたので、 なんか不規則っぽくなってほしいなと思い2行のif文を追加したのだが、 その後、分布を調べる実験をしたところ全く現れない数がたくさんある ことがわかった。やはり若輩者が適当なことをするものではない。先人は偉大だ。 今後は、後述するXorShiftという方法に切り替えようと思う。
「どの値も均等に現れるか」、「ある値ばかり連続して出たりしないか」 など様々な観点から、 擬似乱数を評価することができる。中でも最も重要なものは「どの値も均等に現れるか」という 一様性であろう。例えばランダムで英単語を表示させるソフトを作るときに、 表示されない単語があっては困る。
線形合同法のような単純な方法でも、一応全ての値がいつかは現れる。 簡単な実験を行ってみたが、1個ずつ乱数を発生させていくぶんには、どの値も比較的均等に現れた。 しかし奇数と偶数が交互に現れるという性質上、 2個ずつの乱数の組を発生させたときなどには問題が表れる。 例えば(x, y)座標を順に生成していくような場合では、絶対に現れない組が出てくるのだ。 つまり、線形合同法では2次元空間に一様分布しないということになる。
その点、メルセンヌ・ツイスタは623次元の空間まで一様分布することができる。
メルセンヌ・ツイスタは日本人が編み出した性能のよい擬似乱数である。 乱数列の周期は2の19937乗-1と桁違いに長く、 623次元空間に一様分布する。つまり623個の値を連続で生成して組にしても、 全てのパターンが現れるということである。
以下のページ からC言語のソースファイルもダウンロードできるので、 特に理由が無ければシミュレーションなどにはこれを使えば良い。 除算や乗算を用いていないため、そこまで処理が重くないというのも魅力である。
比較的最近考案された方法に、XorShiftというものがある。 以下に、簡潔にまとまったコードを示す。
XorShiftによる乱数
unsigned long xor128(){
static unsigned long x=123456789,y=362436069,z=521288629,w=88675123;
unsigned long t;
t=(x^(x<<11));x=y;y=z;z=w; return( w=(w^(w>>19))^(t^(t>>8)) );
}
これで、2の128乗の周期らしい。32bitの変数の場合、 線形合同法ではせいぜい2の32乗しか周期がないのに比べると、 なかなかの性能である。また、シフト演算とビット演算を多用しているため、 比較的高速で、なんかかっこいい。(これは筆者の主観である。)
結論として、ゲームなんかで使うぶんにはこのXorShiftでいいんじゃないかと思われる。 実装も簡単だし、速度も性能もそこそこ、ということで。
カオスとか複雑系とかの話をすると、まず最初に出てくるのがロジスティック写像 というものである。ロジスティック写像というのは、x = ax(1-x) という形のシンプルな漸化式による写像である。a に4付近の値を入れたとき、 初期値によってその後のxが0と1に間で予測不能な変動をする。 初期値をほんの少し変えただけで、その後の変動に大きな影響が出るということ。 こういうのをカオス的な振る舞いという。
予測不可能な変動をするのなら、これを乱数に使えないのかなぁと思ったのだが、 まだその方法は確立されていないようだ。調べてみると研究論文はいくつか見つかった。 まあ冷静に考えてみれば、
■ 相手がカオスなだけに、解析が難しい
■ つーか浮動小数が出てくる時点で誤差が怖い
■ 普通に使うと一様に分布しない(0と1付近の値が多くなる)
という理由で色々と問題があることがわかる。研究対象としては面白そうだが。
色々と制限の多いFlashLite1.1 の random() がどうなっているかはドキュメントが見当たらなかったが、 とりあえず偶数と奇数が交互に出るような単純な線形合同法ではなさそうだ。 なお、FlashLiteは起動時にタイマーで乱数の初期化を行っている模様。 FlashLiteでXorShiftを書いてみようかとも思ったが、 どうやらFlashLite1.1ではXOR演算が使えないようだ。がっかり。
筆者は、擬似乱数でも自然乱数でもない、人間が作り出す乱数(言わば人間乱数) というものに興味がある。人間が「できるだけランダムっぽくなるように」 考えて出した数字は果たしてどれくらい自然な乱数になっているのか、 人間だからこそ生まれる数字間の相関性はあるのか、など。 それなりに研究も行われているようである。