广播#

另请参阅

numpy.broadcast

术语“广播”描述了 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 足够智能,可以在不实际复制的情况下使用原始标量值,从而使广播操作在内存和计算上尽可能高效。

A scalar is broadcast to match the shape of the 1-d array it is being multiplied to.

图 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 时,则使用另一个维度。换句话说,大小为 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 中,由于形状不兼容而引发异常。

A 1-d array with shape (3) is stretched to match the 2-d array of shape (4, 3) it is being added to, and the result is a 2-d array of shape (4, 3).

图 2#

一维数组添加到二维数组时,如果一维数组的元素数量与二维数组的列数匹配,则会发生广播。

A huge cross over the 2-d array of shape (4, 3) and the 1-d array of shape (4) shows that they can not be broadcast due to mismatch of shapes and thus produce no result.

图 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.]])
A 2-d array of shape (4, 1) and a 1-d array of shape (3) are stretched to match their shapes and produce a resultant array of shape (4, 3).

图 4#

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

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

实用示例:向量量化#

广播在实际问题中经常出现。一个典型的例子发生在信息论、分类及其他相关领域中使用的向量量化(VQ)算法。VQ 中的基本操作是找到一组点(在 VQ 术语中称为 `codes`)中与给定点(称为 `observation`)最近的点。在下面所示的非常简单的二维案例中,`observation` 中的值描述了待分类运动员的体重和身高。`codes` 代表不同类别的运动员。[1] 找到最近的点需要计算 `observation` 与每个 `codes` 之间的距离。最短距离提供最佳匹配。在此示例中,`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
A height versus weight graph that shows data of a female gymnast, marathon runner, basketball player, football lineman and the athlete to be classified. Shortest distance is found between the basketball player and the athlete to be classified.

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

注脚