广播#

也可以看看

numpy.broadcast

术语“广播”描述了 NumPy 在算术运算期间如何处理不同形状的数组。受到某些限制,较小的数组会“广播”到较大的数组上,以便它们具有兼容的形状。广播提供了一种向量化数组操作的方法,以便循环发生在 C 而不是 Python 中。它无需制作不必要的数据副本即可完成此操作,并且通常会实现高效的算法实现。然而,在某些情况下,广播并不是一个好主意,因为它会导致内存使用效率低下,从而减慢计算速度。

NumPy 运算通常在数组对上逐个元素地完成。在最简单的情况下,两个数组必须具有完全相同的形状,如下例所示:

>>> 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 的广播规则会放宽此约束。最简单的广播示例发生在数组和标量值组合在运算中时:

>>> a = np.array([1.0, 2.0, 3.0])
>>> b = 2.0
>>> a * b
array([2.,  4.,  6.])

结果相当于前面的例子,其中b是一个数组。我们可以认为标量在算术运算期间b拉伸成与 形状相同的数组a。中的新元素 (如图1b所示)只是原始标量的副本。拉伸类比只是概念性的。 NumPy 足够聪明,可以使用原始标量值,而无需实际复制,以便广播操作尽可能提高内存和计算效率。

广播标量以匹配与其相乘的一维数组的形状。

图1

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

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

一般广播规则#

当操作两个数组时,NumPy 按元素比较它们的形状。它从尾随(即最右边)维度开始并向左移动。两个维度兼容时

  1. 它们是相等的,或者

  2. 其中之一是 1。

如果不满足这些条件, 则会引发异常,表明数组的形状不兼容。ValueError: operands could not be broadcast together

输入数组不需要具有相同的维。生成的数组将具有与具有最大维度的输入数组相同的维度数,其中每个维度的大小是输入数组中相应维度的最大大小。请注意,缺失的维度被假定为大小一。

例如,如果您有一个256x256x3RGB 值数组,并且想要将图像中的每种颜色缩放不同的值,则可以将该图像乘以具有 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.shapeis (5,1)、b.shapeis (1,6)、c.shapeis (6,) 和d.shapeis () 且d是标量,则abcd均可广播到维度 (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

将一维数组添加到二维数组时的广播示例:

>>> 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 #

当数组的尾部维度不相等时,广播会失败,因为无法将第一个数组的行中的值与第二个数组的元素对齐以进行逐元素加法。

广播提供了一种获取两个数组的外积(或任何其他外部操作)的便捷方法。以下示例显示两个一维数组的外部加法运算:

>>> 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数组与b形状为 的组合(3,),生成一个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 编写算法的外循环。这也可能会产生更可读的代码,因为随着广播中维度数量的增加,使用广播的算法往往会变得更难以解释。

脚注