我有一些代码使用MCMC(特别是Metropolis Hastings)对后验分布进行采样。我使用scipy生成随机样本:
import numpy as np
from scipy import stats
def get_samples(n):
"""
Generate and return a randomly sampled posterior.
For simplicity, Prior is fixed as Beta(a=2,b=5), Likelihood is fixed as Normal(0,2)
:type n: int
:param n: number of iterations
:rtype: numpy.ndarray
"""
x_t = stats.uniform(0,1).rvs() # initial value
posterior = np.zeros((n,))
for t in range(n):
x_prime = stats.norm(loc=x_t).rvs() # candidate
p1 = stats.beta(a=2,b=5).pdf(x_prime)*stats.norm(loc=0,scale=2).pdf(x_prime) # prior * likelihood
p2 = stats.beta(a=2,b=5).pdf(x_t)*stats.norm(loc=0,scale=2).pdf(x_t) # prior * likelihood
alpha = p1/p2 # ratio
u = stats.uniform(0,1).rvs() # random uniform
if u <= alpha:
x_t = x_prime # accept
posterior[t] = x_t
elif u > alpha:
x_t = x_t # reject
posterior = posterior[np.where(posterior > 0)] # get rid of initial zeros that don't contribute to distribution
return posterior
通常,我会尽量避免在python中使用显式的for循环-我会尝试使用纯numpy生成所有内容。但是,对于这种算法,使用if语句的for循环是不可避免的。因此,代码很慢。当我分析我的代码时,它花费了大部分时间(显然)在for循环内,更具体地说,最慢的部分是生成随机数。 stats.beta().pdf()
和stats.norm().pdf()
。
有时我使用numba来加快我的矩阵运算代码。尽管numba与某些numpy操作兼容,但生成随机数并不是其中之一。 Numba具有cuda rng,但这仅限于正态分布和均匀分布。
我的问题是,是否有一种方法可以使用某种与numba兼容的各种分布的随机采样来显着加快上述代码的速度?
我们不必局限于numba,但这是我所知道的唯一易于使用的优化程序。更笼统地说,我正在寻找在python中的for循环内加快各种分布(β,γ,泊松)随机抽样的方法。
答案 0 :(得分:6)
在开始考虑numba et。之前,您可以对此代码进行很多优化。等(仅通过对算法的实现很精明,我设法使此代码的速度提高了25倍)
首先,您在实施Metropolis-Hastings算法时出现错误。无论您的链条是否移动,都需要保持该方案的每次迭代。也就是说,您需要从代码中删除posterior = posterior[np.where(posterior > 0)]
,并在每个循环的末尾添加posterior[t] = x_t
。
第二,这个例子似乎很奇怪。通常,对于这些类型的推断问题,我们希望根据一些观察来推断分布的参数。但是,在这里,分布的参数是已知的,而您是对观测值进行采样?无论如何,无论如何,我都很乐意为您介绍示例,并向您展示如何加快示例速度。
要开始使用,请从主t
循环中删除与for
的值无关的所有内容。首先从for循环中删除随机游动创新的生成:
x_t = stats.uniform(0,1).rvs()
innov = stats.norm(loc=0).rvs(size=n)
for t in range(n):
x_prime = x_t + innov[t]
当然也可以从for循环中移动u
的随机生成:
x_t = stats.uniform(0,1).rvs()
innov = stats.norm(loc=0).rvs(size=n)
u = np.random.uniform(size=n)
for t in range(n):
x_prime = x_t + innov[t]
...
if u[t] <= alpha:
另一个问题是,您正在计算每个循环中的当前后p2
,这是不必要的。在每个循环中,您需要计算建议的后验p1
,并且在接受建议后,可以将p2
更新为等于p1
:
x_t = stats.uniform(0,1).rvs()
innov = stats.norm(loc=0).rvs(size=n)
u = np.random.uniform(size=n)
p2 = stats.beta(a=2,b=5).pdf(x_t)*stats.norm(loc=0,scale=2).pdf(x_t)
for t in range(n):
x_prime = x_t + innov[t]
p1 = stats.beta(a=2,b=5).pdf(x_prime)*stats.norm(loc=0,scale=2).pdf(x_prime)
...
if u[t] <= alpha:
x_t = x_prime # accept
p2 = p1
posterior[t] = x_t
一个非常小的改进可能是将scipy stats函数直接导入名称空间:
from scipy.stats import norm, beta
另一个非常小的改进是注意到代码中的elif
语句不执行任何操作,因此可以将其删除。
将其放在一起并使用更合理的变量名,我想到了:
from scipy.stats import norm, beta
import numpy as np
def my_get_samples(n, sigma=1):
x_cur = np.random.uniform()
innov = norm.rvs(size=n, scale=sigma)
u = np.random.uniform(size=n)
post_cur = beta.pdf(x_cur, a=2, b=5) * norm.pdf(x_cur, loc=0, scale=2)
posterior = np.zeros(n)
for t in range(n):
x_prop = x_cur + innov[t]
post_prop = beta.pdf(x_prop, a=2, b=5) * norm.pdf(x_prop, loc=0, scale=2)
alpha = post_prop / post_cur
if u[t] <= alpha:
x_cur = x_prop
post_cur = post_prop
posterior[t] = x_cur
return posterior
现在,为了进行速度比较:
%timeit get_samples(1000)
3.19 s ± 5.28 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit my_get_samples(1000)
127 ms ± 484 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
速度提高了25倍
值得注意的是,对于MCMC算法而言,蛮横的速度并不是一切。确实,您感兴趣的是您可以从每秒后验中进行的独立( ish )绘制次数。通常,使用ESS (effective sample size)进行评估。您可以通过调整随机游走来提高算法的效率(从而提高每秒提取的有效样本数)。
要这样做,通常需要进行初次试运行,即samples = my_get_samples(1000)
。根据此输出计算sigma = 2.38**2 * np.var(samples)
。然后,应使用此值将方案中的随机游走调整为innov = norm.rvs(size=n, scale=sigma)
。似乎是任意出现的2.38 ^ 2的起源是:
随机行走都会区的弱收敛性和最佳缩放 算法(1997)。 A. Gelman,W。R. Gilks和G.O. Roberts。
为说明调整可以使我们对算法进行两次运行(一次已调整,另一次未调整)的改进,均使用10000次迭代:
x = my_get_samples(10000)
y = my_get_samples(10000, sigma=0.12)
fig, ax = plt.subplots(1, 2)
ax[0].hist(x, density=True, bins=25, label='Untuned algorithm', color='C0')
ax[1].hist(y, density=True, bins=25, label='Tuned algorithm', color='C1')
ax[0].set_ylabel('density')
ax[0].set_xlabel('x'), ax[1].set_xlabel('x')
fig.legend()
您可以立即看到调整对我们算法效率的改进。记住,两次运行都进行了相同数量的迭代。
如果您的算法收敛时间很长,或者您的样本具有大量的自相关,那么我考虑考虑使用Cython
来寻求进一步的速度优化。
我还建议您检出PyStan
项目。它需要一点时间来适应,但是它的NUTS HMC算法可能会胜过您可以手动编写的任何Metropolis-Hastings算法。
答案 1 :(得分:1)
不幸的是,除了用numba兼容的python代码自己重写随机分布之外,我真的看不到有任何方法可以加快随机分布。
但是,加快代码瓶颈的一种简单方法是将对stats函数的两个调用替换为两个一对一的调用:
p1, p2 = (
stats.beta(a=2, b=5).pdf([x_prime, x_t])
* stats.norm(loc=0, scale=2).pdf([x_prime, x_t]))
另一小调整可能是将u
的生成工作外包给for循环:
x_t = stats.uniform(0, 1).rvs() # initial value
posterior = np.zeros((n,))
u = stats.uniform(0, 1).rvs(size=n) # random uniform
for t in range(n): # and so on
然后在循环内索引u
(当然必须删除循环中的行u = stats.uniform(0,1).rvs() # random uniform
):
if u[t] <= alpha:
x_t = x_prime # accept
posterior[t] = x_t
elif u[t] > alpha:
x_t = x_t # reject
较小的更改还可以通过省略elif
语句来简化if条件,或者如果出于其他目的需要将其替换为else
则可以简化if条件。但这实际上只是一个微小的改进:
if u[t] <= alpha:
x_t = x_prime # accept
posterior[t] = x_t
基于jwalton的答案的另一项改进:
def new_get_samples(n):
"""
Generate and return a randomly sampled posterior.
For simplicity, Prior is fixed as Beta(a=2,b=5), Likelihood is fixed as Normal(0,2)
:type n: int
:param n: number of iterations
:rtype: numpy.ndarray
"""
x_cur = np.random.uniform()
innov = norm.rvs(size=n)
x_prop = x_cur + innov
u = np.random.uniform(size=n)
post_cur = beta.pdf(x_cur, a=2,b=5) * norm.pdf(x_cur, loc=0,scale=2)
post_prop = beta.pdf(x_prop, a=2,b=5) * norm.pdf(x_prop, loc=0,scale=2)
posterior = np.zeros((n,))
for t in range(n):
alpha = post_prop[t] / post_cur
if u[t] <= alpha:
x_cur = x_prop[t]
post_cur = post_prop[t]
posterior[t] = x_cur
return posterior
在以下方面改进了时间:
%timeit my_get_samples(1000)
# 187 ms ± 13 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit my_get_samples2(1000)
# 1.55 ms ± 57.3 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
这比jwalton的答案提高了121倍。这是通过外包post_prop
计算来完成的。
检查直方图,这似乎没问题。但是最好向jwalton询问是否真的可以,他似乎对该主题有更多的了解。 :)