CHAPTER 07

NumPy:科学计算基础

AI 的血液——矩阵和向量运算。神经网络的每一次计算都依赖 NumPy 的思想,PyTorch 的 Tensor 就是 GPU 加速版的 NumPy 数组。

NumPy 核心术语

  • ndarray(N 维数组) NumPy 的核心数据结构,存储同类型元素的多维数组。与 Python list 相比,ndarray 连续存储在内存中,支持 CPU SIMD 指令级向量化运算,速度快 100 倍以上。
  • dtype(数据类型) ndarray 中所有元素的类型,如 float32、float64、int32。AI 训练通常使用 float32(32位浮点数),在精度和显存之间取得平衡。float16 或 bfloat16 用于混合精度训练,节省显存。
  • shape(形状) 数组各维度的大小,用 tuple 表示。(32, 3, 224, 224) 表示 32 张 RGB 224×224 图像(批次×通道×高×宽)。理解 shape 是读懂 AI 代码的关键。
  • 轴(Axis) 数组的某个维度。axis=0 通常是批次维,axis=1 是通道或特征维。arr.sum(axis=0) 沿 axis=0 求和(跨样本求和),arr.sum(axis=1) 沿 axis=1 求和(跨特征求和)。
  • 广播(Broadcasting) 不同形状的数组进行运算时,NumPy 自动将较小的数组"扩展"以匹配较大数组的形状。规则:从右往左比较各维度大小,维度为 1 的可以扩展,维度为 n 的必须与另一数组相同。
  • 视图(View)vs 副本(Copy) 切片操作 a[1:3] 返回视图,修改视图会影响原数组;a[1:3].copy() 返回真正的副本。不理解这个区别会导致难以追踪的 bug。

ndarray —— NumPy 的核心

NumPy 最重要的对象是 ndarray(N 维数组),它是一个同类型元素构成的多维数组。与 Python 列表不同,ndarray 中所有元素类型相同,运算速度快 100 倍以上,因为底层用 C 实现了向量化运算。

核心概念

向量化(Vectorization)

NumPy 的运算是对整个数组一次性操作,而不是 Python 的逐元素循环。a + b(两个 1000 元素的数组相加)在 NumPy 中比 Python for 循环快约 100 倍。这就是为什么 AI 计算必须用 NumPy/Tensor。

PYTHON · 创建 ndarray
import numpy as np

# 从列表创建
v = np.array([1, 2, 3, 4])           # 1D:向量
m = np.array([[1,2],[3,4]])           # 2D:矩阵
t = np.array([[[1,2],[3,4]],          # 3D:张量(图像常见)
               [[5,6],[7,8]]])

print(v.shape)   # (4,)    → 4 个元素的向量
print(m.shape)   # (2, 2)  → 2x2 矩阵
print(t.shape)   # (2, 2, 2) → 三维张量
print(v.dtype)   # int64
print(v.ndim)    # 1(几维)

# 特殊创建方式
np.zeros((3, 4))       # 3x4 全0矩阵
np.ones((2, 3))        # 2x3 全1矩阵
np.eye(4)              # 4x4 单位矩阵(对角线为1)
np.random.rand(3, 4)  # [0,1) 随机矩阵,常用于初始化权重
np.random.randn(3, 4) # 标准正态分布随机矩阵
np.arange(0, 10, 2)   # [0, 2, 4, 6, 8]
np.linspace(0, 1, 5)  # [0, 0.25, 0.5, 0.75, 1.0](等间距)
💡

AI 中的数据形状

AI 中处处是矩阵:一批 32 张 28×28 灰度图像 → shape (32, 28, 28);一批 16 张 224×224 彩色图像 → shape (16, 3, 224, 224)(批量、通道、高、宽)。理解 shape 是读懂 AI 代码的关键。

索引与切片

NumPy 的索引比 Python 列表更强大,支持多维索引、布尔索引和花式索引,这在数据筛选中非常常用。

PYTHON · 索引与切片
a = np.array([[1, 2, 3],
               [4, 5, 6],
               [7, 8, 9]])

# 基本索引
print(a[0, 1])    # 2 → 第0行第1列
print(a[1])      # [4, 5, 6] → 整行
print(a[:, 0])   # [1, 4, 7] → 整列

# 切片
print(a[:2, 1:])  # [[2,3],[5,6]] → 前两行、后两列

# 布尔索引(AI 中超常用!)
scores = np.array([0.9, 0.4, 0.8, 0.2, 0.95])
mask   = scores > 0.7          # [True, False, True, False, True]
print(scores[mask])              # [0.9, 0.8, 0.95]

# 直接用条件筛选(更简洁)
print(scores[scores > 0.7])     # [0.9, 0.8, 0.95]

# 花式索引:用下标数组取行
data = np.random.rand(10, 5)    # 10 个样本,每个 5 维
indices = [0, 3, 7]              # 取第 0、3、7 个样本
batch = data[indices]            # shape: (3, 5)

数组运算与广播

NumPy 的运算都是逐元素的。两个形状相同的数组直接相加,对应位置相加。神奇的地方在于广播(Broadcasting)机制:形状不完全相同的数组也能运算,NumPy 会自动"扩展"较小的数组。

PYTHON · 运算与广播
a = np.array([1, 2, 3, 4])
b = np.array([10, 20, 30, 40])

# 逐元素运算
print(a + b)   # [11, 22, 33, 44]
print(a * b)   # [10, 40, 90, 160]
print(a ** 2)  # [1, 4, 9, 16]

# 广播:标量自动扩展
print(a * 2)   # [2, 4, 6, 8]

# 广播:向量与矩阵
m   = np.array([[1,2,3],[4,5,6]])  # shape (2, 3)
row = np.array([10, 20, 30])        # shape (3,)
print(m + row)
# [[11, 22, 33],   ← 每行都加了 [10, 20, 30]
#  [14, 25, 36]]

# AI 场景:批量归一化(减去均值,除以标准差)
batch  = np.random.rand(32, 128)  # 32 个样本,128 维特征
mean   = batch.mean(axis=0)        # 每个特征的均值,shape (128,)
std    = batch.std(axis=0)         # 每个特征的标准差
normed = (batch - mean) / (std + 1e-8)  # 广播:shape (32,128) op (128,)
print(normed.shape)  # (32, 128)

线性代数:矩阵运算

神经网络的核心运算就是矩阵乘法:输入 × 权重矩阵 + 偏置。NumPy 提供了高效的矩阵运算,理解这些是理解深度学习的基础。

PYTHON · 矩阵运算
# 矩阵乘法(@ 运算符,等同于 np.matmul)
X = np.array([[1, 2], [3, 4]])  # (2, 2)
W = np.array([[0.5, 0], [0, 0.5]])  # (2, 2)
print(X @ W)
# [[0.5, 1.0],
#  [1.5, 2.0]]

# 模拟神经网络一层的前向传播
batch_size = 4
input_dim  = 3
output_dim = 2

x   = np.random.randn(batch_size, input_dim)   # (4, 3) 输入
W   = np.random.randn(input_dim, output_dim)   # (3, 2) 权重
b   = np.zeros(output_dim)                       # (2,)   偏置

y   = x @ W + b                                  # (4, 2) 输出
print(f"输入: {x.shape}, 权重: {W.shape}, 输出: {y.shape}")

# 常用线性代数函数
np.linalg.norm(x)         # 向量/矩阵的范数
np.linalg.det(X)           # 行列式
np.linalg.inv(X)           # 矩阵求逆
np.linalg.eig(X)           # 特征值分解(PCA 的基础)
X.T                        # 转置

统计与常用函数

PYTHON · 统计函数与 reshape
data = np.array([[1, 2, 3], [4, 5, 6]])  # shape (2, 3)

# 统计函数
print(data.sum())             # 21(所有元素之和)
print(data.sum(axis=0))      # [5, 7, 9] → 按列求和
print(data.sum(axis=1))      # [6, 15]   → 按行求和
print(data.mean())            # 3.5
print(data.max(), data.min())# 6  1
print(data.argmax())          # 5 → 最大值在扁平化后的第5个位置
print(data.argmax(axis=1))   # [2, 2] → 每行最大值的列索引

# reshape:改变形状(不改变数据)
flat = np.arange(12)          # [0,1,2,...,11]  shape (12,)
mat  = flat.reshape(3, 4)    # shape (3, 4)
cube = flat.reshape(2, 2, 3) # shape (2, 2, 3)
back = mat.reshape(-1)        # -1 自动计算,等于 flatten

# AI 中常见:展平图像送入全连接层
images   = np.random.rand(32, 28, 28)    # 32张 28×28 图像
flattened = images.reshape(32, -1)       # (32, 784)
print(flattened.shape)  # (32, 784)
🤖

NumPy 与 PyTorch Tensor 的关系

PyTorch 的 torch.Tensor 和 NumPy 的 ndarray 接口几乎完全相同:tensor.shapetensor.reshape()tensor.mean() 都和 NumPy 一样。区别是 Tensor 可以在 GPU 上运算,并且支持自动微分。学好 NumPy,PyTorch 就学了一半。

常见误区与边界情况

⚠️

视图修改陷阱

NumPy 切片返回视图而非副本!b = a[1:3] 后修改 b[0] = 99 会同时修改 a[1]。需要独立副本时用 b = a[1:3].copy()。PyTorch 中同理,.detach() 返回视图,.detach().clone() 返回副本。

⚠️

广播规则的常见误解

广播不是按列对齐,而是从右到左对齐维度。(3,)(2,3) 可以广播(3对3),但 (3,)(3,2) 则不行(3对2不匹配)。遇到形状不匹配的报错,先打印两个数组的 shape 来排查。

⚠️

dtype 不匹配导致精度损失

NumPy 运算时,如果两个数组 dtype 不同,会自动提升为更高精度的类型(如 float32 + float64 → float64)。但在 PyTorch 中,float32 + float64 会报错。统一使用 np.float32np.float64,不要混用。AI 训练模型权重通常用 float32 以平衡性能和精度。

PYTHON · NumPy 性能优化技巧
import numpy as np

# 1. 指定 dtype 减少内存(AI 常用 float32)
weights = np.random.randn(1000, 1000).astype(np.float32)
print(f"float64: {weights.astype(np.float64).nbytes / 1e6:.1f} MB")
print(f"float32: {weights.nbytes / 1e6:.1f} MB")    # 节省一半内存

# 2. 避免不必要的拷贝
a = np.ones((1000, 1000))
b = a[100:200, :]   # 视图,无内存拷贝
c = a[100:200, :].copy()  # 副本,需要内存

# 3. 用 np.einsum 做复杂矩阵运算(Attention 机制)
Q = np.random.randn(8, 64)   # 8个 query,64维
K = np.random.randn(8, 64)   # 8个 key,64维
# Attention score:Q @ K.T / sqrt(d_k)
scores = np.einsum('ij,kj->ik', Q, K) / np.sqrt(64)
print(scores.shape)  # (8, 8) - 8x8 的注意力矩阵

# 4. 利用 np.where 做条件选择(比 if 快)
x = np.array([-2, -1, 0, 1, 2])
# 手动实现 ReLU
relu_x = np.where(x > 0, x, 0)
print(relu_x)  # [0, 0, 0, 1, 2]

综合示例:手写 Softmax 和损失函数

用 NumPy 实现深度学习中的核心运算,感受 NumPy 在 AI 中的真正用法:

PYTHON · Softmax 与交叉熵损失
import numpy as np

def softmax(x):
    """将原始分数转换为概率分布"""
    # 减去最大值防止数值溢出(数学上等价)
    e_x = np.exp(x - x.max(axis=-1, keepdims=True))
    return e_x / e_x.sum(axis=-1, keepdims=True)

def cross_entropy_loss(probs, labels):
    """交叉熵损失:衡量预测概率与真实标签的差距"""
    batch_size = probs.shape[0]
    # 取每个样本在正确类别上的概率
    correct_probs = probs[np.arange(batch_size), labels]
    # 负对数似然
    loss = -np.log(correct_probs + 1e-8).mean()
    return loss

# 模拟:4 个样本,分 3 类
logits = np.array([
    [2.0, 1.0, 0.5],  # 样本0,真实类别是0
    [0.1, 3.0, 0.3],  # 样本1,真实类别是1
    [0.5, 0.2, 2.8],  # 样本2,真实类别是2
    [1.5, 0.8, 0.3],  # 样本3,真实类别是0
])
labels = np.array([0, 1, 2, 0])

probs = softmax(logits)
loss  = cross_entropy_loss(probs, labels)

print("概率分布:\n", probs.round(3))
print(f"交叉熵损失: {loss:.4f}")
# 预测类别
predicted = probs.argmax(axis=1)
print(f"预测: {predicted}, 真实: {labels}")
💡

本章小结

NumPy 是 AI 计算的基础库,核心是 ndarray(N维数组)。关键概念:shape(数组形状)、dtype(元素类型)、axis(轴/维度)。核心操作:切片/索引(注意视图 vs 副本)、向量化运算(比循环快100倍)、广播(自动扩展形状)、矩阵乘法(@ 运算符)。PyTorch 的 Tensor 接口和 NumPy 几乎完全相同,区别是 Tensor 支持 GPU 和自动微分。常见陷阱:切片是视图(修改会影响原数组)、广播规则从右到左对齐、混合 dtype 会触发自动类型提升。