scipy `SparseEfficiencyWarning` 在 csr_matrix 的行上除法时

scipy `SparseEfficiencyWarning` when division on rows of csr_matrix

假设我已经有一个 csr_matrix:

import numpy as np
from scipy.sparse import csr_matrix
indptr = np.array([0, 2, 3, 6])
indices = np.array([0, 2, 2, 0, 1, 2])
data = np.array([1., 2., 3., 4., 5., 6.])
mat = csr_matrix((data, indices, indptr), shape=(3, 3))
print(mat.A)

[[1. 0. 2.]
 [0. 0. 3.]
 [4. 5. 6.]]

如果我想把这个分成一行就很简单csr_matrix:

mat[0] /= 2
print(mat.A)

[[0.5 0.  1. ]
 [0.  0.  3. ]
 [4.  5.  6. ]]

但是,如果我想更改多行,它会抛出警告:

mat[np.array([0,1])]/=np.array([[1],[2]])
print(mat.A)

[[1.  0.  2. ]
 [0.  0.  1.5]
 [4.  5.  6. ]]

SparseEfficiencyWarning: Changing the sparsity structure of a csr_matrix is expensive. lil_matrix is more efficient.
  self._set_arrayXarray(i, j, x)

多行除法如何改变其稀疏性?它建议我更改为lil_matrix,但是当我检查def tolil()的代码时:

def tolil(self, copy=False):
    lil = self._lil_container(self.shape, dtype=self.dtype)

    self.sum_duplicates()
    ptr,ind,dat = self.indptr,self.indices,self.data
    rows, data = lil.rows, lil.data

    for n in range(self.shape[0]):
        start = ptr[n]
        end = ptr[n+1]
        rows[n] = ind[start:end].tolist()
        data[n] = dat[start:end].tolist()

    return lil

基本上循环所有行,我认为在我的情况下没有必要。如果我只想划分几行csr_matrix,正确的方法可能是什么?谢谢!

看来问题出在访问矩阵数据的方式上

mat[np.array([0,1])]/=np.array([[1],[2]])

直接访问行似乎是问题所在。我没有找到任何可靠的数据来说明为什么会这样,但我认为这会强制构建具有 CSR 格式通常会跳过的所有 0 值的行。

缓解该问题的一种方法是首先将数据 np.array([[1],[2]]) 写入 1/x 以获得 [[1],[0.5]] 然后从中创建对角线 csr_matrix 以便我们有

mat_x = csr_matrix(([1,0.5,1], [0,1,2], [0,1,2,3]), shape=(3, 3))

如果我们然后从 left 乘以这个矩阵来影响 rows,我们会得到想要的输出而没有任何警告

mat = mat_x * mat

你的矩阵:

In [208]: indptr = np.array([0, 2, 3, 6])
     ...: indices = np.array([0, 2, 2, 0, 1, 2])
     ...: data = np.array([1., 2., 3., 4., 5., 6.])
     ...: mat = sparse.csr_matrix((data, indices, indptr), shape=(3, 3))
In [209]: mat
Out[209]: 
<3x3 sparse matrix of type '<class 'numpy.float64'>'
    with 6 stored elements in Compressed Sparse Row format>
In [210]: mat.A
Out[210]: 
array([[1., 0., 2.],
       [0., 0., 3.],
       [4., 5., 6.]])

简单除法只是改变 mat.data 值,in-place:

In [211]: mat/= 3
In [212]: mat
Out[212]: 
<3x3 sparse matrix of type '<class 'numpy.float64'>'
    with 6 stored elements in Compressed Sparse Row format>
In [213]: mat *= 3
In [214]: mat.A
Out[214]: 
array([[1., 0., 2.],
       [0., 0., 3.],
       [4., 5., 6.]])

您案例的 RHS 生成一个 np.matrix 对象:

In [215]: mat[np.array([0,1])]/np.array([[1],[2]])
Out[215]: 
matrix([[1. , 0. , 2. ],
        [0. , 0. , 1.5]])

将其分配给 mat 子集会产生警告:

In [216]: mat[np.array([0,1])] = _
/usr/local/lib/python3.8/dist-packages/scipy/sparse/_index.py:146: SparseEfficiencyWarning: Changing the sparsity structure of a csr_matrix is expensive. lil_matrix is more efficient.
  self._set_arrayXarray(i, j, x)

你的警告和我的警告发生在设置步骤中:

self._set_arrayXarray(i, j, x)

如果我再次划分我不会收到警告:

In [217]: mat[np.array([0,1])]/=np.array([[1],[2]])
In [218]: mat
Out[218]: 
<3x3 sparse matrix of type '<class 'numpy.float64'>'
    with 9 stored elements in Compressed Sparse Row format>

为什么?因为在第一次赋值之后,mat 有 9 个 non-zero 项,而不是原来的六个。所以 [217] 不会改变稀疏性。

mat 转换回 6 个零,我们再次收到警告:

In [219]: mat.eliminate_zeros()
In [220]: mat
Out[220]: 
<3x3 sparse matrix of type '<class 'numpy.float64'>'
    with 6 stored elements in Compressed Sparse Row format>
In [221]: mat[np.array([0,1])]/=np.array([[1],[2]])
/usr/local/lib/python3.8/dist-packages/scipy/sparse/_index.py:146: SparseEfficiencyWarning: Changing the sparsity structure of a csr_matrix is expensive. lil_matrix is more efficient.
  self._set_arrayXarray(i, j, x)

以及稀疏度的变化:

In [222]: mat
Out[222]: 
<3x3 sparse matrix of type '<class 'numpy.float64'>'
    with 9 stored elements in Compressed Sparse Row format>

分配 [215] 的稀疏矩阵不会触发警告:

In [223]: mat.eliminate_zeros()
In [224]: m1=sparse.csr_matrix(mat[np.array([0,1])]/np.array([[1],[2]]))
In [225]: m1
Out[225]: 
<2x3 sparse matrix of type '<class 'numpy.float64'>'
    with 3 stored elements in Compressed Sparse Row format>
In [226]: mat[np.array([0,1])]=m1
In [227]: mat
Out[227]: 
<3x3 sparse matrix of type '<class 'numpy.float64'>'
    with 6 stored elements in Compressed Sparse Row format>

===

最好将 [215] 划分视为一个 ndarray 行动,而不是一个稀疏的行动:

In [232]: mat[np.array([0,1])]/np.array([[1],[2]])
Out[232]: 
matrix([[1.     , 0.     , 2.     ],
        [0.     , 0.     , 0.09375]])
In [233]: mat[np.array([0,1])].todense()/np.array([[1],[2]])
Out[233]: 
matrix([[1.     , 0.     , 2.     ],
        [0.     , 0.     , 0.09375]])

此除法的详细信息可在 sparse._base.pymat._divide 中找到,根据 other 是标量、密集数组还是稀疏矩阵,具有不同的操作。稀疏矩阵除法不实现broadcasting.

一般来说,矩阵乘法是最有效的稀疏计算。事实上,像行或列总和这样的操作是用它来实现的。某些形式的索引也是如此。如果可以将 Element-wise 计算应用于 M.data 数组而不考虑行或列索引(例如平方、幂、标量乘法),则计算是可以的。 M.multiply 是 element-wise,但没有密集数组的全部广播能力。稀疏划分就更有限了。

编辑

sklearn 有一些实用程序可以执行某些类型的 sparse 它需要的操作,例如缩放和规范化。

In [274]: from sklearn.utils import sparsefuncs

https://scikit-learn.org/stable/modules/classes.html#module-sklearn.utils

https://scikit-learn.org/stable/modules/generated/sklearn.utils.sparsefuncs.inplace_row_scale.html#sklearn.utils.sparsefuncs.inplace_row_scale

与示例mat:

In [275]: mat
Out[275]: 
<3x3 sparse matrix of type '<class 'numpy.float64'>'
    with 6 stored elements in Compressed Sparse Row format>
In [276]: mat.A
Out[276]: 
array([[1.    , 0.    , 2.    ],
       [0.    , 0.    , 0.1875],
       [4.    , 5.    , 6.    ]])

应用行缩放:

In [277]: sparsefuncs.inplace_row_scale(mat,np.array([10,20,1]))
In [278]: mat
Out[278]: 
<3x3 sparse matrix of type '<class 'numpy.float64'>'
    with 6 stored elements in Compressed Sparse Row format>
In [279]: mat.A
Out[279]: 
array([[10.  ,  0.  , 20.  ],
       [ 0.  ,  0.  ,  3.75],
       [ 4.  ,  5.  ,  6.  ]])

缩放数组的长度必须匹配。在您的情况下,您需要取 [[1],[2]] 的倒数,并用 1 填充它以作用于所有行。

查看源代码,我发现它使用 sparsefuncs.inplace_csr_row_scale。反过来:

X.data *= np.repeat(scale, np.diff(X.indptr))

此操作的详细信息是:

In [283]: mat.indptr
Out[283]: array([0, 2, 3, 6], dtype=int32)
In [284]: np.diff(mat.indptr)
Out[284]: array([2, 1, 3], dtype=int32)
In [285]: np.repeat(np.array([10,20,1]), _)
Out[285]: array([10, 10, 20,  1,  1,  1])
In [286]: mat.data
Out[286]: array([100., 200.,  75.,   4.,   5.,   6.])

所以它将 scale 数组转换为与 data 形状匹配的数组。那么就地 *= 数组乘法就很容易了。