Day 21 常见的降维算法

@浙大疏锦行

今日任务:

  1. LDA线性判别
  2. PCA主成分分析
  3. t-sne降维
  • 自由作业:探索下什么时候用到降维?降维的主要应用?或者让ai给你出题,群里的同学互相学习下。可以考虑对比下在某些特定数据集上t-sne的可视化和pca可视化的区别。

在day 20 学习了SVD,在今天需要学习另外三种常见的降维算法:LDA线性判别、PCA主成分分析及t-sne降维。

特征降维

特征降维方法,包括有监督降维无监督降维两种方法,一般来说,很多时候默认为无监督降维。

无监督降维,即在降维过程中不适用任何标签信息(回归中的目标值、分类中的类别),仅输入特征矩阵X,根据数据特征的特点来降维。主要是为了保留数据固有结构、方差、可视化等,下面是常用的方法及其目标:

  • PCA:保留数据中方差大的作为主成分
  • t-SNE,UMAP,Isomap:保留数据的局部或全局流形结构
  • Autoencoder:能够有效重构原始数据的紧凑表示
  • ICA:统计上的独立成分

有监督降维,即在降维过程中使用标签信息,通过输入特征矩阵X和标签向量y,找到一个使得不同类别的数据点能被更好地分开的低维子空间。主要是为了提高后续监督学习的性能。

  • LDA:最大化不同类别之间的可分性,同时最小化同一类别内部的离散度
  • 找到对预测目标变量 y 最有信息量的特征组合

举个例子,PCA(无监督)就像是按身高排队,不考虑这些人会做什么。只要使得相邻的人身高接近就行(总体的差异化),但是兴趣爱好可能完全不同。而LDA(有监督)就像是按运动项目分组,目的是为了参加特定比赛。所以最终的结果是同一组的人适合相同的项目(适合某运动的技能点突出)。

PCA

PCA(Principal Component Analysis,主成分分析),即将一组可能存在相关性的变量,通过线性变换,转换成一组线性不相关的变量(方差最大,称为“主成分”),同时保留数据中最重要的特征,并舍弃次要的、冗余的特征。

对于PCA的计算过程,理论上,主要有以下几个步骤:特征中心化、协方差矩阵计算、特征分解、主成分选择及数据转换。实际上,更常用的方法是使用SVD进行PCA计算。

  • 特征中心化,得到矩阵X
  • 对矩阵X进行奇异值分解:X=UΣV^{T}
  • 右奇异向量V为主成分方向
  • 奇异值评估主成分的重要性(解释方差)
  • 降维后的数据:Xpca​=XVk​(Vk 是 V 的前k列)

下面是用代码来实现上述过程:

from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix
import time
import numpy as np # 确保numpy导入

#根据累积解释方差确定主成分数量

# 步骤 1: 特征缩放
scaler_pca = StandardScaler() #标准化,方差受特征尺度的影响
X_train_scaled_pca = scaler_pca.fit_transform(X_train) #训练并转换训练集
X_test_scaled_pca = scaler_pca.transform(X_test) # 使用在训练集上fit的scaler,转换测试集

# 步骤 2: PCA降维
# 选择降到10维,或者你可以根据累积解释方差来选择,例如:
pca_expl = PCA(random_state=42) #实例化
pca_expl.fit(X_train_scaled_pca) #训练
#计算累积解释方差
#pca_expl.explained_variance_ratio_: 每个主成分所能解释的方差占总方差的百分比
cumsum_variance = np.cumsum(pca_expl.explained_variance_ratio_) #np.cumsum()计算累积和
#确定主成分的数量,保证捕获原数据中95%的方差
n_components_to_keep_95_var = np.argmax(cumsum_variance >= 0.95) + 1 #返回索引,但主成分数量从1开始
print(f"为了保留95%的方差,需要的主成分数量: {n_components_to_keep_95_var}") #26个

对于PCA降维,它适合以下场景:

  • 数据可视化与探索:比如在之前的聚类算法中使用PCA降维到2维进行可视化
  • 多重共线性:特征高度相关,PCA生产的新特征是彼此正交(不相关)
  • 多维数据但样本有限
  • 数据存储与压缩、降噪声

但是,在以下场景中,不适合使用PCA方法:

  • 关键信息存在于低方差维度中:PCA默认‘高方差=高信息量’
  • 非线性数据:如流形、螺旋形等;PCA是线性变换,此时选用t-SNE等非线性降维
  • 解释特征重要性:PCA是原始特征的线性组合
  • 特征尺度不一致:PCA处理前需要标准化

t-SNE

t-SNE(T-distributed stochastic neighbor embedding,t-分布随机邻域嵌入),即将高维空间中的数据点之间的“相似性关系”,尽可能忠实地在低维空间(通常是2D或3D)中保留下来。

具体来说,就是让“在高维空间里靠近的点,在低维空间里也靠近;在高维空间里远离的点,在低维空间里也远离”。属于非线性降维方法,关注局部邻域结构,主要用于高维数据可视化

在使用t-SNE进行降维时,要关注以下几点:

  • 计算成本高昂:计算成对相似度,复杂度为 O(n2),不适合大型数据集(>10000个样本)。一般先使用PCA降维至适中的维度,再使用t-SNE。
  • 仅用于可视化结果不能用于下游任务!低维坐标是随机的,没有物理意义。不能对新的测试数据进行变换(没有 .transform() 方法)。通常不直接用于提高分类器性能的降维,LDA或PCA(某些情况下)更适合。
  • 结果具有随机性:每次运行都会产生略有不同的结果(需设置 random_state 复现)。簇的大小和密度在 t-SNE 图中没有直接意义。点之间的距离在全局上没有意义
  • 对超参数敏感:不同参数可能导致完全不同的解释。perplexity(困惑度) 对结果影响巨大,需要尝试不同值。困惑大小与邻域大小成正比。此外,还有learning_rate(学习率)、max_iter(迭代次数)。
  • 只保留局部结构:可能会扭曲全局结构。例如,集群之间的距离和大小没有意义。

下面是代码:

from sklearn.manifold import TSNE
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix
import time
import numpy as np
import matplotlib.pyplot as plt # 用于可选的可视化
import seaborn as sns # 用于可选的可视化


# 确保 X_train, X_test 是DataFrame或Numpy Array
# 简单测试t-SNE降维并重新训练模型
print("       标准 t-SNE 主要用于可视化,直接用于分类器输入可能效果不佳。")


# 步骤 1: 特征缩放
scaler_tsne = StandardScaler()
X_train_scaled_tsne = scaler_tsne.fit_transform(X_train)
X_test_scaled_tsne = scaler_tsne.transform(X_test) # 使用在训练集上fit的scaler

# 步骤 2: t-SNE 降维
# 我们将降维到与PCA相同的维度(例如10维)或者一个适合分类的较低维度。
# t-SNE通常用于2D/3D可视化,但也可以降到更高维度。
# 然而,降到与PCA一样的维度(比如10维)对于t-SNE来说可能不是其优势所在,
# 并且计算成本会显著增加,因为高维t-SNE的优化更困难。
# 为了与PCA的 n_components=10 对比,我们这里也尝试降到10维。
# 但请注意,这可能非常耗时,且效果不一定好。
# 通常如果用t-SNE做分类的预处理(不常见),可能会选择非常低的维度(如2或3)。

# n_components_tsne = 10 # 与PCA的例子保持一致,但计算量会很大
n_components_tsne = 2    # 更典型的t-SNE用于分类的维度,如果想快速看到结果
                         # 如果你想严格对比PCA的10维,可以将这里改为10,但会很慢



# 对训练集进行 fit_transform
tsne_model_train = TSNE(n_components=n_components_tsne,
                        perplexity=30,    # 常用的困惑度值
                        max_iter=1000,      # 足够的迭代次数
                        init='pca',       # 使用PCA初始化,通常更稳定
                        learning_rate='auto', # 自动学习率 (sklearn >= 1.2)
                        random_state=42,  # 保证结果可复现
                        n_jobs=-1)        # 使用所有CPU核心
print("正在对训练集进行 t-SNE fit_transform...")
start_tsne_fit_train = time.time()
X_train_tsne = tsne_model_train.fit_transform(X_train_scaled_tsne)
end_tsne_fit_train = time.time()
print(f"训练集 t-SNE fit_transform 完成,耗时: {end_tsne_fit_train - start_tsne_fit_train:.2f} 秒")


# 对测试集进行 fit_transform
# 再次强调:这是独立于训练集的变换,由于t-SNE本身没有transform方法
tsne_model_test = TSNE(n_components=n_components_tsne,
                       perplexity=30,
                       max_iter=1000,
                       init='pca',
                       learning_rate='auto',
                       random_state=42, # 保持参数一致,但数据不同,结果也不同
                       n_jobs=-1)
print("正在对测试集进行 t-SNE fit_transform...")
start_tsne_fit_test = time.time()
X_test_tsne = tsne_model_test.fit_transform(X_test_scaled_tsne) # 注意这里是 X_test_scaled_tsne
end_tsne_fit_test = time.time()
print(f"测试集 t-SNE fit_transform 完成,耗时: {end_tsne_fit_test - start_tsne_fit_test:.2f} 秒")

print(f"t-SNE降维后,训练集形状: {X_train_tsne.shape}, 测试集形状: {X_test_tsne.shape}")

start_time_tsne_rf = time.time()
# 步骤 3: 训练随机森林分类器
rf_model_tsne = RandomForestClassifier(random_state=42)
rf_model_tsne.fit(X_train_tsne, y_train)

# 步骤 4: 在测试集上预测
rf_pred_tsne_manual = rf_model_tsne.predict(X_test_tsne)
end_time_tsne_rf = time.time()

print(f"t-SNE降维数据上,随机森林训练与预测耗时: {end_time_tsne_rf - start_time_tsne_rf:.4f} 秒")
total_tsne_time = (end_tsne_fit_train - start_tsne_fit_train) + \
                  (end_tsne_fit_test - start_tsne_fit_test) + \
                  (end_time_tsne_rf - start_time_tsne_rf)
print(f"t-SNE 总耗时 (包括两次fit_transform和RF): {total_tsne_time:.2f} 秒")


print("\n手动 t-SNE + 随机森林 在测试集上的分类报告:")
print(classification_report(y_test, rf_pred_tsne_manual))
print("手动 t-SNE + 随机森林 在测试集上的混淆矩阵:")
print(confusion_matrix(y_test, rf_pred_tsne_manual))

LDA

LDA(Linear Discriminant Analysis,即线性判别分析),找到一个线性组合的轴(投影平面),将高维数据投影到低维空间,同时满足类间方差最小化和类内方差最小化。换句话说,就是找一个方向,让数据在这个方向上投影后,“不同类的点离得越远越好,同类的点聚得越紧越好”

虽然与PCA都是线性降维方法,但不同的是LDA属于有监督降维,明确使用标签y来指导学习过程,最终寻找到能最好区分不同类别的方向。

下面是使用LDA时的注意点:

  • 降维目标维度存在上限:min(n_features, n_classes - 1)n_class - 1如果有 K 个类别,最多只需要 K-1 维空间,就可以将这 K 个点(各类别的均值点)完美地放置并区分开来。如对于二分类,只需要一条线(1维)就可分开两个点。n_features因为LDA是在原始特征空间里寻找投影方向,无法创造出比原始特征更多的线性无关的方向。
  • 线性变换:寻找最佳分界线。对于二分类问题,LDA找到的是一条直线(判别轴),当所有数据点投影到这条直线上时,两个类别的重叠部分最小。对于多分类问题,LDA找到的是一组相互正交的判别轴(类似于PCA的主成分),共同张成一个低维的“判别子空间”。
  • 数据假设:理论上,LDA 假设每个类别的数据服从多元高斯分布,所有类别具有相同的协方差矩阵。在实践中,即使这些假设不完全满足,LDA 通常也能表现良好,尤其是在类别大致呈椭球状分布且大小相似时。
  • 标签y不需要独热编码:保证为一维数组或Series类型。标签的类别数量直接决定降维上限。

关于LDA的适用场景:

  • 当目标是提高后续分类模型的性能时,LDA 是一个强有力的降维工具。
  • 当类别信息已知且被认为是区分数据的主要因素时。
  • 当希望获得具有良好类别区分性的低维表示时,尤其可用于数据可视化(如果能降到2D或3D)。

关于LDA实现的代码:

from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.preprocessing import StandardScaler
import time
import numpy as np
#import matplotlib.pyplot as plt
#from mpl_toolkits.mplot3d import Axes3D # 如果需要3D绘图
#import seaborn as sns

# LDA降维
# 代码看上去很多,主要是为了确保降维目标限度是合理的(if判断语句)
# 步骤 1: 特征缩放
scaler_lda = StandardScaler()
X_train_scaled_lda = scaler_lda.fit_transform(X_train)
X_test_scaled_lda = scaler_lda.transform(X_test) # 使用在训练集上fit的scaler

# 步骤 2: LDA 降维,得到特征数与类别数
n_features = X_train_scaled_lda.shape[1] #原始特征数量
if hasattr(y_train, 'nunique'): #hasattr()是否有该属性,用于判断y_train的类型
    n_classes = y_train.nunique() #y_train为pandas Series
elif isinstance(y_train, np.ndarray): 
    n_classes = len(np.unique(y_train)) #y_train为numpy数组
else:
    n_classes = len(set(y_train)) #其他情况(如列表)

max_lda_components = min(n_features, n_classes - 1) #得到最大可能降维维度

# 设置目标降维维度,检查LDA适用性
n_components_lda_target = 10

if max_lda_components < 1:
    print(f"LDA 不适用,因为类别数 ({n_classes})太少,无法产生至少1个判别组件。")
    X_train_lda = X_train_scaled_lda.copy() # 使用缩放后的原始特征
    X_test_lda = X_test_scaled_lda.copy()   # 使用缩放后的原始特征
    actual_n_components_lda = n_features
    print("将使用缩放后的原始特征进行后续操作。")
else:
    # 实际使用的组件数不能超过LDA的上限,也不能超过我们的目标(如果目标更小)
    actual_n_components_lda = min(n_components_lda_target, max_lda_components)
    if actual_n_components_lda < 1: # 这种情况理论上不会发生,因为上面已经检查了 max_lda_components < 1
        #额外安全检查
        print(f"计算得到的实际LDA组件数 ({actual_n_components_lda}) 小于1,LDA不适用。")
        X_train_lda = X_train_scaled_lda.copy()
        X_test_lda = X_test_scaled_lda.copy()
        actual_n_components_lda = n_features
        print("将使用缩放后的原始特征进行后续操作。")
    else:
        #正常执行LDA
        print(f"原始特征数: {n_features}, 类别数: {n_classes}")
        print(f"LDA 最多可降至 {max_lda_components} 维。")
        print(f"目标降维维度: {n_components_lda_target} 维。")
        print(f"本次 LDA 将实际降至 {actual_n_components_lda} 维。")

        lda_manual = LinearDiscriminantAnalysis(n_components=actual_n_components_lda, solver='svd')
        X_train_lda = lda_manual.fit_transform(X_train_scaled_lda, y_train) #有监督,需要标签
        X_test_lda = lda_manual.transform(X_test_scaled_lda)

print(f"LDA降维后,训练集形状: {X_train_lda.shape}, 测试集形状: {X_test_lda.shape}")


下面是三种特征降维方法的简单对比:

特性PCA (找视角)t-SNE (画地图)LDA (找分界线)
核心目标保留数据最大方差,最大化信息保留局部结构与相似性,可视化最大化类间区分度
学习方式无监督无监督有监督
输入需求特征 (X)特征 (X)特征 (X)  标签 (y)
降维维度无理论限制 (≤ n_features)无理论限制 (通常降至2/3维)有严格上限:≤ min(n_features, n_classes-1)
输出特性线性变换,成分可解释非线性变换,成分不可解释线性变换,成分可解释
保留信息全局结构局部结构(近邻关系)判别信息(分类边界)
主要应用数据压缩、去噪、探索性分析高维数据可视化(如细胞分群)分类任务前的特征提取、数据可视化
优点计算高效、结果稳定、全局特征对复杂流形结构效果好、可视化直观专为分类优化、降维后特征判别力强
缺点对非线性关系无效、忽略类别信息计算慢、结果不稳定、难以解释需标签、依赖线性假设、受维度上限限制

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值