我需要获取包含对数概率的两个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)))
使用
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'方法抹掉了我的!
答案 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)
您正在访问res
和b
的列,其中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倍。
在这里您可以知道我的答案在数值上是稳定的:
logsumexp
函数在数值上是稳定的。logmatmulexp
函数在数值上是稳定的。我的实现还有另一个不错的属性。如果不是使用numpy在pytorch中编写相同的代码,或者使用具有自动微分功能的另一个库,则会自动获得数值稳定的反向传递。我们可以通过以下方法知道反向传递在数值上是稳定的:
np.max
)以下是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.