
NumPy数值计算库:AI开发的数学引擎
一、为什么需要NumPy?
1.1 Python原生列表的痛点
python
# Python原生列表的问题
python_list = [1, 2, 3, 4, 5]
# 问题1:元素级运算需要循环
doubled = [x * 2 for x in python_list] # 必须写循环
print(f"列表乘法: {doubled}")
# 问题2:多维数据难以表示
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
# 想取第一列?需要列表推导式
first_col = [row[0] for row in matrix]
# 问题3:数学运算慢(Python是解释型语言)
import time
large_list = list(range(1000000))
start = time.time()
result = [x ** 2 for x in large_list]
print(f"列表平方耗时: {time.time() - start:.4f}秒")
1.2 NumPy的解决方案
NumPy = Numeric Python,核心是ndarray(N-dimensional array)
python
import numpy as np
# NumPy数组的优势
np_array = np.array([1, 2, 3, 4, 5])
# 优势1:向量化运算(无需循环)
doubled = np_array * 2
print(f"NumPy数组乘法: {doubled}") # [2 4 6 8 10]
# 优势2:真正的多维数组
matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
first_col = matrix[:, 0] # 简洁的切片语法
print(f"第一列: {first_col}") # [1 4 7]
# 优势3:底层用C实现,速度快
large_array = np.arange(1000000)
start = time.time()
result = large_array ** 2
print(f"NumPy平方耗时: {time.time() - start:.4f}秒")
# 通常比Python列表快10-100倍!
# 优势4:内存连续,高效存储
print(f"内存占用: {large_array.nbytes / 1024 / 1024:.2f} MB")
1.3 NumPy在AI开发中的核心地位
AI开发数据流:
原始数据 → NumPy数组 → 预处理 → PyTorch/TensorFlow张量 → 模型
↑
80%的AI代码在这里
为什么AI离不开NumPy?
- 图像数据本质是三维数组(高×宽×通道)
- 文本特征用矩阵表示(样本×词向量)
- 模型参数是矩阵(权重×偏置)
- 损失函数、梯度计算都是向量化运算
二、ndarray的创建与基本属性
2.1 创建ndarray的多种方式
python
import numpy as np
# === 从列表创建 ===
arr1 = np.array([1, 2, 3, 4, 5])
print(f"从列表: {arr1}")
arr2 = np.array([[1, 2, 3], [4, 5, 6]])
print(f"二维数组:\n{arr2}")
# 注意:列表中的元素类型必须一致
mixed = np.array([1, 2, 3.5, 4])
print(f"类型自动提升: {mixed}") # [1. 2. 3.5 4.] 全部转为float
# === 使用NumPy内置函数创建 ===
# 全零数组
zeros = np.zeros((3, 4)) # 3行4列
print(f"全零数组:\n{zeros}")
# 全一数组
ones = np.ones((2, 3))
print(f"全一数组:\n{ones}")
# 单位矩阵
eye = np.eye(4) # 4x4单位矩阵
print(f"单位矩阵:\n{eye}")
# 未初始化数组(内容随机)
empty = np.empty((2, 2))
print(f"未初始化: {empty}") # 值不确定,速度快
# 填充固定值
full = np.full((3, 3), 7)
print(f"填充7:\n{full}")
# === 创建序列数组 ===
# arange:类似range,但返回数组
arange_arr = np.arange(0, 10, 2) # start, stop, step
print(f"arange: {arange_arr}") # [0 2 4 6 8]
# linspace:等间距点(包含终点)
lin_arr = np.linspace(0, 1, 5) # start, stop, 点数
print(f"linspace: {lin_arr}") # [0. 0.25 0.5 0.75 1.]
# 对数间隔
log_arr = np.logspace(0, 3, 4) # 10^0 到 10^3,4个点
print(f"logspace: {log_arr}") # [1. 10. 100. 1000.]
# === 随机数组(重要!)===
# 均匀分布[0,1)
rand_arr = np.random.rand(2, 3) # 2x3
print(f"均匀分布:\n{rand_arr}")
# 标准正态分布
randn_arr = np.random.randn(3, 3)
print(f"正态分布:\n{randn_arr}")
# 指定范围的随机整数
randint_arr = np.random.randint(0, 100, size=(3, 3))
print(f"随机整数:\n{randint_arr}")
2.2 理解ndarray的核心属性
python
# 创建一个三维数组(模拟3张RGB图像)
# shape: (batch_size, height, width, channels)
img_batch = np.random.rand(32, 224, 224, 3)
print("=== ndarray核心属性 ===")
print(f"shape(形状): {img_batch.shape}")
# shape是一个元组,描述每个维度的大小
# 含义:32张图,每张224x224像素,3个颜色通道
print(f"ndim(维度数): {img_batch.ndim}")
# 4维数组
print(f"size(总元素数): {img_batch.size}")
# 32 * 224 * 224 * 3 = 4,816,896
print(f"dtype(数据类型): {img_batch.dtype}")
# float64(双精度浮点)
print(f"itemsize(单个元素字节): {img_batch.itemsize}")
# float64占8字节
print(f"nbytes(总字节数): {img_batch.nbytes / 1024 / 1024:.2f} MB")
# 总内存占用
# === 理解shape的层次 ===
arr = np.array([
[[1, 2], [3, 4]],
[[5, 6], [7, 8]]
])
print(f"\n三维数组:\n{arr}")
print(f"shape: {arr.shape}") # (2, 2, 2)
# 解读:2个矩阵,每个2行2列
2.3 dtype详解(理解数据类型)
python
# NumPy的数据类型比Python更精细
# 原因:控制内存和计算精度
# === 常用dtype ===
types = [
('int8', np.int8, '有符号8位整数', -128, 127),
('int32', np.int32, '有符号32位整数', -2^31, 2^31-1),
('float32', np.float32, '单精度浮点', 'AI模型常用'),
('float64', np.float64, '双精度浮点', 'NumPy默认'),
('bool', np.bool, '布尔值', 'True/False'),
]
# 创建时指定dtype
arr_int8 = np.array([1, 2, 3], dtype=np.int8)
print(f"int8数组: {arr_int8}, 占用: {arr_int8.nbytes}字节")
arr_float32 = np.array([1.5, 2.5, 3.5], dtype=np.float32)
print(f"float32数组: {arr_float32}, 精度: {arr_float32.dtype}")
# 类型转换(astype)
arr_float = np.array([1.5, 2.7, 3.9])
arr_int = arr_float.astype(np.int32) # 截断小数
print(f"float转int: {arr_float} -> {arr_int}")
# AI实践:图像数据转换
image_uint8 = np.random.randint(0, 256, (224, 224, 3), dtype=np.uint8)
print(f"原始图像(0-255): {image_uint8.dtype}")
# 归一化到[0,1](必须转float)
image_float = image_uint8.astype(np.float32) / 255.0
print(f"归一化后: {image_float.dtype}, 范围: [{image_float.min():.2f}, {image_float.max():.2f}]")
三、索引、切片与布尔索引
3.1 基础索引(理解视图vs副本)
python
# 创建示例数组
arr = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
print(f"原始数组:\n{arr}")
# === 基础索引 ===
print(f"\n单个元素: arr[1, 2] = {arr[1, 2]}") # 第2行第3列 = 7
print(f"等价写法: arr[1][2] = {arr[1][2]}") # 不推荐,效率低
# 获取一行
row = arr[1, :] # 第2行所有列
print(f"第2行: {row}")
# 获取一列
col = arr[:, 2] # 所有行第3列
print(f"第3列: {col}")
# 获取子矩阵
sub = arr[0:2, 1:3] # 前2行,第2-3列
print(f"子矩阵:\n{sub}")
# === 重要概念:视图(View) vs 副本(Copy) ===
sub_view = arr[0:2, 0:2]
sub_view[0, 0] = 999 # 修改视图
print(f"\n修改视图后原数组:\n{arr}")
# 原数组也被修改了!因为切片返回的是视图
# 如果需要独立副本,使用.copy()
sub_copy = arr[0:2, 0:2].copy()
sub_copy[0, 0] = 888
print(f"修改副本不影响原数组:\n{arr}")
3.2 高级切片技巧
python
# 创建示例
arr = np.arange(24).reshape(4, 6)
print(f"原始数组 4x6:\n{arr}")
# === 步长切片 ===
# 每隔一个取一个
step_arr = arr[::2, ::2] # 行步长2,列步长2
print(f"步长2采样:\n{step_arr}")
# 反向(翻转)
reverse_arr = arr[::-1, :] # 上下翻转
print(f"上下翻转:\n{reverse_arr}")
# 同时反向
full_reverse = arr[::-1, ::-1] # 180度旋转
print(f"完全反转:\n{full_reverse}")
# === ... 省略号(处理高维数组)===
arr_3d = np.random.rand(2, 3, 4)
print(f"\n3D数组shape: {arr_3d.shape}")
# ...代表"所有剩余维度"
print(f"arr_3d[0, ...] shape: {arr_3d[0, ...].shape}") # 等价于arr_3d[0, :, :]
print(f"arr_3d[..., 0] shape: {arr_3d[..., 0].shape}") # 等价于arr_3d[:, :, 0]
# === newaxis增加维度 ===
arr_1d = np.array([1, 2, 3])
print(f"1D shape: {arr_1d.shape}")
# 增加行维度(变成列向量)
col_vec = arr_1d[:, np.newaxis]
print(f"列向量 shape: {col_vec.shape}") # (3, 1)
# 增加列维度(变成行向量)
row_vec = arr_1d[np.newaxis, :]
print(f"行向量 shape: {row_vec.shape}") # (1, 3)
# AI实践:批量处理时的维度调整
batch = np.random.rand(32, 224, 224, 3) # (batch, h, w, c)
# 想取每个batch的第一行
first_row = batch[:, 0, :, :] # shape: (32, 224, 3)
print(f"取每张图第一行: {first_row.shape}")
3.3 布尔索引(条件筛选)
python
# 布尔索引是AI数据清洗的核心工具
# 创建示例数据
data = np.array([12, 45, 23, 67, 89, 34, 56, 78])
print(f"原始数据: {data}")
# === 基础布尔索引 ===
# 创建条件掩码
mask = data > 50
print(f"大于50的掩码: {mask}") # [False False False True True False True True]
# 使用掩码筛选
filtered = data[mask]
print(f"大于50的值: {filtered}") # [67 89 56 78]
# 一行写法
result = data[data > 50]
print(f"一行写法: {result}")
# === 多条件组合 ===
# 与 (&)、或 (|)、非 (~)
mask_and = (data > 30) & (data < 70) # 30-70之间
print(f"30-70之间: {data[mask_and]}")
mask_or = (data < 20) | (data > 80) # 小于20或大于80
print(f"小于20或大于80: {data[mask_or]}")
mask_not = ~(data > 50) # 不大于50
print(f"不大于50: {data[mask_not]}")
# === 实战:异常值检测与处理 ===
# 模拟传感器数据
sensor_data = np.random.randn(1000) * 10 + 50 # 均值50,标准差10
print(f"\n传感器数据: 均值={sensor_data.mean():.2f}, 标准差={sensor_data.std():.2f}")
# 检测异常值(3倍标准差外)
mean = sensor_data.mean()
std = sensor_data.std()
outliers = np.abs(sensor_data - mean) > 3 * std
print(f"异常值数量: {outliers.sum()}")
print(f"异常值: {sensor_data[outliers]}")
# 处理异常值:替换为中位数
median = np.median(sensor_data)
sensor_data[outliers] = median
print(f"处理后异常值: {sensor_data[outliers].sum()}") # 全被替换成中位数
# === AI实战:图像二值化 ===
# 模拟灰度图像(0-255)
image = np.random.randint(0, 256, (10, 10))
print(f"\n原始图像:\n{image}")
# 二值化:大于128的设为1,否则0
binary = np.where(image > 128, 1, 0)
print(f"二值化后:\n{binary}")
# 更复杂的条件:三段式
# 小于100置0,100-200置1,大于200置2
segmented = np.piecewise(image,
[image < 100, (image >= 100) & (image <= 200), image > 200],
[0, 1, 2])
print(f"分段处理:\n{segmented}")
四、广播机制(Broadcasting)
4.1 广播的原理
广播是NumPy最强大也最易混淆的特性,理解它才能写出高效代码。
python
# 核心思想:不同形状的数组进行运算时,NumPy会自动扩展较小数组
# === 最简单的广播:标量 vs 数组 ===
arr = np.array([1, 2, 3, 4, 5])
result = arr + 10 # 标量10被广播到所有元素
print(f"数组+标量: {arr} + 10 = {result}")
# 原理:10被扩展为 [10, 10, 10, 10, 10]
# === 一维 vs 二维广播 ===
matrix = np.array([[1, 2, 3],
[4, 5, 6]])
vector = np.array([10, 20, 30])
result = matrix + vector
print(f"\n矩阵 + 向量:\n{matrix}\n+\n{vector}\n=\n{result}")
# 广播过程:
# vector形状(3,) → 广播为(2,3) → [[10,20,30], [10,20,30]]
# 然后逐元素相加
4.2 广播规则(理解这个就够了)
规则:从后往前比较维度,满足以下条件之一即可广播:
- 维度相等
- 其中一个维度为1
- 其中一个维度不存在
python
# 规则1:维度相等
arr1 = np.random.rand(3, 4)
arr2 = np.random.rand(3, 4)
# ✅ 可以广播:形状完全相同
# 规则2:其中一个为1
arr1 = np.random.rand(3, 4)
arr2 = np.random.rand(1, 4)
# ✅ arr2的1可以广播为3 → (3,4)
arr1 = np.random.rand(3, 4, 5)
arr2 = np.random.rand(4, 1)
# ✅ (3,4,5) vs (4,1)
# 从后往前:5 vs 1 ✅ 1广播为5
# 4 vs 4 ✅ 相等
# 3 vs (无) ✅ 维度不存在
# 规则3:无法广播的例子
arr1 = np.random.rand(3, 4)
arr2 = np.random.rand(2, 4)
# ❌ 3 vs 2,都不为1,无法广播 → ValueError
4.3 广播实战案例
python
# === 案例1:数据标准化(零均值单位方差)===
data = np.random.randn(100, 50) # 100个样本,50个特征
# 计算每列的均值和标准差
col_mean = data.mean(axis=0) # shape (50,)
col_std = data.std(axis=0) # shape (50,)
# 广播:将(50,)广播到(100,50)
normalized = (data - col_mean) / col_std
print(f"标准化后: 均值={normalized.mean():.2f}, 标准差={normalized.std():.2f}")
# === 案例2:为每个样本添加偏置 ===
features = np.random.rand(32, 256) # 32个样本,256维特征
bias = np.random.rand(256) # 每个特征的偏置
# 广播加法
biased_features = features + bias
print(f"添加偏置后shape: {biased_features.shape}")
# === 案例3:外积计算(无需循环)===
x = np.array([1, 2, 3])
y = np.array([4, 5, 6, 7])
# 想要计算 x_i * y_j 的所有组合
outer_product = x[:, np.newaxis] * y[np.newaxis, :]
print(f"外积:\n{outer_product}")
# [[ 4 5 6 7]
# [ 8 10 12 14]
# [12 15 18 21]]
# === 案例4:图像数据增强(减去均值)===
# 模拟一批RGB图像
batch = np.random.randint(0, 256, (64, 224, 224, 3)).astype(np.float32)
# 计算全局RGB均值(3个通道)
channel_mean = batch.mean(axis=(0, 1, 2)) # shape (3,)
print(f"RGB均值: {channel_mean}")
# 广播减去均值
batch_centered = batch - channel_mean
print(f"去均值后shape: {batch_centered.shape}")
# === 案例5:计算样本间的距离矩阵 ===
# 计算所有样本对之间的欧氏距离
samples = np.random.rand(100, 10) # 100个样本,10维
# 利用广播:|a-b|^2 = a^2 + b^2 - 2ab
# (100,1,10) vs (1,100,10) 广播为 (100,100,10)
diff = samples[:, np.newaxis, :] - samples[np.newaxis, :, :]
distances = np.sqrt((diff ** 2).sum(axis=2))
print(f"距离矩阵shape: {distances.shape}") # (100, 100)
五、通用函数(ufunc)
5.1 什么是ufunc?
ufunc = Universal Function,对数组逐元素操作的函数
python
# ufunc的核心优势:向量化、无需循环、速度快
# 创建测试数据
arr = np.array([1, 2, 3, 4, 5])
# 数学运算
print(f"原始: {arr}")
print(f"平方: {np.square(arr)}") # [1 4 9 16 25]
print(f"开方: {np.sqrt(arr)}") # [1. 1.414 1.732 2. 2.236]
print(f"指数: {np.exp(arr)}") # e^x
print(f"自然对数: {np.log(arr)}") # ln(x)
print(f"以10为底: {np.log10(arr)}") # log10(x)
# 三角函数
angles = np.array([0, np.pi/6, np.pi/4, np.pi/3, np.pi/2])
print(f"\n角度: {angles}")
print(f"sin: {np.sin(angles)}")
print(f"cos: {np.cos(angles)}")
5.2 常用数学ufunc详解
python
# === 1. 指数和对数(AI中最常用)===
# Softmax函数的核心
def softmax(x):
"""Softmax实现,展示exp和sum的配合"""
exp_x = np.exp(x - np.max(x)) # 减去最大值防止溢出
return exp_x / np.sum(exp_x)
logits = np.array([2.0, 1.0, 0.1])
probs = softmax(logits)
print(f"Softmax输出: {probs}")
print(f"概率和: {probs.sum()}")
# === 2. 统计函数 ===
data = np.random.randn(1000)
print(f"求和: {np.sum(data):.2f}")
print(f"均值: {np.mean(data):.2f}")
print(f"中位数: {np.median(data):.2f}")
print(f"标准差: {np.std(data):.2f}")
print(f"方差: {np.var(data):.2f}")
print(f"最小值: {np.min(data):.2f}")
print(f"最大值: {np.max(data):.2f}")
print(f"取值范围: {np.ptp(data):.2f}") # max-min
# axis参数(指定计算轴)
matrix = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
print(f"\n原矩阵:\n{matrix}")
print(f"按列求和(axis=0): {np.sum(matrix, axis=0)}") # [12 15 18]
print(f"按行求和(axis=1): {np.sum(matrix, axis=1)}") # [6 15 24]
print(f"全部求和: {np.sum(matrix)}") # 45
# keepdims保持维度
row_sum = np.sum(matrix, axis=1, keepdims=True)
print(f"保持维度: {row_sum.shape}") # (3,1)
# === 3. 累加和累乘 ===
arr = np.array([1, 2, 3, 4, 5])
print(f"累加: {np.cumsum(arr)}") # [1 3 6 10 15]
print(f"累乘: {np.cumprod(arr)}") # [1 2 6 24 120]
5.3 点积与矩阵乘法(AI核心运算)
python
# === np.dot:点积/矩阵乘法 ===
# 向量点积
v1 = np.array([1, 2, 3])
v2 = np.array([4, 5, 6])
dot_product = np.dot(v1, v2) # 1*4 + 2*5 + 3*6 = 32
print(f"向量点积: {dot_product}")
# 矩阵乘法(最重要的运算)
A = np.array([[1, 2],
[3, 4]])
B = np.array([[5, 6],
[7, 8]])
C = np.dot(A, B)
print(f"\n矩阵A:\n{A}")
print(f"矩阵B:\n{B}")
print(f"矩阵乘法A@B:\n{C}")
# 手动验证:C[0,0] = A[0,0]*B[0,0] + A[0,1]*B[1,0] = 1*5 + 2*7 = 19
# === 其他矩阵乘法方式 ===
# 方法1:np.dot
result1 = np.dot(A, B)
# 方法2:@运算符(Python 3.5+,推荐)
result2 = A @ B
# 方法3:np.matmul
result3 = np.matmul(A, B)
print(f"三种方式等价: {np.allclose(result1, result2)}")
# === 神经网络中的矩阵乘法 ===
# 输入层:32个样本,每个784维
input_layer = np.random.randn(32, 784)
# 权重矩阵:784个输入,256个输出
weights = np.random.randn(784, 256)
# 偏置:256个神经元
bias = np.random.randn(256)
# 全连接层计算
output = input_layer @ weights + bias # shape (32, 256)
print(f"\n全连接层输出shape: {output.shape}")
# === 理解维度匹配规则 ===
# (m, n) @ (n, p) = (m, p)
# 关键:第一个矩阵的列数 = 第二个矩阵的行数
5.4 聚合函数的高效使用
python
# 创建3D数据(模拟视频帧:10帧,224x224,3通道)
video = np.random.randn(10, 224, 224, 3)
# === 多轴聚合 ===
# 计算每帧的均值(保留batch维度)
frame_mean = video.mean(axis=(1, 2, 3)) # shape (10,)
print(f"每帧均值shape: {frame_mean.shape}")
# 计算所有像素的全局均值
global_mean = video.mean()
print(f"全局均值: {global_mean:.4f}")
# === 实战:计算准确率 ===
predictions = np.array([0, 1, 1, 0, 1, 0, 1, 1, 0, 0])
labels = np.array([0, 1, 0, 0, 1, 1, 1, 0, 0, 1])
accuracy = np.mean(predictions == labels)
print(f"\n准确率: {accuracy:.2f}")
# === 实战:梯度裁剪 ===
gradients = np.random.randn(1000) * 10
print(f"裁剪前: 最大值={gradients.max():.2f}, 最小值={gradients.min():.2f}")
# 限制梯度范围[-5, 5]
clipped = np.clip(gradients, -5, 5)
print(f"裁剪后: 最大值={clipped.max():.2f}, 最小值={clipped.min():.2f}")
六、随机数生成(np.random)
6.1 随机数种子:可重复性的关键
python
# AI训练必须设置随机种子,否则结果不可复现
# 不设置种子,每次运行结果不同
print("无种子:")
print(np.random.rand(3)) # 每次不同
# 设置种子,结果固定
np.random.seed(42)
print("\n设置种子42:")
print(np.random.rand(3)) # [0.37454012 0.95071431 0.73199394]
np.random.seed(42) # 再次设置相同种子
print(np.random.rand(3)) # [0.37454012 0.95071431 0.73199394] 相同!
# 最佳实践:在程序开头统一设置
SEED = 42
np.random.seed(SEED)
# 如果使用其他库,也要设置
# random.seed(SEED)
# torch.manual_seed(SEED)
6.2 各种分布采样
python
np.random.seed(42)
# === 均匀分布 ===
# [0,1) 均匀分布
uniform = np.random.rand(1000)
print(f"均匀分布: 均值={uniform.mean():.3f}, 标准差={uniform.std():.3f}")
# [low, high) 均匀分布
uniform_range = np.random.uniform(low=-1, high=1, size=(3, 3))
print(f"[-1,1)均匀分布:\n{uniform_range}")
# === 正态分布(高斯分布)===
# 标准正态 N(0,1)
normal_std = np.random.randn(1000)
print(f"\n标准正态: 均值={normal_std.mean():.3f}, 标准差={normal_std.std():.3f}")
# 自定义正态 N(μ, σ²)
normal_custom = np.random.normal(loc=5, scale=2, size=1000)
print(f"N(5,4): 均值={normal_custom.mean():.3f}, 标准差={normal_custom.std():.3f}")
# === 整数分布 ===
# [low, high) 随机整数
integers = np.random.randint(low=0, high=10, size=(2, 5))
print(f"\n随机整数:\n{integers}")
# === 选择与排列 ===
data = np.array([10, 20, 30, 40, 50])
# 随机选择(可重复)
choice_with_replacement = np.random.choice(data, size=5, replace=True)
print(f"有放回采样: {choice_with_replacement}")
# 随机选择(不重复)
choice_no_replacement = np.random.choice(data, size=3, replace=False)
print(f"无放回采样: {choice_no_replacement}")
# 随机打乱
np.random.shuffle(data)
print(f"打乱后: {data}")
# 随机排列(返回新数组)
permuted = np.random.permutation(10)
print(f"0-9随机排列: {permuted}")
6.3 AI实战:数据增强采样
python
np.random.seed(42)
class DataAugmentation:
"""图像数据增强工具"""
@staticmethod
def random_flip(image):
"""随机水平翻转"""
if np.random.rand() > 0.5:
return np.fliplr(image)
return image
@staticmethod
def random_rotation(image, max_angle=20):
"""随机旋转(模拟)"""
angle = np.random.uniform(-max_angle, max_angle)
# 实际应用中会使用scipy.ndimage.rotate
return image # 简化演示
@staticmethod
def random_brightness(image, max_delta=0.2):
"""随机亮度调整"""
delta = np.random.uniform(-max_delta, max_delta)
return np.clip(image + delta, 0, 1)
@staticmethod
def random_crop(image, crop_size):
"""随机裁剪"""
h, w = image.shape[:2]
top = np.random.randint(0, h - crop_size)
left = np.random.randint(0, w - crop_size)
return image[top:top+crop_size, left:left+crop_size]
# 使用示例
sample_image = np.random.rand(224, 224, 3)
aug = DataAugmentation()
# 随机应用多种增强
augmented = sample_image.copy()
if np.random.rand() > 0.5:
augmented = aug.random_flip(augmented)
if np.random.rand() > 0.5:
augmented = aug.random_brightness(augmented)
print(f"数据增强完成,输出shape: {augmented.shape}")
# === 实战:Mini-batch采样 ===
def sample_mini_batch(data, labels, batch_size):
"""随机采样mini-batch"""
n_samples = len(data)
indices = np.random.choice(n_samples, size=batch_size, replace=False)
return data[indices], labels[indices]
# 模拟数据集
X = np.random.randn(10000, 784)
y = np.random.randint(0, 10, 10000)
# 采样batch
batch_X, batch_y = sample_mini_batch(X, y, batch_size=32)
print(f"Batch X shape: {batch_X.shape}")
print(f"Batch y shape: {batch_y.shape}")
七、线性代数基础(np.linalg)
7.1 矩阵运算基础
python
# 创建示例矩阵
A = np.array([[1, 2],
[3, 4]])
B = np.array([[5, 6],
[7, 8]])
# === 矩阵转置 ===
print(f"矩阵A:\n{A}")
print(f"A的转置:\n{A.T}")
# === 矩阵的迹(对角线之和)===
trace = np.trace(A)
print(f"\n矩阵的迹: {trace}") # 1 + 4 = 5
# === 矩阵的行列式 ===
det = np.linalg.det(A)
print(f"行列式: {det:.2f}") # 1*4 - 2*3 = -2
# 行列式的几何意义:线性变换的面积缩放因子
# |det| > 1: 面积放大;|det| < 1: 面积缩小
7.2 逆矩阵
python
# === 逆矩阵:A @ A^(-1) = I ===
A = np.array([[2, 1],
[5, 3]])
# 计算逆矩阵
A_inv = np.linalg.inv(A)
print(f"原矩阵A:\n{A}")
print(f"\n逆矩阵A⁻¹:\n{A_inv}")
# 验证
identity = A @ A_inv
print(f"\nA @ A⁻¹:\n{identity}")
print(f"是否为单位矩阵: {np.allclose(identity, np.eye(2))}")
# 求解线性方程组 Ax = b
# 例:2x + y = 8, 5x + 3y = 19
A = np.array([[2, 1],
[5, 3]])
b = np.array([8, 19])
# 方法1:使用逆矩阵
x = np.linalg.inv(A) @ b
print(f"解 (x, y) = {x}") # [5. -2.] 即 x=5, y=-2
# 方法2:使用solve(数值更稳定,推荐)
x = np.linalg.solve(A, b)
print(f"使用solve: {x}")
# 验证
print(f"Ax = {A @ x}, b = {b}")
# 注意:只有方阵且可逆才能求逆
# 不可逆矩阵(奇异矩阵)
singular = np.array([[1, 2],
[2, 4]]) # 第二行是第一行的2倍
try:
inv_singular = np.linalg.inv(singular)
except np.linalg.LinAlgError as e:
print(f"奇异矩阵无法求逆: {e}")
7.3 特征值与特征向量
python
# 特征值分解:A·v = λ·v
# v是特征向量,λ是特征值(代表变换的缩放因子)
A = np.array([[4, 2],
[1, 3]])
# 计算特征值和特征向量
eigenvalues, eigenvectors = np.linalg.eig(A)
print(f"矩阵A:\n{A}")
print(f"\n特征值: {eigenvalues}")
print(f"特征向量:\n{eigenvectors}")
# 验证:A·v = λ·v
for i in range(len(eigenvalues)):
v = eigenvectors[:, i]
lambda_v = eigenvalues[i] * v
Av = A @ v
print(f"\n验证特征向量{i+1}:")
print(f"λ·v = {lambda_v}")
print(f"A·v = {Av}")
print(f"相等: {np.allclose(lambda_v, Av)}")
# 特征值的应用:主成分分析(PCA)原理
# 协方差矩阵的特征向量就是主成分方向
7.4 AI实战:线性回归的闭式解
python
# 线性回归:y = X·w,最小二乘解 w = (X^T·X)^(-1)·X^T·y
# 生成合成数据
np.random.seed(42)
X = np.random.randn(100, 3) # 100个样本,3个特征
true_w = np.array([2, -1, 0.5])
y = X @ true_w + np.random.randn(100) * 0.1 # 添加噪声
# === 最小二乘求解 ===
# 方法1:正规方程
w_hat = np.linalg.inv(X.T @ X) @ X.T @ y
print(f"真实权重: {true_w}")
print(f"估计权重: {w_hat}")
print(f"误差: {np.linalg.norm(true_w - w_hat):.6f}")
# 方法2:使用lstsq(更稳定)
w_hat_lstsq, residuals, rank, s = np.linalg.lstsq(X, y, rcond=None)
print(f"lstsq估计: {w_hat_lstsq}")
# === 预测 ===
X_new = np.random.randn(10, 3)
y_pred = X_new @ w_hat
print(f"\n预测值前3个: {y_pred[:3]}")
# === 计算R²分数 ===
y_pred_train = X @ w_hat
ss_res = np.sum((y - y_pred_train) ** 2)
ss_tot = np.sum((y - np.mean(y)) ** 2)
r2 = 1 - (ss_res / ss_tot)
print(f"R²分数: {r2:.4f}") # 接近1表示拟合良好
八、实战项目:从零实现KNN分类器
综合运用NumPy所有知识点的完整示例:
python
import numpy as np
from collections import Counter
class KNNClassifier:
"""
K近邻分类器 - 使用NumPy实现
原理:一个样本的类别由它最近的K个邻居投票决定
"""
def __init__(self, k=3):
self.k = k
self.X_train = None
self.y_train = None
def fit(self, X, y):
"""训练:只保存数据"""
self.X_train = X
self.y_train = y
def _compute_distances(self, X_test):
"""
计算测试集与训练集的距离矩阵
利用广播机制高效计算欧氏距离
"""
# X_test: (n_test, n_features)
# X_train: (n_train, n_features)
# 平方和:|a-b|^2 = a^2 + b^2 - 2ab
# (n_test, 1, n_features) vs (1, n_train, n_features)
diff = X_test[:, np.newaxis, :] - self.X_train[np.newaxis, :, :]
distances = np.sqrt(np.sum(diff ** 2, axis=2))
return distances # shape: (n_test, n_train)
def predict(self, X_test):
"""预测类别"""
# 1. 计算距离
distances = self._compute_distances(X_test)
# 2. 找到最近的K个邻居的索引
# np.argsort返回排序后的索引
k_nearest_indices = np.argsort(distances, axis=1)[:, :self.k]
# 3. 获取这些邻居的标签
k_nearest_labels = self.y_train[k_nearest_indices]
# 4. 投票决定类别
predictions = []
for labels in k_nearest_labels:
# 统计每个标签出现的次数
vote_counts = Counter(labels)
# 选择出现最多的标签
most_common = vote_counts.most_common(1)[0][0]
predictions.append(most_common)
return np.array(predictions)
def predict_proba(self, X_test):
"""预测概率分布"""
distances = self._compute_distances(X_test)
k_nearest_indices = np.argsort(distances, axis=1)[:, :self.k]
k_nearest_labels = self.y_train[k_nearest_indices]
# 计算每个类别的概率
n_classes = len(np.unique(self.y_train))
probas = []
for labels in k_nearest_labels:
counts = np.bincount(labels, minlength=n_classes)
probas.append(counts / self.k)
return np.array(probas)
def accuracy(self, X_test, y_test):
"""计算准确率"""
y_pred = self.predict(X_test)
return np.mean(y_pred == y_test)
# === 生成测试数据 ===
np.random.seed(42)
def generate_data(n_samples=300):
"""生成三类数据,每类100个样本"""
X = []
y = []
# 类别0:以(0,0)为中心
X0 = np.random.randn(100, 2) + np.array([0, 0])
y0 = np.zeros(100, dtype=int)
# 类别1:以(3,0)为中心
X1 = np.random.randn(100, 2) + np.array([3, 0])
y1 = np.ones(100, dtype=int)
# 类别2:以(1.5, 3)为中心
X2 = np.random.randn(100, 2) * 0.8 + np.array([1.5, 3])
y2 = np.full(100, 2, dtype=int)
X = np.vstack([X0, X1, X2])
y = np.hstack([y0, y1, y2])
# 打乱数据
indices = np.random.permutation(len(X))
return X[indices], y[indices]
# === 训练和评估 ===
print("=" * 60)
print("KNN分类器实战")
print("=" * 60)
# 生成数据
X, y = generate_data(300)
print(f"数据集大小: {X.shape}")
print(f"类别分布: {Counter(y)}")
# 划分训练集和测试集
split_ratio = 0.8
split_idx = int(len(X) * split_ratio)
X_train, X_test = X[:split_idx], X[split_idx:]
y_train, y_test = y[:split_idx], y[split_idx:]
print(f"\n训练集: {len(X_train)}, 测试集: {len(X_test)}")
# 训练模型
knn = KNNClassifier(k=5)
knn.fit(X_train, y_train)
# 预测
y_pred = knn.predict(X_test)
accuracy = knn.accuracy(X_test, y_test)
print(f"\n预测结果前10个: {y_pred[:10]}")
print(f"真实标签前10个: {y_test[:10]}")
print(f"准确率: {accuracy:.4f}")
# 预测概率
probas = knn.predict_proba(X_test[:5])
print(f"\n前5个样本的概率分布:")
for i, prob in enumerate(probas):
print(f" 样本{i}: 类别0={prob[0]:.2f}, 类别1={prob[1]:.2f}, 类别2={prob[2]:.2f}")
# === 不同K值的影响 ===
print("\n" + "=" * 60)
print("K值对准确率的影响")
print("=" * 60)
for k in [1, 3, 5, 7, 9, 11, 15, 20]:
knn = KNNClassifier(k=k)
knn.fit(X_train, y_train)
acc = knn.accuracy(X_test, y_test)
print(f"K={k:2d}, 准确率={acc:.4f}")
# === 可视化决策边界(简化版)===
print("\n" + "=" * 60)
print("决策边界分析")
print("=" * 60)
# 创建网格点
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 50),
np.linspace(y_min, y_max, 50))
# 预测网格点的类别
grid_points = np.c_[xx.ravel(), yy.ravel()]
knn = KNNClassifier(k=5)
knn.fit(X_train, y_train)
grid_pred = knn.predict(grid_points)
grid_pred = grid_pred.reshape(xx.shape)
# 计算边界清晰度(预测熵)
# 熵值越高,边界越模糊
from scipy.stats import entropy
unique, counts = np.unique(grid_pred, return_counts=True)
if len(unique) > 1:
probs = counts / counts.sum()
boundary_entropy = entropy(probs)
print(f"决策边界熵: {boundary_entropy:.4f}")
print("熵值越小,类别分离越清晰")
print("\nKNN实现完成!✅")
九、性能优化技巧
9.1 向量化 vs 循环
python
# 创建数据
arr = np.random.randn(1000000)
# ❌ 慢:Python循环
def add_one_loop(arr):
result = []
for x in arr:
result.append(x + 1)
return np.array(result)
# ✅ 快:NumPy向量化
def add_one_vectorized(arr):
return arr + 1
# 性能对比
import time
start = time.time()
result_loop = add_one_loop(arr)
time_loop = time.time() - start
start = time.time()
result_vec = add_one_vectorized(arr)
time_vec = time.time() - start
print(f"循环耗时: {time_loop:.4f}秒")
print(f"向量化耗时: {time_vec:.4f}秒")
print(f"加速比: {time_loop / time_vec:.1f}x")
# 结论:永远使用向量化操作!
9.2 内存优化
python
# 1. 选择合适的数据类型
arr_float64 = np.random.randn(1000000)
arr_float32 = arr_float64.astype(np.float32)
print(f"float64内存: {arr_float64.nbytes / 1024 / 1024:.2f} MB")
print(f"float32内存: {arr_float32.nbytes / 1024 / 1024:.2f} MB")
# 2. 使用视图而非副本
arr = np.arange(1000000)
# 视图(不复制内存)
view = arr[::2] # 只改变步长,不复制数据
print(f"视图是视图: {view.base is arr}") # True
# 副本(复制内存)
copy = arr[::2].copy()
print(f"副本是视图: {copy.base is arr}") # False
# 3. 原地操作(避免创建新数组)
arr = np.random.randn(1000)
# 创建新数组
arr_new = arr + 1
# 原地操作(更快,省内存)
arr += 1
# 4. 使用np.empty而非np.zeros
# 如果后续会填充所有值,用empty更快
arr = np.empty((1000, 1000))
arr[:] = 42 # 填充值
十、常见错误与解决方案
| 错误 | 原因 | 解决方案 |
|---|---|---|
ValueError: operands could not be broadcast together |
形状不兼容 | 检查shape,使用reshape或newaxis |
MemoryError |
数组太大 | 使用分块处理或更小的dtype |
LinAlgError: Singular matrix |
矩阵不可逆 | 使用np.linalg.lstsq代替inv |
| 浮点精度问题 | 数值计算误差 | 使用np.allclose()比较浮点数 |
| 修改了视图影响原数组 | 忘记.copy() |
明确使用.copy()创建副本 |
python
# 错误示例与修正
# 错误1:广播错误
A = np.random.rand(3, 4)
B = np.random.rand(3, 5)
try:
C = A + B
except ValueError as e:
print(f"错误: {e}")
# 修正:检查维度
print(f"A shape: {A.shape}, B shape: {B.shape}")
# 错误2:浮点数比较
a = 0.1 + 0.2
b = 0.3
print(f"直接比较: {a == b}") # False!
print(f"正确比较: {np.allclose(a, b)}") # True
# 错误3:忘记copy导致意外修改
original = np.array([1, 2, 3])
view = original[:2]
view[0] = 999
print(f"原数组被修改: {original}") # [999, 2, 3]
# 修正
original = np.array([1, 2, 3])
copy = original[:2].copy()
copy[0] = 999
print(f"原数组未修改: {original}") # [1, 2, 3]
十一、学习检查清单
基础掌握(必须)
- 创建ndarray(
array,zeros,ones,arange,linspace) - 查看属性(
shape,dtype,ndim,size) - 索引和切片(包括步长、负索引)
- 改变形状(
reshape,ravel,flatten) - 数组运算(
+,-,*,/,**) - 统计函数(
sum,mean,std,min,max)
进阶掌握(重要)
- 广播机制的理解和应用
- 布尔索引和条件筛选
- 轴参数(
axis=0/1)的理解 - 随机数生成和种子设置
- 矩阵乘法(
@,dot)
扩展了解(按需)
- 线性代数(
inv,det,eig,solve) - 文件读写(
loadtxt,savetxt,save,load) - 高级索引(花式索引)
- 通用函数(ufunc)
十二、总结
NumPy的核心价值:
- ndarray:高效的多维数组容器
- 向量化:用C速度执行Python级别的运算
- 广播:优雅处理不同形状的数组
- ufunc:丰富的数学函数库
- 随机数:完整的数据采样工具
- 线性代数:AI算法的基础支撑
学习建议:
- 先用10个核心函数(
array,reshape,mean,std,dot,random,where,concatenate,split,transpose) - 理解shape和broadcasting(这是NumPy的灵魂)
- 能用向量化就别用循环
- 遇到问题先查shape
下一步:
- 学习Pandas(基于NumPy的数据分析库)
- 学习Matplotlib(数据可视化)
- 学习PyTorch/TensorFlow(GPU加速的张量计算)
记住:NumPy是AI开发的数学引擎,掌握NumPy就等于掌握了AI数据处理的核心。