NumPy 可以直接在相同大小和形状的数组之间执行操作,例如将两个 $3 \times 3$ 矩阵相加。但是,当需要对不同形状的数组执行操作(例如加法)时,情况会怎样呢?例如,将一个单独的数字(一个标量)加到数组的每个元素上,或者将一个一维数组(一个向量)加到二维数组(一个矩阵)的每一行上。手动操作时,你可能会想到编写循环来执行这些操作。然而,NumPy 提供了一种更高效、更简洁的机制,称为广播(broadcasting)。广播描述了 NumPy 用来处理不同形状数组之间算术运算的一套规则,它有效地“拉伸”或复制较小的数组,使其形状与较大的数组匹配,而无需实际使用额外的内存。这使得向量化操作成为可能,它们比显式的 Python 循环快得多。广播的规则如果数组满足特定的兼容性条件,广播机制就使得在不同形状数组之间进行操作成为可能。NumPy 从末尾(最右边)的维度开始,逐元素比较数组的形状。两个维度兼容的条件是:它们相等,或者任一维度为 1。如果这些条件对于任何维度对都不满足,就会引发 ValueError: operands could not be broadcast together 错误。让我们分解一下 NumPy 如何应用这些规则:对齐维度: 如果两个数组的维度数量不同,NumPy 会在较小数组的形状前添加 1,直到它们具有相同数量的维度。例如,将形状为 (3, 4) 的二维数组与形状为 (4,) 的一维数组进行比较时,一维数组的形状会被视为 (1, 4)。检查兼容性并拉伸: 然后,NumPy 会从最右边的维度开始,比较每个维度的大小。如果维度大小匹配,则继续检查下一个维度。如果有一个维度大小是 1,NumPy 会“拉伸”或“广播”该维度,以匹配另一个数组中对应维度的大小。其效果就好像大小为 1 的维度上的数据被复制以匹配更大的大小。如果维度大小不匹配,并且两个维度都不是 1,则无法进行广播,NumPy 会引发错误。一旦形状兼容并完成了广播,NumPy 就会执行逐元素操作。广播示例我们来看一下广播的实际作用。示例 1:标量与数组最简单的例子是数组与标量值之间的操作。标量被视为零维数组。import numpy as np arr = np.array([1, 2, 3]) scalar = 5 result = arr + scalar print(f"数组:\n{arr}") print(f"形状:{arr.shape}\n") print(f"标量:{scalar}\n") print(f"结果 (arr + scalar):\n{result}") print(f"形状:{result.shape}") # 输出: # 数组: # [1 2 3] # 形状:(3,) # # 标量:5 # # 结果 (arr + scalar): # [6 7 8] # 形状:(3,)在这里,标量 5 被有效地广播到数组 arr 的所有元素上。遵循规则:arr 的形状是 (3,),scalar 实际的形状是 ()。NumPy 为标量添加维度以匹配 arr 的维度数量:形状变为 (1,)。等等,标量是 0 维的。数组是 1 维的(形状 (3,))。标量的形状被视为通过重复其值来匹配任何数组的形状。该操作将标量视为一个与 arr 形状相同的数组 [5, 5, 5]。示例 2:一维数组与二维数组我们考虑将一个一维数组加到二维数组的每一行上。matrix = np.arange(6).reshape((2, 3)) # 形状 (2, 3) row_vector = np.array([10, 20, 30]) # 形状 (3,) result = matrix + row_vector print(f"矩阵 (形状 {matrix.shape}):\n{matrix}\n") print(f"行向量 (形状 {row_vector.shape}):\n{row_vector}\n") print(f"结果 (matrix + row_vector) (形状 {result.shape}):\n{result}") # 输出: # 矩阵 (形状 (2, 3)): # [[0 1 2] # [3 4 5]] # # 行向量 (形状 (3,)): # [10 20 30] # # 结果 (matrix + row_vector) (形状 (2, 3)): # [[10 21 32] # [13 24 35]]我们来追踪一下广播规则:matrix 形状:(2, 3)。row_vector 形状:(3,)。对齐维度:NumPy 会在 row_vector 的形状前添加 1。它变为 (1, 3)。从右到左比较维度:末尾维度:3(来自矩阵)和 3(来自向量)。它们匹配。下一个维度:2(来自矩阵)和 1(来自向量)。有一个是 1,因此兼容。拉伸:NumPy 将向量的第一个维度从 1 拉伸到 2。row_vector 实际上变为 [[10, 20, 30], [10, 20, 30]]。执行逐元素加法。以下图表说明了此过程:digraph G { rankdir=LR; node [shape=plaintext]; subgraph cluster_matrix { label = "矩阵 (2, 3)"; m [label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> <TR><TD>0</TD><TD>1</TD><TD>2</TD></TR> <TR><TD>3</TD><TD>4</TD><TD>5</TD></TR> </TABLE> >]; } subgraph cluster_vector { label = "向量 (3,) -> (1, 3)"; v [label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> <TR><TD>10</TD><TD>20</TD><TD>30</TD></TR> </TABLE> >]; } subgraph cluster_broadcasted_vector { label = "广播后的向量 (2, 3)"; bv [label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" BGCOLOR="#e9ecef"> <TR><TD>10</TD><TD>20</TD><TD>30</TD></TR> <TR><TD BGCOLOR="#adb5bd">10</TD><TD BGCOLOR="#adb5bd">20</TD><TD BGCOLOR="#adb5bd">30</TD></TR> </TABLE> >]; caption [label="沿轴 0 拉伸"]; } subgraph cluster_result { label = "结果 (2, 3)"; r [label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> <TR><TD>10</TD><TD>21</TD><TD>32</TD></TR> <TR><TD>13</TD><TD>24</TD><TD>35</TD></TR> </TABLE> >]; } m -> r [label="+"]; v -> bv [style=dashed, label="广播"]; bv -> r [label="+"]; }一维数组 [10, 20, 30](形状 (3,))首先被视为形状 (1, 3),然后沿第一个轴广播(拉伸),以匹配矩阵的形状 (2, 3) 进行逐元素加法。灰色行表示数据的复制。示例 3:列向量与行向量广播还可以组合数组以生成更高维度的结果。我们来将一个列向量加到一个行向量上。col_vector = np.array([[0], [10], [20]]) # 形状 (3, 1) row_vector = np.array([1, 2, 3]) # 形状 (3,) result = col_vector + row_vector print(f"列向量 (形状 {col_vector.shape}):\n{col_vector}\n") print(f"行向量 (形状 {row_vector.shape}):\n{row_vector}\n") print(f"结果 (col + row) (形状 {result.shape}):\n{result}") # 输出: # 列向量 (形状 (3, 1)): # [[ 0] # [10] # [20]] # # 行向量 (形状 (3,)): # [1 2 3] # # 结果 (col + row) (形状 (3, 3)): # [[ 1 2 3] # [11 12 13] # [21 22 23]]我们来追踪一下这个过程:col_vector 形状:(3, 1)。row_vector 形状:(3,)。对齐维度:NumPy 将 row_vector 视为形状 (1, 3)。从右到左比较维度 (3, 1) 与 (1, 3):末尾维度:1(来自列)和 3(来自行)。有一个是 1,兼容。下一个维度:3(来自列)和 1(来自行)。有一个是 1,兼容。拉伸:col_vector 大小为 1 的维度被拉伸到 3。它变为形状 (3, 3)。row_vector 大小为 1 的维度被拉伸到 3。它也变为形状 (3, 3)。对广播后的 (3, 3) 版本执行逐元素加法。广播失败的情况广播只有在形状根据规则兼容的情况下才有效。如果在任何时候维度大小不同且都不是 1,NumPy 将无法解决这种不明确性,并会引发错误。arr1 = np.arange(6).reshape((2, 3)) # 形状 (2, 3) arr2 = np.array([1, 2]) # 形状 (2,) try: result = arr1 + arr2 except ValueError as e: print(f"错误:{e}") # 输出: # 错误:操作数无法一起广播,形状为 (2,3) (2,)这里,arr1 是 (2, 3),arr2 是 (2,)。NumPy 将 arr2 视为 (1, 2)。比较 (2, 3) 与 (1, 2):末尾维度:3 与 2。它们不同,且都不是 1。广播失败。为了使其正常工作,arr2 需要具有形状 (3,) 或 (2, 1) 或 (1, 3),具体取决于预期的操作。广播是 NumPy 中一个基本机制,它通过避免显式的 Python 循环,让你能编写更清晰、更简洁、明显更快的代码,用于对兼容但不同形状的数组执行操作。了解其规则对于使用 NumPy 进行高效的数值编程非常重要。