NumPy 矩阵类的弃用状态

Posted

技术标签:

【中文标题】NumPy 矩阵类的弃用状态【英文标题】:Deprecation status of the NumPy matrix class 【发布时间】:2019-04-14 17:39:32 【问题描述】:

matrix 类在 NumPy 中的状态如何?

我一直被告知我应该改用ndarray 类。在我编写的新代码中使用 matrix 类是否值得/安全?我不明白为什么我应该改用ndarrays。

【问题讨论】:

【参考方案1】:

tl;博士: 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)(并不是说这是任何实际重要性)。 索引:索引数组可以根据how you are indexing it 为您提供任意大小的数组。矩阵上的索引表达式总是会给你一个矩阵。这意味着二维数组的arr[:,0]arr[0,:] 都为您提供一维ndarray,而mat[:,0] 的形状为(N,1),而mat[0,:] 的形状为(1,M),以防matrix。 算术运算:过去使用矩阵的主要原因是对矩阵的算术运算(特别是乘法和幂)执行矩阵运算(矩阵乘法和矩阵幂)。数组同样会导致元素乘法和幂。因此,mat1 * mat2mat1.shape[1] == mat2.shape[0] 时有效,但arr1 * arr2arr1.shape == arr2.shape 时有效(当然结果意味着完全不同的东西)。此外,令人惊讶的是,mat1 / mat2 执行两个矩阵的 elementwise 除法。这种行为可能继承自ndarray,但对矩阵没有意义,尤其是考虑到* 的含义。 特殊属性:矩阵有a few handy attributes,除了数组有:mat.Amat.A1是数组视图,分别与np.array(mat)np.array(mat).ravel()具有相同的值。 mat.Tmat.H是矩阵的转置和共轭转置(伴随); arr.Tndarray 类中唯一存在的此类属性。最后,mat.Imat 的逆矩阵。

编写适用于 ndarray 或矩阵的代码非常容易。但是当这两个类有可能在代码中进行交互时,事情就开始变得困难了。特别是,很多代码可以自然地为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)。两者是broadcast一起塑造(3,3)

最后,当the @ matmul operator was introduced in python 3.5 首次实现in 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 运算符修复了与矩阵相比 ndarray 次优的最常见情况,但使用 ndarray 优雅地处理线性代数仍然存在一些缺点(尽管人们仍然倾向于相信总体而言,最好坚持后者)。一个这样的例子是矩阵幂:mat ** 3 是矩阵的正确三次矩阵幂(而它是 ndarray 的元素立方)。不幸的是,numpy.linalg.matrix_power 更加冗长。此外,就地矩阵乘法仅适用于矩阵类。相比之下,虽然 PEP 465 和 python grammar 都允许 @= 作为 matmul 的增强赋值,但从 numpy 1.15 开始,ndarrays 没有实现这一点。

弃用历史记录

考虑到上述matrix 类的复杂性,长期以来一直在反复讨论它可能被弃用。 @ 中缀运算符的引入是此过程 happened in September 2015 的巨大先决条件。不幸的是,早期矩阵类的优势意味着它的使用范围很广。有一些依赖于矩阵类的库(最重要的依赖之一是 scipy.sparse,它同时使用 numpy.matrix 语义,并且在致密化时经常返回矩阵),因此完全弃用它们一直是个问题。

已经在a numpy mailing list thread from 2009发现了诸如

之类的言论

numpy 是为通用计算需求而设计的,而不是任何一种 数学分支。 nd-arrays 对很多事情都非常有用。在 相比之下,例如,Matlab 最初被设计为一个简单的 前端到线性代数包。就个人而言,当我使用 Matlab 时,我 发现这很尴尬——我通常写 100 行代码 这与线性代数无关,因为每隔几行 实际上做了矩阵数学。所以我更喜欢 numpy 的方式——线性 代数代码行更长更尴尬,但其余的很多 更好。

Matrix 类是一个例外:它是为了提供一个 表达线性代数的自然方式。然而,事情变得有点棘手 当您混合矩阵和数组时,甚至在坚持使用矩阵时 存在混淆和限制——你如何表达一行与一 列向量?遍历矩阵时会得到什么?等等

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

这些反映了矩阵类带来的好处和困难。我能找到的最早的弃用建议是from 2008,尽管部分原因是自那以后发生了变化的不直观行为(特别是,对矩阵进行切片和迭代将导致最可能期望的(行)矩阵)。该建议表明,这是一个极具争议的主题,并且矩阵乘法的中缀运算符至关重要。

下一次提到我可以找到is from 2014,结果证明这是一个非常富有成果的线程。随后的讨论提出了一般处理 numpy 子类的问题,which general theme is still very much on the table。还有strong criticism:

引发这场讨论(在 Github 上)的原因是不可能 编写正确工作的鸭子类型代码:

ndarrays 矩阵 scipy.sparse 稀疏矩阵

三者的语义不同; scipy.sparse 在某处 在矩阵和 ndarrays 之间,有些东西随机工作,比如 矩阵和其他不是。

添加了一些夸张的词,从开发人员的角度可以这么说 看来, np.matrix 正在做并且已经做了坏事,只是存在, 通过弄乱 Python 中未说明的 ndarray 语义规则。

随后对矩阵可能的未来进行了大量有价值的讨论。即使当时没有@ 运算符,也有很多关于矩阵类的弃用以及它如何影响下游用户的想法。据我所知,这个讨论直接导致了 PEP 465 的开始,引入了 matmul。

In early 2015:

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

我认为将 np.matrix 修复为当前状态并不可行 一个 ndarray 子类,但即使是一个固定的矩阵类也不真正属于 numpy 本身,它的发布周期和兼容性太长 实验的保证——更不用说仅仅存在 numpy 中的矩阵类导致新用户误入歧途。

一旦@ 运算符可用了一段时间the discussion of deprecation surfaced again,reraising the topic 关于矩阵弃用和scipy.sparse 的关系。

最终,first action to deprecate numpy.matrix was taken in late November 2017。关于班级的家属:

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

他们很长一段时间都不会去任何地方(直到稀疏的 ndarray 至少实现)。因此 np.matrix 需要移动,而不是删除。

(source) 和

虽然我想摆脱 np.matrix 任何人,很快就这样做会真的破坏性的。

有大量的小脚本是由那些 不知道更好;我们确实希望他们学会不使用 np.matrix 但是 破坏他们所有的脚本是一种痛苦的方式

像 scikit-learn 这样的大型项目根本没有 替代使用 np.matrix,因为 scipy.sparse。

所以我认为前进的道路是这样的:

现在或每当有人聚在一起发布 PR:发布 np.matrix.__init__ 中的 PendingDeprecationWarning (除非它杀死 scikit-learn 和朋友的性能),并放一个大警告框 在文档的顶部。这里的想法是不要真正打破 任何人的代码,但开始发出我们肯定的信息 如果有其他选择,不要认为任何人都应该使用它。

在有了 scipy.sparse 的替代方案之后:增加警告, 可能一直到 FutureWarning,这样现有的脚本就不会 break 但他们确实会收到嘈杂的警告

最终,如果我们认为它会降低维护成本:拆分它 进入子包

(source).

现状

截至 2018 年 5 月(numpy 1.15,相关 pull request 和 commit)matrix class docstring 包含以下注释:

不再推荐使用这个类,即使是线性代数。而是使用常规数组。将来可能会删除该类。

同时PendingDeprecationWarning 已添加到matrix.__new__。不幸的是,deprecation warnings are (almost always) silenced by default,所以大多数 numpy 的最终用户不会看到这个强烈的提示。

最后,截至 2018 年 11 月,the numpy roadmap 提到多个相关主题作为“[numpy 社区] 将在其中投入资源的任务和功能”之一:

NumPy 内部的某些东西实际上与 NumPy 的 Scope 不匹配。

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

只要较大的库/许多用户(尤其是scipy.sparse)依赖于矩阵类,这种状态就可能一直存在。但是,有ongoing discussion 移动scipy.sparse 以依赖于其他东西,例如pydata/sparse。无论弃用过程的发展如何,用户都应在新代码中使用ndarray 类,如果可能,最好移植旧代码。最终,矩阵类可能会以一个单独的包结束,以消除其当前形式存在所带来的一些负担。

【讨论】:

我不认为 scipy.sparse 依赖于 np.matrix。是的,它的实现仅限于 2d,并且它对运算符的使用是 np 版本的模型。但是没有一个稀疏格式是np.matrix 的子类。而np.matrixsparse.todense的转换器实际上实现为np.asmatrix(M.toarray()) 最初sparse是为线性代数创建的,csrcsc是中心,其他格式作为创建工具。它以 MATLAB 代码为模型,据我所知,它仅限于 csc 格式。然而sparse 在机器学习和大数据使用中得到了更多的使用。 sklearn 有一组自己的稀疏实用程序。我不知道这些其他用途是否受益于 nd 稀疏数组。也许切线pandas 有自己的稀疏版本(系列和数据框)。 稀疏矩阵的行和列和确实返回密集矩阵。我必须检查实现,但我怀疑这是否是深度依赖。 我特别喜欢infinity = 32 @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) 的二维数组。使用矩阵而不是向量点可能会对性能造成一些影响,但应该按照您喜欢的方式保留语义。

以上是关于NumPy 矩阵类的弃用状态的主要内容,如果未能解决你的问题,请参考以下文章

sklearn 的弃用警告

Bootstrap SCSS 中的弃用警告

AppAuth 的弃用警告

如何解决 unarchiveObject(withFile:) 的弃用问题

连接到猫鼬时的弃用警告

我可以防止嵌套的弃用方法调用引发警告吗?