☰
Fisher线性判别与LDA实现:从原理到Python实战
2026/10/1 12:01:24 网站建设 项目流程

简介:基于Python的Fisher线性判别模式识别资源包,面向模式识别课程学习者、需要完成相关大作业的学生,也适合对线性分类算法感兴趣的初学者。该方法的本质是将两类样本投影到一条直线上,再在一维空间中寻找分类阈值,而Fisher判别的核心在于选择投影方向,使类间距离尽可能大、类内离散度尽可能小。资源包共14个文件,压缩包大小1.02MB,内含LDA_Fisher.py等2个Python脚本、sonar与iris两套经典的data数据文件,以及docx、pdf、md等说明文档,文件类型覆盖源码、数据集、实验报告与项目说明。已有250人浏览学习。通过直接运行脚本并搭配数据集,可以复现Fisher线性判别的完整流程,深入理解投影方向选择与阈值确定的具体实现,同时借助附带的文档和PDF报告梳理实验步骤与数学推导,是一份适合课程设计、期末大作业或入门自学的紧凑参考资料。

1. Fisher线性判别:两分类问题中最不该跳过的一课

模式识别课程里,Fisher线性判别永远是最先登场的那几个算法之一。它不像神经网络那样有层数可调,也不像SVM那样有核函数可选,核心逻辑只有一句话:把所有样本投影到一个方向上,让两类在这个一维空间里"距离最远、各自最聚拢",然后选一个阈值切开。这个思路朴素,但数学推导完整,而且直接通向后来所有基于散度矩阵的分类方法。

这份资源包里是完整的Python实现(LDA_Fisher.py),加上IRIS和SONAR两组经典数据集,附带一份模式识别大作业PDF和Fisher线性判别的说明文档。适合正在做模式识别作业的在校学生,也适合刚接触线性判别分析(LDA)的工程师。接下来我把原理、代码、数据集和边界坑全部拆开讲,读完你不仅能跑通,还能知道什么场景该用、什么场景该绕开。

2. Fisher线性判别的数学原理:类间散度与类内散度的比值最大化

2.1 从"投影到一条线"说起:为什么降维反而是分类的有效策略

Fisher处理的是两类分类问题:给定d维空间中的样本,找一个方向w,把每个样本投影成w方向上的一个标量。投影之后,分类边界从d维超平面退化成一维阈值,分类器变成了"数值大于阈值判一类、小于阈值判另一类"。

但问题来了:随便找一个方向行不行?行,但效果参差不齐。做过实操的同行喜欢用"山包"来比喻——把每一类样本投影到w上之后,会形成两个一维分布,近似看就是两个高斯山包。如果w选得好,两个山包分得远、峰形窄;如果w选得差,两个山包叠在一起,阈值怎么放都有很高的误判率。

Fisher的贡献在于:把"选方向"这个直觉问题转化成"最大化一个比值"的数学问题。定义类内散度矩阵Sw和类间散度矩阵Sb之后,最优方向就是让Fisher准则函数取最大值的w:

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

这里Sw衡量样本在各类内部的离散程度,Sb衡量两个类之间的离散程度。J(w)越大,投影后类间距离相对类内散布越大,两类越容易分开。换一个角度看,Fisher判别本质上是"最佳一维投影"的求解器。真实数据的特征维度往往很高,但分类边界可能只取决于少数几个线性组合方向。Fisher找到的这个方向,就是在线性可分假设下信息量最大的那个方向,这也是它经常被用作降维和特征抽取手段的原因。

2.2 最优投影向量与阈值:Sw逆、均值差和适用边界

对两类问题,类间散度矩阵Sb可以展开成(m1 - m2)(m1 - m2)^T的形式,其中m1、m2是两个类的均值向量。对J(w)求导并令导数为零,经过化简得到一个非常干净的结论:

w* = Sw^(-1) (m1 - m2)

这个式子值得拆开看。第一层,投影方向由两个均值之差和类内散度共同决定:均值差越大,方向越偏向它;某个特征方向上方差越大(Sw对应元素大),该方向的贡献越小,因为内部散布大、不利于区分。第二层,如果Sw退化成单位矩阵的倍数(两类都是球形分布),那么w*就退化成m1 - m2,方向与两个类别中心的连线一致,跟直觉完全吻合。

阈值怎么选?Fisher原始推导里建议在投影空间取两类均值的中点,也就是t = w^T (m1 + m2) / 2。原因是投影后的两个类近似服从同一方差的高斯分布时,贝叶斯最优决策面就是两个均值的平均点。注意这里的"同一方差"假设很关键——如果两类方差差异明显、或者先验比例失衡,中点不是最优阈值。后面我会专门展开讲什么时候要调、怎么调。

还有一个容易忽略的边界:如果两类均值几乎重合,也就是m1约等于m2,那么Sb趋近于零矩阵,J(w)对任何方向都不敏感,Fisher失效。这也解释了为什么Fisher线性判别在两个类本身重叠很严重的场景下表现不佳,这是算法结构决定的,不是换一组参数能弥补的。

2.3 LDA_Fisher.py代码拆解:numpy矩阵运算落地

资源包里的LDA_Fisher.py是核心实现,按常见的实现路径拆成三个函数来看。先看数据加载和投影方向计算:

import numpy as np def load_data(filepath): """读取逗号分隔的数据文件,最后一列是标签""" raw = np.loadtxt(filepath, delimiter=',', dtype=str) X = raw[:, :-1].astype(float) y = raw[:, -1] return X, y def compute_fisher(X, y): """计算Fisher最优投影方向w和两类均值投影值""" classes = np.unique(y) X1 = X[y == classes[0]] X2 = X[y == classes[1]] m1 = np.mean(X1, axis=0) m2 = np.mean(X2, axis=0) S1 = np.cov(X1.T) * (X1.shape[0] - 1) S2 = np.cov(X2.T) * (X2.shape[0] - 1) Sw = S1 + S2 w = np.linalg.inv(Sw).dot(m1 - m2) return w, m1, m2

load_data用np.loadtxt配合dtype=str读文本数据,因为iris.data和sonar.all-data的标签列是字符串,直接按浮点读会报错。统一按字符串读入再转float,兼容性更好。compute_fisher里,np.cov(X1.T)计算特征维度间的协方差矩阵,乘以X1.shape[0] - 1抵消自由度修正,得到原始散度矩阵。S1与S2相加是两类共用的类内散度,np.linalg.inv(Sw)求逆后乘上均值差,就是w。

def predict(X, w, m1, m2): """投影后按均值中点阈值分类""" proj = X.dot(w) t = w.dot(m1 + m2) / 2.0 return (proj > t).astype(int), proj, t

predict把样本投影到w方向,阈值t取两类均值投影的中点。这里proj是标量序列,t是标量,返回的pred是0/1数组,配合原始标签做准确率统计。跑通这段代码的前提是本机python环境装好了numpy,如果还没装,用pip install numpy装一下,几分钟的事。不管你是用pycharm还是vscode写脚本,只要解释器能找到numpy就够。

提示:这份资源没有依赖sklearn,Fisher核心逻辑全部用numpy手写。好处是数学过程完全透明,坏处是Sw奇异时没有自动防错,后面会看到怎么处理。

其实这份资源里的PDF文档还把Fisher线性判别从投影公式到准则函数推导写了一遍,适合对着代码一起看。如果只跑代码不懂推导,遇到Sw奇异这类问题就会一头雾水;反过来只读推导不碰代码,又很难理解为什么w要这么算。两样搭着看是最快路径。

3. 在IRIS与SONAR数据集上完整复现:数据准备到分类精度

3.1 IRIS数据集:两类切分与特征组合选择

资源包里的iris.data是完整的150个样本、4个特征、3个类别(Setosa、Versicolor、Virginica)。Fisher只支持两类判别,所以第一步是决定"哪两类拿来判别"。常见选择有两种。

第一种,取Setosa和Versicolor,这两类在花瓣长度和花瓣宽度特征上区分度很高,Fisher投影后准确率接近100%,适合用来验证流程是否正确。第二种,取Versicolor和Virginica,这两类在特征空间中有交叠,Fisher投影后的准确率通常在90%到95%之间,能看到算法在"不那么好分"的数据上表现如何。

raw = np.genfromtxt('iris.data', delimiter=',', dtype=str) X = raw[:, :4].astype(float) y = raw[:, 4] mask = (y == 'Iris-versicolor') | (y == 'Iris-virginica') X_sub = X[mask] y_sub = y[mask] y_bin = np.where(y_sub == 'Iris-versicolor', 0, 1) w, m1, m2 = compute_fisher(X_sub, y_bin) pred, proj, t = predict(X_sub, w, m1, m2) print('Versicolor vs Virginica 准确率:', np.mean(pred == y_bin))

np.genfromtxt对含字符串的混合类型更宽容,读入后第4列是类别名。mask过滤出后两类,np.where把Versicolor映射成0、Virginica映射成1。这一步处理好后,compute_fisher和predict都能直接吃进数值标签。

还有一个值得做的操作:特征组合筛选。IRIS只有4个特征,但花瓣长度和花瓣宽度相关系数很高,几乎携带相同的信息。Fisher对特征冗余敏感,因为Sw里存在强相关会让矩阵趋于奇异。我一般会快速算一下特征间的相关系数,把相关系数超过0.95的其中一个删掉,再跑Fisher。只保留两个区辨力最好的特征时,准确率通常不会掉太多,但Sw矩阵更稳定,w的可解释性也更清晰。

3.2 SONAR数据集:高维小样本导致Sw奇异,先处理可逆性

SONAR数据集(sonar.all-data)是模式识别课里经常用来"虐新手"的benchmark。208个样本,60个特征,标签是R(岩石)和M(矿石)。这组数据的特性是特征维度远高于IRIS,但样本量只有208,且60个特征包含大量相似频段的能量值,实际有效维度远低于60。

直接在这个数据集上调用compute_fisher,大概率会撞上np.linalg.inv(Sw)报奇异矩阵错误。因为60维特征构成的Sw是60×60矩阵,虽然理论上样本数大于维度,但特征之间高度相关,Sw的有效秩远低于60,代入求逆就崩了。标准做法有两个,第一个是给Sw加正则项:

Sw_reg = Sw + 1e-5 * np.eye(60) w = np.linalg.inv(Sw_reg).dot(m1 - m2)

这个1e-5不是拍脑袋定的。它需要足够大以保证数值稳定,又不能大到扭曲Sw本身的特征结构。经验值在1e-8到1e-3之间,优先从1e-5试起。加上正则之后投影方向会更平滑,代价是引入一点偏差。如果正则系数调得过大,比如0.1级别,投影方向会被过度修正,准确率反而下降。

第二个方案是先降维再Fisher。用主成分分析把60维压到15到20维,再跑Fisher,效果通常比直接正则化更稳,因为PCA在降维的同时把噪声和共线性都削弱了:

from sklearn.decomposition import PCA pca = PCA(n_components=20) X_pca = pca.fit_transform(X) w, m1, m2 = compute_fisher(X_pca, y_bin)

一句话总结:遇到Sw不可逆,优先PCA,其次正则化。正则化用在"不想损失原始特征维度"的场景,PCA用在"想要一个干净低维输入"的场景。SONAR数据集配套的sonar.names文件里有每个特征的物理含义说明,看一下会发现很多特征本来就是同一个频段的能量,降维处理完全合理。

3.3 运行时序:训练集、验证集与随机种子

Fisher判别没有显式的"训练模型"过程,整个计算是解析解。但如果你只在一个数据集上算w、再在同一个数据集上预测,得到的是训练集准确率,不能说明泛化能力。合理的运行时序是切出测试集:

from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y_bin, test_size=0.3, random_state=42, stratify=y_bin) w, m1, m2 = compute_fisher(X_train, y_train) pred_train, _, _ = predict(X_train, w, m1, m2) pred_test, _, _ = predict(X_test, w, m1, m2) print('训练集准确率:', np.mean(pred_train == y_train)) print('测试集准确率:', np.mean(pred_test == y_test))

train_test_split里random_state=42固定随机种子,保证每次跑的结果一致,方便复现和对比。stratify=y_bin让训练集和测试集的类别比例与原始数据保持一致,对SONAR这种样本不算多的数据集尤其重要。测试集比例取0.3,208个样本会留出约62个做验证,够用了。

分完数据后,w和阈值t只在训练集上计算。测试集只是把样本投影到训练集学到的方向上,再做阈值判断。如果测试集准确率比训练集低很多,比如差10个百分点以上,要怀疑两类协方差差异过大、或者w在训练集上已经过拟合。在这个意义上,Fisher虽然数学上简单,但它的泛化能力同样需要验证集来兜底。

4. 避坑指南:Fisher线性判别实战中的四个高频问题

4.1 Sw奇异导致np.linalg.inv报错

现象:运行LDA_Fisher.py时,控制台抛出LinAlgError: Singular matrix,或者虽然不报错但投影结果里出现inf或nan。

原因:特征维度d大于等于样本量n时,Sw一定不可逆;d接近n但特征之间高度相关时,Sw也接近奇异。SONAR的60维特征、208个样本是典型case,IRIS的4维特征不会有这个问题。

解决:在Sw上加小正则项Sw += 1e-5 * np.eye(d),或者先PCA降维再算。两个方案都用过之后,我的习惯是:特征维度低于50时加正则最快,高于50时直接PCA压到20维,顺便降低噪声干扰。如果用了正则项准确率还是异常低,检查一下正则项是否过大——它会把w的模长压小,导致投影值全部挤在阈值附近,分类退化。

4.2 数据标准化该不该做:别以为Fisher对量纲不敏感

现象:在SONAR上不标准化跑Fisher准确率68%,标准化之后变成75%,差异非常明显。

原因:Sw和Sb是带量纲的量,特征量纲不同时,量纲大的特征在散度矩阵里的权重天然更大。Fisher不像决策树那样对尺度不敏感,它的距离是真实尺度下的距离,量纲差异会直接扭曲投影方向。

解决:做z-score标准化,每个特征减去均值除以标准差。关键细节是标准化参数只在训练集上获取,再应用到测试集:

from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_std = scaler.fit_transform(X_train) X_test_std = scaler.transform(X_test)

注意X_test_std用的是transform而不是fit_transform。如果在整份数据上先做标准化再切训练测试集,测试集的均值和标准差会混进训练过程,这是数据泄露,会造成验证结果虚高。

4.3 均值中点阈值失效:样本不均衡时的调法

现象:两类样本比例是7:3,用w^T(m1+m2)/2作为阈值,预测结果大量偏向多数类,少数类召回率只有30%。

原因:类别均值中点假设两类先验相等、误分类代价相等。当样本不均衡时,最优阈值应该向先验概率更大的那类的均值方向移动,中点就不再是最优决策面。

解决:在投影后的训练集上做阈值网格扫描,选让F1分数最大化的阈值:

from sklearn.metrics import f1_score proj_train = X_train.dot(w) grid = np.linspace(proj_train.min(), proj_train.max(), 200) best_f1, best_t = 0, 0 for val in grid: pred_val = (proj_train > val).astype(int) f1 = f1_score(y_train, pred_val, zero_division=1) if f1 > best_f1: best_f1, best_t = f1, val print('最优阈值:', best_t, 'F1:', best_f1)

这个扫描本质是一维搜索,计算开销很低。200个候选阈值是从投影最小值到最大值的均匀划分,够用了。注意这里用F1而不是准确率——在样本不均衡场景下,准确率容易被多数类带偏,F1能同时反映两个类的表现。

4.4 把两类Fisher直接平移到多类

现象:拿到三类数据后,直接在三个类别上调用compute_fisher,得到的w是一个向量,分类结果一塌糊涂。

原因:两类Fisher的类间散度矩阵来自两个均值之差的外积,这是秩1结构。三类及以上时,类间信息分布在一个更高维的子空间里,一条投影线装不下,需要多个投影方向。

解决:多类场景用sklearn的LinearDiscriminantAnalysis,它内部用广义特征值分解替代了"Sw逆乘以均值差"的单一计算,支持多类数据:

from sklearn.discriminant_analysis import LinearDiscriminantAnalysis lda = LinearDiscriminantAnalysis() lda.fit(X_train, y_train) print('多类LDA验证准确率:', lda.score(X_test, y_test))

如果坚持手写多类Fisher,需要求解Sw^(-1)Sb的广义特征值问题,取前类别数减1个特征向量作为投影矩阵。现实情况是sklearn的LDA已经处理好了Sw奇异问题,内部用SVD分解而不是直接求逆,数值稳定性比手写numpy好一个量级。没有必要为了"不用库"而去重复造这个轮子。

5. 进阶路线:多类探索、模型对比与泛化评估

5.1 从两类到多类:散度矩阵的推广与广义特征值问题

两类Fisher的类间散度矩阵是(m1-m2)(m1-m2)^T,这是一个只有单一方向的秩1矩阵。多类情况下,类间分散在多个方向上,类间散度矩阵扩展为:

Sb = Σ[i=1..k] n_i (m_i - m)(m_i - m)^T

其中k是类别数,n_i是第i类样本数,m_i是第i类均值,m是全局均值。Sw仍然是各类类内散度之和。此时Fisher准则矩阵形式化为Sw^(-1)Sb,最优投影方向是它的前几个特征向量。

由于Sw^(-1)Sb不保证对称,通常用广义特征值分解求解:Sw^(-1)Sb w = λ w。特征值λ表示该投影方向上的判别能力,取前r个方向构成投影矩阵,r = 类别数 - 1。这个多类LDA推广有两个典型用途:一是分类,二是降维可视化。三类数据降到2维后,可以直接在二维平面上画出三类的分布,观察类间分离程度。

如果用numpy/scipy实现广义特征值分解,写法如下:

from scipy.linalg import eigh eigenvalues, eigenvectors = eigh(Sb, Sw) idx = np.argsort(eigenvalues)[::-1] W = eigenvectors[:, idx[:r]] # r = 类别数 - 1

但这里有个现实问题:Sw奇异时eigh会直接报错或返回nan。scipy.linalg.eigh的第二个参数是广义特征值问题的分母矩阵,如果Sw不可逆,通常还是要先加正则或者PCA降维。相比之下,sklearn内部用了SVD,通过数据白化隐式处理奇异问题,所以多类场景我建议直接用sklearn的LinearDiscriminantAnalysis。

5.2 Fisher判别 vs 逻辑回归 vs SVM:数据分布决定选型

Fisher线性判别的优势是解析解、计算快、可解释性强,但它的假设是两类共享协方差结构。如果这个假设不成立,比如两类方差差异很大,二次判别分析(QDA)更合适——它为每个类单独估计协方差矩阵,代价是参数数量翻倍,小样本下容易过拟合。

在这个场景下,逻辑回归是更好的折中:它不要求同类协方差,加上L2正则后在高维小样本数据上比Fisher稳定得多。SVM则是另一种选择,线性SVM通过最大化间隔来决定分类面,对数据分布形式的假设更少。Fisher能直接给出投影方向和可解释的w,SVM给的是支持向量,解释性不同。

场景推荐方案理由
两类协方差接近,样本量中等Fisher / LDA简单、可解释、有解析解
两类协方差差异大QDA二次判别每类单独建模协方差
高维小样本逻辑回归 + L2 / 线性SVM避免Sw奇异
非线性边界RBF核SVM / 随机森林线性判别器失效

用sklearn做逻辑回归对比也很直接:

from sklearn.linear_model import LogisticRegression lr = LogisticRegression(C=1.0, max_iter=1000) lr.fit(X_train_std, y_train) print('逻辑回归测试集准确率:', lr.score(X_test_std, y_test))

C是正则化强度的倒数,C越小正则化越强。在SONAR这种高维小样本场景,C从1.0开始往下调,往0.01方向试,通常能找到比Fisher更稳的结果。逻辑回归不需要Sw,也就没有奇异矩阵这个坎,这是它在这个数据上的一大优势。

5.3 k折交叉验证:评估Fisher判别的真实泛化能力

Fisher判别没有"epoch"概念,w和阈值都是解析计算出来的,所以交叉验证的流程很清晰:每折独立计算w、独立定阈值、在验证折上测准确率。唯一容易踩的是数据泄漏——标准化和PCA降维如果混入验证折数据,会人为拔高验证结果。

from sklearn.model_selection import StratifiedKFold from sklearn.preprocessing import StandardScaler skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) fold_accs = [] for train_idx, val_idx in skf.split(X, y_bin): X_train, X_val = X[train_idx], X[val_idx] y_train, y_val = y_bin[train_idx], y_bin[val_idx] scaler = StandardScaler().fit(X_train) X_train = scaler.transform(X_train) X_val = scaler.transform(X_val) w, m1, m2 = compute_fisher(X_train, y_train) pred_val, _, _ = predict(X_val, w, m1, m2) fold_accs.append(np.mean(pred_val == y_val)) print('5折平均: %.3f ± %.3f' % (np.mean(fold_accs), np.std(fold_accs)))

重点在scaler = StandardScaler().fit(X_train)这一行——它只学习训练折的均值和标准差,验证折是事后用同一套参数转换的。StratifiedKFold保证每折类别比例与总体一致,样本不均衡时比普通KFold可靠。random_state=42固定每折划分,结果可复现。

折数怎么选?208个样本的SONAR,5折每折验证集约41个样本,10折每折约20个。折数越多训练集越大,但验证集越小,评估方差越大。这个数据规模我建议5折;如果样本小于100,考虑3折或留一法。Fisher是解析解,计算开销极低,留一法跑208折也没压力,只是评估结果方差可能偏大。

6. 一个高阶技巧:用投影直方图代替散点图,直观定阈值

Fisher做两分类,最终所有样本都被投影到一条直线上。与其盯着准确率数字反复调参,我更建议把投影结果画成直方图:两个山包一左一右,中间的低谷就是阈值的最优位置。这张图能直接告诉你"分得开还是分不开",比任何指标都直观。

import matplotlib.pyplot as plt proj = X.dot(w) plt.hist(proj[y_bin == 0], bins=20, alpha=0.6, label='Class 0') plt.hist(proj[y_bin == 1], bins=20, alpha=0.6, label='Class 1') plt.axvline(t, color='red', linestyle='--', label='Threshold') plt.xlabel('Projection onto Fisher direction') plt.ylabel('Count') plt.legend() plt.show()

bins=20是个稳妥默认值:太少会掩盖分布形状,太多会被单样本噪声干扰。直方图的重叠面积大致反映当前特征组合下的分类错误下限——如果两个山包几乎完全重叠,说明在当前特征上两类线性不可分,调阈值和调正则化都救不回来,唯一的出路是换特征或换非线性模型。

这个可视化的另一个用处是把网格扫描得到的最优阈值画上去。当数据不均衡导致中点阈值失效时,你会看到最优阈值确实落在两个山包之间的谷底、但明显偏向某一侧。把标准化、PCA、正则化分别做一遍,再各画一张直方图,就能直观感受到每个操作对投影分布的改善程度。这个习惯帮我避开了很多看似是算法问题、实则是特征选择问题的弯路。从那以后,我每次跑Fisher判别都会先强制画一遍投影直方图,再决定要不要动阈值和正则化参数,希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询