【技術】擬似乱数への道のり(1)

  • calendar2023.03.28
  • reload2023.04.14
投稿者:歌島侃勇
ホーム » 技術 » 【技術】擬似乱数への道のり(1)

はじめに

ある仕事の担当をランダムに決めたい場合やプログラムに対するテストケースを無作為に作りたい場合など、ランダム性が求められることはよくあります。しかし、コンピュータは決定的な動作をするのが得意であるため、真の乱数を生成することは困難です[1]。そこで、厳密な乱数を扱うのは諦めて、一見乱数のように見える疑似乱数というものを扱います。疑似乱数生成器は決まった数式と初期値(シード)をもちます。数式と初期値にしたがって疑似乱数が生成されていくので、疑似乱数生成器 $f$, 初期値 $s$ として、$n$ 番目の疑似乱数 $r_{n}$ は $r_{n}=f\left(n,s\right)$ と表せます。本稿では、擬似乱数に求められる性質を考えながら、いくつかの擬似乱数生成アルゴリズムを紹介します。

乱択アルゴリズム

あるアルゴリズムにおいて入力が決まれば常に同じ結果を出力するとき、そのアルゴリズムを決定的といいます。これとは対照的に、同じ入力であっても実行するたびに異なる値になる可能性があるとき、そのアルゴリズムを乱択アルゴリズムといいます[1]。

身近な乱択アルゴリズムの例として、円周率を求めるモンテカルロ法があります。1 辺の長さが 2 の正方形の中に半径の長さが 1 の円を書きます。正方形の中にでたらめに沢山点をうち、円の中に入った個数を数えます。打った点の総数と円の中に入った点の数との比から円周率を求める方法です[1]。

1 辺の長さが2 の正方形を 1/4 にして、ランダムに 20 個の点を打つと下図のようになりました。

Calculate PI by Monte Carlo method

円内には 16 個の点が入っているので、

$$ \frac{\pi}{4}:1\approx16:20\\ \pi \approx \frac{16}{5}=3.2 $$

のように円周率を(大雑把に)求めることができました。

円周率を求めるモンテカルロ法の他にも、素数判定を効率的に行うミラーラビン素数判定法[2] など様々な乱択アルゴリズムが存在します。

乱数の有用性を確認したところで、実際に疑似乱数を生成するアルゴリズムを紹介していきます。

平方採中法

初期に提案された疑似乱数に、平方採中法(平方中抜き法, middle-square method)があります[3]。平方採中法では、$2n$ 桁のシード $x$ に対し、$x^2$ の上位 $n$ 桁と下位 $n$ 桁を削除したものを次の疑似乱数とします。すなわち、平方採中法によって得られる $p$ 進数 $2n$ 桁の疑似乱数列を $r_i$ のように書くことにすると、

$$ r_{n+1}=\lfloor (r_n^2 \bmod p^{3n})/p^n \rfloor $$

と表せます。ただし、($m>0$ に対し、)$a\mod m$ は$a$ を $m$ で割った余りとします。$a$ を $m$ で割った余りとは、$a=qm+r\;(0\leq r < m)$ を満たす $r$ を指します。

例えば、10 進数 4 桁で $r_0=1234$ とすると、

$$ r_1=5227, \;r_2=3215\;, r_3=3362,\; r_4=3030,\; r_5=1809,\; r_6=2724,\dots $$

となり、なんだか良さげな列が生成されているように見えます。しかし、平方採中法には「0 が続く列になってしまいやすい」などの問題点があります[4]。実際、先ほど示した系列も $r_{56}=0$ となり、以降 0 が続く列となってしまいます。このような欠点があるため、現在では平方採中法が利用されることはほぼありません。

疑似乱数の周期

多くの擬似乱数生成器は内部状態を持ち、内部状態にしたがって次の擬似乱数を生成します。内部状態はコンピュータ上に置かれるためそのサイズは有限であり、その種類数は有限です。したがって、擬似乱数を生成し続ければいつか同じ内部状態となります。このことから、内部状態を持つ疑似乱数生成器が生成する疑似乱数列はある周期をもつことがわかります。ただし、疑似乱数列 $\{r_i\}_{i=0}^\infty$ において、${}^\exists n_0\geq 0,{}^\forall i\geq n_0,\;r_i=r{i+p}$ となる $p>0$ が存在し、またそのような $p$ のうち最小であるとき、この疑似乱数列は周期$p$ を持つということにします。例えば、平方採中法で、10進数4桁で $r_0=6100$ とすると、

$$ r_1=2100,\; r_2 = 4100,\;r_3=8100,\;r_4=6100,\;r_5=2100,\dots $$

となります。平方採中法では $r_{n+1}$ は $r_n$ のみによって定まることを思えば、$6100\rightarrow2100\rightarrow4100\rightarrow8100\rightarrow\dots$ のループとなっており、周期が 4 であることがわかります。0 が続く列も周期 1 と捉えることができます。このように周期が短いのは疑似乱数として好ましくありません。

次に紹介する線形合同法はこの周期に関してかなり良い性質を持っています。

線形合同法

整数の3つ組 $(a,b,M)$ が $0<a<M, 0\leq b < M$ を満たすとします。このとき、初期値 $0\leq r_0 < M$ として,$r_1, r_2,\dots$ を次の漸化式で定めます。

$$ r_{n+1}=(ar_{n}+b) \bmod M $$

線形合同法(linear congruential generators)はこの漸化式にしたがって、疑似乱数を生成するアルゴリズムです[5][6]。

例として、$a=4,b=5,M=6,r_0=2$ の場合を考えてみると、

$$ \begin{align} r_1 &= (4\times2+5)\bmod6=1\\ r_2&=(4\times 1+5)\bmod6=3\\ r_3&=(4\times 3+5)\bmod 6=5\\ r_4&=(4\times 5+5)\bmod 6=1\\ \dots \end{align} $$

のようになります。

線形合同法で生成される疑似乱数 $r_i$ は $0\leq r_i<M$ を明らかに満たします。したがって、周期は $M$ 以下です。実は、線形合同法について周期 $M$ を達成するための必要十分条件が知られていて、次のようになります[6]。

定理(線形合同法の最大周期)

パラメータ $(a,b,M)$ の線形合同法の周期を $\lambda$ とする。このとき、次の必要十分条件が成り立つ。ただし、$x\bot y$ は $x,y$ が互いに素であることを意味し、$x|y$ は $x$ が $y$ を割り切ることを意味するものとする。

$$ \lambda=M \Leftrightarrow \begin{cases} (i)\; b\bot M\\ (ii)\; 任意の素数p に対してp|M\Rightarrow p|(a-1)\\ (iii)\; 4|M\Rightarrow 4|(a-1)\\ \end{cases} $$

これらを満たす例としては $(a,b,M)=(5,3,8)$ などが挙げられます。初期値 $x_0=0$ として、疑似乱数列を計算してみると、

$$ \begin{align}
5\cdot0+3&=3 \\
5\cdot3+3&=2 \\
5\cdot2+3&=13=5 \\
5\cdot5+3&=28=4 \\
5\cdot4+3&=23=7 \\
5\cdot7+3&=38=6 \\
5\cdot6+3&=33=1 \\
5\cdot1+3&=8=0
\end{align}$$

となり、確かに周期 8 であることがわかります。

$M$ が与えられたとき、最大周期になるように $a,b$ を設定するのは(可能であるなら、計算量を無視すれば)簡単で、

$$ a=\begin{cases} \prod_{p|M, p>2} p+1& 2\bot M\\ \prod_{p|M, p>2} p\times 2+1 &GCD(4,M)=2 \\ \prod_{p|M, p>2} p\times 4+1 &4|M \end{cases} $$

のようにすれば条件を満たす最小の数 $a$ が得られるので、これが $M$ 未満であれば、$b\bot M$ なる $b$ とともに用いればよいです。 ただし、$M$ が偶数で最大周期を達成しようとすると偶数奇数が順に出るようになってしまうので、注意が必要です。

📎 $M$ が偶数のとき、条件(i)(ii) から $a,b$ は奇数。偶数で割った余りを取るということは何らかの偶数を引くことであることに注意すると、
$$ \begin{align} a\cdot0+b-0\equiv1\cdot0+1\equiv1\pmod2\\ a\cdot1+b-0\equiv1\cdot1+1\equiv0\pmod2 \end{align}$$
となるため。

大抵のプログラミング言語に用意されている rand() 関数は線形合同法を使って実装されることがあります[1]。例えば、C++ の rand() 関数は線形合同法で実装されており、その周期は $2^{32}$ 以下です[7]。

パラメータ $(a,b,M)$ の線形合同法によって生成される疑似乱数 $r$ は $0\leq r < M$ の $M$ 通りの値のみを取りえます。したがって、周期が $M$ のとき疑似乱数は $\{0,1,\dots,M-1\}$ 上に一様分布します。この「取りうる値上に一様分布する」という性質は疑似乱数にとって重要な性質です。パラメータによっては一様分布する線形合同法は優れた疑似乱数生成器のように思えます。しかし、連続するいくつかの疑似乱数を取ってきて多次元で考えると、線形合同法では一様分布しなくなってしまいます[7]。

次に紹介するメルセンヌ・ツイスタは $2^{19937}-1$ という長大な周期と、623 次元という高次元に一様分布するという性質を持つ疑似乱数生成器です[8]。

メルセンヌ・ツイスタ

まず、$k$ 次元に一様分布するとはどういうことかを確認します。[8] では、これは $k$-distribution として次のように定義されています。

定義 ($k$-distribution)

周期 $P$ の擬似乱数生成器が生成する擬似乱数列を $\boldsymbol{x}_0,\boldsymbol{x}_1,\dots$ とする。また、$trunc_v(\boldsymbol{x})$ を $w$ ビット擬似乱数 $\boldsymbol{x}$ の先頭 $v$ ビットを取ってきたものとする。$0\leq i <P$ について,$kv$ ビットベクトル

$$ \left(trunc_v(\boldsymbol{x}_i), trunc_v(\boldsymbol{x}_{i+1}),\dots, trunc_v(\boldsymbol{x}_{i+k-1})\right) $$

を作る.$kv$ ビットベクトルは $2^{kv}$ の値を取りうるが、そのどれもが同じ回数だけ上記の $P$ 個のベクトルに登場する(ただし、$\boldsymbol{0}$ は他のベクトルより登場回数が1少ない)とき、擬似乱数列 $\boldsymbol{x}_0,\boldsymbol{x}_1,\dots$ は $k$-distribution を $v$ ビットで満足するという。また、ある $v$ について,満たす $k$-distribution の最大値を $k(v)$ とし、$k(v)$ を調べることを $k$-distribution test と呼ぶ。

この定義は次のように解釈できます[8]。

$v$ ビットベクトルを数値とみなしたもの $\boldsymbol x_i$ を $2^v$ で割ることで,実数 $[0,1)$ に対応させたものを $x_i$ と書くことにします。ここで、ベクトル $(x_i,\dots,x_{i+k-1})$ は、$k$ 次元単位超立方体内部の点となります。ベクトル $(x_i,\dots,x_{i+k-1})$ は,$k$ 次元単位超立方体の各軸を $2^v$ 個に分割してできる格子点 のいずれかにあたります。$k$-distribution を $v$ ビットで満足するとき、周期 $P$ 個のベクトルが全て同じ回数($\boldsymbol{0}$ は 1 回少ない)だけ各格子点上に位置するということを意味しています。

例えば、$3$-distribution を $v$ ビットで満足するときは、下の図に示すような格子点に一様分布するということになります。

3次元の格子点。全ての軸が同様に分割されており、格子点は$2^{3v}$ 個存在する

メルセンヌ・ツイスタの構成

$k$ 次元に一様分布することの意味が確認できたので、ここからはメルセンヌ・ツイスタ(mersenne twister)について説明していきます。メルセンヌ・ツイスタは以下のように構成されます[8][9][10]。

$w$ ビットの整数からなる乱数列を生成する場合を考えます。また、整数を各ビットで分けて $w$ 次元行ベクトル $\boldsymbol{x}_i$ として考えます。ここで、メルセンヌ・ツイスタの式は、

$$ \boldsymbol{x}_{k+n}=\boldsymbol{x}_{k+m}\oplus \left( \boldsymbol{x}_k^{up}|| \boldsymbol{x}_{k+1}^{low}\right)A $$

となります($n>m$)。ただし、$\left( \boldsymbol{x}_k^{up}|| \boldsymbol{x}_{k+1}^{low}\right)$は $\boldsymbol{x}_{k}$ の上位 $w – r$ ビットと $\boldsymbol{x}_{k+1}$ の下位 $r$ ビットを連結したベクトルであり、$A$ は次に示す $w\times w$ 正方行列

$$ A= \begin{pmatrix} 0 & 1 & 0 & \dots & 0\\ 0 & 0 & 1 & \dots & 0\\ & \vdots & & \ddots \\ 0 & 0 & 0 & \dots & 1 \\ a_{w-1} & a_{w_2} & a_{w-3} & \dots & a_0 \end{pmatrix} $$

です。また、$\boldsymbol{x}A$ の計算は有限体 $\mathbb{F}_2$ で行います。すなわち、足し算はXOR 演算であり、掛け算はAND 演算です。$\boldsymbol{x}A$ の計算は、愚直に行うと $O(w^2)$ 回ほどの演算が必要になりますが、$A$ の構造を利用して次のように高速に計算することが可能です。

$$ \boldsymbol{x}A = \begin{cases} (\boldsymbol{x} \gg 1) & x_0=0\\ (\boldsymbol{x} \gg 1)\oplus \boldsymbol{a} & x_0=1 \end{cases} $$

ただし、

$$ \begin{align} \boldsymbol{x}=(x_{w-1},x_{w-2},\dots,x_0)\\ \boldsymbol{a}=(a_{w-1},a_{w-2},\dots,a_0) \end{align} $$

です。

ここで、$\boldsymbol x_i$ の分布を改善するために,$w\times w$ の正則行列 $T$ を右から掛けます。この工程はTempering と呼ばれます。ここでも、$T$ を愚直に掛けることはせず、 $T$ を掛けることに相当する演算として、パラメータ $u,s,t,\ell,\boldsymbol b, \boldsymbol c$ に対して以下を行います。

$$ \begin{align}
\boldsymbol{y}_1 &= \boldsymbol{x}\oplus (\boldsymbol{x}\gg u)\\
\boldsymbol{y}_2 &= \boldsymbol{y}_1\oplus ((\boldsymbol{y}_1\ll s)\& \boldsymbol{b})\\
\boldsymbol{y}_3 &= \boldsymbol{y}_2\oplus ((\boldsymbol{y}_2\ll t)\& \boldsymbol{c})\\
\boldsymbol{z} &= \boldsymbol{y}_3\oplus (\boldsymbol{y}_3\gg \ell)\\
\end{align}$$

例えば、$\boldsymbol{y}_1 = \boldsymbol{x}\oplus (\boldsymbol{x}\gg u)$ は $\boldsymbol x$ に

$$ \begin{pmatrix} 1&0&\dots&0 & & \dots & 0\\ 0&1&\dots&0 & & \dots & 0\\ &\vdots&\ddots &0 & & \dots & 0\\ 1 & 0 & \dots& 1 & 0 &\dots &0\\ 0 & 1 & \dots & 0 & \ddots & & 0\\ & \vdots& \ddots & 0 & & \ddots & 0\\ 0 & 0 & \dots& 1 & 0 & \dots & 1 \end{pmatrix} $$

という形式の行列を右から掛けることに対応します。他の式に関しても、左下の 1 の列が右上に移動したり、この 1 の列に 0 が混じるような列となります[11]。各行列は明らかに rank が $w$ で正則なので、$T$ は正則です。こうして得られる $\boldsymbol{z}$ を疑似乱数として出力します(内部状態は $\boldsymbol{x}_i$ で持っておくことに注意してください)。

メルセンヌ・ツイスタの仕組みは上に説明した通りです。ここで、

$$ \begin{align} w&= 32\\ n&= 624\\ m&= 397\\ r&= 31\\ u&= 11\\ s&= 7\\ t&= 15\\ \ell &= 18\\ \boldsymbol{a}&= {\rm 0x9908B0DF}\\ \boldsymbol{b}&= {\rm 0x9D2C5680}\\ \boldsymbol{c}&= {\rm 0xEFC60000}\\ \end{align} $$

とパラメータを設定すると、周期が $2^{19937}-1$ で 623-distribution を 32 ビットで満足するようになります。このパラメータが設定されたメルセンヌ・ツイスタは MT19937 と呼ばれます[8][10]。

詳細と性質

さて、 $\boldsymbol x_{k+n}$ を計算するには、$\boldsymbol{x}_{k+m}, \boldsymbol{x}_{k+1}, \boldsymbol{x}_k$ が必要でした。したがって、$\boldsymbol x_{k+n-1}$ から $\boldsymbol{x}_k$ までを状態として保存しておく必要があります。これは、長さ $n$ の配列で、$k\bmod n$ の位置にヘッドがくるようにすればよいです。

また、初期状態 $\boldsymbol x_0, \boldsymbol x_1,\dots, \boldsymbol x_{n-1}$ をどうするかという問題があります。実は、$\boldsymbol x_1,\dots,\boldsymbol x_{n-1}$ および $\boldsymbol x_0$ の上位 $w – r$ ビットが全て $0$ である場合を除けば、どの初期状態からでも $2^{19937}-1$ 通りの内部状態全てを巡ります[8]。このことは、次のように説明できます。

MT19937 の内部状態のうち以降生成される疑似乱数に関わるビットは $\boldsymbol x_{k+1},\dots,\boldsymbol x_{k+n-1}$ と$\boldsymbol x_k$ の上位 $w-r$ ビットの合計 $n\times w-r=19937$ ビットです。$19937$ ビットの列は $2^{19937}$ 通り存在します。ここで、$\boldsymbol{0}=\boldsymbol{0}\oplus \boldsymbol{0}A$ なので、$19937$ ビットが全て $0$ である場合、内部状態はずっと $19937$ ビットの$0$ であることがわかります。したがって、周期 $2^{19937}-1$ は以降生成される疑似乱数に関わる $19937$ ビットについて、全て $0$ である場合を除いてありうる全ての状態を巡ることを意味しています。

上の議論から周期 $2^{19937}-1$ というのは、$624\times 32$ ビットの内部状態に対して殆ど限界といえる性能をしていることがわかります。このことは623次元に一様分布するという性質についても同様です。

$k(v)$ と周期$P$ に関して、$2^{k(v)v}-1\leq P$ という不等式が成り立ちます[7]。もし $2^{k(v)v}-1>P$ だと、$P$ 個のベクトルで $k(v)v$ ビットベクトルの全て($\boldsymbol 0$ の登場回数は 1 少ない)をカバーできないからです。 ここで、MT19937について $v=w=32$ で考えてみると、$2^{32k(32)}-1\leq 2^{19937}-1$ なので、$32k(32)\leq 19937$ となり、$k(32)\leq 623$ がわかります。

ここまで説明したように、メルセンヌ・ツイスタは非常に優れた性質を持っています。しかし、連続する 624 個の疑似乱数を取得すれば、内部状態が再現できてしまい以降の疑似乱数が予測できてしまいます[11]。これは、Tempering に用いる $T$ が正則であることから内部状態 $\boldsymbol x_i$ が取得できるからです。

暗号論的疑似乱数へ

暗号文は決定的に生成されるべきでなく、暗号は乱択アルゴリズムであることが望まれます[5]。

しかし、ここまで紹介してきた疑似乱数生成器のように次の乱数が予測可能であることは、暗号に利用する上で問題となります[1]。例えば、共通鍵の推測による解読や、初期化ベクトルの推測による暗号化プロセスのシミュレートにつながります。

このように、暗号の用途で使われる乱数はシミュレーションなどで使われる擬似乱数とは異な る性質が要求されるため、通常の擬似乱数と区別するために暗号論的擬似乱数と呼ばれます(暗号に利用する前提なら単に疑似乱数ということもあります)[1]。

次回は、暗号論的疑似乱数に求められる性質やその構成について解説します。

まとめ

  • コンピュータで乱数を扱うため、疑似乱数というものを考える
  • 疑似乱数を生成するアルゴリズムのなかには、平方採中法、線形合同法、メルセンヌ・ツイスタがある。また、それらの仕組みについて説明した
  • 疑似乱数にとって、周期と分布は重要な性質である
  • ここで紹介した疑似乱数は暗号利用されるべきでなく、暗号利用するには暗号論的疑似乱数を用いるべき

参考文献

  1. 光成滋生, “クラウドを支えるこれからの暗号技術”, 2021. https://herumi.github.io/ango/
  2. Rabin, Michael O. “Probabilistic algorithm for testing primality.” Journal of number theory  12.1 (1980): 128-138. https://www.sciencedirect.com/science/article/pii/0022314X80900840
  3. J. Von Neumann, “Various techniques used in connection with random digits,” Applied Math Series, Notes by G. E. Forsythe, in National Bureau of Standards, Vol. 12, pp. 36-38, 1951. https://mcnp.lanl.gov/pdf_files/InBook_Computing_1961_Neumann_JohnVonNeumannCollectedWorks_VariousTechniquesUsedinConnectionwithRandomDigits.pdf
  4. 二乗中抜き法の欠点とTAOCP https://qiita.com/saka1_p/items/30ad1770d26e60e630b4
  5. Park, Stephen K., and Keith W. Miller. “Random number generators: good ones are hard to find.” Communications of the ACM 31.10 (1988): 1192-1201. https://dl.acm.org/doi/pdf/10.1145/63039.63042
  6. 線形合同法(擬似乱数生成法)の周期****,**** tsujimotterのノートブック. https://tsujimotter.hatenablog.com/entry/period-of-linear-congruential-generators
  7. 松本眞, “あなたの使っている乱数、大丈夫?-危ない標準乱数と、メルセンヌ・ツイスター開発秘話-,” 第50回市村学術賞記念 先端技術講演会(2014) http://www.math.sci.hiroshima-u.ac.jp/m-mat/TEACH/ichimura-sho-koen.pdf
  8. Matsumoto Makoto, and Takuji Nishimura. “Mersenne twister: a 623-dimensionally equidistributed uniform pseudo-random number generator.” ACM Transactions on Modeling and Computer Simulation (TOMACS) 8.1 (1998): 3-30. https://dl.acm.org/doi/pdf/10.1145/272991.272995
  9. Mersenne Twister with improved initialization (2002) http://www.math.sci.hiroshima-u.ac.jp/m-mat/MT/MT2002/mt19937ar.html
  10. メルセンヌ・ツイスタをわかった気になる https://6715.jp/posts/5/
  11. メルセンヌ・ツイスタを倒す https://6715.jp/posts/6/