ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

无需计算p(x)的贝叶斯分类器:从生成式到判别式的统一视角

无需计算p(x)的贝叶斯分类器:从生成式到判别式的统一视角 1. 从“没有 p(x)”说起贝叶斯分类器到底在算什么很多人第一次学贝叶斯分类器脑子里都会被一个公式钉死后验概率正比于似然乘以先验也就是 (p(y|x) \propto p(x|y)p(y))。然后紧接着教材就会告诉你分母 (p(x)) 是对所有类别求和或积分得到的证据因子用来做归一化。于是问题来了如果 (p(x)) 根本算不出来、或者算起来代价高得离谱这个分类器还能不能做答案是可以而且在实际工程里我们经常这么干。核心原因在于分类任务的本质是“比较”不是“求绝对概率”。对于一个输入样本 (x)我们只需要知道哪个类别的后验概率最大而不需要知道这个最大后验概率具体等于多少。既然 (p(x)) 对所有类别都是同一个常数它在比较过程中会被约掉。这就是“finding bayes classifier without p(x)”最朴素也最关键的出发点。我先把结论摆在这里贝叶斯分类器可以完全绕开 (p(x)) 的显式计算只要你能对每个类别给出一个可比较的判别分数。这个分数可以是似然乘先验可以是似然比可以是取对数后的线性判别式甚至可以是任意单调变换后的量。真正决定分类边界的是不同类别之间判别分数的相对大小而不是它们的绝对数值。这件事听起来简单但它背后牵扯出一整套从生成式建模到判别式建模的思路转变。很多人学贝叶斯分类器时被 (p(x)) 卡住是因为默认了“必须先把概率密度建出来”。可实际上高斯判别分析、朴素贝叶斯、逻辑回归、甚至线性分类器都可以从这个“去掉 (p(x))”的视角统一理解。下面我会把这条线索完整拆开从数学原理到代码实现再到踩坑经验一层层讲清楚。适合读这篇内容的人有三类一是正在学模式识别、机器学习基础被贝叶斯公式绕晕的学生二是做工程落地发现概率密度估计不稳定、想找更稳方案的同学三是想真正理解“为什么逻辑回归和贝叶斯有关系”的从业者。不管你是哪一类只要跟着把判别分数这条主线走一遍很多之前割裂的知识点会突然连起来。2. 为什么 p(x) 可以不要判别分数的数学逻辑2.1 从后验比较到判别函数贝叶斯决策规则写出来是这样的给定样本 (x)选择使后验概率最大的类别。[ \hat{y} \arg\max_{y} p(y|x) ]用贝叶斯公式展开[ p(y|x) \frac{p(x|y)p(y)}{p(x)} ]其中 (p(x) \sum_{y} p(x|y)p(y))离散情况或积分连续情况。注意这里的关键点(p(x)) 与类别 (y) 无关。对于固定的输入 (x)它就是一个常数。既然 (\arg\max) 只关心哪个 (y) 让表达式最大那么除以一个正数常数不改变结果。所以[ \hat{y} \arg\max_{y} p(x|y)p(y) ]这一步就是“去掉 (p(x))”的全部数学依据。你可以定义一个判别函数 (g_y(x) p(x|y)p(y))然后比较各个类别的 (g_y(x)) 大小即可。(p(x)) 从头到尾没有出现在决策里。我见过不少人在这里犯迷糊觉得“没有 (p(x)) 那概率不就不归一了吗”。对就是不归一。但分类器不需要归一化的概率它需要的是排序。归一化只在你想输出“置信度”或者做阈值决策、代价敏感决策时才需要。纯粹的类别判定排序就够了。2.2 取对数把乘法变成加法实际计算时(p(x|y)p(y)) 这种连乘形式很容易下溢尤其是特征维度高的时候。每个概率都小于 1几十个特征乘起来直接变成 0。所以工程上几乎都会取对数[ \log g_y(x) \log p(x|y) \log p(y) ]对数函数是单调递增的所以 (\arg\max) 的结果不变。这一步不仅解决了数值下溢还把乘法结构变成了加法结构后面推导高斯判别分析时会非常方便。提示取对数后得到的是“对数判别分数”它不再是概率但类别之间的相对大小关系完全保留。不要把它当概率去解释。2.3 判别式视角直接建模类别边界上面走的是生成式路线先建 (p(x|y)) 和 (p(y))再比较。还有一条路是判别式路线直接建模 (p(y|x)) 或者直接建模判别函数 (g_y(x))完全不碰 (p(x))。逻辑回归就是典型例子。二分类下它建模的是[ p(y1|x) \sigma(w^T x b) ]这里根本没有出现 (p(x))也没有出现 (p(x|y))。它直接参数化后验概率。你可能会问这跟贝叶斯有什么关系关系在于当 (p(x|y)) 取高斯分布且协方差相同时生成式推导出来的后验正好是逻辑回归的形式。也就是说逻辑回归可以看作“在高斯假设下绕开 (p(x)) 后的贝叶斯分类器”。这条线索很重要它说明“without p(x)”不只是一个计算技巧而是连接生成式和判别式模型的桥梁。理解了这一点你再看线性判别分析、朴素贝叶斯、逻辑回归就不会觉得它们是三套互不相干的东西。2.4 一个容易忽略的前提去掉 (p(x)) 有一个隐含前提所有类别共享同一个输入空间且 (p(x)) 对每个类别是同一个值。这在标准分类设定下成立。但如果你的问题变成“异常检测”或者“开集识别”情况就不一样了。异常检测里你往往只有一个类别的数据没有“其他类别”的概念这时候 (p(x)) 本身反而成了核心因为你要判断的是“这个样本像不像正常数据”。所以“without p(x)”适用于闭集分类类别集合固定每个测试样本一定属于某个已知类别。这个边界要清楚不然容易把方法用错地方。3. 高斯判别分析一个完整的无 p(x) 推导实例3.1 假设与建模高斯判别分析GDA是展示“without p(x)”最干净的案例。假设每个类别的条件分布是多元高斯[ p(x|yc) \frac{1}{(2\pi)^{d/2}|\Sigma|^{1/2}} \exp\left(-\frac{1}{2}(x-\mu_c)^T\Sigma^{-1}(x-\mu_c)\right) ]先验是 (p(yc)\pi_c)。注意这里我假设所有类别共享同一个协方差矩阵 (\Sigma)这是 GDA 的常见简化版本也是它产生线性边界的原因。判别分数取对数[ \log g_c(x) -\frac{1}{2}(x-\mu_c)^T\Sigma^{-1}(x-\mu_c) \log \pi_c \text{常数} ]那个“常数”包含了 (-\frac{d}{2}\log(2\pi) - \frac{1}{2}\log|\Sigma|)它对所有类别都一样可以直接扔掉。这就是又一次“去掉与类别无关项”的操作和去掉 (p(x)) 是同一个道理。3.2 展开得到线性判别式把二次项展开[ (x-\mu_c)^T\Sigma^{-1}(x-\mu_c) x^T\Sigma^{-1}x - 2\mu_c^T\Sigma^{-1}x \mu_c^T\Sigma^{-1}\mu_c ]其中 (x^T\Sigma^{-1}x) 这一项与类别 (c) 无关可以扔掉。剩下的判别分数变成[ \delta_c(x) \mu_c^T\Sigma^{-1}x - \frac{1}{2}\mu_c^T\Sigma^{-1}\mu_c \log \pi_c ]这是一个关于 (x) 的线性函数。也就是说共享协方差的高斯判别分析最终得到的决策边界是线性的。整个过程我们从来没有计算过 (p(x))甚至连 (p(x|y)) 的归一化常数都没完整保留。3.3 参数估计怎么做实际训练时我们需要估计 (\mu_c)、(\Sigma)、(\pi_c)。用最大似然(\hat{\pi}_c \frac{n_c}{n})就是类别频率。(\hat{\mu}c \frac{1}{n_c}\sum{i:y_ic} x_i)就是类别内均值。(\hat{\Sigma} \frac{1}{n}\sum_c \sum_{i:y_ic} (x_i-\hat{\mu}_c)(x_i-\hat{\mu}_c)^T)注意这里是除以 (n) 而不是 (n-C)不同教材有差异实践中影响不大。这里有个细节值得说协方差矩阵的估计在维度高、样本少时非常不稳定甚至不可逆。这时候要么做正则化加一个小量到对角线要么用对角协方差假设要么先降维。这是 GDA 落地时最常见的坑。3.4 代码实现不碰 p(x) 的 GDA下面是一段可以直接跑的 Python 实现用的是 numpy没有依赖 sklearn 的分类器方便你看清每一步。import numpy as np class GDA: def __init__(self, reg1e-6): self.reg reg # 协方差正则化系数 def fit(self, X, y): self.classes np.unique(y) n, d X.shape self.priors {} self.means {} # 估计均值和先验 for c in self.classes: Xc X[y c] self.priors[c] len(Xc) / n self.means[c] Xc.mean(axis0) # 估计共享协方差 cov np.zeros((d, d)) for c in self.classes: Xc X[y c] diff Xc - self.means[c] cov diff.T diff cov / n # 正则化防止不可逆 cov self.reg * np.eye(d) self.cov_inv np.linalg.inv(cov) return self def decision_scores(self, X): scores {} for c in self.classes: mu self.means[c] # delta_c(x) mu^T Sigma^-1 x - 0.5 mu^T Sigma^-1 mu log pi linear X self.cov_inv mu const -0.5 * mu self.cov_inv mu np.log(self.priors[c]) scores[c] linear const return scores def predict(self, X): scores self.decision_scores(X) # 堆叠后取 argmax score_matrix np.column_stack([scores[c] for c in self.classes]) idx np.argmax(score_matrix, axis1) return self.classes[idx]这段代码里decision_scores返回的就是每个类别的判别分数完全没有 (p(x)) 的影子。你可以拿它和 sklearn 的LinearDiscriminantAnalysis对比在共享协方差设定下结果应该非常接近。注意cov / n这里用的是有偏估计。如果你用n - len(classes)做无偏估计数值上会有细微差别但分类边界基本不变。工程上不必纠结。4. 朴素贝叶斯另一个绕开 p(x) 的经典路径4.1 条件独立假设带来的简化朴素贝叶斯是另一个“without p(x)”的典型。它假设特征在给定类别下条件独立[ p(x|yc) \prod_{j1}^{d} p(x_j|yc) ]判别分数取对数[ \log g_c(x) \sum_{j1}^{d} \log p(x_j|yc) \log p(yc) ]同样没有 (p(x))。这个假设在现实中几乎从来不成立但朴素贝叶斯在很多文本分类、垃圾识别任务上表现意外地好。原因之一就是分类只关心排序条件独立假设虽然让概率估计有偏但排序往往还能保持正确。4.2 离散特征与连续特征的处理差异对于离散特征(p(x_j|yc)) 直接用频率估计配合拉普拉斯平滑[ p(x_jv|yc) \frac{\text{count}(x_jv, yc) \alpha}{\text{count}(yc) \alpha \cdot V} ]其中 (V) 是特征取值个数(\alpha) 通常取 1。对于连续特征常见做法是假设高斯分布估计每个类别下每个特征的均值和方差。这就是 Gaussian Naive Bayes。注意这里的协方差是对角的和 GDA 的共享全协方差不同所以决策边界是二次的每个特征独立贡献一个二次项。4.3 实操中的数值稳定技巧朴素贝叶斯最大的坑是“零概率”和“下溢”。零概率用平滑解决下溢用对数解决。但还有一个更隐蔽的问题当某个特征在某个类别下从未出现平滑后的概率仍然可能非常小取对数后变成很大的负数导致这个类别被彻底否定。这时候可以考虑增大平滑系数 (\alpha)但要小心过度平滑。对特征做分桶把连续值离散化减少稀疏。使用对数域下的加法避免先乘后取对数。我个人的经验是文本分类里 (\alpha1) 通常够用但如果类别极不平衡可以适当调小。连续特征做高斯朴素贝叶斯时一定要检查方差是否为 0否则会除零。加一个极小的 (\epsilon) 到方差上是标准操作。4.4 一个文本分类的最小示例from collections import defaultdict import math class NaiveBayesText: def __init__(self, alpha1.0): self.alpha alpha self.class_prior {} self.word_count {} self.class_total {} self.vocab set() def fit(self, docs, labels): class_doc_count defaultdict(int) for doc, label in zip(docs, labels): class_doc_count[label] 1 self.word_count.setdefault(label, defaultdict(int)) self.class_total.setdefault(label, 0) for word in doc: self.word_count[label][word] 1 self.class_total[label] 1 self.vocab.add(word) n len(docs) for label, cnt in class_doc_count.items(): self.class_prior[label] math.log(cnt / n) def predict(self, doc): scores {} V len(self.vocab) for label in self.class_prior: score self.class_prior[label] total self.class_total[label] for word in doc: count self.word_count[label].get(word, 0) # 拉普拉斯平滑后的对数概率 score math.log((count self.alpha) / (total self.alpha * V)) scores[label] score return max(scores, keyscores.get)这段代码里predict直接比较对数判别分数没有任何归一化。你可以把它用在简单的邮件分类或者评论情感分类上效果通常比想象中好。5. 从生成式到判别式逻辑回归与贝叶斯的隐藏联系5.1 高斯假设下后验的推导前面提到当 (p(x|y)) 是共享协方差的高斯时后验可以写成 sigmoid 形式。我来把这个推导补全因为它是理解“without p(x)”深层含义的关键。二分类下令 (a \log g_1(x) - \log g_0(x))。根据前面的线性判别式[ a (\mu_1 - \mu_0)^T\Sigma^{-1}x - \frac{1}{2}(\mu_1^T\Sigma^{-1}\mu_1 - \mu_0^T\Sigma^{-1}\mu_0) \log\frac{\pi_1}{\pi_0} ]这是一个关于 (x) 的线性函数记作 (w^T x b)。而后验概率[ p(y1|x) \frac{g_1(x)}{g_1(x)g_0(x)} \frac{1}{1\exp(-a)} \sigma(a) ]看到了吗(p(x)) 在分子分母里同时出现被约掉了。最终后验只依赖于线性判别分数 (a)。这就是为什么逻辑回归的形式会自然出现。5.2 判别式训练的优势与代价既然高斯假设能推出逻辑回归那为什么不直接用逻辑回归因为逻辑回归是判别式训练直接优化条件似然不需要假设 (p(x|y)) 的具体形式。当高斯假设不成立时逻辑回归往往更稳健。代价是判别式模型不能生成新样本也不能直接处理缺失值生成式模型可以对缺失特征做边缘化。所以选哪个取决于你的任务。如果你只需要分类判别式通常更省事如果你需要理解数据结构、做异常检测、处理缺失生成式更有优势。5.3 一个统一视角把这两条路放在一起看方法建模对象是否用 p(x)决策边界高斯判别分析p(x|y), p(y)不用线性共享协方差朴素贝叶斯p(x|y), p(y)不用线性或二次逻辑回归p(y|x)不用线性二次判别分析p(x|y), p(y)不用二次各类协方差不同这张表的核心信息是所有这些方法都不需要显式计算 (p(x))。它们的区别在于建模假设和优化目标而不在于是否处理了 (p(x))。6. 实操中的坑与排查清单6.1 数值问题速查问题现象可能原因解决思路所有判别分数都是 -inf概率下溢全程用对数域计算协方差矩阵不可逆维度高、样本少、特征共线加正则项、降维、用对角协方差某类别永远不被预测先验过小或平滑不足检查类别平衡调整平滑系数训练集准确率 100%测试集很差过拟合协方差估计不稳增加正则化减少特征判别分数差距极小特征尺度差异大标准化特征6.2 特征标准化为什么重要高斯判别分析和逻辑回归都对特征尺度敏感。如果一个特征取值范围是 0 到 1另一个是 0 到 10000协方差矩阵的条件数会非常大求逆不稳定。标准做法是每个特征减均值除标准差。注意标准化要用训练集的统计量然后应用到测试集不能各自标准化否则会引入信息泄漏。6.3 先验概率的陷阱先验 (p(y)) 在判别分数里是加性项。如果训练集类别极不平衡比如正类占 1%先验项会让负类天然占优。这在某些场景下是合理的但如果你关心的是召回率可能需要手动调整先验或者用代价敏感方法。我见过有人直接拿不平衡数据训练然后奇怪为什么模型几乎不预测少数类问题就出在先验上。6.4 判别分数不是概率这一点必须反复强调。很多人拿到判别分数后想把它当概率输出比如“这个样本有 80% 概率是正类”。如果你没有做归一化这个解释是错的。要得到概率需要做 softmax 或者 sigmoid 变换而且变换后的数值是否校准还要看模型假设是否成立。实践中如果只是做排序或者阈值决策直接用判别分数就够了不必强行转概率。7. 我个人的一些实操体会我第一次真正理解“without p(x)”是在做一个高维文本分类任务时。当时用高斯判别分析协方差矩阵 5000 乘 5000求逆直接卡死。后来改成对角协方差加正则化判别分数只用了均值和方差训练时间从几十分钟降到几秒准确率只掉了不到一个百分点。那一刻我才意识到(p(x)) 从来不是分类的必要条件它只是生成式建模的一个中间产物。后来做异常检测时我又反过来用到了 (p(x))。因为异常检测没有“其他类别”你必须建模正常数据的分布这时候 (p(x)) 就成了核心。所以我的建议是先想清楚你的任务是不是闭集分类。如果是大胆去掉 (p(x))如果不是老老实实把密度估计做好。还有一个经验是判别分数之间的差距可以作为置信度的粗略代理。差距大说明模型比较确定差距小说明样本落在决策边界附近可能需要人工复核。这个技巧在风控和医疗辅助场景里很实用不需要额外训练校准模型。最后分享一个小技巧如果你不确定该用生成式还是判别式可以先跑一个逻辑回归作为基线再跑一个高斯判别分析或朴素贝叶斯。如果两者性能接近说明数据大致符合高斯或独立假设可以进一步分析如果判别式明显更好说明假设不成立就别在生成式上死磕了。这个对比实验花不了多少时间但能帮你快速定位方向。
返回列表