加快都市圈-使用Python进行停顿

时间:2019-02-19 10:12:30

标签: python numpy random numba mcmc

我有一些代码使用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循环内加快各种分布(β,γ,泊松)随机抽样的方法。

2 个答案:

答案 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倍

ESS

值得注意的是,对于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()

您可以立即看到调整对我们算法效率的改进。记住,两次运行都进行了相同数量的迭代。 enter image description here

最终想法

如果您的算法收敛时间很长,或者您的样本具有大量的自相关,那么我考虑考虑使用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询问是否真的可以,他似乎对该主题有更多的了解。 :)