【问题标题】:Speed up Metropolis--Hastings in Python加速 Metropolis——Python 中的 Hastings
【发布时间】:2019-02-19 10:12:30
【问题描述】:

我有一些使用 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循环中加速各种分布(beta、gamma、poisson)随机抽样的方法。

【问题讨论】:

标签: python numpy random numba mcmc


【解决方案1】:

在您开始考虑 numba et 之前,您可以对这段代码进行 很多 优化。人。 (仅通过对算法的实现进行聪明的处理,我才设法将这段代码的速度提高了 25 倍)

首先,您的 Metropolis--Hastings 算法的实现存在错误。无论您的链是否移动,您都需要保持方案的每次迭代。也就是说,您需要从代码中删除posterior = posterior[np.where(posterior &gt; 0)],并在每个循环结束时添加posterior[t] = x_t

其次,这个例子看起来很奇怪。通常,对于这些类型的推理问题,我们希望根据一些观察来推断分布的参数。但是,在这里,分布的参数是已知的,而不是您在抽样观察?无论如何,无论如何,不​​管怎样,我很乐意用你的例子向你展示如何加快速度。

加速

首先,从for 主循环中删除不依赖于t 值的任何内容。首先从 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]

当然也可以将随机生成的u从for循环中移出:

    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 起源于:

随机游走Metropolis的弱收敛和最优缩放 算法(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 算法。

【讨论】:

  • 如果您将np.random.seed() 设置为特定状态,结果应该相同,但事实并非如此。因此,您的函数不会产生与问题中的函数相同的结果。
  • 当然返回的样本不完全相同。随机数的生成顺序完全不同。
  • 不客气。在你研究 numba 之前,你首先应该调整你的方案的随机游走。我现在正在为我的答案添加更多信息。
  • @jwalton3141,并不是说它会提高性能,但在我们返回 posterior 之前包含多行代码可能会有所帮助。我正在考虑稀释样本以减少自相关,并且只从 1000 开始(老化期)抽取样本以删除初始随机样本:posterior = posterior[1000::4] # use 1000 onwards (burn in) and take every 4th sample (thinning)
  • @Euler_Salter posterior[burn_in::M] 会做你想做的事。就个人而言,除非内存是一个问题,否则我会从 get_samples 返回 posterior,用轨迹图、自相关图等检查输出。然后才决定要进行多少老化和细化添加。看看这与 PyRsquared 的从 get_samples 返回 posterior[burn_in::M] 的建议有何不同——在这种情况下,我们将决定一个老化期并在没有看到完整输出的情况下进行细化。
【解决方案2】:

不幸的是,我真的看不到任何加速随机分布的可能性,除了自己用 numba 兼容的 python 代码重写它们。

但是加快代码瓶颈的一种简单方法是将对统计函数的两次调用替换为:

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 语句,或者如果出于其他目的需要将其替换为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 是否真的可以,他似乎对这个话题有更多的理解。 :)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-23
    • 1970-01-01
    相关资源
    最近更新 更多