从 SciPy 稀疏矩阵获取左、右、上、下非零邻居

Get Left, Right, Up, Down Nonzero Neighbor from SciPy Sparse Matrix

假设我有一个 2D SciPy 稀疏矩阵:

import numpy as np
from scipy.sparse import csc_matrix

arr = np.array([[0, 0, 1, 0, 1],
                [1, 0, 0, 1, 0],
                [0, 1, 1, 0, 0],
                [1, 0, 0, 1, 0],
                [0, 1, 0, 0, 0],
               ])

csc = csc_matrix(arr)

对于矩阵中的每个非零元素,我想创建四个新的稀疏矩阵,其中包含对应于下一个最近的左、右、上和下非零邻居的索引。末端的元素可以有环绕的邻居(想想水平和垂直方向上的循环双向链表或环形)。如果某个元素是其 row/column 中唯一的非零元素,则相应的索引将指向其自身。此外,由于索引可以具有零值(当引用第一行或第一列时)并且与自然零元素无法区分,我们将这些零索引设置为 -1 以消除真实索引与零元素的歧义。

对于上面的矩阵,密集的左矩阵和下矩阵看起来像:

left = np.array([[0, 0, 4,  0, 2],
                 [3, 0, 0, -1, 0],
                 [0, 2, 1,  0, 0],
                 [3, 0, 0, -1, 0],
                 [0, 1, 0,  0, 0],
                ])

down = np.array([[0, 0,  2, 0, -1],
                 [3, 0,  0, 3,  0],
                 [0, 4, -1, 0,  0],
                 [1, 0,  0, 1,  0],
                 [0, 2,  0, 0,  0],
                ])

请记住,索引值为 -1 的元素实际上是对索引零的引用。当然,我需要将这些矩阵设为稀疏矩阵形式,因为我的实际矩阵太大且稀疏,无法放入内存。

这里有一种可能的方法来做左邻居。 它不是特别有效,但如果整个矩阵中没有很多非零项,它可能工作正常。您可以通过获取每行的非零条目并仅计算一次 j[i==row] 来稍微优化它。

请注意,我只是将索引向上移动一位,而不是将 0 设置为 -1

i,j = csc.nonzero()
ind = sp.sparse.csc_matrix(csc.shape,dtype='int')
for row in range(csc.shape[0]):
    ind[row,j[i==row]] = np.roll(j[i==row]+1,1)

ind.A = array([[0, 0, 5, 0, 3],
   [4, 0, 0, 1, 0],
   [0, 3, 2, 0, 0],
   [4, 0, 0, 1, 0],
   [0, 2, 0, 0, 0]])

一个可能的答案(密集形式):

ix, iy = csc.nonzero()
w = np.where(np.insert(np.diff(ix), 0,1) != 0)[0]
iy2 = np.concatenate([np.roll(_, 1) for _ in np.split(iy,w)])
iy2[iy2==0] = -1

left = csc_matrix(arr.shape)
left[ix, iy] = iy2

ix, iy = csc.transpose().nonzero()
w = np.where(np.insert(np.diff(ix), 0,1) != 0)[0]
iy2 = np.concatenate([np.roll(_, 1) for _ in np.split(iy,w)])
iy2[iy2==0] = -1

down = csc_matrix(arr.T.shape)
down[ix, iy] = iy2
down = down.transpose()
print(left.todense(), '\n', down.todense())


 >> [[ 0  0  4  0  2]
 [ 3  0  0 -1  0]
 [ 0  2  1  0  0]
 [ 3  0  0 -1  0]
 [ 0  1  0  0  0]]

[[ 0  0  2  0 -1]
 [ 3  0  0  3  0]
 [ 0  4 -1  0  0]
 [ 1  0  0  1  0]
 [ 0  2  0  0  0]]
In [183]: arr = np.array([[0, 0, 1, 0, 1],
     ...:                 [1, 0, 0, 1, 0],
     ...:                 [0, 1, 1, 0, 0],
     ...:                 [1, 0, 0, 1, 0],
     ...:                 [0, 1, 0, 0, 0],
     ...:                ])
     ...:                
In [184]: from scipy import sparse
In [185]: M = sparse.lil_matrix(arr)
In [186]: M.rows
Out[186]: 
array([list([2, 4]), list([0, 3]), list([1, 2]), list([0, 3]), list([1])],
      dtype=object)

这与您从密集数组中获得的信息相同:

In [187]: [np.where(row)[0] for row in arr]
Out[187]: [array([2, 4]), array([0, 3]), array([1, 2]), array([0, 3]), array([1])]

我假设你已经想出了如何从密集数组生成所需的 left(或 right),所以我不会详细介绍这些细节(我太懒了与您的包装规格搏斗)。

对于列:

 In [189]: M.T.rows
 Out[189]: 
 array([list([1, 3]), list([2, 4]), list([0, 2]), list([1, 3]), list([0])],
  dtype=object)

您可以使用 csc 格式:

In [190]: Mc = sparse.csc_matrix(arr)
In [191]: Mc.indptr
Out[191]: array([0, 2, 4, 6, 8, 9], dtype=int32)
In [192]: Mc.indices
Out[192]: array([1, 3, 2, 4, 0, 2, 1, 3, 0], dtype=int32)
In [193]: for i in range(5):
     ...:     print(Mc.indices[Mc.indptr[i]:Mc.indptr[i+1]])
     ...:     
[1 3]
[2 4]
[0 2]
[1 3]
[0]

在此示例中,所有行或列都只有 1 或 2 个非零值。我想在更大更一般的情况下会有很多非零值。同样对于 csc(和 csr,每个 'row' 的索引可能未排序 - 有一种稀疏方法可以解决这个问题。

至于构建 return 稀疏矩阵,您可以修改副本的 data 属性(它将具有相同的稀疏性)。

In [194]: M.data
Out[194]: 
array([list([1, 1]), list([1, 1]), list([1, 1]), list([1, 1]), list([1])],
      dtype=object)
In [195]: Mc.data
Out[195]: array([1, 1, 1, 1, 1, 1, 1, 1, 1], dtype=int64)

或者从数组构造一个稀疏矩阵(对于 coo 样式输入来说是正常的)。


使用我的 lil 版本,tch's 解决方案稍微快一些:

ind = sparse.lil_matrix(M.shape,dtype='int')
for i,row in enumerate(M.rows):
    k = np.array(row)
    ind[i,k] = np.roll(k+1,1)

更好的是我的想法是替换 data:

ind = M.copy()
for row,dat in zip(ind.rows,ind.data):
    k = np.array(row)
    dat[:] = np.roll(k+1,1).tolist()

或者用Mr = Mc.tocsr()

ind = Mr.copy()
for i in range(Mr.shape[0]):
    slc = slice(Mr.indptr[i],Mr.indptr[i+1])
    k = Mr.indices[slc]
    ind.data[slc] = np.roll(k+1,1)

更向量化的方法:

csc = csc_matrix(arr)
inds = (csc.indices,csc.indptr)
irows = np.split(*inds)[1:-1]

down = csc_matrix((np.hstack([np.roll(row,-1) for row in irows]),*inds))
up = csc_matrix((np.hstack([np.roll(row,1) for row in irows]),*inds))

检查:

>>> down.A 
array([[0, 0, 2, 0, 0],
       [3, 0, 0, 3, 0],
       [0, 4, 0, 0, 0],
       [1, 0, 0, 1, 0],
       [0, 2, 0, 0, 0]], dtype=int32)

Left 和 Right 可以用 CSR 表示得到。

我不认为用 -1 编码 0 是个好主意,因为 if 会破坏所有稀疏计算改进。只有 csc.nonzeros() 设计的地方必须参观。