降维算法

本节定位
真实数据往往有几十甚至上千个特征。降维能减少特征数量,同时保留重要信息——既能加速训练,又能帮助可视化。本节在第 4 站 PCA 基础上深入实战应用。
学习目标
- 深入理解 PCA 的原理与实战应用(与第 4 站衔接)
- 掌握方差解释比分析
- 了解 t-SNE 可视化原理与使用
- 了解 UMAP 降维方法
先说一个很重要的学习预期
这一节很容易让新人一开始就被工具名带偏:
- PCA
- t-SNE
- UMAP
但更适合第一遍先学会的不是背工具差异,而是先分清:
你是在为了建模预处理做降维,还是为了可视化探索做降维。
只要这个目的先分清,后面的方法选择就会顺很多。
先建立一张地图
降维这节很容易被学成“会几个工具名”,但真正重要的是先分清目的。
因为你做降维,可能是在解决完全不同的问题:
- 想压缩特征,加速训练
- 想降低噪声和相关性
- 想把高维数据画出来看看结构
更稳的学习顺序是:
先把“为了建模”和“为了可视化”分开,是这节最重要的第一步。
一、为什么需要降维?
1.1 高维数据的问题
| 问题 | 说明 |
|---|---|
| 维度灾难 | 特征越多,数据越稀疏,模型越难学习 |
| 计算成本 | 特征多 → 训练慢、内存大 |
| 多重共线性 | 很多特征高度相关,是冗余的 |
| 可视化 | 超过 3 维的数据无法直接画图 |
1.2 降维的两种思路
| 思路 | 方法 | 说明 |
|---|---|---|
| 特征选择 | 挑选重要特征 | 保留原始特征的子集 |
| 特征提取 | 生成新特征 | 把原始特征变换成更少的新特征(PCA、t-SNE) |
1.3 第一次学降维,最容易混的点
很多新人会把“降维”和“删特征”混在一起。
其实它们不是一回事:
- 特征选择:保留原始列里的某几列
- 降维:把原始列重新组合成更少的新轴
所以 PCA 之后得到的主成分, 已经不再是原始那几个字段本身,而是它们的线性组合。
1.3.1 一个更适合新人的类比
你可以先把降维想成:
- 把一大包零散信息重新压缩成更少的几条主线
这不是简单地把一些字段删掉,
而更像是把很多原始特征重新拧成几根“信息更浓缩的新轴”。
所以降维最值得先记住的,不是算法名字,而是:
- 它在做信息压缩和表示重组
二、PCA 实战
2.1 回顾原理
与第 4 站的衔接
在第 4 站 1.3 节"特征值与特征向量"中,你已经学过 PCA 的数学原理:
- 计算协方差矩阵
- 求特征值和特征向量
- 选最大特征值对应的方向作为主成分
本节重点是实战应用——如何在真实数据上使用 PCA。
PCA 核心思想:找到数据方差最大的方向,投影过去。
2.2 手写数字降维
from sklearn.datasets import load_digits
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
import numpy as np
import matplotlib.pyplot as plt
# 加载手写数字数据
digits = load_digits()
X, y = digits.data, digits.target
print(f"原始数据: {X.shape[0]} 样本, {X.shape[1]} 特征")
# 先看几个样本
fig, axes = plt.subplots(2, 10, figsize=(15, 3))
for i, ax in enumerate(axes.ravel()):
ax.imshow(digits.images[i], cmap='gray')
ax.set_title(str(y[i]), fontsize=9)
ax.axis('off')
plt.suptitle('手写数字样本(8×8 = 64 个特征)')
plt.tight_layout()
plt.show()
# 标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# PCA 降到 2 维
pca_2d = PCA(n_components=2)
X_2d = pca_2d.fit_transform(X_scaled)
print(f"降维后: {X_2d.shape}")
print(f"保留方差比: {pca_2d.explained_variance_ratio_.sum():.1%}")
# 可视化
plt.figure(figsize=(10, 8))
scatter = plt.scatter(X_2d[:, 0], X_2d[:, 1], c=y, cmap='tab10', s=10, alpha=0.6)
plt.colorbar(scatter, label='数字')
plt.xlabel(f'PC1(方差占比 {pca_2d.explained_variance_ratio_[0]:.1%})')
plt.ylabel(f'PC2(方差占比 {pca_2d.explained_variance_ratio_[1]:.1%})')
plt.title('PCA 降维到 2D(手写数字)')
plt.grid(True, alpha=0.3)
plt.show()
2.3 方差解释比分析
关键问题:保留多少个主成分才够?
# 用全部主成分
pca_full = PCA()
pca_full.fit(X_scaled)
# 方差解释比
explained = pca_full.explained_variance_ratio_
cumulative = np.cumsum(explained)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 每个主成分的方差占比
axes[0].bar(range(1, len(explained)+1), explained, color='steelblue', alpha=0.7)
axes[0].set_xlabel('主成分编号')
axes[0].set_ylabel('方差解释比')
axes[0].set_title('各主成分的方差占比')
axes[0].set_xlim(0, 30)
# 累积方差
axes[1].plot(range(1, len(cumulative)+1), cumulative, 'bo-', markersize=3)
axes[1].axhline(y=0.9, color='r', linestyle='--', label='90% 阈值')
axes[1].axhline(y=0.95, color='orange', linestyle='--', label='95% 阈值')
# 标注达到 90% 的点
n_90 = np.argmax(cumulative >= 0.9) + 1
n_95 = np.argmax(cumulative >= 0.95) + 1
axes[1].axvline(x=n_90, color='r', linestyle=':', alpha=0.5)
axes[1].axvline(x=n_95, color='orange', linestyle=':', alpha=0.5)
axes[1].set_xlabel('主成分数量')
axes[1].set_ylabel('累积方差解释比')
axes[1].set_title('累积方差解释比(Scree Plot)')
axes[1].legend()
for ax in axes:
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print(f"保留 90% 方差需要 {n_90} 个主成分(原始 64 个)")
print(f"保留 95% 方差需要 {n_95} 个主成分(原始 64 个)")
2.3.1 保留 90% 还是 95%,怎么判断更稳?
这没有一个永远固定的答案,但新人第一次做项目时可以先这样:
- 如果你更在意训练速度和压缩率,先试 90%
- 如果你更担心信息丢失,先试 95%
- 最后一定用下游模型性能来验证,而不是只看方差解释比
因为“保留了多少方差”不等于“下游任务一定最好”。
2.4 PCA 对模型性能的影响
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
import time
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 对比不同主成分数量
n_components_list = [2, 5, 10, 20, 30, 64]
results = []
for n in n_components_list:
pipe = make_pipeline(
StandardScaler(),
PCA(n_components=n) if n < 64 else PCA(),
LogisticRegression(max_iter=5000, random_state=42)
)
start = time.time()
pipe.fit(X_train, y_train)
train_time = time.time() - start
score = pipe.score(X_test, y_test)
results.append({'n': n, 'score': score, 'time': train_time})
print(f"PC={n:3d} | 准确率: {score:.1%} | 训练时间: {train_time:.3f}s")
# 可视化
fig, ax1 = plt.subplots(figsize=(8, 5))
ax2 = ax1.twinx()
ns = [r['n'] for r in results]
scores = [r['score'] for r in results]
times = [r['time'] for r in results]
ax1.plot(ns, scores, 'bo-', label='准确率')
ax2.plot(ns, times, 'rs-', label='训练时间')
ax1.set_xlabel('主成分数量')
ax1.set_ylabel('准确率', color='blue')
ax2.set_ylabel('训练时间 (s)', color='red')
ax1.set_title('PCA 降维对模型性能与速度的影响')
ax1.legend(loc='lower right')
ax2.legend(loc='center right')
ax1.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()