2
NumPy:数值计算地基
NumPy Arrays & Broadcasting
为什么不用 Python 列表?因为慢。NumPy 把数据放在一块连续的内存里,底层是 C,你写一行 Python,它在底下跑一圈 C。处理十万行数据,Python 列表要几秒,NumPy 几毫秒。Pandas、PyTorch 全建立在 NumPy 之上,所以这关绕不过去。
ndarray:多维数组
import numpy as np
# 创建数组
a = np.array([1, 2, 3, 4])
print(a) # [1 2 3 4]
print(a.shape) # (4,) 形状
print(a.dtype) # int64 数据类型
print(a.ndim) # 1 几维
# 批量创建:zeros / ones / arange / linspace / random
print(np.zeros((2, 3))) # 2行3列全0
print(np.arange(0, 10, 2)) # [0 2 4 6 8] 从0到10步长2
print(np.linspace(0, 1, 5)) # [0. 0.25 0.5 0.75 1. ] 5个等分点
print(np.random.rand(3)) # [0.xxxx 0.xxxx 0.xxxx] 0~1均匀分布
# 二维数组(矩阵)
m = np.array([[1, 2], [3, 4]])
print(m.shape) # (2, 2)
索引、切片、布尔索引
a = np.array([10, 20, 30, 40, 50])
# 普通索引
print(a[0]) # 10
print(a[1:3]) # [20 30]
# 布尔索引:用条件当"筛子"
print(a > 25) # [False False True True True]
print(a[a > 25]) # [30 40 50] 只要大于25的
# 花式索引:传一个索引数组
print(a[[0, 2, 4]]) # [10 30 50]
逐元素运算与广播
论广播(broadcasting)是什么
两个形状不同的数组相加,NumPy 会自动把小的那个"扩展"到大的形状——这就是广播。比如 (3,3) 的矩阵加一个 (3,) 的向量,向量会被"复制"三行。规则:从右往左对齐,维度为 1 的那一维会被自动扩展。这是 NumPy 最优雅的设计,也是最容易踩坑的地方。
# 逐元素运算:不用写循环
a = np.array([1, 2, 3])
b = np.array([10, 20, 30])
print(a + b) # [11 22 33]
print(a * 2) # [2 4 6] 标量广播
print(a * b) # [10 40 90] 逐元素乘(不是矩阵乘!)
# 矩阵乘法:@ 或 np.dot
m1 = np.array([[1, 2], [3, 4]])
m2 = np.array([[5, 6], [7, 8]])
print(m1 @ m2)
# [[19 22]
# [43 50]]
# 常用统计函数
x = np.array([1, 2, 3, 4, 5])
print(x.sum(), x.mean(), x.max(), x.min(), x.std())
# 15 3.0 5 1 1.414...
print(x.argmax()) # 4 最大值的下标
# 重塑 reshape、转置 T、拼接 concatenate
y = np.arange(12) # [0..11]
z = y.reshape(3, 4) # 变成3行4列
print(z.T) # 转置成4行3列
性能对比:NumPy vs Python 循环
import time
size = 1_000_000
py_list = list(range(size))
np_arr = np.arange(size)
start = time.time()
py_result = [x * 2 for x in py_list]
print("Python 循环:", time.time() - start)
start = time.time()
np_result = np_arr * 2
print("NumPy 向量化:", time.time() - start)
# Python 循环:约 0.05 秒
# NumPy 向量化:约 0.002 秒 快 25 倍
NumPy 进阶:线性代数与随机数
# 线性代数
v = np.array([3, 4])
print(np.linalg.norm(v)) # 5.0 向量长度
A = np.array([[1,2], [3,4]])
print(np.linalg.inv(A)) # 矩阵求逆
print(np.linalg.eig(A)) # 特征值特征向量
# 沿轴操作:axis=0 是跨行(每列),axis=1 是跨列(每行)
m = np.array([[1, 2], [3, 4]])
print(m.sum(axis=0)) # [4 6] 每列求和
print(m.sum(axis=1)) # [3 7] 每行求和
# 随机数(新版 API,推荐)
rng = np.random.default_rng(42)
print(rng.integers(0, 10, size=5)) # 5个0-9随机整数
print(rng.normal(0, 1, 5)) # 标准正态分布采样
# 排序与唯一值
x = np.array([3, 1, 2, 3, 1])
print(np.sort(x)) # [1 1 2 3 3]
print(np.unique(x)) # [1 2 3]
广播机制:完整案例
论广播三规则
① 维度少的那一方,前面补 1 直到维度对齐;② 某一维上两边要么相等,要么其中一边是 1,否则报错;③ 结果形状取每维的最大值。一句话:"从尾巴对齐,1 可以被拉长成任何值"。
# 案例1:标量广播 —— 标量对数组的每个元素
print(np.array([1, 2, 3]) * 10)
# [10 20 30]
# 案例2:向量(3,) 加到矩阵(2,3) 的每一行
mat = np.array([[1, 2, 3],
[4, 5, 6]])
vec = np.array([10, 20, 30])
print(mat + vec)
# [[11 22 33]
# [14 25 36]]
# 案例3:列向量(2,1) 加行向量(1,3) —— 生成(2,3) 矩阵
col = np.array([[1], [2]]) # shape (2,1)
row = np.array([[10, 20, 30]]) # shape (1,3)
print(col + row)
# [[11 21 31]
# [12 22 32]]
# 案例4:(3,) + (3,1) = (3,3) —— 最经典的"形状意外"
a = np.array([0, 1, 2]) # (3,)
b = np.array([[100], [200], [300]]) # (3,1)
print(a + b)
# [[100 101 102]
# [200 201 202]
# [300 301 302]]
# 案例5:常见错误 —— (3,) 和 (4,) 不能广播
# np.array([1,2,3]) + np.array([1,2,3,4])
# ValueError: operands could not be broadcast together with shapes (3,) (4,)
高级索引:布尔索引与花式索引
# 布尔索引:用一个布尔数组当"筛子",返回满足条件的元素(拷贝)
data = np.array([10, -20, 30, -40, 50])
mask = data > 0
print(mask) # [ True False True False True]
print(data[mask]) # [10 30 50]
# 把所有负数替换成 0(非常常用)
data[data < 0] = 0
print(data) # [10 0 30 0 50]
# 多条件组合:&(与)、|(或)、~(非),每个条件要加括号
arr = np.arange(10)
print(arr[(arr >= 3) & (arr <= 7)])
# [3 4 5 6 7]
# 花式索引(fancy indexing):传一个整数数组选行/列
X = np.array([[1, 2],
[3, 4],
[5, 6],
[7, 8]])
print(X[[0, 2, 3]]) # 取第0、2、3行
# [[1 2]
# [5 6]
# [7 8]]
print(X[[0, 1], [ 0, 1]]) # 取 (0,0) 和 (1,1)
# [1 4]
# np.where:条件表达式,"三元运算符"的向量化版
scores = np.array([95, 55, 78, 40])
result = np.where(scores >= 60, "及格", "不及格")
print(result)
# ['及格' '不及格' '及格' '不及格']
视图 vs 拷贝:面试必考
切片(slice)是视图,花式索引和布尔索引是拷贝。视图共享内存,改视图会改原数组;拷贝是独立的。例:X = np.arange(10); y = X[1:4]; y[0] = 999 会把 X[1] 也改成 999;但 z = X[[1,2,3]] 改 z 不影响 X。想要显式拷贝用 .copy()。
线性代数子库 np.linalg
import numpy as np
A = np.array([[4, 2],
[1, 3]], dtype=np.float64)
# 1. 矩阵乘法(@ 或 np.dot)
B = np.array([[1, 0], [0, 1]])
print(A @ B)
# [[4. 2.]
# [1. 3.]]
# 2. 转置
print(A.T)
# [[4. 1.]
# [2. 3.]]
# 3. 逆矩阵(存在的条件:行列式非 0)
A_inv = np.linalg.inv(A)
print(np.round(A @ A_inv, 10)) # 应等于单位阵
# [[1. 0.]
# [0. 1.]]
# 4. 行列式
print(np.linalg.det(A)) # 10.0
# 5. 特征值与特征向量
eigvals, eigvecs = np.linalg.eig(A)
print(eigvals) # [5. 2.] 特征值
print(np.round(eigvecs, 3)) # 每列是对应特征向量
# 6. SVD 奇异值分解(PCA、推荐系统、压缩都用它)
U, S, Vt = np.linalg.svd(A)
print(S) # 奇异值(降序)
# 7. 解线性方程组 Ax = b
b = np.array([1, 2])
x = np.linalg.solve(A, b)
print(x) # [ 0.2 -0.6] 即 x=(A^-1)b
# 8. 范数:L2 范数(向量长度)、L1 范数(绝对值和)
v = np.array([3, 4])
print(np.linalg.norm(v)) # 5.0
print(np.linalg.norm(v, ord=1)) # 7.0
NumPy 章节面试题
面试 · NumPy
Q1. NumPy 里视图(view)和拷贝(copy)的区别?
查看答案
切片操作(如 arr[1:3])返回视图,共享底层内存,改视图会改原数组;花式索引 arr[[1,2,3]] 和布尔索引返回拷贝,改副本不影响原数组。需要独立副本时用 .copy()。
Q2. 广播机制的规则是什么?
查看答案
从右边开始逐维对齐;某一维上两边要么相等,要么其中一边长度为 1(自动扩展),否则报错。维度不同时把少的那个前面补 1。
Q3. * 和 @ 在 NumPy 里有什么区别?
查看答案
* 是逐元素乘(Hadamard 积);@ / np.dot / np.matmul 是矩阵乘法。搞混是经典 bug。
Q4. 为什么 arr.sum(axis=0) 是对列求和?
查看答案
axis=0 表示"沿着第 0 维走",也就是把第 0 维压缩掉,剩下的就是每一列的和;axis=1 同理压缩第 1 维得到每行和。记法:"axis 是被吃掉的那个维度"。
Q5. 怎么找数组里最大值的下标?
查看答案
arr.argmax() 返回展平后最大值的下标;多维时 np.unravel_index(arr.argmax(), arr.shape) 可以还原成多维坐标。