🧮 第2章 有监督学习
📄 一句话概括

朴素贝叶斯不找边界,它数数: 每类里每个词出现了几次,算成概率,然后把一封新邮件里所有词的概率乘起来,谁大判谁。 「朴素」两个字说的是它假设每个词条件独立——一个明显不成立、但很好用的假设。 实测 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. 把拉普拉斯平滑的 +1 改成 +0.1,三封测试邮件的分数会怎么变?
  2. 给一封只含「会议 附件」两个词的邮件打分,它会判成哪类?为什么?
  3. 如果训练集里垃圾邮件只有 1 封,先验变成多少?分类结果会怎么偏?

常见误区

  1. 把输出的对数分数当成概率。−20.57 不是概率,是 log 后的分数,要转回概率得取 exp 再归一化。 而且这个概率本身也不准——独立假设让它系统性偏高。
  2. 在训练集上评估。朴素贝叶斯的参数就是词频,训练集上的分数必然漂亮。 它尤其容易过拟合小语料,测试集不能省。
  3. 以为它适合所有分类问题。它擅长「特征是离散计数」的场景(文本、事件序列), 连续特征要用高斯朴素贝叶斯,而且特征相关的场景它表现很差。
自测三问(点开对答案) 1. 「朴素」这两个字指的是什么假设?为什么这个假设几乎一定不成立? —— 指「给定类别后,各特征条件独立」,于是联合概率可以拆成连乘。 不成立是因为词与词明显相关(「优惠」和「券」总一起出现),但在分类任务上它往往够用。 2. 如果去掉拉普拉斯平滑,会发生什么? —— 训练集里没出现过的词,条件概率为 0。连乘里出现一个 0,整封邮件的分数就是 0,直接判到对立面。 一个没见过的词就能推翻全部证据。 3. 为什么要取对数?取了对数之后分数还是概率吗? —— 因为概率连乘会下溢到浮点数存不下。取对数后连乘变连加,数值稳定。 取完不再是概率,是「对数概率」,但比大小不变。

2.6 节是「一个模型」。2.7 节开始讲「一群模型」——把很多棵决策树凑到一起投票,这就是随机森林。

相关笔记