做分类任务这几年,我踩过一个特别典型的坑:手头是一批高维特征,老板让“先降维再建模”,我下意识拿起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再从中提取最有利于分类的判别方向。我常用的顺序是:
- 标准化所有特征;
- PCA降到一个中等维度,候选区间通常从N-C到2C之间,具体用交叉验证调;
- 对降维结果做LDA,最多取C-1个判别方向;
- 把得到的判别特征喂给逻辑回归或决策树。
这套流程的优点是稳定、可解释,最终特征有明确的判别语义;缺点是丢失了原始特征的直接物理含义。如果业务方要求每个特征能对应到具体指标,就要谨慎使用LDA了。
5.5 哪些场景真正适合LDA
根据我的经验,LDA在下面几类场景特别好用:图像识别领域的Fisherface思路(人脸降维后分类)、工业传感器的故障诊断(把多种工况投影到低维平面做状态可视化)、文本分类中的判别词空间压缩,以及营销客群分析中把高维消费特征压成几个直观的判别轴。
它也很适合做“分类可行性诊断”。我习惯在建模初期先跑一版LDA,把数据降到2维或3维画出来。如果不同类别聚团清晰,说明原始特征里确实有可分的信号,后续建模值得投入;如果投影图糊成一团,那我就会回头重新做特征工程,而不是急着堆模型。
写在最后的几句实在话
我后来做降维任务,第一反应不再是无脑PCA了,而是先问一句:降完维给谁用?给可视化和压缩就是PCA,给分类判别就优先考虑LDA。LDA本身不复杂,思想说白了就是“让同类更聚、异类更远”,但它的价值被很多人低估了——它不只是降维工具,还是一个特别好的分类诊断工具。遇上高维小样本就加点正则化或者先走一遍PCA,发现投影效果不理想就检查数据分布假设,这些招数多试几次自然就熟了。LDA不可能解决所有问题,但在合适的场景里,它是性价比极高、还特别容易讲清楚的一把利器。