在numpy中乘以对数概率矩阵的数值稳定方法

时间:2014-05-13 11:40:22

标签: python numpy matrix matrix-multiplication logarithm

我需要获取包含对数概率的两个NumPy矩阵(或其他2d数组)的矩阵乘积。由于显而易见的原因,天真的方式np.log(np.dot(np.exp(a), np.exp(b)))不是首选。

使用

from scipy.misc import logsumexp
res = np.zeros((a.shape[0], b.shape[1]))
for n in range(b.shape[1]):
    # broadcast b[:,n] over rows of a, sum columns
    res[:, n] = logsumexp(a + b[:, n].T, axis=1) 

工作但运行速度比np.log(np.dot(np.exp(a), np.exp(b)))

慢约100倍

使用

logsumexp((tile(a, (b.shape[1],1)) + repeat(b.T, a.shape[0], axis=0)).reshape(b.shape[1],a.shape[0],a.shape[1]), 2).T

或tile和reshape的其他组合也可以工作,但运行速度比上面的循环慢,因为实际大小的输入矩阵需要大量的内存。

我目前正在考虑在C中编写一个NumPy扩展来计算它,但我当然要避免这样做。是否有既定的方法来执行此操作,或者是否有人知道执行此计算的内存密集程度较低的方法?

修改 感谢larsmans提供此解决方案(参见下面的推导):

def logdot(a, b):
    max_a, max_b = np.max(a), np.max(b)
    exp_a, exp_b = a - max_a, b - max_b
    np.exp(exp_a, out=exp_a)
    np.exp(exp_b, out=exp_b)
    c = np.dot(exp_a, exp_b)
    np.log(c, out=c)
    c += max_a + max_b
    return c

使用iPython的魔术logdot_old函数快速将此方法与上面发布的方法(%timeit)进行比较,得出以下结果:

In  [1] a = np.log(np.random.rand(1000,2000))

In  [2] b = np.log(np.random.rand(2000,1500))

In  [3] x = logdot(a, b)

In  [4] y = logdot_old(a, b) # this takes a while

In  [5] np.any(np.abs(x-y) > 1e-14)
Out [5] False

In  [6] %timeit logdot_old(a, b)
1 loops, best of 3: 1min 18s per loop

In  [6] %timeit logdot(a, b)
1 loops, best of 3: 264 ms per loop

显然是larsmans'方法抹掉了我的!

4 个答案:

答案 0 :(得分:21)

logsumexp通过评估等式的右边来工作

log(∑ exp[a]) = max(a) + log(∑ exp[a - max(a)])

即,它在开始求和之前拉出最大值,以防止在exp中溢出。在执行矢量点积之前可以应用相同的内容:

log(exp[a] ⋅ exp[b])
 = log(∑ exp[a] × exp[b])
 = log(∑ exp[a + b])
 = max(a + b) + log(∑ exp[a + b - max(a + b)])     { this is logsumexp(a + b) }

但是通过在推导中采取不同的转向,我们获得了

log(∑ exp[a] × exp[b])
 = max(a) + max(b) + log(∑ exp[a - max(a)] × exp[b - max(b)])
 = max(a) + max(b) + log(exp[a - max(a)] ⋅ exp[b - max(b)])

最终形式的内部有一个矢量点积。它也很容易扩展到矩阵乘法,所以我们得到算法

def logdotexp(A, B):
    max_A = np.max(A)
    max_B = np.max(B)
    C = np.dot(np.exp(A - max_A), np.exp(B - max_B))
    np.log(C, out=C)
    C += max_A + max_B
    return C

这会创建两个A大小的临时数和两个B大小的临时数,但其中一个可以通过

消除
exp_A = A - max_A
np.exp(exp_A, out=exp_A)

,同样适用于B。 (如果函数可以修改输入矩阵,则可以消除所有临时值。)

答案 1 :(得分:1)

您正在访问resb的列,其中locality of reference较差。要尝试的一件事是将它们存储在column-major order

答案 2 :(得分:0)

假设A.shape==(n,r)B.shape==(r,m)。在计算矩阵乘积C=A*B时,实际上有n*m个求和。为了在日志空间中工作时获得稳定的结果,在每个这些求和中都需要使用logsumexp技巧。幸运的是,使用numpy广播很容易分别控制A和B的行和列的稳定性。

代码如下:

def logdotexp(A, B):
    max_A = np.max(A,1,keepdims=True)
    max_B = np.max(B,0,keepdims=True)
    C = np.dot(np.exp(A - max_A), np.exp(B - max_B))
    np.log(C, out=C)
    C += max_A + max_B
    return C

注意:

其背后的原因与FredFoo的答案相似,但他为每个矩阵使用了一个最大值。由于他没有考虑每个n*m的总和,因此最终矩阵的某些元素可能仍然不稳定,如其中一条评论中所述。

使用@ identity-m计数器示例与当前接受的答案进行比较:

def logdotexp_less_stable(A, B):
    max_A = np.max(A)
    max_B = np.max(B)
    C = np.dot(np.exp(A - max_A), np.exp(B - max_B))
    np.log(C, out=C)
    C += max_A + max_B
    return C

print('old method:')
print(logdotexp_less_stable([[0,0],[0,0]], [[-1000,0], [-1000,0]]))
print('new method:')
print(logdotexp([[0,0],[0,0]], [[-1000,0], [-1000,0]]))

可打印

old method:
[[      -inf 0.69314718]
 [      -inf 0.69314718]]
new method:
[[-9.99306853e+02  6.93147181e-01]
 [-9.99306853e+02  6.93147181e-01]]

答案 3 :(得分:0)

Fred Foo当前接受的答案以及Hassan的答案在数值上都是不稳定的(Hassan的答案更好)。稍后将提供Hassan回答失败的输入示例。我的实现如下:

import numpy as np
from scipy.special import logsumexp

def logmatmulexp(log_A: np.ndarray, log_B: np.ndarray) -> np.ndarray:
    """Given matrix log_A of shape ϴ×R and matrix log_B of shape R×I, calculates                                                                                                                                                             
    (log_A.exp() @ log_B.exp()).log() in a numerically stable way.                                                                                                                                                                           
    Has O(ϴRI) time complexity and space complexity."""
    ϴ, R = log_A.shape
    I = log_B.shape[1]
    assert log_B.shape == (R, I)
    log_A_expanded = np.broadcast_to(np.expand_dims(log_A, 2), (ϴ, R, I))
    log_B_expanded = np.broadcast_to(np.expand_dims(log_B, 0), (ϴ, R, I))
    log_pairwise_products = log_A_expanded + log_B_expanded  # shape: (ϴ, R, I)                                                                                                                                                              
    return logsumexp(log_pairwise_products, axis=1)

就像哈桑的答案和弗雷德·福的答案一样,我的答案的时间复杂度为O(ϴRI)。他们的答案具有空间复杂度O(ϴR + RI)(我实际上不确定),而我的不幸的是具有空间复杂度O(ϴRI)-这是因为numpy可以将ϴ×R矩阵乘以R×I矩阵而无需分配大小为ϴ×R×I的附加数组。具有O(ϴRI)空间复杂度不是我的方法的内在属性-我认为如果使用循环将其写出,则可以避免这种空间复杂度,但是不幸的是,我不认为可以使用常规的numpy函数来做到这一点。 / p>

我检查了我的代码运行的实际时间,它比常规矩阵乘法慢20倍。

在这里您可以知道我的答案在数值上是稳定的:

  1. 很明显,除返回线外,所有其他线在数值上都是稳定的。
  2. 已知logsumexp函数在数值上是稳定的。
  3. 因此,我的logmatmulexp函数在数值上是稳定的。

我的实现还有另一个不错的属性。如果不是使用numpy在pytorch中编写相同的代码,或者使用具有自动微分功能的另一个库,则会自动获得数值稳定的反向传递。我们可以通过以下方法知道反向传递在数值上是稳定的:

  1. 我的代码中的所有函数到处都是可区分的(不同于np.max
  2. 很明显,除了返回线以外,所有线的向后传播都是数值稳定的,因为在那里绝对没有发生任何奇怪的事情。
  3. 通常pytorch的开发人员都知道他们在做什么。因此足以让他们相信,他们以数值稳定的方式实现了logumexp的反向传递。
  4. 实际上,logsumexp的梯度是softmax函数(供参考,谷歌“ softmax是logsumexp的梯度”或参见https://arxiv.org/abs/1704.00805命题1)。众所周知,可以通过数值稳定的方式来计算softmax。因此,pytorch开发人员可能只在此处使用softmax(我实际上没有检查过)。

以下是pytorch中的相同代码(如果需要反向传播)。由于pytorch反向传播的工作原理,在正向传递过程中,它将为后向传递保存log_pairwise_products张量。该张量很大,您可能不希望保存它-您可以在向后传递过程中再次重新计算它。在这种情况下,我建议您使用检查点-这真的很容易-请参见下面的第二个功能。

import torch
from torch.utils.checkpoint import checkpoint

def logmatmulexp(log_A: torch.Tensor, log_B: torch.Tensor) -> torch.Tensor:
    """Given matrix log_A of shape ϴ×R and matrix log_B of shape R×I, calculates                                                                                                                                                             
    (log_A.exp() @ log_B.exp()).log() and its backward in a numerically stable way."""
    ϴ, R = log_A.shape
    I = log_B.shape[1]
    assert log_B.shape == (R, I)
    log_A_expanded = log_A.unsqueeze(2).expand((ϴ, R, I))
    log_B_expanded = log_B.unsqueeze(0).expand((ϴ, R, I))
    log_pairwise_products = log_A_expanded + log_B_expanded  # shape: (ϴ, R, I)                                                                                                                                                              
    return torch.logsumexp(log_pairwise_products, dim=1)


def logmatmulexp_lowmem(log_A: torch.Tensor, log_B: torch.Tensor) -> torch.Tensor:
    """Same as logmatmulexp, but doesn't save a (ϴ, R, I)-shaped tensor for backward pass.                                                                                                                                                   

    Given matrix log_A of shape ϴ×R and matrix log_B of shape R×I, calculates                                                                                                                                                                
    (log_A.exp() @ log_B.exp()).log() and its backward in a numerically stable way."""
    return checkpoint(logmatmulexp, log_A, log_B)

这是哈桑的实现失败的输入,但是我的实现给出了正确的输出:

def logmatmulexp_hassan(A, B):
    max_A = np.max(A,1,keepdims=True)
    max_B = np.max(B,0,keepdims=True)
    C = np.dot(np.exp(A - max_A), np.exp(B - max_B))
    np.log(C, out=C)
    C += max_A + max_B
    return C

log_A = np.array([[-500., 900.]], dtype=np.float64)
log_B = np.array([[900.], [-500.]], dtype=np.float64)
print(logmatmulexp_hassan(log_A, log_B)) # prints -inf, while the correct answer is approximately 400.69.