- 
                Notifications
    
You must be signed in to change notification settings  - Fork 39
 
Description
概要
aiscript/src/interpreter/lib/std.ts
Lines 408 to 427 in 537fa96
| 'Math:rnd': FN_NATIVE(([min, max]) => { | |
| if (min && min.type === 'num' && max && max.type === 'num') { | |
| return NUM(Math.floor(Math.random() * (Math.floor(max.value) - Math.ceil(min.value) + 1) + Math.ceil(min.value))); | |
| } | |
| return NUM(Math.random()); | |
| }), | |
| 'Math:gen_rng': FN_NATIVE(([seed]) => { | |
| expectAny(seed); | |
| if (seed.type !== 'num' && seed.type !== 'str') return NULL; | |
| const rng = seedrandom(seed.value.toString()); | |
| return FN_NATIVE(([min, max]) => { | |
| if (min && min.type === 'num' && max && max.type === 'num') { | |
| return NUM(Math.floor(rng() * (Math.floor(max.value) - Math.ceil(min.value) + 1) + Math.ceil(min.value))); | |
| } | |
| return NUM(rng()); | |
| }); | |
| }), | 
現行の実装では、 
しかし、この方法では偏りが生じます。
現行実装の問題点
浮動小数点数演算の丸め誤差による偏り
まず、 
Math.random()はこれらの数の中から何らかの分布(選定されたパターンを倍精度浮動小数点数として解釈し観測した場合に一様分布となるような分布)に従ってランダムに値を選定して返します。
これを範囲変換する際に浮動小数点数の乗算や加算を行っています。
浮動小数点数では、四則演算を行う度に丸めが発生します。ここでの丸めは切り捨てや切り上げではなく、Rounding half to even(日本語版)と呼ばれる方法に基づいて行われることが多いです。
また、浮動小数点数はそもそも無限桁の演算を行うことが出来ない為、四則演算は殆どの場合単射ではありません。
ここで偏りが発生します。
内部的に生成されうる全パターンを等分出来ないことによる偏り
まず、 
Math.random()では 
Math.floor()により丸めを行うと、 
他の範囲についても同様のことが言えます。
提案手法
偏りを除去するためのアルゴリズムは複数存在します。
Rejection Sampling系アルゴリズム
最もシンプルな方法として、"Rejection Sampling"と呼ばれる方法が広く知られています。
以下に示すアルゴリズムはそのうちの一種類です。
- 
$\lceil log_2(range + 1)\rceil$ bitの乱数列を得ることにより、$[0, 2^{\lceil log_2(range + 1)\rceil})$ の範囲の整数を一様分布で生成する。 - 1で生成した数がrangeを
- 超える場合、1に戻る。
 - 超えない場合、その数をそのまま返す。
 
 
TypeScriptでの実装例:
min以上max以下の整数を生成する例(max-minは2^32 以下である必要があります):
const trueMin = Math.ceil(min);
const scale = Math.floor(max) - trueMin + 1;
const array = new Uint32Array(1);
const shift = Math.clz32(scale);
let result: number;
do {
    result = (crypto.getRandomValues(array)[0] ?? -1) >> shift;
} while (result > scale);
return Math.floor(Math.random() * scale + trueMin);この方法は最も簡潔ですが、maxInclusiveが二の冪(=パターン数が二の冪+1)であるときに約50%もの確率で生成をやり直すことになり、殆どの乱数列が無駄になります。
Lemire's algorithm 及びその変種
.NET RuntimeやOpenSSL等で採用された方法です。
詳細な解説は以下にあるのでそれをご確認ください。
どちらも固定小数点数をスケールして範囲に収めていますが、偏りの原因となるパターンが生成されたことを検知して精度を向上した上で乗算する処理が入っており、ループ回数を制限しなければ偏りを取り除くことが出来ます。