数据集的规模和复杂性一路飙升,特征或维度的数量也常常跟着膨胀到难以处理的地步。这不仅让计算需求水涨船高,还容易引发过拟合,模型的解释力也跟着下降。好在,降维技术提供了一条出路——它能在保留数据核心信息的同时,丢掉那些冗余或信息量有限的特征。
这个过程不只是让计算任务变得更轻巧,它还能帮助我们看清数据的趋势,有效缓解维度诅咒,最终提升机器学习模型的泛化能力。从图像和语音处理,到金融和生物信息学,降维的应用场景相当广泛。在这些领域里,面对海量数据,能不能提取出有意义的模式,直接决定了我们能否做出明智决策、建好有效的预测模型。

接下来,我们将深入聊聊三种经典的降维技术:主成分分析(PCA)、线性判别分析(LDA)和奇异值分解(SVD)。不仅会介绍它们背后的基本原理,也会一并看看各自的优缺点。
主成分分析(PCA)
主成分分析,简称为PCA,是数据分析和机器学习领域里相当常用的一种降维技术。它的核心目标,就是把高维数据转换到一个低维空间,同时牢牢抓住其中最关键的、信息含量最丰富的部分。
既然我们是要识别数据里的模式,那自然希望数据在各个维度上都尽量散开,而且这些维度之间最好相互独立。方差,作为衡量数据波动性的标准,本质上反映的就是数据集分散的程度。用数学语言来说,就是每个值与平均值的平均平方偏差。方差的公式用 var(x) 表示如下:

协方差,则用来量化两组有序数据里,对应元素变化趋势的相似程度。用 cov(x, y) 表示变量 x 和 y 之间的协方差。xi 代表第 i 维中 x 的值,而 x柱 和 y柱 则代表它们各自的平均值。假设我们有一个 m×n 的矩阵 X,其中包含 n 个数据点,每个点有 m 个维度,那么协方差矩阵的计算方式如下:

这个协方差矩阵的重点有两个:
- 主对角线上的元素是各个维度的方差。
- 非对角线上的元素是不同维度之间的协方差。
我们的目标很明确:希望数据广泛分散,也就是各维度方差要大;同时,还要消除维度之间的相关性,这意味着维度之间的协方差应为零,表明它们线性无关。所以,对数据进行变换的目的,就是让它的协方差矩阵呈现出下面这种特征:
- 主对角线上的值是显著的实数值。
- 非对角线上的值都是零。
因此,我们需要对原始数据点进行一番变换,使它新的协方差矩阵看起来像一个对角矩阵。把这个矩阵变成对角阵的过程,在数学上称为对角化,而这恰恰是主成分分析(PCA)背后的核心动机。
PCA 的工作流程
1. 标准化
当特征的量纲不一致时,比如一个特征是厘米,另一个是千克,就需要先做个标准化处理。具体做法是减去均值,再除以标准差。要是不做这一步,那些尺度大的特征就可能主导分析结果,产生误导。
2. 计算协方差矩阵
按照之前讨论的方法,计算出数据的协方差矩阵。
3. 计算特征向量和特征值
这一步是求协方差矩阵的特征向量和特征值。
可以这样理解:特征向量指明了方向(也就是主成分),而特征值则告诉我们在那个方向上数据的方差有多大。
4. 特征值排序
把特征值按从大到小的顺序排好。对应最大特征值的那个特征向量,就是捕获数据中最大方差的第一主成分。
5. 选择主成分
根据需要我们解释多少方差,来选择前 k 个特征向量(即主成分)。常见的做法是设定一个阈值,比如保留总方差的85%。
6. 数据转换
最后,利用选出的特征向量来变换原始数据:
假设我们有 m 维的 n 个数据点 X (m×n),而我们选择了一个 k×m 的矩阵 P,那么变换后的数据 Y = PX,结果就是一个 k×n 的矩阵。这个新矩阵里,每一个数据点都降到了 k 维。
优点
- 降维:PCA能有效地减少特征数量,对于缓解维度诅咒效果显著。
- 特征独立性:主成分之间是正交的(不相关),这意味着它们捕获的是相互独立的信息,这有助于简化对降维后特征的解读。
- 降噪:通过重点关注那些解释数据最大方差的成分,PCA能在一定程度上过滤掉噪声。
- 可视化:降到二维或三维的数据可以很方便地画出来,有助于我们直观理解数据的底层结构和模式。
缺点
- 原始特征的可解释性会在变换后丢失,因为主成分是原始特征的线性组合,很难直接说清它代表什么。
- PCA假设变量之间的关系是线性的,但现实中很多关系并非如此。
- PCA对特征的尺度很敏感,所以标准化是必不可少的一步。
- 异常值会显著影响PCA的结果,因为它主要关注捕获最大方差,而异常值往往带着很大的方差。
何时使用
- 高维数据:当你面对特征数量特别多的数据集,想缓解维度诅咒时,PCA是一个好选择。
- 共线特征:如果特征之间高度相关,PCA能有效地捕捉它们共享的信息,用更少的成分来代表。
- 可视化:想要把高维数据投射到低维空间进行直观展示。
- 线性关系:当变量之间的关系大多是线性时,PCA的效果最好。
Python 代码示例
import numpy as np
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris
# 以鸢尾花数据集为例
iris = load_iris()
X = iris.data
y = iris.target
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 标准化数据(对PCA很重要)
scaler = StandardScaler()
X_train_std = scaler.fit_transform(X_train)
X_test_std = scaler.transform(X_test)
# 初始应用PCA,保留所有成分以查看方差解释情况
pca = PCA()
X_train_pca = pca.fit_transform(X_train_std)
# 计算累计解释方差比
cumulative_variance_ratio = np.cumsum(pca.explained_variance_ratio_)
# 找出能解释至少85%方差所需的成分数
n_components = np.argmax(cumulative_variance_ratio >= 0.85) + 1
# 用选定的成分数再次应用PCA
pca = PCA(n_components=n_components)
X_train_pca = pca.fit_transform(X_train_std)
X_test_pca = pca.transform(X_test_std)
# 展示结果
print("原始训练数据形状:", X_train.shape)
print("PCA降维后训练数据形状:", X_train_pca.shape)
print("选定的主成分数量:", n_components)
注意,上面代码里,第一次应用 PCA() 时没有指定成分数,这会保留所有成分。接着计算累计方差,再找出能解释85%方差所需的最少成分数,最后用这个数量重新做一次PCA。通常,PCA的拟合只针对训练数据,然后对测试数据应用 transform 方法即可。
线性判别分析(LDA)
线性判别分析(LDA),既是降维技术,也是一种分类方法。它的目标很明确:最大化数据集中不同类别之间的区分度。LDA在有监督学习场景中尤其受欢迎,因为我们预先知道每个数据点属于哪个类别。
一个关键的区别在于:PCA可以被看作是“无监督”的算法,它不关心类别标签,只顾着找能使数据集方差最大的主成分;而LDA则走的是“有监督”的路线。LDA会计算“线性判别器”,这些判别器就像坐标轴,其指向能最大限度地将不同类别的数据分开。我们用一个经典的“Iris”(鸢尾花)数据集来走一遍LDA的计算流程。这个数据集包含了来自三个不同鸢尾花品种的150朵花的花萼、花瓣尺寸数据。
Iris 数据集有三个类别:
- Iris-setosa (50个样本)
- Iris-versicolor (50个样本)
- Iris-virginica (50个样本)
每个样本有四个特征:
- 花萼长度 (cm)
- 花萼宽度 (cm)
- 花瓣长度 (cm)
- 花瓣宽度 (cm)
LDA 的工作步骤
1. 计算各类别的均值向量
计算三个不同花类的平均向量 mi (i=1, 2, 3):
类别1均值向量: [5.006, 3.418, 1.464, 0.244]
类别2均值向量: [5.936, 2.77, 4.26, 1.326]
类别3均值向量: [6.588, 2.974, 5.552, 2.026]
每个向量包含了该类别下四个特征的平均值。
2. 计算类内散度矩阵 (Sw)
这个矩阵衡量的是每个类别内部数据的散布情况。

计算结果如下:
类内散度矩阵:
[[38.9562, 13.683, 24.614, 5.6556]
[13.683, 17.035, 8.12, 4.9132]
[24.614, 8.12, 27.22, 6.2536]
[5.6556, 4.9132, 6.2536, 6.1756]]
3. 计算类间散度矩阵 (Sb)
这个矩阵衡量的是不同类别之间的“距离”或散布情况。

计算结果如下:
类间散度矩阵:
[[63.2121, -19.534, 165.1647, 71.3631]
[-19.534, 10.9776, -56.0552, -22.4924]
[165.1647, -56.0552, 436.6437, 186.9081]
[71.3631, -22.4924, 186.9081, 80.6041]]
4. 计算 Sw⁻¹Sb 的特征值和特征向量
这一步和PCA类似。在我们的例子中,会得到4个特征值和对应的特征向量。
特征向量 1: [[-0.2049]
[-0.3871]
[ 0.5465]
[ 0.7138]] 特征值 1: 3.23e+01
特征向量 2: [[-0.009 ]
[-0.589 ]
[ 0.2543]
[-0.767 ]] 特征值 2: 2.78e-01
特征向量 3: [[ 0.179 ]
[-0.3178]
[-0.3658]
[ 0.6011]] 特征值 3: -4.02e-17
特征向量 4: [[ 0.179 ]
[-0.3178]
[-0.3658]
[ 0.6011]] 特征值 4: -4.02e-17
5. 对特征向量排序并选择
将特征向量按对应特征值从大到小排序,然后选出前 k 个。在这个例子里,我们选两个信息量最大的特征对,构成一个 d×k 维的特征向量矩阵(称它为 W)。结果是这样的:
矩阵 W:
[[-0.2049, -0.009 ]
[-0.3871, -0.589 ]
[ 0.5465, 0.2543]
[ 0.7138, -0.767 ]]
6. 数据转换
最后,使用矩阵 W(4×2)将原始样本变换到新的子空间:Y = X * W。这里的 X 是原始数据矩阵(150×4),Y 就是变换后的数据集(150×2)。
优点
- 最大化类间分离:LDA的目标就是让不同类别之间的距离最大化,因此它在分类任务中非常有效。
- 降维:和PCA一样,LDA也可以用于降维,而且它的一个额外优势是,在降维过程中考虑了类别信息。
缺点
- 对异常值敏感:LDA对异常值的存在相当敏感,它们会显著影响方法的性能。
- 正态性假设:LDA假设每个类别里的特征都服从正态分布。如果这个假设不成立,它的表现可能会打折扣。
- 需要足够的数据:当每个类别的样本数量很少时,LDA的效果可能不佳。更多的样本有助于更准确地估计类别参数。
何时使用
- 分类任务:当你的最终目标是把数据分到预先定义好的类别中时,LDA会是不错的选择。
- 保留类别信息:在你想要降低维度,同时又希望保留那些用于区分不同类别的关键信息时,LDA的价值就体现出来了。
- 正态性假设成立:当数据基本符合每个类别内特征正态分布的假设时,LDA能表现出色。
- 有监督降维:需要用类别标签来指导降维过程时,LDA就是那个合适的技术。
Python 代码示例
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.datasets import make_classification
from sklearn.preprocessing import StandardScaler
# 生成一个示例数据集
X, y = make_classification(n_samples=1000, n_features=20, n_classes=2, random_state=42)
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 标准化特征(对LDA也很重要)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
# 初始化LDA并在训练集上拟合
lda = LinearDiscriminantAnalysis()
X_train_lda = lda.fit_transform(X_train, y_train)
# 计算每个成分的方差解释比
explained_variance_ratio = lda.explained_variance_ratio_
# 计算累计解释方差
cumulative_explained_variance = np.cumsum(explained_variance_ratio)
# 找出能解释至少75%方差所需的成分数
n_components = np.argmax(cumulative_explained_variance >= 0.75) + 1
# 将训练集和测试集都变换到选定的成分数
X_train_lda_selected = lda.transform(X_train)[:, :n_components]
X_test_lda_selected = lda.transform(X_test)[:, :n_components]
# 打印选定的成分数量
print(f"选定的成分数量: {n_components}")
奇异值分解(SVD)
奇异值分解是一种应用极其广泛的矩阵分解技术,在信号处理、机器学习等领域无处不在。它能把一个矩阵拆解成三个更简单的矩阵,以一种更紧凑的形式来代表原始矩阵。

SVD 的工作流程
1. 矩阵分解
假设有一个大小为 m×n 的矩阵 M(可以理解成有 m 行 n 列数据)。SVD会将M分解成三个矩阵相乘的形式:M = U * Σ * Vᵀ。
其中:
- U 是一个 m×m 的正交矩阵。
- Σ 是一个 m×r 的对角矩阵。
- V 是一个 r×n 的正交矩阵。这里的 r 是矩阵 M 的秩。
Σ 对角线上的元素被称为奇异值,它们是原矩阵 M 的奇异值,并且是按从大到小排列的。U 的列是左奇异向量,构成了 M 的列空间的正交基。V 的列是右奇异向量,构成了 M 的行空间的正交基。
2. 截断SVD (Truncated SVD)
对于降维任务,我们通常不会使用完整的SVD,而是用它的截断版本。我们会保留 Σ 中最大的 k 个奇异值,同时从 V 中选择对应的前 k 行。这样就能用选出的矩阵来重构一个更小的新矩阵。
优点
- 降维:SVD可以通过只保留最重要的几个奇异值和向量来实现降维。
- 数据压缩:SVD常用于数据压缩,因为它能显著降低矩阵的存储需求。
- 降噪:只保留最大的奇异值,相当于去除了那些代表噪声的小奇异值,有助于数据去噪。
- 数值稳定性:SVD在数值上相当稳定,适合解决一些病态的线性问题。
- 正交性:分解出的U和V矩阵是正交的,这有助于保留原矩阵行与列之间的关系。
- 推荐系统中的应用:SVD是协同过滤推荐系统背后的核心技术之一。
缺点
- 计算复杂度高:对一个大型矩阵做完整的SVD,计算量非常巨大。
- 内存占用大:存储完整的 U、Σ、V 矩阵,尤其是对于大型矩阵,会消耗大量内存。
- 对缺失值敏感:SVD对数据中的缺失值很敏感,需要专门的技巧来处理。
何时使用
- 降维:当你希望在保持数据基本结构的前提下降低维度。
- 推荐系统:在基于协同过滤的推荐系统里,SVD用于发现用户和物品交互背后的潜在因素。
- 数据压缩:需要压缩或近似表示大型数据集时。
- 信号处理:用于降噪和特征提取。
- 主题建模:像潜在语义分析(LSA)这样的主题建模技术,其核心就是SVD。
Python 代码示例
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.decomposition import TruncatedSVD
from sklearn.datasets import make_classification
from sklearn.preprocessing import StandardScaler
# 生成一个示例数据集
X, y = make_classification(n_samples=1000, n_features=20, n_classes=2, random_state=42)
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 标准化特征(对SVD很重要)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
# 初始化SVD,成分数设为一比特征总数少一个
svd = TruncatedSVD(n_components=X_train.shape[1] - 1)
X_train_svd = svd.fit_transform(X_train)
# 计算每个成分的方差解释比
explained_variance_ratio = svd.explained_variance_ratio_
# 计算累计解释方差
cumulative_explained_variance = np.cumsum(explained_variance_ratio)
# 找出能解释至少75%方差所需的成分数
n_components = np.argmax(cumulative_explained_variance >= 0.75) + 1
# 将训练集和测试集都变换到选定的成分数
X_train_svd_selected = svd.transform(X_train)[:, :n_components]
X_test_svd_selected = svd.transform(X_test)[:, :n_components]
# 打印选定的成分数量
print(f"选定的成分数量: {n_components}")
总结:如何选择?
在PCA、LDA和SVD之间做选择,最终取决于你的具体目标和你手头数据的特性。下面是一些通用的指导原则,可以帮助你做出决定:
主成分分析(PCA):
- 核心目标是降低数据集的维度。
- 关键是你想捕获数据中的全局模式和关系。
- 常用于探索性数据分析和可视化。
线性判别分析(LDA):
- 主要用在分类问题里,目的是增强类别之间的分离度。
- 你拥有一个带标签的数据集,目标是找到一个能最大化类别区分度的投影方向。
- 当数据基本符合正态分布且各类别协方差矩阵相近时,LDA效果最好。
奇异值分解(SVD):
- 当你处理的数据很稀疏,或者含有缺失值时。
- 用于推荐系统中的协同过滤。
- 也适用于数据压缩和去噪的场景。
三者的简要对比:
- 无监督 vs 有监督:PCA是无监督的,LDA是有监督的。你的选择取决于是否有标签数据。
- 类可分性:如果你的目标是增强类别之间的可分性,LDA是首选。而PCA和SVD关注的则是整体方差。
- 数据特征:数据的线性程度、类别分布、异常值情况等,都会影响最终选择。
- 应用需求:考虑你的具体需求,比如是否看重结果的可解释性、计算效率,或者是否需要处理缺失值。
总的来说,PCA适用于无监督的降维任务,LDA则更适合有监督、关注类别可分性的场景。而SVD更像一把瑞士军刀,通用性极强,在很多地方都能派上用场,尤其是在协同过滤和矩阵分解这类应用中。
