从 0 开始的机器学习------NumPy 线性代数全攻略
目录
- 标量与向量(定义与基本运算)
- 矩阵基础(乘法、转置、逆、迹、行列式、秩)
- 机器学习中常用的矩阵操作(详细解释) ← 已加强
- 张量简介(高阶数组与轴)
- 矩阵求导与梯度(标量对向量、二次型、最小二乘)
- 数值验证(有限差分)
- 实战:线性回归的矩阵推导与实现
1. 标量与向量(不变,只改公式格式)
标量(Scalar) :单一数值,通常记为 a,b,αa, b, \alphaa,b,α。
向量(Vector) :一维数组,列向量记为 x∈Rn\mathbf{x} \in \mathbb{R}^nx∈Rn,例如:
x=[x1 x2 ⋮ xn] \mathbf{x} = \begin{bmatrix} x_1 \ x_2 \ \vdots \ x_n \end{bmatrix} x=[x1 x2 ⋮ xn]
常见操作:
- 向量加法:u+v=(u1+v1,...,un+vn)T\mathbf{u} + \mathbf{v} = (u_1+v_1,\dots,u_n+v_n)^Tu+v=(u1+v1,...,un+vn)T
- 数乘(标量乘向量):αv=(αv1,...,αvn)T\alpha\mathbf{v} = (\alpha v_1,\dots,\alpha v_n)^Tαv=(αv1,...,αvn)T
- 点积(内积):u⋅v=∑i=1nuivi=uTv\mathbf{u}\cdot\mathbf{v} = \sum_{i=1}^n u_i v_i = \mathbf{u}^T\mathbf{v}u⋅v=i=1∑nuivi=uTv
- 二范数(长度):∣x∣∗2=∑∗i=1nxi2|\mathbf{x}|*2 = \sqrt{\sum*{i=1}^n x_i^2}∣x∣∗2=∑∗i=1nxi2
python
import numpy as np
# 向量示例
u = np.array([1.0, 2.0, 3.0])
v = np.array([4.0, 5.0, 6.0])
print('u =', u)
print('v =', v)
print('\nu + v =', u + v)
print('2 * u =', 2 * u)
print('\nu · v =', np.dot(u, v))
print('\n||u||_2 =', np.linalg.norm(u))
2. 矩阵基础(只改公式格式)
矩阵记为 A∈Rm×nA\in\mathbb{R}^{m\times n}A∈Rm×n:
A=[a11a12...a1n a21a22...a2n ⋮⋮⋱⋮ am1am2...amn] A = \begin{bmatrix} a_{11} & a_{12} & \dots & a_{1n} \ a_{21} & a_{22} & \dots & a_{2n} \ \vdots & \vdots & \ddots & \vdots \ a_{m1} & a_{m2} & \dots & a_{mn} \end{bmatrix} A=[a11a12...a1n a21a22...a2n ⋮⋮⋱⋮ am1am2...amn]
常见操作:
- 加法:A+BA + BA+B(相同形状)
- 标量乘:αA\alpha AαA
- 矩阵乘法:C=AB,Cij=∑k=1pAikBkjC = AB,\quad C_{ij} = \sum_{k=1}^{p} A_{ik} B_{kj}C=AB,Cij=k=1∑pAikBkj,其中 A∈Rm×p,;B∈Rp×nA\in\mathbb{R}^{m\times p},; B\in\mathbb{R}^{p\times n}A∈Rm×p,;B∈Rp×n。
- 转置:ATA^TAT,满足 (AB)T=BTAT(AB)^T = B^T A^T(AB)T=BTAT。
- 逆矩阵(若存在):A−1A^{-1}A−1 使得 AA−1=IA A^{-1} = IAA−1=I。
- 迹(trace):tr(A)=∑iAii\operatorname{tr}(A) = \sum_{i} A_{ii}tr(A)=i∑Aii。
- 行列式(determinant):det(A)\det(A)det(A),判断方阵可逆性的一个标志:若 det(A)≠0\det(A)\neq 0det(A)=0 则方阵可逆。
python
# 矩阵示例
A = np.array([[1.,2.],[3.,4.]])
B = np.array([[5.,6.],[7.,8.]])
print('A =\n', A)
print('\nB =\n', B)
print('\nA + B =\n', A + B)
print('\nA @ B =\n', A @ B)
print('\nA.T =\n', A.T)
print('\ninv(A) =\n', np.linalg.inv(A))
print('\ndet(A) =', np.linalg.det(A))
print('\ntrace(A) =', np.trace(A))
3. 机器学习中常用的矩阵操作(更详细、带直觉与实践建议)
下面把每个常用操作讲清楚:做什么、为什么这样做、数值与计算注意点、对应 NumPy 操作。
3.1 批次向量化(Batching)------把循环变成矩阵乘法
做什么 :把多个样本堆成一个矩阵,一次性做运算。例如 N 个样本、每个样本 d 个特征,堆成矩阵 X∈RN×dX\in\mathbb{R}^{N\times d}X∈RN×d,参数是 w∈Rdw\in\mathbb{R}^dw∈Rd,则模型预测(对每个样本)可以写成:
y^=Xw∈RN \hat{y} = X w \in \mathbb{R}^N y^=Xw∈RN
为什么:矩阵乘法把对每个样本的重复计算整合成高度优化的 BLAS/向量化 操作,比 Python 循环快很多(尤其是大数据量时)。另外,把样本放在第一维方便做批归一化、并行计算梯度。
数值注意:
- 如果 N 很大但 d 也大,内存可能不够,通常做小批次(mini-batch)。
- 保持数据类型一致(float32 常用于深度学习,能节省内存并加速)。
NumPy:
python
# X: N x d, w: d
y_hat = X @ w # 或 np.dot(X, w)
3.2 中心化与协方差矩阵(为什么要减均值?)
做什么 :协方差衡量特征之间的线性相关性。给定样本矩阵 X∈RN×dX\in\mathbb{R}^{N\times d}X∈RN×d(每行一个样本),先将每列减去均值(中心化):
Xˉ=X−1μT,μ=1N∑i=1NXi⋅ \bar{X} = X - \mathbf{1}\mu^T, \quad \mu = \frac{1}{N}\sum_{i=1}^N X_{i\cdot} Xˉ=X−1μT,μ=N1i=1∑NXi⋅
协方差矩阵定义(样本协方差,Bessel 校正):
Σ=1N−1XˉTXˉ∈Rd×d \Sigma = \frac{1}{N-1}\bar{X}^T \bar{X} \in \mathbb{R}^{d\times d} Σ=N−11XˉTXˉ∈Rd×d
为什么中心化:协方差反映的是"变量围绕均值的共同变化"。如果不减均值,方差/协方差会被均值偏移影响,从而不能正确衡量变量之间的真实线性关系。
直觉:
- 对角线 Σii\Sigma_{ii}Σii = 第 i 个特征的方差(越大说明该特征在样本中波动越大)。
- 非对角项 Σij\Sigma_{ij}Σij = 第 i、j 特征的协方差(正则同向变化,负则反向变化)。
NumPy(常用写法):
python
X_centered = X - X.mean(axis=0)
Sigma = (X_centered.T @ X_centered) / (X_centered.shape[0] - 1)
# 或直接用 np.cov(X, rowvar=False, bias=False)
实践建议:
- PCA 之前务必中心化数据。
- 若特征量纲差异大,常先做标准化(减均值再除以标准差)。
3.3 奇异值分解(SVD)------最通用的分解工具
公式 :任意矩阵 A∈Rm×nA\in\mathbb{R}^{m\times n}A∈Rm×n 都可分解为
A=UΣVT A = U \Sigma V^T A=UΣVT
其中 U∈Rm×mU\in\mathbb{R}^{m\times m}U∈Rm×m、V∈Rn×nV\in\mathbb{R}^{n\times n}V∈Rn×n 正交(列向量两两正交),Σ\SigmaΣ 为对角(奇异值)。
为什么有用:
- SVD 给出矩阵固有的"能量"分布(奇异值越大,表示对应方向重要)。
- 最低秩近似:取前 r 个奇异值,可以得到最佳的秩 r 近似(Frobenius 范数最小)。
- 用于稳定解线性系统(伪逆)、PCA(通过对中心化数据做 SVD 得到主成分)。
直觉 :SVD 把输入空间分成三个步骤:先把右侧空间旋转(VTV^TVT),再按不同方向放缩(Σ\SigmaΣ),最后在输出空间旋转(UUU)。
NumPy:
python
U, S, VT = np.linalg.svd(A, full_matrices=False)
# S 是向量,实际对角矩阵是 np.diag(S)
A_recon = U @ np.diag(S) @ VT
数值与效率:
- 对大矩阵,使用
scipy.sparse.linalg.svds或randomized SVD(sklearn.utils.extmath.randomized_svd)求前 k 个奇异值更快且内存友好。 - SVD属于 O(m n min(m,n)) 的复杂度(很贵),对大矩阵需慎用。
3.4 特征分解(Eigen-decomposition)------只对方阵有用
公式 (方阵):若 A∈Rn×nA\in\mathbb{R}^{n\times n}A∈Rn×n,若存在可逆矩阵 QQQ 使:
A=QΛQ−1 A = Q \Lambda Q^{-1} A=QΛQ−1
其中 Λ\LambdaΛ 对角(特征值),列向量是特征向量。
若 AAA 是对称矩阵(真实且对称),则有正交对角化:
A=QΛQT A = Q \Lambda Q^T A=QΛQT
为什么重要:
- 对称矩阵的特征值/特征向量描述"自然振型"(物理类比),在 PCA、谱聚类、Markov 链分析中广泛使用。
- 与 SVD 的关系:对中心化数据的协方差矩阵 Σ\SigmaΣ 做特征分解,与对原始数据做 SVD 得到的右奇异向量等价(两者在 PCA 中是一致的)。
NumPy:
python
eigvals, eigvecs = np.linalg.eig(A) # 任意方阵
eigvals_sym, eigvecs_sym = np.linalg.eigh(SymmetricA) # 对称矩阵更稳定
数值提示:
- 对称矩阵用
eigh更稳定、更快。 - 特征值靠近的情况下(重根)。数值不稳定,要小心解释特征向量(方向可能旋转)。
3.5 低秩近似(为什么有用?)
做什么:用少数几个基向量近似原矩阵,去噪、压缩或加速计算。
数学:用 SVD 的前 r 项得到:
A≈UrΣrVrT A \approx U_r \Sigma_r V_r^T A≈UrΣrVrT
这是在 Frobenius 范数意义下的最优秩 r 近似。
为什么在机器学习有用:
- PCA 用于降维:保留大部分数据方差同时减少维度。
- 推荐系统(SVD 低秩分解)用于捕捉隐含因子。
- 加速矩阵乘法或构造低秩预条件子。
3.6 矩阵逆与伪逆(数值稳定性)
正规方程与逆:最小二乘闭式解常写成:
w∗=(XTX)−1XTy w^* = (X^T X)^{-1} X^T y w∗=(XTX)−1XTy
为什么要小心:
- XTXX^T XXTX 可能病态(接近奇异),直接求逆会放大误差。
- 更稳健的做法是用 QR 分解或 SVD /
np.linalg.lstsq:
python
w, residuals, rank, s = np.linalg.lstsq(X, y, rcond=None)
# 或者 w = np.linalg.pinv(X.T @ X) @ X.T @ y (尽量用 lstsq/pinv)
伪逆(Moore-Penrose):
X+=VΣ+UT X^{+} = V \Sigma^{+} U^T X+=VΣ+UT
对于欠定/过定系统,伪逆给出最小范数解。
3.7 广播(Broadcasting)与维度对齐(实践常见问题)
做什么:NumPy 的广播规则允许不同形状数组参与运算,只要从右到左对应维度要么相等,要么其中一方为 1。
为什么重要 :在批量计算、加偏置、元素级操作时非常方便。例如给每行加同一个偏置向量 b∈Rdb\in\mathbb{R}^db∈Rd:
python
# X: N x d, b: d,
X + b # 自动把 b 广播成 N x d
小心点:
- 广播不会复制大量数据,但会在内存视图上进行操作;某些情况下会产生临时大数组(例如先扩展再做复杂计算),注意内存峰值。
- 推荐明确 reshape/keepdims 保持可读性。
3.8 常见矩阵恒等式
把这些常用的恒等式记住会非常有帮助:
- (AB)T=BTAT(AB)^T = B^T A^T(AB)T=BTAT
- tr(AB)=tr(BA)\operatorname{tr}(AB) = \operatorname{tr}(BA)tr(AB)=tr(BA)(只要两个乘积都定义)
- ddw(wTAw)=(A+AT)w\frac{d}{dw} (w^T A w) = (A + A^T) wdwd(wTAw)=(A+AT)w(二次型的一般表达)
- 如果 AAA 对称,则 ddw(12wTAw)=Aw\frac{d}{dw} ( \tfrac12 w^T A w ) = A wdwd(21wTAw)=Aw
这些恒等式常用于快速推导梯度与实现。
3.9 小结(何时用哪个工具)
- 想做降维 / PCA / 低秩近似 → 用 SVD(对大数据用 randomized SVD)。
- 想解最小二乘 → 首选
np.linalg.lstsq或使用 QR 分解,而不是直接求逆。 - 想分析协方差 / 特征 → 中心化后做 eigh(对称矩阵)。
- 想做隐因子模型或压缩 → 低秩分解(SVD)或矩阵分解(如 NMF、ALS 等)。
- 想做批量预测 → 把样本堆成矩阵 XXX,用矩阵乘法一次算完。
python
# 批次运算与协方差
np.random.seed(0)
X = np.random.randn(5,3) # 5 samples, 3 features
w = np.array([0.5, -1.0, 2.0])
print('X =\n', X)
print('\nX @ w =', X @ w)
# 中心化并计算协方差
X_centered = X - X.mean(axis=0)
Sigma = (X_centered.T @ X_centered) / (X_centered.shape[0]-1)
print('\nCovariance matrix Sigma =\n', Sigma)
# SVD
A = np.random.randn(4,3)
U, S, VT = np.linalg.svd(A, full_matrices=False)
print('\nA shape:', A.shape)
print('U shape:', U.shape, 'S shape:', S.shape, 'VT shape:', VT.shape)
print('\nReconstructed A from SVD =\n', U @ np.diag(S) @ VT)
# Eigen decomposition (symmetric)
M = Sigma
eigvals, eigvecs = np.linalg.eigh(M)
print('\nEigenvalues of Sigma =', eigvals)
4. 张量
张量是高阶数组:
- 标量是 0 阶张量
- 向量是 1 阶张量
- 矩阵是 2 阶张量
例如张量 T∈R2×3×4T\in\mathbb{R}^{2\times3\times4}T∈R2×3×4 有 3 个轴(axes),长度分别为 2、3、4。einsum 仍然是表达复杂张量操作的利器,写法示例完全用 $$ 包围公式。
5. 矩阵求导
二次型:
f(x)=12xTAx⇒∇xf=12(A+AT)x f(x) = \frac{1}{2} x^T A x \quad\Rightarrow\quad \nabla_x f = \frac{1}{2}(A + A^T)x f(x)=21xTAx⇒∇xf=21(A+AT)x
对称时简化为:
∇xf=Ax \nabla_x f = A x ∇xf=Ax
线性回归最小二乘:
L(w)=12∣Xw−y∣22=12(Xw−y)T(Xw−y) L(w) = \frac{1}{2}|X w - y|_2^2 = \frac{1}{2}(Xw - y)^T (Xw - y) L(w)=21∣Xw−y∣22=21(Xw−y)T(Xw−y)
梯度:
∇wL=XT(Xw−y) \nabla_w L = X^T (X w - y) ∇wL=XT(Xw−y)
python
# 张量操作示例
T = np.arange(24).reshape(2,3,4)
print('T shape =', T.shape)
print('T =\n', T)
# einsum 举例:对最后一轴求和,得到矩阵
M = np.einsum('ijk->ij', T)
print('\nSum over axis 2 => shape', M.shape)
# 使用 einsum 实现矩阵乘法
A = np.random.randn(2,3)
B = np.random.randn(3,4)
C = np.einsum('ik,kj->ij', A, B)
print('\nCheck einsum matmul equals @ :', np.allclose(C, A @ B))
6. 数值验证(有限差分)
有限差分的公式(用于数值验证):
∂f∂xi≈f(x+εei)−f(x−εei)2ε \frac{\partial f}{\partial x_i} \approx \frac{f(x + \varepsilon e_i) - f(x - \varepsilon e_i)}{2\varepsilon} ∂xi∂f≈2εf(x+εei)−f(x−εei)
python
# 数值验证:二次型与最小二乘梯度
import numpy as np
def numeric_grad(f, x, eps=1e-6):
x = x.astype(float)
grad = np.zeros_like(x)
for i in range(x.size):
orig = x.flat[i]
x.flat[i] = orig + eps
f1 = f(x)
x.flat[i] = orig - eps
f2 = f(x)
x.flat[i] = orig
grad.flat[i] = (f1 - f2) / (2*eps)
return grad
# 二次型
A = np.array([[3.,1.],[1.,2.]])
x = np.array([1.0, 2.0])
f = lambda z: 0.5 * z.T @ A @ z
analytical = A @ x
numeric = numeric_grad(lambda z: f(z), x)
print('Analytical grad (Ax) =', analytical)
print('Numeric grad =', numeric)
print('Close?', np.allclose(analytical, numeric, atol=1e-5))
# 最小二乘
np.random.seed(1)
X = np.random.randn(6,3)
w = np.random.randn(3)
y = X @ w + 0.1 * np.random.randn(6)
L = lambda ww: 0.5 * np.linalg.norm(X @ ww - y)**2
analytical_w = X.T @ (X @ w - y)
numeric_w = numeric_grad(lambda z: L(z), w)
print('\nAnalytical grad (least squares) =', analytical_w)
print('Numeric grad =', numeric_w)
print('Close?', np.allclose(analytical_w, numeric_w, atol=1e-5))