朴素贝叶斯垃圾邮件识别:从理论到实现

摘要

本文从理论推导角度,完整解释朴素贝叶斯模型做垃圾邮件识别的可行性,包括:为什么文字需要向量化、贝叶斯公式如何推导出分类规则、"朴素"假设为什么不严格但仍然好用、训练集拆分的细节,以及这套流程是否适用于其他分类任务。


一、问题定义

给定一封邮件,判断它是垃圾邮件(spam)还是正常邮件(ham)。

预期行为:输入一段文字,模型输出一个判断,以及对应的置信度。

输入: "Win FREE money now, click here!"
输出: 垃圾邮件(置信度 97%)

二、为什么需要向量化

模型的本质是数学运算,无法直接处理文字。向量化的作用是把文字转换成数字。

最常用的方式是 词频向量(CountVectorizer):统计训练集里出现过的所有词,构建一个词汇表,然后用每封邮件里各词出现的次数表示这封邮件。

词汇表: [click, free, hello, money, win, ...]
                ↓
"Win FREE money" → [1, 1, 0, 1, 1, ...]
"Hello, how are you" → [0, 0, 1, 0, 0, ...]

每封邮件变成一个数字数组,模型才能进行计算。

关键细节: vectorizer.fit_transform(X_train)vectorizer.transform(X_test) 是两个不同的步骤。

  • fit_transform:在训练集上学习词汇表,同时完成转换
  • transform:用已经学好的词汇表转换新数据,不重新学习

测试集和预测时只能用 transform,否则词汇表不一致,模型就失效了。


三、标签映射

结果同样需要数字化:

df['label_num'] = df['label'].map({'spam': 1, 'ham': 0})

训练集里每封邮件都有正确答案(0 或 1),模型在训练时以此为目标反复调整参数。

理论上,在训练集上准确率应该接近 100%,因为模型见过这些数据,相当于对着答案学习。真正衡量模型好坏的是测试集上的准确率——在没见过的数据上表现如何,才能说明模型真正学到了规律。


四、贝叶斯公式推导

4.1 我们想要什么

给定邮件内容,计算它是垃圾邮件的概率:

P(spam | 'win free money')   ← 这封邮件是垃圾邮件的概率
P(ham  | 'win free money')   ← 这封邮件是正常邮件的概率

比大小,谁大判谁

4.2 贝叶斯公式

直接计算 P(spam | 邮件) 很难,但可以用贝叶斯公式翻转条件:

P(spam | 邮件) = P(邮件 | spam) × P(spam)
                 ─────────────────────────
                        P(邮件)

三个部分分别是:

符号 名称 含义 怎么得到
P(spam) 先验概率 训练集里垃圾邮件占比 直接统计
P(邮件|spam) 似然 假设是垃圾邮件,这些词同时出现的概率 统计各词频率
P(邮件) 证据 这组词在所有邮件里出现的概率 对两类都一样,可以消掉

4.3 消掉分母

比较 spam 和 ham 时,分母 P(邮件) 对两边完全一样,比大小时可以直接消掉:

比较:
P(spam) × P(邮件|spam)   vs   P(ham) × P(邮件|ham)

谁大判谁,结论和除以相同分母后完全一致

五、"朴素"假设

5.1 假设内容

P(邮件|spam) 是多个词同时出现的联合概率,严格计算应该是:

P('win' 且 'free' 且 'money' | spam)
= P('win'|spam) × P('free'|spam, win已出现) × P('money'|spam, win和free已出现)

朴素贝叶斯直接假设每个词独立,无视词与词之间的关联:

P('win free money' | spam) ≈ P('win'|spam) × P('free'|spam) × P('money'|spam)

5.2 假设成不成立

严格来说不成立。 "free"和"money"同时出现的概率,显然高于各自独立出现概率的乘积,它们之间有关联。

5.3 为什么还能用

我们不需要概率值精确,只需要比大小:

只要:
P(spam) × ∏P(词|spam)  >  P(ham) × ∏P(词|ham)

分类结果就是正确的,中间的计算误差不影响最终判断

实验结果也证明了这点:垃圾邮件分类准确率能稳定达到 98% 以上,"朴素"假设够用。

5.4 对数处理防止下溢

实际代码里不直接连乘,而是取对数:

原始连乘(会下溢变成 0):
0.134 × 0.0234 × 0.0456 × 0.0289 × ... = 0.000000000000001

取对数(加法,不会下溢):
log(0.134) + log(0.0234) + log(0.0456) + log(0.0289) + ...
= -2.01 + (-3.75) + (-3.09) + (-3.54) + ...
= -12.39

对数是单调递增函数,两边同时取对数不改变大小关系,结论完全一致。


六、置信度是怎么来的

model.predict_proba() 输出的不是原始贝叶斯后验概率,而是经过归一化的结果:

原始值:
P(spam|邮件) = 0.0000041
P(ham|邮件)  = 0.000000000008

归一化(强制两者加起来等于 1):
置信度(spam) = 0.0000041 / (0.0000041 + 0.000000000008) ≈ 0.9999
置信度(ham)  = 1 - 0.9999 ≈ 0.0001

置信度的含义:在 spam 和 ham 两种可能里,模型更倾向哪一边,以及倾向程度有多强。

注意置信度高不代表模型一定对,只代表模型"很确定"。模型可能非常自信地判断错了,这种情况在训练数据不平衡或者样本很罕见时会出现。


七、训练集拆分

7.1 为什么要拆分

如果用同一批数据训练和评估,模型相当于"对着答案学习再对着答案考试",准确率虚高,无法反映真实能力。

拆分后:

  • 训练集(80%):模型学习规律
  • 测试集(20%):模拟真实场景,评估模型在没见过的数据上的表现

7.2 random_state=42 是什么

X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,
    random_state=42    # ← 这个参数
)

拆分时需要随机打乱数据再切割,random_state 是随机数种子——固定种子,每次运行得到完全相同的拆分结果

  • random_state=42:42 本身没有特殊含义,只是约定俗成的写法(来自《银河系漫游指南》里"宇宙终极答案是 42"的梗)
  • 不设置此参数:每次运行拆分结果不同,准确率数字会变动,难以复现和对比
  • 团队协作时统一 random_state 值,保证大家在相同数据上做实验

7.3 拆分的实际过程

原始数据 5572 条
    ↓ 随机打乱顺序(按 random_state=42 的规则)
    ↓ 前 80% → 训练集(4457 条)
    ↓ 后 20% → 测试集(1115 条)

测试集在整个训练过程中模型完全看不到,只在最后评估时用一次。


八、这套流程是否通用

是的,这是监督学习分类任务的标准流程,换数据就能做不同的事。

原始数据(文字/图片/数字)
    ↓ 特征工程(向量化/归一化/编码)
    ↓ 训练集/测试集拆分
    ↓ 选择模型(朴素贝叶斯/逻辑回归/随机森林/神经网络)
    ↓ 训练(model.fit)
    ↓ 预测(model.predict)
    ↓ 评估(准确率/精确率/召回率)

同一套框架,换数据集能做的事:

换成什么数据 能做什么
电商评论(好评/差评) 情感分析
新闻文章(政治/科技/体育) 文本分类
用户行为日志(流失/留存) 用户流失预测
医疗指标(患病/健康) 疾病风险预测
金融交易记录(欺诈/正常) 欺诈检测

不同任务之间的差别主要在:

  • 特征工程:文本用 CountVectorizer,图片用像素值,表格数据可能需要处理缺失值和类别编码
  • 模型选择:文本分类朴素贝叶斯/SVM 效果好,图像分类用 CNN,表格数据用随机森林/XGBoost

核心流程完全一致。


九、完整推导回顾

目标: 判断 "Win FREE money" 是 spam 还是 ham

Step 1  向量化
"Win FREE money" → [1, 1, 0, 1, 1, 0, ...]

Step 2  计算先验概率(从训练集统计)
P(spam) = 747 / 5572 = 0.134
P(ham)  = 4825 / 5572 = 0.866

Step 3  计算似然(朴素假设,各词独立)
P('win'|spam)  = 0.0234,  P('win'|ham)  = 0.0003
P('free'|spam) = 0.0456,  P('free'|ham) = 0.0001
P('money'|spam)= 0.0289,  P('money'|ham)= 0.0003

Step 4  取对数,计算得分
score(spam) = log(0.134) + log(0.0234) + log(0.0456) + log(0.0289) = -12.8
score(ham)  = log(0.866) + log(0.0003) + log(0.0001) + log(0.0003) = -29.6

Step 5  比大小
-12.8 > -29.6 → spam 得分更高 → 判定为垃圾邮件

Step 6  归一化输出置信度
置信度(spam) ≈ 97%

十、问题

fit_transform 和 transform 的区别

fit 做的事情就是建词汇表,扫描所有训练集文本,把出现过的词收集起来,给每个词分配一个固定的索引位置。

训练集:
  "Win FREE money"
  "Hello how are you"
  "Click here FREE prize"

fit 之后建立的词汇表:
{
  'are':    0,
  'click':  1,
  'free':   2,
  'hello':  3,
  'here':   4,
  'how':    5,
  'money':  6,
  'prize':  7,
  'win':    8,
  'you':    9
}
transform 做的是按已有词汇表把文本转成向量,不修改词汇表。

所以:

fit_transform:先建词汇表,再转换(只能在训练集上用)
transform:直接用已有词汇表转换(测试集和新数据用这个)

直接忽略,不报错,不影响其他词。

遇到没见过的词怎么办

比如训练时没有 “lottery” 这个词
predict_spam(“Win FREE lottery money”)

向量化结果:lottery 对应的列根本不存在,直接跳过
只保留词汇表里有的: win, free, money
这是合理的处理方式——模型对没见过的词一无所知,与其猜测不如忽略。代价是信息有损失,但不会崩溃。

词汇表里的词是唯一的,邮件怎么向量化

词汇表里每个词只出现一次,每个词对应向量里的一个位置。

向量的长度 = 词汇表的大小,每个位置存的是这个词在这封邮件里出现的次数。

具体示例:“free free” 的情况

词汇表(简化版,只用5个词):
{'click': 0, 'free': 1, 'hello': 2, 'money': 3, 'win': 4}
 索引位置:     0          1          2           3        4
三封邮件的向量化结果:

                    click  free  hello  money  win
"free free"      →  [ 0,    2,    0,     0,    0 ]  ← free出现2次,索引1位置填2
"win free money" →  [ 0,    1,    0,     1,    1 ]
"hello"          →  [ 0,    0,    1,     0,    0 ]
所以你的理解是对的,free free 对应的那个位置会是 2。

用代码验证一下
python
from sklearn.feature_extraction.text import CountVectorizer

corpus = [
    "Win FREE money",
    "Hello how are you",
    "Click here FREE prize"
]

vectorizer = CountVectorizer()
X = vectorizer.fit_transform(corpus)

# 查看词汇表
print(vectorizer.vocabulary_)
# {'win': 8, 'free': 2, 'money': 6, 'hello': 3, ...}

# 查看矩阵
print(X.toarray())
# 每一行是一封邮件,每一列是一个词的出现次数

# 测试 "free free"
test = vectorizer.transform(["free free"])
print(test.toarray())
# [[0, 2, 0, 0, 0, 0, 0, 0, 0, 0]]
#       ↑ 索引2(free)位置是2

在 Jupyter 里跑一下这段,把词汇表和矩阵都打印出来看,比文字描述直观很多。

参考资料

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐