首页/知识库/PCA:主成分分析降维

PCA:主成分分析降维

✍️ AI Master📅 创建 2026-04-12📖 18 min 阅读
💡

文章摘要

从协方差矩阵到奇异值分解,理解最常用的降维算法

1降维的动机——维度灾难

现实世界的数据往往具有极高的维度。一张 224×224 的 RGB 图像有 150,528 维,一个包含 10,000 个基因的表达矩阵有 10,000 维。高维度带来的第一个问题是「维度灾难」:随着维度增加,数据在高维空间中变得极度稀疏,样本之间的欧氏距离趋于收敛,基于距离的算法(如 KNN、K-Means)性能急剧下降。

降维的本质是在信息损失可接受的前提下,将数据投影到一个低维子空间中。降维有三大动机:可视化(降到 2D/3D 方便人类观察)、去噪(丢弃方差小的方向通常对应噪声)、加速计算(减少后续模型的输入维度,降低过拟合风险)。PCA 是其中最经典的方法——它通过线性变换找到数据中方差最大的方向,用尽可能少的主成分来近似表达原始数据。

python
import numpy as np
import matplotlib.pyplot as plt

# 维度灾难演示:随机点之间的平均距离随维度增长趋于收敛
dims = np.arange(1, 100)
avg_dists = []
rng = np.random.RandomState(42)
for d in dims:
    points = rng.randn(1000, d)
    # 随机采样 5000 对点计算平均欧氏距离
    idx1, idx2 = rng.randint(0, 1000, (2, 5000))
    dists = np.sqrt(np.sum((points[idx1] - points[idx2])**2, axis=1))
    avg_dists.append(dists.mean())

plt.figure(figsize=(8, 4))
plt.plot(dims, avg_dists, 'b-', linewidth=2)
plt.xlabel("Dimension"); plt.ylabel("Average Euclidean Distance")
plt.title("Dimensionality Curse: Distances Converge")
plt.grid(True, alpha=0.3)
plt.show()
python
# 不同降维算法对比概览
from sklearn.decomposition import PCA, KernelPCA, FastICA
from sklearn.manifold import TSNE, Isomap

# PCA: 线性、保留全局方差、速度快
pca = PCA(n_components=2)

# Kernel PCA: 非线性核技巧
kpca = KernelPCA(n_components=2, kernel='rbf', gamma=15)

# t-SNE: 保留局部邻域结构,适合可视化
tsne = TSNE(n_components=2, perplexity=30, random_state=42)

# Isomap: 基于测地线距离的流形学习
iso = Isomap(n_components=2, n_neighbors=10)

# 选择依据:数据是否有非线性流形结构?
# 线性数据 → PCA;非线性流形 → t-SNE/Isomap
# 计算效率要求高 → PCA;可视化探索 → t-SNE
算法线性/非线性保留的结构适用场景

PCA

线性

全局方差

通用降维/预处理

t-SNE

非线性

局部邻域

高维数据可视化

UMAP

非线性

局部+全局

大规模可视化

AutoEncoder

非线性

重建损失

复杂特征学习

降维前务必标准化数据,否则量纲大的特征会主导方差方向。

降维必然造成信息损失——保留的方差比例越高,损失越小,但维度降低幅度也越小。

2协方差矩阵与特征值分解

PCA 的数学核心是协方差矩阵。假设我们有 n 个样本、d 个特征的数据矩阵 X(已中心化,即每个特征减去均值),协方差矩阵 C = (1/(n-1)) X^T X 是一个 d×d 的对称矩阵。C[i][j] 衡量第 i 个和第 j 个特征之间的线性相关程度:正值表示正相关,负值表示负相关,零表示不相关。

协方差矩阵的关键性质在于它是实对称矩阵,根据谱定理,实对称矩阵一定有 d 个实特征值,且对应的特征向量两两正交。PCA 就是要找到这些特征向量——每个特征向量定义了数据空间中的一个方向,对应的特征值表示数据沿该方向的方差大小。特征值越大,该方向携带的信息越多。将这些特征向量按特征值从大到小排列,就得到了主成分的优先级。

python
import numpy as np

# 生成二维相关数据
rng = np.random.RandomState(42)
theta = np.pi / 6  # 30 度旋转
R = np.array([[np.cos(theta), -np.sin(theta)],
              [np.sin(theta),  np.cos(theta)]])
s = np.array([[3, 0], [0, 0.5]])  # 拉伸
A = R @ s  # 变换矩阵
X_raw = rng.randn(200, 2) @ A.T

# 中心化
X = X_raw - X_raw.mean(axis=0)

# 计算协方差矩阵
cov_matrix = np.cov(X, rowvar=False)
print("协方差矩阵:")
print(cov_matrix)

# 特征值分解
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
# eigh 返回升序排列,需要反转
idx = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]

print(f"\n特征值: {eigenvalues}")
print(f"主成分方向(PC1): {eigenvectors[:, 0]}")
print(f"主成分方向(PC2): {eigenvectors[:, 1]}")
python
import matplotlib.pyplot as plt

# 可视化:数据 + 主成分方向
fig, ax = plt.subplots(figsize=(7, 7))
ax.scatter(X[:, 0], X[:, 1], alpha=0.5, s=20, label='Data')

# 绘制主成分轴(按特征值比例缩放)
for i in range(2):
    v = eigenvectors[:, i] * np.sqrt(eigenvalues[i]) * 3
    ax.arrow(0, 0, v[0], v[1],
             head_width=0.3, head_length=0.3,
             fc=['red', 'green'][i], ec=['red', 'green'][i],
             linewidth=2, label=f'PC{i+1} (λ={eigenvalues[i]:.2f})')

ax.axhline(0, color='gray', lw=0.5)
ax.axvline(0, color='gray', lw=0.5)
ax.set_aspect('equal')
ax.legend()
ax.set_title('PCA: Eigenvectors of Covariance Matrix')
ax.grid(True, alpha=0.3)
plt.show()
概念数学表达物理意义

协方差矩阵

C = X^T X / (n-1)

特征间的线性相关性

特征值 λ

Cv = λv

沿主成分方向的方差

特征向量 v

单位向量

主成分的方向

谱分解

C = VΛV^T

将矩阵分解为正交基

使用 np.linalg.eigh 而非 eig,因为 eigh 专门针对对称矩阵,数值稳定性更好。

协方差矩阵对异常值极其敏感——几个极端值就能显著改变特征向量方向,考虑先做异常值检测或鲁棒标准化。

3最大化方差 vs 最小化重构误差

PCA 有两种等价的推导视角,理解这一点能深化对算法本质的认识。第一种视角是「最大化投影方差」:我们寻找一个单位方向向量 w,使得数据投影到 w 上的方差 w^T C w 最大化。通过拉格朗日乘子法,这恰好等价于求协方差矩阵的最大特征值对应的特征向量。第二种视角是「最小化重构误差」:我们寻找一个 k 维子空间,使得所有样本点到该子空间的正交距离平方和最小。

这两个看似不同的目标——一个最大化保留的信息,一个最小化丢失的信息——在数学上被证明是完全等价的。这个等价性是 PCA 优美之处:无论从哪个角度切入,答案都是协方差矩阵的前 k 个特征向量。理解这一点很重要,因为不同的教科书可能从不同角度推导 PCA,但殊途同归。

python
import numpy as np
from scipy.optimize import minimize

# 视角一:最大化投影方差(数值验证)
rng = np.random.RandomState(42)
X = rng.randn(100, 3)
X = X - X.mean(axis=0)
C = X.T @ X / (X.shape[0] - 1)

def neg_variance(w, C):
    """负方差(因为 minimize 做最小化)"""
    w = w / np.linalg.norm(w)  # 归一化
    return -(w @ C @ w)

# 优化找第一主成分
result = minimize(neg_variance, np.array([1, 0, 0]), args=(C,))
w1_opt = result.x / np.linalg.norm(result.x)

# 对比 eigh 的结果
eigenvalues, eigenvectors = np.linalg.eigh(C)
w1_eig = eigenvectors[:, -1]  # 最大特征值对应的向量

print("优化得到 PC1:", w1_opt)
print("eigh 得到  PC1:", w1_eig)
print("方向一致:", np.abs(np.dot(w1_opt, w1_eig)) > 0.99)
python
# 视角二:最小化重构误差(数值验证)
from scipy.spatial.distance import cdist

def reconstruction_error(W, X):
    """计算投影到 W 子空间后的重构误差"""
    # W 已正交归一化 (d×k)
    X_proj = X @ W @ W.T  # 投影
    return np.sum((X - X_proj) ** 2)

# 用 eigh 的前 k 个特征向量
k = 2
W_eig = eigenvectors[:, -k:]
error_eig = reconstruction_error(W_eig, X)
print(f"特征向量子空间的重构误差: {error_eig:.4f}")

# 对比随机子空间
W_rand = rng.randn(3, k)
W_rand, _ = np.linalg.qr(W_rand)  # QR 正交化
error_rand = reconstruction_error(W_rand, X)
print(f"随机子空间的重构误差:     {error_rand:.4f}")
print(f"PCA 误差更小: {error_eig < error_rand}")
视角优化目标约束结论

最大化方差

max w^T C w

||w|| = 1

最大特征向量

最小化重构误差

min ||X - XWW^T||²

W^T W = I

前 k 个特征向量

信息论视角

max 投影熵

正交约束

最大不确定性方向

两个视角的等价性说明 PCA 是一个「帕累托最优」方案——在给定维度下,它同时做到了方差最大和损失最小。

重构误差最小化等价于方差最大化仅在使用正交投影时成立。如果允许斜投影,结论不再保证。

4SVD 实现——数值稳定的 PCA

虽然 PCA 的数学推导基于特征值分解,但实际实现中几乎都使用奇异值分解(SVD)。SVD 将数据矩阵 X 分解为 X = UΣV^T,其中 U 是 n×n 正交矩阵(左奇异向量),Σ 是 n×d 对角矩阵(奇异值),V 是 d×d 正交矩阵(右奇异向量)。关键关系是:V 的列就是协方差矩阵 X^T X 的特征向量,Σ 的对角元素 σ_i 与特征值的关系为 λ_i = σ_i² / (n-1)。

为什么用 SVD 而不用直接的特征值分解?原因有三:数值稳定性更好(不需要显式计算 X^T X,避免了条件数平方带来的精度损失)、可以处理 n < d 的情况(此时协方差矩阵是奇异的)、现代线性代数库对 SVD 的优化极为成熟(LAPACK 的 dgesdd 使用分治算法,效率极高)。这也是 scikit-learn 中 PCA 默认使用 SVD 的原因。

python
import numpy as np

# 从 SVD 推导 PCA
rng = np.random.RandomState(42)
X = rng.randn(50, 5)
X = X - X.mean(axis=0)  # 中心化

# SVD 分解
U, S, Vt = np.linalg.svd(X, full_matrices=False)
# Vt 的行是右奇异向量 = 主成分方向
# S 是奇异值
print("右奇异向量形状:", Vt.shape)  # (5, 5)
print("奇异值:", S)

# 验证 SVD 与特征值分解等价
cov = X.T @ X / (X.shape[0] - 1)
eigenvalues, eigenvectors = np.linalg.eigh(cov)

# 特征值 = σ² / (n-1)
svd_eigenvalues = S**2 / (X.shape[0] - 1)
print(f"\nSVD 推导特征值: {svd_eigenvalues[::-1]}")
print(f"eigh 计算特征值: {eigenvalues[::-1]}")
print(f"两者一致: {np.allclose(svd_eigenvalues[::-1], eigenvalues[::-1])}")

# 投影数据
X_pca_svd = X @ Vt.T  # 等价于 U @ np.diag(S)
print(f"\nSVD 投影形状: {X_pca_svd.shape}")
python
# 对比三种 PCA 实现的数值精度
from sklearn.decomposition import PCA

# 方法一:scikit-learn(SVD 实现)
sklearn_pca = PCA()
X_sklearn = sklearn_pca.fit_transform(X)
variances_sklearn = sklearn_pca.explained_variance_
components_sklearn = sklearn_pca.components_

# 方法二:eigh
idx = np.argsort(eigenvalues)[::-1]
eigenvalues_sorted = eigenvalues[idx]
eigenvectors_sorted = eigenvectors[:, idx]
variances_eigh = eigenvalues_sorted
components_eigh = eigenvectors_sorted.T

# 方法三:SVD
variances_svd = S**2 / (X.shape[0] - 1)
components_svd = Vt

# 验证方差一致性
print("方差对比:")
print(f"  sklearn: {np.round(variances_sklearn, 4)}")
print(f"  eigh:    {np.round(variances_eigh, 4)}")
print(f"  svd:     {np.round(variances_svd, 4)}")
print(f"  全部一致: {np.allclose(variances_sklearn, variances_eigh) and np.allclose(variances_sklearn, variances_svd)}")
方法时间复杂度数值稳定性适用场景

协方差 + eigh

O(d²n + d³)

中(X^T X 条件数平方)

d 较小的情况

SVD (full)

O(min(n²d, nd²))

通用场景

SVD (truncated)

O(ndk)

d 很大,只需前 k 个

Randomized SVD

O(nd log k)

超大规模数据

scikit-learn 的 PCA 默认使用 SVD,设置 svd_solver='randomized' 可在大数据集上显著加速。

不要显式计算 X^T X 再做特征值分解——条件数会被平方,在病态数据上导致严重的数值不稳定。

5选择主成分数量——碎石图与累计方差

PCA 降维中最关键的超参数是保留多少个主成分 k。选得太大,降维效果不明显;选得太小,会丢失重要信息。实践中有三种常用方法:累计方差解释比例、碎石图(Scree Plot)和交叉验证。累计方差解释比例是最直观的方法——计算前 k 个主成分的累计方差占总方差的比例,通常选择使这个比例超过某个阈值(如 95%)的最小 k 值。

碎石图则提供了更视觉化的判断:绘制每个主成分对应的特征值(或方差解释比例),寻找曲线中的「肘部」(elbow)——肘部之前的主成分携带大量信息,之后的主成分方差贡献急剧下降。肘部对应的 k 就是合理的选择。这个方法源自地质学中的碎石坡比喻——坡上是大石块(高方差主成分),坡下是碎石(低方差主成分)。需要注意的是,并非所有数据集都有明显的肘部,这时应结合业务需求和下游任务的性能来选择。

python
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits

# 加载手写数字数据集 (8×8 = 64 维)
X, y = load_digits(return_X_y=True)
X = X - X.mean(axis=0)

# SVD
U, S, Vt = np.linalg.svd(X, full_matrices=False)

# 方差解释比例
explained_variance_ratio = S2 / np.sum(S2)
cumulative_variance = np.cumsum(explained_variance_ratio)

fig, axes = plt.subplots(1, 2, figsize=(12, 4))

# 图1: 碎石图
axes[0].plot(range(1, len(explained_variance_ratio) + 1),
             explained_variance_ratio, 'bo-', markersize=4)
axes[0].axvline(x=10, color='r', linestyle='--', alpha=0.7, label='k=10')
axes[0].set_xlabel('Principal Component'); axes[0].set_ylabel('Explained Variance Ratio')
axes[0].set_title('Scree Plot'); axes[0].legend(); axes[0].grid(True, alpha=0.3)

# 图2: 累计方差
axes[1].plot(range(1, len(cumulative_variance) + 1),
             cumulative_variance, 'g-', linewidth=2)
axes[1].axhline(y=0.95, color='r', linestyle='--', alpha=0.7, label='95%')
axes[1].set_xlabel('Number of Components'); axes[1].set_ylabel('Cumulative Variance')
axes[1].set_title('Cumulative Explained Variance'); axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

# 自动选择 k(累计方差 >= 95%)
k_95 = np.argmax(cumulative_variance >= 0.95) + 1
print(f"保留 95% 方差需要 {k_95} 个主成分(原始维度 64)")
python
# 方法三:交叉验证选择 k(基于下游任务性能)
from sklearn.model_selection import cross_val_score
from sklearn.neighbors import KNeighborsClassifier

X, y = load_digits(return_X_y=True)
k_values = [5, 10, 15, 20, 30, 40, 50]
cv_scores = []

for k in k_values:
    pca = PCA(n_components=k)
    X_pca = pca.fit_transform(X)
    knn = KNeighborsClassifier(n_neighbors=5)
    scores = cross_val_score(knn, X_pca, y, cv=5, scoring='accuracy')
    cv_scores.append(scores.mean())
    print(f"k={k:2d}: 交叉验证准确率 = {scores.mean():.4f} (±{scores.std():.4f})")

# 找到最佳 k
best_k = k_values[np.argmax(cv_scores)]
print(f"\n基于下游任务的最佳主成分数: {best_k}")

plt.figure(figsize=(8, 4))
plt.plot(k_values, cv_scores, 'ro-', linewidth=2, markersize=6)
plt.xlabel('Number of Components'); plt.ylabel('Cross-Val Accuracy')
plt.title('KNN Accuracy vs PCA Components')
plt.grid(True, alpha=0.3)
plt.show()
方法选择标准优点缺点

累计方差

≥ 95% 方差

简单直观

可能过度保留

碎石图肘部

方差下降拐点

可视化判断

主观、可能无肘部

交叉验证

下游任务最优

面向任务

计算开销大

对于高维稀疏数据(如 TF-IDF 文本特征),通常前几十个主成分就能解释 90%+ 方差,可以先用累计方差粗筛。

不要盲目追求 99% 方差——在某些场景(如去噪)中,保留 80-90% 方差反而效果更好,因为它丢弃了可能是噪声的小方差方向。

6PCA 的局限性

尽管 PCA 应用广泛,但它有几个本质局限。首先是线性假设——PCA 只能发现数据中的线性结构。如果数据分布在一个非线性流形上(如瑞士卷 S-curve),PCA 无法有效展开它。想象一下:把一张弯曲的纸摊平,PCA 只是找到纸在三维空间中的最佳倾斜角度,而不是把纸真正展开。这就是 Kernel PCA、t-SNE、UMAP 等非线性方法存在的理由。

其次,PCA 对异常值极其敏感。协方差矩阵的计算依赖于均值和平方差,几个极端值就能把主成分方向拉偏。这在真实数据中很常见——传感器故障、录入错误、罕见事件都可能产生异常值。解决方案包括使用鲁棒 PCA(RPCA,将数据分解为低秩部分和稀疏异常部分)、先做异常值检测剔除、或使用对异常值不敏感的标准化方法(如分位数标准化)。最后,PCA 假设高方差方向就是「重要」方向,但在分类任务中,最有判别性的方向可能恰恰是方差较小的方向。

python
import numpy as np
import matplotlib.pyplot as plt

# PCA 在线性 vs 非线性数据上的表现对比
rng = np.random.RandomState(42)

# 线性数据
X_linear = rng.randn(300, 2) @ np.array([[2, 0.5], [0.5, 1]])

# S-curve 非线性流形
t = 1.5 * np.pi * (1 + 2 * rng.rand(300))
x = np.cos(t)
y = rng.rand(300) * 2 - 1
z = np.sin(t)
X_nonlinear = np.column_stack([x, z])  # 投影到 2D 看非线性

# PCA 降维
def pca_1d(X):
    X_c = X - X.mean(axis=0)
    U, S, Vt = np.linalg.svd(X_c, full_matrices=False)
    return X_c @ Vt[0], Vt[0]

z_linear, v1 = pca_1d(X_linear)
z_nonlinear, v2 = pca_1d(X_nonlinear)

fig, axes = plt.subplots(2, 2, figsize=(10, 8))
axes[0, 0].scatter(X_linear[:, 0], X_linear[:, 1], alpha=0.5)
axes[0, 0].arrow(0, 0, v1[0]*3, v1[1]*3, color='red', width=0.05)
axes[0, 0].set_title('Linear Data + PC1'); axes[0, 0].set_aspect('equal')
axes[0, 1].scatter(X_linear[:, 0], X_linear[:, 1], c=z_linear, cmap='viridis')
axes[0, 1].set_title('Linear: PCA Projection')

axes[1, 0].scatter(X_nonlinear[:, 0], X_nonlinear[:, 1], alpha=0.5)
axes[1, 0].arrow(0, 0, v2[0]*3, v2[1]*3, color='red', width=0.05)
axes[1, 0].set_title('S-curve Data + PC1'); axes[1, 0].set_aspect('equal')
axes[1, 1].scatter(X_nonlinear[:, 0], X_nonlinear[:, 1], c=z_nonlinear, cmap='viridis')
axes[1, 1].set_title('S-curve: PCA Fails')
plt.tight_layout()
plt.show()
python
# 鲁棒 PCA 示例:使用 sklearn 的 RobustScaler + PCA
from sklearn.preprocessing import RobustScaler
from sklearn.decomposition import PCA
from sklearn.datasets import load_iris

X, y = load_iris(return_X_y=True)

# 注入异常值
rng = np.random.RandomState(42)
outlier_idx = rng.choice(len(X), 10, replace=False)
X[outlier_idx] *= 5

# 标准 PCA(受异常值影响)
pca_std = PCA(n_components=2)
X_pca_std = pca_std.fit_transform(X)

# 鲁棒标准化 + PCA
scaler = RobustScaler()  # 使用中位数和四分位距
X_robust = scaler.fit_transform(X)
pca_robust = PCA(n_components=2)
X_pca_robust = pca_robust.fit_transform(X_robust)

# 比较累积方差
print("标准 PCA 方差解释比:", pca_std.explained_variance_ratio_)
print("鲁棒 PCA 方差解释比:", pca_robust.explained_variance_ratio_)
print(f"\n标准 PCA 总方差保留: {sum(pca_std.explained_variance_ratio_):.4f}")
print(f"鲁棒 PCA 总方差保留: {sum(pca_robust.explained_variance_ratio_):.4f}")
局限性原因替代方案

只能捕获线性关系

基于线性投影

Kernel PCA / t-SNE / UMAP

对异常值敏感

协方差使用平方差

Robust PCA / 先做异常值检测

高方差 ≠ 高判别力

无监督、不考虑标签

LDA(线性判别分析)

可解释性差

主成分是原始特征的组合

稀疏 PCA / 因子分析

如果数据有明显的类别标签,试试 LDA(线性判别分析)——它在降维的同时最大化类间方差与类内方差的比值。

PCA 不能处理缺失值——数据中如果有 NaN,协方差矩阵计算会失败。需要先做缺失值填充(均值/中位数/KNN 填充)。

7sklearn 实战与可视化

scikit-learn 提供了高度优化的 PCA 实现,几行代码即可完成完整的降维流程。实战中有几个关键细节需要注意:PCA 默认在 fit 时自动中心化数据(减去均值),但不会自动标准化——如果你的特征量纲不同(比如一个特征是 0-1 的概率,另一个是 1000-10000 的金额),必须先做 StandardScaler。此外,PCA 的 transform 方法可以处理训练时未见过的数据,这使得 PCA 可以无缝嵌入到 Pipeline 中,避免数据泄露。

下面通过 MNIST 手写数字数据集演示完整的 PCA 流程:降维到 2D 可视化、重建误差分析、以及 Pipeline 集成。这个实战涵盖了 PCA 在真实项目中的典型用法——从数据预处理到模型评估的完整链路。

python
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

# 加载数据
X, y = load_digits(return_X_y=True)
print(f"原始数据形状: {X.shape}")  # (1797, 64)

# Pipeline: 标准化 → PCA
pca_pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('pca', PCA(n_components=0.95))  # 保留 95% 方差
])

X_reduced = pca_pipeline.fit_transform(X)
pca = pca_pipeline.named_steps['pca']

print(f"降维后形状: {X_reduced.shape}")
print(f"主成分数量: {pca.n_components_}")
print(f"累计方差解释比: {np.sum(pca.explained_variance_ratio_):.4f}")

# 重建误差分析
X_reconstructed = pca_pipeline.inverse_transform(X_reduced)
reconstruction_error = np.mean((X - X_reconstructed)**2)
print(f"平均重构误差: {reconstruction_error:.4f}")
python
# 2D 可视化 + 重建数字展示
fig, axes = plt.subplots(2, 5, figsize=(14, 6))

# 第一行: 原始数字
for i in range(5):
    axes[0, i].imshow(X[i].reshape(8, 8), cmap='gray')
    axes[0, i].axis('off')
    axes[0, i].set_title(f'Original (label={y[i]})')

# 第二行: PCA 重建(降到 2D 再还原)
pca_2d = PCA(n_components=2)
X_2d = pca_2d.fit_transform(StandardScaler().fit_transform(X))
X_2d_recon = pca_2d.inverse_transform(X_2d)

for i in range(5):
    axes[1, i].imshow(X_2d_recon[i].reshape(8, 8), cmap='gray')
    axes[1, i].axis('off')
    axes[1, i].set_title(f'Reconstructed (2D)')

plt.suptitle('PCA: Original vs 2D Reconstruction', fontsize=14)
plt.tight_layout()
plt.show()

# 降维后可视化
fig, ax = plt.subplots(figsize=(10, 7))
scatter = ax.scatter(X_2d[:, 0], X_2d[:, 1], c=y, cmap='tab10',
                     alpha=0.6, s=30, edgecolors='none')
ax.set_title('MNIST Digits: PCA to 2D')
ax.set_xlabel('PC1')
ax.set_ylabel('PC2')
plt.colorbar(scatter, label='Digit Label')
plt.show()
Pipeline 步骤作用关键参数

StandardScaler

零均值单位方差

with_mean=True, with_std=True

PCA(n_components=k)

线性降维

k=整数/小数(方差比)/'mle'

inverse_transform

重建回原始空间

评估信息损失

GridSearchCV

自动选择最优 k

param_grid={'pca__n_components': [...]}

将 PCA 嵌入 Pipeline 是最佳实践——它确保交叉验证和测试集只使用训练集计算的变换参数,避免数据泄露。

PCA(n_components='mle') 使用 MLE 自动估计维度,但假设数据来自高斯分布,对非高斯数据可能不准确。

继续你的 AI 学习之旅

浏览更多 AI 知识库文章,或者探索 GitHub 上的优质 AI 项目