从 spmatrix 迁移到 sparray#
本文档提供了将 scipy.sparse
中代码从稀疏矩阵转换为稀疏数组的指南。
从稀疏矩阵到稀疏数组的转变类似于从 np.matrix
到 np.ndarray
的转换。本质上,我们必须从一个以矩阵乘法为中心的全二维 matrix
对象,转换为一个支持矩阵乘法运算符和逐元素计算的一维或二维“数组”对象。
符号约定:在本指南中,我们通常将稀疏数组类表示为 sparray
,将稀疏矩阵类表示为 spmatrix
。稠密 numpy 数组表示为 np.ndarray
,稠密矩阵类表示为 np.matrix
。支持的稀疏格式有 BSR、COO、CSC、CSR、DIA、DOK、LIL,所有格式都受 sparray 和 spmatrix 支持。sparse
一词指 sparray
或 spmatrix
,而 dense
指 np.ndarray
或 np.matrix
。
概述和宏观理解#
构造函数名称
*_matrix
,例如csr_matrix
,已更改为*_array
。spmatrix
M
总是二维的(行 x 列),即使是例如M.min(axis=0)
。sparrayA
可以是一维或二维的。对于完全(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
,只要indices
和indptr
中的值较小,即使输入数组的dtype
为int64
。相比之下,当输入数组为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。您选择哪种方法应取决于您的库、用户和您的偏好。
建议的迁移步骤#
第一遍(在代码中保留 spmatrix)
在您的 spmatrix 代码中,将用于矩阵乘法的
*
更改为@
。请注意,稀疏数组的标量乘法应使用*
。(参见下面的辅助代码 使用测试查找 * 和 ** 位置)矩阵幂运算,例如
M**3
,应转换为scipy.sparse.linalg.matrix_power(A, 3)
。为不再支持的函数/方法实现替代方案,例如
A.getnnz()
->A.nnz
(参见下面的 已删除的方法和属性)。根据需要更改与
issparse()
和isspmatrix()
相关的任何逻辑。通常,这意味着将isspmatrix
替换为issparse
,将isspmatrix_csr(G)
替换为issparse(G) and G.format == "csr"
。此外,isspmatrix_csr(G) or isspmatrix_csc(G)
变为issparse(G) and G.format in ['csr', 'csc']
。git 搜索习惯用法git grep 'isspm[a-z_]*('
可以帮助找到这些。将所有
spdiags
调用转换为dia_matrix
。参见spdiags
中的文档。在此处只需搜索spdiags
即可。对生成的代码运行所有测试。您仍在使用 spmatrix,而不是 sparray。但您的代码和测试已为更改做好准备,您应该能够将 sparray 作为输入传递给您的代码,并且它们大多会“正常工作”。
第二遍(切换到 sparray)
将
diags
和triu
等构造函数转换为数组版本(参见下面的 详细信息:构造函数)。将所有
*_matrix
构造函数调用重命名为*_array
。检查所有因迁移而导致返回一维值的函数/方法。这些主要是索引和规约函数(参见下面的 详细信息:形状变化和规约)。
检查所有迭代 spmatrix 的位置,并进行更改以适应 sparray 返回一维 sparray 而不是二维 spmatrix 的情况。
查找并更改代码中利用
np.matrix
特性的位置。将其转换为np.ndarray
特性。如果您的代码使用
mmread
、hb_read
或loadmat
从文件读取稀疏数据,请在这些函数中使用新的关键字参数spmatrix=False
以读取到 sparray。如果您使用的稀疏库仅接受
int32
索引数组进行稀疏表示,我们建议使用即时转换。在调用需要int32
的代码之前,将其转换为int32
。sparray
根据输入数组的 dtype 而非数组中的值选择索引 dtype。因此,如果您希望索引数组为int32
,则需要确保传递给csr_array
的每个索引数组(如indptr
)的 dtype 为int32
。使用spmatrix
时,很容易对numpy
数组使用默认的 int64 dtype,并依赖稀疏构造函数在值较小时进行向下转换。但这种向下转换在与其他矩阵、切片或算术表达式一起使用时会导致额外的重新转换。对于sparray
,您仍然可以依赖构造函数来选择 dtype。但您也可以通过传入索引数组的 dtype 而非其值来选择索引 dtype。因此,如果您想要int32
,请在创建索引数组时设置 dtype,例如indices = np.array([1,3,6], dtype=np.int32)
或indptr = np.arange(9, dtype=np.int32)
。有关更多信息,请参见下面的 索引数组 DTypes。在许多情况下,索引数组 dtype 并不关键,您可以直接让构造函数为 sparray 和 spmatrix 选择 dtype。测试您的代码。并**阅读**您的代码。您已迁移到 sparray。
详细信息:构造函数#
这四个函数是新增的,并且仅处理 sparray:block_array
、diags_array
、eye_array
和 random_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 的 rand
和 random
不同,后者默认使用全局 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:kron
、kronsum
、hstack
、vstack
、block_diag
、tril
和 triu
。它们的签名是
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.newaxis
或None
将其转换为二维。例如,A[3, None, :]
得到一个二维行。对二维 sparray 进行隐式(未给定)列索引会得到一维结果,例如A[3]
(注意:最好不要这样做——请改写为A[3, :]
)。如果您需要二维结果,请在索引中使用np.newaxis
或None
,或者将整数索引包装成列表,以便高级索引得到二维结果,例如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_shape
、getrow
、getcol
、asfptype
、getnnz
、getH
以及属性.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
/ravel
、np.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
除外,它返回一个稠密数组。