☰
线性判别分析LDA降维实战:从Fisher准则到Python手写实现
2026/10/17 14:43:28 网站建设 项目流程

前阵子整理以前的实验代码,翻到一个特别典型的例子:两堆随机生成的高斯点,散点图上一眼就能看清边界,可一旦用PCA降到一维,投影直方图上两堆几乎完全叠在一起。后来我把降维方法换成线性判别分析(LDA),同样是二维到一维,投影结果却泾渭分明。这个对比让我对"有监督降维"有了真正的体感:降维不是"保留最大方差"就万事大吉,带标签的数据有它自己的逻辑。今天就把"随机生成数的二维到一维降维"这条路从原理到代码完整走一遍,内容包括LDA的目标函数、散布矩阵推导、手写Python实现、与sklearn结果互验,以及实际使用中最容易踩的边界问题。适合刚接触LDA的读者,也适合那些会调包但说不清投影方向为什么是那个方向的进阶开发者。

1. 一维投影的本质:LDA在找一个"分得开的方向"

1.1 从平面到直线,所有信息都压缩在一个数上

二维数据点(x1, x2)要降到一维,本质是找一条过原点的直线,方向向量记为w,然后把每个点都投影到这条直线上,得到标量t = w1x1 + w2x2。方向相同但长度不同的w代表同一个投影,所以常规做法是把w归一化成单位向量。投影完之后,每个样本只剩一个数t,所有后续的分类、聚类都发生在这个一维数轴上。也就是说,降维的好坏完全取决于你选的这条直线:两类样本在二维平面里再分开,如果投到直线上的影子大面积重叠,那这个方向就是失败的。

我习惯把这理解成"影子问题":阳光从某个角度照下来,立体物投到地面的影子可能重叠也可能分开,我们要选的,就是让两类物体影子分得最开的那个角度。数据从二维到一维,道理完全一样,只不过"光照角度"换成了待优化的方向向量w。

1.2 Fisher准则:把"类间要远、类内要挤"写成数学

LDA的核心目标可以概括成一句话:找一个投影方向,让两类样本投影后的中心点距离尽量大,同时每类样本自己投影后的散布尽量小。这句话翻成数学就是一个比值,也就是Fisher准则:

J(w) = (w^T S_B w) / (w^T S_W w)

分子衡量类间距离,分母衡量类内散布,LDA要找的就是让这个比值达到最大的w。这里的S_B是类间散布矩阵,S_W是类内散布矩阵,下一节会细讲它们的构造。

为什么不能只优化"类间距离最大"?因为单看类间距离,方向会往均值连线方向偏,完全不考虑每类内部的胖瘦。极端情况下,某个方向让两类影子中心相距很远,但每类影子也拉得很长,结果照样大面积重叠。Fisher准则用比值同时约束两边,等于自动做了一次归一化,让"拉开"和"收紧"两件事同时成立。

1.3 和PCA的分工差异

PCA的目标是"投影后整体方差最大",即J(w) = w^T S_total w,纯粹看整体数据的展开程度,标签信息完全不用。LDA的目标是Fisher准则,标签是核心输入。从结果上看,PCA倾向于保留数据整体最"能铺开"的方向,LDA则优先保留对类别区分最有利的方向。如果整体方差最大方向恰好和类别可分方向一致,两者结论相近;如果数据是长条形斜着分布,而类别差异刚好在另一个方向上,PCA就会选错方向——这正是开头那个反直觉现象的根本原因。

说句公道话,PCA不是没用,它是无监督降维的默认选项,数据没标签时只能靠它。只是当你手里有标签、目标是做分类时,LDA通常更对口。

2. 公式拆解:从散布矩阵到最终投影方向

2.1 散布矩阵怎么构造

先把记号说清楚。假设只有两类,类别k的样本集合记为X_k,均值记为μ_k。LDA里需要两个关键矩阵。

第一是类内散布矩阵。对每个类别,先算它内部样本相对自己均值的离差外积和:

S_k = Σ_{x∈X_k} (x - μ_k)(x - μ_k)^T

然后把两类加起来:

S_W = S_1 + S_2

S_k和协方差矩阵很像,只差一个除以样本数的归一化系数,所以S_W描述的,本质上就是"两类样本各自围绕自己中心散布的总和"。样本数越多、每个类拖得越长,S_W的数值就越大。

第二是类间散布矩阵。在两类情况下,它有一个非常简洁的形式:

S_B = (μ_1 - μ_2)(μ_1 - μ_2)^T

这是两个均值差向量做外积得到的矩阵。严格的教科书定义里还会乘一个与样本数相关的系数,但因为后面最大化的是比值,常数倍并不会改变最终方向,所以这里直接用最简形式。注意S_B是一个秩为1的矩阵,它唯一携带的信息就是两个均值之间的差方向。这个性质非常关键,后面会反复用到。

2.2 最大化比值如何变成特征值问题

把J(w) = w^T S_B w / w^T S_W w 对w求导并令导数为零,可以得到一个广义特征值问题:

S_W^{-1} S_B w = λ w

其中λ是广义特征值(这里简写为普通特征值形式,因为S_W通常可逆)。Fisher准则的最大值恰好等于最大的特征值λ_max,对应的特征向量w就是我们要找的投影方向。整个过程可以理解为:LDA对方向w做了一次"评分"(比值J),然后用线性代数的工具找出评分最高的那个方向。

这里有一个值得记住的性质:既然S_B的秩是1,那么S_W^{-1}S_B的秩最多也是1,意味着它只有一个非零特征值,其余特征值全为0。换句话说,在二分类问题里,"最有判别力的方向"天然只有一个,从二维降到一维正好把这个方向完整用上,不多不少。

2.3 二分类的捷径:不必做完整的特征分解

因为S_B秩为1,它对任意向量作用的结果都平行于d = μ_1 - μ_2。把这个观察代入特征方程,可以直接推出最优方向满足:

w ∝ S_W^{-1} (μ_1 - μ_2)

这是一个非常实用的结论:两类问题里,LDA的投影方向就是把均值差向量经过S_W^{-1}做一次线性变换,再归一化。代码层面用一行np.linalg.solve(Sw, diff)就能算出来,完全不需要调用复杂的特征分解接口。

我自己实现时就留意过这点:小矩阵直接np.linalg.inv(Sw) @ diff也不是不行,但从数值稳定性角度,解线性方程组比显式求逆更稳妥,尤其当矩阵接近奇异时,逆矩阵的误差会被放大,而solve的报错信息也更明确。这个小习惯后来帮我省了不少事。

3. 随机数据全流程:造数、手写LDA、与sklearn互验

3.1 造一批"有挑战性"的随机数

先说实验环境:Python 3.10,numpy做矩阵运算,matplotlib画图,sklearn只在最后做对比验证。所有随机数据都用固定种子生成,保证过程可复现。

为了让例子不过于理想化,我给两类设置了不同的协方差结构:类别1的主方向带正相关,类别2带轻微负相关。这样两团点并不是完美球形,LDA必须在"压缩类内散布"和"拉开类间距离"之间做权衡,投影方向不会简单等于均值连线方向,分析起来更有意思。

import numpy as np import matplotlib.pyplot as plt np.random.seed(2024) n1, n2 = 120, 150 mu1 = np.array([2.0, 2.0]) mu2 = np.array([5.0, 4.0]) cov1 = np.array([[1.2, 0.5], [0.5, 0.8]]) cov2 = np.array([[1.0, -0.3], [-0.3, 1.1]]) x1 = np.random.multivariate_normal(mu1, cov1, n1) x2 = np.random.multivariate_normal(mu2, cov2, n2) plt.scatter(x1[:, 0], x1[:, 1], alpha=0.5, label="class1") plt.scatter(x2[:, 0], x2[:, 1], alpha=0.5, label="class2") plt.legend() plt.show()

生成之后可以先画散点图确认一下:两类中心分别在(2,2)和(5,4),均值差向量大致沿(3,2)方向,两团点在平面里明显分开但边缘有重叠,这正好是"有难度但可以分"的状态,比两团完全分离的数据更能说明LDA的取舍逻辑。

3.2 手写LDA核心流程

按前面的推导,实现只需要三步:算类内散布矩阵、解线性方程组求原始方向、归一化。完整代码如下:

def scatter_matrix(X): Xc = X - X.mean(axis=0) return Xc.T @ Xc S1 = scatter_matrix(x1) S2 = scatter_matrix(x2) Sw = S1 + S2 diff = x1.mean(axis=0) - x2.mean(axis=0) w_raw = np.linalg.solve(Sw, diff) # 解 Sw @ w_raw = diff w = w_raw / np.linalg.norm(w_raw) # 归一化成单位方向 p1 = x1 @ w p2 = x2 @ w print("投影方向 w:", w)

这里每一行都有含义。scatter_matrix把每个类转成散布矩阵,Sw把两类合并成统一的类内几何结构;diff是均值差向量,代表分类信息所在的方向;np.linalg.solve(Sw, diff)实现的就是S_W^{-1}(μ1-μ2),本质是对均值差向量做一次"按类内方差反比加权"的调整——类内方差越大的方向,结果越不可靠,贡献被压低;类内方差越小的方向,区分类别越划算,贡献被放大。

提示:能用 np.linalg.solve 就别用 np.linalg.inv(Sw) @ diff,前者数值上更稳定,这在很多矩阵计算场景都适用。

3.3 投影之后怎么评估

方向算出来了,但好不好不能凭感觉。我一般看两个指标加一张图。第一个指标是一个简化版的类可分性指标,思想沿用Fisher准则,直接比较投影后两类均值的距离和类内总方差:

f_lda = (p1.mean() - p2.mean())**2 / (p1.var() + p2.var()) print("LDA Fisher score:", f_lda)

这个值越大越好,它衡量的正是"两类均值差相对类内总方差的比值",和线性分类器在该投影下的可分程度直接相关。想要更严谨就把var()换成ddof=1的无偏版本,不过对比场景下区别不大。

第二个指标是同场对比PCA。同样降到一维,看PCA在这个数据上的Fisher score是多少:

from sklearn.decomposition import PCA X_all = np.vstack([x1, x2]) proj_pca = PCA(n_components=1).fit_transform(X_all).ravel() pc1, pc2 = proj_pca[:n1], proj_pca[n1:] f_pca = (pc1.mean() - pc2.mean())**2 / (pc1.var() + pc2.var()) print("PCA Fisher score:", f_pca)

最后把两类投影结果画成直方图,和散点图对照着看。在这组参数下,LDA的Fisher score通常能到5到8,PCA往往只有1到3,直方图的重叠程度一眼就有差别。具体数值会因为随机种子和协方差参数浮动,但"LDA明显优于PCA"这个结论在这个构造下是很稳定的。

3.4 用sklearn验证手写结果

手写代码最怕写错,好在有现成实现可以当基准:

from sklearn.discriminant_analysis import LinearDiscriminantAnalysis y = np.array([0]*n1 + [1]*n2) lda = LinearDiscriminantAnalysis() X_lda1d = lda.fit_transform(X_all, y).ravel() w_sk = lda.coef_[0] w_sk = w_sk / np.linalg.norm(w_sk) print("手写方向:", w) print("sklearn方向:", w_sk) print("方向一致性:", abs(np.dot(w, w_sk)))

对这个例子,手写方向与sklearn的coef_归一化后几乎完全一致,点积的绝对值基本等于1。注意方向符号可能相反,但几何上完全等价——正着反着是同一条直线,不影响投影后的可分性。我在复现这类算法时习惯保留这一步对照,一方面验证实现,另一方面也顺便确认自己对接口行为的理解没跑偏。

4. 三个随机实验:把LDA的假设条件逐个破坏

LDA能稳定工作是有前提的,最核心的三个假设是:两类均值有差异、两类协方差大致相同、两类样本量不至于悬殊到让散布矩阵失衡。下面三个实验分别破坏其中一个假设,看看会发生什么。亲手跑过这些场景,比看十遍原理都管用。

4.1 实验一:同协方差、仅均值不同——最舒服的区间

把两类协方差都设成cov1,只保留均值差异。这是LDA最理想的情形:两团形状完全一样的点云,只是位置不同。此时S_W对两类的刻画都很准,投影方向几乎就是"均值连线被类内协方差逆变换修正后的方向"。

有个反直觉的细节:即使均值差是(3,2),投影方向也不一定严格指向(3,2)。如果类内沿某个方向的方差特别大,S_W^{-1}会把均值差向量中沿这个方向的分量压缩,结果是投影方向偏向"方差小、更可靠"的方向。这是LDA非常优雅的行为——它不是机械地画一条穿过两个中心的线,而是主动避开类内拖尾严重的角度,让两类影子在投影轴上收得更紧。

4.2 实验二:均值相同、协方差不同——线性判别直接失效

把两类均值都设成(3,3),但类别1是圆形的cov1=[[1,0],[0,1]],类别2是沿x轴拉长的椭圆cov2=[[2,0],[0,0.5]]。散点图上看,两类确实不同,一个圆、一个扁椭圆,区分它们靠的是"形状"而不是"位置"。

问题随之而来:两个均值相等,S_B = (μ1-μ2)(μ1-μ2)^T = 0,Fisher准则的分子恒为零,J(w)对所有方向都是0,LDA没有可优化的目标。数值实现里会给出一个由数值噪声决定的方向,用它做分类准确率基本靠猜。这正好说明LDA的定位是"线性位置判别",它天生不擅长协方差结构差异问题,这种情况应该转向QDA或核方法。

这个实验也提醒我一件事:遇到"LDA效果差"的结果,先别急着调参,检查两类均值是不是太接近甚至相等。如果是,问题多半出在问题设定本身,而不是算法实现细节。

4.3 实验三:样本量悬殊——共享协方差估计被拉偏

把类别1设为300个样本,类别2只有30个,均值差异照旧。此时S_W由两类的散布矩阵直接相加,类别1对S_W的贡献大约是类别2的十倍,所以S_W更多反映的是大类的协方差结构,小类的结构被严重稀释。

后果是投影方向虽然还大致指向均值差方向,但细节上是偏的——它对小类最有利的分离角度不再敏感。更实际的影响在分类阈值上:如果用投影后两类均值的中间点做阈值,又忽略了先验概率的巨大差异,小类样本被误判的概率会明显上升。我的处理办法是:要么按类样本数对散布矩阵做归一化后再合并,相当于给各类相同的权重;要么在分类阶段显式考虑先验,用sklearn时留意类别先验的设置。

把三个实验的结论整理成一张表,方便对照:

实验场景被破坏的假设现象建议
同协方差、均值不同无LDA表现优秀,Fisher score高标准应用场景
均值相同、协方差不同两类均值有差异S_B为零,方向无意义改用QDA或核方法
样本量悬殊协方差估计均衡性方向偏斜、阈值失真归一化S_W或显式考虑先验

5. 一维投影常见的坑:奇异矩阵、不稳定方向与信息损失

最后集中说说实际操作中容易踩的坑。每一个都是"能跑但结果可疑"的隐蔽问题,遇到时容易让人摸不着头脑。

5.1 类内散布矩阵奇异怎么办

S_W是d×d矩阵,二维情形下要可逆,条件很朴素:两类内部不能出现完全共线的样本,且每类的样本数至少要比特征维度多。最常触发的场景是某类样本太少——比如一类只有2个点,它的散布矩阵秩最多是1,两类合并后有效方向不足,S_W不满秩,np.linalg.solve会直接报矩阵奇异错误。

我印象很深的一次排查:把类别2的样本数从150改成2,代码立刻报LinAlgError。排查顺序是:先看S_W的行列式是否接近0,再检查两个类的样本数和共线性,最后用正则化解法收尾。解决办法分三层。第一层是给S_W对角线加一个小的正则项:

Sw_reg = Sw + 1e-4 * np.eye(2) w_reg = np.linalg.solve(Sw_reg, diff) w_reg = w_reg / np.linalg.norm(w_reg)

第二层是用伪逆np.linalg.pinv(Sw)替代逆矩阵。第三层是直接用sklearn的收缩版本:

lda = LinearDiscriminantAnalysis(solver='eigen', shrinkage='auto')

这三层的本质都是在S_W不可适时注入一点先验信息,让解从"无穷多"或"不存在"变成唯一。我一般优先第三层,因为shrinkage='auto'会自动估计收缩系数,不用手调ε。

5.2 特征值接近零与"方向不稳定"信号

还有一种更隐蔽的情况:S_W可逆,但μ1和μ2非常接近,导致广义特征值λ很小。Fisher score很小,投影后两堆几乎重叠。这时候LDA给出的方向看起来确定,实际高度不稳定——换个随机种子,方向可能大幅摆动,因为它本质上是在拟合两个几乎重合的分布之间的噪声。

判断方法很简单:看那个唯一的非零特征值。如果它非常小,例如小于0.5,投影方向的可信度就要打问号。这正好说明特征值不只是中间产物,它本身就是"类别可分性"的数值化表达。我在做模拟实验时,会把特征值、Fisher score、投影直方图三个量一起看,任何一个提示"可分性差",就不再强行解释那个方向向量了。

5.3 特征尺度不一致:一个常被忽略的预处理问题

LDA不是尺度不变的。假设两个特征一个是"长度",一个是"时间",数值量级差好几个数量级,S_W会被大数值特征主导,均值差向量里的小数值分量完全被压制,投影方向几乎只由大尺度特征决定。这未必是错的,但如果你希望两个特征公平参与判别,就应该先做标准化。

我在自己的流程里默认加一步StandardScaler再做LDA。一个直观的检验方法是:把某个特征整体乘以10或除以10,看投影方向有没有剧烈变化。如果变化很大,说明特征尺度在主导结果,这时候的结论解释要格外谨慎。

5.4 二维到一维的信息损失是结构性的

最后必须承认:维度从2降到1,无论用哪种方法,都会丢掉一个方向的信息。LDA能做的只是保证丢掉的那个方向对分类最不重要。如果两类数据的区分信息需要两个方向才能描述,一维必然不够用。

我常用的经验法则是:先看Fisher score,如果大于4左右(粗略对应两类均值差超过两倍标准差),一维投影基本够用;如果小于这个值,我宁愿保留二维直接上分类器,或者考虑QDA。降维本身不是目的,分类效果才是。别为了"做一个降维demo"强行把数据压到一维。

这篇文章里的实验代码,我全部按"造数—手写—对照sklearn—换参数扰动"的顺序跑过一遍。个人最推荐的练习方式是:改均值和协方差,观察投影方向和Fisher score怎么变,改着改着,你对LDA的理解会比读十遍推导都深。尤其是把两个类的协方差从相同慢慢拉成不同,亲眼看着Fisher score一路下滑,那个过程非常直观,也比任何文字都更有说服力。

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

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

立即咨询