NumPy 矩阵类的弃用状态
Posted
技术标签:
【中文标题】NumPy 矩阵类的弃用状态【英文标题】:Deprecation status of the NumPy matrix class 【发布时间】:2019-04-14 17:39:32 【问题描述】:matrix
类在 NumPy 中的状态如何?
我一直被告知我应该改用ndarray
类。在我编写的新代码中使用 matrix
类是否值得/安全?我不明白为什么我应该改用ndarray
s。
【问题讨论】:
【参考方案1】:tl;博士: numpy.matrix
类正在被弃用。有一些高调的库依赖于类作为依赖项(最大的一个是scipy.sparse
),这阻碍了该类的适当短期弃用,但强烈建议用户使用ndarray
类(通常创建使用numpy.array
便利功能)代替。随着矩阵乘法 @
运算符的引入,矩阵的许多相对优势已被消除。
为什么(不是)矩阵类?
numpy.matrix
是numpy.ndarray
的子类。它最初是为了方便在涉及线性代数的计算中使用,但与更一般的数组类的实例相比,它们的行为方式存在局限性和令人惊讶的差异。行为根本差异的示例:
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 * mat2
在mat1.shape[1] == mat2.shape[0]
时有效,但arr1 * arr2
在arr1.shape == arr2.shape
时有效(当然结果意味着完全不同的东西)。此外,令人惊讶的是,mat1 / mat2
执行两个矩阵的 elementwise 除法。这种行为可能继承自ndarray
,但对矩阵没有意义,尤其是考虑到*
的含义。
特殊属性:矩阵有a few handy attributes,除了数组有:mat.A
和mat.A1
是数组视图,分别与np.array(mat)
和np.array(mat).ravel()
具有相同的值。 mat.T
和mat.H
是矩阵的转置和共轭转置(伴随); arr.T
是 ndarray
类中唯一存在的此类属性。最后,mat.I
是mat
的逆矩阵。
编写适用于 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.matrix
、sparse.todense
的转换器实际上实现为np.asmatrix(M.toarray())
。
最初sparse
是为线性代数创建的,csr
和csc
是中心,其他格式作为创建工具。它以 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 矩阵类的弃用状态的主要内容,如果未能解决你的问题,请参考以下文章