从几何到代码:Python实战Fisher线性判别分析(以鸢尾花数据集为例)
1. Fisher线性判别分析的几何直觉想象你面前摆着三杯不同品种的鸢尾花花瓣长度和宽度各不相同。现在需要画一条直线让不同品种的花朵尽可能分开同品种的花朵尽可能聚拢——这就是Fisher判别法的核心思想。我第一次接触这个概念时发现它比主成分分析PCA更符合人类分类直觉因为PCA只考虑数据整体分布而Fisher专门针对类别区分优化。类间散度就像不同班级学生身高的差异类内散度则像同一个班级内部的身高波动。数学上我们用矩阵运算来描述这种差异类间散度矩阵 $S_B \sum_{i1}^c N_i(\mu_i - \mu)(\mu_i - \mu)^T$类内散度矩阵 $S_W \sum_{i1}^c \sum_{x\in X_i} (x-\mu_i)(x-\mu_i)^T$其中$\mu_i$是第i类样本的均值向量$\mu$是所有样本的全局均值。在二维情况下最优投影方向就是使$J(w) \frac{w^T S_B w}{w^T S_W w}$最大化的向量$w$。这个比值被称为Fisher准则函数我在实际项目中常用它快速评估特征组合的有效性。2. 鸢尾花数据集的实战准备使用Python处理数据前我们需要理解数据集结构。鸢尾花数据集包含150个样本每个样本有4个特征萼片长宽、花瓣长宽和1个类别标签setosa/versicolor/virginica。我推荐先用pandas快速浏览数据分布import seaborn as sns iris sns.load_dataset(iris) print(iris.describe()) # 可视化特征分布 sns.pairplot(iris, huespecies, markers[o, s, D])运行后会看到花瓣长度petal_length在setosa和其他两类间存在明显分界这正是Fisher方法能捕捉的差异。实际工程中我通常会先做这种探索性分析而不是直接套算法。数据预处理阶段要注意标准化不是必须的因为Fisher基于方差而非距离但若特征量纲差异大如cm和mm混用建议统一缩放from sklearn.preprocessing import StandardScaler X iris.iloc[:, :4].values y iris.species.factorize()[0] X StandardScaler().fit_transform(X)3. 从数学推导到Python实现Fisher判别分析的核心是求解广义特征值问题$(S_W^{-1}S_B)w \lambda w$。对于二分类问题可以直接用闭式解import numpy as np # 计算类均值向量 mean_vectors [] for cls in np.unique(y): mean_vectors.append(np.mean(X[ycls], axis0)) # 构建类内散度矩阵 S_W np.zeros((4,4)) for cls, mv in zip(range(3), mean_vectors): class_scatter np.zeros((4,4)) for row in X[y cls]: row, mv row.reshape(4,1), mv.reshape(4,1) class_scatter (row - mv).dot((row - mv).T) S_W class_scatter # 构建类间散度矩阵 total_mean np.mean(X, axis0).reshape(4,1) S_B np.zeros((4,4)) for i, mean_vec in enumerate(mean_vectors): n X[yi].shape[0] mean_vec mean_vec.reshape(4,1) S_B n * (mean_vec - total_mean).dot((mean_vec - total_mean).T)对于多分类问题如鸢尾花的3类我们需要提取前$c-1$个特征向量c为类别数。这里有个实用技巧用np.linalg.eig求解时记得对特征值排序eigen_vals, eigen_vecs np.linalg.eig(np.linalg.inv(S_W).dot(S_B)) eigen_pairs [(np.abs(eigen_vals[i]), eigen_vecs[:,i]) for i in range(len(eigen_vals))] eigen_pairs sorted(eigen_pairs, keylambda k: k[0], reverseTrue) # 取前两个最大特征值对应的特征向量 W np.hstack((eigen_pairs[0][1].reshape(4,1), eigen_pairs[1][1].reshape(4,1)))4. 可视化与决策边界将高维数据投影到判别向量后我们可以用matplotlib展示分类效果。这里分享一个我常用的可视化技巧——绘制决策边界def plot_decision_regions(X, y, resolution0.02): from matplotlib.colors import ListedColormap markers (s, x, o) colors (red, blue, lightgreen) cmap ListedColormap(colors[:len(np.unique(y))]) # 生成网格点 x1_min, x1_max X[:, 0].min() - 1, X[:, 0].max() 1 x2_min, x2_max X[:, 1].min() - 1, X[:, 1].max() 1 xx1, xx2 np.meshgrid(np.arange(x1_min, x1_max, resolution), np.arange(x2_min, x2_max, resolution)) # 预测每个网格点 Z classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T) Z Z.reshape(xx1.shape) plt.contourf(xx1, xx2, Z, alpha0.4, cmapcmap) plt.xlim(xx1.min(), xx1.max()) plt.ylim(xx2.min(), xx2.max()) # 绘制样本点 for idx, cl in enumerate(np.unique(y)): plt.scatter(xX[y cl, 0], yX[y cl, 1], alpha0.8, colorcmap(idx), markermarkers[idx], labelcl)实际项目中我发现用前两个判别向量通常能保留80%以上的判别信息。可以通过计算累积方差贡献率验证tot sum(eigen_vals.real) discr [(i / tot) for i in sorted(eigen_vals.real, reverseTrue)] cum_discr np.cumsum(discr) plt.bar(range(1,5), discr, alpha0.5, aligncenter, labelindividual discriminability) plt.step(range(1,5), cum_discr, wheremid, labelcumulative discriminability)5. 与逻辑回归的对比实验很多初学者会混淆Fisher判别和逻辑回归。我在教学时喜欢设计对比实验用相同数据训练两种模型比较决策边界差异。from sklearn.linear_model import LogisticRegression # Fisher投影后的数据 X_lda X.dot(W) lr LogisticRegression() lr.fit(X_lda, y) # 原始数据直接逻辑回归 lr_raw LogisticRegression() lr_raw.fit(X[:, :2], y) # 只用前两个特征方便可视化实验结果往往显示FisherLDA的组合在特征线性可分时表现更优当类别边界非线性时逻辑回归更具弹性Fisher方法对特征缩放不敏感而逻辑回归需要标准化6. 工程实践中的注意事项在真实业务场景应用Fisher判别时我总结了几点经验维度灾难当样本数n小于特征数p时$S_W$会奇异。这时需要先用PCA降维或添加正则化项类别不平衡可以通过修改类间散度矩阵的权重来解决weights {0: 1.0, 1: 10.0} # 第二类样本权重放大10倍 S_B sum([weights[i] * n * (mean_vec - total_mean).dot((mean_vec - total_mean).T) for i, mean_vec in enumerate(mean_vectors)])非线性扩展通过核技巧可以实现非线性判别分析Kernel LDA一个完整的工程实现还应包含模型持久化import pickle with open(fisher_model.pkl, wb) as f: pickle.dump({W: W, mean: total_mean}, f)加载模型时需要注意保持特征顺序一致我在项目中曾因特征顺序错乱导致过严重bug。