LDS(2)

http://d.hatena.ne.jp/m-a-o/20091212#p3
の続き。


前回のあらすじ。ある領域上で定義された関数の積分を計算するには、その領域で一様分布する数列に対する関数値の平均を取ればいい。一様分布する数列としては乱数がよく使われてモンテカルロ法として知られている。けども、乱数でなくても、一様分布する数列は存在する。存在するだけでなく、乱数より"ずっと速く一様分布に収束する"数列が存在してLow-discrepancy sequence(LDS)と呼ばれている。LDSを使ってモンテカルロ積分は、乱数を使ったモンテカルロ積分よりも収束が速くなる(Koksma-Hlawkaの定理)。


LDSを使ったモンテカルロ法は、準モンテカルロ法と呼ばれるらしい。けど、モンテカルロ法を最初に行ったvon Neumann-Ulamが使った数列は、そもそも乱数ではなかったので、全部モンテカルロでいいじゃんとか思う


積分領域が一次元の場合は、被積分関数のクラスに応じて、よい方法が沢山あるので、よっぽど病的な関数ばっかを扱うとかでない限り、ありがたみはそこまでないと思う。ので、必然的に高次元LDSを考える。高次元LDSを作る方法としては、「互いに相関のない」1次元LDSを並べるというのが、一つある。この方向で、van der Corput列を並べたのがHalton列。有理数体上一次独立な回転角を持つ無理数回転を並べたものを、Kronecker列とか呼ぶらしい。


実用上は、Faure列とかSobol列とかいうものがよく使われるらしい。Owenという人が考えたScramblingという操作を行ったものを、一般化Faure列とか一般化Sobol列とかいうらしいけど、これは、Faure列とか、Sobol列とかは、隣の点は近くにあることが多いので、一般には積分が収束していく時の振動幅が大きくなったり偏りが生じがちだけど、適当に順番入れ替えることで、そこらへんを改善できるという感じなのだと思う。ユーザ的には、一般化Faure列使っとけばいいんじゃね?という感じっぽい。


まあ、scramblingはいいとして、Faure列の定義は分かりやすい。Faure列がLDSであることを示すには、Niederreiterが導入した(t,s)-sequenceの概念を知っていれば比較的簡単(むしろ、(t,s)-sequenceの定義は、Faure列とか見て思いついたんだと思う)。


(t,s)-sequenceの定義は何も知らないで見たら意味不明だけど、要は、一様分布するということは、ある領域に含まれる点の数がその領域の体積に比例していればよいということを言ってるに過ぎない。とはいえ、有限個の点を取ってきた場合、適当な領域を取ったら、1個も点が含まれていないということは起こるので、代わりに基本区間というものを考える。van der Corput列とかの時と同じ意味でbを基数とすると、基本区間は、超直方体(?)で、各辺の座標軸への射影が、区間[a/b^j,(a+1)/b^j]になってるようなもの。で、最初のb^m個の点のうち、この基本区間に入っている点の数が、基本区間の体積に比例していればいいよねとかいうのが、アイデア


(t,s)-sequence自体は、数列の性質を述べたに過ぎないので、その性質を満たす数列がどのくらい存在するかとか、どうやって構成するかとかは別問題だけど、Niederreiterは、(t,s)-sequenceを構成する体系的な方法を与えている。Faure列とかSobol列も(t,s)-sequenceで、Niederreiterの構成の特殊な場合とみなせる。


試しに7次元領域上の関数
(1*2*3*4*5*6*7)*cos(x1)*cos(2*x2)*cos(3*x3)*cos(4*x4)*cos(5*x5)*cos(6*x6)*cos(7*x7)
の数値積分を、乱数・Halton列・Faure列・Kronecker列で行って、積分誤差を比較してみた。Nは、サンプル点の数で、結果。

N = 1000
乱数: 9.9055483122
Halton: 9.15376775417
Faure: 5.72693149852
Kronecker: 3.04231847824

N = 10000
乱数: 6.46412740778
Halton: 1.11944591219
Faure: 4.12439321412
Kronecker: 1.70737628191

N = 100000
乱数: 0.21314196244
Halton: 0.13139014844
Faure: 0.189523901958
Kronecker: 0.245784242938

N = 500000
乱数: 0.478886945071
Halton: 0.34728002196
Faure: 0.0320830069256
Kronecker: 0.0997440714485

被積分関数がKronecker列に有利だったり不利だったりするんじゃないかという疑惑があるが、この程度の次元数だと、乱数も頑張ってるし、あんまりどの方法でも差がでない。あと、Faure列とかHalton列の生成が結構遅い(実装のせいだけど


以下、実装。

from random import Random
import time
from math import acos,pi

#一次元LDS
class vanderCorputSequence:
    def __init__(self , b):
        self.base = b
        self.n = 0
    def __iter__(self):
        return self
    def __getitem__(self,n):
        cs = []
        ret = 0.0
        n = self.n
        b = self.base
        while n > 0:
            cs.append(n%b)
            n = n/b
        for a in cs[::-1]:
           ret = ret/b+a
        return ret/b
    def next(self):
        self.n += 1
        return self.__getitem__(self.n)

#前回と少し変更
class WeylSequence:
    def __init__(self, _x):
        self.prev = 1
        self.x = _x
        self.cur = _x
        self.mirror = False
        self.reflect = False
        self.theta = acos(_x)/pi
    def __iter__(self):
        return self
    def next(self):
        val = acos(self.cur)/pi
        if self.mirror:
            ret = 1-val
        else:
            ret = val
        #ret = 1-val if self.mirror else val
        #Chebyshev polynomial
        next = 2*(self.x)*(self.cur) - (self.prev)
        self.prev = self.cur
        self.cur = next
        if val < self.theta or 1-val < self.theta:
             if self.reflect:
                 self.reflect = False
             else:
                 self.mirror = not(self.mirror)
                 self.reflect = True
        return ret


#高次元LDS
#pure random number
class RandomSequence:
    def __init__(self , dimension):
        self.generators = [Random(time.time()) for i in xrange(dimension)]
    def __iter__(self):
        return self
    def next(self):
        return tuple([g.random() for g in self.generators])

#Halton sequence
class HaltonSequence:
    def __init__(self , dimension):
        p = 2
        primes = []
        while True:
            if(len(primes)==dimension):break
            for i in xrange(2,p):
               if p%i==0:break
            else:
               primes.append(p)
            p += 1
        self.generators = [vanderCorputSequence(p) for p in primes]
    def __iter__(self):
        return self
    def next(self):
        return tuple([s.next() for s in self.generators])

#Kronecker
class KroneckerSequence:
    def __init__(self , dimension):
        self.generators = [WeylSequence(0.01-float(2*i+1)/(2*dimension+1)) for i in xrange(dimension)]
    def __iter__(self):
        return self
    def next(self):
        return tuple([s.next() for s in self.generators])


def combination(n,k):
   if n==0 or k==0 or n==k:return 1
   else:
     return combination(n-1,k-1)+combination(n-1,k)

#Faure
class FaureSequence:
    def __init__(self , dimension):
        p = dimension+1
        while True:
           for i in xrange(2,p):
               if p%i==0:
                   p += 1
                   break
           else:
               self.base = p
               self.n = 0
               self.dimension = dimension
               return None
    def __iter__(self):
        return self
    def __getitem__(self,n):
        cs = []
        ret = []
        n = self.n
        b = self.base
        dimension = self.dimension
        while n > 0:
            cs.append(n%b)
            n = n/b
        for i in xrange(dimension):
            val = 0.0
            for a in cs[::-1]:
                val = val/b + a
            ret.append( val/b )
            tmp = [i for i in cs]
            cs = []
            for i in xrange(len(tmp)):
                a = 0
                for j in xrange(i,len(tmp)):
                    a += combination(j,i)*tmp[j]
                cs.append( a%b )
        return tuple(ret)
    def next(self):
        self.n += 1
        return self.__getitem__(self.n)


def monteCarloIntegral(fun , seq , N):
    val = 0.0
    for (n , v) in enumerate(seq):
        if n==N:break
        val += apply(fun , v)
    return val/N


if __name__=="__main__":
    from math import exp,sin,cos
    def f(x1,x2,x3,x4,x5,x6,x7):
        return 5040*cos(x1)*cos(2*x2)*cos(3*x3)*cos(4*x4)*cos(5*x5)*cos(6*x6)*cos(7*x7)
    D = 7
    exactval = sin(1)*sin(2)*sin(3)*sin(4)*sin(5)*sin(6)*sin(7)
    for N in [1000 , 10000 , 100000 , 500000]:
        print "N =",N
        seq = RandomSequence(D)
        print "乱数:",abs(exactval - monteCarloIntegral(f , seq , N))
        seq = HaltonSequence(D)
        print "Halton:",abs(exactval - monteCarloIntegral(f , seq , N))
        seq = FaureSequence(D)
        print "Faure:",abs(exactval - monteCarloIntegral(f , seq , N))
        seq = KroneckerSequence(D)
        print "Kronecker:",abs(exactval - monteCarloIntegral(f , seq , N))
        print ""