写在前面
一、那个让你永远在"调 API"的东西
二、向量——AI 世界的基本语言
从一个数组说起
# 程序员眼中的"数组"user_ages = [25, 30, 28, 35, 22]# 数学家眼中的"向量"import numpy as npuser_vector = np.array([25, 30, 28, 35, 22])
方向和长度——向量的几何直觉
import numpy as np# 两个向量v1 = np.array([3, 4]) # 指向右上v2 = np.array([-2, 3]) # 指向左上# 计算长度(模)print(f"v1 的长度: {np.linalg.norm(v1)}") # 5.0(勾股定理:√(9+16)=5)print(f"v2 的长度: {np.linalg.norm(v2)}") # ~3.61# 向量的方向(归一化 / 单位向量)v1_unit = v1 / np.linalg.norm(v1)print(f"v1 的单位向量: {v1_unit}") # [0.6, 0.8]
向量加法和数乘——组合与缩放
import numpy as npv1 = np.array([1, 2])v2 = np.array([3, 1])# 加法:两个向量首尾相连,结果是从原点到终点的向量v_sum = v1 + v2 # [4, 3]# 数乘:等比例放大或缩小,方向不变v_scaled = 2 * v1 # [2, 4],长度翻倍,方向不变# 更有意思的:线性组合v_combo = 0.5 * v1 + 0.5 * v2 # 取两个向量的"中间点"print(f"线性组合: {v_combo}") # [2.0, 1.5]
点积——AI 中最重要的运算
import numpy as npa = np.array([1, 2, 3])b = np.array([4, 5, 6])# 点积:对应位置相乘再求和dot = np.dot(a, b) # 1×4 + 2×5 + 3×6 = 32print(f"点积: {dot}")
import numpy as np# 方向接近a = np.array([1, 0])b = np.array([0.9, 0.1])print(f"方向接近,点积: {np.dot(a, b):.2f}") # 0.9# 互相垂直c = np.array([1, 0])d = np.array([0, 1])print(f"互相垂直,点积: {np.dot(c, d):.2f}") # 0.0# 方向相反e = np.array([1, 0])f = np.array([-1, 0])print(f"方向相反,点积: {np.dot(e, f):.2f}") # -1.0
余弦相似度——忽略长度,只看方向
import numpy as npdef cosine_similarity(a, b): """余弦相似度 = 归一化后的点积""" norm_a = np.linalg.norm(a) norm_b = np.linalg.norm(b) return np.dot(a, b) / (norm_a * norm_b)# 两段相似的文字text1_vec = np.array([0.8, 0.6, 0.1]) # 比如讲"机器学习"的文章text2_vec = np.array([0.7, 0.7, 0.05]) # 也是讲"机器学习"的文章# 两段不相关的文字text3_vec = np.array([0.1, 0.2, 0.9]) # 比如讲"烹饪"的文章print(f"相似文章的余弦相似度: {cosine_similarity(text1_vec, text2_vec):.4f}")# ~0.986print(f"不相关文章的余弦相似度: {cosine_similarity(text1_vec, text3_vec):.4f}")# ~0.227
三、矩阵——数据的长相
从向量到矩阵:数据升级
import numpy as np# 一个用户的行为数据(向量)user = np.array([1, 0, 1, 1, 0]) # 看了第1、3、4篇文章# 5个用户的行为数据(矩阵)users = np.array([ [1, 0, 1, 1, 0], # 用户A [0, 1, 1, 0, 1], # 用户B [1, 1, 0, 0, 1], # 用户C [0, 0, 1, 1, 1], # 用户D [1, 1, 1, 0, 0], # 用户E])print(f"矩阵形状: {users.shape}") # (5, 5) — 5个用户,5篇文章
import numpy as np# 用矩阵表示一张 8×8 的灰度图# 每个数字代表一个像素的亮度(0=黑,1=白)image = np.array([ [0, 0, 1, 1, 1, 1, 0, 0], [0, 1, 0, 0, 0, 0, 1, 0], [1, 0, 1, 0, 0, 1, 0, 1], [1, 0, 0, 0, 0, 0, 0, 1], [1, 0, 1, 0, 0, 1, 0, 1], [1, 0, 0, 1, 1, 0, 0, 1], [0, 1, 0, 0, 0, 0, 1, 0], [0, 0, 1, 1, 1, 1, 0, 0],])# 矩阵的每个元素就是一个像素print(f"图片矩阵形状: {image.shape}") # (8, 8)print(f"左上角像素: {image[0, 0]}") # 0(黑色)print(f"第一行像素: {image[0]}")# 矩阵运算:把图片变亮brighter = np.clip(image + 0.3, 0, 1)# 矩阵运算:水平翻转flipped = image[:, ::-1] # 列反转# 矩阵运算:裁剪左上角 4×4cropped = image[:4, :4]
矩阵乘法——AI 最重要的操作
import numpy as np# 一个简单的二维数据点point = np.array([1, 0]) # x 轴上的一个点# 旋转矩阵(逆时针旋转 90 度)rotation_90 = np.array([ [0, -1], [1, 0],])# 矩阵乘法 = 做变换rotated = rotation_90 @ point # [0, 1] — 旋转到了 y 轴上print(f"原始点: {point}")print(f"旋转 90° 后: {rotated}")# 缩放矩阵(x 方向放大 2 倍,y 方向缩小 0.5 倍)scale = np.array([ [2, 0], [0, 0.5],])scaled = scale @ point # [2, 0] — x 方向被拉长了print(f"缩放后: {scaled}")
import numpy as np# 神经网络的一层,本质上就是矩阵运算# 输入:128维向量(比如一个词的表示)# 权重矩阵:128 × 64(这一层有64个神经元)# 输出:64维向量(变换后的表示)np.random.seed(42)input_vector = np.random.randn(128) # 128维输入weight_matrix = np.random.randn(128, 64) # 权重矩阵bias = np.random.randn(64) # 偏置# 这就是一层的全部运算!output = input_vector @ weight_matrix + biasprint(f"输入形状: {input_vector.shape}") # (128,)print(f"权重形状: {weight_matrix.shape}") # (128, 64)print(f"输出形状: {output.shape}") # (64,)
为什么 AI 的每一步都是矩阵运算
- **数据输入**:图片 → 像素矩阵
- **特征提取**:矩阵 × 权重矩阵 = 新矩阵(卷积操作本质上也是矩阵乘法)
- **注意力计算**:Query 矩阵 × Key 矩阵的转置 = 注意力分数矩阵
- **输出预测**:最终矩阵 × 输出权重矩阵 = 预测结果
import numpy as np# 模拟一个简化版的 Transformer Attentionseq_len = 4 # 序列长度:4 个 tokend_model = 8 # 每个 token 用 8 维向量表示np.random.seed(42)X = np.random.randn(seq_len, d_model) # 输入序列# 三个权重矩阵W_Q = np.random.randn(d_model, d_model) # Query 权重W_K = np.random.randn(d_model, d_model) # Key 权重W_V = np.random.randn(d_model, d_model) # Value 权重# 矩阵乘法生成 Q、K、VQ = X @ W_Q # (4, 8)K = X @ W_K # (4, 8)V = X @ W_V # (4, 8)# Attention 分数 = Q × K^T(还是矩阵乘法!)scores = Q @ K.T # (4, 4)print(f"Attention 分数矩阵形状: {scores.shape}")# 4×4 — 每个 token 对其他所有 token 的关注程度# Softmax 归一化def softmax(x): e_x = np.exp(x - x.max(axis=-1, keepdims=True)) return e_x / e_x.sum(axis=-1, keepdims=True)attention_weights = softmax(scores)# 加权求和(又是矩阵乘法)attention_output = attention_weights @ V # (4, 8)print(f"Attention 输出形状: {attention_output.shape}")
四、特征值与特征向量——找到数据的"主心骨"
一个变换中的"不变量"
import numpy as np# 一个变换矩阵A = np.array([ [4, 1], [2, 3],])# 求特征值和特征向量eigenvalues, eigenvectors = np.linalg.eig(A)print("特征值:", eigenvalues) # 比如 [5, 2]print("特征向量:")print(eigenvectors)# 验证核心公式:A × v = λ × vfor i in range(len(eigenvalues)): v = eigenvectors[:, i] Av = A @ v lambda_v = eigenvalues[i] * v print(f"\n特征值 {eigenvalues[i]:.2f}: A×v ≈ λ×v? {np.allclose(Av, lambda_v)}")
为什么特征值能衡量"重要性"
手写 PCA——把数据压缩到最重要的方向
import numpy as np# 生成一些二维数据(沿着某个方向分布)np.random.seed(42)n = 200# 原始数据:沿 45 度方向拉长X = np.random.randn(n, 2) @ np.array([[3, 1], [1, 1]]) + np.array([5, 3])print(f"原始数据形状: {X.shape}") # (200, 2)# ---- 手写 PCA ----# 第一步:中心化(减去均值,让数据中心在原点)X_centered = X - X.mean(axis=0)# 第二步:计算协方差矩阵# 协方差矩阵衡量各个维度之间的相关性cov_matrix = (X_centered.T @ X_centered) / (n - 1)print(f"\n协方差矩阵:\n{np.round(cov_matrix, 2)}")# 第三步:求协方差矩阵的特征值和特征向量eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)# 第四步:按特征值从大到小排序(最重要的排前面)idx = np.argsort(eigenvalues)[::-1]eigenvalues = eigenvalues[idx]eigenvectors = eigenvectors[:, idx]print(f"\n特征值(从大到小): {np.round(eigenvalues, 2)}")print(f"每个特征值的信息占比: {np.round(eigenvalues / eigenvalues.sum(), 3)}")# 比如输出 [0.92, 0.08]# 说明第一个方向包含了 92% 的信息# 第五步:取前 k 个特征向量,做投影(降维)k = 1 # 只保留 1 个方向W = eigenvectors[:, :k] # 投影矩阵 (2, 1)X_pca = X_centered @ W # 降到 1 维 (200, 1)# 用降维后的数据重构回 2 维X_reconstructed = X_pca @ W.T + X.mean(axis=0)print(f"\n降维后形状: {X_pca.shape}") # (200, 1)print(f"重构后形状: {X_reconstructed.shape}") # (200, 2)# 计算重构误差error = np.mean((X - X_reconstructed) ** 2)print(f"重构均方误差: {error:.4f}")
五、奇异值分解(SVD)——万能的矩阵分解术
把矩阵拆成三个有意义的部分
- **U**:左奇异矩阵,描述了"行"(比如用户)的特征
- **Σ**:奇异值对角矩阵,每个值代表一个维度的重要性
- **V^T**:右奇异矩阵的转置,描述了"列"(比如商品)的特征
import numpy as np# 一个评分矩阵:5 个用户对 4 部电影的评分# 行 = 用户,列 = 电影,值 = 评分(0 表示没看过)R = np.array([ [5, 5, 0, 0], # 用户1:喜欢电影1和2 [5, 0, 3, 0], # 用户2:喜欢电影1和3 [3, 4, 0, 0], # 用户3:有点喜欢电影1和2 [0, 0, 5, 5], # 用户4:喜欢电影3和4 [0, 0, 4, 4], # 用户5:喜欢电影3和4])# SVD 分解U, S, Vt = np.linalg.svd(R, full_matrices=False)print(f"U 的形状: {U.shape}") # (5, 4) — 用户的特征矩阵print(f"S 的形状: {S.shape}") # (4,) — 奇异值print(f"Vt 的形状: {Vt.shape}") # (4, 4) — 电影的特征矩阵print(f"\n奇异值: {np.round(S, 2)}")print(f"奇异值占比: {np.round(S / S.sum(), 3)}")# 可能输出类似 [0.55, 0.32, 0.08, 0.05]# 前两个奇异值就占了 87% 的信息量
推荐系统——Netflix 竞赛背后的魔法
import numpy as np# 接着上面的例子R = np.array([ [5, 5, 0, 0], [5, 0, 3, 0], [3, 4, 0, 0], [0, 0, 5, 5], [0, 0, 4, 4],])U, S, Vt = np.linalg.svd(R, full_matrices=False)# 只保留前 k 个奇异值(低秩近似)k = 2U_k = U[:, :k] # (5, 2) — 用户在 2 个隐维度上的表示S_k = np.diag(S[:k]) # (2, 2) — 每个维度的重要性Vt_k = Vt[:k, :] # (2, 4) — 电影在 2 个隐维度上的表示# 重构评分矩阵(这是"预测"的评分!)R_approx = U_k @ S_k @ Vt_kprint("原始评分矩阵:")print(R)print("\nSVD 预测评分矩阵(保留 2 个奇异值):")print(np.round(R_approx, 1))# 给用户 1 推荐他没看过的电影user1_predicted = R_approx[0]unwatched = R[0] == 0 # 没看过的电影recommendations = np.where(unwatched)[0]print(f"\n用户1没看过的电影: {recommendations}")print(f"预测评分: {np.round(user1_predicted[recommendations], 1)}")# 推荐预测评分最高的那部电影
用 SVD 做图像压缩
import numpy as np# 创建一张 64×64 的测试图片(波纹图案)x = np.linspace(0, 1, 64)y = np.linspace(0, 1, 64)X_grid, Y_grid = np.meshgrid(x, y)image = np.sin(2 * np.pi * X_grid) * np.cos(2 * np.pi * Y_grid)# SVD 分解U, S, Vt = np.linalg.svd(image, full_matrices=False)print(f"完整奇异值数量: {len(S)}")# 看看前几个奇异值包含了多少信息cumsum = np.cumsum(S) / S.sum()print(f"前 5 个奇异值的累计信息: {cumsum[4]:.2%}")print(f"前 10 个奇异值的累计信息: {cumsum[9]:.2%}")print(f"前 20 个奇异值的累计信息: {cumsum[19]:.2%}")# 用不同的 k 值重构图片for k in [5, 10, 20]: U_k = U[:, :k] S_k = np.diag(S[:k]) Vt_k = Vt[:k, :] compressed = U_k @ S_k @ Vt_k # 计算压缩率和误差 original_size = 64 * 64 compressed_size = 64 * k + k + k * 64 # U_k + S_k + Vt_k ratio = compressed_size / original_size error = np.mean((image - compressed) ** 2) print(f"\nk={k}: 压缩率 {ratio:.1%}, 均方误差 {error:.6f}") print(f" 存储对比: 原始 {original_size} 个数 → 压缩后 {compressed_size} 个数")
SVD 和 PCA 的关系
import numpy as np# 用同一组数据验证np.random.seed(42)X = np.random.randn(200, 5) @ np.diag([3, 2, 1, 0.5, 0.1])# 方法1:PCA(通过协方差矩阵的特征分解)X_centered = X - X.mean(axis=0)cov = (X_centered.T @ X_centered) / (len(X) - 1)eigenvalues_pca = np.sort(np.linalg.eigvals(cov))[::-1]var_ratio_pca = eigenvalues_pca / eigenvalues_pca.sum()# 方法2:SVDU, S, Vt = np.linalg.svd(X_centered, full_matrices=False)# SVD 的奇异值² / (n-1) 就是 PCA 的特征值eigenvalues_svd = (S ** 2) / (len(X) - 1)var_ratio_svd = eigenvalues_svd / eigenvalues_svd.sum()print("PCA 的方差解释比:", np.round(var_ratio_pca, 3))print("SVD 的方差解释比:", np.round(var_ratio_svd, 3))# 两个结果一样!
六、张量——深度学习的数据容器
升维之路:标量 → 向量 → 矩阵 → 张量
- **标量(0D)**:一个数,比如 `42`
- **向量(1D)**:一列数,比如 `[1, 2, 3]`
- **矩阵(2D)**:一个表格,比如 `[[1,2], [3,4]]`
- **张量(3D+)**:更高维的数组
import torch# 标量 — 0 维张量scalar = torch.tensor(42)print(f"标量: {scalar}, 维度: {scalar.dim()}") # dim=0# 向量 — 1 维张量vector = torch.tensor([1, 2, 3, 4, 5])print(f"向量形状: {vector.shape}, 维度: {vector.dim()}") # dim=1# 矩阵 — 2 维张量matrix = torch.tensor([[1, 2, 3], [4, 5, 6]])print(f"矩阵形状: {matrix.shape}, 维度: {matrix.dim()}") # dim=2# 3 维张量(一张彩色图片)image = torch.randn(3, 224, 224) # 3 通道, 224×224print(f"彩色图片形状: {image.shape}, 维度: {image.dim()}") # dim=3# 4 维张量(一个 batch 的图片)batch_images = torch.randn(32, 3, 224, 224) # 32 张图片print(f"Batch 形状: {batch_images.shape}, 维度: {batch_images.dim()}") # dim=4# 5 维张量(一个 batch 的视频)batch_videos = torch.randn(8, 16, 3, 224, 224) # 8个视频,每个16帧print(f"视频 Batch 形状: {batch_videos.shape}, 维度: {batch_videos.dim()}") # dim=5
- **0D** 标量:`()` — 一个数
- **1D** 向量:`(512,)` — 一个特征向量
- **2D** 矩阵:`(batch, features)` — 表格数据
- **3D** 序列/图片:`(batch, seq_len, hidden)` 或 `(channels, height, width)`
- **4D** 图片 batch:`(batch, channels, height, width)`
- **5D** 视频 batch:`(batch, frames, channels, height, width)`
为什么 GPU 能加速张量运算
import torchimport time# 创建两个大矩阵size = 2000A = torch.randn(size, size)B = torch.randn(size, size)# CPU 计算start = time.time()C_cpu = A @ Bcpu_time = time.time() - startprint(f"CPU 矩阵乘法 ({size}×{size}): {cpu_time:.4f} 秒")# GPU 计算(如果有 GPU 的话)if torch.cuda.is_available(): A_gpu = A.cuda() B_gpu = B.cuda() # 预热(第一次运行会慢,不计入时间) _ = A_gpu @ B_gpu torch.cuda.synchronize() start = time.time() C_gpu = A_gpu @ B_gpu torch.cuda.synchronize() # 等GPU算完 gpu_time = time.time() - start print(f"GPU 矩阵乘法 ({size}×{size}): {gpu_time:.4f} 秒") print(f"GPU 加速比: {cpu_time / gpu_time:.1f}x")else: print("没有检测到 GPU,跳过 GPU 测试")
PyTorch 张量基础操作
import torch# ---- 创建张量 ----# 从列表创建t1 = torch.tensor([[1, 2], [3, 4]])# 创建全零、全一张量zeros = torch.zeros(3, 4) # 3×4 全零ones = torch.ones(2, 3, 5) # 2×3×5 全一# 随机初始化(最常用)randn = torch.randn(32, 3, 224, 224) # 一个 batch 的图片print(f"随机张量形状: {randn.shape}") # [32, 3, 224, 224]print(f"随机张量数据类型: {randn.dtype}") # torch.float32# ---- 张量运算 ----a = torch.randn(3, 4)b = torch.randn(4, 2)# 矩阵乘法c = a @ b # 或 torch.matmul(a, b)print(f"矩阵乘法结果形状: {c.shape}") # (3, 2)# 逐元素运算d = torch.randn(3, 4)e = torch.randn(3, 4)sum_de = d + e # 逐元素加法product_de = d * e # 逐元素乘法(注意!不是矩阵乘法)# ---- 形状操作(非常常用)----x = torch.randn(32, 3, 224, 224) # 图片 batch# 查看/改变形状print(f"原始形状: {x.shape}")flat = x.reshape(32, 3 * 224 * 224) # 展平print(f"展平后: {flat.shape}") # (32, 150528)restored = flat.reshape(32, 3, 224, 224) # 恢复# 转置(交换维度)# 把 (batch, channels, H, W) 变成 (batch, H, W, channels)transposed = x.permute(0, 2, 3, 1)print(f"转置后: {transposed.shape}") # (32, 224, 224, 3)# ---- 维度操作 ----# 添加维度x_unsqueezed = x.unsqueeze(1) # 在第1维添加一个维度print(f"unsqueeze: {x.shape} → {x_unsqueezed.shape}")# 压缩维度(去掉大小为1的维度)x_squeezed = x_unsqueezed.squeeze(1)print(f"squeeze: {x_unsqueezed.shape} → {x_squeezed.shape}")# ---- 广播机制 ----# 不同形状的张量运算时,自动扩展batch = torch.randn(32, 64) # 32个样本,每个64维bias = torch.randn(64) # 64维偏置result = batch + bias # 自动把 bias 扩展到 32×64print(f"广播结果: {result.shape}") # (32, 64)
一张图片、一段视频——从 3D 到 5D
import torch# ---- 一张灰度图 ----gray_image = torch.randn(28, 28) # 2D:28×28print(f"灰度图: {gray_image.shape}, 维度: {gray_image.dim()}")# ---- 一张彩色图 ----color_image = torch.randn(3, 224, 224) # 3D:3通道×224×224print(f"彩色图: {color_image.shape}, 维度: {color_image.dim()}")# ---- 一个 batch 的彩色图 ----batch = torch.randn(32, 3, 224, 224) # 4D:32张×3通道×224×224print(f"图片 batch: {batch.shape}, 维度: {batch.dim()}")# ---- 一段视频(50帧彩色图) ----video = torch.randn(50, 3, 224, 224) # 4D:50帧×3通道×224×224print(f"一段视频: {video.shape}, 维度: {video.dim()}")# ---- 一个 batch 的视频 ----video_batch = torch.randn(8, 50, 3, 224, 224) # 5D:8段×50帧×3通道×224×224print(f"视频 batch: {video_batch.shape}, 维度: {video_batch.dim()}")# 看看数据量num_elements = video_batch.numel()print(f"\n视频 batch 的总数据量: {num_elements:,} 个浮点数")print(f"占用内存(float32): {num_elements * 4 / 1024 / 1024:.1f} MB")
夜雨聆风