☰
Fisher线性判别详解:基于Iris数据集的LDA降维与可视化实践
2026/10/14 15:24:24 网站建设 项目流程

Iris数据集对于搞机器学习的来说,就像学编程必写“Hello World”一样,绕不过去。Fisher线性判别则是这个经典数据集上最经典的一把刀。很多教程喜欢把LDA讲成查公式、调库的一锤子买卖,但我个人觉得,如果不把Fisher判别的几何直觉和Iris数据本身的分布特点吃透,就永远只是在“调用”算法,而不是“使用”算法。

这篇就拿Iris数据集做靶子,把Fisher线性分类从头到尾捋一遍,加上数据可视化的完整思路和实操代码,把我踩过的坑和“原来还能这样看数据”的经验一并倒出来。

1. 这个项目到底在解决什么问题

1.1 从三簇鸢尾花开始

Iris数据集的原始观察记录很有意思,植物学家对三种鸢尾花——山鸢尾、变色鸢尾、维吉尼亚鸢尾,各自量了50朵花的花萼长度、花萼宽度、花瓣长度、花瓣宽度。这4个维度就是我们要用的特征,标签是花的类别。

这本质上是一个三维分类问题,或者说,给定4个特征,预测样本属于三个类别中的哪一个。Fisher线性判别解决这个问题的方式非常聪明:它先找到一条“最优直线”(严格来说是高维空间里的一个最优线性子空间),然后把数据投影到这条直线上,最后在投影后的空间里做分类。

我当年第一次接触这个思路,觉得有点绕——“为什么要把高维数据压到一条线上?”答案其实就藏在高维数据的“维度灾难”里。4个特征在4维空间里分布,人类的眼睛最多只能看到3维,你没法直接观察数据形态。Fisher的想法就是找到一个方向,使得沿着这个方向看数据时,不同类别的点最能分得开。

这里必须补充一个关键前提:Fisher线性判别是一个监督学习方法,也就是说,在寻找投影方向时它是知道每个样本属于哪个类的。它做的事可以理解为“带着答案找最优视角”,这和PCA那种无监督的“最大方差视角”有本质区别。

1.2 线性判别为什么能“一招制敌”

在多数入门教材里,Fisher线性判别和LDA(Linear Discriminant Analysis)基本是同义词。但严谨地说,Fisher判别提出的是一个“寻找最优投影方向”的思想框架,LDA是这个框架下的完整实现。它的核心目标是最大化类间距离、最小化类内距离。

用人话解释一下:我们希望在投影后的直线上,不同类别样本的中心点离得越远越好,同时同一类别内部的样本点尽量聚拢。

Iris数据集恰好是一个“近乎线性可分”的数据集,其中山鸢尾和另外两种鸢尾在花瓣长度、花瓣宽度两个维度上几乎完全分开,但变色鸢尾和维吉尼亚鸢尾之间存在少量重叠区域。这就是Fisher判别最理想的试验场——它不需要复杂的非线性映射,仅凭一个线性投影就能取得非常高的分类准确率。

2. 核心原理拆解

2.1 Fisher判别的目标函数是怎么来的

数学推导不打算从头抄一遍教材,但核心的几个概念必须吃透。整个方法围绕着“散布矩阵”转,注意是散布(scatter)而不是方差(variance)。

定义第i类的样本集合为,类别总数为K,每个类别的样本数为。全局均值向量的计算公式为,第i类的均值向量为。

类内散布矩阵的定义是所有类别内部样本偏离本类中心的程度之和:

分母是总样本数减1,这个细节很多人推导时容易忽略。

类间散布矩阵描述的是各类别均值点相对全局均值的偏离程度,并且用样本数量做了加权:

Fisher目标函数定义为投影后类间散布与类内散布之比:

J(W)越大,说明投影后类间分得越开、类内聚得越拢。求解这个最大化问题,最终可以转化为求解广义特征值问题:

其中特征向量就是我们要找的投影方向。

这里有个非常实用的结论:对于K分类问题,Fisher判别最多能找到K-1个非零特征值,对应K-1个投影方向。Iris数据有3类,所以最多能得到2个有效投影方向,数据被投影到一个二维平面上。这就是后面可视化部分的理论依据。

2.2 为什么是线性,而不是非线性

很多人问过“为什么不用SVM核函数、为什么不用神经网络”。答案不是“Fisher简单”,而是“在Iris数据上线性假设已经够用”。

从实际分布来看,四个特征的线性组合足以将三类鸢尾花几乎完全区分。只有在原始特征空间内线性不可分的情况下,才需要考虑核方法或神经网络。做项目最忌“杀鸡用牛刀”,先尝试线性模型,评估其性能后,再考虑复杂度更高的模型,这在工程实践中是必须养成的习惯。

我在第一次做这个项目时,直接上了复杂的集成模型,结果一个决策树就轻松达到95%以上准确率,完全没必要。后来改用Fisher判别,不仅效果相当,还能天然给出降维后的二维可视化,模型可解释性完全不在一个量级。

2.3 参数和计算过程剖析

具体到数值计算,Fisher判别的核心可以拆成以下几步。

Iris数据集共150条样本,4个特征,3个类别,每类50条。第一步是标准化,这一步在原始Fisher公式中并不显式存在,但强烈建议执行。特别是后续要同时计算S_W和S_B时,特征量纲不统一会导致逆矩阵计算极不稳定,且特征值的大致范围在不同的特征尺度下严重失衡。

第二步是计算各类别均值向量和总体均值向量。类均值很好理解,就是每一类的50条样本在4个特征上的平均值。总体均值则是全部150条样本的平均值。

第三步是计算类内散布矩阵。这里要注意统计里的“自由度”问题,如果你用Python的np.cov计算协方差矩阵,默认除以n-1,而理论公式里通常除以n,这会带来细微数值差异,但不影响特征向量方向,我自己在项目中用n和n-1两种方式算过,分类结果完全一致。

第四步是求解广义特征值。Numpy里可以用np.linalg.eigh直接求解对称矩阵的广义特征值问题,但实际工程中更稳妥的做法是先用SVD分解求转换矩阵,再转成普通特征值问题。原因在于直接求矩阵逆+S_V分解数值上不够稳定,当某个类别的样本数很少时,S_W可能接近奇异。

第五步是选取前K-1个最大特征值对应的特征向量组成投影矩阵,将原始数据降维。

3. 可视化方案整体设计

3.1 可视化要回答哪三个问题

做数据可视化不能无脑画图。在Fisher判别这个项目里,我认为可视化必须回答三个问题。

第一个问题:原始4维空间里,三类数据到底是什么样的分布?这是摸底,用于确认数据是否线性可分。

第二个问题:Fisher判别找到的投影方向到底是什么?它和原始特征的组合关系是什么?这是理解模型的关键所在。

第三个问题:投影后数据的分类效果如何?这是验证模型效果的直观证据。

关于第一个问题,最常用的是散点图和三维散点图。Iris数据只有4个特征,你可以通过组合任意两列来观察分布。我在项目中快速画了6组二维组合图,观察发现花瓣长度和花瓣宽度组合下,山鸢尾的点和另外两类几乎完全分开。

第二个问题的可视化,我会把投影矩阵W中每个特征上的分量进行排序,画出贡献度柱状图。Fisher判别的第一个方向(对应最大特征值)在花瓣长度和花瓣宽度上的负载绝对值最高,这是符合直觉的——这两个特征最能区分品种。

第三个问题是重头戏,把150条样本投影到Fisher判别得到的二维平面上,画出散点图,并按类别着色。这张图基本就是一眼万年:三类数据乖乖地分成三个集群,即使存在少量重叠,也远优于PCA降维的效果。

3.2 为什么Fisher图看起来比PCA图更“分得开”

这里讲一个非常有意思的对比。同样把Iris数据降到二维,PCA和LDA的可视化结果差异能让你直观感受到“有监督”和“无监督”的天壤之别。

PCA只关注数据本身方差最大的方向,它会保留全局数据结构,但不考虑类别。所以PCA之后的散点图上,三类数据仍然混杂在一起,看起来就像三团有一定分离趋势但边界模糊的大杂烩。

LDA则不同。它的优化目标就是在降维的同时保证类别之间尽量分开,因此降维后的二维图上三类簇的自然分离度会好得多。这一点在Iris这种数据上,如果你把两个降维结果拼在一起,差异是肉眼可见的。

有些文章喜欢把LDA吹成“更好的降维方法”,这种说法并不准确。它说的好,仅仅是在“有类别标签且目标是分类”这个任务语义下。如果你要做的是无监督探索,PCA的作用无法替代。两者定位不同,关键看你手里有没有标签、任务目标是什么。

4. 实操过程全记录

4.1 工具选型与数据准备

我用的Python,核心库是numpy、pandas、matplotlib、seaborn、sklearn。如果你只是练习,直接用sklearn里的datasets.load_iris()就可以。

import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns from sklearn.datasets import load_iris from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split iris = load_iris() X = iris.data y = iris.target feature_names = iris.feature_names target_names = iris.target_names print("特征矩阵形状:", X.shape) print("类别分布:", np.bincount(y))

输出内容如下:特征矩阵形状为(150, 4),对应150条样本、4个特征。类别分布是每类50条,没有类别不平衡问题。

需要补充的是,Iris原始数据里的4个特征单位一致(厘米),但不代表可以直接开始计算,因为不同特征的数量级差异会污染散布矩阵。所以在完整流程里必须加一步标准化。这一步用z-score标准化就可以,让每个特征均值为0、方差为1。

4.2 手写Fisher判别:从零到一

先看看手写版的实现方式,便于理解底层原理。不建议一上来就调sklearn,手写一遍能让你彻底看清公式中的每一步在算什么。

第一步,将数据按类别拆分,计算每类的均值向量和全局均值向量。

def fisher_lda(X, y): classes = np.unique(y) K = len(classes) n_features = X.shape[1] # 各类别均值 mean_vectors = [] for cls in classes: mean_vectors.append(np.mean(X[y == cls], axis=0)) # 全局均值 overall_mean = np.mean(X, axis=0) # 类内散布矩阵 S_W S_W = np.zeros((n_features, n_features)) for cls, mv in zip(classes, mean_vectors): class_samples = X[y == cls] S_W += (class_samples - mv).T @ (class_samples - mv) # 类间散布矩阵 S_B S_B = np.zeros((n_features, n_features)) for cls, mv in zip(classes, mean_vectors): n_cls = X[y == cls].shape[0] mean_diff = (mv - overall_mean).reshape(-1, 1) S_B += n_cls * (mean_diff @ mean_diff.T) return S_W, S_B S_W, S_B = fisher_lda(X_scaled, y)

第二步,求解广义特征值问题。在数值计算上推荐先对S_W做SVD或使用伪逆,然后构造矩阵,再做普通特征值分解。

def solve_lda(S_W, S_B): # 对 S_W 求伪逆,确保数值稳定 inv_S_W = np.linalg.pinv(S_W) # 构造矩阵 M = inv_S_W @ S_B # 求特征值和特征向量 eigenvalues, eigenvectors = np.linalg.eig(M) # 按特征值大小排序 idx = np.argsort(eigenvalues)[::-1] eigenvalues = eigenvalues[idx] eigenvectors = eigenvectors[:, idx] return eigenvalues, eigenvectors

这里有个小经验,不要用np.linalg.inv,要用np.linalg.pinv。原因是在标准化后的数据上,S_W的某些特征值会很小,直接求逆在数值上非常危险。即使是Iris这种良性数据集,也依然建议用伪逆,因为散布矩阵本质上是一个秩可能亏欠的矩阵。

顺便说一句,标准化后的特征值会告诉你“哪个方向真的重要”。通常Iris数据得到的广义特征值中,第一个特征值占绝对主导地位,可能达到几十甚至上百,第二个特征值则小得多但依然显著,第三个特征值因为类别数为3只能算到K-1=2个,所以理论上就是零或者数值噪声。如果你观察到第三个特征值莫名很大,说明计算流程存在问题。

第三步,将数据投影到选取的投影方向上。

def project(X, eigenvectors, n_components): W = eigenvectors[:, :n_components] return X @ W, W X_projected, W = project(X_scaled, eigenvectors, n_components=2)

到这步,数据已经被压成二维,每行是一个样本在Fisher判别空间里的坐标。

4.3 交叉验证和分类效果

光降维还不够,我们要在投影后的特征上跑一个分类器来评估效果。Fisher判别本身是一种线性模型,但我们在实际验证中可以在投影后的空间上使用简单分类器。

X_train, X_test, y_train, y_test = train_test_split(X_projected, y, test_size=0.3, random_state=42, stratify=y) from sklearn.linear_model import LogisticRegression clf = LogisticRegression(max_iter=1000) clf.fit(X_train, y_train) accuracy = clf.score(X_test, y_test) print("投影后逻辑回归分类准确率: {:.4f}".format(accuracy))

在Iris数据上,投影到2维后使用逻辑回归的准确率通常在0.95到1.0之间浮动。我跑多次,最高到1.0,最低也有0.93。可以说,4维数据降成2维几乎无损,这就是Fisher判别在Iris数据集上的强大之处。

这里有个值得记录的经验:即使不用逻辑回归,直接使用简单的距离最近邻分类器效果也极好。因为Fisher判定已经天然把不同类别的点在投影空间里聚成了比较紧致的簇,类间间距远大于类内间距,最后补一刀分类器本质上是“锦上添花”。

4.4 可视化代码与图形解读

可视化的重头戏分三块:原始数据的散点图矩阵、降维后的二维分布、判别方向特征负载柱状图。

散点图矩阵用seaborn的pairplot一步到位,按品种着色。从图中可以看到花瓣长度和其他三个特征之间几乎呈线性关系,且山鸢尾与其他两类无论在哪个二维截面都存在明显间隙。变色鸢尾和维吉尼亚鸢尾则在四个特征的二维投影上存在部分重叠。

iris_df = pd.DataFrame(X_scaled, columns=feature_names) iris_df['species'] = target_names[y] sns.pairplot(iris_df, hue='species', diag_kind='hist') plt.show()

投影后的二维图是核心,直接绘制两类散点。因为Fisher判别有2个有效方向,散点图上的横纵坐标就是这两个方向上的投影值。

plt.figure(figsize=(8, 6)) colors = ['#1f77b4', '#ff7f0e', '#2ca02c'] for cls, color in zip(np.unique(y), colors): mask = y == cls plt.scatter(X_projected[mask, 0], X_projected[mask, 1], alpha=0.7, label=target_names[y[mask][0]], color=color) plt.xlabel('LD1') plt.ylabel('LD2') plt.title('Fisher LDA Projection') plt.legend() plt.grid(alpha=0.3) plt.show()

图形上一共有三个明显的簇,其中山鸢尾簇和另外两个簇完全分开,变色鸢尾和维吉尼亚鸢尾的簇也大体分开,仅在边界上有极少数点的交叠。这个可视化让你瞬间明白“线性可分”到底长什么样。

特征负载柱状图反映的是投影向量的每个原始特征成分。LD1的主要正负载集中在花瓣长度和花瓣宽度上,这表示第一个判别方向和这两个特征正相关。洛德2则更多反映花萼宽度和花萼长度的信息。通过这个图,你能将“机器如何做判断”翻译成“植物学语言”:花瓣形态是判别鸢尾品种的首选特征。

5. 常见问题与避坑手册

5.1 相同代码不同结果,问题出在哪

如果你从头手写Fisher判别并在不同机器上运行,会发现特征向量矩阵的某些列符号正好相反,这是广义特征值问题计算结果“符号不定”的特性。这不是错误,因为特征向量v与-v张成同一条直线,投影方向本质上相同。在绘图中你会看到完全镜像的散点分布,但相对位置不变,分类效果不受影响。

另一个常见现象是随机种子不同导致分类准确率小幅波动。如果做交叉验证时数据划分不一样,0.93和1.0之间的浮动都是正常的。但如果你发现准确率跌破0.9,那么大概率是标准化步骤缺失、或S_W/S_B的计算顺序搞错了,建议逐行检查散布矩阵计算。

5.2 散布矩阵到底是除以n还是n-1

这个问题我见过无数次争论。统计口径上,协方差矩阵估计用n-1,散布矩阵定义偏爱直接用n。实际结论是:影响可忽略。关键在于特征向量的方向是由矩阵的比值关系决定的,而不是由标量缩放决定。你和同事用不同口径写代码,得到的分类结果几乎完全一样,不必在这个问题上钻牛角尖。

5.3 为什么我的LDA投影之后所有点挤成一团

如果你用sklearn的LinearDiscriminantAnalysis然后直接取transform后的结果,刚学的人经常会看到一个挤成一团的图。这多半是因为用了sklearn的auto_solver时,它默认是“收缩”过的。解决办法是显式设置solver为svd或eigen。另一个可能性是特征标准化没做,导致某些特征比例失衡压过了其他特征。

5.4 绝大多数人容易忽略的核心教训

对于准备上手做这个项目的朋友,最需要领会的不是公式,而是“先看数据长什么样,再决定方法上不上”这句经验。我做过的很多非学术实际项目里,看懂了数据的形态,后面分析就顺了;一旦偷懒不看数据,直接上模型,出来的结果常常让人抓头发。

Iris这个数据集还算简单,哪怕你从4维直接降到1维,用投影直方图也能看到三类分布有明显的间隔。但这不代表所有数据集都这么友好。我强烈建议你在完成Fisher判别之后,再找一份真实的多分类数据集练一遍,对比LDA与PCA的降维效果,你才能真正体会“监督信息”在降维中的作用。

6. 扩展思路与后续进阶方向

Fisher判别项目做完之后,不应该停在“我画了一张好看的图”这个成就感上。我建议至少做三个方向的延伸,收益会更大。

第一个方向是核Fisher判别,把原始特征映射到高维空间再做线性判别,这样能处理线性不可分的数据集。第二个方向是在Fisher判别得到投影后,尝试不同的分类器组合,例如决策树、随机森林、或支持向量机,观察“降维+弱分类器”和“不降维+强分类器”之间的性能权衡。第三个方向是尝试用Fisher判别做特征选择,对比投影矩阵中的特征负载,找出冗余特征并删除之。

这些都是紧紧围绕“线性判别+数据可视化”这个核心的合理延展,做完成这一整套,你再回头去看其他降维方法,比如t-SNE、UMAP,会发现所有降维工具的本质都是“在某种目标函数下寻找好的观察视角”。Fisher判别是最古老、最直白的那个起点。

我在实际项目中还有一个小习惯,就是每次分析完数据都强制自己用大白话写一段结论,比如“山鸢尾和另外两种鸢尾在花瓣宽度上差异显著,Fisher第一个判别方向主要抓的就是这个”。这样做能倒逼自己真正理解模型到底学到了什么,而不是只满足于跑出一个准确率数字。

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

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

立即咨询