Ускорить Метрополис - Гастингс в Python - PullRequest
0 голосов
/ 19 февраля 2019

У меня есть код, который выбирает апостериорное распределение с использованием 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

Как правило, я стараюсь избегать использования явных циклов for в python - я бы попытался сгенерировать все, используя чистый numpy.Однако в случае этого алгоритма цикл for с операторами if неизбежен.Поэтому код довольно медленный.Когда я профилирую свой код, он проводит большую часть времени в цикле for (очевидно), а более конкретно, самые медленные части находятся в генерации случайных чисел;stats.beta().pdf() и stats.norm().pdf().

Иногда я использую numba , чтобы ускорить мой код для матричных операций.Хотя numba совместима с некоторыми операциями с числами, генерация случайных чисел не входит в их число.У Numba есть cuda rng , но это ограничено нормальным и равномерным распределением.

Мой вопрос, есть ли способ значительно ускорить приведенный выше код, используя какую-то случайную выборкуразличные дистрибутивы, совместимые с numba?

Нам не нужно ограничиваться только numba, но это единственный простой в использовании оптимизатор, который я знаю.В более общем смысле, я ищу способы ускорить случайную выборку для различных распределений (бета, гамма, пуассон) в цикле for в python.

Ответы [ 2 ]

0 голосов
/ 19 февраля 2019

Есть много оптимизаций, которые вы можете внести в этот код, прежде чем задуматься о numba et.и др.(Мне удалось ускорить этот код в 25 раз только благодаря умной реализации алгоритма)

Во-первых, в вашей реализации алгоритма Метрополис - Гастингс есть ошибка.Вам нужно сохранять каждую итерацию схемы, независимо от того, движется ваша цепочка или нет.То есть вам нужно удалить 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]

Конечно, также можно переместить случайное поколение 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)

Это увеличение скорости на 25x

ESS

Стоит отметить, что грубая скорость - это еще не все, когда речь идет об алгоритмах MCMC.На самом деле, вас интересует количество независимых ( ish ) розыгрышей, которые вы можете сделать сзади в секунду.Как правило, это оценивается с помощью ESS (эффективный размер выборки) .Вы можете повысить эффективность своего алгоритма (и, следовательно, увеличить количество эффективных выборок, извлекаемых в секунду), настроив случайное блуждание.

Для этого обычно выполняется первоначальный пробный запуск, то есть 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, WR Gilks ​​и GO 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, вероятно, превзойдет любой алгоритм Метрополис - Гастингс, который вы можете написать вручную.

0 голосов
/ 19 февраля 2019

К сожалению, я действительно не вижу никакой возможности ускорить случайные распределения, кроме как переписать их самостоятельно в коде Python, совместимом с Numba.

Но одна простая возможность ускорить узкое место вашего кода, этодля замены двух вызовов функций 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

Незначительные изменения также могут упростить условие if, пропуская elif заявление или, если требуется для других целей, заменив его на else.Но это действительно небольшое улучшение:

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)

Это ускорение в 121 раз по сравнению с ответом Джвальтона.Это достигается путем аутсорсинга post_prop расчета.

Проверка гистограммы, похоже, все в порядке.Но лучше спросите jwalton, если это действительно хорошо, он, кажется, гораздо лучше понимает эту тему.:)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...