广播#
术语“广播”描述了 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 个值的一维数组。根据广播规则对齐这些数组的尾随轴的大小,可以看出它们是兼容的:
Image (3d array): 256 x 256 x 3
Scale (1d array): 3
Result (3d array): 256 x 256 x 3
当比较的维度之一为 1 时,使用另一个维度。换句话说,大小为 1 的维度会被扩展或“复制”以匹配另一个维度。
在以下示例中,A
和 B
数组都有长度为 1 的轴,在广播操作期间被扩展为更大的大小:
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 # 尾随维度不匹配
A (2d array): 2 x 1
B (3d array): 8 x 4 x 3 # 倒数第二个维度不匹配
一个 1-d 数组添加到 2-d 数组的广播示例:
>>> 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 中,由于形状不兼容,抛出了异常。
广播提供了一种方便的方法来获取两个数组的外积(或任何其他外操作)。以下示例展示了两个 1-d 数组的外加法操作:
>>> 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
数组与 b
结合,b
的形状为 (3,)
,生成一个 4x3
数组。
一个实际示例:向量量化#
广播在现实世界的问题中经常出现。一个典型的例子是信息论、分类和其他相关领域中使用的向量量化(vector quantization,简写为 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 # 广播发生在这里
>>> 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 中编写算法的外层循环。这也可以产生更具可读性的代码,因为使用广播的算法在广播的维度增加时往往会变得更加难以解释。
脚注