LoginSignup
4
4

More than 1 year has passed since last update.

Xorshiftなどの擬似乱数をプロットして比較してみた

Last updated at Posted at 2020-02-15

Xorshift で得られた乱数が適度にばらけているか視覚的に確認するため、平面に分布をプロットしました。

比較のためアルゴリズムが選べるようにしましたが、残念ながら違いは感じられませんでした。

See the Pen Xorshift32 by 七誌 (@7shi) on CodePen.

↑ エラーになる場合は一度 CodePen を開いてから、この記事をリロードしてください。

この記事には Haskell 版があります。

実装

Xorshift は実装が簡単な割に良好な擬似乱数を生成します。

JavaScript で実装します。

const [XSgetSeed, XSgetNext, XSrand] = (() => {
  const m = 2 ** 32;
  const XSgetSeed = () => Math.floor(Math.random() * (m - 1)) + 1;
  const s = Uint32Array.of(XSgetSeed());
  function XSgetNext(seed) {
    if (seed !== undefined) s[0] = seed;
    s[0] ^= s[0] << 13;
    s[0] ^= s[0] >>> 17;
    s[0] ^= s[0] << 5;
    return s[0];
  }
  return [XSgetSeed, XSgetNext, (seed) => XSgetNext(seed) / m];
})();
  • 計算結果を符号なし整数で格納するため 1 要素の TypedArray を使用しました。
  • 右シフトを符号なしで行うため >>> を使用しました。(コメント欄でご教示いただきました)
  • 引数でシード値が指定できるようにしました。指定しなければ保持していた値を使います。初期値は JavaScript 組み込みの乱数から得ます。
  • XSrandMath.random と同じ使い方ができるように結果を 0 以上 1 未満の浮動小数点数で返します。
  • シフト回数の組み合わせは複数あります。このコードで使用している 13, 17, 5 は原著論文で例として使われていた組み合わせです。他の記事では 13, 17, 15 もよく使われますが、どちらも動作します。

Math.random

Math.random ではシード値が指定できません。

実装側で乱数生成アルゴリズムの初期シードを選択し、ユーザーが初期シードを選択、またはリセットすることは出来ません。

シード値が必要な場合に Xorshift は手軽な代替手段となるでしょう。

線形合同法

従来用いられていた単純なアルゴリズムです。

Stephen K. Park と Keith W. Miller が、彼らのサーベイ中で「最低基準」として示したもので、より良い選択肢が無いのならば、自作などせずにこれを使うべしというもの。

X_{n+1}=\left(48,271\times X_{n}\right)\ {\bmod {\ }}(2^{31}-1)

4. comp.lang.c FAQ list · Question 13.15 ただしソースコードでは乗数が 16807 になっているので注意すること。48271 を使ったほうが(もしかしたら計算がわずかに重くなるかもしれないが)少し良い乱数列になる。ソースコード中のその数の部分を書き換えるだけでよい。

これを JavaScript に移植しました。シード値の扱いは Xorshift の getNext に合わせています。

const [PMgetSeed, PMgetNext, PMrand] = (() => {
  const a = 48271;
  const m = 2 ** 31 - 1;
  const q = Math.floor(m / a);
  const r = m % a;
  const PMgetSeed = () => Math.floor(Math.random() * (m - 1)) + 1;
  const s = Int32Array.of(PMgetSeed(), 0, 0, 0);
  function PMgetNext(seed) {
    if (seed) s[0] = seed;
    s[1] = s[0] / q;
    s[2] = s[0] % q;
    s[3] = a * s[2] - r * s[1];
    s[0] = s[3] > 0 ? s[3] : s[3] + m;
    return s[0];
  }
  return [PMgetSeed, PMgetNext, (seed) => PMgetNext(seed) / m];
})();

※ XorShift と比較できるように、コードの構成を似せています。

Web Crypto API

MDN には次のように言及があります。

Math.random() の提供する乱数は、暗号に使用可能な安全性を備えていません。セキュリティに関連する目的では使用しないでください。代わりに Web Crypto API(より正確にはwindow.crypto.getRandomValues() メソッド)を使用してください。

こちらも比較に使ってみました。

const cryptoRand = (() => {
  const m = 2 ** 32;
  const buf = new Uint32Array(1);
  return () => window.crypto.getRandomValues(buf)[0] / m;
})();

プロット

Canvas にサイズ 1×1 の長方形を描くことで点を打ちます。最初の 20 個の乱数は値を表示します。

function test() {
  result.innerHTML = "";
  const ctx = canvas.getContext("2d");
  const w = canvas.width;
  const h = canvas.height;
  ctx.fillStyle = "white";
  ctx.fillRect(0, 0, w, h);
  ctx.fillStyle = "black";
  const count = parseInt(dots.value);
  const rnd = select.selectedOptions[0].function;
  for (let i = 0; i < count; i++) {
    const rx = rnd();
    const ry = rnd();
    if (i < 10) {
      log(rx);
      log(ry);
    }
    const x = Math.floor(rx * w);
    const y = Math.floor(ry * h);
    ctx.fillRect(x, y, 1, 1);
  }
}

この種のプロットについて Wikipedia に言及があります。

線形合同法一般の欠点に、多次元で規則的に分布するという性質がある。線形合同法による乱数列$r _ 0$, $r _ 1$, $r _ 2$, $r _ 3$, ... から($r _ 0$, $r _ 1$), ($r _ 1$, $r _ 2$), ($r _ 2$, $r _ 3$), ... のように順番に割り当ててプロットすると、それが疎になるのはしょうがないのだが(例として、全部で10000個しかない点を、10000x10000 の矩形にプロットすることになるのだから、稠密にはなりえない)、一定の間隔の平面上に点が並んでしまうのが問題である。

今回のプロットは 100×100 と小さかったため、目で見て分かるほどには表面化しませんでした。

関連記事

出力の log() は次の実装を使っています。

4
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4