朴素贝叶斯不找边界,它数数: 每类里每个词出现了几次,算成概率,然后把一封新邮件里所有词的概率乘起来,谁大判谁。 「朴素」两个字说的是它假设每个词条件独立——一个明显不成立、但很好用的假设。 实测 11 封邮件、43 个词,两封测试邮件都判对,第三封两边只差 1.23,模型给的是两个分数, 不是真理。
前面五个算法都在「算距离、找边界」。 朴素贝叶斯换了个完全不同的思路:用概率说话。 它是最早的文本分类方法, 也是「生成式模型」的代表——它先假设每类数据是怎么产生的,再用数据去估这个产生过程的参数。 这一节还会顺手讲清「拉普拉斯平滑」和「取对数」,这两个技巧在别处到处都是。
一、先用一句大白话说清它在算什么
一封邮件来了,要判它是正常还是垃圾。 朴素贝叶斯问的是: 在「这封信是垃圾」的前提下,出现这些词的概率有多大?在「这封信正常」的前提下,又有多大? 哪个大就判哪个。 写成公式就是贝叶斯定理:后验正比于「先验 × 似然」。 先验是「垃圾邮件本来占多少比例」,似然是「垃圾邮件里出现这些词的概率」。 这里有个关键:似然算的是「出现这么一堆词的联合概率」。 要精确算它,得考虑词与词的搭配——「优惠」后面跟「券」的概率,和跟「会议」的概率完全不一样。 词表有 43 个词,两两搭配就有 1800 多种组合,三个词一起更是天文数字。 朴素贝叶斯直接假设它们互不影响,于是联合概率变成各自概率的连乘。 这就是「朴素」两个字的全部含义:不是模型简单,是假设很粗暴。
二、为什么这个粗暴假设还能用
这是一个很自然的问题:假设明明是错的,为什么结果常常对? 原因在于分类只需要比较大小,不需要精确的概率值。 即使每个概率都估偏了, 只要「偏的方向」对两类一致,大小关系还是对的。 教材没有深究这一点,但值得知道:朴素贝叶斯输出的概率几乎一定不准,但它排序往往是对的。 这正是「判别」和「估计」的区别——要判断谁大,不需要知道各自多大。 它还有一个别的算法比不了的优点:样本极少也能用。 逻辑回归要几百个样本才能把系数定稳, 朴素贝叶斯几十个就敢出结果,因为它要估的只是「每个词每类的频率」,数一数就有了。 代价是它对特征之间的关系完全瞎。如果两个词总是一起出现,它会把这份证据算两遍,导致判断过度自信。 这是它最常见的失效方式。
三、拉普拉斯平滑:一个没见过的词不能判死刑
直接数频率有个致命问题。假设「中奖」这个词在 6 封正常邮件里一次都没出现,那 P(中奖 | 正常) = 0。 而连乘里只要有一个 0, 整个乘积就是 0——一封内容很正常的邮件,只因为出现了一个训练集里没见过的词,就被判成垃圾。 解决的办法很土:给每个词的出现次数加 1,给总数加词表大小。 这叫拉普拉斯平滑。 加 1 之后,没出现过的词概率不再是 0,而是一个很小的正数,权重自然就低了。 Demo 里「预算」在正常邮件出现 4 次、垃圾 0 次,平滑后概率是 0.0485 与 0.0115; 「点击」反过来,0.0097 与 0.0690。平滑让「没出现」变成一个弱证据,而不是一票否决。 为什么要加词表大小而不是别的数? 这是有讲究的:加 1 到分子、加 V 到分母,保证所有词的概率加起来仍然等于 1(归一化)。 如果加别的数,概率和就不为 1 了,虽然对分类结果影响不大,但不好解释。
四、取对数:连乘会下溢
一封邮件有 20 个词,每个词的条件概率平均 0.03,连乘起来是 0.03²⁰,大约是 3.5e−31。 再多几个词,浮点数就存不下了(双精度下限约 1e−308)。 解决办法是取对数:连乘变连加,数值从 1e−31 变成 −70 这样的普通数。 而且对数单调, 比大小不变——原来谁的乘积大,现在谁的对数和大。 Demo 里三封邮件的分数都是负数(−20.57、−27.58 这样的),看着不像「概率」, 那是因为它们是对数概率。分数越接近 0 越可信,越负越不可信。 这个技巧在别处也到处都是:语言模型的困惑度、隐马尔可夫的前向算法、神经网络的对数似然损失, 全都在做同一件事——把连乘搬进对数域。
五、动手 Demo:数一数词,然后乘起来
11 封邮件,人工标好 6 封正常、5 封垃圾。 这份语料很小——小到能一眼看完,但足够把朴素贝叶斯的每个环节摆出来。 词表是从这 11 封里挑出来的 43 个词。 真实项目里词表会上万,但流程一模一样:分词、数频、平滑、取对数、加起来比大小。
import numpy as np
vocab = sorted({w for _t, s in DOCS for w in s.split()})
vidx = {w: i for i, w in enumerate(vocab)}
cnt = {u"正常": np.zeros(len(vocab)), u"垃圾": np.zeros(len(vocab))}
for tag, s in DOCS:
for w in s.split():
cnt[tag][vidx[w]] += 1.0
# 拉普拉斯平滑:分子加 1,分母加词表大小
score = {}
for tag in (u"正常", u"垃圾"):
score[tag] = np.log(ndoc[tag] / float(tot))
for w in mail.split():
p = (cnt[tag][vidx[w]] + 1.0) / (cnt[tag].sum() + len(vocab))
score[tag] += np.log(p)
print(score)
实跑输出:
--------------------------------------------------------------------
朴素贝叶斯:数一数词,然后乘起来
--------------------------------------------------------------------
11 封邮件,人工标好 6 封正常、5 封垃圾。词表是从这 11 封里挑出来的 43 个词。
**这就是朴素贝叶斯的全部原料:两堆词频表。**
【一】先验:两类各占多少
类别 邮件数 先验 P(类)
--------------------------------
正常 6 0.5455
垃圾 5 0.4545
【二】条件概率:每个词在每类里出现的频率
词 正常出现 垃圾出现 P(词|正常) P(词|垃圾)
----------------------------------------------------------
预算 4 0 0.0485 0.0115
会议 3 0 0.0388 0.0115
点击 0 5 0.0097 0.0690
中奖 0 3 0.0097 0.0460
免费 0 4 0.0097 0.0575
`+1` 那一项叫**拉普拉斯平滑**。
没有它,垃圾邮件里从没出现过的词,条件概率就是 0,一乘起来整封邮件直接判 0——
**一个没见过的词就能把结论全部推翻。**
【三】一封新邮件来了:算两边的对数分
新邮件里的词 log 正常 log 垃圾 判为 人工标
----------------------------------------------------------------
预算 审批 会议 附件 请 确认 -20.57 -27.58 正常 正常
限时 抢购 点击 领取 现金 大奖-28.41 -18.07 垃圾 垃圾
会议 点击 链接 优惠 预算 报告-24.03 -22.80 垃圾 难分
分数是**对数**,
因为概率连乘会小到浮点数都存不下:二十个 0.01 相乘就是 1e-40。取对数之后连乘变连加,
谁的分数高就判谁。顺带注意最后一封:两边的分数只差一点点,**这种信就该人工看一眼**,
模型给出的不是「真理」,是两个分数。
【四】「朴素」这两个字在说什么
算 P(所有词 | 类) 本该考虑词与词的搭配,那是词表规模的指数级组合。朴素贝叶斯直接假设
**每个词条件独立**,于是联合概率变成连乘——从「算不完」变成「乘一遍」。
这个假设几乎从来不成立(「优惠」和「券」显然不独立),但它的分类结果常常够用。
**这是一个「明显错、但很好用」的模型**,工程上这种取舍很常见:**先要能跑,再要跑得准。**
先验是 0.5455 与 0.4545——正常邮件略多。 这个数在数据不平衡时影响很大:如果垃圾邮件只占 1%,那先验本身就会把大部分邮件推向「正常」, 这时候得靠条件概率把它掰回来。 条件概率那张表最能看出算法的性格。「预算」在正常邮件出现 4 次、垃圾 0 次;「点击」正相反。 它不需要理解这些词的意思,只需要数数。 这也是它在小语料上能用的原因。 平滑前后差多少?如果去掉那个 +1,「预算」在垃圾邮件里的概率就是 0, 任何一封提到「预算」的邮件都会被判成正常——哪怕它写着「预算 抢购 中奖」。 一个词就能锁死整个判断,这是没平滑的典型崩溃方式。 三封测试邮件的分数:第一封 −20.57 对 −27.58,判正常,对;第二封 −28.41 对 −18.07,判垃圾,对; 第三封 −24.03 对 −22.80——两边只差 1.23,判成了垃圾,人工标的是「难分」。 第三封这里藏着这一节最该有的态度:模型给出的不是答案,是两个分数。 分数差得远时可以信, 差得近时就该人工看一眼。 真实系统里通常会给一个「置信度阈值」,低于阈值转人工——这不是模型不行,是把模型的边界用对了。
30 秒实验
- 把拉普拉斯平滑的 +1 改成 +0.1,三封测试邮件的分数会怎么变?
- 给一封只含「会议 附件」两个词的邮件打分,它会判成哪类?为什么?
- 如果训练集里垃圾邮件只有 1 封,先验变成多少?分类结果会怎么偏?
常见误区
- 把输出的对数分数当成概率。−20.57 不是概率,是 log 后的分数,要转回概率得取 exp 再归一化。 而且这个概率本身也不准——独立假设让它系统性偏高。
- 在训练集上评估。朴素贝叶斯的参数就是词频,训练集上的分数必然漂亮。 它尤其容易过拟合小语料,测试集不能省。
- 以为它适合所有分类问题。它擅长「特征是离散计数」的场景(文本、事件序列), 连续特征要用高斯朴素贝叶斯,而且特征相关的场景它表现很差。
自测三问(点开对答案)
1. 「朴素」这两个字指的是什么假设?为什么这个假设几乎一定不成立? —— 指「给定类别后,各特征条件独立」,于是联合概率可以拆成连乘。 不成立是因为词与词明显相关(「优惠」和「券」总一起出现),但在分类任务上它往往够用。 2. 如果去掉拉普拉斯平滑,会发生什么? —— 训练集里没出现过的词,条件概率为 0。连乘里出现一个 0,整封邮件的分数就是 0,直接判到对立面。 一个没见过的词就能推翻全部证据。 3. 为什么要取对数?取了对数之后分数还是概率吗? —— 因为概率连乘会下溢到浮点数存不下。取对数后连乘变连加,数值稳定。 取完不再是概率,是「对数概率」,但比大小不变。2.6 节是「一个模型」。2.7 节开始讲「一群模型」——把很多棵决策树凑到一起投票,这就是随机森林。
- ☐ (本节完,下一节 2.7 算法 7:随机森林)