データ発見隊

最終回 乱数思考

この記事を読むのに必要な時間:およそ 6 分

ニセ乱数

本当にランダムなものではなくランダムっぽく感じられる値を得るためには,最近出ていない値が出やすいようなニセ乱数を作るとよいでしょう。たとえば次のような工夫をします。

  • 最近出た値は出さない
  • 最近一度も出ていない値は出現確率を上げる

このような性質を持つニセ乱数関数rand2()を用いて,先ほどのスライドショープログラムを書き換えます。

n = (ARGV.shift || 100).to_i # スライドの数
trials = (ARGV.shift || 10000).to_i # 試行回数
histdiv = (ARGV.shift || 10).to_i # ヒストグラムの粒度
lim = (ARGV.shift || 1000).to_i # 表示上限
hist = [] # ヒストグラム

$randhist = [] # ニセ乱数の過去値リスト

def rand2(n)
  # 出やすさを決めてprob[]に格納
  prob = []
  $randhist.each { |val|
    prob[val] = 1
  }
  # 特に最近出たものは外す
  recentlen = 3
  recentlen = n-1 if recentlen >= n
  (1..recentlen).each { |i|
    break if $randhist[-i].nil?
    prob[$randhist[-i]] = 0
  }
  # 出ていないもの優先で割り当て
  (0...n).each { |i|
    if !prob[i] then
      prob[i] = 10 # 10倍出やすくする
    end
  }

  # prob[] に基づいてニセ乱数を計算
  a = []
  (0...n).each { |i|
    prob[i].times {
      a.push(i)
    }
  }
  v = a[rand(a.length)]

  # ヒストリ保存
  $randhist.shift if $randhist.length >= n
  $randhist.push(v)
  return v
end

trials.times {
  shown = []
  nshown = 0
  count = 0
  while nshown < n do
    ind = rand2(n)
    if !shown[ind] then
      shown[ind] = true
      nshown += 1
    end
    count += 1
  end
  hist[count/histdiv] = hist[count/histdiv].to_i + 1
}

100000.times { |i|
  puts "#{i*histdiv} #{hist[i].to_i}"
  break if i*histdiv >= lim
}

rand()の代わりにrand2()を使うと,図4のようなヒストグラムが得られます。160回あたりにピークがあり,300回すればほぼ確実にすべてのスライドを表示できることがわかります。

図4 ニセ乱数によるスライドショー

図4 ニセ乱数によるスライドショー

二次元ニセ乱数

ランダムに感じる二次元表示を行いたい場合も,本当の乱数を使うよりもニセ乱数を利用するほうがランダム感が出ます。

図5「文字列探しパズル」では普通の乱数を使って文字を並べているため,同じ文字が3個以上縦や横に並んでいる場所がいくつも存在し,ランダム感があまり感じられません。

図5 乱数による文字列探しパズル

図5 乱数による文字列探しパズル

一方,前述のニセ乱数と同じような方法を使って上下と同じ文字が出にくいようにすると,図6のようになります。図5よりもランダムさが大きいように見えるでしょう。

図6 二次元ニセ乱数による文字列探しパズル

図6 二次元ニセ乱数による文字列探しパズル

乱数の精度

乱数で簡単にいろいろなシミュレーションができることはわかりましたが,その結果はどの程度信用できるものでしょうか?

最近のプログラミング言語には優秀な乱数生成ライブラリがあるのが普通です。たとえばRuby 1.8以降のrand()関数はメルセンヌツイスターという優秀な乱数生成手法を利用していますが,古いライブラリでは問題が出ることもあります。たとえばCの古いrand()関数でランダムな座標を生成しようとすると,条件によっては乱数として利用できません。

#include
main()
{
  for(;;){
    int x = rand();
    int y = rand();
    if(x < 0x100000 && y < 0x100000){
      printf("%d %d\n",x & 0xff,y & 0xff);
    }
  }
}

上のプログラムをSnowLeopard上で実行して生成した座標をプロットすると,図7のように平行線上に点が並んでしまいます。

図7 Cのrand()関数による二次元座標

図7 Cのrand()関数による二次元座標

ここではrand()が小さな値を返したときだけプロットするという変な条件を付けているために目に見える形でこのような不具合が発生しています。今回の記事で紹介したような簡単なシミュレーションではこういう問題が出ることはめったにありませんが,複雑なシミュレーションを行うときは,予想外の問題が発生するのを避けるため,性質の良い乱数を使うように注意するのがよいでしょう。

この問題の場合はrand()をrandom()に変更するとこのようなことは起こりません。乱数を使って複雑なシミュレーションを行う場合は注意しましょう。

連載のおわりに

連載「データ発見隊」は今回が最終回になります。1年にわたって,ちょっと変わったデータの扱いに関する話題をいろいろ紹介してきました。ふだん普通に使っているファイルやデータでも,見方を変えることによって新たな活用ができる機会はまだまだ多いと思いますので,いろんな視点でデータを活用していただければと願っています。

著者プロフィール

増井俊之(ますいとしゆき)

慶應義塾大学教授。ユビキタス時代のインタフェース技術の研究開発に従事。

本棚.orgGyazoQuickMLFeedTVなど各種のネットサービスを運用中。

http://www.pitecan.com/