1. 需要一个比线性边界更灵活的模型时
做分类任务的朋友应该都遇到过这种情况:拿逻辑回归或者线性判别分析(LDA)跑了一版模型,测试集准确率卡在某个数值怎么都上不去。画一下决策边界发现,两类数据之间那条分界线压根不是直线,硬用一个线性边界去切,代价就是样本在边界附近错分一大片。这时候二次判别分析(Quadratic Discriminant Analysis,简称QDA)就可能是个值得试试的方案。
QDA名字里的“二次”指的是它生成的决策边界是二次曲线(在二维特征空间里是抛物线或椭圆弧,在高维空间里是二次曲面),核心假设比LDA放松了一步——允许每个类别拥有自己的协方差矩阵。正是这一步松动,让它能拟合出非线性边界,在处理异方差性数据、流形有明显曲率的分类问题时比LDA更稳。
这篇文章适合两类人看:一是正在学统计机器学习,想把判别分析原理吃透的初学者;二是已经在用scikit-learn做分类,觉得线性模型上限不够、想多掌握一个工具箱的实践者。我不会只丢给你sklearn的API文档,而是把QDA背后的推导逻辑、与LDA的血缘关系、Python实现细节以及调试经验一步步拆开讲,争取你读完就能在真实数据集上用起来。
2. QDA的核心思路拆解:从贝叶斯决策到二次判别函数
2.1 生成模型视角:QDA到底在算什么
先建立一个关键认知:LDA和QDA都是生成式模型,它们在做的不是直接拟合“类别边界”,而是为每一个类别单独建立一个概率分布模型,然后通过贝叶斯公式把新样本归到后验概率最大的那一类。
具体到QDA,它假设每个类别下特征向量服从多元高斯分布:
p(x | y = k) = N(μ_k, Σ_k)注意划重点:每个类别有自己独立的协方差矩阵Σ_k。这是相对LDA的那个“所有类别共享同一个协方差矩阵Σ”的假设而言的。你可能觉得这一下子区别不大,但数学上会产生质变。
根据贝叶斯公式,后验概率:
P(y = k | x) ∝ P(y = k) · p(x | y = k)对两边取对数,展开多元高斯密度函数的指数部分:
-1/2 · (x - μ_k)ᵀ Σ_k⁻¹ (x - μ_k) - 1/2 · ln|Σ_k| + ln π_k在做类别两两比较的判断时,只看这个判别函数值的大小。注意到关键点:(x - μ_k)ᵀ Σ_k⁻¹ (x - μ_k) 这玩意儿是马氏距离的平方,而且因为Σ_k随类别变化,它展开后会出现x的二次项。这就是“二次”之名的数学出处——判别边界由这些二次项的等值线决定,自然就是二次曲面。
用生活化的类比来说:LDA好比给每一类人统一买一件均码外套,不管高矮胖瘦都往一个尺码上套,拟合时用一个共享协方差;QDA则是按各自身形分别量体裁衣,每一类都得到贴合自己的那件。衣服合身了,边界自然能根据各群体分布变化。
2.2 分界面到底长什么样:为什么QDA能出非线性边界
如果把两类数据的判别函数令为相等,得到决策边界方程:
δ_1(x) = δ_2(x)这个等式重新整理后,依然是一个关于x的二次方程。二维情况下,解出来的边界可能是抛物线、椭圆、双曲线中的一种。
很多初学者误以为“二次边界就是想怎么弯就怎么弯”,其实不是。QDA的模型容量有上限:边界只能是二次曲面族中的一种,不能像树模型那样无限拟合。这种“半灵活”的特点在统计上反而是优点——因为它避免了完全非参数方法的过拟合,同时又保留了参数模型的稳定性。我在实践中倾向于把QDA看作:比LDA灵活一个级别、但依然有强结构约束的中间地带的模型。
2.3 判别分析家族的小型对照
| 模型 | 协方差假设 | 边界类型 | 参数估计复杂度 | 训练量需求 |
|---|---|---|---|---|
| LDA | 各类别共享Σ | 线性 | 低 | 低 |
| QDA | 各类别各自Σ_k | 二次曲面 | 中 | 中高 |
| 朴素贝叶斯(高斯) | 各类别、各特征独立 | 线性/复杂 | 低 | 低 |
| KNN | 无分布假设 | 任意 | 训练几乎为零 | 高(预测时高) |
学完LDA的人很容易忘记QDA,但其实两者是一条藤上结出的两个瓜。LDA是QDA的约束版本(把Σ_k全部设成同一个Σ)。从参数效率角度看,LDA因为共享协方差,需要估计的参数大幅减少,在较小数据集上有优势;但代价是不允许各类方差结构不同。
有些实际场景里,你明知道每个类别的数据离散程度差很多。比如医疗诊断里病例组和健康组的代谢指标往往方差就不同;比如金融风控里正常用户的消费波动规律和套现用户完全不是一个量级。这种时候硬压成同一个协方差,等于主动扔掉了信息。
经验:当各类别样本量的量级差距超过3倍,且小类别样本量不足特征维度的5倍以上时,LDA估计出的共享协方差矩阵会被大类主导,小类的真实分布被抹平,优先考虑QDA或降维再做判别分析。
3. 深入QDA的推导细节与判别规则
3.1 判别函数的完整推导过程
我不想跳过推导直接给结论。漏掉推导,你就只能被动接受sklearn的返回结果,遇到边界怪异的情况根本不知道为什么。
从贝叶斯最优判别规则出发,分到第k类的条件是它产生的后验概率最大:
C(x) = argmax_k P(y = k | x)后验概率的分母对所有类别都一样,可以去掉。所以只比较分子——也就是先验和似然的乘积。取对数之后得到判别函数:
δ_k(x) = ln π_k - 1/2 · (x - μ_k)ᵀ Σ_k⁻¹ (x - μ_k) - 1/2 · ln det(Σ_k)其中π_k表示类别k的样本占比,μ_k和Σ_k用极大似然估计:
μ_k = (1/N_k) Σ_{i: y_i = k} x_i Σ_k = (1/(N_k - 1)) Σ_{i: y_i = k} (x_i - μ_k)(x_i - μ_k)ᵀ现在再看判别规则的展开。两类情况,把δ_1(x) > δ_2(x)展开一下,二次项为中心协方差之差的逆矩阵。想一下,如果两类数据的Σ完全一样,二次项直接抵消——这个规则瞬间退回LDA的线性形式。所以QDA对LDA是包含关系,这个细节值得你在代码里实际验证一下。
3.2 正则化与退化问题:为什么小样本下QDA会翻车
讲了QDA的优点,也该讲它过敏的地方。每个类别单独估计Σ_k,这种自由度的成本是你必须拿足够多的样本去填充每一个协方差矩阵。当特征维度p和某个类别的样本量关系失衡(N_k < p + 1),Σ_k就不是满秩矩阵了,逆矩阵都不存在,判别函数直接没法算。
严格讲,N_k可以等于p也能估,但算出来几乎是病态的,泛化能力极差。
scikit-learn的QuadraticDiscriminantAnalysis实现里我看过源码,它有一个reg_param超参数(默认0),本质是对每个Σ_k做正则化:
Σ_k' = (1 - reg_param) · Σ_k + reg_param · I理解它的物理意义:把原始估计出的协方差矩阵朝着单位矩阵方向收缩,抬高特征值下限,让矩阵恢复良态。这等价于在原有估计基础上加上一根“安全绳”。
我在一个开源数据集上做过对比:某类别只有60个样本,特征维度到了27维,不设reg_param时训练集准确率很高但验证集一塌糊涂;把reg_param调到0.1之后,虽然训练集准确率稍微下降了2个百分点,但验证集准确率反而回升了约9个百分点。这是典型的“用小代价换大收益”。
3.3 参数估计流程的实操细节
用sklearn实现QDA只需要几行:
from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.metrics import classification_report # 对特征做标准化,数值稳定性优先 scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) qda = QuadraticDiscriminantAnalysis(reg_param=0.01) qda.fit(X_train_scaled, y_train) y_pred = qda.predict(X_test_scaled) print(classification_report(y_test, y_pred))但我想提醒你,标准化这一步对QDA不是“сенсорно必做”,因为QDA用的是各个类别自身的协方差,单独缩放每个特征会让协方差矩阵的对角线变得相似,反而会削弱QDA对各类别方差结构差异的感知。我倒是建议:如果特征的量级差异太大(数值跨了好几个数量级),优先用对数变换而不是StandardScaler。
此外,sklearn的QDA是基于cholesky分解实现求逆的,特征维度过高时即使有reg_param也可能在数值上不太稳定。所以实际项目中我倾向先对高维稀疏特征做PCA降维,再做QDA,顺序别搞反。
细心的朋友还会发现QDA对象里有决策函数值可以用:
# 拿到每一类的后验概率 prob = qda.predict_proba(X_test_scaled) # 或拿到判别函数值 decision = qda.decision_function(X_test_scaled)predict_proba直接给你每个类别的概率估计,这在工程上特别有用——比如设定置信阈值,只对预测概率超过0.8的样本做自动决策,其余进入人工复核通道。
4. 实操过程:让QDA在真实业务场景中落地
4.1 从数据探索到模型选型的完整决策流
我在做一次客户流失预测项目时,团队一开始走的是逻辑回归基线。数据显示两类用户的重度使用指标差异明显,但画散点图发现边界是一条明显的弧线,而非直线——逻辑回归的线性边界在测试集上表现挣扎。
这时候我做了个简单实验:先把同一批特征分别喂给LDA和QDA,对比交叉验证分数。LDA只有约退了0.72的AUC,换成QDA后提升到了0.81。这是一个很典型的信号——说明两个类别的协方差结构大概率不等。
如果你更谨慎,可以在做选择之前跑一个Box's M-test检验各类协方差矩阵是否相等。但这个检验对非正态很敏感,在样本量大的时候连微小的差异都会判定为显著。所以我的习惯是:业务上先判断,如果各类别方差有明显差异的迹象(可以从各个类别的特征箱线图宽度看出来),直接上QDA截图对比,效果提升就留下,没提升就换回LDA。
4.2 超参数reg_param的调参思路与交叉验证方案
QDA的超参数不多,核心就是reg_param和tol那两个。reg_param我之前说过它收缩Σ_k的对角线,调参逻辑大概这样:
- 如果训练集和测试集差距巨大,说明过拟合,reg_param调大。
- 如果训练集就已经很差,那reg_param要调小或者为0,问题不在这里。
- 如果训练集很好、测试集稍差且数据量本身不够,按0.01、0.05、0.1三个档位网格搜索,通常会落在0.1以下。
一个实用做法是写个简单交叉验证循环:
from sklearn.model_selection import GridSearchCV from sklearn.pipeline import Pipeline pipe = Pipeline([ ('qda', QuadraticDiscriminantAnalysis()) ]) param_grid = {'qda__reg_param': [0.0, 0.01, 0.05, 0.1, 0.2, 0.5]} gs = GridSearchCV(pipe, param_grid, cv=5, scoring='roc_auc') gs.fit(X_train_scaled, y_train) print(gs.best_params_)注意,别盲目追求最高测试集分数,那样容易把阈值定得过于适配当前这一份切分。我在一个二分类数据上跑过:reg_param=0时一个切分下的AUC是0.84,另一个切分只有0.77;reg_param=0.05虽然两个切分都只有0.82/0.80,但差距小得多。稳定性胜于一时的表现。
4.3 多分类场景中的QDA表现与置信区间
multiclass分类场景里QDA不需要任何额外改动,它天然支持K类判别——算每一类的判别函数δ_k,选择最大值对应的类。和多个二分类器投票的“一对其余”策略不同,QDA一次拟合就得到完整模型,推理时算K个判别函数值取最大就行。
在执行阶段,请注意类别不均衡的影响。先验π_k在判别函数里扮演的角色很直接:它给样本量大的类加了一个常数偏置。如果业务里类别比例天然不均衡,这个偏置是合理的;但若是采样导致的比例失真(比如负采样过的场景样本),下次别忘了把priors手动设置成实际业务比例。
qda = QuadraticDiscriminantAnalysis(priors=[0.9, 0.1])同时,在sklearn实现里,默认采用留一分位点剪枝的先验估计,对少量不均衡的类别有轻微拉平效果,这样能避免小样本类的判别函数被严重低估。
4.4 实操中遇到的那些坑与排查方法
坑一:特征相关性高且各类别协方差差别大导致数值爆炸。一个包含两个强相关特征的业务数据,在两个类别里它们的相关性符号恰好相反——QDA把相关结构都刻进去了,看起来很美。可是新来一批数据,那点微弱相关性一波动,预测结果就变脸。对此我建议从业务层面审查相关性是否稳定,不是光看验证集分数。
坑二:训练数据和预测数据分布不一致。QDA的核心是生成式描述,如果生产环境的数据分布已经偏移(covariate shift),所有生成式模型的退化速度都比判别式模型快。应对方案是定期监控特征边缘分布,一旦发现漂移,重训并重估特征集。
坑三:在分布长尾重、离群点多的数据上生用QDA。多元高斯假设对离群点极度敏感。单个离群点可能把某一类的Σ_k对角线拉大数倍,导致真正的判别信息被稀释。我处理这类数据时往往先修剪离群点,再跑到几十次实验对比修剪前后分数。
5. 从实战角度聊聊QDA什么时候用、什么时候别用
5.1 特征维度爆炸与数据量不足时的替代方案
开头说的回归,QDA也不是包治百病。特征维度上去了,数据量却不够,QDA就会像穿了一件大一号的衣服,晃荡得厉害。LDA共享协方差意味着只需要估计一个p×p矩阵;QDA要估计K个,训练量越大要求越低。
如果样本量确实紧张,可以考虑先用PCA把维度压缩到十来个主成分,再上QDA。主成分提取方向和数据方差对齐的部分,那正好是QDA判别关注的方差结构。这样一套流程在几个项目中都帮我挽回了不少训练集里看起来要崩的模型。
另一条路是改成正则化判别分析(RDA),它是LDA和QDA的线性插值版:把各类别协方差矩阵估计向共享协方差压缩,程度由超参数γ控制。RDA在中小样本场景下常常比QDA更稳,只是sklearn没直接给RDA类,需要你用一点代码技巧自己包装。
5.2 QDA与神经网络:不是一个赛道的选手
还有一点要提醒:不要拿QDA和深度学习硬比。神经网络是柔性模型,小微数据上容易过拟合,建模周期长。QDA的参数个数是K×(p(p+1)/2 + p) + K,以十维特征二分类为例,模型参数约120个;但一个小型全连接网络可能在一层就上千参数。数据量没到千级样本,神经网络本身的复杂度就碾压统计可辨识性了。
适合QDA的场景就一个字:稳。样本量几千到小几万、特征十几到几十维、数据近似单峰分布(类内大致团状),此时QDA性价比极高,训练秒级,推理轻量,决策边界又有非线性表达力。
5.3 和PCA结合的一个直觉理解
PCA做的事是找到方差最大的方向线性变换,而QDA在这些变换后的特征上计算判别函数。想象一堆团块数据,方差最大方向正好区分两类。用PCA降维再进QDA,其实是用数据自身结构辅助判别,这比盲目的特征筛选要好用。
但顺序错了就会出事。如果先PCA保留下方差最大的主成分,而恰好这个方向的类间差异很小,等于把判别信息都扔了。万全做法是:PCA + QDA做基线,再和完全不做PCA的QDA对比,留表现好的。
6. 关于QDA应用的经验补充
我在实际操作中还有几个印象比较深的地方,最后一块儿说了。
第一,判别函数与可视化的联动。QDA的predict_proba输出对业务方展示特别友好,比决策树那套if-else输出更容易讲清楚。不用讲公式,对业务人员就说“系统给每个客户打了一个分,分数落到这种弧形区域就判定为流失风险高,这个弧形是从数据里学出来的”。
第二,类先验必须谨慎。有的朋友因为类别比例不平衡,就在训练前做SMOTE过采样或者欠采样。QDA不依赖全局协方差,采样改变的是各类别协方差估计时用的样本量,新样本从哪来会直接影响Σ_k的精度。过采样生成的合成样本会增加小类别协方差的伪证据,反而带偏模型。做类别重平衡时确实慎重点,或者干脆用priors参数表达业务偏移。
第三,把QDA当异常检测器用的变式思路。QDA给出的是后验概率,如果样本落在所有类别的分布尾部,预测概率可能并不低——因为它是相对概率,不是绝对概率。要拿它做异常检测,用的是每个类别下的马氏距离本身。距离超过阈值就判为“不在任何已知分布内”,这一招在一些工业质检项目中我用过,效果比纯阈值还稳定。
第四,和集成方法配合。QDA本身方差大,因为对数据变动敏感,容易被过拟合。放进bagging里当基学习器反而能大幅提升稳定性,这点可能在主流教材里很少提。用BaggingClassifier包QDA跑了一下,在具有一定量噪声的数据集上,AUC提升大概在3到5个百分点之间。思路值得记录:一个高方差低偏置的模型,正好适合bagging去压方差。
最后再分享一个小经验:模型选型时别被“非线性”三个字牵着走。QDA的非线性是有特定形状的,它解决的是“各类协方差不同”的数据,而不是“任意非线性关系”的数据。如果你的数据呈现出明显多模态或长尾形态,数据空间里密的地方有好几团,那QDA就帮不上大忙,该上密度估计或者更灵活的非参数模型。使用前先搞清楚数据长什么样,这比记住一堆公式实际得多。