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通过找到数据方差最大的方向(主成分)来进行降维。

核心步骤:

  1. 数据中心化
  2. 计算协方差矩阵
  3. 特征值分解
  4. 选择前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)是一种非线性降维方法,特别适合高维数据可视化。

核心思想:

  1. 在高维空间中计算点对的相似度(高斯分布)
  2. 在低维空间中用t分布表示相似度
  3. 最小化两个分布的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

本文最后一次更新为 天前,文章中的某些内容可能已过时!