NumPy 数值计算:多维数组与广播机制
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.T或np.transpose(arr)返回数组的转置(对于二维数组即行列互换)。np.swapaxes(arr, axis1, axis2)交换任意两个轴。
arr = np.arange(6).reshape(2, 3)
print(arr.T) # 形状 (3, 2)
添加与删除维度
np.newaxis或np.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,则会被“广播”复制以匹配另一个维度尺寸。
- 如果尺寸不相等且都不为 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:
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)
该操作无需任何循环,简洁高效。
总结与最佳实践
- 利用形状操作替代循环:
reshape、newaxis和swapaxes可大幅提升代码可读性和性能。 - 掌握广播规则:遇到形状不匹配错误时,从尾部对齐检查维度,必要时使用
np.newaxis显式插入维度。 - 优先使用向量化:NumPy 的数学算子(
+,-,*,/,**)和通用函数(np.sin,np.exp等)天然支持广播,应尽量避免 Python 级别的迭代。 - 注意内存视图:切片返回视图,赋值需谨慎。创建副本用
.copy()。
通过不断练习多维数组索引、形状变换和广播,你将能够游刃有余地处理大规模数值计算任务,为后续学习 SciPy、Pandas 等高级库打下坚实基础。