广播#

术语 broadcasting 描述了 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 中的新元素,如 Figure 1 所示,只是原始标量的副本.拉伸的比喻只是概念性的.NumPy 足够聪明,可以在不实际复制的情况下使用原始标量值,从而使广播操作在内存和计算上尽可能高效.

标量会被广播以匹配它所乘的1-d数组的形状.

Figure 1#

在广播的最简单示例中,标量 b 被拉伸以成为一个与 a 形状相同的数组,因此形状适合逐元素乘法.

第二个示例中的代码比第一个更高效,因为在乘法过程中广播移动的内存更少(b 是一个标量而不是数组).

通用广播规则#

当操作两个数组时,NumPy 会逐元素比较它们的形状.它从尾随(即最右边)维度开始,并向左进行.两个维度在以下情况下是兼容的:

  1. 它们是相等的,或者

  2. 其中之一是 1.

如果这些条件未满足,则会抛出 ValueError: operands could not be broadcast together 异常,表明数组形状不兼容.

输入数组不需要具有相同的*数量*维数.结果数组将具有与输入数组中维数最多的数组相同的维数,其中每个维度的*大小*是相应维度在输入数组中的最大大小.请注意,缺失的维度假设大小为一.

例如,如果你有一个 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 数组都有长度为一的轴,在广播操作期间这些轴被扩展到更大的尺寸:

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,)

Figure 2 所示,``b`` 被添加到 a 的每一行中.在 Figure 3 中,由于形状不兼容,引发了一个异常.

一个形状为 (3) 的 1-d 数组被拉伸以匹配其正在相加的形状为 (4, 3) 的 2-d 数组,结果是一个形状为 (4, 3) 的 2-d 数组.

Figure 2#

一个一维数组添加到一个二维数组中,如果一维数组的元素数量与二维数组的列数匹配,则会导致广播.

一个巨大的十字架横跨形状为 (4, 3) 的二维数组和形状为 (4) 的一维数组,表明由于形状不匹配,它们不能被广播,因此不会产生结果.

Figure 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.]])
一个形状为 (4, 1) 的二维数组和一个形状为 (3) 的一维数组 被拉伸以匹配它们的形状并生成一个形状为 (4, 3) 的结果数组.

Figure 4#

在某些情况下,广播会拉伸两个数组以形成一个比初始数组中任何一个都大的输出数组.

这里,``newaxis`` 索引操作符在 a 中插入一个新的轴,使其成为一个二维的 4x1 数组.将 4x1 数组与形状为 (3,)b 结合,得到一个 4x3 数组.

一个实际的例子:向量量化#

广播在现实世界问题中经常出现.一个典型的例子出现在信息论、分类学和其他相关领域的向量量化(VQ)算法中.VQ中的基本操作是找到一组点中最接近给定点的点,这组点在VQ术语中称为``codes``,给定点称为``observation``.在下面非常简单的二维情况下,``observation``中的值描述了要分类的运动员的体重和身高.``codes``代表不同类别的运动员.[#f1]_ 找到最接近的点需要计算观察点与每个代码之间的距离.最短的距离提供了最佳匹配.在这个例子中,``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
一个显示女性体操运动员、马拉松跑者、篮球运动员、橄榄球前锋和待分类运动员数据的高度与体重图.最短距离在篮球运动员和待分类运动员之间找到.

Figure 5#

向量量化的基本操作计算了待分类对象(黑色方块)与多个已知码字(灰色圆圈)之间的距离.在这个简单的情况下,码字代表单个类别.更复杂的情况使用每个类别的多个码字.

通常,大量的 ``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编写算法的外层循环.这还可能产生更易读的代码,因为使用广播的算法随着广播中维数的增加往往会变得更加难以解释.

脚注