3、Numpy基础与数组操作:Numpy数组创建与属性、数组索引与切片、数组形状变换与广播机制、通用函数与向量化计算
好,我们进入第三章。说实话,Numpy 是 Python 数据科学生态里的基石。你后面所有的数据处理、特征工程、模型训练,底层几乎都在跟 Numpy 数组打交道。我个人习惯把 Numpy 比作「数据科学的乐高积木」—— 基础但万能。这一章,我们就把它彻底吃透。
3.1 Numpy数组创建与属性
先聊聊怎么创建数组。最直接的方式,就是从 Python 列表转换过来。
import numpy as np
# 从列表创建
arr = np.array([1, 2, 3, 4, 5])
print(arr) # [1 2 3 4 5]
# 二维数组
arr2d = np.array([[1, 2, 3], [4, 5, 6]])
print(arr2d)
嗯,这里要注意:Numpy 数组要求所有元素类型一致。如果你混入字符串,整个数组都会变成字符串类型。我在项目中遇到过这种坑,数据读进来后做数值计算直接报错,排查了半天才发现是某列混了个空字符串。
除了手动创建,Numpy 还提供了一些非常实用的工厂函数:
| 函数 | 说明 | 示例 |
|---|---|---|
np.zeros() |
创建全0数组 | np.zeros((3,4)) |
np.ones() |
创建全1数组 | np.ones((2,3)) |
np.arange() |
类似 range,生成等差数列 | np.arange(0, 10, 2) |
np.linspace() |
生成指定数量的等间隔数 | np.linspace(0, 1, 5) |
np.random.randn() |
标准正态分布随机数 | np.random.randn(3,3) |
np.random.randn() 或 np.random.uniform()。千万别用 np.zeros() 初始化权重,否则所有神经元输出相同,梯度也相同,模型根本学不动。
数组创建好后,有几个核心属性你必须要记住:
ndim:维度数(1维、2维、3维...)shape:形状元组,比如 (3,4) 表示3行4列dtype:数据类型,比如 float64、int32size:元素总数
arr = np.array([[1, 2, 3], [4, 5, 6]])
print(arr.ndim) # 2
print(arr.shape) # (2, 3)
print(arr.dtype) # int64
print(arr.size) # 6
3.2 数组索引与切片
索引和切片,说白了就是怎么从数组里「掏东西」。一维数组的索引和 Python 列表完全一样,但多维数组就有点意思了。
arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
# 取第2行第3列的元素
print(arr2d[1, 2]) # 6
# 取第1行所有列
print(arr2d[0, :]) # [1 2 3]
# 取所有行第2列
print(arr2d[:, 1]) # [2 5 8]
你想想看,这种用逗号分隔的索引方式,是不是比 Python 列表的 arr2d[1][2] 优雅多了?而且性能也更好,因为 Numpy 底层是 C 实现的,一次索引到位。
.copy()。
sub = arr2d[0:2, 0:2] # 视图
sub[0, 0] = 999
print(arr2d[0, 0]) # 999,原数组也被改了!
# 正确做法
sub_copy = arr2d[0:2, 0:2].copy()
sub_copy[0, 0] = 888
print(arr2d[0, 0]) # 还是 999,原数组不受影响
布尔索引也是个很实用的技巧。比如你想筛选出数组中所有大于5的元素:
arr = np.array([1, 4, 7, 2, 9, 3])
mask = arr > 5
print(arr[mask]) # [7 9]
这种写法在数据清洗时特别常用。我处理缺失值时,经常用 np.isnan() 生成布尔掩码,然后批量替换或删除。
3.3 数组形状变换与广播机制
形状变换,就是改变数组的维度结构。最常用的两个函数是 reshape 和 resize。
arr = np.arange(12) # [0 1 2 ... 11]
# reshape 成 3行4列
arr_3x4 = arr.reshape(3, 4)
print(arr_3x4)
# 注意:reshape 返回新数组,不修改原数组
# 如果想原地修改,用 resize
arr.resize(4, 3)
print(arr)
这里有个小细节:reshape 要求新形状的元素总数必须与原数组一致。你可以在某个维度上写 -1,Numpy 会自动计算该维度的大小。
arr = np.arange(12)
print(arr.reshape(3, -1)) # 自动算出来是 4 列
print(arr.reshape(-1, 6)) # 自动算出来是 2 行
接下来聊聊广播机制。这是 Numpy 最强大的特性之一,也是很多新手容易懵的地方。
广播的核心思想是:当两个数组形状不同时,Numpy 会尝试「拉伸」较小的数组,使其与较大数组兼容,然后进行逐元素运算。
# 最简单的例子:数组 + 标量
arr = np.array([1, 2, 3])
print(arr + 10) # [11 12 13] —— 10 被广播成了 [10 10 10]
# 二维 + 一维
arr2d = np.array([[1, 2, 3], [4, 5, 6]])
row = np.array([10, 20, 30])
print(arr2d + row)
# [[11 22 33]
# [14 25 36]]
# row 被广播成了 [[10 20 30], [10 20 30]]
为什么会这样?广播遵循两条规则:
- 如果两个数组的维度数不同,小维度数组的形状会在前面补1
- 如果某个维度上大小不同,但其中一个为1,则可以广播
我在做图像数据预处理时经常用到广播。比如给一批图片(形状为 (N, H, W, C))减去均值,均值向量形状是 (C,),广播机制会自动完成逐通道的减法,代码写起来非常简洁。
3.4 通用函数与向量化计算
通用函数,也叫 ufunc,是 Numpy 对数组进行逐元素运算的函数。说白了,就是让你不用写循环,直接对整个数组做数学操作。
arr = np.array([1, 2, 3, 4, 5])
# 数学运算
print(np.sqrt(arr)) # 平方根
print(np.exp(arr)) # 指数
print(np.log(arr)) # 自然对数
print(np.sin(arr)) # 正弦
# 比较运算
print(np.greater(arr, 3)) # [False False False True True]
你想想看,如果用 Python 原生循环写这些操作,不仅代码冗长,性能还差得远。Numpy 的 ufunc 底层是用 C 实现的,向量化计算能利用 CPU 的 SIMD 指令集,速度比 Python 循环快几十倍甚至上百倍。
常用的通用函数可以分为几类:
| 类别 | 函数举例 | 说明 |
|---|---|---|
| 算术运算 | np.add, np.subtract, np.multiply, np.divide |
对应 + - * / |
| 三角函数 | np.sin, np.cos, np.tan |
角度用弧度制 |
| 指数对数 | np.exp, np.log, np.log10 |
自然底数和10为底 |
| 统计函数 | np.sum, np.mean, np.std, np.max, np.min |
支持 axis 参数指定维度 |
| 逻辑运算 | np.logical_and, np.logical_or, np.logical_not |
用于布尔数组 |
这里特别提一下 axis 参数。很多新手搞不清 axis=0 和 axis=1 的区别。我的记忆方法是:axis=0 是「跨行操作」,即沿着行的方向压缩;axis=1 是「跨列操作」,即沿着列的方向压缩。
arr2d = np.array([[1, 2, 3], [4, 5, 6]])
print(np.sum(arr2d, axis=0)) # [5 7 9] 每一列求和
print(np.sum(arr2d, axis=1)) # [6 15] 每一行求和
print(np.sum(arr2d)) # 21 全部元素求和
嗯,到这里 Numpy 的核心内容就差不多了。你可能会问:学这些够了吗?说实话,对于构建数据集和训练模型来说,这些基础操作已经覆盖了 90% 以上的场景。剩下的 10%,等你真正遇到具体问题时,查文档也来得及。
下一章我们会把这些操作串起来,用 Numpy 实现一个完整的数据预处理流程。到时候你会发现,今天学的每一个知识点都会派上用场。