四、NumPy 基础与数组操作
好,咱们进入正题。NumPy 是什么?说白了,它就是 Python 科学计算的“心脏”。你后面做特征协同优化,所有数据都得装进 NumPy 数组里才能高效跑起来。我个人习惯是,拿到原始数据的第一件事,就是用 np.array() 把它框起来。这玩意儿比 Python 原生的列表快几十倍不止。
4.1 创建数组:你得知道的几种姿势
创建数组方法很多,但真正常用的就那几种。我挑干活最顺手的几个说说。
import numpy as np
# 从列表创建
arr1 = np.array([1, 2, 3, 4])
# 全零数组,指定类型
arr2 = np.zeros((3, 4), dtype=np.float64)
# 全一数组
arr3 = np.ones((2, 3))
# 等差数列
arr4 = np.arange(0, 10, 2)
# 等间隔采样
arr5 = np.linspace(0, 1, 5)
# 随机数组
arr6 = np.random.randn(3, 3)
嗯,这里要注意 arange 和 linspace 的区别。前者给步长,后者给个数。我在做特征归一化时经常用 linspace 来生成均匀的阈值候选,省去手动计算的麻烦。
dtype 显式声明类型。默认 float64 没问题,但如果你要处理海量数据,换成 float32 能节省一半内存。我曾经在一个内存受限的嵌入式部署中,就靠这招救回来的。
4.2 索引与切片:别小看基本功
索引切片是操作数组最频繁的动作。你想想看,后面要从原始特征里抽子集、改局部值,全得靠它。
arr = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
# 取第2行第3列(注意索引从0开始)
print(arr[1, 2]) # 输出 7
# 切片:前两行,第二到第四列
print(arr[:2, 1:4])
# 花式索引:取第0行和第2行的第1、3列
print(arr[[0, 2], [1, 3]])
为什么会强调这个?因为切片返回的是视图,不是副本。你改了切片,原数组也跟着变。我记得有次调试了好久才发现,以为是副本操作结果原数据被污染了。如果你真想复制一份,用 .copy()。
# 浅拷贝 vs 深拷贝
slice_view = arr[0, :]
slice_copy = arr[0, :].copy()
slice_view[0] = 999 # arr 也会被改
slice_copy[0] = 0 # arr 不受影响
4.3 广播机制:让形状不同的数组也能一起玩
广播机制是 NumPy 最核心也最容易踩坑的地方。说白了,它让不同形状的数组做运算时,自动把小数组“扩展”成大数组的尺寸。
a = np.array([[1, 2, 3],
[4, 5, 6]])
b = np.array([10, 20, 30])
# 形状:(2,3) 和 (3,) —— 能广播吗?
c = a + b
print(c)
能。广播规则是:从最后一个维度开始比较,要么相等,要么一个是1,要么缺失。你想想看 b 的形状是 (3,),它会被视为 (1,3),然后沿着行方向复制成 (2,3)。
| 场景 | 形状 A | 形状 B | 能否广播 | 说明 |
|---|---|---|---|---|
| 标量 + 数组 | (3, 4) | () | ✅ | 标量自动扩展 |
| 行向量 + 列向量 | (1, 4) | (3, 1) | ✅ | 两个方向同时扩展 |
| 维度不一致 | (3, 4, 5) | (4, 5) | ✅ | B从末尾对齐 |
| 维度长度不匹配 | (3, 4) | (3, ) | ❌ | 最后一个维度4≠3,且没有1 |
4.4 线性代数运算:优化实战的硬核基础
做参数优化与特征协同,说白了离不开矩阵运算。NumPy 提供了 np.linalg 模块,我常用的操作罗列给你。
import numpy as np
A = np.array([[1, 2],
[3, 4]])
B = np.array([[5, 6],
[7, 8]])
# 矩阵乘法
C = np.dot(A, B)
# 矩阵转置
A_T = A.T
# 矩阵求逆
A_inv = np.linalg.inv(A)
# 特征值与特征向量
eigvals, eigvecs = np.linalg.eig(A)
# 解线性方程组 Ax = b
b = np.array([9, 10])
x = np.linalg.solve(A, b)
我打个比方,在你做特征相关性分析时,协方差矩阵的计算本质就是矩阵乘法与转置的组合。而 np.linalg.solve 比直接求逆再乘更稳定、更快——这是底层调用了 LAPACK 库的原因。
np.dot 或 @ 运算符。千万别用 *,那是逐元素乘法。二者差之一念,错之千里。
还有一个我常用的操作是矩阵的 reshape 与 flatten。特征协同优化中经常需要把多维特征展平为向量。
M = np.arange(12).reshape(3, 4)
flat = M.flatten() # 返回副本
ravel = M.ravel() # 返回视图(修改会影响原数组)
ravel 展平后直接修改了原始矩阵,导致后续复用原始矩阵时数据全乱了。后来我改成了 flatten。如果你不确定要不要改原数据,就乖乖用 flatten。
好了,NumPy 基础就聊到这。这几个操作是后面所有优化算法的地基,你得熟练掌握。下面我为你归纳了一张本章的核心知识图谱,方便你快速回顾。