Skip to content

PCA主成分分析:从高维到低维的数学地图 ​

5月24日2026年

先验直觉:当数据有几十、几百个特征时,可视化困难、模型容易过拟合、计算成本高。PCA(Principal Component Analysis)通过线性变换将高维数据映射到低维,同时最大化保留数据的方差(信息量)。

关键词:Python,matplotlib,PCA,正则化,过拟合,交叉验证,特征工程,聚类


一、降维问题

当数据有几十、几百个特征时,可视化困难、模型容易过拟合、计算成本高。PCA(Principal Component Analysis)通过线性变换将高维数据映射到低维,同时最大化保留数据的方差(信息量)。

核心思想: 找到数据方差最大的方向作为第一主成分,与第一主成分正交且方差次大的方向作为第二主成分,依此类推。通过这种正交变换,PCA将原始 p 维空间重新定向到一组新的坐标轴——主成分,这些坐标轴按照它们"捕获"的数据方差大小排序。

为什么关注方差?在统计学中,方差度量数据的离散程度。如果一个方向上方差很小,意味着所有样本在该方向上的取值几乎相同,那么这个方向的信息量就很低。反之,方差大的方向包含更多关于样本之间差异的信息。PCA正是基于这个直觉:保留方差 ≈ 保留信息。

二、数学原理:从协方差矩阵到SVD的等价推导

2.1 问题形式化 ​

给定中心化后的数据矩阵 X∈Rn×p(n个样本,p个特征),我们要找到一个方向向量 w∈Rp(∥w∥=1),使得投影 Xw 的方差最大。

投影后的方差为:

Var(Xw)=1n(Xw)T(Xw)=wT(1nXTX)w=wTCw

其中 C=1nXTX 是协方差矩阵。于是问题转化为:

maxw:∥w∥=1wTCw

2.2 特征值分解(EVD)视角 ​

利用拉格朗日乘数法求解上述优化问题:

L=wTCw−λ(wTw−1)

对 w 求导并令为零:Cw=λw。这正是特征值方程!

因此,w 是协方差矩阵 C 的特征向量,λ 是对应的特征值。代入目标函数:

wTCw=wT(λw)=λ

所以投影方差就等于特征值 λ。最大方差方向 = 最大特征值对应的特征向量。

协方差矩阵的特征值分解:

C=VΛVT
  • V 的列是特征向量(主成分方向),VTV=I(正交矩阵)
  • Λ=diag(λ1,λ2,...,λp) 是特征值的对角矩阵,且 λ1≥λ2≥...≥λp≥0

方差解释率(Explained Variance Ratio): λi/∑j=1pλj,表示第 i 个主成分携带的信息占比。

2.3 几何意义 ​

把特征值和特征向量放在一起看:

  • 特征向量(主成分方向):数据在新的坐标系中的"轴"。第一特征向量指向数据散布最大的方向,第二特征向量在与第一轴正交的平面内指向散布次大的方向。这些特征向量构成了数据的新基(basis)。
  • 特征值(每个方向上的方差):对应轴上数据投影的方差大小。特征值越大,说明数据在该方向上越分散,"信息量"越大。注意特征值永远非负,因为协方差矩阵是半正定的。
  • 方差解释率:一个方向上的方差占总方差的百分比。PC1的方差解释率 = 92% 意味着数据中92%的变化可以用第一个主成分一个轴来解释。

可以这样想象:数据点分布像一个拉长的椭球体。PCA就是在找这个椭球体的主轴——长轴对应第一主成分,次长轴对应第二主成分,依此类推。如果将数据投影到长轴方向上,点与点之间的相对位置差异最大(也就是最容易区分);如果投影到短轴方向,所有点挤在一起,难以区分。

具体到Iris数据: 原始4维空间中的每个样本可以看作4维空间中的一个点。PCA找到的4个主成分方向中,PC1方向上的方差为4.23(占总方差92.46%),这意味着150个样本在该方向上的投影值分布范围很广;而PC4方向上的方差仅为0.02(占0.52%),说明几乎所有样本在该方向上的取值几乎相同,可以安全舍弃。

为什么标准化会影响结果? 几何上,标准化相当于将每个坐标轴缩放为相同长度(方差=1),将原来"拉长的椭球"变成了更接近球形的形状。此时各方向的重要性不受原始量纲影响,更能反映特征间真实的相关结构。

2.4 奇异值分解(SVD)视角 ​

实际上,PCA也可以通过SVD来实现,而且更加数值稳定。

对中心化数据矩阵 X 进行SVD:

X=UΣVT
  • U∈Rn×p:左奇异向量矩阵,UTU=I
  • Σ∈Rp×p:奇异值的对角矩阵,σ1≥σ2≥...≥σp≥0
  • V∈Rp×p:右奇异向量矩阵,VTV=I

2.5 EVD与SVD的等价性 ​

两种方法之间的联系非常简洁:

C=1nXTX=1n(UΣVT)T(UΣVT)=1nVΣTUTUΣVT=VΣ2nVT

对比 C=VΛVT 可得:

Λ=Σ2n⇒λi=σi2n

即:协方差矩阵的特征值 = 奇异值平方 / 样本数,而特征向量矩阵 V 就是SVD中的右奇异向量矩阵。

2.6 为什么sklearn用SVD而非EVD? ​

sklearn的PCA实现(sklearn.decomposition.PCA)底层默认使用SVD,原因有:

  1. 数值稳定性: 直接计算 XTX 会平方条件数,丢失精度(条件数变为 κ2)。SVD直接在 X 上操作,条件数保持为 κ。当特征值很小时(接近0),这种差距尤为明显。例如,如果某个方向上的方差极小,EVD可能会算出负的特征值(由于浮点误差),而SVD能保证奇异值始终非负。

  2. 计算效率: 当 n<p 时(样本少于特征,如基因表达数据),XTX 是 p×p 的大矩阵(维度由特征数决定),而SVD可以只对 n×n 的内核矩阵进行分解(维度由样本数决定),大幅降低计算量。sklearn自动选择经济型分解模式(full_matrices=False)。

  3. 无需显式计算协方差矩阵: SVD一步到位,同时得到主成分方向(V)和主成分得分(UΣ),且 U 和 V 都是正交矩阵,具有良好的数学性质。

  4. 处理稀疏数据: sklearn的PCA支持Truncated SVD(截断SVD),对于稀疏矩阵效率远高于EVD。当只需要前k个主成分时,可以用迭代方法(scipy.sparse.linalg.svds),避免对整个矩阵做完全分解。

一句话总结: EVD需要先算 XTX(可能丢失精度且计算量大),而SVD直接分解 X(更稳定、更灵活、更高效)。sklearn选择SVD是工程实践中的最佳选择。

验证等价性的代码:

python
import numpy as np
from sklearn.datasets import load_iris

iris = load_iris()
X = iris.data
X_centered = X - np.mean(X, axis=0)
n = X_centered.shape[0]

# EVD路线:协方差矩阵 → 特征值分解
cov_matrix = (X_centered.T @ X_centered) / n
eigvals, eigvecs = np.linalg.eigh(cov_matrix)
idx = np.argsort(eigvals)[::-1]
eigvals = eigvals[idx]
eigvecs = eigvecs[:, idx]

# SVD路线
U, S, Vt = np.linalg.svd(X_centered, full_matrices=False)

# 验证等价性
print("Top 3 特征值 (EVD):", eigvals[:3])
print("Top 3 λ = σ²/n (SVD):", (S[:3]**2) / n)
print("特征向量 vs 右奇异向量 (cosine相似度):")
for i in range(3):
    cos_sim = np.dot(eigvecs[:, i], Vt[i, :])
    print(f"  PC{i+1}: {cos_sim:.6f}")

预期输出:

Top 3 特征值 (EVD): [4.228 0.243 0.078]
Top 3 λ = σ²/n (SVD): [4.228 0.243 0.078]
特征向量 vs 右奇异向量 (cosine相似度):
  PC1: 1.000000
  PC2: 1.000000
  PC3: 1.000000

可以看到特征值和 σ2/n 完全相等,特征向量和右奇异向量方向一致(可能差±1的符号,但方向相同)。

三、数据准备

使用Iris(鸢尾花)数据集——150个样本,4个特征(花萼长/宽、花瓣长/宽),3个品种(Setosa、Versicolor、Virginica)。

python
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
import seaborn as sns
import datetime

today = datetime.date.today().strftime('%Y-%m-%d')

# 加载数据
iris = load_iris()
X = iris.data
y = iris.target
feature_names = iris.feature_names
target_names = iris.target_names

print(f"数据形状: {X.shape}")
print(f"特征: {feature_names}")
print(f"类别: {target_names}")
print(f"各类别样本数: {np.bincount(y)}")

预期输出:

数据形状: (150, 4)
特征: ['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']
类别: ['setosa' 'versicolor' 'virginica']
各类别样本数: [50 50 50]

四、手写PCA:从特征值分解开始

4.1 数据中心化与协方差矩阵 ​

python
# 数据中心化(减均值)
X_centered = X - np.mean(X, axis=0)

# 计算协方差矩阵
cov_matrix = np.cov(X_centered, rowvar=False)
print(f"协方差矩阵:\n{np.round(cov_matrix, 2)}")

预期输出:

协方差矩阵:
[[ 0.69 -0.04  1.27  0.52]
 [-0.04  0.19 -0.33 -0.12]
 [ 1.27 -0.33  3.12  1.3 ]
 [ 0.52 -0.12  1.3   0.58]]

对角线是各特征的方差,非对角线是特征间的协方差。花瓣长度(petal length)的方差最大(3.12),说明它在数值上变化最大。

4.2 特征值分解与方差解释率 ​

python
# 特征值分解(对称矩阵用 eigh 更快更稳定)
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)

# 按特征值从大到小排序
idx = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]

# 方差解释率
explained_var_ratio = eigenvalues / np.sum(eigenvalues)
cumulative_ratio = np.cumsum(explained_var_ratio)

print("特征值、方差解释率、累计方差解释率:")
for i in range(len(eigenvalues)):
    print(f"  PC{i+1}: λ={eigenvalues[i]:.4f}, "
          f"方差解释率={explained_var_ratio[i]:.2%}, "
          f"累计={cumulative_ratio[i]:.2%}")

预期输出:

特征值、方差解释率、累计方差解释率:
  PC1: λ=4.2282, 方差解释率=92.46%, 累计=92.46%
  PC2: λ=0.2427, 方差解释率=5.31%, 累计=97.77%
  PC3: λ=0.0782, 方差解释率=1.71%, 累计=99.48%
  PC4: λ=0.0238, 方差解释率=0.52%, 累计=100.00%

几何解读: PC1 解释了 92.46% 的方差,意味着仅用第一个主成分就能保留数据中超过 92% 的信息量。PC1 + PC2 达到 97.77%,说明前两个主成分几乎可以完整代表原始4维数据。这4个特征实际上存在很强的冗余(相关性),PCA成功地将它们压缩到了更少的维度。

4.3 投影到2D空间 ​

python
# 取前2个特征向量作为投影矩阵
top_vectors = eigenvectors[:, :2]

# 投影:X_pca = X_centered @ V_top
X_pca = X_centered @ top_vectors

print("前5个样本的2D投影 (手动PCA):")
print(np.round(X_pca[:5], 3))

预期输出:

前5个样本的2D投影 (手动PCA):
[[-2.684  0.327]
 [-2.715 -0.177]
 [-2.89  -0.138]
 [-2.746 -0.328]
 [-2.509 -0.098]]

每个样本从4维向量变成了2维向量,损失了约2.23%的方差信息,但保留了97.77%。

五、可视化1:碎石图(Scree Plot)

碎石图以折线图展示各主成分的特征值大小,帮助我们直观判断"拐点"——特征值开始趋于平缓的位置。

python
plt.figure(figsize=(8, 5))

# 碎石图:特征值降序折线
pc_range = range(1, len(eigenvalues) + 1)
plt.plot(pc_range, eigenvalues, 'bo-', linewidth=2, markersize=8, label='特征值')
plt.scatter(pc_range, eigenvalues, c='#2980b9', s=80, zorder=5)

# 在数据点上标注具体数值
for i, val in enumerate(eigenvalues):
    plt.text(i + 1, val + 0.1, f'{val:.3f}', ha='center', fontsize=10)

plt.xlabel('主成分序号', fontsize=12)
plt.ylabel('特征值(方差)', fontsize=12)
plt.title('QIAN DATA: 碎石图 - ' + today, fontsize=13)
plt.xticks(pc_range)
plt.axhline(y=1, color='red', linestyle='--', alpha=0.5, label='λ=1(Kaiser准则阈值)')
plt.legend()
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig('pca_scree.png', dpi=200)
plt.show()

pca_scree.png

解读: PC1的特征值(4.23)远大于其他主成分,PC2次之(0.24)。在PC2之后曲线趋于平缓,形成明显的"肘部"——说明保留2个主成分是合理的选择。Kaiser准则建议保留特征值 > 1 的主成分,这里也只有PC1满足。

六、可视化2:累积方差解释率曲线

累积方差解释率曲线帮助我们量化判断:要达到某个信息保留阈值(如90%、95%、99%),需要保留多少个主成分。

python
plt.figure(figsize=(8, 5))

# 累积方差解释率曲线
bars = plt.bar(pc_range, explained_var_ratio, color='skyblue',
               edgecolor='white', alpha=0.7, label='单主成分方差解释率')
line = plt.plot(pc_range, cumulative_ratio, 'ro-', linewidth=2,
                markersize=8, label='累积方差解释率', zorder=5)

# 标注数值
for i, (single, cum) in enumerate(zip(explained_var_ratio, cumulative_ratio)):
    plt.text(i + 1, single + 0.02, f'{single:.1%}', ha='center', fontsize=9)
    plt.text(i + 1, cum - 0.08, f'{cum:.1%}', ha='center', fontsize=10,
             color='red', fontweight='bold')

# 添加90%和95%的参考线
plt.axhline(y=0.90, color='green', linestyle='--', alpha=0.6, label='90% 阈值')
plt.axhline(y=0.95, color='orange', linestyle='--', alpha=0.6, label='95% 阈值')

plt.xlabel('主成分序号', fontsize=12)
plt.ylabel('方差解释率', fontsize=12)
plt.title('QIAN DATA: 累积方差解释率 - ' + today, fontsize=13)
plt.xticks(pc_range)
plt.ylim(0, 1.05)
plt.legend(loc='lower right')
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig('pca_cumulative.png', dpi=200)
plt.show()

pca_cumulative.png

解读: PC1单独就超过了90%的阈值(92.46%),PC1 + PC2超过95%阈值(97.77%)。这意味着我们只需要2个主成分就能保留原始数据中几乎全部有效信息。对于Iris数据集,4维到2维是极度高效的降维。

七、可视化3:2D主成分投影散点图

将Iris数据投影到前两个主成分上,用不同颜色标记三种鸢尾花。

python
plt.figure(figsize=(8, 6))

colors = ['#e74c3c', '#3498db', '#2ecc71']
markers = ['o', 's', '^']

for i, name in enumerate(target_names):
    mask = (y == i)
    plt.scatter(X_pca[mask, 0], X_pca[mask, 1],
                c=colors[i], marker=markers[i], label=name.capitalize(),
                alpha=0.8, edgecolors='white', linewidth=0.5, s=60)

plt.xlabel(f'第一主成分 (PC1, {explained_var_ratio[0]:.1%})', fontsize=12)
plt.ylabel(f'第二主成分 (PC2, {explained_var_ratio[1]:.1%})', fontsize=12)
plt.title('QIAN DATA: PCA 2D投影 - ' + today, fontsize=13)
plt.legend(fontsize=11)
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig('pca_2d_projection.png', dpi=200)
plt.show()

pca_2d_projection.png

解读: Setosa(红色圆圈)在PC1方向上与另外两个品种完全分离——它位于PC1的负半轴。Versicolor(蓝色方块)和Virginica(绿色三角)在PC2方向上部分重叠但整体可分。PC1成功捕获了Setosa与其他品种之间的主要差异(主要是花瓣长度和宽度的差异)。

八、可视化4:3D主成分投影(mplot3d)

有时2D不足以展示完整的分离模式。下面用 mplot3d 绘制前3个主成分的三维投影。

python
from mpl_toolkits.mplot3d import Axes3D

# 取前3个主成分的投影
X_pca_3d = X_centered @ eigenvectors[:, :3]

fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')

colors = ['#e74c3c', '#3498db', '#2ecc71']
for i, name in enumerate(target_names):
    mask = (y == i)
    ax.scatter(X_pca_3d[mask, 0], X_pca_3d[mask, 1], X_pca_3d[mask, 2],
               c=colors[i], marker='o', label=name.capitalize(),
               alpha=0.8, edgecolors='white', linewidth=0.5, s=50)

ax.set_xlabel(f'PC1 ({explained_var_ratio[0]:.1%})', fontsize=11, labelpad=10)
ax.set_ylabel(f'PC2 ({explained_var_ratio[1]:.1%})', fontsize=11, labelpad=10)
ax.set_zlabel(f'PC3 ({explained_var_ratio[2]:.1%})', fontsize=11, labelpad=10)
ax.set_title('QIAN DATA: PCA 3D投影 - ' + today, fontsize=13)
ax.legend(fontsize=11)

# 设置视角,使分离效果最佳
ax.view_init(elev=20, azim=45)
plt.tight_layout()
plt.savefig('pca_3d_projection.png', dpi=200)
plt.show()

pca_3d_projection.png

解读: 3D空间中,三个品种的分离更加清晰。PC3虽然只贡献了1.71%的方差,但在区分Versicolor(蓝色)和Virginica(绿色)时提供了额外帮助。这也说明——方差最大化的方向不一定是分类信息最大化的方向,详见后文局限性分析。

九、可视化5:Biplot——主成分方向与原始变量贡献

Biplot同时展示两样东西:

  1. 散点: 样本在主成分空间中的坐标(已标准化)
  2. 箭头: 原始特征向量在主成分空间中的投影方向

箭头越长,说明该原始变量对对应主成分的贡献越大;箭头之间的夹角越小,说明两个原始变量越相关。

python
from sklearn.preprocessing import StandardScaler

# 标准化数据(Biplot通常用标准化后的数据)
X_scaled = StandardScaler().fit_transform(X)

# 用sklearn PCA 拟合2个主成分
pca_bi = PCA(n_components=2)
X_pca_bi = pca_bi.fit_transform(X_scaled)

plt.figure(figsize=(9, 8))

# 绘制样本散点
for i, name in enumerate(target_names):
    mask = (y == i)
    plt.scatter(X_pca_bi[mask, 0], X_pca_bi[mask, 1],
                c=colors[i], marker=markers[i], label=name.capitalize(),
                alpha=0.6, edgecolors='white', linewidth=0.5, s=50)

# 绘制原始特征的箭头(使用sklearn中的components_)
# components_ 的每一行是一个主成分,每一列对应一个原始特征
# 箭头方向 = components_ 的转置
components = pca_bi.components_.T  # shape: (4, 2)

# 缩放因子,让箭头大小适配散点范围
scale_factor = np.max(np.abs(X_pca_bi)) * 0.8

for i, feature in enumerate(feature_names):
    arrow_x = components[i, 0] * scale_factor
    arrow_y = components[i, 1] * scale_factor
    plt.arrow(0, 0, arrow_x, arrow_y,
              head_width=0.3, head_length=0.2, fc='darkred', ec='darkred',
              alpha=0.9, linewidth=2)
    plt.text(arrow_x * 1.1, arrow_y * 1.1,
             feature[:8], fontsize=10, color='darkred', fontweight='bold')

# 添加单位圆参考
circle = plt.Circle((0, 0), scale_factor, fill=False, linestyle='--',
                     alpha=0.3, color='gray')
plt.gca().add_patch(circle)

plt.xlabel(f'PC1 ({pca_bi.explained_variance_ratio_[0]:.1%})', fontsize=12)
plt.ylabel(f'PC2 ({pca_bi.explained_variance_ratio_[1]:.1%})', fontsize=12)
plt.title('QIAN DATA: PCA Biplot - ' + today, fontsize=13)
plt.axhline(y=0, color='gray', linestyle='-', alpha=0.3)
plt.axvline(x=0, color='gray', linestyle='-', alpha=0.3)
plt.legend(loc='best', fontsize=10)
plt.grid(alpha=0.2)
plt.axis('equal')
plt.tight_layout()
plt.savefig('pca_biplot.png', dpi=200)
plt.show()

pca_biplot.png

解读:

  • 花瓣长度(petal length) 和 花瓣宽度(petal width) 的箭头非常接近,且都指向PC1正方向——说明这两个特征高度相关,共同贡献了PC1的大部分方差。
  • 花萼宽度(sepal width) 指向PC2的正方向,是PC2的主要贡献者。
  • Setosa样本位于PC1负方向的上方(高PC2),与其小花瓣、宽花萼的特征一致。
  • 箭头的长度反映了变量对主成分的重要性——花瓣相关的两个特征箭头最长,说明它们是最有区分力的特征。

十、用sklearn验证

使用 sklearn.decomposition.PCA 验证我们的手动实现。

python
# 标准化(sklearn PCA推荐先标准化)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# 拟合sklearn PCA
pca_sklearn = PCA(n_components=2)
X_pca_sklearn = pca_sklearn.fit_transform(X_scaled)

print("sklearn PCA 方差解释率:", np.round(pca_sklearn.explained_variance_ratio_, 4))
print("sklearn PCA 累计方差解释率:", np.round(np.cumsum(pca_sklearn.explained_variance_ratio_), 4))
print("\n前5个样本的2D投影:")
print(np.round(X_pca_sklearn[:5], 3))

预期输出:

sklearn PCA 方差解释率: [0.7297 0.2285]
sklearn PCA 累计方差解释率: [0.7297 0.9582]

前5个样本的2D投影:
[[-2.264  0.48 ]
 [-2.081 -0.674]
 [-2.364 -0.342]
 [-2.299 -0.597]
 [-2.39   0.047]]

注意:这里使用标准化后,方差解释率与之前不同了(PC1=72.97%,而非92.46%)。原因是标准化让每个特征具有相同的权重(方差=1),花瓣的方差优势被消除,各特征更加均衡地贡献到主成分中。在实践中,当特征量纲不同时,标准化是必须的一步。

十一、PCA的降维效应

用KNN分类器验证降维是否影响分类效果。

python
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import cross_val_score

# 原始4维数据 vs PCA 2维数据
knn_original = KNeighborsClassifier()
knn_pca = KNeighborsClassifier()

score_original = cross_val_score(knn_original, X_scaled, y, cv=5)
score_pca = cross_val_score(knn_pca, X_pca_sklearn, y, cv=5)

print(f"原始4维 KNN 准确率: {score_original.mean():.2%} ± {score_original.std():.2%}")
print(f"PCA 2维 KNN 准确率: {score_pca.mean():.2%} ± {score_pca.std():.2%}")

# 降维带来的速度优势
import time
start = time.time()
for _ in range(100):
    cross_val_score(knn_original, X_scaled, y, cv=5)
time_original = time.time() - start

start = time.time()
for _ in range(100):
    cross_val_score(knn_pca, X_pca_sklearn, y, cv=5)
time_pca = time.time() - start

print(f"\n100次交叉验证耗时:")
print(f"  原始4维: {time_original:.2f}s")
print(f"  PCA 2维: {time_pca:.2f}s")
print(f"  加速比: {time_original / time_pca:.1f}x")

预期输出:

原始4维 KNN 准确率: 95.33% ± 3.79%
PCA 2维 KNN 准确率: 92.00% ± 4.22%

100次交叉验证耗时:
  原始4维: 1.85s
  PCA 2维: 1.21s
  加速比: 1.5x

虽然PCA降维后准确率有轻微下降(约3%),但训练速度提升1.5倍,且模型更简单、可解释性更强。在更大规模的数据上,这种收益会更加显著。

十二、PCA的局限性分析

12.1 线性假设 ​

PCA是线性方法——它只能找到数据中的线性相关性结构。当数据具有非线性流形结构时(如瑞士卷、环形分布),PCA无法正确展开数据,降维后的投影会严重扭曲数据的真实结构。

python
# 非线性数据示例:同心圆
from sklearn.datasets import make_circles

X_circle, y_circle = make_circles(n_samples=300, factor=0.5, noise=0.05)

pca_circle = PCA(n_components=2)
X_circle_pca = pca_circle.fit_transform(X_circle)

plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.scatter(X_circle[:, 0], X_circle[:, 1], c=y_circle, cmap='coolwarm',
            alpha=0.6, edgecolors='white')
plt.title('原始同心圆数据', fontsize=12)
plt.axis('equal')

plt.subplot(1, 2, 2)
plt.scatter(X_circle_pca[:, 0], X_circle_pca[:, 1], c=y_circle, cmap='coolwarm',
            alpha=0.6, edgecolors='white')
plt.title('PCA降维后(投影重叠)', fontsize=12)
plt.axis('equal')
plt.tight_layout()
plt.savefig('pca_nonlinear_failure.png', dpi=200)
plt.show()

pca_nonlinear_failure.png

解读: 同心圆的内外圈在原始空间中是完全可分的(通过到圆心的距离),但PCA只考虑方差最大化方向,将两个圈投影到一条直线上,导致严重重叠。这种情况下,t-SNE、UMAP或Kernel PCA(见下文)能更好地保留数据拓扑结构。

12.2 方差最大化 ≠ 分类信息最大化 ​

PCA的目标函数是最大化方差,而非最大化类别区分度。这意味着:

  • PCA不利用标签信息,属于无监督方法。它不知道数据的类别标签,只是寻找数据整体散布最大的方向。
  • 如果最重要的分类信息恰好位于方差最小的方向上,PCA会将其丢弃,导致分类性能下降。
  • 有监督的降维方法(如LDA——线性判别分析)可能更适合分类任务。LDA的目标是最大化类间散度与类内散度之比,直接优化类别分离性。

一个经典的反例: 假设有两个类别,它们在PC1方向上的投影完全重叠,但在PC2方向(方差很小)上完全分离。PCA会优先保留PC1(方差大)而丢弃PC2,结果反而损失了最重要的分类信息。这就是为什么在分类任务中,不能盲目信任PCA的降维结果,而应该用交叉验证来评估PCA保留多少维最优。

Iris数据的启示: 在Iris数据中,PC1单独解释了92.46%的方差,且三个类别在PC1上已经明显分层(Setosa在负半轴,其他两个在正半轴),所以PCA在这个特例中巧合地也找到了对分类有用的方向。但这并非必然——在实践中应该验证这一假设。

12.3 对异常值敏感 ​

由于PCA基于方差(二次项),异常值会在协方差矩阵中产生很大影响,严重扭曲主成分方向。在使用PCA之前,应检查并处理异常值。

12.4 可解释性挑战 ​

主成分是原始特征的线性组合,通常每个主成分都包含所有原始特征的权重。这使得主成分的物理含义不如原始特征直观。例如,PC1 = 0.52×花萼长度 - 0.27×花萼宽度 + 0.58×花瓣长度 + 0.56×花瓣宽度,很难用一个简洁的名称概括。

12.5 何时慎用PCA ​

场景推荐做法
特征高度线性相关✅ PCA非常有效
数据有非线性流形结构❌ 改用Kernel PCA、t-SNE、UMAP
分类任务需要保持类别分离❌ 改用LDA或有监督方法
需要可解释的特征❌ 改用特征选择方法
数据量纲差异大✅ 必须先标准化再PCA

十三、扩展:Kernel PCA简介

为了克服PCA的线性限制,Kernel PCA引入核技巧,在高维特征空间中执行PCA。

13.1 核心思想 ​

  1. 通过非线性映射 ϕ:Rp→F 将原始数据映射到高维特征空间。这个 ϕ 通常是非线性函数,例如将二维点 (x1,x2) 映射到 (x1,x2,x12,x22,x1x2),使得原本线性不可分的数据在高维空间中变得线性可分。
  2. 在特征空间 F 中执行标准PCA——找到 ϕ(X) 的主成分方向。由于 F 的维度可能非常高(甚至无穷维),无法直接计算 ϕ(X)。
  3. 利用核技巧避免显式计算 ϕ(x):PCA中只需要计算样本之间的内积 ϕ(xi)Tϕ(xj),而核函数 k(xi,xj)=ϕ(xi)Tϕ(xj) 可以直接在原始空间中高效计算(无需知道 ϕ 的具体形式)。

核PCA最终只需要计算核矩阵 Kij=k(xi,xj)(一个 n×n 的矩阵),然后对中心化后的核矩阵做特征值分解。

常用的核函数包括:

  • RBF核(高斯核): k(xi,xj)=exp⁡(−γ∥xi−xj∥2),最常用的核函数,只有一个参数 γ 需要调节。
  • 多项式核: k(xi,xj)=(xiTxj+r)d,d 控制多项式次数。
  • Sigmoid核: k(xi,xj)=tanh⁡(αxiTxj+r),源自神经网络。

其中RBF核是最常用的选择,因为它只有一个参数且能逼近任意复杂的非线性映射。

13.2 Kernel PCA vs 标准PCA:同心圆数据演示 ​

下面用同心圆数据直观展示二者的区别——标准PCA完全无法分离内圈和外圈,而Kernel PCA可以。

python
from sklearn.decomposition import KernelPCA

# 在同心圆数据上测试Kernel PCA
kpca = KernelPCA(n_components=2, kernel='rbf', gamma=15)
X_circle_kpca = kpca.fit_transform(X_circle)

plt.figure(figsize=(10, 4))

plt.subplot(1, 2, 1)
plt.scatter(X_circle[:, 0], X_circle[:, 1], c=y_circle, cmap='coolwarm',
            alpha=0.6, edgecolors='white')
plt.title('原始同心圆数据', fontsize=12)
plt.axis('equal')

plt.subplot(1, 2, 2)
plt.scatter(X_circle_kpca[:, 0], X_circle_kpca[:, 1], c=y_circle, cmap='coolwarm',
            alpha=0.6, edgecolors='white')
plt.title('Kernel PCA (RBF核) 降维后', fontsize=12)
plt.axis('equal')
plt.tight_layout()
plt.savefig('pca_kernel_pca.png', dpi=200)
plt.show()

pca_kernel_pca.png

解读: 与标准PCA不同,Kernel PCA成功将同心圆数据展开为线性可分的结构。RBF核将数据映射到高维空间,使得原本在欧氏空间中难以分离的内外圈变得线性可分。注意Kernel PCA的两个主成分(KPC1和KPC2)不再具有"方差大小依次递减"的性质,因为经过核映射后的空间结构更加复杂。

Kernel PCA的局限性:

  • 需要调节核参数(如RBF的 γ),不同参数产生截然不同的结果
  • 核矩阵是 n×n,当样本量很大时(n>104),计算和存储成本过高
  • 投影结果难以解释——KPC1和KPC2没有物理含义,只是抽象的非线性组合
  • 没有直接的逆变换(从KPC回到原始空间),不适合数据重建任务

13.3 在Iris上的效果对比 ​

python
# Iris数据上对比PCA和Kernel PCA
kpca_iris = KernelPCA(n_components=2, kernel='rbf', gamma=1)
X_iris_kpca = kpca_iris.fit_transform(X_scaled)

plt.figure(figsize=(12, 5))

plt.subplot(1, 2, 1)
for i, name in enumerate(target_names):
    mask = (y == i)
    plt.scatter(X_pca_sklearn[mask, 0], X_pca_sklearn[mask, 1],
                c=colors[i], marker=markers[i], label=name.capitalize(),
                alpha=0.7, edgecolors='white', s=50)
plt.title('标准PCA', fontsize=12)
plt.xlabel('PC1'); plt.ylabel('PC2')
plt.legend(); plt.grid(alpha=0.3)

plt.subplot(1, 2, 2)
for i, name in enumerate(target_names):
    mask = (y == i)
    plt.scatter(X_iris_kpca[mask, 0], X_iris_kpca[mask, 1],
                c=colors[i], marker=markers[i], label=name.capitalize(),
                alpha=0.7, edgecolors='white', s=50)
plt.title('Kernel PCA (RBF, γ=1)', fontsize=12)
plt.xlabel('KPC1'); plt.ylabel('KPC2')
plt.legend(); plt.grid(alpha=0.3)

plt.tight_layout()
plt.savefig('pca_vs_kpca_iris.png', dpi=200)
plt.show()

pca_vs_kpca_iris.png

对于Iris这种线性可分结构较强的数据,标准PCA已经表现很好,Kernel PCA没有明显优势。但当数据呈现非线性结构时(如同心圆、瑞士卷),Kernel PCA的价值就会充分体现。

十四、PCA使用流程总结

原始数据 (n×p)
    │
    ├─ 检查缺失值、异常值 → 处理
    │
    ├─ 标准化 (StandardScaler)
    │   └─ 均值为0,标准差为1
    │
    ├─ 计算PCA
    │   ├─ 方法1:协方差矩阵 → EVD
    │   ├─ 方法2:数据矩阵 → SVD(推荐)
    │   └─ sklearn:PCA() 底层用SVD
    │
    ├─ 确定保留维度
    │   ├─ 碎石图找拐点
    │   ├─ 累积方差≥90%/95%
    │   ├─ Kaiser准则(λ > 1)
    │   └─ 交叉验证(分类/回归任务)
    │
    ├─ 投影到低维空间
    │
    └─ 下游任务
        ├─ 可视化(2D/3D散点图)
        ├─ 分类/回归(降维后建模)
        ├─ 去噪(丢弃小方差成分后重建)
        └─ 特征压缩(后续分析)

PCA的假设: 方差最大的方向就是信息最多的方向。当重要信息不在方差最大的方向时(如信号弱但区分度高的特征),PCA不适用。对于分类任务,建议对比LDA的结果。

标准化: 如果特征量纲不同(如cm vs kg),必须先标准化,否则方差大的特征会主导主成分。在Iris数据中,未标准化时花瓣长度(方差3.12)对PC1的主导远超花萼宽度(方差0.19),但标准化后各特征贡献更均衡。

何时用SVD版PCA: 大多数情况下直接使用 sklearn.decomposition.PCA(基于SVD)。当 n < p 时(基因数据常见),SVD版PCA尤其高效。当需要截断(只计算前k个主成分)时,使用 TruncatedSVD。

PCA vs t-SNE vs UMAP: PCA是线性方法、可解释、可逆变换;t-SNE和UMAP是非线性方法、注重保持局部邻域结构、不可逆。PCA适合做预处理和初步分析,t-SNE/UMAP适合做可视化。

Kernel PCA调参注意: 核函数(主要是RBF核)的gamma参数对结果敏感,gamma太小近似线性PCA,gamma太大则过拟合。通常需要通过网格搜索选择最佳参数。

十一、数学文化:降维的三次思想跃迁

11.1 卡尔·皮尔逊(Karl Pearson, 1857-1936) ​

英国统计学家,1901年首次提出主成分分析(Principal Component Analysis)的思想。他在《关于最大方差方向》一文中证明:在高维数据中,沿着最大方差方向投影能保留最多的信息。这个发现比矩阵特征值分解的计算机应用早了半个世纪。

11.2 哈罗德·霍特林(Harold Hotelling, 1895-1973) ​

美国统计学家,1933年正式将PCA发展为一种统计方法。他不仅给出了PCA的数学推导,还提出了保留方差比例作为选择主成分数量的准则——这个准则至今仍在沿用。霍特林还发展了正则相关分析(CCA),是降维理论的另一个支柱。

11.3 伊戈尔·T·乔洛夫斯基(Igor T. Jolliffe, 1944-) ​

英国统计学家,1986年出版的《主成分分析》是PCA领域最权威的专著。他在书中系统总结了PCA的理论、算法和应用——从协方差矩阵的特征分解到SVD实现,从降维可视化到变量选择,为PCA成为数据分析的标准工具做出了决定性贡献。


关注公众号:QIAN数据