问题 在numpy中,计算一个矩阵,其中每个单元格包含该行中所有其他条目的乘积


我有一个矩阵

A = np.array([[0.2, 0.4, 0.6],
              [0.5, 0.5, 0.5],
              [0.6, 0.4, 0.2]])

我想要一个新的矩阵,其中第i行和第j列中的条目的值是第i行的所有条目的乘积,除了第j列中该行的单元格。

array([[ 0.24,  0.12,  0.08],
       [ 0.25,  0.25,  0.25],
       [ 0.08,  0.12,  0.24]])

我第一次遇到的解决方案是

np.repeat(np.prod(A, 1, keepdims = True), 3, axis = 1) / A

但只有当条目的值为零时,这才有效。

有什么想法吗?谢谢!

编辑:我已经开发了

B = np.zeros((3, 3))
for i in range(3):
    for j in range(3):
        B[i, j] = np.prod(i, A[[x for x in range(3) if x != j]])

但肯定有更优雅的方法来实现这一点,这使得numpy的高效C后端而不是低效的python循环?


10986
2018-03-16 09:28


起源



答案:


如果你愿意忍受一个循环:

B = np.empty_like(A)
for col in range(A.shape[1]):
    B[:,col] = np.prod(np.delete(A, col, 1), 1)

这可以计算出你需要的东西,一次一列。它没有理论上那么有效,因为 np.delete() 创造一份副本;如果您非常关心内存分配,请使用掩码:

B = np.empty_like(A)
mask = np.ones(A.shape[1], dtype=bool)
for col in range(A.shape[1]):
    mask[col] = False
    B[:,col] = np.prod(A[:,mask], 1)
    mask[col] = True

4
2018-03-16 09:53



但是,记忆方面,两者是等价的。使用布尔掩码进行索引也会创建副本。第二个版本没有任何优势。 (布尔索引是一种“花式”索引形式,它总是复制。与普通切片不同,没有办法用偏移量和步幅来描述结果。) - Joe Kington
@JoeKington:你说得对,谢谢。您是否知道是否有更智能的方法按照我们需要的方式对列进行子集化?我们可能希望避免使用副本,但我们需要为任意列表达“除一个列之外的所有列”的想法。我不知道如何通过偏移和步幅实现这一点,但也许你知道其他可以节省内存的方法吗?或许今天在NumPy中没有办法。 - John Zwinck


答案:


如果你愿意忍受一个循环:

B = np.empty_like(A)
for col in range(A.shape[1]):
    B[:,col] = np.prod(np.delete(A, col, 1), 1)

这可以计算出你需要的东西,一次一列。它没有理论上那么有效,因为 np.delete() 创造一份副本;如果您非常关心内存分配,请使用掩码:

B = np.empty_like(A)
mask = np.ones(A.shape[1], dtype=bool)
for col in range(A.shape[1]):
    mask[col] = False
    B[:,col] = np.prod(A[:,mask], 1)
    mask[col] = True

4
2018-03-16 09:53



但是,记忆方面,两者是等价的。使用布尔掩码进行索引也会创建副本。第二个版本没有任何优势。 (布尔索引是一种“花式”索引形式,它总是复制。与普通切片不同,没有办法用偏移量和步幅来描述结果。) - Joe Kington
@JoeKington:你说得对,谢谢。您是否知道是否有更智能的方法按照我们需要的方式对列进行子集化?我们可能希望避免使用副本,但我们需要为任意列表达“除一个列之外的所有列”的想法。我不知道如何通过偏移和步幅实现这一点,但也许你知道其他可以节省内存的方法吗?或许今天在NumPy中没有办法。 - John Zwinck


使用的解决方案的变体 repeat,用途 [:,None]

np.prod(A,axis=1)[:,None]/A

我第一次处理 0s 是:

In [21]: B
array([[ 0.2,  0.4,  0.6],
       [ 0. ,  0.5,  0.5],
       [ 0.6,  0.4,  0.2]])

In [22]: np.prod(B,axis=1)[:,None]/(B+np.where(B==0,1,0))
array([[ 0.24,  0.12,  0.08],
       [ 0.  ,  0.  ,  0.  ],
       [ 0.08,  0.12,  0.24]])

但正如评论所指出的那样; [0,1]单元格应为0.25。

这可以解决这个问题,但是当连续存在多个0时,现在会出现问题。

In [30]: I=B==0
In [31]: B1=B+np.where(I,1,0)
In [32]: B2=np.prod(B1,axis=1)[:,None]/B1
In [33]: B3=np.prod(B,axis=1)[:,None]/B1
In [34]: np.where(I,B2,B3)
Out[34]: 
array([[ 0.24,  0.12,  0.08],
       [ 0.25,  0.  ,  0.  ],
       [ 0.08,  0.12,  0.24]])

In [55]: C
array([[ 0.2,  0.4,  0.6],
       [ 0. ,  0.5,  0. ],
       [ 0.6,  0.4,  0.2]])
In [64]: np.where(I,sum1[:,None],sum[:,None])/C1
array([[ 0.24,  0.12,  0.08],
       [ 0.5 ,  0.  ,  0.5 ],
       [ 0.08,  0.12,  0.24]])

Blaz Bratanic的 epsilon 方法是最好的非迭代解决方案(到目前为止):

In [74]: np.prod(C+eps,axis=1)[:,None]/(C+eps)

迭代列的不同解决方案:

def paulj(A):
    P = np.ones_like(A)
    for i in range(1,A.shape[1]):
        P *= np.roll(A, i, axis=1)
    return P

In [130]: paulj(A)
array([[ 0.24,  0.12,  0.08],
       [ 0.25,  0.25,  0.25],
       [ 0.08,  0.12,  0.24]])
In [131]: paulj(B)
array([[ 0.24,  0.12,  0.08],
       [ 0.25,  0.  ,  0.  ],
       [ 0.08,  0.12,  0.24]])
In [132]: paulj(C)
array([[ 0.24,  0.12,  0.08],
       [ 0.  ,  0.  ,  0.  ],
       [ 0.08,  0.12,  0.24]])

我在一个大矩阵上尝试了一些时间

In [13]: A=np.random.randint(0,100,(1000,1000))*0.01

In [14]: timeit paulj(A)
1 loops, best of 3: 23.2 s per loop

In [15]: timeit blaz(A)
10 loops, best of 3: 80.7 ms per loop

In [16]: timeit zwinck1(A)
1 loops, best of 3: 15.3 s per loop

In [17]: timeit zwinck2(A)
1 loops, best of 3: 65.3 s per loop

epsilon近似可能是我们可以预期的最佳速度,但有一些舍入问题。必须迭代许多列会损害速度。我不知道为什么 np.prod(A[:,mask], 1) 方法是最慢的。

eeclo https://stackoverflow.com/a/22441825/901925 建议使用 as_strided。这是我认为他的想法(改编自重叠的块问题, https://stackoverflow.com/a/8070716/901925

def strided(A):
    h,w = A.shape
    A2 = np.hstack([A,A])
    x,y = A2.strides
    strides = (y,x,y)
    shape = (w, h, w-1)
    blocks = np.lib.stride_tricks.as_strided(A2[:,1:], shape=shape, strides=strides)
    P = blocks.prod(2).T # faster to prod on last dim
    # alt: shape = (w-1, h, w), and P=blocks.prod(0)
    return P

(1000,1000)数组的时序相对于列迭代而言是一个相当大的改进,但仍然比它慢得多 epsilon 做法。

In [153]: timeit strided(A)
1 loops, best of 3: 2.51 s per loop

另一种索引方法虽然相对简单,但速度较慢,并且会更快地产生内存错误。

def foo(A):
    h,w = A.shape
    I = (np.arange(w)[:,None]+np.arange(1,w))
    I1 = np.array(I)%w
    P = A[:,I1].prod(2)
    return P

3
2018-03-16 19:34



我不这么认为 不 如果有零则工作。第一行的第0个元素应该是0.25,不是吗? - DSM
我已经纠正了这个问题,虽然解决方案比我想要的更麻烦。 - hpaulj
不幸的是,我认为这也无济于事。假设连续有两个零:那么每个输出行应该为零,因为每个产品都包含零。但是你的新方法会将零转换为非零元素的乘积(因为它们将采用零替换为1的情况。)如果使用4x4矩阵进行测试,则更容易看到行有两个非零元素和两个零。 - DSM
[通过“每个输出行”我当然意味着“输出行的每个元素”,而不是整个输出数组应该为零。] - DSM
hpaulj:没有注意到你已经包含了我想到的版本,但我也包括了一个编辑给我的帖子。你的版本不完全正确;但无论如何,大的速度差异让我感到惊讶。实际上prod是在一个小步幅轴上进行的,因此我们应该获得良好的缓存行为,因此唯一的额外成本是连接数组;然而,除了实际产品之外,epsilon方法还会对阵列进行三次传递......我不太明白。 - Eelco Hoogendoorn


我在奔跑,所以我没有时间研究这个解决方案;但是id做的是在最后一个轴上创建一个连续的圆形视图,通过沿着最后一个轴将数组连接到自身,然后使用np.lib.index_tricks.as_strided选择合适的元素来获取np.prod 。没有python循环,没有数值近似。

编辑:你走了:

import numpy as np

A = np.array([[0.2, 0.4, 0.6],
              [0.5, 0.5, 0.5],
              [0.5, 0.0, 0.5],
              [0.6, 0.4, 0.2]])

B = np.concatenate((A,A),axis=1)
C = np.lib.index_tricks.as_strided(
        B,
        A.shape  +A.shape[1:],
        B.strides+B.strides[1:])
D = np.prod(C[...,1:], axis=-1)

print D

注意:这种方法并不理想,因为它是O(n ^ 3)。看我的其他发布解决方案,即O(n ^ 2)


3
2018-03-16 10:03



这与我的步调版本相同。 - hpaulj
除了正确的输出;) - Eelco Hoogendoorn
但 np.allclose(strided(A),double_cumprod(A)) 返回True。 - hpaulj
确实如此,但是大型矩阵到处都是零。 ~0.5 ** 1000一直下浮你的花车。尝试将结果打印为小矩阵。 - Eelco Hoogendoorn


如果您愿意容忍小错误,可以使用您最初提出的解决方案。

A += 1e-10
np.around(np.repeat(np.prod(A, 1, keepdims = True), 3, axis = 1) / A, 9)

2
2018-03-17 09:42





这是一个没有python循环或数值近似的O(n ^ 2)方法:

def double_cumprod(A):
    B = np.empty((A.shape[0],A.shape[1]+1),A.dtype)
    B[:,0] = 1
    B[:,1:] = A
    L = np.cumprod(B, axis=1)
    B[:,1:] = A[:,::-1]
    R = np.cumprod(B, axis=1)[:,::-1]
    return L[:,:-1] * R[:,1:]

注意:它似乎是数值近似方法的两倍慢,这符合预期。


1



在我的大矩阵上,这比Blaz的近似测试快一点。 - hpaulj
我也对它进行了测试,但浮点数为1000x1000矩阵。在这种情况下,近似值在我的机器上拉动,但是对于它们,它们的速度大致相等。 - Eelco Hoogendoorn
当问题被陈述为时,这种cumprod方法变得更加明显 prod(A[j,:i-1])*prod(A[j,i+1:])。也就是说,2个序列的乘积。如最初所述,问题集中于消除 A[j,i] 从单一 prod()。 - hpaulj