
文章目录
课前导读
欢迎来到第26课!在过去的课程中,我们处理的数据通常只有几个或几十个特征。但真实世界的数据往往高得惊人:一张 512×512 的灰度图像就有 26 万维;基因表达数据可能有 2 万个特征;文本经过独热编码后可达数十万维。直接在这些高维数据上训练模型,不仅训练极慢、内存爆炸,而且由于“维度灾难”,样本在高维空间中变得稀疏,距离度量几乎失效。
降维是解决高维问题的核心手段。本课的主角——主成分分析(PCA),是最常用、最强大的线性降维方法。它的思想很巧妙:找到数据中“信息量最大”的方向(即方差最大的方向),将数据投影到这些方向上,丢弃那些方差小的方向(可以视为噪声)。PCA 不仅能压缩数据、加速后续算法,还能可视化高维数据(降到 2D/3D),甚至去噪和防止过拟合。本课将带你从零推导 PCA 的数学过程,学会用 sklearn 的 PCA 优雅降维,掌握累计方差解释率的解读、主成分个数的选择,以及白化(whitening)的高级用法。学完本课,你将能轻松应对高维数据挑战。
学习目标
完成本课学习后,你将能够:
- 解释 PCA 的核心思想:将数据投影到方差最大的方向上,实现降维
- 描述 PCA 的数学步骤:中心化 → 计算协方差矩阵 → 特征值分解 → 选择前 k 个特征向量
- 使用
PCA进行降维,理解关键参数:n_components、svd_solver、whiten、random_state - 确定 主成分数量:根据累计方差贡献率(如 95%)或指定整数维度
- 理解 白化(whitening)的作用:使降维后的特征具有单位方差且不相关,常用于信号处理
- 可视化 高维数据降维后的 2D/3D 散点图
- 应用 PCA 作为数据预处理步骤集成到机器学习 Pipeline 中,并比较降维前后的模型性能
知识点理论讲解
一、为什么需要降维?
高维数据带来的问题:
- 计算复杂度:O(N·d) 甚至更高,训练缓慢。
- 存储开销:内存消耗大。
- 维度灾难:随着维度 d 增加,样本在空间中越来越稀疏,基于距离的算法(KNN、KMeans)失效。
- 多重共线性:特征间可能高度相关,浪费信息。
- 过拟合:模型参数过多,容易捕捉噪声。
降维可以在保留主要信息的同时,缓解上述问题。PCA 通过线性变换将原始特征压缩为少数几个不相关的主成分。
二、PCA 的直观理解
PCA 试图找到一组新的正交坐标系(主成分),使得:
- 第一主成分(PC1)的方向上,数据的方差最大(即信息量最大)。
- 第二主成分(PC2)在与 PC1 正交的方向上,方差次大。
- 以此类推,每个后续主成分都在前面所有主成分正交的方向上最大化剩余方差。
因此,PCA 本质上是方差最大化的投影。
几何解释:将原始数据投影到一个超平面上,使得投影点到原投影点的距离平方和(即投影后的方差)最大,等价于最小化投影误差(点到超平面的距离平方和)。
三、PCA 的数学原理
假设我们有中心化的数据矩阵 X X X( n n n 个样本, d d d 个特征,每列均值为 0)。
目标:找到单位向量 u 1 u_1 u1,使得投影 X u 1 X u_1 Xu1 的方差最大。投影方差为 u 1 T Σ u 1 u_1^T \Sigma u_1 u1TΣu1,其中 Σ = 1 n − 1 X T X \Sigma = \frac{1}{n-1} X^T X Σ=n−11XTX 是协方差矩阵(有时用 X T X X^T X XTX 除以 n)。
通过拉格朗日乘子法,该问题等价于求解 Σ u 1 = λ 1 u 1 \Sigma u_1 = \lambda_1 u_1 Σu1=λ1u1,即 u 1 u_1 u1 是 Σ \Sigma Σ 的最大特征值对应的特征向量,方差 λ 1 \lambda_1 λ1 即为特征值。
步骤:
- 中心化: X c e n t e r e d = X − μ X_{centered} = X - \mu Xcentered=X−μ(每个特征减去均值)。
- 计算协方差矩阵: C = 1 n − 1 X c e n t e r e d T X c e n t e r e d C = \frac{1}{n-1} X_{centered}^T X_{centered} C=n−11XcenteredTXcentered。
- 特征分解: C = V Λ V T C = V \Lambda V^T C=VΛVT,其中 V V V 是特征向量矩阵(主成分方向), Λ \Lambda Λ 是对角阵(特征值)。
- 选择前 k 个最大特征值对应的特征向量,构成投影矩阵 W k W_k Wk( d × k d \times k d×k)。
- 降维: X n e w = X c e n t e r e d W k X_{new} = X_{centered} W_k Xnew=XcenteredWk( n × k n \times k n×k)。
方差解释率:第 i 个主成分的方差贡献率为 λ i / ∑ j = 1 d λ j \lambda_i / \sum_{j=1}^d \lambda_j λi/∑j=1dλj。累计方差贡献率用于选择 k。
四、PCA 与 SVD 的关系
实际计算中,协方差矩阵的特征分解可能数值不稳定。scikit-learn 的 PCA 使用奇异值分解(SVD):
X
c
e
n
t
e
r
e
d
=
U
Σ
V
T
X_{centered} = U \Sigma V^T
Xcentered=UΣVT,则
V
V
V 的列就是主成分方向,
Σ
2
/
(
n
−
1
)
\Sigma^2 / (n-1)
Σ2/(n−1) 对应特征值。SVD 更稳定高效,且能处理高维情况(通过 svd_solver 参数)。
五、白化(Whitening)
白化是对降维后的数据做进一步变换,使得:
- 各主成分的方差归一化为 1。
- 主成分之间不相关(已经正交,本身不相关)。
在 sklearn 中,whiten=True 会将降维后的数据除以对应主成分的标准差,即
X
w
h
i
t
e
n
=
X
n
e
w
/
λ
i
X_{whiten} = X_{new} / \sqrt{\lambda_i}
Xwhiten=Xnew/λi(每个特征独立缩放)。白化后的数据满足各维度均值为 0、方差为 1、协方差为单位矩阵。
用途:
- 使后续算法(如 KMeans、SVM)对特征尺度不敏感。
- 常用于独立成分分析(ICA)预处理。
- 在图像处理中可去除像素间的相关性,突出边缘。
注意:白化会放大噪声(因为方差小的成分也会被拉伸),通常只保留足够的主成分后再白化。
六、PCA 的优缺点
| 优点 | 缺点 |
|---|---|
| 线性方法,计算高效 | 只能捕捉线性结构,对非线性数据效果差 |
| 降维后可解释:主成分是原始特征的线性组合 | 主成分通常难以解释具体业务含义 |
| 去除特征相关性 | 对异常值敏感(因依赖协方差) |
| 可用方差解释率定量评估信息损失 | 失去原始特征的稀疏性 |
| 可与监督模型无缝集成 | 需要数据标准化(中心化) |
核心原理通俗拆解
找一个最“分散”的方向
想象你有一堆三维空间中的点,你想把它们拍扁到二维平面上,同时保留尽可能多的“形状”。你会选择哪个平面?PCA 告诉你,应该选择使投影点尽可能分散的平面。因为分散的方向包含了更多的区分信息(方差大)。这就像给一个西瓜拍 X 光片,你会选择角度让西瓜的形状最清晰。
主成分:数据的主脉搏
如果数据像一根瘦长的黄瓜,第一主成分就是沿着黄瓜的方向;第二主成分就是垂直于黄瓜的方向(黄瓜的粗细)。你只需记住黄瓜的长度和粗度这两个数,就能近似恢复整个黄瓜的形状。PCA 就是找出这种“主脉搏”。
底层数学逻辑
1. 协方差矩阵的特征分解
协方差矩阵 C C C 是对称半正定的,可以正交对角化。特征值 λ i \lambda_i λi 是各主成分上的方差,特征向量 v i v_i vi 是主成分方向。
2. 投影后的方差
投影到 u u u 方向后的样本方差 = u T C u u^T C u uTCu。
3. 累计方差解释率
explained_variance_ratio i = λ i / ∑ j = 1 d λ j \text{explained\_variance\_ratio}_i = \lambda_i / \sum_{j=1}^d \lambda_j explained_variance_ratioi=λi/∑j=1dλj。选择使累计和 ≥ 0.95 \ge 0.95 ≥0.95 的最小 k。
scikit-learn API详解
PCA
from sklearn.decomposition import PCA
pca = PCA(
n_components=None, # 整数(降维后维数)或浮点数(保留方差比例,如0.95)
copy=True, # 是否复制数据
whiten=False, # 是否白化
svd_solver='auto', # 'auto', 'full', 'arpack', 'randomized'
tol=0.0, # 浮点数精度
iterated_power='auto', # 随机 SVD 的迭代次数
random_state=None
)
# 拟合
pca.fit(X) # 学习主成分
# 降维
X_reduced = pca.transform(X) # 投影到主成分空间
# 或一步完成
X_reduced = pca.fit_transform(X)
# 逆变换(近似重建)
X_reconstructed = pca.inverse_transform(X_reduced)
# 属性
pca.components_ # 主成分方向(形状 (n_components, n_features))
pca.explained_variance_ # 各主成分的方差(特征值)
pca.explained_variance_ratio_ # 方差贡献率
pca.singular_values_ # 奇异值
pca.mean_ # 训练集均值(用于中心化)
pca.n_components_ # 实际保留的主成分数
环境配置与依赖安装
本课使用已有环境,需要 matplotlib 和 numpy。
conda activate sklearn_tutorial
python -c "import sklearn; print(sklearn.__version__)"
完整代码实战(带详细注释)
实战1:PCA 基本步骤与手动对比
# -*- coding: utf-8 -*-
"""
手动实现 PCA(使用特征分解),与 sklearn PCA 对比
"""
import numpy as np
from sklearn.decomposition import PCA
# 生成简单二维数据
np.random.seed(42)
X = np.dot(np.random.randn(200, 2), np.array([[2, 1], [1, 2]])) # 产生相关结构
# 1. 中心化
X_centered = X - X.mean(axis=0)
# 2. 计算协方差矩阵
C = np.cov(X_centered.T)
# 3. 特征分解
eig_vals, eig_vecs = np.linalg.eig(C)
# 按特征值降序排列
idx = np.argsort(eig_vals)[::-1]
eig_vals = eig_vals[idx]
eig_vecs = eig_vecs[:, idx]
# 4. 选择前 k=1 个主成分
k = 1
W = eig_vecs[:, :k]
X_pca_manual = X_centered @ W
# sklearn PCA
pca = PCA(n_components=k)
X_pca_sk = pca.fit_transform(X)
print("手动计算的主成分方向(单位向量):\n", W.T)
print("sklearn 主成分方向:\n", pca.components_)
print("手动计算的方差解释率:", eig_vals[:k] / eig_vals.sum())
print("sklearn 方差解释率:", pca.explained_variance_ratio_)
# 重建(逆变换)
X_recon_manual = X_pca_manual @ W.T + X.mean(axis=0)
X_recon_sk = pca.inverse_transform(X_pca_sk)
print("重建误差(手动):", np.mean((X - X_recon_manual)**2))
print("重建误差(sklearn):", np.mean((X - X_recon_sk)**2))
实战2:选择主成分数量(累计方差解释率曲线)
# -*- coding: utf-8 -*-
"""
绘制累计方差贡献率曲线,确定 n_components
使用手写数字数据集(64维)
"""
from sklearn.datasets import load_digits
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
import numpy as np
digits = load_digits()
X = digits.data
y = digits.target
# 拟合 PCA(不降维)
pca = PCA()
pca.fit(X)
# 累计方差解释率
cumsum = np.cumsum(pca.explained_variance_ratio_)
plt.figure(figsize=(8,5))
plt.plot(range(1, len(cumsum)+1), cumsum, 'bo-')
plt.axhline(y=0.95, color='r', linestyle='--', label='95% 方差')
plt.axvline(x=np.argmax(cumsum >= 0.95) + 1, color='g', linestyle='--', label='95% 对应的维数')
plt.xlabel('主成分数量')
plt.ylabel('累计方差贡献率')
plt.title('PCA 累计方差解释率')
plt.legend()
plt.grid(True)
plt.show()
# 保留 95% 方差的维数
n_components_95 = np.argmax(cumsum >= 0.95) + 1
print(f"保留 95% 方差需要 {n_components_95} 个主成分")
# 使用该维数进行降维
pca_95 = PCA(n_components=0.95)
X_95 = pca_95.fit_transform(X)
print(f"原始维度: {X.shape[1]}, 降维后: {X_95.shape[1]}")
实战3:PCA 可视化高维数据(降到2D)
# -*- coding: utf-8 -*-
"""
将手写数字降到2维,并用不同颜色显示类别
"""
from sklearn.datasets import load_digits
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
digits = load_digits()
X, y = digits.data, digits.target
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X)
plt.figure(figsize=(10, 8))
scatter = plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y, cmap='tab10', alpha=0.6, edgecolors='k')
plt.colorbar(scatter, label='数字类别')
plt.xlabel(f'第一主成分 (方差贡献率 {pca.explained_variance_ratio_[0]:.2%})')
plt.ylabel(f'第二主成分 (方差贡献率 {pca.explained_variance_ratio_[1]:.2%})')
plt.title('手写数字 PCA 降维到 2D')
plt.show()
实战4:白化(whiten)的效果
# -*- coding: utf-8 -*-
"""
对比白化前后的数据分布(使用鸢尾花数据)
白化使各主成分方差为1,更适合后续聚类或分类
"""
from sklearn.datasets import load_iris
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
X, y = load_iris(return_X_y=True)
pca_normal = PCA(n_components=2, whiten=False)
X_pca_normal = pca_normal.fit_transform(X)
pca_white = PCA(n_components=2, whiten=True)
X_pca_white = pca_white.fit_transform(X)
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
axes[0].scatter(X_pca_normal[:, 0], X_pca_normal[:, 1], c=y, cmap='viridis')
axes[0].set_title(f'标准 PCA (方差: {pca_normal.explained_variance_})')
axes[0].set_xlabel('PC1')
axes[0].set_ylabel('PC2')
axes[1].scatter(X_pca_white[:, 0], X_pca_white[:, 1], c=y, cmap='viridis')
axes[1].set_title(f'白化 PCA (方差: {pca_white.explained_variance_})')
axes[1].set_xlabel('PC1 (白化)')
axes[1].set_ylabel('PC2 (白化)')
plt.show()
print("白化后各主成分方差被归一化为 1(近似)")
实战5:PCA 重建人脸图像(去噪效果)
# -*- coding: utf-8 -*-
"""
使用 PCA 重建人脸,展示降维后图像质量
数据集:Olivetti Faces(需网络下载)
"""
from sklearn.datasets import fetch_olivetti_faces
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
faces = fetch_olivetti_faces(shuffle=True, random_state=42)
X = faces.data
# 取第一个人的前几张图像
first_person_idx = 0
X_person = X[first_person_idx * 10: (first_person_idx+1) * 10] # 每人10张
pca = PCA(n_components=50) # 从 4096 降到 50
X_pca = pca.fit_transform(X_person)
X_recon = pca.inverse_transform(X_pca)
# 显示原始和重建图像
fig, axes = plt.subplots(2, 5, figsize=(10, 4))
for i in range(5):
axes[0, i].imshow(X_person[i].reshape(64, 64), cmap='gray')
axes[0, i].set_title(f'原图{i+1}')
axes[0, i].axis('off')
axes[1, i].imshow(X_recon[i].reshape(64, 64), cmap='gray')
axes[1, i].set_title(f'重建{i+1}')
axes[1, i].axis('off')
plt.suptitle(f'PCA 降维到 50 维 (保留方差 {pca.explained_variance_ratio_.sum():.2%})')
plt.tight_layout()
plt.show()
实战6:PCA 作为预处理提升分类器性能
# -*- coding: utf-8 -*-
"""
在手写数字上,对比原始数据与 PCA 降维后使用 SVM 的分类性能和时间
"""
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.decomposition import PCA
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
import time
digits = load_digits()
X, y = digits.data, digits.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 1. 原始数据 + SVM(标准化)
pipe_raw = Pipeline([
('scaler', StandardScaler()),
('svm', SVC(kernel='rbf', gamma='scale'))
])
start = time.time()
pipe_raw.fit(X_train, y_train)
time_raw = time.time() - start
acc_raw = pipe_raw.score(X_test, y_test)
# 2. PCA 降维 + SVM
pipe_pca = Pipeline([
('scaler', StandardScaler()),
('pca', PCA(n_components=0.95)),
('svm', SVC(kernel='rbf', gamma='scale'))
])
start = time.time()
pipe_pca.fit(X_train, y_train)
time_pca = time.time() - start
acc_pca = pipe_pca.score(X_test, y_test)
print(f"原始数据: 准确率={acc_raw:.4f}, 训练时间={time_raw:.2f}秒, 特征维数={X.shape[1]}")
print(f"PCA降维: 准确率={acc_pca:.4f}, 训练时间={time_pca:.2f}秒, 特征维数={pipe_pca.named_steps['pca'].n_components_}")
实战7:使用随机化 SVD 加速大维度数据(svd_solver=‘randomized’)
# -*- coding: utf-8 -*-
"""
生成高维数据,对比 svd_solver='auto' vs 'randomized' 的时间
"""
import numpy as np
from sklearn.decomposition import PCA
import time
# 高维数据(2000样本,500特征)
X = np.random.randn(2000, 500)
for solver in ['auto', 'randomized']:
pca = PCA(n_components=50, svd_solver=solver, random_state=42)
start = time.time()
pca.fit(X)
elapsed = time.time() - start
print(f"solver={solver:10s} 耗时: {elapsed:.3f} 秒")
案例实操演示
案例:PCA 在股票市场中的应用(因子降维)
# 模拟股票因子数据(多个技术指标),用 PCA 提取主要特征
import pandas as pd
import numpy as np
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
np.random.seed(42)
# 模拟 500 个交易日,10 个技术指标
n_days = 500
n_features = 10
factors = np.random.randn(n_days, n_features)
# 添加相关性结构
factors = factors @ np.random.randn(n_features, n_features) * 0.5 + factors
pca = PCA(n_components=0.90)
factors_pca = pca.fit_transform(factors)
print(f"原始特征数: {n_features}, 降维后: {factors_pca.shape[1]}")
print(f"累计方差贡献率: {pca.explained_variance_ratio_.sum():.2%}")
# 可视化主成分的载荷(第一个主成分对原始因子的权重)
loadings = pca.components_[0]
plt.bar(range(n_features), loadings)
plt.xlabel('原始因子编号')
plt.ylabel('第一主成分载荷')
plt.title('第一主成分的因子载荷')
plt.show()
常见报错与避坑指南
报错1:ValueError: n_components must be between 1 and min(n_samples, n_features)
原因:指定的主成分数大于样本数或特征数。
解决:设置 n_components 不大于 min(n_samples, n_features)。若用方差比例,则不会报错。
报错2:PCA with svd_solver='arpack' requires n_components < min(X.shape)
原因:arpack 要求主成分数严格小于样本数和特征数。
解决:改用 svd_solver='full' 或 'auto'。
报错3:白化后数据方差为0或极小的主成分
说明:某些主成分的特征值接近0,白化时会除一个很小的数,导致数值爆炸。
解决:确保 n_components 只保留非零特征值的主成分(通过方差解释率选择)。
报错4:PCA 结果不稳定(不同运行结果不一致)
原因:使用了随机 SVD(svd_solver='randomized')且未固定 random_state。
解决:设置 random_state 固定种子。
避坑总结
- 必须中心化:PCA 依赖数据均值中心化,sklearn 自动处理,但确保不用稀疏矩阵。
- 方差解释率选择主成分数:通常取 0.95 或 0.99,而非固定整数。
- 白化谨慎使用:会在降维后改变距离尺度,如需保留距离关系,慎用。
- 对异常值敏感:PCA 基于协方差,异常值会扭曲主成分方向。可先用 RobustScaler 或做异常检测。
- 非线性数据:PCA 效果差,改用 KernelPCA 或 t-SNE/UMAP。
知识点总结
本课系统讲解了主成分分析(PCA)的完整知识体系:
原理
- 目标:最大化投影后的方差(等价于最小化投影误差)。
- 方法:数据中心化 → 协方差矩阵特征分解 → 选择前 k 个特征向量。
- 方差解释率:各主成分方差占比,累计和决定 k。
API
PCA核心参数:n_components(整数或小数)、whiten、svd_solver。- 属性:
components_、explained_variance_ratio_、mean_。 - 方法:
fit、transform、fit_transform、inverse_transform。
实践
- 通过累计方差曲线选择 k。
- 白化使各主成分方差为1,用于独立成分分析或预处理。
- PCA 可集成到 Pipeline 中提升模型效率。
- 随机化 SVD(
svd_solver='randomized')加速大数据降维。
局限性
- 线性变换,无法捕捉非线性结构。
- 对异常值敏感。
- 主成分解释性差。
课后练习题
选择题
-
PCA 寻找的第一主成分是:
A. 与目标变量相关性最大的方向 B. 数据方差最大的方向 C. 数据均值最小的方向 D. 特征数最多的方向 -
白化(whiten=True)的作用是:
A. 使数据均值为0 B. 使各主成分方差为1 C. 增加特征独立性 D. 提高计算速度 -
当特征维度远大于样本数时,推荐使用哪种 SVD 求解器?
A. ‘full’ B. ‘arpack’ C. ‘randomized’ D. ‘auto’ 会自动选择 ‘randomized’
填空题
from sklearn.decomposition import PCA
# 1. 创建 PCA,保留 95% 方差
pca = PCA(n_components=________)
# 2. 训练并降维
X_pca = pca.fit_transform(X)
# 3. 获取主成分方向(每行是一个主成分)
components = pca.________
# 4. 获取每个主成分的方差贡献率
var_ratio = pca.________
实操题
-
累积方差曲线:使用
load_digits,绘制不同主成分数量(1-64)下的累计方差贡献率曲线。找出达到 90%、95%、99% 所需的主成分数。 -
PCA 重建误差:在
load_digits上,用 PCA 将维度降到 10、30、50,分别计算重建图像与原始图像的均方误差(MSE),并显示重建图像。观察随着主成分增加,图像质量如何变化。 -
白化对 KNN 的影响:在鸢尾花数据集上,先用 PCA 降到 2 维(
whiten=False),然后用 KNN(K=3)分类;再同样用whiten=True重复。比较两次分类准确率,并分析差异原因。
思考题
在图像压缩中,我们使用 PCA 保留前 k 个主成分重建图像。如果原始图像是 512×512 灰度图(262144 维),使用 k=200 的主成分,压缩比是多少?为什么直接保存主成分而不是原图?PCA 压缩与 JPEG 压缩有何本质区别?
下一课预告:第27课我们将学习随机森林和 Bagging 集成方法——通过组合多个决策树来提升稳定性和准确率,是 Kaggle 竞赛的利器。敬请期待!
🔗《30节课 scikit-learn 从入门到精通》系列课程导航
第一部分:基础入门 & 环境准备(1-6 课)
第二部分:数据预处理 & 数据集操作(7-12 课)
第三部分:传统机器学习回归算法(13-17 课)
第四部分:分类算法精讲(18-23 课)
第五部分:聚类 & 降维 & 集成学习(24-29 课)
第六部分:结业大型项目实战(第 30 课)
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

466

被折叠的 条评论
为什么被折叠?



