线性判别分析LDA原理与Python实战:西瓜数据集分类避坑指南

发布时间:2026/10/10 4:25:33
线性判别分析LDA原理与Python实战:西瓜数据集分类避坑指南
简介LDA.zip是一份面向机器学习初学者与数据挖掘实验者的线性判别分析LDA实战资源围绕西瓜数据集3.0版本演示如何利用Python实现特征降维与二分类判别。资源包含10个文件核心为两个Python脚本及配套CSV数据文件另含编译缓存、项目配置与辅助文件压缩包仅12KB轻量易用。已有585人学习浏览适合正在完成西瓜数据集分类作业、课程设计或想快速上手LDA的读者。通过运行脚本可以直观看到数据预处理、训练集划分、LDA模型拟合、预测及分类报告输出等完整流程还能参考脚本中的注释与自定义函数设计理解类内散度、类间散度与投影方向的关系快速迁移应用到其他线性判别任务中。1. LDA.zip 这个压缩包里到底装了什么西瓜数据集上的线性判别分析LDA.zip 这个压缩包名把 lda、线性判别分析、西瓜 Python 和西瓜数据集凑在一起对应的正是机器学习入门里最经典的一个实验用经典的西瓜数据集17 条记录取密度和含糖率两个连续特征实现线性判别分析LDA判断西瓜是好瓜还是坏瓜。这个组合几乎所有学分类的人都手写过样本量小到每一步中间结果都能肉眼核对特别适合把 LDA 从黑匣子变成白盒。但真做一遍的人会发现一堆课本不讲的坑协方差矩阵求逆报错、好瓜坏瓜数量不均、17 条样本怎么划分都像在碰运气。这篇文章就把 LDA 的判别原理、Python 实现、参数设计和几处翻车点一次讲透新手能照步骤跑通老手能借机重新审视那些想当然的细节。2. 线性判别分析的核心逻辑从投影到最优分类方向线性判别分析的目标用一句话概括找一个投影方向 w把样本都投影到这条直线上让两类点在这条线上分得越开越好。这里的“判别”是监督的它利用了每个样本的类别标签这一点决定了它和 PCA 有本质区别也是理解 LDA 所有后续代码的第一步。2.1 什么叫“判别”先想清楚 LDA 不是 PCAPCA 找的是数据方差最大的方向它不关心类别标签LDA 找的是让两类“中心距离最大、类内散布最小”的方向。用西瓜数据说好瓜和坏瓜在密度—含糖率这个二维平面上的分布总方差最大的方向往往由两类整体散布主导而不是由“两类之间”的差异主导。所以 PCA 的第一主成分方向未必能帮我们区分类别LDA 则完全为分类服务。拿到一份数据先别急着跑代码我一般会做一个小验证把样本按类别标好颜色画散点图目测两个类中心的连线方向再用 NumPy 算一下 PCA 第一主成分方向对比两者夹角。夹角越大说明这个数据集的“最大方差方向”和“最大可分方向”差异越大LDA 越值得用夹角接近零说明类别差异基本沿着数据的总体散布方向用 PCA 降维也能凑合LDA 的优势就不明显。这个小验证不需要任何机器学习库只用中心化和特征分解就能完成是判断“该不该用 LDA”的最快路径。它也能帮新手建立直觉LDA 不追求保留数据的主要信息只追求保留分类需要的信息这是它和 PCA 在目标上的分水岭。2.2 类内散度与类间散度LDA 的数学骨架两个散度矩阵构成 LDA 的核心。类内散度矩阵 Sw Σ_k Σ_{x∈C_k} (x - μ_k)(x - μ_k)^T衡量每个类别内部的样本离自己中心的远近类间散度矩阵 Sb (μ_0 - μ_1)(μ_0 - μ_1)^T衡量两个类中心的差异。注意 Sb 是用外积定义的两个向量的外积结果是一个秩至多为 1 的矩阵这决定了二分类 LDA 的投影方向只有一个。LDA 要最大化目标函数 J(w) (w^T Sb w) / (w^T Sw w)。分子是两类中心投影到 w 上后的距离平方分母是两类样本投影后的合并方差。这个比值是广义瑞利商最大化它的 w 有闭式解w Sw^{-1}(μ_0 - μ_1)。这个公式是整份 LDA.zip 项目里最值钱的一行手写实现时也会落到这一行上。import numpy as np # 用8条样本的迷你数据演示散度矩阵计算方便核对每一步数字 X_demo np.array([ [0.6, 0.1], # 好瓜 [0.7, 0.2], # 好瓜 [0.5, 0.3], # 好瓜 [0.8, 0.4], # 好瓜 [0.1, 0.5], # 坏瓜 [0.2, 0.4], # 坏瓜 [0.3, 0.6], # 坏瓜 [0.0, 0.5], # 坏瓜 ]) y_demo np.array([1, 1, 1, 1, 0, 0, 0, 0]) # 按类别分组计算类内散度矩阵 Sw Sw np.zeros((2, 2)) class_means [] for c in np.unique(y_demo): Xc X_demo[y_demo c] mean_c Xc.mean(axis0) class_means.append(mean_c) Xc_centered Xc - mean_c Sw Xc_centered.T Xc_centered # 每个类别内部做外积累加 # 类间散度矩阵 Sb 只用两个类的中心差决定 mean0, mean1 class_means Sb np.outer(mean0 - mean1, mean0 - mean1) # 闭式解w Sw^{-1}(μ0 - μ1)用 solve 代替显式求逆 w np.linalg.solve(Sw, mean0 - mean1) print(类内散度矩阵 Sw:\n, Sw) print(类间散度矩阵 Sb:\n, Sb) print(投影方向 w:, w)逻辑说明Sw 先按类别各自累加“去中心后的样本外积”再把两类结果加起来Sb 用 np.outer 做外积得到 2x2 矩阵。最后用 np.linalg.solve 求解线性方程组数值上比先 np.linalg.inv(Sw) 再点乘更稳避免显式求逆引入舍入误差。参数说明X_demo 必须是二维数组行是样本、列是特征y_demo 是 0/1 整数标签。换真实西瓜数据集时把这两个变量替换成第 3 章准备的 X 和 y 即可。另外注意投影方向 w 的长度不影响分类结果因为 J(w) 对 w 的缩放是不变的真正起作用的是 w 的方向后续拿 sklearn 对拍时也是比方向而不是比数值。2.3 多分类情况的 LDA降维视角多分类C 类时类内散度 Sw 的定义不变但类间散度要扩展成 Sb Σ_{k1}^{C} N_k (μ_k - μ)(μ_k - μ)^T其中 N_k 是第 k 类样本数μ 是全体样本均值中心。目标变成对 Sw^{-1}Sb 做特征值分解取前 d 个最大特征值对应的特征向量作为投影方向把数据降到一个 d 维子空间。这里有两条实用结论。第一LDA 最多有 C-1 个非零判别方向C 分类最多降到 C-1 维所以二分类的西瓜实验最后得到一条投影轴而不是一个投影平面。第二类别中心算完之后Sw 的规模只由特征维数和样本数决定当某一类只有一条样本时那个类内部的散度矩阵是零矩阵Sw 必然奇异后面的求逆步骤直接崩掉。这两条结论是后文避坑章节的伏笔它们决定了 LDA 可以安全使用的边界。LDA 降维在实际项目里常和可视化搭配多分类先降到两维画散点图看类别分布是否线性可分再决定后面接什么分类器。这种先降维观察、再建模的习惯比一上来就跑黑盒分类器更符合一线排查思路也是这个压缩包项目最值得练的技能点。3. 西瓜数据集的准备把 17 条记录变成能喂给 LDA 的矩阵3.1 数据结构哪些特征能用哪些要编码西瓜数据集共有 17 条记录每条记录包括编号、色泽、根蒂、敲声、纹理、脐部、触感、密度、含糖率、好瓜。按类型分前几个是类别型特征后两个是数值型连续特征“好瓜”是二分类标签。LDA 的输入必须是数值矩阵所以第一个决策是哪些字段能直接送进去哪些需要编码。特征类型取值示例是否直接进入 LDA色泽类别青绿 / 乌黑 / 浅白否需编码根蒂类别蜷缩 / 稍蜷 / 硬挺否需编码敲声类别浊响 / 沉闷 / 清脆否需编码纹理类别清晰 / 稍糊 / 模糊否需编码脐部类别凹陷 / 稍凹 / 平坦否需编码触感类别硬滑 / 软粘否需编码密度数值约 0.24~0.80是含糖率数值约 0.05~0.40是好瓜标签是 / 否标签转为 0/1类别特征不能直接按 1、2、3 编码因为那等于强加顺序关系比如“青绿1、乌黑2、浅白3”会暗示浅白比青绿“更大”而实际上色泽没有大小之分。常见做法是只用密度和含糖率两个连续特征跑 LDA这也是标题里“西瓜 python”这类项目最常见的实现方式。另一个可选做法是把类别特征独热编码后拼进特征矩阵但独热产生的 0/1 指示变量和密度这种带尺度的连续特征混在一起后LDA 的判别方向系数很难解释且会制造大量零值让散度矩阵稀疏。我的选择习惯是教学和复现目的直接用连续特征。如果真的要把色泽、根蒂也放进来就先独热编码再对连续特征做标准化最后在交叉验证里对比“加了离散特征后验证集准确率有没有提升”没有提升就果断去掉。这样选特征有依据不是拍脑袋。3.2 读取数据与特征工程的最小代码数据文件通常是 CSV 或 Excel 格式。读取后要做的只有三件事把“好瓜”列转成 0/1 标签、抽取密度和含糖率两列组成特征矩阵、确认类别样本数。代码很短但每一行背后都有值得注意的参数。import pandas as pd import numpy as np # 读取 CSV 数据文件文件需放在脚本同目录 df pd.read_csv(watermelon.csv) # 把好瓜列转成 0/1 标签是1否0 df[label] (df[好瓜] 是).astype(int) # 只取两列连续特征进入 LDA其余类别特征本例不使用 X df[[密度, 含糖率]].values y df[label].values print(样本数:, X.shape[0], 特征维度:, X.shape[1]) print(好瓜数量:, y.sum(), 坏瓜数量:, (y 0).sum())逻辑说明df[好瓜] 是 得到一个布尔序列astype(int) 把它转成 0/1df[[密度, 含糖率]] 用了双括号表示按列名取多列得到 DataFrame再 .values 转成 NumPy 二维数组行是样本、列是特征。参数说明如果数据是 Excel 文件把 pd.read_csv 换成 pd.read_excel(watermelon.xlsx)需要先装好 openpyxl 库。列名必须和表头严格一致中文列名最容易被复制出来的空格坑到建议打印 df.columns 确认后再写列名。最后的类别数量输出是后续判断类别不平衡的重要信号好瓜 10 条、坏瓜 7 条比例还不算极端但已经足够让中点阈值产生偏差后面会专门讲。3.3 划分训练集与测试集小样本下的划分策略17 条样本做单次划分最大的问题是测试集太小。常见错法是直接 df.sample(frac0.7) 乱切不设随机种子也不分层结果测试集可能只有一两条坏瓜准确率波动极大。正确做法是分层划分保证训练集和测试集里好瓜坏瓜比例基本一致。from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, # 17 条里约 5 条做测试 stratifyy, # 分层训练/测试里好瓜坏瓜比例一致 random_state42 # 固定种子保证实验可复现 ) print(训练集好瓜数:, y_train.sum(), 测试集好瓜数:, y_test.sum())逻辑说明stratifyy 让划分时按标签比例抽样避免小样本下某一类全部跑到测试集random_state42 只是让同一份代码在不同机器上拿到相同划分它不会让结果变好只会让结果可复现。但单次划分仍是“抽一次彩票”。对于 17 条数据我一般不用单次划分做最终评估而是用留一法Leave-One-Out做整体验证每次用 16 条训练、1 条测试循环 17 次把结果平均。LDA 训练一次是微秒级17 次完全可接受换来的是几乎不含随机性的评估结果。留一法的代码骨架先放在这里第 4 章会用真实训练代码把它补全。from sklearn.model_selection import LeaveOneOut loo LeaveOneOut() for train_idx, test_idx in loo.split(X): X_train_loo X[train_idx] y_train_loo y[train_idx] X_test_loo X[test_idx] y_test_loo y[test_idx] # 下一章在这里补上训练 LDA - 预测 - 记录结果逻辑说明loo.split(X) 每次返回一组索引train_idx 是 16 条训练样本的下标test_idx 是那 1 条测试样本的下标。留一法对 17 条样本几乎不引入随机性是判断“这个小模型到底有没有学到信号”的最可靠手段比单次 train_test_split 靠谱得多。4. 手写LDA训练与预测核心代码和参数调优点4.1 从散度矩阵到投影方向训练过程把第 2 章的散度矩阵计算封装成函数就是手写 LDA 的训练过程。函数输出两个东西投影方向 w 和两个类的中心向量。后者在预测阶段用来算阈值所以不能省。import numpy as np def lda_fit(X, y): 训练线性判别分析模型。 输入: X: 二维数组, 每行一个样本, 每列一个特征 y: 一维数组, 类别标签, 取值为 0 和 1 输出: w: 投影方向向量 class_means: 两个类的中心向量, 形状为 (2, 特征数) classes np.unique(y) # 例如 [0, 1] class_means [] Sw np.zeros((X.shape[1], X.shape[1])) # 类内散度矩阵, 特征 x 特征 for c in classes: X_c X[y c] # 取出第 c 类的全部样本 mu_c X_c.mean(axis0) # 该类中心 class_means.append(mu_c) X_c_centered X_c - mu_c # 去中心 Sw X_c_centered.T X_c_centered # 类内散度累加 mean0, mean1 class_means # 两类中心 Sb np.outer(mean0 - mean1, mean0 - mean1) # 类间散度矩阵 w np.linalg.solve(Sw, mean0 - mean1) # 闭式解 w Sw^{-1}(μ0-μ1) return w, np.array(class_means)逻辑说明Sw 是每个类内部去中心后用 X_c_centered.T X_c_centered 做外积累加Sb 由两个类中心的差做外积得到。最后 np.linalg.solve 直接解 Sw w mean0 - mean1相当于求逆但更稳。参数说明X 必须是二维浮点数组不能是 DataFrame如果 X 是 pandas 的 DataFrame先 .values 转数组再传进来。y 的类别值不一定非要是 0/1字符串类标签也可以但第 4.2 节阈值代码按 0/1 写建议提前统一转成 0/1。如果 np.linalg.solve 报奇异矩阵错误说明 Sw 不满秩原因和处理方案在第 5.1 节。4.2 投影与分类器组合阈值选在哪训练完成后把每个样本投影到 w 上得到一维标量问题就变成了在一维直线上找切分点。最常见的阈值是两类投影中心的算术平均代码里先演示这个最直观的做法它的缺陷在第 5.3 节再展开。from sklearn.model_selection import train_test_split # 继续使用第 3 章的 X, y 做一次示例划分 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, stratifyy, random_state42 ) # 训练 LDA w, class_means lda_fit(X_train, y_train) mean0, mean1 class_means # 投影到一维 proj_train X_train w proj_test X_test w # 训练集两类投影中心取平均作为阈值 proj_center0 w mean0 proj_center1 w mean1 threshold (proj_center0 proj_center1) / 2 # 分类结果 y_pred (proj_test threshold).astype(int) acc (y_pred y_test).mean() print(测试集准确率:, acc) print(投影中心0: %.4f 投影中心1: %.4f 阈值: %.4f % (proj_center0, proj_center1, threshold))逻辑说明mean0 和 mean1 是训练阶段算出的类中心在预测阶段它们只是常数向量。w mean0 是第 0 类中心在投影方向上的位置阈值取两个位置的中间点。new样本的投影值大于阈值判为第 1 类否则判为第 0 类这里的 0/1 方向和 lda_fit 里 np.unique 的排序一致。参数说明这个阈值取法等权对待两个类隐含假设两类先验概率相等。好瓜 10 条、坏瓜 7 条时偏差不大还能用但如果两类数量悬殊要继续往下看第 5.3 节调整阈值。另外如果 w[0] 正好是 0比如密度特征完全不参与判别用 w mean0 计算不会出问题但第 4.3 节对拍时做归一化要用 w[1] 当基准。4.3 与标准库结果对拍验证自己的实现手写实现最怕方向向量算错而不自知。验证标准做法是拿 sklearn 的 LinearDiscriminantAnalysis 跑同一份数据比较两边的判别方向。注意要比方向而不是比数值因为 w 和 -w 对应同一条决策边界。from sklearn.discriminant_analysis import LinearDiscriminantAnalysis clf LinearDiscriminantAnalysis(solversvd) clf.fit(X_train, y_train) # 各自归一化到第一个分量消除缩放差异 w_norm w / w[0] sk_norm clf.coef_[0] / clf.coef_[0][0] print(手写方向:, w_norm) print(sklearn 方向:, sk_norm)逻辑说明sklearn 的 clf.coef_[0] 是它学到的判别方向系数solversvd 让库内部用奇异值分解处理散度矩阵不显式求逆。两边方向可能完全反号这是正常的因为 w 和 -w 等价。正常误差应该在几个百分点以内如果差得离谱优先回头检查 Sb 里 mean0 - mean1 的减数是不是写反了或者 np.unique 的类别顺序是否符合预期。参数说明solversvd 适合小样本和特征数较多的场景也是 sklearn 的默认改进点如果数据集很大solverlsqr 配合 shrinkage 更灵活。对拍时不要比较准确率——两边分类结果本来就该一致直接比较 w 方向能定位到具体哪一步算错省得在预测代码里瞎猜。5. LDA实战避坑小数据集、奇异矩阵与类别不平衡这一章是血泪经验。LDA.zip 这个标题看起来简单实际跑起来问题一个接一个而且很多问题在课本例子上根本不会出现。下面五条是我在这个项目上反复踩过的坑每条按“现象 → 原因 → 解决”讲清。注意小数据集上的 LDA 最害人的地方是“训练集准确率好看”任何验证手段都必须和训练过程分离否则你优化的是噪声而不是模型。5.1 协方差矩阵求逆报错奇异矩阵的三种解法现象np.linalg.inv(Sw) 报 “LinAlgError: Singular matrix”或者 np.linalg.solve(Sw, mean0 - mean1) 报 “Matrix is singular”。原因Sw 不满秩。常见来源有三个一是特征维度大于样本数比如特征选择后剩 50 维但每类只有 20 条样本二是两个特征几乎线性相关典型例子是同时保留“含糖率”和“含糖率*2”这类冗余列三是某一类样本只有一条那个类内部的散度矩阵是零矩阵Sw 整体退化。解决按顺序先查相关矩阵删除相关性超过 0.98 的冗余列如果还不满秩给 Sw 加一个小对角矩阵 λIλ 从 1e-4 起步逐步增大这是最常用的正则化手段最后实在不行再用 np.linalg.pinv 求伪逆兜底。在 sklearn 里直接设置 solverlsqr, shrinkageauto 让库自动估计收缩量更省事但手写实现时最好把 λ 暴露成函数参数方便对比不同取值的效果。注意不要直接 Sw_inv np.linalg.inv(Sw) 一把梭。先用 np.linalg.matrix_rank(Sw) 检查秩如果秩小于特征维度先处理数据再求逆。5.2 标准化该不该做尺度、条件数与正则化现象同一份数据先 StandardScaler 再 LDA结果和直接用原数据跑不一样有时候更准有时候反而变差看起来像玄学。原因理论上 LDA 对特征的可逆线性变换是等变的标准化不应该改变分类边界。但数值实现不跟理论完全同步两个特征尺度差到三个数量级以上时Sw 的条件数会很大np.linalg.solve 的舍入误差被放大。更关键的是一旦给 Sw 加正则化项 λIλ 对每个维度加的量是相同的对小尺度特征的实际惩罚远大于大尺度特征标准化在这里直接改变了正则化的语义。解决我的习惯是先算 np.linalg.cond(Sw) 看条件数超过 1e12 就先标准化再进 LDA。只要涉及 shrinkage 正则化一律先做 z-score 标准化再调 λ否则调出来的最优 λ 在不同尺度下毫无可比性。这个顺序是踩过坑才固定的先标准化再调参比先调参再标准化稳定得多。5.3 好瓜多坏瓜少类别不平衡让阈值失效现象阈值取在两个投影中心的中点坏瓜几乎全部被预测成好瓜分错的集中在少数类。原因中点阈值隐含两类先验概率相等的假设。西瓜数据好瓜 10 条、坏瓜 7 条比例不算极端但当某一类样本数只有另一类的五分之一时贝叶斯最优分割点会明显向多数类方向移动中点不在最优位置。解决简单修正法按先验加权设阈值 threshold (proj_center0 * n1 proj_center1 * n0) / (n0 n1)其中 n0、n1 是两类样本数。更稳的做法是画出两类投影的直方图在重叠区域选一个能让少数类召回率提高的切点或者用 ROC 曲线找约登指数最大的点。如果调整阈值后少数类还是救不回来先怀疑特征本身可分性不足而不是继续调阈值。5.4 特征多于样本LDA在高维小样本上为什么容易翻车现象特征维度上百每类样本只有二三十条训练集准确率 100%测试集准确率只剩五成看起来像完美拟合噪声。原因Sw 在高维小样本下几乎必然奇异解被伪逆或正则项主导w 的每个分量都被训练集噪声污染同时高维下“两类协方差相同”这个 LDA 前提假设更难成立散度矩阵容易被极值样本带偏。解决先做维度控制把特征压缩到样本数的五分之一以下再进 LDA常见方案是先 PCA 到 20~30 维再做 LDA或者启用收缩估计让 shrinkage 从 0 到 1 扫一遍再不行就换线性 SVM它对高维小样本的鲁棒性通常比 LDA 好。高维小样本没有银弹重点是别把 LDA 当黑匣子直接往里面灌原始高维特征。5.5 用留一法替代简单划分17条样本的评估玄学现象用 train_test_split(test_size0.3) 反复跑每次准确率在四成到九成之间跳来跳去报告的数字完全取决于 random_state 选了什么。原因17 条样本里测试集只有 5 条左右任何一条样本改变归类准确率就跳 20 个百分点一次划分根本测不出模型的真实能力只能测出“这次划得好不好”。解决改用留一法17 次循环训练把 17 条样本轮流当一次测试样本最终的平均准确率方差小得多。代码骨架在 3.3 节已经给出补上训练和预测就是完整评估。如果数据样本上万留一法太慢就改用重复分层 K 折K5 且重复 10 次取平均。评估时还要看混淆矩阵不要只看准确率——小样本下准确率很容易被多数类掩盖坏瓜全错但准确率照样八九成的情况真的会发生。from sklearn.model_selection import LeaveOneOut from sklearn.metrics import accuracy_score preds, truths [], [] for train_idx, test_idx in loo.split(X): w, means lda_fit(X[train_idx], y[train_idx]) thr 0.5 * (w means[0] w means[1]) # 中点阈值 pred (X[test_idx] w thr).astype(int) preds.append(pred[0]) truths.append(y[test_idx][0]) print(留一法准确率:, accuracy_score(truths, preds))逻辑说明每次循环都用 16 条样本重新训练一个 LDA然后用剩下那 1 条测试。means[0]、means[1] 是 lda_fit 返回的两类中心thr 是两类投影中心的中点。pred 是包含一个元素的数组取 pred[0] 存下来。17 次全部结束后用 accuracy_score 汇总。参数说明这段代码直接复用了第 4.1 节的 lda_fit所以它和单次训练共用同一份实现。如果用了不平衡修正阈值把 thr 那行替换成按先验加权的公式即可。留一法结果比单次划分稳定得多是 17 条样本量下最值得信任的评估方式。6. 把LDA用出效果投影可视化、交叉验证与方向向量的解释6.1 用特征值占比决定保留几个判别方向LDA 在多分类场景下先求 Sw^{-1}Sb 的特征值分解特征值大小表示每个判别方向能解释的类间差异占比。西瓜数据是二分类Sb 的秩是 1只会有一个非零特征值占比必然是 100%所以这个技巧在这里更多是验证性质换成三分类以上时它可以用来决定“降维降到几维才够用”。eig_matrix np.linalg.inv(Sw) Sb eigvals, eigvecs np.linalg.eig(eig_matrix) eigvals np.real(eigvals) # 数值误差可能产生虚部取实部 idx np.argsort(eigvals)[::-1] # 按特征值降序排列 eigvals eigvals[idx] ratio eigvals / eigvals.sum() print(特征值:, eigvals, 占比:, ratio)逻辑说明np.linalg.eig 返回的特征值顺序不保证必须用 argsort 排序后再算占比。二分类场景只有一个非零特征值占比数组是 [1.0]说明所有类间信息都在这一个方向上三分类以上如果前两个占比超过 85%就值得投影到两维画图而不是硬塞三维。6.2 投影分布与决策边界一张图看懂分类质量准确率只是数字投影后的分布图能把 LDA 的“好不好看”直接暴露出来。把全体样本投影到 w 上分别画好瓜和坏瓜的直方图再加一条阈值线一眼就能看清两类重叠程度。import matplotlib.pyplot as plt proj_all X w plt.hist(proj_all[y 0], bins5, alpha0.6, label坏瓜) plt.hist(proj_all[y 1], bins5, alpha0.6, label好瓜) plt.axvline(threshold, colorred, linestyle--, label阈值) plt.xlabel(投影值) plt.ylabel(样本数) plt.legend() plt.show()逻辑说明threshold 来自第 4.2 节的计算结果这里直接用。两张直方图的重叠区域越小说明 LDA 在这个数据集上分得越干净阈值线如果明显偏向某一侧就是在提醒你类别不平衡需要修正阈值。这张图比准确率更能说明模型质量也方便在汇报时把结论讲给不懂代码的人。6.3 用交叉验证选正则化参数别让shrinkage成为玄学给 Sw 加正则化 λ 时λ 不是一个可以凭感觉拍的值。对小样本把留一法包在参数搜索外面逐个试 λ取留一法平均准确率最高的值。这是把参数选择从“玄学”变成“可复现实验”的关键一步。from sklearn.discriminant_analysis import LinearDiscriminantAnalysis from sklearn.model_selection import LeaveOneOut, cross_val_score best_acc, best_shrink 0, 0 for shrink in np.logspace(-3, 0, 10): clf LinearDiscriminantAnalysis(solverlsqr, shrinkageshrink) scores cross_val_score(clf, X, y, cvLeaveOneOut()) if scores.mean() best_acc: best_acc scores.mean() best_shrink shrink print(最优 shrinkage:, best_shrink, 留一法准确率:, best_acc)逻辑说明cross_val_score 会自动完成划分、训练、评估cvLeaveOneOut() 指定内部用留一法外层循环在 0.001 到 1 之间按对数均匀取 10 个候选值。只有 solverlsqr 支持 shrinkage 参数用 solversvd 时传 shrinkage 会直接报错。参数说明np.logspace(-3, 0, 10) 表示从 10^-3 到 10^0 取 10 个点覆盖从几乎不加正则到强正则的范围。如果想更细可以把网格加密到 20 个点但 17 条样本下网格过密意义不大留一法本身已经足够稳定差一个位数的 shrinkage 对结果影响通常有限。我在重做这个 LDA 小项目时养成了一个固定顺序先跑通手写版本并和 sklearn 对拍确认方向一致再画投影直方图看阈值是否合理最后才谈准确率而且只用留一法做最终评估。这个顺序让我在换到其他数据集时也能最快定位问题——是先怀疑数据再怀疑实现而不是反过来对着准确率瞎猜。希望帮到你。本文还有配套的精品资源点击获取