
1. 从判别分析说起QDA到底是什么最近好几个朋友问我关于二次判别分析QDAQuadratic Discriminant Analysis的问题说看书看了公式但不知道代码怎么写也不知道什么时候该用它。这其实是很多做分类任务的同学都会卡住的地方——判别分析说起来老派但实际业务里特别好用尤其是当你的数据规模不大、特征维度不高、又希望模型能被清晰解释的时候。先说结论QDA是一种生成式分类器它的核心思路是给每个类别分别估计一个高斯分布然后根据贝叶斯规则把新样本判给“概率最大”的那个类。和它的表兄弟线性判别分析LDA最大的区别在于LDA假设所有类别共享同一个协方差矩阵而QDA给每个类别一个独立的协方差矩阵。就因为这个区别QDA的决策边界从“直线超平面”变成了“二次曲线超曲面”所以叫“二次判别分析”。这篇文章基于我真实项目和教学中的踩坑经历把QDA从原理到Python实现完整拆一遍包括scikit-learn的代码、参数选择、模型评估以及那些文档里不会告诉你的坑。适合刚接触判别分析、或者用LDA效果不好想试试QDA的读者。2. 核心原理与模型假设2.1 QDA的数学逻辑为什么每个类别要有自己的协方差矩阵QDA假设每个类别k的样本都服从一个多元高斯分布即x | y k ~ N(μ_k, Σ_k)其中μ_k是该类别的均值向量Σ_k是该类别的协方差矩阵。这个假设意味着对于每一个类别数据点围绕该类别的中心呈现“椭球形”分布。每个类别有自己独立的椭球形状和朝向这正是QDA和LDA最根本的分道扬镳之处。有了这个假设结合贝叶斯定理我们可以推导出判别规则给定一个新样本x我们计算它属于各个类别的后验概率P(y k | x)然后把x判给后验概率最大的那个类别。经过对数变换和化简之后QDA得到的是一个关于x的二次型判别函数δ_k(x) -0.5 * ln|Σ_k| - 0.5 * (x - μ_k)^T Σ_k^{-1} (x - μ_k) ln π_k其中π_k是第k类的先验概率。可以看到这个函数里有x的二次项这就是“二次”二字的本源。生活点说LDA是“大家都穿一个尺码的衣服”QDA是“每个班级自己定尺码”。如果各类别数据分布的分散程度差异很大比如一类很聚集、一类很分散那么QDA会更符合真实情况。这也是为什么家庭聚会里QDA往往比LDA更能派上用场。2.2 算法假设条件与使用场景边界QDA并不神秘但它有几个隐藏的假设用的时候一定要心里有数数据近似服从高斯分布如果特征明显偏态或者多峰先用变换比如对数变换、Box-Cox让数据更接近正态。各类别协方差矩阵可逆也就是说每个类别的样本数必须多于特征维度。否则Σ_k是奇异矩阵QDA直接报错。特征之间允许相关协方差矩阵天然会刻画特征间的相关性不像朴素贝叶斯那样要求特征独立。这一点是QDA比朴素贝叶斯更适合同一特征强相关场景的原因。QDA最典型的应用场景是样本量不大几百到几千条、特征维度适中几个到几十个、类别相对清晰但各类别的数据分布形态差异明显。例如在金融领域做信用评分目标变量是“违约/正常”两个类别正常用户的特征分布通常比较“胖”违约用户比较“瘦”这时候QDA常常能比LDA拿到更好的效果。2.3 与LDA、逻辑回归、KNN的核心对比在真正动手写代码之前我强烈建议先把几个常用分类器的区别想明白。这里有一张我反复对学生用的对比表模型决策边界对数据分布假设数据规模要求可解释性QDA二次曲线各类别各自高斯、协方差可不同各类别样本需多于特征数中等可看判别函数LDA线性直线各类别共享协方差矩阵总样本需多于特征数较强可看系数逻辑回归线性直线无强分布假设样本量需求较大最强系数即权重KNN非线性、任意形态无分布假设对样本密度要求高较弱从这个表能看出QDA的位置很独特它比LDA灵活比KNN稳定比逻辑回归更适合小样本。代价是它需要估计的参数更多对数据质量更敏感。所以你用QDA之前必须先诊断一下自己的数据“配不配”。3. Python实现QDA完整的实操流程3.1 环境准备与数据构造我用的是scikit-learn自带的iris鸢尾花数据集来演示。这个数据集够经典、维度刚好也非常适合观察QDA的非线性分类能力。建议先创建一个干净的虚拟环境然后安装以下依赖包pip install numpy pandas scikit-learn matplotlib seaborn然后我们快速加载数据并且把它切成训练集和测试集import numpy as np import pandas as pd from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis # 加载数据 iris load_iris() X pd.DataFrame(iris.data, columnsiris.feature_names) y pd.Series(iris.target_names[iris.target], namespecies) # 这里刻意只保留两个特征方便后面可视化决策边界 X_reduced X[[sepal length (cm), petal width (cm)]] # 划分训练集与测试集 X_train, X_test, y_train, y_test train_test_split( X_reduced, y, test_size0.3, random_state42, stratifyy ) print(f训练集样本数: {X_train.shape[0]}, 测试集样本数: {X_test.shape[0]})代码里的random_state我固定为42确保实验可以复现stratifyy会保持训练集和测试集中的类别比例一致这个在分类任务中是常规操作千万别省略。3.2 第一次跑通QDA模型接下来就是主角登场。在scikit-learn中QDA的调用方式和LDA几乎一致from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis # 初始化QDA模型 qda QuadraticDiscriminantAnalysis() # 训练模型 qda.fit(X_train, y_train) # 在测试集上预测 y_pred qda.predict(X_test) # 查看分类准确率 from sklearn.metrics import accuracy_score, classification_report acc accuracy_score(y_test, y_pred) print(fQDA测试集准确率: {acc:.4f}) print(\n分类报告:) print(classification_report(y_test, y_pred, target_namesiris.target_names))拿这个简化版的数据跑下来QDA的准确率大约在0.9左右具体数值会因随机种子略有浮动。如果你的数据本身类间重叠严重这个准确率会自然下降不要惊慌——这是数据特性决定的不是模型坏了。3.3 可视化QDA的决策边界QDA之所以叫“二次判别”最有说服力的证据就是画出来的决策边界。我用matplotlib把训练好的模型在二维平面上画出来import matplotlib.pyplot as plt import numpy as np def plot_decision_boundary(model, X_data, y_data, titleQDA Decision Boundary): # 生成网格点 x_min, x_max X_data.iloc[:, 0].min() - 0.5, X_data.iloc[:, 0].max() 0.5 y_min, y_max X_data.iloc[:, 1].min() - 0.5, X_data.iloc[:, 1].max() 0.5 xx, yy np.meshgrid( np.linspace(x_min, x_max, 300), np.linspace(y_min, y_max, 300) ) # 预测网格上每个点的类别 Z model.predict(np.c_[xx.ravel(), yy.ravel()]) Z Z.reshape(xx.shape) # 颜色映射需要把字符串类别转成数值 from sklearn.preprocessing import LabelEncoder le LabelEncoder() y_numeric le.fit_transform(y_data) Z_numeric le.transform(Z.ravel()).reshape(Z.shape) plt.contourf(xx, yy, Z_numeric, alpha0.6, cmapplt.cm.RdYlBu) scatter plt.scatter( X_data.iloc[:, 0], X_data.iloc[:, 1], cy_numeric, edgecolorsk, cmapplt.cm.RdYlBu ) plt.xlabel(Sepal Length (cm)) plt.ylabel(Petal Width (cm)) plt.title(title) plt.show() plot_decision_boundary(qda, X_train, y_train, titleQDA Decision Boundary (Training Set))跑完这段代码你会看到决策边界是一条明显的曲线而不是直线。如果换成LDA边界就会变成一条直线。这个可视化对比是最直观地理解“二次”的方法。3.4 用全特征数据再评估一次只拿两个特征是为了画图但实际业务中当然不希望丢信息。我把四个特征全部放进去重新对比一下LDA和QDAfrom sklearn.discriminant_analysis import LinearDiscriminantAnalysis X_full_train, X_full_test, y_train_full, y_test_full train_test_split( X, y, test_size0.3, random_state42, stratifyy ) models { LDA: LinearDiscriminantAnalysis(), QDA: QuadraticDiscriminantAnalysis(), } for name, model in models.items(): model.fit(X_full_train, y_train_full) y_pred_full model.predict(X_full_test) acc_full accuracy_score(y_test_full, y_pred_full) print(f{name} 准确率: {acc_full:.4f})在iris全特征数据上LDA和QDA的表现通常极为接近都接近0.95甚至更高。这个结果非常有教育意义如果两类模型的泛化能力差不多LDA显然更值得优先考虑——它的参数更少、方差更小、对异常值不那么敏感。QDA只有在它显著优于LDA的场景下才值得选。4. QDA关键参数深度解读4.1 priors先验概率怎么设scikit-learn的QuadraticDiscriminantAnalysis支持一个很重要的参数priors。默认取None意思是直接用训练集中各类别占比作为先验概率。这在大多数场景下没问题但如果样本有偏斜就要慎重考虑。比如在医疗筛选中正例比例很低比如1%模型天然会偏向把样本判成负例。这时候你可以手动调整priors# 假设你只有两个类别且真实患病率约为5% qda_adjusted QuadraticDiscriminantAnalysis( priors[0.05, 0.95] )这个调整实质上是在后验概率计算中放大了稀有类别的权重。我的经验是如果训练集中类别比例与真实场景严重不一致一定不要用默认的None否则模型的概率输出会失真。但同时也要提醒你调整priors会影响模型在测试集上的“表面准确率”因为准确率本身就会偏向多数类。最好结合精确率、召回率、ROC-AUC一起看。4.2 reg_param正则化参数怎么用官方文档对reg_param的描述很简短给协方差矩阵的对角线加上一个固定值用于防止矩阵奇异。说得直白点这个参数就是给协方差矩阵“打气”让它不至于“扁”成一条线。当你的某类别样本数小于特征维度时Σ_k的逆就不存在了scikit-learn会直接抛错。此时设置reg_param为正的小值比如0.01或0.1等于在每个对角元素上加上这个值让矩阵重新变回可逆qda_reg QuadraticDiscriminantAnalysis(reg_param0.01)这里有一个关键的实操经验reg_param不是越大越好。过大的正则化会让QDA丧失“各类别协方差不同”的能力因为对角线被统一拉大类别差异被削弱。我一般按网格搜索的方式从0.0001、0.001、0.01、0.1这样逐级尝试配合交叉验证来决定。4.3 store_covariance与概率校准store_covarianceTrue可以让你保存每个类别的协方差矩阵方便检查和解释模型。这在需要向业务方解释“为什么这一类数据长这样”的时候很好用。还有两个方法值得留意# 获取各类别后验概率 probabilities qda.predict_proba(X_test) # 获取对数后验概率数值上更稳定 log_probabilities qda.predict_log_proba(X_test)在我实际项目中predict_proba的输出经常被用来做排序分比如把“属于高风险类别的概率”作为业务处理优先级的依据。QDA给出的概率本质上是高斯分布密度加贝叶斯公式的结果除了数值大小之外还包含密度形状的影响所以不要直接把它看成校准过的置信度。必要时搭配校准工具继续处理。5. 模型评估与对比实验5.1 用交叉验证替代单次切分单次的训练集/测试集切分容易受随机因素影响特别是数据量小的时候一次好结果并不能说明模型真的强。更可靠的做法是用交叉验证from sklearn.model_selection import cross_val_score # 5折交叉验证 cv_scores cross_val_score( QuadraticDiscriminantAnalysis(), X, y, cv5, scoringaccuracy ) print(fQDA 5折交叉验证准确率: {cv_scores.mean():.4f} ± {cv_scores.std():.4f})我还是拿iris全特征来说单次切分时QDA的准确率可能是0.9556但5折交叉验证之后可能是0.9733 ± 0.0305这个“±0.03”的信息量很大——它告诉你模型在不同数据划分下的稳定性。一个模型的均值高但方差也高说明它对数据分布变化很敏感生产环境里很容易“翻车”。5.2 在模拟数据上验证算法特性除了真实数据集我建议你在仿真数据上做一个有说服力的小实验这能帮助你理解QDA的“二次边界”优势。构造两个类别的数据一类用窄的协方差矩阵另一类用很宽的协方差矩阵二者均值接近这时LDA几乎必然失手而QDA能漂亮地分出来np.random.seed(10) # 类别0窄分布 X0 np.random.multivariate_normal([0, 0], [[0.3, 0.05], [0.05, 0.3]], size200) # 类别1宽分布 X1 np.random.multivariate_normal([0.4, 0.4], [[2.0, 0.5], [0.5, 2.0]], size200) X_sim np.vstack([X0, X1]) y_sim np.hstack([np.zeros(200), np.ones(200)]) for name, model in [(LDA, LinearDiscriminantAnalysis()), (QDA, QuadraticDiscriminantAnalysis())]: scores cross_val_score(model, X_sim, y_sim, cv5, scoringaccuracy) print(f{name}: {scores.mean():.4f} ± {scores.std():.4f})这个实验几乎每次都能得出同样的结论QDA在这种“方差不齐”的数据上明显占优。这也是QDA在现代统计学习中依然占有一席之地的根本原因。如果你的数据没这个特点那QDA大概率不会带来太多增益。5.3 高维场景下的降维预处理QDA在高维场景下表现通常不如LDA因为每个类别都要估计一个完整的协方差矩阵参数数量随特征维度呈平方级增长。举例来说100维特征就要估计每个类别约5050个协方差参数小样本根本喂不饱。我的项目经验是在使用QDA之前先做特征筛选或降维。降维推荐主成分分析PCA或线性判别分析LDA作为前置步骤让特征维度降到20以下再跑QDA就比较稳了。如果你想要的只是“概率输出”而模型性能不敏感甚至可以先跑个简单的随机森林选出重要特征再喂给QDA。6. 实际操作中的六个焦点问题6.1 协方差矩阵奇异如何排查这是QDA最常见的报错The covariance matrix for class X is singular. 从字面理解就是该类别的协方差矩阵不可逆。常规解法有三板斧第一检查每个类别的样本数是否大于特征维度。如果某类只有5个样本但特征有10个那就别指望QDA还能正常工作。这是最根本的问题。第二检查数据中是否存在恒定特征。如果某个特征在某类中完全不变它的方差为0协方差矩阵的行列式就是0必然奇异。先处理掉这类特征。第三如果前两步都没问题再考虑用reg_param做正则化qda_safe QuadraticDiscriminantAnalysis(reg_param0.01)我个人的处置顺序是“先查样本数再查冗余特征最后加正则”不要一上来就调参否则掩盖了真实问题。6.2 类别不平衡时QDA表现下降当某一类的样本很少时这个类别的协方差矩阵估计会非常不稳导致模型在少样本类别上出现过拟合。你在测试集上可能看到整体准确率还行但少样本类别的召回率惨不忍睹。处理方式除了前述的调整priors之外还有一个更彻底的做法改用LDA或者带共享协方差的模型变体。如果业务上必须用QDA那就先对少样本类别做上采样复制再加一点小抖动或者收集更多该类的样本。统计模型对数据量真的很诚实不要指望神奇的参数能弥补数据缺失。6.3 数据不服从正态分布时怎么办很多初学者忽视QDA的高斯假设。如果特征直方图明显严重左偏或右偏直接跑QDA虽然不会报错但模型很可能是错的。此时我的建议是对特征做适当的数值变换比如对数变换、平方根变换甚至用Box-Cox变换让分布更接近高斯形态。变换完之后重跑模型通常效果提升明显。再一个容易被忽略的点QDA对异常值极端敏感因为协方差矩阵只要被几个离群点拉动整个椭球形状就会变形。在训练前可以做winsorize处理把极端值压缩到合理分位数范围内或者直接手动剔除明显异常样本。6.4 回归任务里能用QDA吗有一种容易混淆的场景QDA是分类模型它需要一个离散的目标变量比如“通过/失败”、“高/中/低”。如果你的目标变量是连续数值那就不是QDA的菜应该用线性回归、岭回归或随机森林回归之类的回归模型。之前有朋友拿着QDA去做连续值预测跑完发现预测结果全是类别标签显然不符合业务需求。6.5 QDA与模型可解释性如何平衡如果你在金融、医疗等高监管场景做模型可解释性往往是硬需求。QDA的可解释性比LDA差一些因为判别函数带二次项很难把某个特征的贡献单独切出来看。一个折中的做法是用QDA作为预测模型用SHAP等工具对每个样本计算预测值归因把QDA当黑盒解释。另一个做法是如果数据条件允许先用LDA做一个基线再和QDA效果对比若QDA对业务指标提升不显著就守LDA。6.6 QDA在工业落地中的应用最后聊聊落地。在内容推荐系统的用户分群、客户流失预警、线下门店选址评估、气象分类等场景QDA都曾发挥过作用。我的体会是QDA最适合做“小数据、多类别、需要概率输出”的轻量级模型配合统计报表和可视化能比深度学习模型快速上线也更容易向业务方解释。如果数据规模上来了特征维度高了QDA的优势会逐渐消失。这时可以考虑把它作为集成学习的一个基模型放进随机森林或者堆叠框架中让它和其他异质模型互补。这样既保留QDA对分布敏感的优势又规避它在高维场景下的不稳定性。7. 个人经验与建议在做QDA项目的过程中我最大的感受是不要盲目追求复杂模型。如果LDA的交叉验证结果已经足够好QDA提升有限那就选LDA。模型越简单部署成本越低出错的概率越小。QDA的定位应该是“当你知道各类别协方差结构有明显差异或者LDA已经被证明不能分开数据”时才亮出来的武器。另外调参与正则化一定要控制在验证集上。别为了训练集准确率好看而把reg_param设得很极端否则生产环境会给你残酷的教训。最后值得一提的是QDA虽然老但它和现代贝叶斯方法之间的思想是相通的——每个类别有自己的分布、根据后验概率做决策这个框架在不断变化的应用背景中依然成立。每次解决分类问题把它放到候选模型清单里跑一遍交叉验证通常不会让你失望。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。