从numpy / scipy中的小对数概率向量中采样多项式

时间:2015-11-16 15:01:52

标签: python numpy scipy probability precision

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 个答案:

答案 0 :(得分:21)

首先,我相信您遇到的问题是因为您错误地规范了您的概率。这一行不正确:

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

您将概率除以对数概率,这没有任何意义。相反,你可能想要

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

如果你这样做,你会发现a = [1, 0],你的多项式采样器按预期工作,达到浮点精度的第二个概率。

小N:直方图

的解决方案

那就是说,如果你仍然需要更高的精度和性能并不是一个问题,你可以取得进步的一种方法是从头开始实现一个多项式采样器,然后修改它以更高的精度工作。

NumPy的多项式函数是implemented in 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 数组中的非常小的值,生成的多项式绘制也将保持精确度。 不幸的是,这些基于直方图的解决方案将比原生numpy.multinomial函数慢很多,所以如果性能是一个问题,您可能需要另一种方法。一种选择是使用上面链接的Cython代码在日志空间中工作,使用我在这里使用的类似数学技巧。

大N的解:泊松近似

上述解决方案的问题在于,随着 N 变大,它变得非常慢。 我正在考虑这个问题并意识到这是一个更有效的前进方法,尽管np.random.multinomial失败的概率小于1E-16左右。

这是一个失败的例子:在64位机器上,由于代码的实现方式,第一个条目总是为零,实际上它应该给出接近10的东西:< / p>

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值上窒息 - 这是因为算法computes the value (1 - p),因此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的{​​{1}},因为它会溢出长整数。 但我们可以确认我们的方法适用于使用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]) 的多项分布的非常稳健且非常快速的近似。