【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

 

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