资讯详情

资讯详情

QDA二次判别分析:从协方差矩阵差异到非线性决策边界

之前在某图像处理Demo里做缺陷样本分类特征只有四个类别只有两类。最初顺手用线性判别分析LDA跑了一版训练集和测试集的准确率卡在82%上下怎么调都上不去。后来把每个类别的协方差矩阵拆开单独估计换成二次判别分析QDA准确率直接跳到94%。差别就出在协方差矩阵这个假设上。QDA全称Quadratic Discriminant Analysis中文一般叫二次判别分析。它和LDA一样属于贝叶斯分类器家族核心都是假设每一类的样本服从多元正态分布只不过QDA允许每个类别拥有自己独立的协方差矩阵由此产生的是二次决策边界。这篇内容想把QDA的原理、公式推导、Python手工实现和scikit-learn调用完整梳理一遍适合刚接触判别分析、或者已经会用LinearDiscriminantAnalysis但没想明白QDA和它到底差在哪的人。1. 先搞清楚QDA和LDA的本质差异为什么多算一次协方差矩阵就能赢1.1 从贝叶斯决策说起QDA到底在算什么判别分析本质上就是把贝叶斯分类器具体化。给定一个样本向量x我们想知道它属于类别k的后验概率P(Yk | Xx) π_k × f_k(x) / Σ_j π_j × f_j(x)其中π_k是类别k的先验概率f_k(x)是类别k中样本x的概率密度。分母对所有类别求和对所有类别都是一样的所以在比较时可以直接忽略。判别分析做的关键假设是每一类的f_k(x)都是多元正态分布。这个假设非常强但好处也显而易见——只要估计出每一类的均值向量和协方差矩阵后验概率就能直接算出来。换句话说整个模型要学的东西就是两类参数均值μ_k和协方差矩阵Σ_k。QDA和LDA的分叉点就在这里。它们对协方差矩阵的处理方式完全不同导致决策边界的形态也完全不同。这是理解QDA的钥匙我会在下一小节详细说明。1.2 LDA与QDA的分水岭共享协方差矩阵的假设LDA假设所有类别共享同一个协方差矩阵也就是Σ_1 Σ_2 ... Σ_K Σ。QDA则放开这个约束允许每类有自己的协方差矩阵Σ_k。这个差异看着只是多了一个下标但对决策边界是本质性的影响。当所有类别共用Σ时对数后验概率做差后x的二次项恰好抵消剩下的是关于x的线性函数决策边界在二维空间是一条直线在高维是一个超平面。这也是线性判别分析这个名字的由来。QDA不再共享Σ后每个类别自己的逆协方差矩阵会留在x的二次项里类与类之间的二次项无法抵消决策边界就变成了二次曲线。二维下是椭圆、双曲线或者抛物线高维下是二次超曲面。这就是二次判别分析的含义。我用一个直观的例子说明这个差别有多关键。假设有两类数据均值都是原点(0, 0)但第一类的协方差矩阵很小样本紧紧聚在原点附近第二类的协方差矩阵很大样本散落在一个大范围里。这两类在均值上没有区别LDA完全分不开因为LDA靠均值差异构造线性边界。但QDA能分因为两个类别协方差矩阵的差异本身就携带了类别信息。内圈、外圈的分布只有二次边界才能表达。1.3 什么时候QDA值得用什么时候是给自己找麻烦QDA不是万能药它比LDA灵活但代价是估计的参数更多。每个类别要估计一个p×p的协方差矩阵总共K个。p是特征数K是类别数自由度大约是K × p(p1)/2。样本量不够时这些参数很容易被噪声带偏出现过拟合。我的经验是以下几种情况值得优先考虑QDA场景特征推荐模型原因各类别方差或相关性结构差异明显QDA协方差差异本身就是分类信息决策边界明显非线性QDA二次边界能覆盖线性边界不能表达的形状每类样本量充足建议至少是特征数的5到10倍QDA参数估计稳定协方差矩阵可逆数据近似多元正态分布QDA模型假设与实际分布匹配后验概率估计准确不适合用QDA的情况也很明确某类样本数小于特征数时协方差矩阵不可逆计算直接崩溃数据本身线性可分时QDA虽然也能用但比LDA更容易过拟合没有必要变量之间存在严重的长尾、多峰分布时多元正态假设会被严重违反QDA的准确率会明显下滑。2. QDA的数学推导把二次判别函数一步步拆开2.1 多元正态分布的密度函数QDA的前提QDA的起点是多元正态分布。p维随机向量x在类别k下的概率密度为f_k(x) 1 / ((2π)^(p/2) × |Σ_k|^(1/2)) × exp(-1/2 × (x - μ_k)^T Σ_k^{-1} (x - μ_k))这里的|Σ_k|是协方差矩阵的行列式(x - μ_k)^T Σ_k^{-1} (x - μ_k)是马氏距离的平方。马氏距离和欧氏距离的区别在于它考虑了特征之间的相关性和每个特征的方差。如果特征之间高度相关这个二次型会把相关性折进去给出的距离才是有意义的。2.2 判别函数的推导去掉常数项后剩下的东西把贝叶斯公式取对数然后丢掉对所有类别都相同的部分就得到QDA的判别函数。后验概率取对数后f_k(x)里的-1/2 × p × log(2π)对每个类别都一样可以扔掉。剩下的判别函数是δ_k(x) -1/2 × log|Σ_k| - 1/2 × (x - μ_k)^T Σ_k^{-1} (x - μ_k) log π_k对任意一个新样本x我们把每个类别k的δ_k(x)都算一遍取最大值对应的类别作为预测结果。关键在于把中间的二次型展开(x - μ_k)^T Σ_k^{-1} (x - μ_k) x^T Σ_k^{-1} x - 2 μ_k^T Σ_k^{-1} x μ_k^T Σ_k^{-1} μ_k可以看到第一项x^T Σ_k^{-1} x是x的二次项它的系数矩阵是Σ_k^{-1}。因为QDA允许每个类别有自己的Σ_k所以当两个类别的δ做差时这个二次项不会抵消δ_k(x) - δ_j(x) -1/2 × log(|Σ_k|/|Σ_j|) 1/2 × x^T(Σ_j^{-1} - Σ_k^{-1})x ...线性项令δ_k(x) δ_j(x)得到的是一个关于x的二次方程。这就是QDA决策边界的完整来源。2.3 决策边界的几何直觉为什么是二次从几何上看δ_k(x) δ_j(x)在二维平面里是一条二次曲线可能是椭圆、双曲线或者抛物线。具体是哪一种取决于两个协方差矩阵之差Σ_j^{-1} - Σ_k^{-1}的特征值和符号。一个最典型的例子是同心圆分布。假设两类数据的均值都在原点第一类的协方差是0.1 × I第二类是4 × I那么Σ_1^{-1} - Σ_2^{-1} (10 - 0.25) × I是对角且相等的矩阵。于是边界方程变成x_1^2 x_2^2 常数这就直接变成了一个圆。LDA在这种情况下毫无办法因为共享协方差矩阵会把两类差异完全平均掉边界只能是一根直线无论怎么摆都无法把内圈和外圈分开。这就是QDA表达能力的核心它能通过协方差矩阵的差异刻画同样是距离原点一个范围但密度不同的分类模式。3. Python手工实现QDA不依赖sklearn的完整过程3.1 手工实现的三个环节参数估计、判别函数、预测理解了公式之后手工实现其实非常直接。一共三步第一步对每个类别k用训练数据估计均值向量μ_k、协方差矩阵Σ_k和先验概率π_k。先验概率一般用类别频率来估计。第二步实现判别函数δ_k(x)也就是前面推导的那个公式。第三步对任意新样本x计算所有类别的δ_k(x)取最大值对应的类别。我在生产环境里其实更喜欢用sklearn的现成实现但手工写一遍的价值在于你可以随时打印中间结果直接看到每个类别的协方差矩阵长什么样这对排查问题有巨大的帮助。下面这个类是完整可运行的版本import numpy as np from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score class QDAHand: def __init__(self, priorsNone): self.priors priors self.classes_ None self.means_ {} self.covariances_ {} def fit(self, X, y): self.classes_ np.unique(y) for c in self.classes_: X_c X[y c] self.means_[c] np.mean(X_c, axis0) # rowvarFalse 表示每一列是一个特征 self.covariances_[c] np.cov(X_c, rowvarFalse) if self.priors is None: n len(y) self.priors {c: np.sum(y c) / n for c in self.classes_} return self def _discriminant(self, x, c): mu self.means_[c] cov self.covariances_[c] diff x - mu # 用伪逆代替求逆避免严格不可逆时直接崩溃 inv np.linalg.pinv(cov) det np.linalg.det(cov) if det 0: det 1e-12 return -0.5 * np.log(det) - 0.5 * diff inv diff np.log(self.priors[c]) def predict(self, X): X np.atleast_2d(X) pred [] for x in X: scores {c: self._discriminant(x, c) for c in self.classes_} pred.append(max(scores, keyscores.get)) return np.array(pred)我在这里用了np.linalg.pinv伪逆而不是np.linalg.inv。原因是当协方差矩阵接近奇异时直接求逆会给出一个数值上极其不稳定的结果伪逆虽然也有风险但至少不会让程序当场崩溃。不过这只是一种兜底手段真正遇到奇异协方差矩阵还是得回到数据层面去解决后面专门讲。3.2 用公开鸢尾花数据验证手工实现验证一个手工实现是否写对了最直接的方式是用一个大家都熟悉的数据集。这里用经典的鸢尾花数据集跑一遍data load_iris() X, y data.data, data.target X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) model QDAHand() model.fit(X_train, y_train) y_pred model.predict(X_test) print(QDA手工实现准确率: {:.3f}.format(accuracy_score(y_test, y_pred)))在我本机跑出来的结果大约是0.978。这个数据集的三个类别在特征空间里大体是线性可分的所以QDA和LDA的表现都在0.97左右差距不大这很正常。但注意一点鸢尾花数据里Setosa这个类别和其他两个类别的协方差结构差异其实不小QDA在这里并没有吃亏。这个案例的意义在于验证实现逻辑正确而不是证明QDA一定优于LDA。3.3 手工实现对理解模型的额外价值手工实现除了用来验证原理还有一个实际的好处你可以直接检查模型学习到的参数。训练完之后打印一下每个类别的协方差矩阵你能立刻发现问题。比如在鸢尾花数据上for c in model.classes_: print(类别, c) print(model.means_[c]) print(model.covariances_[c])你会看到某些类别的方差明显大于另一些类别这就是QDA能发挥作用的信号。如果发现某个类别的协方差矩阵对角线出现极端值那通常意味着该特征在这个类别内部几乎不变或者样本量太少导致估计不稳。这些信息在sklearn的黑盒调用里不容易看到手工实现却一览无余。4. 用scikit-learn跑通QDAAPI使用与决策边界可视化4.1 QuadraticDiscriminantAnalysis的关键参数如果只是想在项目里快速用起来直接调sklearn就够了。sklearn的判别分析模块在sklearn.discriminant_analysis里QDA对应的是QuadraticDiscriminantAnalysis类。它的参数不多但有几个值得认真对待参数作用使用建议priors手动指定各类别先验概率类别不平衡时可以传入自定义值默认按训练集频率估计reg_param协方差矩阵正则化收缩参数取值0到1协方差矩阵奇异时用0.1起步逐步调store_covariance是否保存各类别协方差矩阵需要分析模型时设为True否则保持默认Falsetol判断矩阵是否奇异的阈值默认即可出现奇异报错时再调最常用的是regate这个参数。它的原理是让估计的协方差矩阵向单位矩阵收缩具体来说sklearn内部会把协方差矩阵替换成(1 - reg_param) × Σ_k reg_param × I。当reg_param 1时协方差矩阵变成单位矩阵QDA等价于朴素贝叶斯分类器各特征条件独立且方差相同。实话说这个情形很少用但在协方差矩阵奇异时把reg_param设成0.01到0.1通常就能让模型稳定跑起来。基本调用方式from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis qda QuadraticDiscriminantAnalysis(store_covarianceTrue) qda.fit(X_train, y_train) y_pred qda.predict(X_test) y_proba qda.predict_proba(X_test) # 训练好的参数 print(qda.means_) # 每类均值向量 print(qda.covariance_) # 每类协方差矩阵需要store_covarianceTruepredict_proba输出的概率估计在很多场景下非常有用。它不是简单的类别标签而是后验概率的估计值可以直接对接ROC曲线、业务决策阈值或者作为模型融合时的一个输入特征。4.2 一个能体现QDA优势的模拟数据同心圆二分类为了直观看到QDA和LDA的差别我构造了一个同心圆分布的二分类数据。用sklearn的make_circles生成from sklearn.datasets import make_circles X, y make_circles(n_samples400, noise0.08, factor0.3, random_state42)这里内圈是一类外圈是另一类真实边界是一个圆。线性模型在这类数据上基本束手无策。我用训练集分别训练QDA和LDA然后在测试集上看准确率。结果大致是这样的LDA在测试集上的准确率只有0.55左右几乎和抛硬币差不多QDA能到0.93以上因为没有太多噪声圆形的边界学得相当准这个模拟实验的价值在于它排除了一切干扰单一变量就是决策边界的形状。它清楚地说明了一件事QDA真正强的地方不是数据有多复杂而是二次边界恰好拟合了数据的几何结构。4.3 把决策边界画出来光看数字还不够直观我建议把决策边界直接画出来。这段代码可以直接复现import numpy as np import matplotlib.pyplot as plt from sklearn.discriminant_analysis import LinearDiscriminantAnalysis, QuadraticDiscriminantAnalysis from sklearn.datasets import make_circles X, y make_circles(n_samples400, noise0.08, factor0.3, random_state42) def plot_boundary(ax, model, X, y, title): x_min, x_max X[:, 0].min() - 0.5, X[:, 0].max() 0.5 y_min, y_max X[:, 1].min() - 0.5, X[:, 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) ax.contourf(xx, yy, Z, alpha0.3, cmapplt.cm.RdYlBu) ax.scatter(X[:, 0], X[:, 1], cy, cmapplt.cm.RdYlBu, edgecolork, s20) ax.set_title(title) ax.set_xlabel(x1) ax.set_ylabel(x2) fig, axes plt.subplots(1, 2, figsize(12, 5)) plot_boundary(axes[0], LinearDiscriminantAnalysis().fit(X, y), X, y, LDA: linear boundary) plot_boundary(axes[1], QuadraticDiscriminantAnalysis().fit(X, y), X, y, QDA: quadratic boundary) plt.tight_layout() plt.show()运行之后你会看到左边那张图的边界是一条直线无论怎么摆都只能勉强穿过圆环右边那张图的边界是一个接近正圆的椭圆把内圈和外圈干净利落地分开。一幅图胜过千言万语这张图基本上就是QDA存在的全部理由。5. 实测中容易踩的坑协方差矩阵、样本量和标准化5.1 协方差矩阵奇异样本量比维度还小怎么办QDA应用中最常见的问题是一条报错信息The covariance matrix of class X is singular. 意思是某类样本的协方差矩阵不可逆导致判别函数里的求逆运算无法完成。什么情况下会出现很简单某类样本数小于特征数或者某些特征在该类内部完全不变化方差为0或者特征之间高度线性相关。举个具体的场景某次做用户画像分类特征是20维其中一个小众类别在训练集里只有15个样本。这种情况下样本协方差矩阵秩最多是1420维的矩阵必然奇异。处理方案有三个按优先级排给sklearn的QDA传入reg_param参数比如0.01到0.1让协方差矩阵向单位矩阵收缩保证可逆先用PCA或特征选择把维度降下来让每类的有效样本数大于降维后的特征数如果降维后还是不够不如直接换成LDA它共享协方差矩阵对样本量的需求小得多需要注意的是手工实现里用np.linalg.pinv虽然不会报错但伪逆在奇异矩阵下等价于把某些维度彻底忽略这可能会导致分类结果非常偏。所以我更推荐的正规做法是回到数据层面处理。5.2 标准化该不该做QDA与LDA的不同处境这个问题被问得很多。LDA和QDA内部都用到了协方差矩阵的逆理论上它们对特征的尺度变换有一定自适应性。但实际数值计算中如果某个特征的方差是10000另一个是0.01协方差矩阵的条件数会非常差求逆的结果在浮点数精度下会引入很大误差。标准化的操作本身很简单from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_s scaler.fit_transform(X_train) X_test_s scaler.transform(X_test)我的建议是先不标准化跑一版如果准确率正常且没有数值警告就不用画蛇添足如果出现协方差矩阵奇异或结果不稳定再标准化并重新训练然后用交叉验证对比两个版本哪个更稳。标准化不会改变马氏距离的几何含义但会改变协方差矩阵的条件数这往往是数值稳定性的关键。5.3 类别不平衡与先验概率的设定QDA对类别不平衡的敏感度比LDA高原因是它不仅要估计均值还要估计每个类的协方差矩阵。少数类的协方差估计天然不稳定样本越少协方差矩阵的对角线越容易被低估或高估。处理思路有两个。第一个是直接调整priors参数给少数类更高的先验概率强行让模型更偏向于它第二个是在训练时对少数类做上采样比如用SMOTE之类的算法。我在实际项目里的经验是如果少数类的样本量已经低到连协方差矩阵都稳不住了调整先验只是让模型多猜少数类并不能真正学到结构。这时候还是先想办法拿到更多真实样本更实际。6. 模型评估与多模型横向对比心得6.1 用交叉验证对比QDA、LDA、逻辑回归和KNN无论理论说得多漂亮最终都要回到评估上。我不建议只看一次训练测试划分的结果因为随机性太大。更稳妥的做法是用交叉验证来对比几个基础模型。还是在同心圆数据上跑一遍这个代码from sklearn.model_selection import cross_val_score from sklearn.linear_model import LogisticRegression from sklearn.neighbors import KNeighborsClassifier from sklearn.discriminant_analysis import LinearDiscriminantAnalysis, QuadraticDiscriminantAnalysis models { LDA: LinearDiscriminantAnalysis(), QDA: QuadraticDiscriminantAnalysis(), LogisticRegression: LogisticRegression(max_iter1000), KNN(5): KNeighborsClassifier(n_neighbors5), } for name, model in models.items(): scores cross_val_score(model, X, y, cv5) print(f{name}: {scores.mean():.3f} (/- {scores.std():.3f}))在我本机上输出大致是LDA: 0.55左右QDA: 0.93左右LogisticRegression: 0.55左右KNN: 0.87左右这个结果说明两点。第一在明确非线性的边界下QDA确实能打第二KNN这种非参数模型也能接近QDA但它需要存储全部训练样本预测时也没有概率输出在一些需要解释模型的应用里不如QDA好使。QDA的优势是它既给了二次边界又给了概率估计还能直接把协方差矩阵拿出来解释每个类别在特征空间里的形态。6.2 从混淆矩阵看错误模式交叉验证的准确率只是一个汇总指标真正定位模型痛点要看混淆矩阵。这里以QDA在同心圆数据上为例内圈和外圈的交界处会有少量样本被分错集中在圆环边缘的噪声点上。这符合预期因为噪声就是落在边界两侧的样本无论什么模型都很难完美。在真实项目里我更关心混淆矩阵的不对称性。如果类别A的样本经常被错分成类别B但反过来很少那就说明类别A的协方差矩阵范围比类别B更宽QDA学到的两个椭圆边界严重重叠。看到这种模式我一般会回去检查两个类别在原始特征上的方差确认有没有异常值把协方差矩阵拉大了。6.3 什么时候该回到LDA什么时候该换成其他模型QDA作为baseline非常好用但它在部分场景下会被其他模型取代。我给自己总结了一条经验链如果数据量足够且各类方差结构差异明显先上QDA准没错如果QDA在交叉验证里和LDA差不多说明边界接近线性那就改用LDA参数少、方差小、更稳定如果QDA明显不如KNN或树模型往往说明真实边界不是二次曲线能表达的这时候再去尝试SVM、随机森林或梯度提升模型也来得及。QDA还有一个容易被忽略的优点它的决策函数计算成本极低预测时只是几个矩阵向量乘法不像KNN每次预测都要遍历全部训练数据。在需要低延迟响应的线上推理场景里QDA作为判别式模型里的性价比之王我经常拿它当第一版baseline。最后再分享一个我自己的使用习惯。我拿到一份分类数据不管最终打算用什么模型都会先跑一遍LDA、再跑一遍QDA对比交叉验证结果。如果两者差不多说明数据趋于线性边界如果QDA明显占优说明类别协方差结构里有信息如果LDA反而更好那往往是样本量不足以支撑二次模型我会回头看特征数量是不是太多了。这三步走完之后我对数据的几何结构心里基本就有底了后面选模型、做特征工程都有了参照系。这个习惯保存了很多不必要的折腾你也可以试试。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →