从特征值分解到 SVD,理解数据压缩的数学本质
开头:高维数据的「诅咒」
上一篇我们学习了正则化,防止过拟合。
今天我们学习主分量分析(PCA)——最经典的降维算法。
你有没有遇到过这种情况?
数据有 1000 个特征 → 计算量大 → 存储空间大 → 可视化困难 → 噪声多 PCA 的思想: 找到数据变化最大的方向(主成分) 只保留最重要的几个方向 → 降维 + 去噪 + 可视化一、PCA 的直觉
1.1 问题定义
PCA 要解决的问题 ═══════════════════════════════════════════════════════════════════ 输入:高维数据 X ∈ R^{n×d} n 个样本,d 个特征 输出:低维数据 Y ∈ R^{n×k} n 个样本,k 个特征(k << d) 目标: 找到数据变化最大的 k 个方向 保留最多的信息1.2 几何直觉
PCA 的几何直觉 ═══════════════════════════════════════════════════════════════════ 原始数据(2D): x₂ ↑ │ × × × │ × × × │ × × × │ × × × └──────────→ x₁ 数据主要沿着一个方向变化(对角线方向) PCA 找到这个方向: x₂ ↑ │ × × × │ × × × ←── 主成分方向(方差最大) │ × × × │ × × × └──────────→ x₁ 降维后(1D): 只保留主成分方向上的坐标 → 从 2D 降到 1D二、数学推导
2.1 协方差矩阵
协方差矩阵 ═══════════════════════════════════════════════════════════════════ 数据中心化: X_centered = X - mean(X) 协方差矩阵: C = (1/n) X_centeredᵀ X_centered C 是 d×d 的对称矩阵: C[i,j] = 第 i 个特征和第 j 个特征的协方差 C[i,i] = 第 i 个特征的方差2.2 特征值分解
特征值分解 ═══════════════════════════════════════════════════════════════════ 对协方差矩阵 C 做特征值分解: C = Q Λ Qᵀ 其中: Q = [q₁, q₂, ..., q_d] (特征向量矩阵) Λ = diag(λ₁, λ₂, ..., λ_d) (特征值对角矩阵) 特征值的含义: λᵢ = 第 i 个主成分方向上的方差 λ₁ ≥ λ₂ ≥ ... ≥ λ_d (按降序排列) 特征向量的含义: qᵢ = 第 i 个主成分方向2.3 降维
PCA 降维 ═══════════════════════════════════════════════════════════════════ 选择前 k 个主成分: Q_k = [q₁, q₂, ..., q_k] 降维: Y = X_centered @ Q_k 信息保留率: ratio = (λ₁ + λ₂ + ... + λ_k) / (λ₁ + λ₂ + ... + λ_d) 通常选择 k 使得 ratio > 95%(保留 95% 以上信息)三、Python 实现 PCA
3.1 从零实现
importnumpyasnpimportmatplotlib.pyplotaspltclassPCA:"""主分量分析(PCA)"""def__init__(self,n_components=2):""" 参数: n_components: 保留的主成分数量 """self.n_components=n_components self.components=Noneself.mean=Noneself.eigenvalues=Nonedeffit_transform(self,X):""" 拟合并转换数据 参数: X: 输入数据,形状 (n_samples, n_features) 返回: X_reduced: 降维后的数据,形状 (n_samples, n_components) """n_samples,n_features=X.shape# 步骤 1:中心化self.mean=np.mean(X,axis=0)X_centered=X-self.mean# 步骤 2:计算协方差矩阵C=np.cov(X_centered.T)# 步骤 3:特征值分解eigenvalues,eigenvectors=np.linalg.eigh(C)# 步骤 4:按特征值降序排列idx=np.argsort(eigenvalues)[::-1]eigenvalues=eigenvalues[idx]eigenvectors=eigenvectors[:,idx]# 步骤 5:选择前 k 个主成分self.components=eigenvectors[:,:self.n_components]self.eigenvalues=eigenvalues# 步骤 6:降维X_reduced=X_centered @ self.componentsreturnX_reduceddefexplained_variance_ratio(self):"""计算方差解释率"""returnself.eigenvalues[:self.n_components]/np.sum(self.eigenvalues)definverse_transform(self,X_reduced):"""逆变换(重构)"""returnX_reduced @ self.components.T+self.mean3.2 使用 SVD 实现(更稳定)
classPCA_SVD:"""使用 SVD 实现的 PCA(更稳定)"""def__init__(self,n_components=2):self.n_components=n_components self.components=Noneself.mean=Noneself.singular_values=Nonedeffit_transform(self,X):n_samples,n_features=X.shape# 步骤 1:中心化self.mean=np.mean(X,axis=0)X_centered=X-self.mean# 步骤 2:SVD 分解U,S,Vt=np.linalg.svd(X_centered,full_matrices=False)# 步骤 3:选择前 k 个主成分self.components=Vt[:self.n_components].T self.singular_values=S# 步骤 4:降维X_reduced=U[:,:self.n_components]*S[:self.n_components]returnX_reduceddefexplained_variance_ratio(self):"""方差解释率"""return(self.singular_values[:self.n_components]**2)/np.sum(self.singular_values**2)3.3 测试:二维数据降维
# 生成二维数据np.random.seed(42)n_samples=200# 生成有相关性的数据mean=[0,0]cov=[[1,0.8],[0.8,1]]# 高相关性X=np.random.multivariate_normal(mean,cov,n_samples)# PCA 降维pca=PCA(n_components=1)X_reduced=pca.fit_transform(X)print(f"原始数据形状:{X.shape}")print(f"降维后形状:{X_reduced.shape}")print(f"方差解释率:{pca.explained_variance_ratio():.2%}")# 可视化fig,(ax1,ax2)=plt.subplots(1,2,figsize=(12,4))ax1.scatter(X[:,0],X[:,1],alpha=0.5)ax1.set_title('Original Data (2D)')ax1.set_xlabel('x₁')ax1.set_ylabel('x₂')ax1.grid(True)ax2.scatter(X_reduced,np.zeros_like(X_reduced),alpha=0.5)ax2.set_title('Reduced Data (1D)')ax2.set_xlabel('Principal Component 1')ax2.grid(True)plt.tight_layout()plt.show()3.4 测试:高维数据可视化
fromsklearn.datasetsimportload_irisfromsklearn.preprocessingimportStandardScaler# 加载鸢尾花数据(4 维)iris=load_iris()X=iris.data y=iris.target# 标准化scaler=StandardScaler()X_scaled=scaler.fit_transform(X)# PCA 降到 2 维pca=PCA(n_components=2)X_2d=pca.fit_transform(X_scaled)print(f"原始数据:{X.shape}(4 维)")print(f"降维后:{X_2d.shape}(2 维)")print(f"方差解释率:{pca.explained_variance_ratio()}")# 可视化plt.figure(figsize=(10,6))foriinrange(3):mask=y==i plt.scatter(X_2d[mask,0],X_2d[mask,1],label=iris.target_names[i],alpha=0.7)plt.xlabel(f'PC1 ({pca.explained_variance_ratio()[0]:.1%})')plt.ylabel(f'PC2 ({pca.explained_variance_ratio()[1]:.1%})')plt.title('Iris Dataset - PCA Visualization')plt.legend()plt.grid(True)plt.show()四、奇异值分解(SVD)
4.1 SVD 的定义
奇异值分解(SVD) ═══════════════════════════════════════════════════════════════════ 对于任意矩阵 X ∈ R^{m×n}: X = U Σ Vᵀ 其中: U ∈ R^{m×m}:左奇异向量(正交矩阵) Σ ∈ R^{m×n}:奇异值对角矩阵 V ∈ R^{n×n}:右奇异向量(正交矩阵) 奇异值: σ₁ ≥ σ₂ ≥ ... ≥ σ_r > 0 r = rank(X)4.2 SVD 与 PCA 的关系
SVD 与 PCA 的关系 ═══════════════════════════════════════════════════════════════════ 对中心化数据 X_centered 做 SVD: X_centered = U Σ Vᵀ 协方差矩阵: C = (1/n) X_centeredᵀ X_centered = (1/n) V Σᵀ Uᵀ U Σ Vᵀ = (1/n) V Σ² Vᵀ 所以: V 的列就是 C 的特征向量(主成分方向) Σ²/n 就是 C 的特征值(方差) 结论: PCA 可以用 SVD 实现,而且更稳定4.3 为什么用 SVD 而不是特征值分解?
SVD vs 特征值分解 ═══════════════════════════════════════════════════════════════════ 特征值分解: 需要计算协方差矩阵 C = XᵀX / n 然后对 C 做特征值分解 SVD: 直接对 X 做分解 不需要显式计算 C 优点: - 数值更稳定 - 避免计算 XᵀX(可能有精度损失) - 对大规模数据更高效五、核 PCA
5.1 问题:PCA 只能处理线性数据
PCA 的局限性 ═══════════════════════════════════════════════════════════════════ 原始数据(非线性结构): ○ ○ ○ ○ ● ● ○ ○ ● ● ○ ○ ○ ○ PCA 只能找到线性方向: → 无法捕捉非线性结构5.2 核 PCA 的思想
核 PCA ═══════════════════════════════════════════════════════════════════ 核心思想: 用核函数映射到高维空间,再做 PCA 步骤: 1. 映射:x → φ(x) 2. 在高维空间做 PCA 3. 用核技巧避免显式映射 核矩阵: K[i,j] = K(x_i, x_j) = φ(x_i)ᵀ φ(x_j) 对核矩阵做特征值分解: K = U Λ Uᵀ 降维: Y = U_k √Λ_k5.3 Python 实现核 PCA
classKernelPCA:"""核 PCA"""def__init__(self,n_components=2,kernel='rbf',gamma=1.0):self.n_components=n_components self.kernel=kernel self.gamma=gamma self.eigenvectors=Noneself.eigenvalues=Noneself.X_fit=Nonedef_kernel_function(self,X1,X2):"""计算核矩阵"""ifself.kernel=='linear':returnX1 @ X2.Telifself.kernel=='rbf':sq_dists=np.sum(X1**2,axis=1).reshape(-1,1)+\ np.sum(X2**2,axis=1).reshape(1,-1)-\2*X1 @ X2.Treturnnp.exp(-self.gamma*sq_dists)elifself.kernel=='poly':return(X1 @ X2.T+1)**3deffit_transform(self,X):self.X_fit=X n_samples=X.shape[0]# 计算核矩阵K=self._kernel_function(X,X)# 中心化核矩阵one_n=np.ones((n_samples,n_samples))/n_samples K_centered=K-one_n @ K-K @ one_n+one_n @ K @ one_n# 特征值分解eigenvalues,eigenvectors=np.linalg.eigh(K_centered)# 按特征值降序排列idx=np.argsort(eigenvalues)[::-1]eigenvalues=eigenvalues[idx]eigenvectors=eigenvectors[:,idx]# 选择前 k 个self.eigenvalues=eigenvalues[:self.n_components]self.eigenvectors=eigenvectors[:,:self.n_components]# 降维X_reduced=self.eigenvectors*np.sqrt(self.eigenvalues)returnX_reduced5.4 测试:非线性数据
fromsklearn.datasetsimportmake_circles# 生成环形数据(非线性)X,y=make_circles(n_samples=200,noise=0.05,factor=0.3,random_state=42)# 线性 PCApca_linear=PCA(n_components=2)X_pca=pca_linear.fit_transform(X)# 核 PCAkpca=KernelPCA(n_components=2,kernel='rbf',gamma=15)X_kpca=kpca.fit_transform(X)# 可视化fig,(ax1,ax2)=plt.subplots(1,2,figsize=(12,5))ax1.scatter(X_pca[y==0,0],X_pca[y==0,1],alpha=0.7,label='Class 0')ax1.scatter(X_pca[y==1,0],X_pca[y==1,1],alpha=0.7,label='Class 1')ax1.set_title('Linear PCA')ax1.legend()ax1.grid(True)ax2.scatter(X_kpca[y==0,0],X_kpca[y==0,1],alpha=0.7,label='Class 0')ax2.scatter(X_kpca[y==1,0],X_kpca[y==1,1],alpha=0.7,label='Class 1')ax2.set_title('Kernel PCA (RBF)')ax2.legend()ax2.grid(True)plt.tight_layout()plt.show()六、工业应用
6.1 数据可视化
数据可视化 ═══════════════════════════════════════════════════════════════════ 高维数据(>3 维)无法直接可视化 → 用 PCA 降到 2D 或 3D 应用场景: - 数据探索:观察数据分布 - 聚类结果可视化 - 分类结果可视化6.2 去噪
去噪 ═══════════════════════════════════════════════════════════════════ 思想: 主成分方向包含主要信息 小特征值方向主要是噪声 步骤: 1. PCA 降维(保留主要成分) 2. 逆变换重构 效果: 去除噪声,保留主要信息6.3 特征提取
特征提取 ═══════════════════════════════════════════════════════════════════ 将原始特征映射到主成分空间: 原始特征:100 维 主成分特征:10 维 用于: - 机器学习预处理 - 加速训练 - 防止维度灾难6.4 AOI 中的应用
AOI 项目中的 PCA ═══════════════════════════════════════════════════════════════════ 图像预处理: - 降维加速训练 - 去除噪声 特征提取: - 将图像特征降到低维 - 用于分类/聚类 异常检测: - 正常样本在主成分空间聚集 - 异常样本偏离主成分空间七、避坑指南:使用 PCA 的 3 个陷阱
坑 1:不中心化 → 结果错误
错误做法:直接做 PCA
# ❌ 不中心化pca=PCA(n_components=2)X_reduced=pca.fit_transform(X)# X 未中心化# 结果错误!正确做法:先中心化
# ✅ 中心化X_centered=X-np.mean(X,axis=0)pca=PCA(n_components=2)X_reduced=pca.fit_transform(X_centered)坑 2:k 选择不当 → 信息丢失太多
错误做法:k 太小
# ❌ k 太小pca=PCA(n_components=1)# 原始 100 维,只保留 1 维X_reduced=pca.fit_transform(X)# 信息丢失太多正确做法:根据方差解释率选择 k
# ✅ 选择 k 使得方差解释率 > 95%pca=PCA(n_components=0.95)# 保留 95% 信息X_reduced=pca.fit_transform(X)print(f"保留{X_reduced.shape[1]}个主成分")坑 3:数据尺度差异大 → 需要标准化
错误做法:直接用原始数据
# ❌ 特征尺度差异大X=np.array([[1000,0.1],# 第一个特征很大[2000,0.2],[3000,0.3],])# PCA 会偏向大尺度特征正确做法:标准化
# ✅ 标准化fromsklearn.preprocessingimportStandardScaler scaler=StandardScaler()X_scaled=scaler.fit_transform(X)pca=PCA(n_components=2)X_reduced=pca.fit_transform(X_scaled)八、本篇总结
核心要点回顾
- PCA 的目标:找到数据变化最大的方向(主成分)
- 协方差矩阵:描述特征之间的相关性
- 特征值分解:找到主成分方向和方差
- SVD:更稳定的 PCA 实现
- 核 PCA:处理非线性数据的 PCA
- 方差解释率:衡量信息保留程度
下篇预告
下一篇我们学习自组织映射(SOM)。
PCA 是有监督的降维(需要标签),SOM 是无监督的聚类(不需要标签)。
下一篇你将学到:
- 竞争学习的原理
- Kohonen 网络的结构
- SOM 的学习算法
- 用 Python 手写 SOM
本期互动
你对 PCA 有什么看法?
- 你用过 PCA 吗?在什么场景下?
- 你觉得 PCA 和 t-SNE 有什么区别?
- 你知道 PCA 的哪些变种?
欢迎在评论区留言。
系列目录
| 篇 | 标题 | 状态 |
|---|---|---|
| 01 | Haykin 精讲开篇:从「只会调参」到「理解神经网络的灵魂」 | ✅ 完成 |
| 02 | 感知器:神经网络的「鼻祖」,为什么它能「学会」分类? | ✅ 完成 |
| 03 | LMS 算法:从最小二乘到随机梯度下降,工业自适应滤波的核心 | ✅ 完成 |
| 04 | 反向传播:神经网络为什么能「学习」?用 NumPy 手写 BP | ✅ 完成 |
| 05 | 核方法:为什么 SVM 能处理非线性问题?理解「升维」的本质 | ✅ 完成 |
| 06 | 支持向量机:最大间隔的「艺术」,为什么它是「小数据之王」? | ✅ 完成 |
| 07 | 正则化:为什么模型越复杂越容易过拟合?L1/L2/Dropout | ✅ 完成 |
| 08 | PCA:为什么降维能「去噪」?从特征值分解到核 PCA | ✅ 当前 |
| 09 | SOM:无监督学习的「聚类之王」,为什么它能「自组织」? | ⏳ 下一篇 |
| 10 | 信息论:为什么「信息最大化」能学特征?从熵到 ICA | ⏳ 待写 |
| 11 | 玻尔兹曼机:深度学习的「前世」,从统计力学到 RBM | ⏳ 待写 |
| 12 | 动态规划:强化学习的「数学基础」,从 MDP 到值迭代 | ⏳ 待写 |
| 13 | Hopfield 网络:联想记忆的「鼻祖」,为什么它能「回忆」? | ⏳ 待写 |
| 14 | 卡尔曼滤波:为什么它能「预测」?从贝叶斯推断到粒子滤波 | ⏳ 待写 |
| 15 | Haykin 精讲终篇:从感知器到深度学习——一部神经网络的「进化史」 | ⏳ 待写 |
点赞收藏转发,是我持续更新的动力!