【python】用蒙塔卡羅方法的重要性採樣估計定積分

前幾天在用蒙特卡洛方法估計定積分的時候,發現中文網站上這方面的資料很少,即使有也沒有說的很詳細,所以這裏專門寫一篇博文記錄自己的學習,僅供大家參考。歡迎指點。

蒙特卡洛方法

蒙特卡羅方法(Monte Carlo method),也稱統計模擬方法,是二十世紀四十年代中期由於科學技術的發展和電子計算機的發明,而被提出的一種以概率統計理論爲指導的一類非常重要的數值計算方法。簡單來說,MCM就是一種使用隨機數(或僞隨機數)來解決計算問題的方法。

舉個例子,用“投針實驗”的方法求圓周率就屬於用蒙特卡洛方法解決問題。所謂“投針試驗”就是我們在一個1*1的正方形區域內隨機採樣若干次,這些點中到原點(任一頂點)的距離在1以爲的概率就是四分之一π,即四分之一圓的面積。就像我們在這個正方形區域內隨機的扔一些“針”,他們落在四分之一圓內的概率就是四分之一圓的面積與正方形面積的比值,即π/4 。

 

求解定積分

蒙特卡洛方法一個很強大的應用,就是求解定積分,尤其是那些難以直接求出數值解的問題。

推導過程如下所示:

一言以蔽之:如果我們用均勻分佈來估計,那麼其實就相當於對區間[a, b]內的被積函數值進行隨機均勻採樣,然後求平均值作爲縱軸長度的期望,接着乘以橫軸上的長度即 (b-a) 就可以得到區間內曲線下面積的期望。

rocks in a box

但是有時候,因爲被積函數值在不同的位置相差較大,那麼此時在值較小的地方採樣就不是很有必要,所以就需要用一個更符合被積函數值波動的分佈來採樣,從而在同樣的採樣次數下達到更精確的值,即上面推導過程中提到的f_X(x)。而f_X(x)要滿足相應兩個條件,其中最主要的就是要保證在積分區間內分佈的概率和爲1。

比如對於求解方程g(x)=x^3在區間[0, 1]之間的定積分值,顯然正態分佈的左半邊和函數值的變化趨勢很相似,所以如果我們用正態分佈採樣而不是均勻分佈,那麼就能夠在更小的採樣次數內得到更精確的估計值。也許有人會問,爲什麼不用直接用符合g(x)=x^3的分佈來採樣?誠然,理論上來說這確實是最佳的採樣方法,但是我們並不能很方便地生成這樣分佈的樣例,然而正態分佈卻可以。如果我們通過精心設計得到這樣特殊分佈的樣例,這上面花費的時間成本以及多出的算法計算複雜度,都不如直接採用正態分佈。所以這其實是一個tradeoff的結果了。

 

具體實現

python正態分佈

這裏首先介紹一下python正態分佈採樣和均分採樣的相關函數。

通過numpy可以直接快速生成正態分佈採樣和均勻分佈的樣本。

import numpy as np
x = np.random.rand(N) # N個[0, 1] 內均分分佈樣本
xis = mu + sig * np.random.randn(N, 1) # N*1個均值和方差爲mu和sig的正態分佈樣本

而對於一維和多維正態分佈的概率密度函數(pdf)和累計分佈函數(cdf),則可以通過scipy來生成。單變量的可以使用scipy.stats.norm,也可以和多變量一起使用scipy.stats.multivariate_normal來生成。

from scipy.stats import multivariate_normal
from scipy.stats import norm

norm.pdf(x-mu, scale=sig)
norm.cdf(x - mu, scale=sig)

rv = multivariate_normal([mux, muy], [[sigx**2, 0], [0, sigy**2]])
rv.cdf([x0, y0])
rv.pdf([x0, y0])

值得一提的是,二維的正態分佈的cdf已經被公認爲沒有數值解了,所以這裏scipy採用的是某種估計方法來得到具體的值,這方面也有很多文獻發表,筆者這裏就不贅述了。經過實驗,可以證實scipy這裏採用的定義是 x <= x_0, y <= y_0 部分的體積。所以求得一個固定範圍內的體積可以通過下面的函數。

def my_cdf(p1, p2, mux, muy, sigx, sigy):
    rv = multivariate_normal([mux, muy], [[sigx**2, 0], [0, sigy**2]])
    return rv.cdf(p1) + rv.cdf(p2) - rv.cdf([p1[0], p2[1]]) - rv.cdf([p2[0], p1[1]])

 

單變量

我們以之前提到的g(x) = x^3 爲例,代碼如下:

import numpy as np
from scipy.stats import norm
import matplotlib.pyplot as plt

its = [5, 10, 20, 30, 40, 50, 60, 70, 80, 100]

mu = 1
sig = .3


def f(x):
    return x**3


def p(x):
    # return (1 / np.sqrt(2 * np.pi * sig ** 2)) * np.exp(-(x - mu) ** 2 / (2.0 * sig ** 2))
    return norm.pdf(x-mu, scale=sig)


def my_norm(x):
    return norm.cdf(x - mu, scale=sig)


results1 = {}
results2 = {}
upper_bound = 1
lower_bound = 0

for it in its:
    r1_list = []
    r2_list = []
    for _ in range(100):
        x = np.random.rand(it)
        r1_list.append(np.mean(f(x)))

        xis = mu + sig * np.random.randn(it, 1)
        xis = xis[(xis < upper_bound) & (xis > lower_bound)]
        if xis.shape[0] == 0:
            continue

        normal = my_norm(upper_bound) - my_norm(lower_bound)
        r2_list.append((upper_bound - lower_bound) * np.mean(f(xis) / (p(xis) / normal)))  # 概率密度函數在採樣區間上的積分需要等於1,所以此處需要除以一個係數,即p(x)在[0 1]上的積分
        # r2_list.append((upper_bound - lower_bound) * np.sum(f(xis) / p(xis)) /it)

    results1[it] = [sum(r1_list) / len(r1_list), np.var(np.array(r1_list))]
    results2[it] = [sum(r2_list) / len(r2_list), np.var(np.array(r2_list))]

print(results1)
print(results2)

這裏 r2_list.append() 的那一行是關鍵,括號內分母p(x)就是之前推導中提到的分佈函數f_X(x),所以爲了保證在區間內該分佈函數的和爲1,我們需要將其除以區間內累積分佈函數的值,也就是計算得到的normal。另外值得一提的是,我們可以通過 樣本在積分區間內的個數/總採樣次數 來對normal進行估計,這兩個式子在採樣次數趨於無窮大的時候是等價的。這爲找不到合適的cdf計算方法時提供了一個解決辦法。但是在採樣次數較小時,這個式子實際上是對上面式子的一個估計,所以在方差上的表現便不如用真正的normal來計算表現得好。

最終的兩種算法的得到的均值都會穩定在真實值1/4左右,且方法隨之採樣次數增加會逐漸減小,如下圖所示(這裏每種採樣次數都進行了100次然後取平均,所以對於橫軸上的100次採樣其實等價於100*100次採樣的結果,之後的圖也一樣)。

但是如果我們把兩種採樣方法的方差都畫出來,就能明顯的看到正態分佈採樣的方差在同樣的採樣次數下穩穩地保持在均勻分佈之下。這還是我們捨棄了一些範圍外樣本的情況。

多變量

多變量我們以 g(x, y) = \frac{y^2 \cdot e^{-y^2} + x^4 \cdot e^{-x^2}}{ x \cdot e^{-x^2}} 在 x \in [2, 4], y \in [-1, 1] 上的定積分爲例。這裏我們可以把被積函數值關於x和y在相應區間內的波動畫出來。注意這裏不能只取一個值,而是(對於所有的x),遍歷y區間計算對應的被積函數值,然後求平均,從而看出一種大概的趨勢。

不難看出,關於x同樣與正態分佈的左半邊類似;而關於y的分佈雖然是對偶函數,但是卻與正態分佈相反。所以我們對x採用正態分佈採樣,對y則採用均勻分佈採樣,因爲兩個變量相對獨立,所以這兩個過程可以分開進行。值得一提的是,這裏因爲y與正態分佈趨勢相反,所以如果對y也採用正態分佈採樣,則會得到一個更大的方差,且計算結果很難穩定下來。代碼如下所示:

import numpy as np
from scipy.stats import multivariate_normal
from scipy.stats import norm
import matplotlib.pyplot as plt

its = [10, 20, 30, 40, 50, 60, 70, 80, 100, 200, 500]

mux = 4
sigx = .6
muy = 0
sigy = .15


def f(x):
    return x * np.exp(-x**2)


def g(x):
    return (x**2) * np.exp(-x**2)


def h(x_y):
    results = []
    for x,y in x_y:
        results.append(g(y)/f(x) + x**3)
    return np.array(results)


def my_pdf(x_y, mux, muy, sigx, sigy):
    x = x_y[:, 0]
    y = x_y[:, 1]
    # print(x)
    pos = np.empty(x.shape + (2,))

    pos[:, 0] = x
    pos[:, 1] = y
    rv = multivariate_normal([mux, muy], [[sigx**2, 0], [0, sigy**2]])
    return rv.pdf(pos)


def my_cdf(p1, p2, mux, muy, sigx, sigy):
    rv = multivariate_normal([mux, muy], [[sigx**2, 0], [0, sigy**2]])
    return rv.cdf(p1) + rv.cdf(p2) - rv.cdf([p1[0], p2[1]]) - rv.cdf([p2[0], p1[1]])


def p(x, mu, sig):
    # return (1 / np.sqrt(2 * np.pi * sig ** 2)) * np.exp(-(x - mu) ** 2 / (2.0 * sig ** 2))
    return norm.pdf(x-mu, scale=sig)


def p_xy(x, mux, sigx, c):
    return p(x, mux, sigx) * c


def my_norm(x, mu, sig):
    return norm.cdf(x - mu, scale=sig)


results = {}
results_rand = {}
upper_bound_x = 4
lower_bound_x = 2
upper_bound_y = 1
lower_bound_y = -1

unit = (upper_bound_x - lower_bound_x) * (upper_bound_y - lower_bound_y)

for it in its:
    r_list = []
    r_list_rand = []
    for _ in range(100):
        xis = mux + sigx * np.random.randn(it, 1)
        xis = xis[(xis < upper_bound_x) & (xis > lower_bound_x)]
        if xis.shape[0] == 0:
            continue
        y = np.random.rand(xis.shape[0])

        normal = (my_norm(upper_bound_x, mux, sigx) - my_norm(lower_bound_x, mux, sigx)) * 1

        x_y = []
        for x_, y_ in zip(xis, y):
            x_y.append([x_, y_])

        # r_list.append(unit * np.sum(h(x_y) / (p_xy(xis, mux, sigx, (upper_bound_y - lower_bound_y))))/it)
        r_list.append(unit * np.mean(h(x_y) / (p_xy(xis, mux, sigx, (upper_bound_y - lower_bound_y)))) * normal)

        # 均勻分佈
        x_rand = np.random.rand(it)
        y_rand = np.random.rand(it)
        x_rand *= 2
        x_rand += 2
        y_rand *= 2
        y_rand -= 1
        x_y_rand = []
        for x, y in zip(x_rand, y_rand):
            x_y_rand.append([x, y])
        x_y_rand = np.array(x_y_rand)
        r_list_rand.append(unit * np.mean(h(x_y_rand)))

    results[it] = [sum(r_list) / len(r_list), np.var(np.array(r_list))]
    results_rand[it] = [sum(r_list_rand) / len(r_list_rand), np.var(np.array(r_list_rand))]
print(results)
print(results_rand)

同樣,採用正態分佈的方差會低於採用均勻分佈的方差。

 

參考資料

https://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.stats.multivariate_normal.html

https://www.cnblogs.com/21207-ihome/p/5269191.html

https://www.zhihu.com/question/66005432

https://blog.csdn.net/u012149181/article/details/78913167

https://blog.csdn.net/hellocsz/article/details/94400402

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章