numpy-形状操作

Posted by Hilda on July 17, 2025

1.什么是广播

在 NumPy 中,广播(Broadcasting)是一种强大的机制,它允许在形状不同的数组之间执行算术运算(如加、减、乘、除等)。当两个数组的形状不完全相同时,NumPy 会尝试通过“扩展”较小数组的维度来使其与较大数组的形状兼容,从而无需显式地复制数据。这极大地简化了代码,并提高了计算效率。

广播是 NumPy 在执行算术运算时处理具有不同形状的数组的一种方式。它允许在不实际创建数据副本的情况下,对维度不匹配的数组执行逐元素操作。其核心思想是,如果两个数组在某个维度上不兼容,但其中一个数组在该维度上的大小为 1,那么这个大小为 1 的维度会被“拉伸”或“广播”以匹配另一个数组在该维度上的大小。

这种机制的优势在于:

  1. 内存效率: 避免了显式创建大型临时数组来匹配形状,从而节省了大量内存。
  2. 代码简洁: 使得编写处理不同形状数组的代码变得更加简单和直观。
  3. 计算速度: 广播操作在 C 语言层面实现,因此非常高效。

【广播规则】

要理解广播,最重要的是掌握 NumPy 判断两个数组是否“可广播”以及如何广播的规则。当对两个数组执行操作时,NumPy 从它们的末尾维度开始,并沿着每个维度向前比较它们的形状。只有满足以下所有条件的维度才被认为是兼容的:

  1. 维度相等: 两个数组在该维度上的大小相同。
  2. 其中一个维度为 1: 两个数组中至少有一个在该维度上的大小为 1。
  3. 其中一个数组没有该维度: 如果一个数组的维度比另一个少,那么较小数组的形状会被“左侧填充”1,直到它们的维度数量相同。

如果所有维度都兼容,那么两个数组就是可广播的。结果数组的形状将是每个维度上最大值的形状。

2.一维数组广播到二维数组

当一个一维数组与一个二维数组进行操作时,如果一维数组的长度与二维数组的最后一维(列数)匹配,那么这个一维数组会被广播。NumPy 会在内部将这个一维数组沿着二维数组的第一个维度(行)进行复制,使其在逻辑上匹配二维数组的形状。

例如,一个形状为 (N,) 的一维数组与一个形状为 (M, N) 的二维数组进行操作时,一维数组会被广播到 (M, N) 的形状,相当于将其复制 M 次,每一行都与一维数组相同。

在 NumPy 内部,当进行广播时,实际上并不会创建数据的物理副本。相反,它会调整数组的“步长”(strides)信息。步长定义了在内存中移动一个元素需要跳过的字节数。对于被广播的维度,NumPy 会将该维度的步长设置为 0。这意味着当访问该维度上的下一个元素时,内存指针不会移动,从而重复使用相同的数据。

对于一维数组 arr2 (shape (3,)) 和二维数组 arr1 (shape (4, 3)) 的加法操作 arr1 + arr2

  1. 维度对齐: arr1 的形状是 (4, 3)arr2 的形状是 (3,)。NumPy 会在 arr2 的左侧填充 1,使其形状变为 (1, 3)
  2. 从右向左比较:
    • 维度 1 (最右边): arr1 的大小是 3arr2 的大小是 3。它们相等,兼容。
    • 维度 0 (左边): arr1 的大小是 4arr2 的大小是 1arr2 的大小为 1,兼容。
  3. 广播: 由于所有维度都兼容,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. 给定以下代码:

    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]
  2. 一个形状为 (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 vs 3 (相等,兼容)
    • 维度 0 (左): 5 vs 1 (Y 为 1,兼容)

    结果形状取最大值:(5, 3)

(2)编程题

  1. 创建一个 3*5 的二维数组 matrix,所有元素初始化为 1。
  2. 创建一个长度为 5 的一维数组 vector,元素为 0,1,2,3,4。
  3. 计算 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. 维度对齐: 两个数组的维度数量相同。
  2. 从右向左比较:
    • 维度 1 (最右边): arr1 的大小是 Narr2 的大小是 1arr2 的大小为 1,兼容。
    • 维度 0 (左边): arr1 的大小是 Marr2 的大小是 M。它们相等,兼容。
  3. 广播: 由于所有维度都兼容,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. 给定以下代码:

    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]])
    
  2. 一个形状为 (3, 5) 的数组 A 与一个形状为 (1, 5) 的数组 B 进行乘法运算。结果数组的形状是什么?

    A. (3, 5) B. (5, 3) C. (1, 5) D. 报错

    答案:A

(2)编程题

  1. 创建一个 4*2 的二维数组 data_matrix,元素为 0 到 7。
  2. 创建一个 4*1 的二维数组 scaling_factor,元素为 10,20,30,40。
  3. 计算 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

  1. 维度对齐: arr1 形状 (3, 4, 2)arr2 形状 (4, 2)。NumPy 会在 arr2 的左侧填充 1,使其形状变为 (1, 4, 2)
  2. 从右向左比较:
    • 维度 2 (最右边): arr1 的大小是 2arr2 的大小是 2。它们相等,兼容。
    • 维度 1 (中间): arr1 的大小是 4arr2 的大小是 4。它们相等,兼容。
    • 维度 0 (最左边): arr1 的大小是 3arr2 的大小是 1arr2 的大小为 1,兼容。
  3. 广播: 由于所有维度都兼容,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)选择题

  1. 一个形状为 (2, 3, 4) 的数组 A 与一个形状为 (4,) 的数组 B 进行加法运算。结果数组的形状是什么?

    A. (2, 3, 4) B. (4, 3, 2) C. (2, 3) D. 报错

    答案:A

  2. 一个形状为 (5, 1, 3) 的数组 X 与一个形状为 (5, 4, 3) 的数组 Y 进行运算。结果数组的形状是什么?

    A. (5, 4, 3) B. (5, 1, 3) C. (5, 4, 1) D. 报错

    答案:A

(2)编程题

  1. 创建一个形状为 (2, 3, 5) 的三维数组 data_3d,所有元素初始化为 1。
  2. 创建一个形状为 (3, 5) 的二维数组 mask_2d,所有元素初始化为 10。
  3. 计算 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 会从它们的末尾维度开始,并沿着每个维度向前比较它们的形状。

  1. 维度数量不同: 如果两个数组的维度数量不同,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)
  2. 维度兼容性: 两个数组在某个维度上是兼容的,如果:
    • 它们在该维度上的大小相等,或者
    • 其中一个数组在该维度上的大小为 1。
  3. 广播扩展: 如果一个维度的大小为 1,它会被“拉伸”以匹配另一个数组在该维度上的大小。
  4. 不兼容报错: 如果在任何维度上,两个数组的大小都不相等,并且都没有一个维度的大小为 1,那么就会引发 ValueError: operands could not be broadcast together with shapes ... 错误。
  5. 结果形状: 结果数组的形状将是每个维度上最大值的形状。

通过理解这些规则,可以预测广播操作的结果,并有效地利用 NumPy 的强大功能进行数组运算。