PCA、LDA与t-SNE详解
前言
高维数据在机器学习中无处不在,但”维度灾难”会导致计算效率低下和过拟合。降维技术通过减少特征数量来解决这些问题,同时保留数据的关键信息。
为什么需要降维
维度灾难
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
# 可视化高维空间中数据稀疏性
def visualize_curse_of_dimensionality():
dimensions = range(1, 51)
n_samples = 1000
# 计算高维空间中点到原点的平均距离
avg_distances = []
for d in dimensions:
points = np.random.uniform(0, 1, (n_samples, d))
distances = np.linalg.norm(points, axis=1)
avg_distances.append(np.mean(distances))
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(dimensions, avg_distances, 'b-', linewidth=2)
ax.set_xlabel('维度')
ax.set_ylabel('平均距离')
ax.set_title('维度灾难:高维空间中点的稀疏性')
ax.grid(True, alpha=0.3)
plt.show()
visualize_curse_of_dimensionality()
降维的好处
| 优势 | 说明 |
|---|---|
| 减少计算成本 | 更少的特征意味着更快的训练 |
| 避免过拟合 | 降低模型复杂度 |
| 数据可视化 | 将高维数据投影到2D/3D |
| 去除噪声 | 保留主要信息,过滤噪声 |
主成分分析(PCA)
数学原理
PCA通过找到数据方差最大的方向(主成分)来进行降维。
核心步骤:
- 数据中心化
- 计算协方差矩阵
- 特征值分解
- 选择前k个主成分
class PCAFromScratch:
"""从零实现PCA"""
def __init__(self, n_components):
self.n_components = n_components
self.components = None
self.mean = None
self.explained_variance = None
self.explained_variance_ratio = None
def fit(self, X):
# 中心化
self.mean = np.mean(X, axis=0)
X_centered = X - self.mean
# 计算协方差矩阵
cov_matrix = np.cov(X_centered.T)
# 特征值分解
eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)
# 排序(降序)
idx = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]
# 选择前n_components个主成分
self.components = eigenvectors[:, :self.n_components].T
self.explained_variance = eigenvalues[:self.n_components]
self.explained_variance_ratio = eigenvalues[:self.n_components] / np.sum(eigenvalues)
return self
def transform(self, X):
X_centered = X - self.mean
return X_centered @ self.components.T
def fit_transform(self, X):
self.fit(X)
return self.transform(X)
# 测试
from sklearn.datasets import load_iris
iris = load_iris()
X = iris.data
y = iris.target
# 使用自定义PCA
pca_custom = PCAFromScratch(n_components=2)
X_pca_custom = pca_custom.fit_transform(X)
print(f"原始数据形状: {X.shape}")
print(f"降维后形状: {X_pca_custom.shape}")
print(f"解释方差比例: {pca_custom.explained_variance_ratio}")
使用sklearn实现
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
# 标准化(PCA对尺度敏感)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# PCA
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)
# 可视化
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 降维结果
ax = axes[0]
scatter = ax.scatter(X_pca[:, 0], X_pca[:, 1], c=y, cmap='viridis', edgecolors='black')
ax.set_xlabel('PC1')
ax.set_ylabel('PC2')
ax.set_title('PCA降维结果')
plt.colorbar(scatter, ax=ax)
# 解释方差
ax = axes[1]
pca_full = PCA()
pca_full.fit(X_scaled)
cumsum = np.cumsum(pca_full.explained_variance_ratio_)
ax.bar(range(1, len(cumsum)+1), pca_full.explained_variance_ratio_, alpha=0.7, label='单个')
ax.plot(range(1, len(cumsum)+1), cumsum, 'r-o', label='累计')
ax.axhline(y=0.95, color='gray', linestyle='--', label='95%阈值')
ax.set_xlabel('主成分')
ax.set_ylabel('解释方差比例')
ax.set_title('解释方差')
ax.legend()
plt.tight_layout()
plt.show()
print(f"前2个主成分解释方差: {sum(pca.explained_variance_ratio_):.2%}")
线性判别分析(LDA)
与PCA的区别
| 特性 | PCA | LDA |
|---|---|---|
| 类型 | 无监督 | 有监督 |
| 目标 | 最大化方差 | 最大化类间可分性 |
| 使用标签 | 否 | 是 |
LDA原理
LDA寻找最大化类间方差与类内方差比值的投影方向。
class LDAFromScratch:
"""从零实现LDA"""
def __init__(self, n_components):
self.n_components = n_components
self.components = None
def fit(self, X, y):
n_features = X.shape[1]
classes = np.unique(y)
# 计算总体均值
mean_overall = np.mean(X, axis=0)
# 初始化类内散度矩阵和类间散度矩阵
S_W = np.zeros((n_features, n_features))
S_B = np.zeros((n_features, n_features))
for c in classes:
X_c = X[y == c]
mean_c = np.mean(X_c, axis=0)
# 类内散度
S_W += (X_c - mean_c).T @ (X_c - mean_c)
# 类间散度
n_c = X_c.shape[0]
mean_diff = (mean_c - mean_overall).reshape(-1, 1)
S_B += n_c * (mean_diff @ mean_diff.T)
# 求解特征值问题
A = np.linalg.inv(S_W) @ S_B
eigenvalues, eigenvectors = np.linalg.eig(A)
# 排序并选择
idx = np.argsort(np.abs(eigenvalues))[::-1]
eigenvectors = eigenvectors[:, idx]
self.components = eigenvectors[:, :self.n_components].T.real
return self
def transform(self, X):
return X @ self.components.T
def fit_transform(self, X, y):
self.fit(X, y)
return self.transform(X)
# 测试
lda_custom = LDAFromScratch(n_components=2)
X_lda_custom = lda_custom.fit_transform(X, y)
print(f"LDA降维后形状: {X_lda_custom.shape}")
使用sklearn实现
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
# LDA
lda = LinearDiscriminantAnalysis(n_components=2)
X_lda = lda.fit_transform(X_scaled, y)
# 对比PCA和LDA
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
ax = axes[0]
scatter = ax.scatter(X_pca[:, 0], X_pca[:, 1], c=y, cmap='viridis', edgecolors='black')
ax.set_xlabel('PC1')
ax.set_ylabel('PC2')
ax.set_title('PCA')
ax = axes[1]
scatter = ax.scatter(X_lda[:, 0], X_lda[:, 1], c=y, cmap='viridis', edgecolors='black')
ax.set_xlabel('LD1')
ax.set_ylabel('LD2')
ax.set_title('LDA')
plt.tight_layout()
plt.show()
t-SNE
原理
t-SNE(t-distributed Stochastic Neighbor Embedding)是一种非线性降维方法,特别适合高维数据可视化。
核心思想:
- 在高维空间中计算点对的相似度(高斯分布)
- 在低维空间中用t分布表示相似度
- 最小化两个分布的KL散度
from sklearn.manifold import TSNE
# t-SNE
tsne = TSNE(n_components=2, perplexity=30, random_state=42)
X_tsne = tsne.fit_transform(X_scaled)
# 对比三种方法
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
ax = axes[0]
ax.scatter(X_pca[:, 0], X_pca[:, 1], c=y, cmap='viridis', edgecolors='black')
ax.set_title('PCA')
ax.set_xlabel('PC1')
ax.set_ylabel('PC2')
ax = axes[1]
ax.scatter(X_lda[:, 0], X_lda[:, 1], c=y, cmap='viridis', edgecolors='black')
ax.set_title('LDA')
ax.set_xlabel('LD1')
ax.set_ylabel('LD2')
ax = axes[2]
ax.scatter(X_tsne[:, 0], X_tsne[:, 1], c=y, cmap='viridis', edgecolors='black')
ax.set_title('t-SNE')
ax.set_xlabel('t-SNE1')
ax.set_ylabel('t-SNE2')
plt.tight_layout()
plt.show()
困惑度参数
# 不同困惑度的效果
perplexities = [5, 30, 50, 100]
fig, axes = plt.subplots(1, 4, figsize=(20, 4))
for ax, perp in zip(axes, perplexities):
tsne = TSNE(n_components=2, perplexity=perp, random_state=42)
X_tsne = tsne.fit_transform(X_scaled)
ax.scatter(X_tsne[:, 0], X_tsne[:, 1], c=y, cmap='viridis', s=30)
ax.set_title(f'Perplexity = {perp}')
plt.tight_layout()
plt.show()
UMAP
简介
UMAP(Uniform Manifold Approximation and Projection)是比t-SNE更快的降维方法。
try:
from umap import UMAP
# UMAP
umap = UMAP(n_components=2, random_state=42)
X_umap = umap.fit_transform(X_scaled)
# 对比t-SNE和UMAP
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
ax = axes[0]
ax.scatter(X_tsne[:, 0], X_tsne[:, 1], c=y, cmap='viridis', edgecolors='black')
ax.set_title('t-SNE')
ax = axes[1]
ax.scatter(X_umap[:, 0], X_umap[:, 1], c=y, cmap='viridis', edgecolors='black')
ax.set_title('UMAP')
plt.tight_layout()
plt.show()
except ImportError:
print("UMAP未安装,请运行: pip install umap-learn")
特征选择 vs 特征提取
区别
| 方法 | 类型 | 特点 |
|---|---|---|
| 特征选择 | 选择原始特征子集 | 保持可解释性 |
| 特征提取 | 创建新特征 | 可能损失可解释性 |
特征选择方法
from sklearn.feature_selection import SelectKBest, f_classif, RFE
from sklearn.ensemble import RandomForestClassifier
# 方差阈值
from sklearn.feature_selection import VarianceThreshold
# 生成更高维的数据
from sklearn.datasets import make_classification
X_high, y_high = make_classification(n_samples=500, n_features=20,
n_informative=10, random_state=42)
# 1. 方差阈值
selector_var = VarianceThreshold(threshold=0.1)
X_var = selector_var.fit_transform(X_high)
print(f"方差阈值后: {X_high.shape} -> {X_var.shape}")
# 2. 单变量特征选择
selector_kbest = SelectKBest(f_classif, k=10)
X_kbest = selector_kbest.fit_transform(X_high, y_high)
print(f"SelectKBest后: {X_high.shape} -> {X_kbest.shape}")
# 3. 递归特征消除
rf = RandomForestClassifier(n_estimators=50, random_state=42)
rfe = RFE(rf, n_features_to_select=10)
X_rfe = rfe.fit_transform(X_high, y_high)
print(f"RFE后: {X_high.shape} -> {X_rfe.shape}")
实际应用
图像降维示例
from sklearn.datasets import load_digits
# 加载手写数字数据
digits = load_digits()
X_digits = digits.data
y_digits = digits.target
print(f"原始数据形状: {X_digits.shape}") # 64维
# 应用不同降维方法
pca_digits = PCA(n_components=2).fit_transform(X_digits)
tsne_digits = TSNE(n_components=2, random_state=42).fit_transform(X_digits)
# 可视化
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
ax = axes[0]
scatter = ax.scatter(pca_digits[:, 0], pca_digits[:, 1], c=y_digits,
cmap='tab10', s=10, alpha=0.7)
ax.set_title('PCA - 手写数字')
ax.set_xlabel('PC1')
ax.set_ylabel('PC2')
plt.colorbar(scatter, ax=ax)
ax = axes[1]
scatter = ax.scatter(tsne_digits[:, 0], tsne_digits[:, 1], c=y_digits,
cmap='tab10', s=10, alpha=0.7)
ax.set_title('t-SNE - 手写数字')
ax.set_xlabel('t-SNE1')
ax.set_ylabel('t-SNE2')
plt.colorbar(scatter, ax=ax)
plt.tight_layout()
plt.show()
常见问题
Q1: PCA需要标准化吗?
是的,PCA对特征尺度敏感,应先进行标准化。
Q2: t-SNE能用于新数据吗?
不能直接用于新数据,它没有显式的transform方法。可以使用参数化t-SNE或UMAP。
Q3: 如何选择主成分数量?
- 累计解释方差达到95%
- 肘部法则
- 交叉验证
Q4: LDA最多能降到多少维?
最多降到 $\min(n_{classes} - 1, n_{features})$ 维。
总结
| 方法 | 类型 | 特点 | 适用场景 |
|---|---|---|---|
| PCA | 线性 | 最大化方差 | 一般降维 |
| LDA | 线性 | 有监督,最大化可分性 | 分类预处理 |
| t-SNE | 非线性 | 保持局部结构 | 可视化 |
| UMAP | 非线性 | 快速,保持全局结构 | 可视化 |
参考资料
- Jolliffe, I. T. (2002). “Principal Component Analysis”
- van der Maaten, L. (2008). “Visualizing Data using t-SNE”
- McInnes, L. et al. (2018). “UMAP: Uniform Manifold Approximation and Projection”
- Scikit-learn官方文档
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:《 机器学习基础系列——降维技术 》
本文链接:http://localhost:3015/ai/%E9%99%8D%E7%BB%B4%E6%8A%80%E6%9C%AF.html
本文最后一次更新为 天前,文章中的某些内容可能已过时!