极小值函数积分对数的数值稳定评估

6
如果我有一个随机数Z,它被定义为另外两个随机数XY总和,那么Z的概率分布是XY的概率分布的卷积。卷积基本上是分布函数乘积的积分。通常,卷积中的积分没有解析解,因此必须使用基本的积分算法进行计算。伪代码如下:
prob_z(z) = integrate(lambda t: prob_x(t) * prob_y(z-t), -inf, inf)

举个具体的例子,正态分布变量X和对数正态分布变量Y的总和Z可以使用以下Python/Scipy代码进行计算:

from scipy.integrate import quad
from scipy.stats import norm, lognorm
from scipy import log

prob_x = lambda x: norm.pdf(x, 0, 1)  # N(mu=0, sigma=1)
prob_y = lambda y: lognorm.pdf(y, 0.1, scale=10)  # LogN(mu=log(10), sigma=0.1)
def prob_z(z):
    return quad(lambda t: prob_x(t)*prob_y(z-t), -inf, inf)

现在我想计算对数概率。朴素的解决方案是简单地执行以下操作:
def log_prob_z(z):
    return log(prob_z(z))

然而,这是数值不稳定的。大约在39个标准偏差之后,概率分布数值变为0.0,因此即使对数概率有一些有限的值,也不能通过简单地取概率的对数来计算。比较norm.pdf(39, 1, 0) 的值为0.0和norm.logpdf(39, 1, 0) 的值约为-761。显然,Scipy没有将logpdf 计算为log(pdf) - 它找到了其他方法 - 否则它会返回-inf,这是一个劣质的响应。同样地,我想找到另一种方法来解决我的问题。
(你可能会想知道为什么我关心远离平均值的值的对数似然。答案是参数拟合。当对数似然是一些极为负数的数时,拟合算法可以更接近,但当它为-infnan时就无能为力了。)
问题是:有人知道我如何重新排列log(quad(...)),以便我不计算quad(...),从而避免在对数中创建0.0吗?

这可能更适合作为一般数学问题。 - Sebastian Mendez
1
如果我认为解析地解决这个积分是可行的,我会同意。但我真正关心的只是从数值角度得出的解决方案。根据我的经验,数值问题最好在这里提问。尽管我同意弄清楚这个问题需要一些数学知识。 - drhagen
正常分布永远不会为0.0-它延伸到无穷大并且始终具有有限值。在两侧,概率变得太小,以简单的方式在计算机上表示。即使您想出了一种计算这些值的方法,您仍然无法使用标准浮点数表示它们。我认为您要寻找的不是解决此特定数学问题的方法,而是一种表示浮点数并增加精度的方法。 - Paul Cornelius
我可能会看一下 SymPy,因为它支持任意精度。 - Sebastian Mendez
1
在概率太小无法用浮点数表示的区域,对数似然可以被准确地表示。我正在寻找一种计算对数似然的方法,而不是简单地天真地取不可表示的概率的对数。 - drhagen
显示剩余3条评论
1个回答

6
问题在于您正在积分的函数值太小,以至于双精度浮点数只能表示1e-308左右。当双精度不足以进行数字计算时,可以使用任意精度浮点运算库mpmath。它有自己的quad例程,但您需要实现pdf函数,使其在mpmath级别上工作(否则将没有任何东西可集成)。有许多内置函数,包括正态分布函数,因此我将用它进行说明。这里我使用SciPy将距离为70的两个正态分布函数卷积。
z = 70
p = quad(lambda t: norm.pdf(t, 0, 1)*norm.pdf(z-t, 0, 1), -np.inf, np.inf)[0]

遗憾的是,p 正好为 0.0。

在这里,我使用 mpmath 进行相同操作,之后进行 import mpmath as mp

z = 70
p = mp.quad(lambda t: mp.npdf(t, 0, 1)*mp.npdf(z-t, 0, 1), [-mp.inf, mp.inf])

现在,p是一个mpmath对象,打印出来是2.95304756048889e-543,远超过双精度范围。它的对数mp.log(p)为-1249.22086778731。

基于SciPy的替代方案:对数偏移

如果由于某种原因您不能使用mpmath,则可以尝试通过将函数的值移动到双精度范围内来“规范化”该函数。这里是一个例子:

z = 70
offset = 2*norm.logpdf(z/2, 0, 1)
logp = offset + np.log(quad(lambda t: np.exp(norm.logpdf(t, 0, 1) + norm.logpdf(z-t, 0, 1) - offset), -np.inf, np.inf)[0])

这里的logp打印出了-1264.66566393,比mpmath的结果差一些(因此我们损失了部分函数),但是还算合理。我的做法是:

  • 计算函数对数最大值的对数(即变量offset)
  • 从pdf的对数中减去该偏移量;这就是代码 norm.logpdf(t, 0, 1) + norm.logpdf(z-t, 0, 1) - offset 的含义
  • 取指数,因为我们不能直接将对数放在积分中。代数上,这等同于概率密度函数的乘积与exp(-offset)的乘积。但是在数值上,这个值不太可能溢出;实际上当t = z/2时,它的值为exp(0)=1。
  • 正常积分;取对数,加上偏移量。代数上,结果只是我们想要求的积分的对数。

mpmath版本易于实现,而且速度惊人地快。我选择了它。它可以在真正巨大的范围内工作。对于我的问题,在默认的mp精度下,它在大约1e20个标准偏差处崩溃,并依赖于Scipy进行对数正态分布的对数概率密度函数。 - drhagen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接