从 spmatrix 迁移到 sparray#

本文档提供了将 scipy.sparse 中代码从稀疏矩阵转换为稀疏数组的指南。

从稀疏矩阵到稀疏数组的转变类似于从 np.matrixnp.ndarray 的转换。本质上,我们必须从一个以矩阵乘法为中心的全二维 matrix 对象,转换为一个支持矩阵乘法运算符和逐元素计算的一维或二维“数组”对象。

符号约定:在本指南中,我们通常将稀疏数组类表示为 sparray,将稀疏矩阵类表示为 spmatrix。稠密 numpy 数组表示为 np.ndarray,稠密矩阵类表示为 np.matrix。支持的稀疏格式有 BSR、COO、CSC、CSR、DIA、DOK、LIL,所有格式都受 sparray 和 spmatrix 支持。sparse 一词指 sparrayspmatrix,而 densenp.ndarraynp.matrix

概述和宏观理解#

  • 构造函数名称 *_matrix,例如 csr_matrix,已更改为 *_array

  • spmatrix M 总是二维的(行 x 列),即使是例如 M.min(axis=0)。sparray A 可以是一维或二维的。对于完全(0D)规约,即 M.min(),返回 NumPy 标量。

  • 迭代 sparray 会得到一维 sparray。迭代 spmatrix 会得到二维行 spmatrix。

  • 行为变化的运算符是:*, @, *=, @=, **

    • 标量乘法,例如 5 * A,使用 *,而 5 @ A 未实现。

    • sparray 使用 * 进行逐元素乘法,使用 @ 进行矩阵乘法,而 spmatrix 使用运算符 *@ 进行矩阵乘法。两者都可以使用 A.multiply(B) 进行逐元素乘法。

    • 标量指数运算,例如 A**2,sparray 使用逐元素幂运算,spmatrix 使用矩阵幂运算。sparray 的矩阵幂运算使用 scipy.sparse.linalg.matrix_power(A, n)

  • 当向构造函数提供索引数组时,spmatrix 根据输入数组的 dtype 和值选择 dtype,而 sparray 仅考虑输入数组的 dtype。例如,M=csr_matrix((data, indices, indptr)) 会导致 M.indices 的 dtype 为 int32,只要 indicesindptr 中的值较小,即使输入数组的 dtypeint64。相比之下,当输入数组为 int64 时,A=csr_array((data, indices, indptr)) 会导致 A.indices 的 dtype 为 int64。这使得 sparray 中的索引 dtype 更具可预测性,通常更大,并且减少了为匹配 dtype 而进行的类型转换。

  • 检查稀疏类型和格式

    • issparse(A) 对任何稀疏数组/矩阵返回 True

    • isspmatrix(M) 对任何稀疏矩阵返回 True

    • isspmatrix_csr(M) 检查特定格式的稀疏矩阵。它应该被替换为与数组兼容的版本,例如

    • issparse(A) and A.format == 'csr',它检查 CSR 稀疏数组/矩阵。

  • 使用稀疏输入/输出处理您的软件包 API

    • 输入数据很容易与 spmatrix 或 sparray 配合使用。只要您使用 A.multiply(B) 进行逐元素乘法,使用 A @ B 进行矩阵乘法,并且使用 sparse.linalg.matrix_power 进行矩阵幂运算,那么在完成下一节描述的迁移步骤的“第一遍”后,您应该会顺利。您的代码将能够互换地处理这两种类型的输入。

    • 从您的函数迁移稀疏输出需要多一点思考。列出所有返回 spmatrix 对象的公共函数。检查您是否可以接受返回 sparray。这取决于您的库及其用户。如果您希望这些函数继续返回 spmatrix 或 sparray 对象,您通常可以使用一个稀疏输入来做到这一点,该输入同时作为应返回何种类型输出的信号。将您的函数设计为返回输入数据的类型。这种方法可以扩展到稠密输入。如果输入是 np.matrix 或一个以 np.matrix 作为其 ._baseclass 属性的掩码数组,则返回 spmatrix。否则返回 sparray。如果没有这些输入,另外两种方法是创建关键字参数以指示返回哪种类型,或者创建一个新函数(例如我们已为 eye_array 所做的那样),该函数具有相同的基本语法,但返回 sparray。您选择哪种方法应取决于您的库、用户和您的偏好。

详细信息:构造函数#

这四个函数是新增的,并且仅处理 sparray:block_arraydiags_arrayeye_arrayrandom_array。它们的签名是

def block_array(blocks, format=None, dtype=None):
def diags_array(diagonals, /, *, offsets=0, shape=None, format=None, dtype=None):
def eye_array(m, n=None, *, k=0, dtype=float, format=None):
def random_array(shape, density=0.01, format='coo', dtype=None, rng=None, data_sampler=None):

random_array 函数使用 shape(2 元组)参数而非两个整数。并且 rng 参数默认为 NumPy 新的 default_rng()。这与 spmatrix 的 randrandom 不同,后者默认使用全局 RandomState 实例。如果您不太在意这些,保留默认值应该没问题。如果您关心随机数的种子,那么在切换函数时,您可能应该为此调用添加一个 rng=... 关键字参数。总而言之,要迁移到 random_array,请更改函数名,将 shape 参数切换为单个元组参数,保持其他参数不变,并考虑是否应使用何种 rng= 参数。

《em class="xref py py-obj"》diags_array《/em》函数对参数使用仅关键字规则。因此,您必须在偏移量参数前输入《em class="xref py py-obj"》offsets=《/em》。这在从使用《em class="xref py py-obj"》diags《/em》迁移时可能看起来很麻烦,但它有助于避免混淆并方便阅读。在此次迁移中,单个 shape 参数也替换了两个整数。

需要仔细迁移的现有函数#

这些函数根据接收的输入类型返回 sparray 或 spmatrix:kronkronsumhstackvstackblock_diagtriltriu。它们的签名是

def kron(A, B, format=None):
def kronsum(A, B, format=None):
def hstack(blocks, format=None, dtype=None):
def vstack(blocks, format=None, dtype=None):
def block_diag(mats, format=None, dtype=None):
def tril(A, k=0, format=None):
def triu(A, k=0, format=None):

应检查这些函数的使用,并调整输入以确保返回值是 sparray。反过来,输出也应被视为 sparray。要返回 sparray,至少一个输入必须是 sparray。如果您使用列表的列表或 numpy 数组作为输入,则应将其中之一转换为稀疏数组以获得稀疏数组输出。

迁移中更名的函数#

函数

新函数

备注

eye

eye_array

identity

eye_array

diags

diags_array

仅限关键字输入

spdiags

dia_array

形状为 2 元组

bmat

block

rand

random_array

形状为 2 元组和 default_rng

random

random_array

形状为 2 元组和 default_rng

详细信息:形状变化和规约#

  • 使用一维值列表构造

    • csr_array([1, 2, 3]).shape == (3,) 一维输入生成一维数组。

    • csr_matrix([1, 2, 3]).shape == (1, 3) 一维输入生成二维矩阵。

  • 索引和迭代

    • sparray 的索引允许一维对象,可以使用 np.newaxisNone 将其转换为二维。例如,A[3, None, :] 得到一个二维行。对二维 sparray 进行隐式(未给定)列索引会得到一维结果,例如 A[3](注意:最好不要这样做——请改写为 A[3, :])。如果您需要二维结果,请在索引中使用 np.newaxisNone,或者将整数索引包装成列表,以便高级索引得到二维结果,例如 A[[3], :]

    • 稀疏对象的迭代:next(M) 产生一个稀疏的二维行矩阵,next(A) 产生一个稀疏的一维数组。

  • 沿轴的规约操作会减小形状

    • M.min(axis=1) 返回沿轴 1 的最小值二维行矩阵。

    • A.min(axis=1) 返回沿轴 1 的最小值一维 coo_array。某些规约返回稠密数组/矩阵而不是稀疏数组/矩阵

      方法

      结果

      sum(axis)

      稠密

      mean(axis)

      稠密

      argmin(axis)

      稠密

      argmax(axis)

      稠密

      min(axis)

      稀疏

      max(axis)

      稀疏

      nanmin(axis)

      稀疏

      nanmax(axis)

      稀疏

    通常,二维 sparray 输入会产生一维结果。二维 spmatrix 输入会产生二维结果。

  • 某些规约返回标量。它们的行为应该与以前相同,在迁移过程中无需考虑。例如 A.min()

已删除的方法和属性#

  • 方法 get_shapegetrowgetcolasfptypegetnnzgetH 以及属性 .A.H 仅存在于 spmatrix 中,而不存在于 sparray 中。建议您在开始转向 sparray 之前,将其替换为替代方案。

    函数

    替代方案

    M.get_shape()

    A.shape

    M.getformat()

    A.format

    M.asfptype(…)

    A.astype(…)

    M.getmaxprint()

    A.maxprint

    M.getnnz()

    A.nnz

    M.getnnz(axis)

    A.count_nonzero(axis)

    M.getH()

    A.conj().T

    M.getrow(i)

    A[i, :]

    M.getcol(j)

    A[:, j]

    M.A

    A.toarray()

    M.H

    A.conj().T

  • sparray 不允许形状赋值(M.shape = (2, 6))。您应该改用 A.reshape

  • M.getnnz() 返回存储值的数量——而不是非零值的数量。A.nnz 作用相同。要获取非零值的数量,请使用 A.count_nonzero()。这并非本次迁移新增,但可能令人困惑。

    要从 M.getnnz(axis=...)axis 参数进行迁移,您可以使用 A.count_nonzero(axis=...),但这并非完全替代,因为它计算非零值的数量而非存储值的数量。区别在于显式存储的零值的数量。如果您确实想要按轴存储值的数量,则需要使用一些 numpy 工具。

    numpy 工具方法适用于 COO、CSR、CSC 格式,因此请转换为其中之一。对于 CSR 和 CSC,主轴是压缩的,np.diff(A.indptr) 返回一个稠密的一维数组,其中包含每个主轴值(CSR 为行,CSC 为列)的存储值数量。次轴可以使用 np.bincount(A.indices, minlength=N) 计算,其中 N 是次轴的长度(例如 CSR 的 A.shape[1])。bincount 函数适用于 COO 格式的任何轴,只需将 A.coords[axis] 替换为 A.indices

使用测试查找 * 和 ** 位置#

  • 在迁移代码时,区分标量乘法 * 和矩阵乘法 * 可能很棘手。理论上,Python 通过引入矩阵乘法运算符 @ 解决了这个问题。* 用于标量乘法,而 @ 用于矩阵乘法。但是,转换同时使用 * 的表达式可能很棘手并导致视觉疲劳。幸运的是,如果您的代码有一个覆盖您需要转换的表达式的测试套件,您可以使用它来查找 * 用于涉及稀疏矩阵的矩阵乘法的位置。将这些更改为 @

    该方法通过猴子补丁(monkey-patch)spmatrix 类的 dunder 方法,使其在 * 用于矩阵乘法时(而非标量乘法时)引发异常。测试套件将在这些位置标记失败。而测试失败在此处是一种成功,因为它显示了需要更改的位置。将导致问题的 * 更改为 @,在附近查找其他类似更改,然后再次运行测试。同样,这种方法有助于找到 ** 用于矩阵幂运算的位置。当 @ 用于标量乘法时,SciPy 会引发异常,因此这将捕获您不应该更改的地方。因此,带有此猴子补丁的测试套件也会检查更正。

    将以下代码添加到您的 conftest.py 文件中。然后本地运行您的测试。如果存在许多矩阵表达式,您可能希望一次测试代码库的一个部分。快速阅读代码表明,每当 * 用于两个类矩阵对象(稀疏或稠密)之间时,以及每当 ** 用于矩阵幂运算时,它都会引发 ValueError。它还会在 sum/mean/min/max/argmin/argmax 与轴一起使用时产生警告,这样 spmatrix 的输出将是二维的,而 sparray 的输出将是一维的。这意味着您需要检查代码是否通过 flatten/ravelnp.atleast_2d 或索引来处理一维或二维输出。

    #================== Added to check spmatrix usage ========================
    import scipy
    from warnings import warn
    
    def flag_this_call(*args, **kwds):
        raise ValueError("Old spmatrix function names for rand/spdiags called")
    
    scipy.sparse._construct.rand = flag_this_call
    scipy.sparse._construct.spdiags = flag_this_call
    
    class _strict_mul_mixin:
        def __mul__(self, other):
            if not scipy.sparse._sputils.isscalarlike(other):
                raise ValueError('Operator * used here! Change to @?')
            return super().__mul__(other)
    
        def __rmul__(self, other):
            if not scipy.sparse._sputils.isscalarlike(other):
                raise ValueError('Operator * used here! Change to @?')
            return super().__rmul__(other)
    
        def __imul__(self, other):
            if not scipy.sparse._sputils.isscalarlike(other):
                raise ValueError('Operator * used here! Change to @?')
            return super().__imul__(other)
    
        def __pow__(self, *args, **kwargs):
            raise ValueError('spmatrix ** found! Use linalg.matrix_power?')
    
        @property
        def A(self):
            raise TypeError('spmatrix A property found! Use .toarray()')
    
        @property
        def H(self):
            raise TypeError('spmatrix H property found! Use .conjugate().T')
    
        def asfptype(self):
            raise TypeError('spmatrix asfptype found! rewrite needed')
    
        def get_shape(self):
            raise TypeError('spmatrix get_shape found! Use .shape')
    
        def getformat(self):
            raise TypeError('spmatrix getformat found! Use .format')
    
        def getmaxprint(self):
            raise TypeError('spmatrix getmaxprint found! Use .shape')
    
        def getnnz(self):
            raise TypeError('spmatrix getnnz found! Use .nnz')
    
        def getH(self):
            raise TypeError('spmatrix getH found! Use .conjugate().T')
    
        def getrow(self):
            raise TypeError('spmatrix getrow found! Use .row')
    
        def getcol(self):
            raise TypeError('spmatrix getcol found! Use .col')
    
        def sum(self, *args, **kwds):
            axis = args[0] if len(args)==1 else args if args else kwds.get("axis", None)
            if axis is not None:
                warn(f"\nMIGRATION WARNING: spmatrix sum found using axis={axis}. "
                     "\nsparray with a single axis will produce 1D output. "
                     "\nCheck nearby to ensure 1D output is handled OK in this spot.\n")
            print(f"{args=} {axis=} {kwds=}")
            return super().sum(*args, **kwds)
    
        def mean(self, *args, **kwds):
            axis = args[0] if len(args)==1 else args if args else kwds.get("axis", None)
            if axis is not None:
                warn(f"\nMIGRATION WARNING: spmatrix mean found using axis={axis}."
                     "\nsparray with a single axis will produce 1D output.\n"
                     "Check nearby to ensure 1D output is handled OK in this spot.\n")
            return super().mean(*args, **kwds)
    
        def min(self, *args, **kwds):
            axis = args[0] if len(args)==1 else args if args else kwds.get("axis", None)
            if axis is not None:
                warn(f"\nMIGRATION WARNING: spmatrix min found using axis={axis}."
                     "\nsparray with a single axis will produce 1D output. "
                     "Check nearby to ensure 1D output is handled OK in this spot.\n")
            return super().min(*args, **kwds)
    
        def max(self, *args, **kwds):
            axis = args[0] if len(args)==1 else args if args else kwds.get("axis", None)
            if axis is not None:
                warn(f"\nMIGRATION WARNING: spmatrix max found using axis={axis}."
                     "\nsparray with a single axis will produce 1D output. "
                     "Check nearby to ensure 1D output is handled OK in this spot.\n")
            return super().max(*args, **kwds)
    
        def argmin(self, *args, **kwds):
            axis = args[0] if len(args)==1 else args if args else kwds.get("axis", None)
            if axis is not None:
                warn(f"\nMIGRATION WARNING: spmatrix argmin found using axis={axis}."
                     "\nsparray with a single axis will produce 1D output. "
                     "Check nearby to ensure 1D output is handled OK in this spot.\n")
            return super().argmin(*args, **kwds)
    
        def argmax(self, *args, **kwds):
            axis = args[0] if len(args)==1 else args if args else kwds.get("axis", None)
            if axis is not None:
                warn(f"\nMIGRATION WARNING: spmatrix argmax found using axis={axis}."
                     "\nsparray with a single axis will produce 1D output. "
                     "Check nearby to ensure 1D output is handled OK in this spot.\n")
            return super().argmax(*args, **kwds)
    
    
    class coo_matrix_strict(_strict_mul_mixin, scipy.sparse.coo_matrix):
        pass
    
    class bsr_matrix_strict(_strict_mul_mixin, scipy.sparse.bsr_matrix):
        pass
    
    class csr_matrix_strict(_strict_mul_mixin, scipy.sparse.csr_matrix):
        pass
    
    class csc_matrix_strict(_strict_mul_mixin, scipy.sparse.csc_matrix):
        pass
    
    class dok_matrix_strict(_strict_mul_mixin, scipy.sparse.dok_matrix):
        pass
    
    class lil_matrix_strict(_strict_mul_mixin, scipy.sparse.lil_matrix):
        pass
    
    class dia_matrix_strict(_strict_mul_mixin, scipy.sparse.dia_matrix):
        pass
    
    scipy.sparse.coo_matrix = scipy.sparse._coo.coo_matrix = coo_matrix_strict
    scipy.sparse.bsr_matrix = scipy.sparse._bsr.bsr_matrix = bsr_matrix_strict
    scipy.sparse.csr_matrix = scipy.sparse._csr.csr_matrix = csr_matrix_strict
    scipy.sparse.csc_matrix = scipy.sparse._csc.csc_matrix = csc_matrix_strict
    scipy.sparse.dok_matrix = scipy.sparse._dok.dok_matrix = dok_matrix_strict
    scipy.sparse.lil_matrix = scipy.sparse._lil.lil_matrix = lil_matrix_strict
    scipy.sparse.dia_matrix = scipy.sparse._dia.dia_matrix = dia_matrix_strict
    
    scipy.sparse._compressed.csr_matrix = csr_matrix_strict
    
    scipy.sparse._construct.bsr_matrix = bsr_matrix_strict
    scipy.sparse._construct.coo_matrix = coo_matrix_strict
    scipy.sparse._construct.csc_matrix = csc_matrix_strict
    scipy.sparse._construct.csr_matrix = csr_matrix_strict
    scipy.sparse._construct.dia_matrix = dia_matrix_strict
    
    scipy.sparse._extract.coo_matrix = coo_matrix_strict
    
    scipy.sparse._matrix.bsr_matrix = bsr_matrix_strict
    scipy.sparse._matrix.coo_matrix = coo_matrix_strict
    scipy.sparse._matrix.csc_matrix = csc_matrix_strict
    scipy.sparse._matrix.csr_matrix = csr_matrix_strict
    scipy.sparse._matrix.dia_matrix = dia_matrix_strict
    scipy.sparse._matrix.dok_matrix = dok_matrix_strict
    scipy.sparse._matrix.lil_matrix = lil_matrix_strict
    
    del coo_matrix_strict
    del bsr_matrix_strict
    del csr_matrix_strict
    del csc_matrix_strict
    del dok_matrix_strict
    del lil_matrix_strict
    del dia_matrix_strict
    #==========================================
    

索引数组 DTypes#

如果您向构造函数提供压缩索引,例如 csr_array((data, indices, indptr)),稀疏数组仅通过检查索引数组的 dtype 来设置索引 dtype,而稀疏矩阵也会检查索引值,并可能向下转换为 int32(有关更多详细信息,请参见 gh-18509)。这意味着您可能会在原本获得 int32 索引的地方获得 int64 索引。您可以通过在实例化之前设置 dtype 或在构造之后重新转换来控制这一点。

两个稀疏实用函数可以帮助处理索引 dtype。在创建索引时使用 get_index_dtype(arrays, maxval, check_contents) 来为您的压缩索引找到合适的 dtype (int32 或 int64)。

在构造后使用 safely_cast_index_arrays(A, idx_dtype) 进行重新转换,同时确保在向下转换时不会产生溢出。此函数实际上不会更改输入数组。返回的是转换后的数组。并且仅在需要时才创建副本。因此您可以使用 if indices is not A.indices: 来检查是否进行了类型转换。

函数签名是

def get_index_dtype(arrays=(), maxval=None, check_contents=False):
def safely_cast_index_arrays(A, idx_dtype=np.int32, msg=""):

get_index_dtype 的示例用法包括以下内容

.. code-block:: python

    # select index dtype before construction based on shape
    shape = (3, 3)
    idx_dtype = scipy.sparse.get_index_dtype(maxval=max(shape))
    indices = np.array([0, 1, 0], dtype=idx_dtype)
    indptr = np.arange(3, dtype=idx_dtype)
    A = csr_array((data, indices, indptr), shape=shape)

safely_cast_index_arrays 的示例

.. code-block:: python

    # rescast after construction, raising exception if shape too big
    indices, indptr = scipy.sparse.safely_cast_index_arrays(B, np.int32)
    B.indices, B.indptr = indices, indptr

其他#

  • 二元运算符 +, -, *, /, @, !=, > 作用于稀疏和/或稠密操作数

    • 如果所有输入都是稀疏的,则输出通常也是稀疏的。例外是 /,它返回稠密数组(除以默认值 0 会得到 nan)。

    • 如果输入混合了稀疏和稠密,结果通常是稠密的(即 np.ndarray)。例外情况是 *,它返回稀疏结果;以及 /,它未针对 dense / sparse 实现,但对于 sparse / dense 返回稀疏结果。

  • 二元运算符 +, -, *, /, @, !=, > 作用于数组和/或矩阵操作数

    • 如果所有输入都是数组,则输出也是数组;矩阵也同理。

    • 当混合稀疏数组和稀疏矩阵时,主操作数决定了输出的类型,例如 sparray + spmatrix 得到一个稀疏数组,而颠倒顺序则得到一个稀疏矩阵。

    • 当混合稠密矩阵和稀疏数组时,结果通常是数组,但比较运算除外,例如 > 返回稠密矩阵。

    • 当混合稠密数组和稀疏矩阵时,结果通常是矩阵,但 array @ sparse matrix 除外,它返回一个稠密数组。