NumPy 数值计算:多维数组与广播机制

FreeGuideOnline 最新 2026-06-16

NumPy 基础:理解多维数组

NumPy 是 Python 数值计算的基石,其核心是 ndarray 对象——一个能够存储同类型数据的多维数组。与 Python 原生列表相比,NumPy 数组在内存占用、运算速度和广播能力上具有显著优势,是数据科学、机器学习和科学计算不可或缺的工具。

为什么选择 NumPy 数组?

  • 内存高效:连续内存存储,无对象开销。
  • 向量化运算:无需显式循环,底层 C 实现加速计算。
  • 广播机制:自动扩展不同形状的数组以进行逐元素运算。
  • 丰富的生态:Pandas、SciPy、Matplotlib 等库均依赖 NumPy。

创建 NumPy 数组

从列表或元组创建

使用 np.array() 函数可将 Python 序列转换为数组,并可指定数据类型 dtype

import numpy as np

# 一维数组
arr1d = np.array([1, 2, 3, 4])
# 二维数组
arr2d = np.array([[1, 2, 3], [4, 5, 6]])
# 指定浮点类型
arr_float = np.array([1, 2, 3], dtype=np.float64)

内置快速创建函数

NumPy 提供多种便捷函数生成特定数组:

函数 说明 示例
np.zeros(shape) 创建全 0 数组 np.zeros((2, 3))
np.ones(shape) 创建全 1 数组 np.ones((3, 2), dtype=int)
np.full(shape, val) 创建填充指定值的数组 np.full((2, 2), 7)
np.eye(N) 创建 N 维单位矩阵 np.eye(3)
np.arange(start, stop, step) 类似 range,返回数组 np.arange(0, 10, 2)
np.linspace(start, stop, num) 创建等间距数组 np.linspace(0, 1, 5)
np.random.rand(d0, d1,...) 创建均匀分布随机数数组 np.random.rand(3, 2)
np.random.randn(d0, d1,...) 创建标准正态分布随机数组 np.random.randn(2, 2)

数组属性

每个 ndarray 都有一组描述其特征的属性:

  • ndim:数组的维数(轴的数量)。
  • shape:一个元组,表示每个维度的大小。
  • size:数组中元素的总数。
  • dtype:元素的数据类型(如 int32, float64)。
arr = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
print(arr.ndim)    # 3
print(arr.shape)   # (2, 2, 2)
print(arr.size)    # 8

多维数组的索引与切片

NumPy 提供了极其灵活的索引方式,对于高维数组,每个维度可以独立切片。

基本切片

语法与 Python 列表类似,但在多个维度上使用逗号分隔。

arr = np.array([[1, 2, 3, 4],
                [5, 6, 7, 8],
                [9, 10, 11, 12]])

# 获取第一行
print(arr[0, :])    # [1 2 3 4]
# 获取第二列
print(arr[:, 1])    # [ 2  6 10]
# 获取子区域:前两行、第二到第四列
print(arr[0:2, 1:4])
# 输出:
# [[2 3 4]
#  [6 7 8]]

花式索引与布尔索引

  • 花式索引:传入整数数组或列表,可任意组合元素。
  • 布尔索引:使用与数组形状相同的布尔值数组选取元素。
arr = np.arange(10)
# 花式索引
indices = [1, 3, 5]
print(arr[indices])   # [1 3 5]

# 布尔索引
mask = arr % 2 != 0   # 选择所有奇数
print(arr[mask])       # [1 3 5 7 9]

# 多维示例
arr2d = np.array([[1, 2], [3, 4], [5, 6]])
row_mask = np.array([True, False, True])
print(arr2d[row_mask])    # 选取第1行和第3行

索引结果的内存关系

重要提示:NumPy 切片返回的是原数组的视图,而非副本。修改切片会影响原数组。如需独立副本,使用 .copy() 方法。

数组形状变换

数据预处理中经常需要调整数组的维度排列,而不改变底层数据。

reshape 与 resize

  • reshape(new_shape):返回一个新形状的数组,元素总数必须一致。
  • resize(new_shape):原地修改形状(或返回新数组,取决于使用方式),元素不够时会重复填充。
a = np.arange(12)
# 变为 3x4 矩阵
b = a.reshape(3, 4)
# 自动计算维度:-1 表示根据其他维度推断
c = a.reshape(2, -1)    # 形状为 (2, 6)

转置与轴交换

  • arr.Tnp.transpose(arr) 返回数组的转置(对于二维数组即行列互换)。
  • np.swapaxes(arr, axis1, axis2) 交换任意两个轴。
arr = np.arange(6).reshape(2, 3)
print(arr.T)   # 形状 (3, 2)

添加与删除维度

  • np.newaxisnp.expand_dims:在指定位置插入长度为 1 的新维度。
  • np.squeeze:移除长度为 1 的维度。
a = np.array([1, 2, 3])   # 形状 (3,)
# 变为列向量
col = a[:, np.newaxis]    # 形状 (3, 1)
# 变为行向量
row = a[np.newaxis, :]    # 形状 (1, 3)

广播机制:不同形状数组的运算核心

广播是 NumPy 的灵魂,它允许不同形状的数组在算术运算时自动扩展至兼容的形状,避免了显式复制大量数据的低效操作。

广播规则

两个数组进行逐元素运算时,NumPy 从尾部维度开始依次比较:

  1. 如果两个维度的尺寸相等,则认为兼容。
  2. 如果其中一个维度尺寸为 1,则会被“广播”复制以匹配另一个维度尺寸。
  3. 如果尺寸不相等且都不为 1,则引发 ValueError

简单示例详解

a = np.array([[1, 2, 3],
              [4, 5, 6]])   # 形状 (2, 3)
b = np.array([10, 20, 30])  # 形状 (3,)
c = a + b

b 的形状 (3,)a 的最后一维 3 匹配,因此 b 被广播为形状 (2,3),等价于 [[10,20,30],[10,20,30]],再逐元素相加。

高维广播

广播可以处理更复杂的组合:

  • (3, 1, 5) 与 (2, 1) 操作
    • 数组 A:(3, 1, 5)
    • 数组 B:(2, 1) → 先将 B 调整为 (1, 2, 1)(对齐尾边)
    • 比较:A 的 dim0=3 与 B 的 dim0=1 → 广播 B 的 dim0 为 3;dim1=1 与 2 → 广播 A 的 dim1 为 2;dim2=5 与 1 → 广播 B 的 dim2 为 5。
    • 结果形状:(3, 2, 5)
A = np.arange(15).reshape(3, 1, 5)
B = np.arange(2).reshape(2, 1)
result = A + B
print(result.shape)  # (3, 2, 5)

广播的实际应用:数据归一化与统计

广播常用于对数组沿某个轴进行操作并保持原有形状,例如均值归一化。

data = np.random.randn(3, 4)      # 形状 (3, 4) 的随机数据
mean = data.mean(axis=1)          # 计算每行均值,形状 (3,)
# 直接相减会出错吗?mean 形状 (3,),而 data 形状 (3,4),根据广播规则需要增加维度
mean_2d = mean[:, np.newaxis]     # 变为 (3, 1)
normalized = data - mean_2d       # 广播 mean_2d 到 (3,4) 完成逐元素相减

或者利用 keepdims 参数:

mean = data.mean(axis=1, keepdims=True)  # 形状 (3, 1)
normalized = data - mean                 # 直接广播

沿轴计算与聚合

理解 axis 参数是掌握多维数组运算的关键。axis=0 表示沿行操作(逐列聚合),axis=1 表示沿列操作(逐行聚合)。

arr = np.array([[1, 2, 3],
                [4, 5, 6]])
print(np.sum(arr, axis=0))   # [5 7 9] 列和
print(np.sum(arr, axis=1))   # [ 6 15] 行和
print(np.max(arr, axis=0))   # [4 5 6] 每列最大值

常用聚合函数:sum, mean, std, min, max, argmin, argmax, cumsum

实践案例:图像处理中的广播

假设有一张 RGB 图像,形状为 (height, width, 3),每个像素的颜色值在 0 到 255 之间。我们想对每个颜色通道分别乘以不同的权重(如增强红色通道、减弱蓝色通道)。

# 模拟图像:高度 4,宽度 4,3 通道
image = np.random.randint(0, 256, size=(4, 4, 3)).astype(np.float32)
# 权重数组,形状为 (3,):分别对应 R, G, B
weights = np.array([1.2, 1.0, 0.8])
# 广播乘法:权重自动扩展为 (1, 1, 3) 再广播到 (4, 4, 3)
adjusted = image * weights
# 将值裁剪到 0-255 范围并转回 uint8
adjusted = np.clip(adjusted, 0, 255).astype(np.uint8)

该操作无需任何循环,简洁高效。

总结与最佳实践

  • 利用形状操作替代循环reshapenewaxisswapaxes 可大幅提升代码可读性和性能。
  • 掌握广播规则:遇到形状不匹配错误时,从尾部对齐检查维度,必要时使用 np.newaxis 显式插入维度。
  • 优先使用向量化:NumPy 的数学算子(+, -, *, /, **)和通用函数(np.sin, np.exp 等)天然支持广播,应尽量避免 Python 级别的迭代。
  • 注意内存视图:切片返回视图,赋值需谨慎。创建副本用 .copy()

通过不断练习多维数组索引、形状变换和广播,你将能够游刃有余地处理大规模数值计算任务,为后续学习 SciPy、Pandas 等高级库打下坚实基础。