☰
别再无脑用PCA:LDA线性判别分析如何实现分类聚团降维
2026/10/18 6:06:46 网站建设 项目流程

做分类任务这几年,我踩过一个特别典型的坑:手头是一批高维特征,老板让“先降维再建模”,我下意识拿起PCA就压到了几十维,结果分类准确率比不降维还低一截。后来才反应过来,PCA只是把数据按方差摊平,它根本不知道我的目标是想把不同类别分开。换到LDA线性判别分析之后,投影到一维上的结果清清楚楚分成两团,那一刻我才真正理解“分类聚团”这四个字的含义。这篇文章就从LDA的核心逻辑讲起,再到手算推导、多分类扩展和常见坑点,适合刚接触降维的数据分析师、算法初学者,以及任何被“降维后反而分不开”困扰过的人。

1. 为什么降维不能无脑上PCA:先想清楚“降完维干嘛用”

1.1 一个让我记住教训的小实验

先看一个极端但很说明问题的模拟数据。我生成两类样本,每类200个点,有两个特征:第一个特征是纯噪声,方差特别大;第二个特征才是真正的区分特征,两类中心在第二维上差了10个单位。代码如下:

import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA rng = np.random.default_rng(42) c1 = np.column_stack([ rng.normal(0, 50, 200), # 第一维:巨大噪声 rng.normal(0, 0.5, 200) # 第二维:真正可分 ]) c2 = np.column_stack([ rng.normal(0, 50, 200), rng.normal(10, 0.5, 200) ]) X = np.vstack([c1, c2]) y = np.r_[np.zeros(200), np.ones(200)] pca = PCA(n_components=1) xp = pca.fit_transform(X) lda = LDA(n_components=1) xl = lda.fit_transform(X, y)

把两种降维后的结果分别画成直方图,效果天差地别。PCA找的是“方差最大”的方向,而这份数据里方差最大的方向是第一个特征,也就是那堆噪声。投影过去之后,两类样本完全重叠,直方图就是一个大鼓包。LDA找的是“类间距离与类内散度比值最大”的方向,它会自动避开那个高方差噪声轴,跑到第二维上,把两类干净利落地分开。

这个例子虽然极端,但它揭示了一个本质问题:降维本身不是目的,让后续任务更好做才是目的。如果任务是可视化、压缩、去冗余,PCA是合理选择;如果任务是把类别分开,那就必须让分类信息参与降维过程,这正是LDA存在的意义。

1.2 信息量大不等于判别力强

很多人对PCA的第一印象是“保留主要信息”,但“信息量大”和“能区分类别”是两码事。拿图像数据说,整个数据集里方差最大的方向往往由光照、亮度、背景变化主导,这些分量占了很大能量,却对“这张图是猫还是狗”几乎没有贡献。反而那些藏在细微纹理里的判别信息,单看方差小得可怜,PCA很可能顺手就丢掉。

LDA的思路则是反过来:我不关心哪个方向方差大,我只关心哪个方向能让“同类更聚、异类更散”。这就是标题里说的“分类聚团”思维——先明确自己的目标,再去选投影方向。这种做法会让降维后的特征天然适合分类器,逻辑上更自洽。

2. 用“分类聚团”读懂LDA的数学:其实只有三个量

2.1 类内散度:同类的点靠不靠拢

要实现“分类聚团”,首先得量化“聚”这个字。对每个类别,先算它的均值中心,然后看每个样本离这个中心有多远。把所有偏差向量的外积叠加起来,得到一个矩阵,这就是类内散布矩阵,通常记作Sw。名字听起来拗口,你把它理解成“每个类别内部的一坨”就行:Sw越小,说明同类样本越扎堆;Sw越大,说明类别内部越散。

以二分类为例,假设第一类样本集合为X1,第二类为X2,每类的类内散布矩阵为Si = ∑(x - mi)(x - mi)^T,总的Sw = S1 + S2。这里用的是“散布矩阵”而不是协方差矩阵,区别只在除以不除以样本数,对求解最优方向没有本质影响,但手推和代码实现时要注意保持一致。

2.2 类间散度:不同类的均值离多远

有了“聚”,还得有“分”。类间散布矩阵Sb描述的是各类别中心之间的距离。具体做法是把每个类的均值向量和全局均值向量做差,乘以该类的样本数权重,再累加外积。直观理解就是:如果各个类别的“中心点”彼此分得越远,Sb的值就越大。

注意,Sb的秩有一个天然上限:类别数为C时,C个类中心相对全局中心只有C-1个独立的差异方向。例如三个类的均值点落在二维平面上,即使原始特征有100维,它们张出的“差异空间”最多也只有2维。这个性质在后面会直接影响LDA的最大降维维度。

2.3 一个比值定方向:Fisher准则

现在有两个量:类内散度Sw(希望它小)和类间散度Sb(希望它大)。怎么把它们合到一个目标里?LDA的做法是找投影方向w,最大化比值:

J(w) = (w^T Sb w) / (w^T Sw w)

为什么不直接最大化w^T Sb w、或者最大化两者的差?因为“差”这个指标受量纲干扰太大:Sw和Sb各自的大小会随特征尺度任意放大缩小,差值没有可比性。用比值相当于把两个量放到同一个天平上,自动归一化了。这也是Fisher准则的核心思想:投影后,类间距离相对类内散度越大越好。

二分类情形下,这个问题有解析解,最优方向满足:

w = Sw^{-1}(m1 - m2)

其中m1、m2是两个类的均值向量。你可以把Sw^{-1}理解为“去除类内形状干扰”的操作:先算出两类中心连线,再根据数据内部的相关结构做一个校正,让投影方向避免被方差大的方向带偏。

3. 手推一遍LDA:从一个能算出来的小例子开始

3.1 数据和均值

数学公式容易把人看晕,我建议你跟着我手动算一遍,用纸笔就能完成。为了保持整洁,我选两类各5个样本,每个样本只有2个特征。

A类:(2,3)、(3,4)、(4,4)、(3,5)、(5,6) B类:(8,7)、(9,8)、(9,9)、(10,8)、(11,10)

先算每类均值:

m_A = (3.4, 4.4) m_B = (9.4, 8.4)

两类中心的差向量为 m_A - m_B = (-6.0, -4.0)。如果完全不考虑类内结构,你可能会认为最优投影方向就是沿(-6,-4),也就是两类中心的连线方向。实际算出LDA方向后会看到,内部散布结构会让方向发生偏转。

3.2 类内散布矩阵Sw

类内散布矩阵是每个样本相对本类均值的偏差向量的外积之和。以A类第一个样本(2,3)为例,偏差为(-1.4,-1.4),外积是一个2×2矩阵:

[[1.96, 1.96], [1.96, 1.96]]

把A类5个样本的偏差外积全部累加,得到A类的类内散布矩阵。同理计算B类,最终两类相加的结果为:

Sw = [[10.4, 8.4], [ 8.4, 10.4]]

这个矩阵对角线都大于0,说明两个特征各自都有不小的内部波动;非对角线是正的,说明在这两类内部,两个特征存在一定同涨同跌的相关性。这些信息在后面都会用到。

3.3 类间散布矩阵Sb

类间散布矩阵基于类均值和全局均值。全局均值为(G = (6.4, 6.4))。两类样本数都是5,所以:

Sb = 5(m_A - G)(m_A - G)^T + 5(m_B - G)(m_B - G)^T

化简后等价于直接利用m_A - m_B的差向量:

Sb = 5/2 * (m_A - m_B)(m_A - m_B)^T = 2.5 * [[36, 24], [24, 16]] = [[90, 60], [60, 40]]

这里有一个常见操作:只要最终求的是方向,Sb前面的常数系数不影响特征向量,所以很多教材直接写成(m_A - m_B)(m_A - m_B)^T = [[36,24],[24,16]]。你自己实现时,用带权重版本还是一般版本,得到的投影方向会保持一致。

3.4 求最优投影方向

二分类LDA的方向可以直接用w = Sw^{-1}(m_A - m_B)计算。Sw^{-1} = (1/37.6) * [[10.4, -8.4], [-8.4, 10.4]],乘上差向量(-6,-4),得到:

w = (-0.766, 0.234)

把它归一化后大约是(-0.956, 0.292)。注意,这个方向并不完全沿着两类中心的连线(-6,-4),而是偏向Y轴方向。原因是类内散布矩阵里两个特征存在正相关,原始连线上有较多“类内重叠”,LDA通过Sw^{-1}做了校正,选择了一条能更好避开类内重叠的方向。这就是LDA“聚团”逻辑的体现:它不只看均值位置,还看每个类内部的形状。

这里还有个容易困惑的点:如果我用m_B - m_A算,会得到方向和上面完全相反(0.956, -0.292)。投影方向取反在分类判别里毫无区别,你只需要保证训练和预测时用的是同一个方向就行。

3.5 用numpy完整实现并验证

手推能加深理解,但日常使用还是要落到代码。下面的numpy实现完整复现了上面的过程:

import numpy as np A = np.array([[2,3],[3,4],[4,4],[3,5],[5,6]], dtype=float) B = np.array([[8,7],[9,8],[9,9],[10,8],[11,10]], dtype=float) m1 = A.mean(axis=0) m2 = B.mean(axis=0) Sw = np.zeros((2,2)) for x in A: d = (x - m1).reshape(-1, 1) Sw += d @ d.T for x in B: d = (x - m2).reshape(-1, 1) Sw += d @ d.T diff = (m1 - m2).reshape(-1, 1) Sb = diff @ diff.T w = np.linalg.pinv(Sw) @ (m1 - m2) w = w / np.linalg.norm(w) print("手算方向:", w.ravel())

再用sklearn验证:

from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA X = np.vstack([A, B]) y = np.array([0]*5 + [1]*5) model = LDA(n_components=1) model.fit(X, y) print("模型系数:", model.coef_.ravel())

这两者的方向要么一致,要么相差一个常数倍,因为模型内部会对系数做归一化处理。你拿到sklearn的coef_之后,同样可以视为一个判别投影方向。

4. 多分类LDA:为什么它最多只能降C-1维

4.1 目标函数怎么扩展

二分类把问题简化为“找一条最优直线”,多分类则是“找一个最优子空间”。类内散布矩阵Sw还是一样的算法,每个类各算各的内部散度再加总。类间散布矩阵Sb则改为:

Sb = ∑ ni (mi - G)(mi - G)^T

其中ni是第i类的样本数,mi是第i类的均值,G是全局均值。也就是说,所有类中心相对整体重心越分散,Sb的“能量”越大。

接下来的操作是把目标函数推广成广义特征值问题:求矩阵Sw^{-1}Sb的前k个最大特征值对应的特征向量,组成投影矩阵W。每个特征向量就是一个判别方向,特征值越大,说明该方向上的类间分离效果越好。

4.2 三分类降维演示

用sklearn生成一个模拟的三分类数据,6个特征,然后降到2维看效果:

from sklearn.datasets import make_classification X, y = make_classification( n_samples=300, n_features=6, n_informative=4, n_redundant=2, n_classes=3, n_clusters_per_class=1, random_state=0 ) lda = LDA(n_components=2) Z = lda.fit_transform(X, y) plt.figure(figsize=(6,5)) plt.scatter(Z[:,0], Z[:,1], c=y, cmap='viridis', s=20) plt.xlabel('LD1') plt.ylabel('LD2') plt.show()

画出来后,三个类别会在二维平面上呈现三个阶段或者三个团块,彼此重叠很少。这里有个处理细节:fit_transform会自动把样本中心化,所以你画出来的散点通常集中在原点附近,这并不影响类别判断。如果你发现有些类别仍然重叠,优先检查特征质量,而不是怀疑LDA本身。

4.3 关于降维维度的硬约束

多分类LDA有一个所有教材都会强调、但初学者经常忽视的规则:最多只能降到C-1维。三分类最多2维,五分类最多4维。原因在于Sb的秩上限是C-1,而最终有效判别空间的维度不会超过Sb的秩。

这一点和PCA完全不同。PCA理论上可以降到任意小于原始特征数的维度,因为协方差矩阵通常是满秩的;LDA则是被类别结构锁死。有人拿到20类数据想降到50维做可视化,忽略了这个约束,程序报错或结果诡异,多半就是在这里出了问题。

如果业务确实需要更低维度,比如只有3类却想降到1维,LDA没问题,直接取第一个判别方向即可;反过来,如果类别数很多、目标维度又高,LDA就力不从心了,这时候可以考虑先做PCA再去噪,或者改用其他非线性降维方法。

5. 实操选型与避坑实录:这些问题文档里一般不写

5.1 Sw不可逆时的三种救法

高维小样本是LDA最经典的杀手。比如每张图片有几千维像素,但训练样本只有几十个,这时Sw很可能是奇异矩阵,直接求逆会报错。我遇到过三次这种情况,分别用了三种不同解法:

第一种,给Sw加正则化扰动,也就是把Sw换成Sw + λI。λ通常取一个很小的正数,比如1e-4,相当于给对角线加一点“地板”,让矩阵可逆。sklearn中的shrinkage参数就是干这个的,直接设成'auto',由库自动估计收缩系数,省心很多。

第二种,先PCA再LDA。先用PCA把维度压到样本数附近,比如保留N-C个主成分,再去算类内散布矩阵。这个组合在图像识别里非常经典,很多人脸识别方案就是“PCA降维去噪,LDA找判别方向”。

第三种,直接用伪逆。np.linalg.pinv(Sw)替代np.linalg.inv(Sw),伪逆在奇异矩阵下也能给出一个有限结果。这个方法最省事,但可解释性稍差,我一般用它做快速验证,而不是上线方案。

5.2 特征值带虚部是数值误差,别慌

手写多分类LDA时,需要求Sw^{-1}Sb的特征值和特征向量。如果你用np.linalg.eig,有时会得到带虚部的复数结果。这时候先别怀疑公式写错了,大概率是矩阵不对称加上浮点误差造成的。Sw^{-1}Sb本身不是对称矩阵,但它的特征值理论上应该是实数。

稳妥的做法是直接调scipy.linalg.eigh处理对称正定结构,或者对特征值取实部,再按实部大小排序。sklearn内部已经处理过这些数值问题,所以日常用库没问题;一旦你自己实现教学代码或者做源码级调试,这个坑就很常见。

5.3 数据分布不符合假设怎么办

LDA的推导隐含了两个假设:各类别数据近似服从高斯分布,且各类的协方差结构差异不大。如果特征明显非高斯,或者有个类别的数据天生就比其他类别分散很多,LDA的判别方向容易被大散度的类别带偏。

我在某次项目里碰到过这种情况:正常类样本特别集中,故障类样本分布很散,LDA投影后正常类和故障类反而叠在一起。后来换成二次判别分析QDA,允许每个类有自己的协方差结构,问题就解决了。所以选型时建议先看一眼各类的散点图或者协方差,别把LDA当万能药。

5.4 和PCA配合使用的经验顺序

实际项目里,“先PCA后LDA”不仅是解决奇异的方案,本身也是一套不错的特征工程流水线。PCA先把原始高维数据里的冗余和噪声过滤掉,LDA再从中提取最有利于分类的判别方向。我常用的顺序是:

  1. 标准化所有特征;
  2. PCA降到一个中等维度,候选区间通常从N-C到2C之间,具体用交叉验证调;
  3. 对降维结果做LDA,最多取C-1个判别方向;
  4. 把得到的判别特征喂给逻辑回归或决策树。

这套流程的优点是稳定、可解释,最终特征有明确的判别语义;缺点是丢失了原始特征的直接物理含义。如果业务方要求每个特征能对应到具体指标,就要谨慎使用LDA了。

5.5 哪些场景真正适合LDA

根据我的经验,LDA在下面几类场景特别好用:图像识别领域的Fisherface思路(人脸降维后分类)、工业传感器的故障诊断(把多种工况投影到低维平面做状态可视化)、文本分类中的判别词空间压缩,以及营销客群分析中把高维消费特征压成几个直观的判别轴。

它也很适合做“分类可行性诊断”。我习惯在建模初期先跑一版LDA,把数据降到2维或3维画出来。如果不同类别聚团清晰,说明原始特征里确实有可分的信号,后续建模值得投入;如果投影图糊成一团,那我就会回头重新做特征工程,而不是急着堆模型。

写在最后的几句实在话

我后来做降维任务,第一反应不再是无脑PCA了,而是先问一句:降完维给谁用?给可视化和压缩就是PCA,给分类判别就优先考虑LDA。LDA本身不复杂,思想说白了就是“让同类更聚、异类更远”,但它的价值被很多人低估了——它不只是降维工具,还是一个特别好的分类诊断工具。遇上高维小样本就加点正则化或者先走一遍PCA,发现投影效果不理想就检查数据分布假设,这些招数多试几次自然就熟了。LDA不可能解决所有问题,但在合适的场景里,它是性价比极高、还特别容易讲清楚的一把利器。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询