简介基于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准则函数取最大值的wJ(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,, dtypestr) 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, axis0) m2 np.mean(X2, axis0) 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, m2load_data用np.loadtxt配合dtypestr读文本数据因为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, tpredict把样本投影到w方向阈值t取两类均值投影的中点。这里proj是标量序列t是标量返回的pred是0/1数组配合原始标签做准确率统计。跑通这段代码的前提是本机python环境装好了numpy如果还没装用pip install numpy装一下几分钟的事。不管你是用pycharm还是vscode写脚本只要解释器能找到numpy就够。提示这份资源没有依赖sklearnFisher核心逻辑全部用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,, dtypestr) 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_components20) 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_size0.3, random_state42, stratifyy_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_state42固定随机种子保证每次跑的结果一致方便复现和对比。stratifyy_bin让训练集和测试集的类别比例与原始数据保持一致对SONAR这种样本不算多的数据集尤其重要。测试集比例取0.3208个样本会留出约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个样本是典型caseIRIS的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(m1m2)/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_division1) 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 Σ[i1..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能直接给出投影方向和可解释的wSVM给的是支持向量解释性不同。场景推荐方案理由两类协方差接近样本量中等Fisher / LDA简单、可解释、有解析解两类协方差差异大QDA二次判别每类单独建模协方差高维小样本逻辑回归 L2 / 线性SVM避免Sw奇异非线性边界RBF核SVM / 随机森林线性判别器失效用sklearn做逻辑回归对比也很直接from sklearn.linear_model import LogisticRegression lr LogisticRegression(C1.0, max_iter1000) 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_splits5, shuffleTrue, random_state42) 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_state42固定每折划分结果可复现。折数怎么选208个样本的SONAR5折每折验证集约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], bins20, alpha0.6, labelClass 0) plt.hist(proj[y_bin 1], bins20, alpha0.6, labelClass 1) plt.axvline(t, colorred, linestyle--, labelThreshold) plt.xlabel(Projection onto Fisher direction) plt.ylabel(Count) plt.legend() plt.show()bins20是个稳妥默认值太少会掩盖分布形状太多会被单样本噪声干扰。直方图的重叠面积大致反映当前特征组合下的分类错误下限——如果两个山包几乎完全重叠说明在当前特征上两类线性不可分调阈值和调正则化都救不回来唯一的出路是换特征或换非线性模型。这个可视化的另一个用处是把网格扫描得到的最优阈值画上去。当数据不均衡导致中点阈值失效时你会看到最优阈值确实落在两个山包之间的谷底、但明显偏向某一侧。把标准化、PCA、正则化分别做一遍再各画一张直方图就能直观感受到每个操作对投影分布的改善程度。这个习惯帮我避开了很多看似是算法问题、实则是特征选择问题的弯路。从那以后我每次跑Fisher判别都会先强制画一遍投影直方图再决定要不要动阈值和正则化参数希望帮到你。本文还有配套的精品资源点击获取