广播机制#
另请参见
术语“广播”描述了 NumPy 在算术运算期间如何处理不同形状的数组。在满足某些约束条件的情况下,较小的数组会在较大的数组上“广播”,以便它们具有兼容的形状。广播提供了一种向量化数组运算的方法,以便在 C 而不是 Python 中进行循环。它无需进行不必要的的数据复制,通常可以实现高效的算法。但是,在某些情况下,广播是一个坏主意,因为它会导致内存使用效率低下,从而降低计算速度。
NumPy 运算通常基于逐元素的基础对成对的数组进行。在最简单的情况下,这两个数组必须具有完全相同的形状,如下例所示。
>>> import numpy as np
>>> a = np.array([1.0, 2.0, 3.0])
>>> b = np.array([2.0, 2.0, 2.0])
>>> a * b
array([2., 4., 6.])
当数组的形状满足某些约束条件时,NumPy 的广播规则会放宽此约束。最简单的广播示例是当数组和标量值组合在一个运算中时。
>>> import numpy as np
>>> a = np.array([1.0, 2.0, 3.0])
>>> b = 2.0
>>> a * b
array([2., 4., 6.])
结果等效于之前的示例,其中b
是一个数组。我们可以认为标量b
在算术运算期间被拉伸成一个与a
形状相同的数组。b
中的新元素,如图 1所示,只是原始标量的副本。拉伸比喻只是概念上的。NumPy 足够聪明,可以使用原始标量值而无需实际创建副本,以便广播操作尽可能高效地利用内存和计算资源。
第二个示例中的代码比第一个示例中的代码更高效,因为广播在乘法过程中移动的内存更少(b
是标量而不是数组)。
通用广播规则#
对两个数组进行运算时,NumPy 会逐元素比较它们的形状。它从尾随(即最右)维度开始,向左工作。当两个维度满足以下条件时,它们是兼容的:
它们相等,或者
其中之一为 1。
如果这些条件不满足,则会引发ValueError: operands could not be broadcast together
异常,表明数组的形状不兼容。
输入数组不需要具有相同数量的维度。生成的数组将与具有最大维度数量的输入数组具有相同数量的维度,其中每个维度的大小是输入数组中相应维度中的最大大小。请注意,缺少的维度假定大小为 1。
例如,如果您有一个256x256x3
的 RGB 值数组,并且想要按不同的值缩放图像中的每种颜色,您可以将图像乘以一个具有 3 个值的 1 维数组。根据广播规则对齐这些数组的尾随轴的大小,表明它们是兼容的。
Image (3d array): 256 x 256 x 3
Scale (1d array): 3
Result (3d array): 256 x 256 x 3
当比较的任何一个维度为一时,则使用另一个维度。换句话说,大小为 1 的维度会被拉伸或“复制”以匹配另一个维度。
在下面的示例中,A
和B
数组都具有长度为一的轴,这些轴在广播操作期间会扩展到更大的大小。
A (4d array): 8 x 1 x 6 x 1
B (3d array): 7 x 1 x 5
Result (4d array): 8 x 7 x 6 x 5
可广播数组#
如果上述规则产生有效的结果,则一组数组称为“可广播”到相同的形状。
例如,如果a.shape
为 (5,1),b.shape
为 (1,6),c.shape
为 (6,),d.shape
为 (),因此 *d* 是标量,则 *a*、*b*、*c* 和 *d* 都可广播到维度 (5,6);并且
*a* 像一个 (5,6) 数组,其中
a[:,0]
广播到其他列,*b* 像一个 (5,6) 数组,其中
b[0,:]
广播到其他行,*c* 像一个 (1,6) 数组,因此像一个 (5,6) 数组,其中
c[:]
广播到每一行,最后,*d* 像一个 (5,6) 数组,其中单个值被重复。
以下是一些更多示例。
A (2d array): 5 x 4
B (1d array): 1
Result (2d array): 5 x 4
A (2d array): 5 x 4
B (1d array): 4
Result (2d array): 5 x 4
A (3d array): 15 x 3 x 5
B (3d array): 15 x 1 x 5
Result (3d array): 15 x 3 x 5
A (3d array): 15 x 3 x 5
B (2d array): 3 x 5
Result (3d array): 15 x 3 x 5
A (3d array): 15 x 3 x 5
B (2d array): 3 x 1
Result (3d array): 15 x 3 x 5
以下是不广播形状的示例。
A (1d array): 3
B (1d array): 4 # trailing dimensions do not match
A (2d array): 2 x 1
B (3d array): 8 x 4 x 3 # second from last dimensions mismatched
将一维数组添加到二维数组时的广播示例。
>>> import numpy as np
>>> a = np.array([[ 0.0, 0.0, 0.0],
... [10.0, 10.0, 10.0],
... [20.0, 20.0, 20.0],
... [30.0, 30.0, 30.0]])
>>> b = np.array([1.0, 2.0, 3.0])
>>> a + b
array([[ 1., 2., 3.],
[11., 12., 13.],
[21., 22., 23.],
[31., 32., 33.]])
>>> b = np.array([1.0, 2.0, 3.0, 4.0])
>>> a + b
Traceback (most recent call last):
ValueError: operands could not be broadcast together with shapes (4,3) (4,)
如图 2所示,b
被添加到a
的每一行。在图 3中,由于形状不兼容,会引发异常。
广播提供了一种方便的方法来获取两个数组的外积(或任何其他外部运算)。以下示例显示了两个一维数组的外部加法运算。
>>> import numpy as np
>>> a = np.array([0.0, 10.0, 20.0, 30.0])
>>> b = np.array([1.0, 2.0, 3.0])
>>> a[:, np.newaxis] + b
array([[ 1., 2., 3.],
[11., 12., 13.],
[21., 22., 23.],
[31., 32., 33.]])
这里,newaxis
索引运算符将一个新轴插入a
中,使其成为一个二维4x1
数组。将4x1
数组与形状为(3,)
的b
组合,将产生一个4x3
数组。
一个实际示例:矢量量化#
广播在实际问题中经常出现。一个典型的例子发生在信息论、分类和其他相关领域中使用的矢量量化 (VQ) 算法中。VQ 中的基本操作是在一组点(在 VQ 行话中称为codes
)中找到最接近给定点(称为observation
)的点。在下面所示的非常简单的二维情况下,observation
中的值描述了要分类的运动员的体重和身高。codes
代表不同类别的运动员。[1] 找到最接近的点需要计算观察值和每个代码之间的距离。最短距离提供最佳匹配。在此示例中,codes[0]
是最接近的类别,表明该运动员很可能是篮球运动员。
>>> from numpy import array, argmin, sqrt, sum
>>> observation = array([111.0, 188.0])
>>> codes = array([[102.0, 203.0],
... [132.0, 193.0],
... [45.0, 155.0],
... [57.0, 173.0]])
>>> diff = codes - observation # the broadcast happens here
>>> dist = sqrt(sum(diff**2,axis=-1))
>>> argmin(dist)
0
在此示例中,observation
数组被拉伸以匹配codes
数组的形状。
Observation (1d array): 2
Codes (2d array): 4 x 2
Diff (2d array): 4 x 2
通常情况下,大量的observations
(例如从数据库读取的数据)会与一组codes
进行比较。考虑以下场景:
Observation (2d array): 10 x 3
Codes (3d array): 5 x 1 x 3
Diff (3d array): 5 x 10 x 3
三维数组diff
是广播的结果,并非计算的必要条件。大型数据集会生成一个大型中间数组,这在计算上效率低下。相反,如果使用Python循环对上述二维示例中的代码对每个观测值进行单独计算,则可以使用更小的数组。
广播是编写简洁且通常直观代码的强大工具,它能够在C语言中非常高效地进行计算。但是,在某些情况下,广播会为特定算法使用不必要的大量内存。在这些情况下,最好用Python编写算法的外循环。这也可以产生更易读的代码,因为随着广播中维度数量的增加,使用广播的算法往往更难以理解。
脚注