6.10. 广播

当一个二元运算符接收到两个形状不完全匹配的数组时,numpy 不会抛出错误——它会进行广播。广播是一小组规则,用来判断两个形状是否兼容,如果兼容,则决定如何将较小的那个虚拟地拉伸以匹配较大的那个。

6.10.1. 规则

当两个操作数的形状分别为 AB 时,numpy 会分两步处理它们。

  1. 对齐秩。 如果一个操作数的轴数比另一个少,numpy 会在其形状的前端虚拟地补上大小为 1 的轴,直到两个形状的轴数相同。一个形状为 (3,) 的一维操作数与一个形状为 (2, 3) 的二维操作数配对时,会变成 (1, 3)(2, 3)

  2. 逐轴检查。 逐轴遍历现在长度相等的两个形状,每一对大小都必须满足以下两个条件之一:大小相等,或其中一个为 1。大小为 1 的轴会被虚拟地拉伸到另一侧的大小以进行运算。(1, 3)(2, 3) 是兼容的,因为第一个轴有一个 1(拉伸为 2),第二个轴匹配(3 == 3);结果的形状为 (2, 3)

如果任何一对轴都不满足上述两个条件,则形状不兼容,运算符会抛出 ValueError

6.10.2. 示例

标量与任意数组。 标量的行为就像形状 (1,),可以拉伸成任何形状:

a = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float)
a + 10                 # (2, 3) + scalar -> (2, 3)

一维向量跨越二维矩阵。 规则 1 在前端补上一个大小为 1 的轴,将 (3,) 变为 (1, 3);规则 2 随后将那一行沿 a 的每一列向下拉伸:

row = np.array([100, 200, 300], dtype=np.float)
a + row                # (2, 3) + (3,) -> (2, 3)

两个长度相等的一维数组逐元素相加——无需广播:

np.arange(4) + np.arange(4)

列向量与行向量 产生一个二维的“外积”形状:(4, 1)(3,) 配对,经过秩前置后变为 (4, 1)(1, 3),规则 2 随后将每个操作数沿其大小为 1 的轴拉伸:

x = np.array([1, 2, 3, 4]).reshape((4, 1))     # column
y = np.array([10, 20, 30])                      # row
x + y                                           # (4, 3) matrix

相同的形状规则适用于任何双参数 ufunc,包括 arctan2():

np.arctan2(y, 1.0)
np.arctan2(y, x)

6.10.3. 广播不会分配什么

拉伸是虚拟的。numpy 会同时遍历两个操作数,沿广播轴重新读取较小的那个,而不是复制它。较短数组的数据绝不会在内存中被复制。

对内存而言,重要的是输出数组的大小。a + row 会分配一个形状与 a 相同的输出,而不是 a 的形状加上 row 的形状。冗长的广播链仍然可能产生庞大的中间结果。

6.10.4. 广播出错时

典型的失败情形是两个形状都没有大小为 1 的轴可供拉伸,且大小不一致——例如 (3, 4)(4, 3)。规则 2 无法让 3 匹配 4,因此 numpy 会抛出 ValueError

一个更微妙的问题是广播成功了,但并非应用程序所期望的方式。(5,)(5, 1) 是典型案例:秩前置将 (5,) 变为 (1, 5),它与 (5, 1) 广播后产生一个 (5, 5) 矩阵——即两个向量的外积组合,而非应用程序很可能想要的长度为 5 的逐元素结果。如有疑问,请打印两侧的 shape,并在动用 reshape()transpose() 之前逐条对照规则。