5.2 广播 (Broadcasting) NumPy 广播 (Broadcasting) 详解 广播 (Broadcasting) 是 NumPy 中一个强大的机制,它允许 NumPy 在算术运算期间处理形状不同的数组。通常,如果你想对两个数组进行运算,它们的形状必须完全相同。 然而,当数组的形状满足某些条件时,NumPy 可以自动地 "广播" 较小的数组,使其形状与较大的数组兼容,从而实现运算。 广播使得在不同形状的数组之间进行操作变得更加容易和高效,避免了显式地复制数据。 5.2.1 广播的规则 广播的规则定义了何时以及如何扩展数组以匹配形状。 广播的规则如下: 维度兼容性: 如果两个数组的维度数量不同,则在维度较小的数组的形状左侧填充 1,直到两个数组的维度数量相同。
广播 (Broadcasting) 是 NumPy 中一个强大的机制,它允许 NumPy 在算术运算期间处理形状不同的数组。通常,如果你想对两个数组进行运算,它们的形状必须完全相同。 然而,当数组的形状满足某些条件时,NumPy 可以自动地 "广播" 较小的数组,使其形状与较大的数组兼容,从而实现运算。 广播使得在不同形状的数组之间进行操作变得更加容易和高效,避免了显式地复制数据。
广播的规则定义了何时以及如何扩展数组以匹配形状。 广播的规则如下:
维度兼容性: 如果两个数组的维度数量不同,则在维度较小的数组的形状左侧填充 1,直到两个数组的维度数量相同。
形状兼容性: 如果两个数组的形状在任何维度上都不相同,则形状为 1 的维度上的数组会被拉伸以匹配另一个数组的形状。
广播失败: 如果在任何维度上,数组的形状既不相等,也不等于 1,则广播失败。
简单来说,为了使两个数组能够进行广播,它们必须满足以下条件之一:
数组具有相同的形状。
数组具有相同的维度数,并且每个维度上的大小要么相等,要么其中一个为 1。
数组具有不同的维度数,但维度较小的数组的所有维度大小都为 1。
NumPy 在执行广播时,会遵循以下步骤:
维度对齐: NumPy 首先将维度较少的数组的形状左侧填充 1,使其与维度较多的数组具有相同的维度数量。
形状匹配: 然后,NumPy 从后向前逐个维度地比较两个数组的形状。如果两个数组在某个维度上的大小相等,或者其中一个数组在该维度上的大小为 1,则该维度上的形状匹配。
数据扩展: 对于形状为 1 的维度,NumPy 会在该维度上复制数组的数据,直到其大小与另一个数组在该维度上的大小相等。 这种复制操作是在逻辑上进行的,实际上并不会在内存中创建新的数据副本。
执行运算: 在完成广播后,NumPy 会对形状匹配的数组执行相应的算术运算。
下面是一些广播的示例,帮助你更好地理解广播的规则和步骤。
示例 1:标量和数组相加
import numpy as np a = np.array([1, 2, 3]) b = 2 # 标量 c = a + b # 广播标量 b 到数组 a 的形状 print(c) # 输出: [3 4 5]
在这个例子中,标量 b 被广播到数组 a 的形状,然后与 a 的每个元素相加。
示例 2:一维数组和二维数组相加
import numpy as np a = np.array([[1, 2, 3], [4, 5, 6]]) # 2x3 数组 b = np.array([10, 20, 30]) # 1x3 数组 c = a + b # 广播数组 b 到数组 a 的形状 print(c) # 输出: # [[11 22 33] # [14 25 36]]
在这个例子中,一维数组 b 被广播到二维数组 a 的形状。 具体来说,b 在第一个维度上被复制,使其形状变为 (2, 3),然后与 a 相加。
示例 3:维度不匹配,需要填充 1
import numpy as np a = np.array([[1, 2, 3]]) # 1x3 数组 b = np.array([[1], [2], [3]]) # 3x1 数组 c = a + b # 广播数组 a 和 b print(c) # 输出: # [[2 3 4] # [3 4 5] # [4 5 6]]
在这个例子中,数组 a 的形状是 (1, 3),数组 b 的形状是 (3, 1)。 为了使它们能够进行广播,NumPy 会将 a 的形状变为 (3, 3),将 b 的形状也变为 (3, 3)。 a 在第一个维度上被复制,b 在第二个维度上被复制。
示例 4:广播失败
import numpy as np a = np.array([[1, 2, 3], [4, 5, 6]]) # 2x3 数组 b = np.array([10, 20]) # 1x2 数组 try: c = a + b # 广播失败 except ValueError as e: print(e) # 输出:operands could not be broadcast together with shapes (2,3) (2,)
在这个例子中,数组 a 的形状是 (2, 3),数组 b 的形状是 (2,)。 由于 b 的形状无法通过填充 1 来匹配 a 的形状,因此广播失败。
广播在 NumPy 中有广泛的应用,可以简化很多常见的操作。
import numpy as np data = np.array([[1, 2, 3], [4, 5, 6]]) mean = data.mean(axis=0) # 计算每一列的均值 std = data.std(axis=0) # 计算每一列的标准差 normalized_data = (data - mean) / std # 使用广播进行标准化 print(normalized_data)
import numpy as np points1 = np.array([[1, 2], [3, 4]]) points2 = np.array([[5, 6], [7, 8], [9, 10]]) # 使用广播计算 points1 和 points2 之间的欧氏距离 distances = np.sqrt(np.sum((points1[:, np.newaxis, :] - points2[np.newaxis, :, :]) ** 2, axis=2)) print(distances)
import numpy as np x = np.array([1, 2, 3]) y = np.array([4, 5]) # 使用广播生成网格 X, Y = np.meshgrid(x, y) print(X) print(Y)
虽然 NumPy 的广播机制在概念上很容易理解,但其内部实现却相当复杂。 为了避免不必要的内存复制,NumPy 使用了一种称为 "虚拟复制" 的技术。
当 NumPy 需要广播一个数组时,它不会真正地复制数组的数据。 相反,它会创建一个新的视图 (view),该视图指向原始数组的数据,并修改其步长 (stride) 信息。 步长定义了在内存中从一个元素移动到下一个元素所需的字节数。 通过修改步长,NumPy 可以让视图看起来像一个具有不同形状的数组,而无需复制任何数据。
例如,假设我们有一个形状为 (1, 3) 的数组 a:
import numpy as np a = np.array([[1, 2, 3]]) print(a.shape) # 输出: (1, 3) print(a.strides) # 输出: (24, 8) (假设数据类型为 int64)
这意味着,在内存中,a 的每一行占用 24 个字节,每一列占用 8 个字节。
现在,如果我们想将 a 广播到形状 (3, 3),NumPy 不会复制 a 的数据。 相反,它会创建一个新的视图,其形状为 (3, 3),步长为 (0, 8)。
这意味着,在新视图中,每一行占用 0 个字节,每一列占用 8 个字节。 因此,访问新视图的每一行都会返回相同的数据,从而实现了广播的效果。
可以使用 Mermaid 绘制一个流程图来更清晰地展示广播的流程:
虽然广播是一个强大的工具,但也需要注意以下几点:
性能: 虽然广播避免了显式的数据复制,但它仍然会带来一些性能开销。 在某些情况下,显式地复制数据可能比使用广播更有效。
内存: 广播创建的视图指向原始数组的数据。 因此,如果修改视图的数据,也会影响原始数组的数据。
可读性: 过度使用广播可能会使代码难以理解。 在编写代码时,应该权衡广播的便利性和代码的可读性。
广播是 NumPy 中一个重要的机制,它允许在形状不同的数组之间进行算术运算。 广播的规则定义了何时以及如何扩展数组以匹配形状。 广播在数据标准化、距离计算、网格生成等许多领域都有广泛的应用。 理解广播的原理和注意事项,可以帮助你编写更高效、更简洁的 NumPy 代码。 通过合理利用广播,可以避免不必要的内存复制,提高代码的执行效率。