这篇回答节选自我在专栏《机器学习中的数学:概率统计》中的文章,一起来聊聊蒙特卡洛方法。
也欢迎关注我的知乎账号 @石溪 ,将持续发布机器学习数学基础及算法应用等方面的精彩内容。
用大样本数据计算出来的频率去估计概率,这就是大数定理的本质,而大数定理思想的一个非常典型的应用就是蒙特卡罗方法。
蒙特卡罗方法,又叫统计模拟方法,名字很洋气,思想很粗暴,真的很管用。它使用随机数来进行场景的模拟或者过程的仿真,其思想核心就是通过模拟出来的大量样本集或者随机过程去近似我们想要研究的实际问题对象,这是一类非常重要的数值计算方法。
该方法的名字来源于世界著名的赌城蒙特卡罗。博彩和概率,二者相视一笑、不谋而合。这种方法最初应用于20世纪40年代美国的曼哈顿原子弹计划,如今在数据分析和机器学习领域中到处都有他的身影。
以下是蒙特卡罗方法的几类非常典型的应用,我们分别来举例介绍
1.近似计算不规则面积/体积/积分
2.模拟随机过程,预测随机过程可能性结果的区间范围
3.结合接受-拒绝采样,对分布的未知参数进行统计推断
要知道,蒙特卡洛方法不仅仅只是一种方法技巧,更是一种思考问题的方式。在这篇回答中,我们主要介绍第一个应用点,让大家进一步理解大数定理的原理和蒙特卡罗方法的要点。第二个和第三个应用如果大家有兴趣可以关注我的专栏。
蒙特卡罗方法可以近似计算出不规则图形的面积,特别对于那些难以用解析方法计算的图像,非常有效。
这里,我们利用蒙特卡罗方法来近似计算一个圆的面积,然后估计出 的近似值,选择圆作为例子的原因不是因为圆无法通过解析法进行计算,而是因为大家对他的面积比较熟悉,方便我们进行结果的对比。
代码片段:
import numpy as np import matplotlib.pyplot as plt from matplotlib.patches import Circle from scipy.stats import uniform import seaborn seaborn.set() n = 100000 r = 1.0 o_x, o_y = (0., 0.) uniform_x = uniform(o_x-r,2*r).rvs(n) uniform_y = uniform(o_y-r,2*r).rvs(n) d_array = np.sqrt((uniform_x - o_x) ** 2 + (uniform_y - o_y) ** 2) res = sum(np.where(d_array < r, 1, 0)) pi = (res / n) /(r**2) * (2*r)**2 fig, ax = plt.subplots(1, 1) ax.plot(uniform_x, uniform_y, 'ro', markersize=0.3) plt.axis('equal') circle = Circle(xy=(o_x, o_y), radius=r, alpha=0.5) ax.add_patch(circle) print('pi={}'.format(pi)) plt.show()
运行结果:
pi = 3.14096
我们结合这张图,详细的分析一下这段程序。
我们近似计算的目标就是这个蓝色的半径为r=1的单位圆面积,而这个单位圆的外接正方形的边长为l=2r=2,因此外接正方形的面积为4。我们生成100000个在外接正方形内均匀分布的点,均匀的撒下去,这里面的核心原理就是:
这样就可以估算出单位圆的面积了。
而为了估算 ,我们回到这个例子中的参数,可以得到这么一个公式:
而且大数定理告诉我们,随着样本数量的增大,我们用这种方式模拟出来的 值应该是越来越趋近于真实值,样本无穷大的时候收敛于真值。这就证明了应用大数定理的蒙特卡罗方法的合理性和有效性。
至于有同学会问了,是怎么判定一个点是否位于圆当中?这里用的是距离法,也就是计算每个点到原点的距离,如果小于等于半径,就说明这个点在圆内。
这里是不是有一个bug?就是说这个规则只对圆这种特殊图像有效,如果是一个完全不规则的图像,我们如何来计算这个图像的面积?
我们可以借助计算机图像中的像素来进行。比如把不规则图像内部都涂黑,图像外部都留白,我们还是均匀的撒下大样本量的点,如果某个点位于的坐标,他的像素是黑色的,则证明这个点在图像内部,反之就在图像外部,利用这个方法就能统计出位于图像内部的点的个数。
关注 @石溪 知乎账号,分享更多机器学习数学基础精彩内容。
我们来看几个实际的场景,并用随机过程对其进行建模,同时运用蒙特卡罗方法对其过程进行模拟。
3.1.博彩中的随机过程
博彩的例子非常普遍,这里我们举一个大家非常熟悉的例子,我们尽量挖掘的更深入一些。
赌徒和庄家对赌抛硬币,如果为正面,本轮赌徒赢,庄家付给赌徒1元,结果为反面,本轮赌徒输,赌徒付给庄家1元。赌徒有初始赌本10元,手上的钱一旦输光则退出赌局,如何来模拟这个博彩过程?
我们首先来分析一下这个过程,赌徒的博彩结果本质上依托于每次抛掷硬币的结果,每一轮博彩就是一个伯努利试验,赢的概率是p=0.5,博彩的过程就是由这一串伯努利试验构成的伯努利随机过程,每轮赌局中,如果赢则赌本增加1元,输则赌本减少1元。
当然了,如果对某一个特定的赌徒,一旦开始进入赌局,则最终由每轮赌局结果构成的序列就是唯一的。那么如果我们想观察整个博彩过程的整体特征,我们该怎么办?好办,还是使用之前讲过的蒙特卡罗方法,采用大量的样本,最终观察样本结果的整体特征。
这里,我们为了说明问题,先采用的样本数也就是赌徒数为10个,轮数为100轮,也就是每个赌徒最多和庄家对赌100轮,如果在这个过程中输光了赌本,则提前退出,如果到100轮还有赌本,赌局也停止。
我们来看一下模拟的代码:
代码片段:
import pandas as pd import random sample_list = [] round_num = 100 person_num = 10 for person in range(1, person_num + 1): money = 10 for round in range(1, round_num + 1): result = random.randint(0, 1) if result == 1: money = money + 1 elif result == 0: money = money - 1 if money == 0: break sample_list.append([person, round, money]) sample_df = pd.DataFrame(sample_list, columns=['person', 'round', 'money']) sample_df.set_index('person',inplace=True) print(sample_df)
运行结果:
round money person 1 64 0 2 100 14 3 90 0 4 100 16 5 88 0 6 100 14 7 100 6 8 78 0 9 52 0 10 18 0
我们简单的分析一些代码和运行结果,其实这段代码中最核心的部分就是:
result = random.randint(0, 1)
在每轮赌局中,我们首先生成了一个随机变量,他在0和1当中等概论选取,模拟的就是抛掷硬币的过程,当结果为1时表示硬币为正面,本轮赢庄家1元,结果为0时表示硬币为反面,本轮输庄家1元。这里设定的是每个赌徒最多赌100轮,如果不到100轮就输光了,则退场,这样就模拟了整个博彩的随机过程。
从运行结果来看,10个赌徒中有6个提前输光退场,剩下的4个打满全场的人中,有3个是挣钱的,有1个是亏钱的。
当然,我们这里是为了打印出所有的结果,所以样本数选择的比较少。蒙特卡洛方法讲求大的样本量,我们把样本数和轮数都修改一下,并且统计一些指标。我们把赌徒的总人数设置为1000000人,轮数设置为100(我们依次修改了轮数为100,1000,10000),来观察样本总体的表现:
代码片段:
import pandas as pd import random sample_list = [] person_num = 100000 round_num = 10000 for person in range(1, person_num + 1): money = 10 for round in range(1, round_num + 1): result = random.randint(0, 1) if result == 1: money = money + 1 elif result == 0: money = money - 1 if money == 0: break sample_list.append([person, round, money]) sample_df = pd.DataFrame(sample_list, columns=['person', 'round', 'money']) sample_df.set_index('person',inplace=True) print("总轮数:{},总人数:{}".format(round_num,person_num)) print("输光赌本提前出局的人数:{}".format(person_num-len(sample_df[sample_df['round']==round_num]))) print("赌满全场且盈利的人数:{}".format(len(sample_df[sample_df['money']>10]))) print("赌满全场且亏损的人数:{}".format(len(sample_df[sample_df['money']<=10][sample_df['money']>0])))
运行结果:
总轮数:100,总人数:100000 输光赌本提前出局的人数:31148 赌满全场且盈利的人数:44458 赌满全场且亏损的人数:23923
总轮数:1000,总人数:100000 输光赌本提前出局的人数:75154 赌满全场且盈利的人数:23441 赌满全场且亏损的人数:1386
总轮数:10000,总人数:100000 输光赌本提前出局的人数:91902 赌满全场且盈利的人数:8060 赌满全场且亏损的人数:38
从结果中不难发现,这种和庄家1:1的对赌,随着轮数的增加,基本上都破产被收割了。换句话说,哪怕庄家不出千,输赢概率各半,赌的越久,基本上都是输光破产走人,原因是什么?原因是庄家的资金量是无穷的。
3.2.模拟股价的变化
博彩的例子结束了,我们再举一个股票的例子,这个让人感觉都很心跳啊,在金融工程中,有下面这样一个公式,他利用目前的股价 去预测 时间之后的股价 :
这其中的参数我来解释一下:
表示股票收益率的期望值,这里我们设定为 ,即
表示股票的波动率,这里设定为
,其中 表示整数年份, 表示在整个估算周期内,取的具体步数,就好比说 为一年, 如果取244,那么 的粒度就是每个交易日了(一年有244个交易日)。
这里面似乎所有的参数都是确定的,唯独除了 之外, 是一个服从标准正态分布的随机变量,这是这个 ,决定了每日的股价 是一个随机变量,而由股价构成的序列是一个随机过程。
我们同样的用蒙特卡罗方法,利用大样本来估计一下在目前股价为 的情况下,1年之后股价的概率分布情况。
代码片段:
import scipy import matplotlib.pyplot as plt import seaborn from math import sqrt seaborn.set() s0 = 10.0 T = 1.0 n = 244 * T mu = 0.15 sigma = 0.2 n_simulation = 10000 dt = T/n s_array = [] for i in range(n_simulation): s = s0 for j in range(int(n)): e = scipy.random.normal() s = s+mu*s*dt+sigma*s*e*sqrt(dt) s_array.append(s) plt.hist(s_array, bins=30, normed=True, edgecolor='k') plt.show()
运行结果:
这是我们模拟了10000个样本经过上述随机过程,在一年之后股价的分布情况。
这里的核心就是:
e = scipy.random.normal() s = s+mu*s*dt+sigma*s*e*sqrt(dt)
每一轮 ,我们都生成一个服从标准正态分布的随机变量 ,不断通过递推公式 ,迭代出下一个时间点的股价,循环往复直到生成一年后的最终结果,这样就模拟出了一年过程中股价随机变量序列构成的随机过程。
我们采用蒙特卡罗方法,设置大样本量(这里设置10000个),最终迭代出10000个对应的一年后股价,然后用柱状图就能看出其总体分布特征。
3.3.股价变化曲线的过程展现
上面我们分析的是这10000个样本在一年之后最终股价的整体分布情况,实际上我们还有一个同样重要的过程可以进行监测和展现,那就是从 时刻起到1年后的这一段时间内,每隔 时间间隔点由实时价格随机变量构成的序列,换句话说就是随机过程的整体展现。
这个在上面的基础上进一步完成,其实不难,就是我们不光要计算出股票最终的价格,还有记录下每个 时间点的价格,并把他记录下来。
代码片段:
import scipy import matplotlib.pyplot as plt import seaborn from math import sqrt import numpy as np seaborn.set() s0 = 10.0 T = 1.0 n = 244 * T mu = 0.15 sigma = 0.2 n_simulation = 100 dt = T/n random_series = np.zeros(int(n), dtype=float) x = range(0, int(n)) for i in range(n_simulation): random_series[0] = s0 for j in range(1,int(n)): e = scipy.random.normal() random_series[j] = random_series[j-1]+mu*random_series[j-1]*dt+sigma*random_series[j-1]*e*sqrt(dt) plt.plot(x, random_series) plt.show()
运行结果:
这里我们清晰的展现出了由244个 时间点的股价数据所构成的序列,这是随着时间变化的随机过程。我们为了从整体把握他的分布特征,设定了100个样本,因此模拟出了100条价格曲线。
这种结果图表面上看起来比较凌乱,实际上可以从整体上发现许多端倪,例如股价在运行过程中的整体分布区间,上下界,集中程度等等,都可以有一个整体的把握。
因此我们不仅可以得到最终股价的分布,也可以知道股价运行变化的完整价格路径,这个价格路径代表了蒙特卡罗方法的精髓,通过这个价格路径的可视化呈现,让我们更加直观的从宏观上把握了随机过程。
4.1.运用蒙特卡洛方法的背景
随机近似方法的核心是蒙特卡洛方法,主要是用采样的方式来进行随机近似,来实现数值积分等目标。
例如我们要求函数 关于分布 的期望,从期望的定义我们知道,求期望的本质就是求积分:
但是这个积分往往是非常难求的,而随机近似的方法核心就是蒙特卡洛方法,他用采样的方法来实现数值积分的目标:
首先我们从原始分布 中采出 个样本点:
然后依据大数定律,用样本均值来模拟积分的真实结果:
如果我们直接令这个函数 ,那么通过上述方法求出的就是分布 的期望。
这种基于蒙特卡洛方法的近似方法,思想上非常简单直观,但是其中有一个问题目前看上去无法跨越,那就是如果 比较复杂,我们似乎很难从中采样出服从概率分布的一组样本点。
于是,为了解决这个问题,就派生出了两种方案,一个称之为接受-拒绝采样,一个称之为是重要性采样,我们这里重点谈谈接受-拒绝采样。
他们都是基于一个事实,那就是目标分布 无法直接采样,而我们引入一个提议分布 ,什么叫提议分布?换句话说就是我们好采样的分布,这个分布可以是任意的,比如均匀分布、高斯分布等等,怎么好采样就怎么来。
4.2.接受-拒绝采样的问题背景
这里我们就来重点介绍接受-拒绝采样方法,虽然在最终实际使用过程中,因为效率、维度等原因,不会实际采用他进行采样,但是他所折射出来的思想方法将直接指导马尔科夫链-蒙特卡洛方法的理解和应用,因此很有必要对其进行深入学习。
接下来看一个具体的例子,我们需要采样的目标分布就是下面这个复杂的概率密度函数:
当然,写的更简洁、专业一点就是:
其中, 是归一化参数,目的是为了让概率密度函数 在整个取值域上的积分为 ,以满足归一化的概率基本定义。
4.3.采样方法和步骤
看上去这个概率密度函数非常复杂,那么针对这个概率密度函数的分布,我们如何进行采样,才能使得样本能够满足这个分布要求,从而支撑我们的后续的统计分析呢?这里,我们介绍最为基本、也最为经典的接受-拒绝采样方法,他的根本目标就是根据给定的概率密度函数 ,产生服从目标分布的样本集 。
在开始采样之前,我们需要做一些准备工作。首先需要找到一个辅助的建议分布,我们将其记作是 ,他的概率密度函数为 ,这个 分布在原理上可以是任意分布,既然是可以任意选择,那么我们就应该去选择一个易于处理、易于通过计算机程序去采样的分布类型,可以说是越简单越好。显然在这里,均匀分布、正态分布是最为合适的。
同时我们还需获取一个常数值 ,使得对于任意变量 ,都能够保证 成立,当然为了提高采样的效率,选取的常数 在满足条件的前提下,当然是越小越好了。
接下来就是真正采样的过程:
第一步:从建议分布 中进行采样,获取一个采样样本 ;
第二步:从 的均匀分布中进行采样,获取一个采样样本 ;
第三步:判断,如果 ,那么我们就接受这个采样值 ,作为我们记录下来的采样值,如果不满足这个不等关系,我们就拒绝这个采样值,舍弃他,重新采样。这就是所谓的接受-拒绝的关键一步。
4.4.接受-拒绝采样的实践
我们在这个例子中,选择均值为1.4,方差为1.2的正态分布作为我们的建议分布: 分布,取常数 值为2.5。
我们先把目标分布的概率密度函数 、建议分布 的概率密度函数的 倍: 绘制在一张图上进行比较:
代码片段:
import numpy as np import matplotlib.pyplot as plt from scipy.stats import norm import seaborn seaborn.set() # 目标采样分布的概率密度函数 def p(x): return (0.3 * np.exp(-(x - 0.3) ** 2) + 0.7 * np.exp(-(x - 2.) ** 2 / 0.3)) / 1.2113 # 建议分布G norm_rv = norm(loc=1.4, scale=1.2) # C值 C = 2.5 x = np.arange(-4., 6., 0.01) plt.plot(x, p(x), color='r', lw=5, label='p(x)') plt.plot(x, C*norm_rv.pdf(x), color='b', lw=5, label='C*g(x)') plt.legend() plt.show()
运行结果:
从图中可以看出,我们所选取的建议分布 的概率密度函数的 倍,确实是恒大于目标分布的概率密度函数 ,因此满足我们接受-拒绝采样的前提条件。
那么接下来,我们就在此基础上,利用上文中所介绍的接受-拒绝采样算法的具体步骤,来进行实际的样本采样。
代码片段:
import numpy as np import matplotlib.pyplot as plt from scipy.stats import uniform, norm import seaborn seaborn.set() # 目标采样分布的概率密度函数 def p(x): return (0.3 * np.exp(-(x - 0.3) ** 2) + 0.7 * np.exp(-(x - 2.) ** 2 / 0.3)) / 1.2113 # 建议分布 norm_rv = norm(loc=1.4, scale=1.2) # C值 C = 2.5 uniform_rv = uniform(loc=0, scale=1) sample = [] for i in range(100000): Y = norm_rv.rvs(1)[0] U = uniform_rv.rvs(1)[0] if p(Y) >= U * C * norm_rv.pdf(Y): sample.append(Y) x = np.arange(-3., 5., 0.01) plt.gca().axes.set_xlim(-3, 5) plt.plot(x, p(x), color='r') plt.hist(sample, color='b', bins=150, normed=True, edgecolor='k') plt.show()
运行结果:
我们通过接受-拒绝采样方法,进行了10万次的数据采样,最后把采样结果绘制成柱状图。通过运行结果我们发现,柱状图的形状几乎和原始目标分布的概率密度函数曲线完全拟合。由此通过实践证明了这个采样的方法确实是有效的,他高度近似的拟合了原始样本的分布特性。
有了高度近似于实际分布的采样值之后,我们就能够很容易的利用这组样本来对原始的分布进行统计特征的分析,比如均值、方差等数字特征的计算。
当然还有《机器学习中的数学(全集)》系列专栏,欢迎大家阅读,配合食用,效果更佳~
有订阅的问题可咨询微信:zhangyumeng0422