NumPy 2.x 完全指南【十九】广播机制
文章目录
- 1. 概述
- 2. 广播规则
- 2.1 补齐维度
- 2.2 扩展大小为 1 的轴
- 3. 案例演示
- 3.1 算数运算
- 3.1.1 加法运算
- 3.1.2 除法运算
- 3.2 比较运算
- 3.3 逻辑运算
1. 概述
广播(Broadcasting
)机制:在满足特定条件的情况下,较小的数组会在较大的数组上进行自动扩展,使它们的形状兼容,从而允许不同形状的数组之间进行数值运算。
如果没有广播机制,在 NumPy
中是对两个数组进行运算时,它们必须具有完全相同的形状,比如进行乘法运算:
a = np.array([1.0, 2.0, 3.0]) # 形状 (3,)
b = np.array([2.0, 2.0, 2.0]) # 形状 (3,)
result = a * b
print(result) # 输出: [2., 4., 6.]
运算过程是逐元素进行的,数学公式表示如下:
a ∗ b = [ 1.0 2.0 3.0 ] ∗ [ 2.0 2.0 2.0 ] = [ 1.0 ∗ 2.0 2.0 ∗ 2.0 3.0 ∗ 2.0 ] = [ 2.0 4.0 6.0 ] \mathbf{a} * \mathbf{b} = \begin{bmatrix} 1.0 \\ 2.0 \\ 3.0 \end{bmatrix} * \begin{bmatrix} 2.0 \\ 2.0 \\ 2.0 \end{bmatrix}= \begin{bmatrix} 1.0 * 2.0 \\ 2.0 * 2.0 \\ 3.0 * 2.0 \end{bmatrix} = \begin{bmatrix} 2.0 \\ 4.0 \\ 6.0 \end{bmatrix} a∗b= 1.02.03.0 ∗ 2.02.02.0 = 1.0∗2.02.0∗2.03.0∗2.0 = 2.04.06.0
在 NumPy
中一个数组可以直接和一个标量值进行运算:
a = np.array([1.0, 2.0, 3.0])
b = 2.0
print(a * b)
# array([2., 4., 6.])
当运算中的 2
个数组的形状不同时,会自动触发广播机制,标量 b
在算术操作过程中自动扩展为与 a
具有相同形状的数组,从而使它们在逐元素乘法时的形状兼容:
🔨 提示: 扩展的类比只是概念性的,实际没有真的复制数据,而是虚拟复制(无实际内存开销),从而使广播操作在内存和计算效率上尽可能高效。
2. 广播规则
广播遵循两个规则,确保不同形状的数组能够兼容:
- 补齐维度:若数组维度数不同,在形状较小的数组前面补
1
,直到所有数组维度数相同。 - 扩展大小为
1
的轴:若数组在某一维度上的大小为1
,则沿该维度复制数据,使其大小与其他数组对应的维度保持一致。
2.1 补齐维度
在对两个数组进行操作时,输入数组不需要具有相同的维度数。NumPy
会从尾部(即最右边的)维度开始,逐元素比较它们的形状,对于维度数少的数组,并用 1
补齐。
例如,有一个 (256x256x3)
的 RGB
图像数据,用不同的值来缩放每个通道的颜色:
image = np.ones((256, 256, 3)) # 形状 (256, 256, 3)
scaling = np.array([0.5, 1.0, 1.5]) # 形状 (3,)
result = image * scaling # 形状 (256, 256, 3)
从右到左比较,维度对齐过程如下:
- 第
3
维中image=3
、scaling=3
,值相等表示形状兼容。 - 第
2
、1
维中image=256
,scaling
维度不足,进行补1
操作, 自动广播为(1, 1, 3)
的形状。
补齐后,需要确保每个维度满足以下条件之一:
- 维度相等:两个数组在该维度上的大小相同。
- 维度为
1
:其中一个数组在该维度上的大小为1
。
示例,若任意维度不满足上述条件,表示数组的形状不兼容,则抛出 ValueError: operands could not be broadcast together
:
a = np.ones((2, 3)) # 形状 (2,3) 和 (1, 2) 不兼容
b = np.ones((2,)) # 形状 (2,) → 补齐为 (1, 2)(左侧补1)
try:
result = a + b
except ValueError as e:
print(e) # 输出: operands could not be broadcast together...
结果数组的维度数是输入数组中维度数的最大值,每个维度的大小按以下规则确定:
- 若两个数组在该维度上的大小相同 → 结果维度大小不变。
- 若其中一个数组在该维度上的大小为
1
→ 结果维度大小为另一个数组的维度大小。 - 若某一数组在该维度不存在(维度数不足) → 默认为大小为
1
,再按上述规则处理。
2.2 扩展大小为 1 的轴
进行维度补齐后,会扩展大小为 1
的轴,仅仅是逻辑上的扩展,实际上并不复制数据。
例如,上面示例中图像和缩放因子的形状在维度补齐后为:
image=256
:(256, 256, 3)
scaling
:(1, 1, 3)
其中,只有 scaling
在第一、二轴上的大小为 1
,那么就需要进行扩展,扩展为另外一个数组同轴大小,即:
0
轴: 扩展为256
1
轴: 扩展为256
最终 scaling
扩展后的形状为 (256, 256, 3)
,和 image
是形状兼容的同型数组,接下来就可以进行逐元素运算了。
3. 案例演示
3.1 算数运算
3.1.1 加法运算
如果一维数组的元素数量与二维数组的列数相匹配,将一维数组加到二维数组上会导致广播。
示例 1 ,二维数组和一维数组相加,其中 b
的维度会补齐为 (1,3)
,然后第一个维度的 1
会被扩展为 a
对应位置的维度大小 4
,最终 b
的形状被扩展为 (4,3)
,与 a
兼容:
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]]) # (4,3)b = np.array([1.0, 2.0, 3.0])# (3)
print(a + b)
# [[ 1. 2. 3.]
# [11. 12. 13.]
# [21. 22. 23.]
# [31. 32. 33.]]
过程图示:
当数组的尾部维度不相等时,广播会失败,因为无法将第一个数组的行中的值与第二个数组的元素对齐进行逐元素相加:
3.1.2 除法运算
图像归一化(Image Normalization)是一种将图像像素值调整到统一范围或分布的数据预处理技术,主要用于消除量纲差异、加速模型训练并提升算法稳定性。
常用的归一化方法有:
Min-Max
归一化(最小-最大值归一化)Z-Score
标准化(均值-方差归一化)
Min-Max
归一化通过线性变换将原始数据缩放到指定的区间,通常为 [0, 1]
或 [-1, 1]
。在深度学习图像任务中,一般需要将图像像素值从 [0, 255]
缩放到 [0, 1]
,便于卷积操作。
基础公式:
X norm = X − X min X max − X min X_{\text{norm}} = \frac{X - X_{\text{min}}}{X_{\text{max}} - X_{\text{min}}} Xnorm=Xmax−XminX−Xmin
符号定义:
- X X X: 原始数据。
- X min X_{\text{min}} Xmin: 数据的最小值。
- X max X_{\text{max}} Xmax: 数据的最大值。
- X norm X_{\text{norm}} Xnorm: 归一化后的数据。
X_min
和 X_max
通常是数据集中每个特征的最小和最大值,对于 8
位无符号整型图像来说,像素范围固定为 [0, 255]
归一化时直接使用:
X norm = X 255 (等价于 X min = 0 , X max = 255 ) X_{\text{norm}} = \frac{X}{255} \quad \text{(等价于 } X_{\text{min}}=0,\ X_{\text{max}}=255 \text{)} Xnorm=255X(等价于 Xmin=0, Xmax=255)
这样,我们直接使用图像数据除以 255
就能实现归一化,示例代码:
# 创建一个形状为 (224, 224, 3) 的随机 RGB 图像
image = np.random.randint(0, 256, size=(224, 224, 3)).astype(np.float32)# 归一化
normalized = image / 255 # 广播到 (224,224,3)
print(normalized) # shape (224,224,3)
# [[[0.8 0.27450982 0.37254903]
# [0.3137255 0.29803923 0.36862746]
# [0.4627451 0.5254902 0. ]......
其中,标量 255
会补齐维度为(1,1,1)
,然后扩展为 (224,224,3)
,再执行逐元素计算。
3.2 比较运算
示例 1 ,一维数组与标量比较:
a = np.array([1, 2, 3]) # 形状 (3,)
result = a > 2 # 标量 2 广播到形状 (3,)
print(result) # [False False True]
示例 2 ,一维数组与二维数组比较:
a = np.array([[1, 2],[3, 4]]) # 形状 (2,2)b = np.array([2, 3]) # 形状 (2,)
result = a >= b # b 广播为(1,2) → 扩展为(2,2)
print(result)
# [[False False]
# [ True True]]
3.3 逻辑运算
示例 1 ,一维数组与标量的逻辑运算:
# 生成布尔数组
a = np.array([True, False, True]) # 形状 (3,)
b = True # 标量result_and = np.logical_and(a, b) # 标量广播到形状(3,)
result_or = np.logical_or(a, b)print("AND:", result_and) # [True False True]
print("OR:", result_or) # [True True True]
示例 2 ,一维数组与二维数组逻辑运算:
a = np.array([[True, False],[False, True]]) # 形状 (2,2)
b = np.array([True, False]) # 形状 (2,)result = np.logical_and(a, b) # b广播为(1,2) → 扩展为(2,2)
print(result)
# [[ True False]
# [False False]]