资讯动态

Fisher准则线性分类器设计与实现:从原理到Python代码

发布时间:2026/10/20 7:48:08 来源:尧图企业网站定制
1. 项目概述与整体设计思路做模式识别和机器学习的朋友一定对Fisher准则下的线性分类器设计不陌生。它是监督学习中一个相当经典的线性降维与分类方向本质是在高维特征空间里找一条最优投影方向让两类样本的投影尽量分开同时每一类内部的投影尽量紧凑。很长一段时间里我处理小样本、低维二分类问题时首先尝试的就是这个方法。它不需要复杂的深度学习框架甚至几十行Python就能落地却能把问题的结构看得非常清楚。这篇文章围绕基于Fisher准则的线性分类器设计展开从数学原理到代码实现再到踩坑排查把整个过程讲透。适合刚入门模式识别、机器学习的学生也适合做传统特征分类、想做基线模型的工程师。读完你至少能亲手实现一个可用的线性判别分类器并知道每个参数改动背后的意义。1.1 这个设计到底解决什么问题Fisher准则下的线性分类器设计可以同时解决两个问题一个是分类一个是降维。分类时我们计算一个投影向量 (w)把每个样本 (x) 投影成标量 (y w^T x)然后在 (y) 上找一个阈值大于阈值判成某一类小于阈值判成另一类。降维时我们把原本 (d) 维的特征压缩到一维而且要求这一维最大化类别之间的可分性。很多工程场景里二维散点图上看起来混杂的数据做完Fisher投影之后往往能一眼看出两类是否还有救。设计过程本质上是在做一次权衡希望不同类别投影后的均值离得越远越好同时每一类的投影散开程度越小越好。Fisher准则把这个直觉量化成了目标函数用“类间距离除以类内离散度”来评价投影方向的好坏。这个比值越大说明投影方向越适合用来分类。理解了这一点后面所有的公式推导和代码实现其实都是在围绕这个比值做优化。1.2 为什么不是感知机、不是逻辑回归刚接触线性分类器时很多人会问感知机和逻辑回归也能做线性分类为什么还要搞Fisher准则我的看法是Fisher在处理低维、小样本、线性可分性尚可的数据时有几个非常难得的优点。感知机用错误驱动更新权重算法是简单但最终收敛到的分界面往往受初始值和训练样本顺序影响解不唯一逻辑回归能输出概率但对特征共线性比较敏感还要调学习率和迭代次数。Fisher准则直接基于样本统计量把投影方向表达成解析解不需要学习率不需要随机梯度下降也不存在“当前解是否收敛”的问题。当然我并没有说Fisher在所有场景下都更好。逻辑回归在概率校准方面有优势感知机在大规模流式数据上有它的用武之地。但Fisher最大的特点是稳定、快速、可复现。你只需要算每类样本的均值向量和类内散度矩阵就能得到唯一的方向。对于需要上线做基线的项目来说这种确定性非常宝贵。很多传统系统里Fisher判别分析也被当作首选基线先跑一版看看特征空间到底有没有可分性。1.3 适合的应用场景根据我的经验凡是满足“两类样本分布近似椭球、协方差差距不那么夸张、类别均值有明显差异”的二分类问题都适合先跑一版Fisher分类器。典型场景包括传感器状态判断、产品质量检测中的正负样本判定、语音端点检测前段特征判别、医学指标初筛等。它也很适合作为特征选择工具比如有上百维特征先用Fisher投影到一维观察投影分布如果两类样本在投影后重叠严重说明现有特征可能不够用这时候再考虑上复杂模型也不迟。对于多分类问题Fisher可以按一对多或者逐一配对的方式扩展也可以直接使用多分类线性判别分析变体。不过多分类时要注意类别不平衡和类别数量增加带来的阈值复杂化。这些我会在后面专门展开。总体而言Fisher在简单场景下可以当主角在复杂场景下可以当哨兵先帮你判断特征是否有效。2. Fisher准则核心原理与数学推导2.1 从两类均值和离散度开始假设有两类样本第 (k) 类的样本集合是 ({x_i^{(k)}})其中 (k1,2)每类样本数量分别是 (n_1) 和 (n_2)。首先计算每类的均值向量[ m_k \frac{1}{n_k}\sum_{i \in 第k类} x_i^{(k)} ]这是类别中心的估计。接下来定义类内散度矩阵 (S_w)它由两类各自的散度矩阵相加得到[ S_k \sum_{i \in 第k类} (x_i^{(k)} - m_k)(x_i^{(k)} - m_k)^T ][ S_w S_1 S_2 ]类内散度矩阵刻画的是每个类别内部样本离自己类别中心的整体远近。如果说每类数据像一团云(S_w) 就是把两团云的“厚度”加起来。另外还要定义类间散度矩阵[ S_b (m_1 - m_2)(m_1 - m_2)^T ]它只和两类均值向量之差有关和样本具体散布无关。Fisher准则的目标就是找到一个投影方向 (w)让投影后的 (w^T S_b w) 尽量大同时让投影后的 (w^T S_w w) 尽量小。2.2 目标函数广义瑞利商把样本投到方向 (w) 上之后第一类投影均值是 (w^T m_1)第二类投影均值是 (w^T m_2)。类间距离的平方可以写成[ (w^T m_1 - w^T m_2)^2 w^T (m_1 - m_2)(m_1 - m_2)^T w w^T S_b w ]而投影后类内离散度的总和就是[ \sum_{i \in 第1类} (w^T x_i^{(1)} - w^T m_1)^2 \sum_{i \in 第2类} (w^T x_i^{(2)} - w^T m_2)^2 w^T S_w w ]于是Fisher准则的目标函数就是一个广义瑞利商[ J(w) \frac{w^T S_b w}{w^T S_w w} ]这个形式和信噪比很像分子是信号分母是噪声。我们可以想象用手电筒从某个角度照射两团点云好的照射角度让两团点云在墙面上的影子拉得很开同时每团影子内部又是紧凑的。Fisher准则选的就是这个角度。2.3 求Fisher判别向量因为 (J(w)) 对 (w) 的尺度不敏感把 (w) 放大任何倍数分子分母都会被同时放大比值不变。所以我们可以人为加上一个约束令[ w^T S_w w 1 ]然后最大化 (w^T S_b w)。使用拉格朗日乘子法[ L w^T S_b w - \lambda (w^T S_w w - 1) ]对 (w) 求导并令梯度为零得到[ S_b w \lambda S_w w ]这是一个广义特征值问题。仔细观察会发现(S_b w) 的方向永远和 ((m_1 - m_2)) 平行因此不用真的去解特征值分解也能直接得到最佳投影方向[ w \propto S_w^{-1}(m_1 - m_2) ]这个形式非常简洁。如果 (S_w) 不可逆或者因为特征维度太高导致数值不稳定一般会在 (S_w) 上加一个小的单位阵[ S_w^{reg} S_w \lambda I ]然后再计算[ w (S_w^{reg})^{-1}(m_1 - m_2) ]这里 (\lambda) 是正则化参数具体取值后面会讲。需要特别注意的是(w) 的方向是最优的但长度不影响分类结果。所以实际代码里我一般会把 (w) 归一化方便画图和比较方向。投影之后怎么分类呢最简单的阈值取两类投影均值的中间点[ th \frac{1}{2}(w^T m_1 w^T m_2) ]假如两类先验概率明显不同或者投影后两类方差差别很大这个阈值就不够用了。更稳妥的做法是在验证集上做阈值搜索或者假设投影后每类服从高斯分布再用贝叶斯决策求阈值。这部分我会在第三节和第五节详细说明。2.4 多分类的推广逻辑Fisher准则天然适合二分类但也可以推广到多分类。多分类情况下定义全局散度矩阵 (S_t) 和类内散度矩阵 (S_w)类间散度矩阵为[ S_b S_t - S_w ]然后通过求解广义特征值问题取若干个最大的广义特征值对应的特征向量构成判别子空间。二分类时只能得到一个有效方向多分类时最多可以得到类别数减一个方向。很多资料里把这种推广直接称为线性判别分析简称LDA。实际工程里如果类别数比较多我倾向于先用LDA把维度压到一两个维度再用简单的最近质心或阈值分类效果往往比直接训练多分类逻辑回归更直观。3. 数据准备与核心代码实现3.1 模拟数据与预处理理论讲完接下来进入实操。为了便于复现我使用某模拟项目X生成的二维数据两个类别都从二元高斯分布采样。第一类中心在 ((2,2))第二类中心在 ((5,4))两类有少量重叠但整体可分性还可以。代码生成如下import numpy as np from sklearn.model_selection import train_test_split rng np.random.default_rng(42) m1 np.array([2.0, 2.0]) cov1 np.array([[1.0, 0.3], [0.3, 1.2]]) X1 rng.multivariate_normal(m1, cov1, size120) m2 np.array([5.0, 4.0]) cov2 np.array([[1.2, -0.2], [-0.2, 1.0]]) X2 rng.multivariate_normal(m2, cov2, size140) X np.vstack([X1, X2]) y np.concatenate([np.zeros(len(X1)), np.ones(len(X2))]) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state0 )这里需要特别注意数据预处理。Fisher推导中会用到协方差矩阵如果特征量纲差别很大数值大的特征会在类内散度矩阵里占据主导投影方向可能被无关的尺度影响。实际项目里我一般先对每个特征做z-score标准化而且只用训练集计算出均值和标准差再放到验证集和测试集上做变换避免信息泄露。mu X_train.mean(axis0) std X_train.std(axis0) 1e-12 X_train_s (X_train - mu) / std X_test_s (X_test - mu) / std3.2 核心代码Fisher判别向量计算下面这段代码是Fisher分类器的核心计算类内散度矩阵加入正则可逆处理然后求出投影方向。我通常会把数据按标签0和1拆成两个矩阵再分别计算均值和散度。def train_fisher(X_train_s, y_train, reg1e-4): x0 X_train_s[y_train 0] x1 X_train_s[y_train 1] m0 x0.mean(axis0) m1 x1.mean(axis0) diff m1 - m0 Sw np.zeros((X_train_s.shape[1], X_train_s.shape[1])) for x in x0: d x - m0 Sw np.outer(d, d) for x in x1: d x - m1 Sw np.outer(d, d) Sw_reg Sw reg * np.eye(X_train_s.shape[1]) w np.linalg.inv(Sw_reg) diff return w代码里用循环计算散度矩阵效率不算高但逻辑很清楚。每个样本减去对应类的均值得到残差向量再和自身外积累加。(S_w) 就是这些外积矩阵的和。对数值稳定性要求高的场景可以用矩阵运算把散度计算写成 (X_0^T X_0) 减去均值外积的形式速度会快一些但初学者先看循环版更容易理解。得到了 (w) 之后需要确认标签0和标签1的投影顺序。因为 (w) 和 (-w) 表示同一个方向只是正负相反如果不统一符号后面阈值和预测就会判反。稳妥的做法是计算两类训练样本的投影均值如果标签0的均值大于标签1就把 (w) 翻转。proj0 X_train_s[y_train 0] w proj1 X_train_s[y_train 1] w if proj0.mean() proj1.mean(): w -w proj0, proj1 X_train_s[y_train 1] w, X_train_s[y_train 0] w threshold 0.5 * (proj0.mean() proj1.mean())3.3 分类阈值选择与投影评估阈值选择是Fisher分类器最容易忽略却非常影响结果的地方。如果两类投影后的方差接近直接用投影均值的中间点就可以。但在真实项目里两类投影方差经常不一样类别不平衡也常见。最简单有效的办法是在验证集上做阈值搜索best_acc 0 best_th threshold for t in np.linspace(proj0.min(), proj1.max(), 200): pred ((X_test_s w) t).astype(int) acc (pred y_test).mean() if acc best_acc: best_acc acc best_th t阈值搜索看起来笨但在二分类任务中非常可靠尤其适合作为分类器调优的兜底方案。如果你不想引入验证集搜索也可以用投影后的每类均值和标准差去拟合高斯曲线然后解贝叶斯阈值方程。不过对于大多数工程场景搜索几百个候选阈值花费的时间可以忽略不计结果也更直观。3.4 完整可运行脚本把上面的逻辑整合成一个完整脚本方便直接复制修改import numpy as np from sklearn.model_selection import train_test_split def train_fisher(X_train_s, y_train, reg1e-4): x0 X_train_s[y_train 0] x1 X_train_s[y_train 1] m0 x0.mean(axis0) m1 x1.mean(axis0) diff m1 - m0 Sw np.zeros((X_train_s.shape[1], X_train_s.shape[1])) for x in x0: d x - m0 Sw np.outer(d, d) for x in x1: d x - m1 Sw np.outer(d, d) Sw reg * np.eye(X_train_s.shape[1]) w np.linalg.inv(Sw) diff proj0 x0 w proj1 x1 w if proj0.mean() proj1.mean(): w -w proj0, proj1 x1 w, x0 w threshold 0.5 * (proj0.mean() proj1.mean()) return w, threshold rng np.random.default_rng(42) X1 rng.multivariate_normal([2.0, 2.0], [[1.0, 0.3], [0.3, 1.2]], 120) X2 rng.multivariate_normal([5.0, 4.0], [[1.2, -0.2], [-0.2, 1.0]], 140) X np.vstack([X1, X2]) y np.concatenate([np.zeros(120), np.ones(140)]) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state0 ) mu X_train.mean(axis0) std X_train.std(axis0) 1e-12 X_train_s (X_train - mu) / std X_test_s (X_test - mu) / std w, threshold train_fisher(X_train_s, y_train, reg1e-4) pred (X_test_s w threshold).astype(int) acc (pred y_test).mean() print(fw {w}) print(fthreshold {threshold:.4f}) print(ftest accuracy {acc:.4f})这个脚本在我本地固定随机种子下得到的投影方向大致指向两类中心连线方向测试准确率在九成以上。数据量、随机种子不同具体数值会有波动但整体趋势是稳定的。4. 实验过程与结果分析4.1 实验一两类重叠较小的模拟数据使用上面生成的数据第一类120个样本第二类140个样本两类中心距离大约在 ((3,2))。经过标准化后我跑出的投影向量大致形如 ([0.82, 0.57]) 这样的方向说明Fisher把更多权重放在了第一维上这和第一维的类间差异更大有关。训练集投影后标签0的均值明显小于标签1阈值落在两类投影分布之间测试集准确率约在0.92到0.94之间。这个结果符合预期。Fisher准则本质上是在数据分布的均值和协方差信息上做最优线性投影当两类协方差相差不大时得到的投影方向接近于两类中心连线的方向分类边界也接近垂直平分线。此时投影后的直方图应该呈现双峰形态中间重叠区域很小说明特征可分性足够。4.2 实验二协方差差距较大时的表现为了看看Fisher分类器的极限我调整了第二类的协方差让它远大于第一类。比如第一类协方差设置为 ([0.2,0;0,0.2])第二类协方差设置为 ([4,0;0,4])两类中心仍然保持一定距离。这种情况下Fisher依然能找到投影方向但分类效果明显变差测试准确率会掉到八成左右甚至更低。原因在于Fisher目标函数的分母是两类类内散度之和。如果某一类样本特别分散分母会被这一类主导投影方向会努力把分散的那一类压得更紧而不是单纯拉开两类中心。如果两类协方差结构差异太大线性投影很难同时满足“类间距离大”和“类内距离小”。这个实验说明Fisher准则并不自动适配异方差情况面对这类数据可以考虑先把特征做非线性变换或者改用二次判别分析。4.3 从实验结果读出什么我每次做完Fisher实验都会做两件事第一画出投影后的直方图或者一维散点图直观判断两类样本是否还有重叠第二检查 (w) 的方向和特征维度的关系看哪些特征对分类贡献大。比如二维特征里如果 (w) 的第一个分量远大于第二个分量说明第一维特征对区分两类更重要。这个信息可以帮助后续做特征筛选。还有一点值得强调Fisher只用到了均值和协方差这些统计量对离群点比较敏感。如果数据里有少量明显错误样本均值和协方差都会被带偏投影方向就会失真。所以实验前我一般会先做离群点筛查至少绘制各类样本的箱线图看一下分布范围。5. 常见问题与排查技巧实录5.1 类内散度矩阵奇异这是我被问得最多的问题。当特征维度大于样本数量或者特征之间存在高度相关时(S_w) 会接近奇异直接求逆会出现巨大数值。解决方案有三个一是加正则化项在 (S_w) 上叠加 (\lambda I)二是先用PCA降维把特征压到样本量以下三是使用伪逆也就是奇异值分解后把零奇异值置为很小的数再求逆。正则化参数 (\lambda) 的取值需要根据特征尺度调整。如果特征已经标准化我通常从 (1 \times 10^{-4}) 开始试最多到 (1 \times 10^{-2})。太小了数值不稳定太大了投影方向会被单位阵主导逐渐退化成直接用均值差分类。你可以观察矩阵条件数如果条件数超过 (10^6)就要警惕数值问题。5.2 投影结果与直觉相反有时候你明明觉得代码没问题但预测结果和想象完全相反比如准确率只有零点几。最大的可能有两个一是标签0和标签1的顺序反了二是 (w) 的符号导致投影方向翻转。这类问题很好查把训练集的投影结果分别打印出来看两类均值的大小关系再对齐阈值判断逻辑就行。我习惯统一约定“投影值大于阈值被预测为标签1”这样排查起来省很多时间。5.3 分类阈值总是不准即使投影方向正确阈值选不好也会严重影响准确率。直接使用两类投影均值的中点隐含假设是两类投影后方差相等而且先验概率也相等。真实数据里要么类别不平衡要么某一类特别分散中点就会偏向噪声多的那一侧。更可靠的做法是使用验证集做阈值搜索也可以根据先验概率调整[ th \frac{w^T m_1 w^T m_2}{2} \frac{\log(p_2/p_1)}{w^T (m_1-m_2)?} ]这个公式推导起来比较绕线上系统里我一般直接用搜索阈值既简单又直观。阈值搜索的粒度可以先粗后细第一轮分成100段找粗略区间第二轮再在最优区间附近细分。5.4 多分类与线性不可分问题Fisher准则用线性投影表达类别差异遇到XOR这类非线性可分问题时直接做效果很差。如果你发现训练和测试准确率都很低而且投影直方图两类完全混在一起就别硬调Fisher了考虑给特征加多项式展开或者换核方法。多分类时一对多策略容易引入类别不平衡一对一策略训练次数较多。条件允许的话直接用多分类LDA求出判别子空间后配合最近质心分类通常更稳定。5.5 问题排查速查表现象可能原因排查方法矩阵求逆报奇异或结果爆炸特征维度高或特征相关性强加正则项用伪逆先PCA降维测试准确率接近0标签顺序或 (w) 符号反了打印两类投影均值统一正负约定训练准确率高但测试低过拟合正则化不足增大正则项增加样本量减少特征阈值怎么调都低特征可分性差或分布非线性看投影直方图尝试特征扩展或换模型某类完全被分错类别不平衡调整阈值换用约登指数或F1搜索这张表是我在项目debug过程中慢慢积累出来的。遇到Fisher分类器效果不好先不要怀疑数学公式大部分时候都是数据、符号、正则化这三类问题。6. 工程细节与个人调参心得6.1 特征标准化是隐形前提很多人照着网上的公式写完代码直接拿原始特征去算 (S_w) 和 (S_b)结果发现投影方向被量纲大的特征带偏。举个极端例子一个特征范围在0到1另一个特征范围在0到10000第二个特征的协方差天然就大类内散度矩阵里它占主导Fisher会把这个特征当成主要区分方向哪怕它实际没有判别力。所以标准化不是可选项而是必要的前置步骤。如果你用了正则化标准化后更容易选择统一的 (\lambda) 范围不用每个特征单独调。6.2 正则项怎么加正则化的核心是让 (S_w \lambda I) 变得可逆且稳定。理论上 (\lambda) 应该尽量小保留数据本身的协方差结构但太小了数值条件数太大求逆结果不可靠。我的一般流程是先做特征标准化然后试 (\lambda 0, 1e-6, 1e-4, 1e-2)观察训练准确率和验证准确率。如果 (\lambda0) 能正常跑就先用0如果报奇异就从 (1e-6) 起步。注意正则化不能解决因样本量太少导致的所有问题如果样本只有几十个而特征有上百维加一点正则也是杯水车薪更需要考虑特征选择。6.3 和PCA的区别Fisher准则和主成分分析虽然在代码上很像本质却完全不同。PCA是无监督方法它找的是方差最大的方向不关心类别标签Fisher是有监督方法它找的是能让类别分开的方向。有时候某个方向PCA方差很大但两类样本在这个方向上的投影完全重叠对分类毫无帮助。反之Fisher选出的方向可能方差不大但投影后两类样本分得清清楚楚。所以千万不要用PCA结果去替代Fisher两者解决的问题不一样。6.4 什么场景下不建议使用Fisher如果数据类别严重不平衡Fisher会遇到小样本类别的协方差估计不稳定问题如果特征大多是离散计数均值和协方差的表达能力也有限如果数据分布存在明显非线性边界线性Fisher无论如何调整阈值都很难拉高上限。这些场景下我会把Fisher当成一个诊断工具而不是最终模型先算一下投影后两类样本的可分性如果连Fisher都分不开再考虑升级模型也不迟。在实际项目中我经常用Fisher做第一版快速原型。它的好处是跑得快、可解释、参数少能很快告诉大家当前特征够不够用。如果在Fisher投影后的直方图里已经能明显看到两个峰那后面上稍微复杂一点的模型效果通常也不会太差。如果Fisher投影后一片混乱那问题多半不在分类器而在特征本身。先花十几分钟跑一版Fisher往往比直接调深度学习模型参数要省下更多时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑