1.什么是广播
在 NumPy 中,广播(Broadcasting)是一种强大的机制,它允许在形状不同的数组之间执行算术运算(如加、减、乘、除等)。当两个数组的形状不完全相同时,NumPy 会尝试通过“扩展”较小数组的维度来使其与较大数组的形状兼容,从而无需显式地复制数据。这极大地简化了代码,并提高了计算效率。
广播是 NumPy 在执行算术运算时处理具有不同形状的数组的一种方式。它允许在不实际创建数据副本的情况下,对维度不匹配的数组执行逐元素操作。其核心思想是,如果两个数组在某个维度上不兼容,但其中一个数组在该维度上的大小为 1,那么这个大小为 1 的维度会被“拉伸”或“广播”以匹配另一个数组在该维度上的大小。
这种机制的优势在于:
- 内存效率: 避免了显式创建大型临时数组来匹配形状,从而节省了大量内存。
- 代码简洁: 使得编写处理不同形状数组的代码变得更加简单和直观。
- 计算速度: 广播操作在 C 语言层面实现,因此非常高效。
【广播规则】
要理解广播,最重要的是掌握 NumPy 判断两个数组是否“可广播”以及如何广播的规则。当对两个数组执行操作时,NumPy 从它们的末尾维度开始,并沿着每个维度向前比较它们的形状。只有满足以下所有条件的维度才被认为是兼容的:
- 维度相等: 两个数组在该维度上的大小相同。
- 其中一个维度为 1: 两个数组中至少有一个在该维度上的大小为 1。
- 其中一个数组没有该维度: 如果一个数组的维度比另一个少,那么较小数组的形状会被“左侧填充”1,直到它们的维度数量相同。
如果所有维度都兼容,那么两个数组就是可广播的。结果数组的形状将是每个维度上最大值的形状。
2.一维数组广播到二维数组
当一个一维数组与一个二维数组进行操作时,如果一维数组的长度与二维数组的最后一维(列数)匹配,那么这个一维数组会被广播。NumPy 会在内部将这个一维数组沿着二维数组的第一个维度(行)进行复制,使其在逻辑上匹配二维数组的形状。
例如,一个形状为 (N,)
的一维数组与一个形状为 (M, N)
的二维数组进行操作时,一维数组会被广播到 (M, N)
的形状,相当于将其复制 M
次,每一行都与一维数组相同。
在 NumPy 内部,当进行广播时,实际上并不会创建数据的物理副本。相反,它会调整数组的“步长”(strides)信息。步长定义了在内存中移动一个元素需要跳过的字节数。对于被广播的维度,NumPy 会将该维度的步长设置为 0。这意味着当访问该维度上的下一个元素时,内存指针不会移动,从而重复使用相同的数据。
对于一维数组
arr2
(shape(3,)
) 和二维数组arr1
(shape(4, 3)
) 的加法操作arr1 + arr2
:
- 维度对齐:
arr1
的形状是(4, 3)
,arr2
的形状是(3,)
。NumPy 会在arr2
的左侧填充 1,使其形状变为(1, 3)
。- 从右向左比较:
- 维度 1 (最右边):
arr1
的大小是3
,arr2
的大小是3
。它们相等,兼容。
- 维度 0 (左边):
arr1
的大小是4
,arr2
的大小是1
。arr2
的大小为 1,兼容。- 广播: 由于所有维度都兼容,
arr2
会被广播。在逻辑上,arr2
会被复制4
次,形成一个(4, 3)
的临时数组,其中每一行都是[1, 2, 3]
。然后,逐元素相加。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
arr1 = np.sort(np.array([0, 1, 2, 3] * 3)).reshape(4, 3) # 4*3
arr1
"""
array([[0, 0, 0],
[1, 1, 1],
[2, 2, 2],
[3, 3, 3]])
"""
arr2 = np.array([1, 2, 3]) # 1*3 (3,)
arr2
"""
array([1, 2, 3])
"""
arr3 = arr1 + arr2
arr3
"""
array([[1, 2, 3],
[2, 3, 4],
[3, 4, 5],
[4, 5, 6]])
"""
(1)选择题
-
给定以下代码:
1 2 3 4
import numpy as np A = np.array([[1, 2], [3, 4]]) # shape (2, 2) B = np.array([10, 20]) # shape (2,) C = A * B
C
的值是什么? A.[[10, 40], [30, 80]]
B.[[10, 20], [30, 40]]
C.[[10, 20], [60, 80]]
D. 报错答案:A,
A
的形状是(2, 2)
,B
的形状是(2,)
。B
会被广播为(1, 2)
,然后逻辑上复制成(2, 2)
的[[10, 20], [10, 20]]
。逐元素相乘:
[1, 2] * [10, 20] = [10, 40]
[3, 4] * [10, 20] = [30, 80]
-
一个形状为
(5, 3)
的数组X
与一个形状为(3,)
的数组Y
进行加法运算。结果数组的形状是什么?A.
(5, 3)
B.(3, 5)
C.(5,)
D. 报错答案:A,
X.shape = (5, 3)
,Y.shape = (3,)
。Y
会被左侧填充 1 变为(1, 3)
。比较:
- 维度 1 (右):
3
vs3
(相等,兼容) - 维度 0 (左):
5
vs1
(Y
为 1,兼容)
结果形状取最大值:
(5, 3)
。 - 维度 1 (右):
(2)编程题
- 创建一个
3*5
的二维数组matrix
,所有元素初始化为 1。 - 创建一个长度为 5 的一维数组
vector
,元素为 0,1,2,3,4。 - 计算
matrix + vector
,并打印结果数组及其形状。解释vector
是如何被广播的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
arr = np.ones((3, 5),dtype=int)
arr
"""
array([[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1]])
"""
vector = np.arange(0, 5)
vector
"""
array([0, 1, 2, 3, 4])
"""
res = vector + arr
res
"""
array([[1, 2, 3, 4, 5],
[1, 2, 3, 4, 5],
[1, 2, 3, 4, 5]])
"""
3.二维数组的广播(列向量广播)
当一个形状为 (M, 1)
的二维数组(即一个列向量)与一个形状为 (M, N)
的二维数组进行操作时,列向量会被广播。NumPy 会在内部将这个列向量沿着第二个维度(列)进行复制,使其在逻辑上匹配二维数组的形状。
例如,一个形状为 (M, 1)
的数组与一个形状为 (M, N)
的数组进行操作时,(M, 1)
的数组会被广播到 (M, N)
的形状,相当于将其每一列都复制 N
次。
与一维数组广播类似,列向量广播也是通过调整步长来实现的。对于形状为
(M, 1)
的arr2
和形状为(M, N)
的arr1
的加法操作arr1 + arr2
:
- 维度对齐: 两个数组的维度数量相同。
- 从右向左比较:
- 维度 1 (最右边):
arr1
的大小是N
,arr2
的大小是1
。arr2
的大小为 1,兼容。
- 维度 0 (左边):
arr1
的大小是M
,arr2
的大小是M
。它们相等,兼容。- 广播: 由于所有维度都兼容,
arr2
会被广播。在逻辑上,arr2
会被复制N
次,形成一个(M, N)
的临时数组,其中每一列都是arr2
的内容。然后,逐元素相加。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
arr1 = np.sort(np.array([0, 1, 2, 3] * 3)).reshape(4, 3)
arr1
"""
array([[0, 0, 0],
[1, 1, 1],
[2, 2, 2],
[3, 3, 3]])
"""
arr2 = np.array([[1], [2], [3], [4]]) # 4 * 1
arr2
"""
array([[1],
[2],
[3],
[4]])
"""
arr3 = arr1 + arr2
arr3
"""
array([[1, 1, 1],
[3, 3, 3],
[5, 5, 5],
[7, 7, 7]])
"""
# arr2 逻辑上被复制成 3 份,形成一个 4x3 的临时数组:
# [[1, 1, 1],
# [2, 2, 2],
# [3, 3, 3],
# [4, 4, 4]]
# 然后与 arr1 逐元素相加:
# [[0,0,0] + [1,1,1] = [1,1,1]
# [1,1,1] + [2,2,2] = [3,3,3]
# [2,2,2] + [3,3,3] = [5,5,5]
# [3,3,3] + [4,4,4] = [7,7,7]]
(1)选择题
-
给定以下代码:
1 2 3 4
import numpy as np X = np.array([[1, 2, 3], [4, 5, 6]]) # shape (2, 3) Y = np.array([[10], [20]]) # shape (2, 1) Z = X - Y
Z
的形状是什么? A.(2, 3)
B.(3, 2)
C.(2, 1)
D. 报错答案:A,得到的Z应该是:
1 2
array([[ -9, -8, -7], [-16, -15, -14]])
-
一个形状为
(3, 5)
的数组A
与一个形状为(1, 5)
的数组B
进行乘法运算。结果数组的形状是什么?A.
(3, 5)
B.(5, 3)
C.(1, 5)
D. 报错答案:A
(2)编程题
- 创建一个
4*2
的二维数组data_matrix
,元素为 0 到 7。 - 创建一个
4*1
的二维数组scaling_factor
,元素为 10,20,30,40。 - 计算
data_matrix * scaling_factor
,并打印结果数组及其形状。解释scaling_factor
是如何被广播的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
data_matrix = np.arange(0, 8).reshape(4, 2)
data_matrix
"""
array([[0, 1],
[2, 3],
[4, 5],
[6, 7]])
"""
scaling_factor = np.array([[10], [20], [30], [40]])
scaling_factor
"""
array([[10],
[20],
[30],
[40]])
"""
res = data_matrix * scaling_factor
res
"""
array([[ 0, 10],
[ 40, 60],
[120, 150],
[240, 280]])
"""
4.三维数组广播
广播机制同样适用于更高维度的数组。当一个低维数组与一个高维数组进行操作时,NumPy 会尝试在低维数组的左侧填充 1,然后从右向左比较维度,并根据广播规则进行扩展。
对于形状为
(3, 4, 2)
的arr1
和形状为(4, 2)
的arr2
的加法操作arr1 + arr2
:
- 维度对齐:
arr1
形状(3, 4, 2)
,arr2
形状(4, 2)
。NumPy 会在arr2
的左侧填充 1,使其形状变为(1, 4, 2)
。- 从右向左比较:
- 维度 2 (最右边):
arr1
的大小是2
,arr2
的大小是2
。它们相等,兼容。
- 维度 1 (中间):
arr1
的大小是4
,arr2
的大小是4
。它们相等,兼容。- 维度 0 (最左边):
arr1
的大小是3
,arr2
的大小是1
。arr2
的大小为 1,兼容。- 广播: 由于所有维度都兼容,
arr2
会被广播。在逻辑上,arr2
会被复制3
次,形成一个(3, 4, 2)
的临时数组,其中每个“切片”(在第一个维度上)都是arr2
的内容。然后,逐元素相加。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
arr1 = np.array([0, 1, 2, 3, 4, 5, 6, 7] * 3).reshape(3, 4, 2)
arr1
"""
array([[[0, 1],
[2, 3],
[4, 5],
[6, 7]],
[[0, 1],
[2, 3],
[4, 5],
[6, 7]],
[[0, 1],
[2, 3],
[4, 5],
[6, 7]]])
"""
arr2 = np.array([0, 1, 2, 3, 4, 5, 6, 7]).reshape(4, 2)
arr2
"""
array([[0, 1],
[2, 3],
[4, 5],
[6, 7]])
"""
arr3 = arr1 + arr2
arr3
"""
array([[[ 0, 2],
[ 4, 6],
[ 8, 10],
[12, 14]],
[[ 0, 2],
[ 4, 6],
[ 8, 10],
[12, 14]],
[[ 0, 2],
[ 4, 6],
[ 8, 10],
[12, 14]]])
"""
(1)选择题
-
一个形状为
(2, 3, 4)
的数组A
与一个形状为(4,)
的数组B
进行加法运算。结果数组的形状是什么?A.
(2, 3, 4)
B.(4, 3, 2)
C.(2, 3)
D. 报错答案:A
-
一个形状为
(5, 1, 3)
的数组X
与一个形状为(5, 4, 3)
的数组Y
进行运算。结果数组的形状是什么?A.
(5, 4, 3)
B.(5, 1, 3)
C.(5, 4, 1)
D. 报错答案:A
(2)编程题
- 创建一个形状为
(2, 3, 5)
的三维数组data_3d
,所有元素初始化为 1。 - 创建一个形状为
(3, 5)
的二维数组mask_2d
,所有元素初始化为 10。 - 计算
data_3d * mask_2d
,并打印结果数组及其形状。解释mask_2d
是如何被广播的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
data_3d = np.ones((2, 3, 5), dtype=int)
data_3d
"""
array([[[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1]],
[[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1]]])
"""
mask_2d = np.zeros((3, 5), dtype=int)
mask_2d
"""
array([[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]])
"""
res = data_3d * mask_2d
res
"""
array([[[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]]])
"""
5.总结
当对两个数组执行操作时,NumPy 会从它们的末尾维度开始,并沿着每个维度向前比较它们的形状。
- 维度数量不同: 如果两个数组的维度数量不同,NumPy 会在维度较少的数组的左侧(前面)填充 1,直到它们的维度数量相同。
- 例如:
A.shape = (4, 3)
,B.shape = (3,)
->B
变为(1, 3)
。 - 例如:
A.shape = (3, 4, 2)
,B.shape = (4, 2)
->B
变为(1, 4, 2)
。
- 例如:
- 维度兼容性: 两个数组在某个维度上是兼容的,如果:
- 它们在该维度上的大小相等,或者
- 其中一个数组在该维度上的大小为 1。
- 广播扩展: 如果一个维度的大小为 1,它会被“拉伸”以匹配另一个数组在该维度上的大小。
- 不兼容报错: 如果在任何维度上,两个数组的大小都不相等,并且都没有一个维度的大小为 1,那么就会引发
ValueError: operands could not be broadcast together with shapes ...
错误。 - 结果形状: 结果数组的形状将是每个维度上最大值的形状。
通过理解这些规则,可以预测广播操作的结果,并有效地利用 NumPy 的强大功能进行数组运算。