Выборка многочлена от малых логарифмических вероятностных векторов в numpy/scipy

Есть ли функция в numpy/scipy, которая позволяет вам выбирать многочлен из вектора малых вероятностей журнала, не теряя точности? Пример:

# sample element randomly from these log probabilities
l = [-900, -1680]

наивный метод терпит неудачу из-за недостаточного потока:

import scipy
import numpy as np
# this makes a all zeroes
a = np.exp(l) / scipy.misc.logsumexp(l)
r = np.random.multinomial(1, a)

это одна попытка:

def s(l):
    m = np.max(l)
    norm = m + np.log(np.sum(np.exp(l - m)))
    p = np.exp(l - norm)
    return np.where(np.random.multinomial(1, p) == 1)[0][0]

- это лучший/самый быстрый метод и можно избежать np.exp() на последнем шаге?

Ответы

Ответ 1

Прежде всего, я считаю, что проблема, с которой вы сталкиваетесь, связана с неправильной нормализацией ваших вероятностей. Эта строка неверна:

a = np.exp(l) / scipy.misc.logsumexp(l)

Вы делите вероятность на логарифмическую вероятность, которая не имеет смысла. Вместо этого вы, вероятно, захотите

a = np.exp(l - scipy.misc.logsumexp(l))

Если вы это сделаете, вы найдете a = [1, 0], и ваш многокомпонентный сэмплер работает как ожидалось до точности с плавающей запятой во второй вероятности.


Решение для малых N: гистограммы

Тем не менее, если вам все еще нужна более высокая точность и производительность, это не так важно, так как вы могли бы добиться прогресса, это реализовать мультиномиальный сэмплер с нуля, а затем модифицировать его для более высокой точности.

Многочленная функция NumPy реализована в Cython и по существу выполняет цикл над несколькими биномиальными образцами и объединяет их в многочленную выборку. и вы можете назвать это следующим образом:

np.random.multinomial(10, [0.1, 0.2, 0.7])
# [0, 1, 9]

(Обратите внимание, что точные выходные значения здесь и ниже являются случайными и будут меняться от вызова к вызову).

Другой способ, которым вы могли бы реализовать мультиномиальный сэмплер, - генерировать N равномерных случайных значений, а затем вычислить гистограмму с ячейками, определенными кумулятивными вероятностями:

def multinomial(N, p):
    rand = np.random.uniform(size=N)
    p_cuml = np.cumsum(np.hstack([[0], p]))
    p_cuml /= p_cuml[-1]
    return np.histogram(rand, bins=p_cuml)[0]

multinomial(10, [0.1, 0.2, 0.7])
# [1, 1, 8]

С учетом этого метода мы можем думать о том, чтобы делать вещи с большей точностью, сохраняя все в лог-пространстве. Основной трюк заключается в том, чтобы понять, что журнал равномерных случайных отклонений эквивалентен отрицательному экспоненциальному случайному отклонению, и поэтому вы можете сделать все выше, не покидая пространства журнала:

def multinomial_log(N, logp):
    log_rand = -np.random.exponential(size=N)
    logp_cuml = np.logaddexp.accumulate(np.hstack([[-np.inf], logp]))
    logp_cuml -= logp_cuml[-1]
    return np.histogram(log_rand, bins=logp_cuml)[0]

multinomial_log(10, np.log([0.1, 0.2, 0.7]))
# [1, 2, 7]

Полученные многолинейные ничьи будут поддерживать точность даже при очень малых значениях в массиве p. К сожалению, эти решения на основе гистограммы будут намного медленнее, чем функция native numpy.multinomial, поэтому, если производительность является проблемой, вам может понадобиться другой подход. Один из вариантов - адаптировать приведенный выше Cython-код для работы в лог-пространстве, используя аналогичные математические трюки, которые я использовал здесь.


Решение для большого N: приближение Пуассона

Проблема с приведенным выше решением состоит в том, что при увеличении N он становится очень медленным. Я подумал об этом и понял там более эффективный путь вперед, несмотря на то, что np.random.multinomial не удалось получить вероятности меньше 1E-16 или так.

Вот пример этого сбоя: на 64-битной машине это всегда будет давать ноль для первой записи из-за способа реализации кода, когда на самом деле он должен дать что-то около 10:

np.random.multinomial(1E18, [1E-17, 1])
# array([                  0, 1000000000000000000])

Если вы вникнете в источник, вы можете проследить эту проблему с помощью биномиальной функции, на которой построена многочлена. Внутренний код cython делает следующее:

def multinomial_basic(N, p, size=None):
    results = np.array([np.random.binomial(N, pi, size) for pi in p])
    results[-1] = int(N) - results[:-1].sum(0)
    return np.rollaxis(results, 0, results.ndim)

multinomial_basic(1E18, [1E-17, 1])
# array([                  0, 1000000000000000000])

Проблема заключается в том, что функция binomial дросселируется при очень малых значениях p - это потому, что алгоритм вычисляет значение (1 - p), поэтому значение p ограничено точностью с плавающей точкой.

Итак, что мы можем сделать? Ну, оказывается, что при малых значениях р распределение Пуассона является чрезвычайно хорошим приближением биномиального распределения, и реализация не имеет этих проблем. Таким образом, мы можем построить надежную многочленную функцию, основанную на надежном биномиальном сэмплере, который переключается на пробоотборник Пуассона при малых значениях p:

def binomial_robust(N, p, size=None):
    if p < 1E-7:
        return np.random.poisson(N * p, size)
    else:
        return np.random.binomial(N, p, size)

def multinomial_robust(N, p, size=None):
    results = np.array([binomial_robust(N, pi, size) for pi in p])
    results[-1] = int(N) - results[:-1].sum(0)
    return np.rollaxis(results, 0, results.ndim)

multinomial_robust(1E18, [1E-17, 1])
array([                 12, 999999999999999988])

Первая запись отлична от нуля и около 10, как и ожидалось! Обратите внимание: мы не можем использовать N больше, чем 1E18, потому что он переполнит длинное целое число. Но мы можем подтвердить, что наш подход работает для меньших вероятностей с использованием параметра size и усреднения по результатам:

p = [1E-23, 1E-22, 1E-21, 1E-20, 1]
size = int(1E6)
multinomial_robust(1E18, p, size).mean(0)
# array([  1.70000000e-05,   9.00000000e-05,   9.76000000e-04,
#          1.00620000e-02,   1.00000000e+18])

Мы видим, что даже при этих очень малых вероятностях многочленные значения появляются в правильной пропорции. В результате получается очень надежное и очень быстрое приближение к многочленному распределению при малых p.