广播#

术语“广播 (broadcasting)”描述了 NumPy 在算术运算期间如何处理具有不同形状的数组.在一定的约束条件下,较小的数组会在较大的数组上“广播 (broadcast)”,以便它们具有兼容的形状.广播提供了一种对数组操作进行矢量化的方法,以便循环发生在 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 具有相同形状的数组. 在 图 1 中显示了 b 中的新元素只是原始标量的副本. 拉伸类比仅是概念上的. NumPy 足够智能,可以使用原始标量值而无需实际进行复制,以便广播操作在内存和计算上尽可能高效.

标量会经过广播来匹配与其相乘的一维数组的形状.

图 1#

在广播的最简单示例中,标量 b 被拉伸成与 a 形状相同的数组,因此形状与元素方式的乘法兼容.

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

通用广播规则#

当对两个数组进行运算时,NumPy 逐个元素地比较它们的形状.它从尾部(即最右边)维度开始,然后向左进行.当满足以下条件时,两个维度是兼容的:

  1. 它们相等,或者

  2. 其中一个是 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 的维度被拉伸或“复制”以匹配另一个维度.

在以下示例中, AB 数组都具有长度为 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 # 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 中,由于形状不兼容,引发了异常.

形状为 (3) 的一维数组被拉伸以匹配形状为 (4, 3) 的二维数组,然后将它们相加,结果是一个形状为 (4, 3) 的二维数组.

图 2#

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

一个巨大的叉横跨形状为 (4, 3) 的二维数组和形状为 (4) 的一维数组,表明由于形状不匹配,它们无法广播,因此不会产生任何结果.

图 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) 的结果数组.

图 4#

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

在这里, 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
一个身高与体重的图表,显示了一名女性体操运动员,马拉松运动员,篮球运动员,橄榄球后卫和要分类的运动员的数据.在篮球运动员和要分类的运动员之间找到最短的距离.

图 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 编写算法的外部循环.这也可以生成更具可读性的代码,因为随着广播中维度数量的增加,使用广播的算法往往变得更难以解释.

脚注