モンテカルロ法による積分

このページでは、乱数を使った「計算」の例として、モンテカルロ法による数値積分を考える。

超球の体積の計算

誰でも知っているとおり、半径1の円の面積はπ, 半径1の球の体積は 4π3 である。 これをさらに一般化して、 d次元の半径1の球の体積は、 Vd=πd/2Γ(d2+1) で与えられる。 ここでΓ()ガンマ関数である。 例えば、d=1の場合は、V1=2であり、次元の増加とともに、Vdは一旦増加するが、d=6以上では減少に転じる。

もしこの公式を知らなかったとしたら、d次元球の体積をどのように見積もったら良いだろうか。

例えば、(二次元中の)半径1の円を考えてみよう。 そして、その面積をSとする。 xに0から1の範囲の一様乱数、yも同様とすると、点(x,y)は面積1の正方形の中に一様に分布するはずである。 そして、そのうちで、角度π/4の扇形 に入っているものの割合は、正方形と扇形の面積の比率、すなわちS/4となるだろう。 そうすると、N個の点を生成し、そのうち扇型に入っているものをカウント数した結果がmとすれば、 mNS4 すなわち、 S4mN と求まる。

ここでの発想のポイントは、ひとつには、面積が既知の図形(正方形)を基準にカウントすること、加えて、乱数の分布の一様性にある。

全く同様の発想で、d次元の球の体積を求めることができるはずだ。 辺の長さが1のd次元の立方体を考えると、その体積は1である。 その中に占める、d次元球の1/2dの区画の割合から、超球の体積を推定するコードの例を以下に示す:

# coding: utf-8

import numpy as np
import math

d = 3

N=100000
cnt=0
for _ in range(N):
    x = np.random.rand(d)
    if np.linalg.norm(x) < 1:
        cnt += 1

v = cnt/N * 2**d
print('v=',v)

このコードの中で、NumPyのnp.random.rand(d)関数は、0から1の範囲の一様乱数をd個生成し、そのリストを返す。 また、np.linalg.norm(x)は、リストxで与えられるベクトルの長さ(L2ノルム)を与える関数である。

icon-pc 練習:球の体積

実際に上のコードを動作させ、数学的な(厳密な)結果と比較してみなさい。

icon-teacher 解説: 結果のゆらぎ

超球の体積をVとすれば、ランダムに生成された点が超球の内部に位置する確率はp=Vである。 N回のサンプリングを行った場合、点が超球の内部にある回数は二項分布で与えられるので、その平均は、Np、分散はNp(1p)である。 すると、体積の推定値のゆらぎは p(1p)N となる。ここで、ゆらぎ(誤差)のN依存性はdには依存しない。

高次元での積分を、各次元ごとにm個のデータ点を使って計算しようとすると、データ点の総数はNmd程度で増大することになる。 例えば、台形法を使った場合の誤差はδ1/m2程度となるので、 δ1/m2N2/d であり、次元数dが大きくなると、同じ程度の計算精度を担保するに必要となるNは膨大となる。 言い換えると、高次元の問題になればなるほど、データ点数の観点からは、次元に関係なく1/N程度で誤差(ゆらぎ)を減らすことができるモンテカルロ法が有利となってくる。

重点サンプリング

積分 I=f(x)dx の計算をモンテカルロ法で行う別の方法を考えてみよう。

ある素性の良い確率分布(提案分布)q(x)を用いて、 f(x)dx=f(x)q(x)q(x)dx のように置き直すと、この積分は q(x) についての w(x)=f(x)q(x) の平均を求める計算に帰着される。 よって、q(x) の生成確率で乱数 Xk を発生させることによってw(Xk)をサンプルすれば、 中心極限定理から I1Nk=1Nw(Xk) によって積分の近似値を得ることができる。 この方法は、q(x)による確率的な荷重によってサンプリングを行うことから、重点サンプリング(importance sampling)と呼ばれている。 いわば、提案分布を「ものさし」にして積分を評価するわけである。

では、上記の線に沿って d 次元の超球の体積を求めてみよう。

まず、半径1の球を表すために、原点からの距離rに依存した分布 f(r)={1(r<1)0otherwise と考えると、この体積積分が超球の体積となる。

他方で、提案分布としてガウス分布 q(r)=1πd/2er2 を考えてみる(平均0、分散1/2)。

そして、q(r)の確率密度を持つようなd次元の正規乱数(x1,x2,,xd)を発生させ、 r=x12+x22++xd2<1ならば w=1/q(r) の、1rならば w=0 の重み付けを行って、点をサンプルすることにしよう。 このとき、サンプル数総数をNとし、wkk番目の重みとすれば、f(r)の積分、すなわち半径1の超球の体積 VdVdkwkN で求められる。

# coding: utf-8
import numpy as np
import math

def normal(r,d):
    return math.exp(-r**2) / math.sqrt(math.pi**d)

d = 10

N=100000
wt=0.0
for _ in range(N):
  x = np.random.normal(loc=0, scale=1/math.sqrt(2), size=d)
  r = np.linalg.norm(x)
  if r < 1:
    wt += 1/normal(r,d)

v = wt / N

v0 = math.pi**(d/2)/math.gamma(d/2+1)

print('v=',v,v0)

icon-teacher 解説: 高次元でのサンプリングの難しさ

上記のサンプル方法でどの程度効率的に積分が評価できるか考察してみよう。

簡単のため1次元の場合をまず考えて、q(x)=12πs2exp(x2/(2s2)) と置いてみる。 そして、サンプリングの効率(誤差)の指標として、w(x) の分散を見積もることにする。

w(x)の平均(すなわち1次元球の体積)は、 W¯=1+11q(x)q(x)dx=2 である。 他方で、w(x)の二乗平均は W2¯=1+1(1q(x))2q(x)dx=2πs21+1exp(x22s2)dx となる。 これから、標準偏差sの関数としてw(x)の分散 W2¯W¯2 を数値計算してプロットすると、以下のようになる。 この値が小さいほど、より「的確」なサンプル値が得られるため、少ないサンプル回数でも良好な推定値が得られると期待できる。 プロットからは、sには最適値があり、それを外れると分散が急激に増大することがわかる。 そして、最適なsとは、提案分布が、f(x)になるべく近いかたちになるような値、と解釈できる。

こちらのプロットにはMapleを使用した。

d次元の場合にも全く同様の議論が可能であって、 W¯d次元球の体積 Vd, すなわち W¯=Vd w(x)の二乗平均は、被積分関数が方向に依存しないことから、動径方向のみの積分を考えれば十分で、 W2¯=(2πs2)d/2dVd01exp(r22s2)rd1dr となる。 これらを用いて、w(x)の相対的なゆらぎの指標 W2¯W¯2W¯2d=8の場合について計算し、プロットした例を以下に示す。

この様子からは、提案分布q(x)の選択の仕方によって、効率(計算誤差)は大きく左右されるだろうと予想される。 さらに、この傾向は次元dの増大と共に一層顕著となる。

icon-pc 練習:サンプリング効率の比較

超球の体積を求める際、次元dが大きくなると、サンプル点が球の内側に入って「採択」される割合が急激に減少するため、著しく計算の効率と精度が低下してしまうことが知られている。

重点サンプリングで体積を求めるコードの例では、 提案分布q(r)として分散が1/2の正規分布を用いたが、その分散を変えられるようにし、計算の効率や精度が分散の大小によってどのように変わるか実験してみなさい。