【问题标题】:How to conveniently use operations on numpy fortran contiguos arrays?如何方便地在 numpy fortran contiguos 数组上使用操作?
【发布时间】:2020-06-03 01:19:06
【问题描述】:

一些像np.matmul(a, b)这样的numpy函数对矩阵堆栈有方便的行为。

手册指出:

如果任一参数为 N-D,N > 2,则将其视为驻留在最后两个索引中的矩阵堆栈并相应地广播。

因此,对于a.shape = (10 , 2, 4)b.shape(10, 4, 2),语句a @ b 是有意义的并且将具有(10, 2, 2) 的形状

但是,我来自线性代数世界,在那里我习惯了 Fortran 连续数组布局。

表示为 Fortran 连续数组的相同 a 将具有形状 (4, 2, 10) 和类似的 b.shape = (2, 4, 10)

要像以前一样执行a @ b,我必须调用 (a.T @ b.T).T.

更糟糕的是,假设您天真地创建了相同的 Fortran 连续数组 a,并考虑了 matmul 的行为,因此它的形状为 (10, 4, 2)。 然后a.strides = (8, 80, 320) 在“堆栈”索引中具有最小的步幅,实际上应该具有最大的步幅。

这真的是要走的路还是我错过了什么?

【问题讨论】:

  • matmul 的第一个维度(共 3 个)是“批量”维度(我认为 order 不会改变这一点)。所以如果你想玩转置,请使用.transpose(0,2,1)。做一些小数组,看看哪种排列最有意义。
  • 二维“C”阶数组的转置是相同数据的“F”阶view。它只是改变了shapestrides
  • 问题是,为什么这是默认值?为什么要一堆矩阵,其中与“批量维度”对应的步幅最小?
  • 当然,我可以使用类似您的.transpose(0,2,1) 解决方案,但这看起来很容易出错,不是吗?
  • matmul 的 Python 规范根据维度对其进行了定义。 orderstrides 没有被提及。 python.org/dev/peps/pep-0465

标签: numpy numpy-ndarray


【解决方案1】:

numpy 的矩阵乘法与数组的内部布局无关。例如,这里有两个 C 有序数组:

>>> import numpy as np
>>> a = np.random.rand(10, 2, 4)
>>> b = np.random.rand(10, 4, 2)
>>> print('a', a.shape, a.strides)
>>> print('b', b.shape, b.strides)
a (10, 2, 4) (64, 32, 8)
b (10, 4, 2) (64, 16, 8)

以下是 Fortran 顺序的等效数组:

>>> af = np.asfortranarray(a)
>>> bf = np.asfortranarray(b)
>>> print('af', af.shape, af.strides)
>>> print('bf', bf.shape, bf.strides)
af (10, 2, 4) (8, 80, 160)
bf (10, 4, 2) (8, 80, 320)

Numpy 将等价的数组视为等价的,不管它们的内部布局如何:

>>> np.allclose(a, af) and np.allclose(b, bf)
True

矩阵乘法的结果不依赖于内部布局:

>>> np.allclose(a @ b, af @ bf)
True

如果你愿意,你甚至可以混合布局:

>>> np.allclose(a @ bf, af @ b)
True

简而言之,在 numpy 中使用 Fortran 有序数组最方便的方法是不用担心内部数组布局:形状才是最重要的。

如果您的数组形状与numpy matmul API 所期望的不同,您最好的办法是重塑数组,例如使用a.transpose(2, 0, 1) @ b.transpose(2, 0, 1) 或类似的,具体取决于您的用例,但不要'不用担心:对于 C 或 Fortran 连续数组,此操作仅调整数组视图周围的元数据,不会导致底层数据缓冲区被复制或重新排序。

【讨论】:

  • 也许我应该在我的问题中说得更清楚,但我真的很担心速度。虽然 C 布局在矩阵乘法速度方面直观地为您提供了良好的布局,但 F 布局却没有。
【解决方案2】:

虽然numpy 可以处理各种布局,但许多细节的设计都考虑到了“C”布局。很好的例子是嵌套列表如何转换为数组,以及 numpy 操作在 matmul 案例中批量处理多余维度的方式。

根据经验,numpy 的结果不依赖于数组布局(FORTRAN,C,非连续)是正确的;然而,速度确实如此,而且非常重要:

rng = np.random.default_rng()
a = rng.random((100,111,200))
b = rng.random((111,77,200))
af = np.array(a,order="F")
bf = np.array(b,order="F")

np.allclose((b.T@a.T).T,(bf.T@af.T).T)
# True
timeit(lambda:(b.T@a.T).T,number=10)
# 5.972857117187232
timeit(lambda:(bf.T@af.T).T,number=10)
# 0.1994628761895001

事实上,有时候非懒惰的转置是完全值得的,即将你的数据复制到最好的布局中:

timeit(lambda:(np.array(b.T,order="C")@np.array(a.T,order="C")).T,number=10)
# 0.3931349152699113

我的建议:如果您想要速度和方便,最好使用“C”布局,它不会花费很长时间来适应并且可以为您省去很多潜在的麻烦。

【讨论】:

  • 这是在什么地方讨论过的吗?偏爱结果的“一致性”而不是速度?对我来说,这似乎是一个非常可疑的决定。特别是,因为我在 F 连续数组的解释中没有看到关于此类事情的警告。
  • @dba 我找不到保证一致性的错误。但我同意你的观点,偶尔的性能警告会很好。另外,请注意堆叠的 matmul 是一个特别不利的例子。我认为但不确定只要您要相乘的矩阵每个至少有一个连续的轴,事情就由 blas 处理,速度差异很小。当矩阵完全跨步时(例如,因为连续轴用于堆叠),事情真的变慢了,这让我怀疑没有 blas 例程。
  • 我想说的是,如果 matmul 运算符依赖于顺序,那么它也可能具有“一致”的行为。现在,我想我将只使用 FORTRAN 数组并使用 np.einsum 进行此类操作。
猜你喜欢
  • 2012-09-17
  • 1970-01-01
  • 2014-04-18
  • 2022-01-26
  • 2021-01-20
  • 2019-03-31
  • 1970-01-01
  • 2018-09-06
  • 1970-01-01
相关资源
最近更新 更多