NumPy矩阵类的弃用状态

65

matrix类在NumPy中的状态是什么?

我一直被告知应该使用ndarray类。在我编写新代码时,使用matrix类是否值得/安全?我不理解为什么应该使用ndarray而不是matrix

1个回答

92

简述: numpy.matrix 类将被弃用。一些重要的依赖于该类的库(最大的一个是 scipy.sparse)使得该类无法短期内被完全弃用,但用户强烈建议使用 ndarray 类(通常使用 numpy.array 方便函数创建)。随着矩阵乘法运算符 @ 的引入,矩阵的相对优势已经被消除了很多。

为什么(不)使用矩阵类?

numpy.matrixnumpy.ndarray 的子类。它最初是为方便进行涉及线性代数的计算而设计的,但与更通用的数组类实例相比,它们的行为存在限制和惊人的差异。以下是基本行为差异的示例:

  • 形状:数组可以有任意数量的维度,从0到无限大(或32)。矩阵始终是二维的。奇怪的是,虽然不能使用更多维度创建矩阵,但是可以将单例维度注入矩阵中,以实现技术上的多维矩阵:np.matrix(np.random.rand(2,3))[None,...,None].shape == (1,2,3,1)(这对于实际应用没有任何重要性)。
  • 索引:对数组进行索引可以获得任意大小的数组,具体取决于您如何对其进行索引。在矩阵上进行索引表达式将始终给出矩阵。这意味着对于2d数组,arr[:,0]arr[0,:]都会给出一个1d ndarray,而mat[:,0]的形状为(N,1)mat[0,:]的形状为(1,M),如果是matrix
  • 算术运算:早期使用矩阵的主要原因是矩阵上的算术运算(特别是乘法和幂)执行矩阵操作(矩阵乘法和矩阵幂)。对数组的操作将导致元素乘法和幂。因此,如果mat1.shape[1] == mat2.shape[0],则mat1 * mat2有效,但是如果arr1.shape == arr2.shape,则arr1 * arr2有效(当然结果完全不同)。另外,令人惊讶的是,mat1 / mat2执行两个矩阵的逐元素除法。这种行为可能是从ndarray继承而来的,但在矩阵中没有意义,特别是考虑到*的含义。
  • 特殊属性:矩阵除了具有数组的一些方便属性之外,还具有其他几个方便的属性:mat.Amat.A1是与np.array(mat)np.array(mat).ravel()具有相同值的数组视图,分别。 mat.Tmat.H是矩阵的转置和共轭转置(伴随);arr.Tndarray类唯一存在的这种属性。最后,mat.Imat的逆矩阵。
写出既适用于ndarrays也适用于矩阵的代码并不难。但是,当这两个类在代码中需要交互时,事情就开始变得困难起来。特别地,很多代码可以自然地适用于ndarray的子类,但是matrix是一个行为不良的子类,它很容易打破试图依赖鸭子类型的代码。考虑以下使用形状为(3,4)的数组和矩阵的示例:
import numpy as np

shape = (3, 4)
arr = np.arange(np.prod(shape)).reshape(shape) # ndarray
mat = np.matrix(arr) # same data in a matrix
print((arr + mat).shape)           # (3, 4), makes sense
print((arr[0,:] + mat[0,:]).shape) # (1, 4), makes sense
print((arr[:,0] + mat[:,0]).shape) # (3, 3), surprising

在沿着切片的维度上添加两个对象的结果会根据维度的不同而有天壤之别。当矩阵和数组的形状相同时,对它们进行加法运算会逐元素进行。以上的前两种情况很直观:我们将两个数组(矩阵)相加,然后从每个数组中添加两行。最后一种情况非常令人惊讶:我们可能本意是要添加两列,但实际上得到了一个矩阵。原因显然是arr[:,0]的形状为(3,),与(1,3)兼容,但mat[:.0]的形状为(3,1)。这两个对象被广播成了形状为(3,3)
最后,矩阵类的最大优势(即能够简洁地表示涉及大量矩阵乘积的复杂矩阵表达式)在Python 3.5中引入了@ matmul 运算符后被消除,该运算符首次实现于numpy 1.10。比较一下简单二次形式的计算:
v = np.random.rand(3); v_row = np.matrix(v)
arr = np.random.rand(3,3); mat = np.matrix(arr)

print(v.dot(arr.dot(v))) # pre-matmul style
# 0.713447037658556, yours will vary
print(v_row * mat * v_row.T) # pre-matmul matrix style
# [[0.71344704]]
print(v @ arr @ v) # matmul style
# 0.713447037658556

看上面的内容,很明显矩阵类在处理线性代数方面是首选:中缀运算符*使表达式更简洁易读。但是,在使用现代Python和NumPy时,我们可以使用@运算符实现相同的可读性。此外,需要注意的是,矩阵情况下得到的形状为(1,1)的矩阵在技术上应该是标量。这也意味着我们不能用这个“标量”乘以列向量:(v_row * mat * v_row.T) * v_row.T在上面的示例中会引发错误,因为形状为(1,1)(3,1)的矩阵不能按此顺序相乘。

为了完整起见,需要注意的是,虽然matmul运算符修复了ndarrays在与矩阵相比不够优秀的最常见情况,但在使用ndarrays处理线性代数时仍存在一些缺点(尽管人们仍倾向于认为总体上坚持使用后者更为可取)。一个例子是矩阵乘方:mat ** 3是矩阵的正确三次乘方(而它是ndarray的逐元素立方)。不幸的是,numpy.linalg.matrix_power要冗长得多。此外,原地矩阵乘法仅适用于矩阵类。相反,尽管PEP 465python grammar都允许将@=作为matmul的增强赋值,但在numpy 1.15中尚未实现这种方式来处理ndarrays。

弃用历史

考虑到与“matrix”类相关的上述复杂性,长期以来一直存在关于其可能废弃的讨论。引入“@”中缀运算符是这个过程的一个重要先决条件,它发生在2015年9月。不幸的是,在早期,“matrix”类的优点意味着它的使用范围广泛。有些库依赖于“matrix”类(其中最重要的之一是“scipy.sparse”,它同时使用了“numpy.matrix”语义,并且在密集化时经常返回矩阵),因此完全废弃它们始终是有问题的。
2009年的一个numpy邮件列表线程中,我已经发现了评论。
numpy旨在满足通用的计算需求,而不是数学的某一分支。nd-arrays在许多方面都非常有用。相比之下,Matlab最初是为线性代数包设计的易于操作的前端。个人而言,当我使用Matlab时,我发现这非常笨拙——对于每几行实际涉及矩阵运算的代码,我通常要写数百行与线性代数无关的代码。因此,我更喜欢numpy的方式——线性代数的代码行更长、更笨拙,但其他部分更好。

Matrix类是一个例外:它被编写为自然地表达线性代数的方法。然而,当你混合矩阵和数组时,事情会变得有点棘手,即使坚持使用矩阵,也存在困惑和限制——如何表示行向量和列向量?当你遍历一个矩阵时会得到什么?等等。

已经有很多关于这些问题的讨论,提出了很多好的想法,也有一些关于如何改进的共识,但没有人有足够的动力去做这件事。

这些反映了矩阵类带来的好处和困难。我能找到的最早的废弃建议是来自2008年,尽管部分动机是由于已经改变的不直观行为(特别是对矩阵进行切片和迭代将产生(行)矩阵,正如大多数人可能期望的那样)。该建议表明这是一个极具争议性的主题,并且中缀运算符用于矩阵乘法至关重要。

我找到的下一个提及是在2014年,这是一个非常富有成果的讨论。随后的讨论引出了一个问题,即如何处理numpy子类,这个问题仍然非常重要。同时也有强烈的批评

这场关于Github的讨论的起因是:无法编写适用于以下情况的鸭子类型代码:

  • ndarrays
  • matrices
  • scipy.sparse稀疏矩阵

所有三种的语义都不同;scipy.sparse介于矩阵和ndarrays之间,某些操作像矩阵一样随机工作,而其他操作则不行。

可以夸张地说,从开发者的角度来看,np.matrix的存在已经并将会继续通过破坏Python中ndarray语义的未声明规则来做恶。

接下来就是对矩阵可能的未来进行了很多有价值的讨论。即使当时还没有@运算符,也有很多思考花费在了废弃矩阵类以及它如何影响下游用户上。据我所知,这次讨论直接导致了PEP 465的产生,引入了matmul。

在2015年初:

我认为,"固定"版本的np.matrix应该(1)不是np.ndarray子类,(2)存在于第三方库中而不是numpy本身。

我认为在其当前状态下作为ndarray子类修复np.matrix并不可行,但即使修复后的矩阵类也不真正属于numpy本身,因为numpy具有太长的发布周期和兼容性保证,无法进行实验——更不用说numpy中矩阵类的存在会误导新用户。

一旦@运算符有了一段时间的使用,关于弃用矩阵的讨论再次浮出水面, 重新提出了矩阵弃用与scipy.sparse之间的关系。

最终,在2017年11月底首次采取了弃用numpy.matrix的行动。关于该类的依赖项:

社区将如何处理scipy.sparse矩阵子类?这些仍然广泛使用。

它们在相当长一段时间内都不会消失(至少直到稀疏ndarrays出现)。因此,需要移动np.matrix,而不是删除它。

(来源)

虽然我和任何人一样希望尽快摆脱np.matrix,但这样做会非常具有破坏性。有很多小脚本是由不懂得更好的人编写的;我们确实希望他们学会不使用np.matrix,但打破所有这些脚本是一种痛苦的方式。有一些主要项目(如scikit-learn)由于scipy.sparse的原因根本没有替代方案,只能使用np.matrix。因此,我认为前进的方式是这样的:现在或者在有人提出PR时,在np.matrix._init_中发布一个PendingDeprecationWarning(除非它影响scikit-learn等项目的性能),并在文档顶部放置一个大警告框。这里的想法是不真正打破任何人的代码,但开始传达我们绝对不认为任何人应该使用这个,如果他们有任何替代方案。在有了scipy.sparse的替代方案之后:加强警告,可能一直到FutureWarning,以便现有脚本不会崩溃,但会收到嘈杂的警告。最终,如果我们认为这将减少维护成本:将其拆分为子包。(source)
截至2018年5月(numpy 1.15,相关pull requestcommit),矩阵类文档字符串包含以下说明:

即使是用于线性代数,也不再建议使用此类。请改用常规数组。该类可能会在未来被删除。

标准数组子类的文档页面上说:

强烈建议不要使用矩阵子类。如下所述,它使编写能够一致处理矩阵和普通数组的函数非常困难。目前,它们主要用于与scipy.sparse交互。我们希望为此提供替代方案,并最终删除matrix子类。

同时,在matrix.__new__中添加了PendingDeprecationWarning。不幸的是,废弃警告(几乎总是)默认被静音处理, 因此大多数numpy的最终用户将看不到这个强烈提示。

最后,numpy路线图截至2018年11月提到了多个相关主题作为“[numpy社区]将投入资源的任务和特性”之一:

NumPy中的一些内容实际上与NumPy的范围不匹配。

  • 为numpy.fft编写后端系统(这样fft-mkl就不需要monkeypatch numpy)
  • 重写掩码数组以不是ndarray子类的方式 - 可能在单独的项目中?
  • MaskedArray作为鸭类型数组,和/或
  • 支持缺失值的数据类型
  • 编写有关如何处理linalg和fft的numpy和scipy之间重叠的策略(并实施它)。
  • 弃用np.matrix

由于更大的库/许多用户(特别是scipy.sparse)依赖于矩阵类,因此这种状态可能会保持不变。然而,正在进行讨论scipy.sparse移动到依赖于其他东西,例如pydata/sparse

SciPy 1.82022年2月发布)中引入了稀疏数组API进行早期测试和反馈,有可能最终移除np.matrix的遗留问题。这个API复制了SciPy稀疏容器,并使用与NumPy数组(而不是矩阵)相匹配的接口。下游库的维护者,如NetworkX和scikit-learn非常渴望尽快切换到新的API
无论废弃过程的发展如何,用户都应该在新代码中使用ndarray类,并尽可能移植旧代码。最终,矩阵类可能会以单独的包形式出现,以消除其目前形式存在的一些负担。

2
我认为scipy.sparse并不依赖于np.matrix。但事实上,它的实现受限于二维,并且操作符的使用方式是基于np版本的。但是,没有一个稀疏格式是np.matrix的子类。而将其转换为np.matrix的转换器sparse.todense实际上是采用np.asmatrix(M.toarray())的方式实现的。 - hpaulj
1
最初,“sparse”是为线性代数而创建的,其中“csr”和“csc”是核心,其他格式则用作创建工具。它是基于MATLAB代码建模的,据我所知,MATLAB代码仅限于“csc”格式。然而,“sparse”在机器学习和大数据应用中得到了更多的使用。“sklearn”有一组自己的稀疏实用程序。我不知道这些其他用途是否受益于nd稀疏数组。或许“pandas”有其自己版本的稀疏性(系列和数据帧)。 - hpaulj
1
作为一个更多地使用numpy应用程序的人 - 真是太好了。在解析代码和追踪基于混淆ndarraymatrix的错误之间,以及尝试使用语言进行更高维度的张量代数运算,这个分叉自从我开始使用numpy以来一直是一个巨大的头痛。非常感谢那些在后台进行困难编码的人,我知道他们正在努力完成这项工作。 - Daniel F
3
我很喜欢无穷等于32这个说法。 - pipe
2
@A.Donda 经过一番思考:您可以使用形状为 (1,n)(n,1) 的数组来限制操作,就像您想让 matrix 类工作的方式一样。考虑 vrow = np.random.rand(3)[None,:]; vcol = np.random.rand(3)[:,None]; M = np.random.rand(3,3)。生成的数组将仅遵守线性代数,而单例维度将被保留,因此 vrow @ vcol 是一个形状为 (1,1) 的二维数组,vcol @ vrow 是一个形状为 (3,3) 的二维数组。使用矩阵而不是向量点可能会有一些性能损失,但语义应该与您所期望的相同。 - Andras Deak -- Слава Україні
显示剩余9条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接