AI学习-朴素贝叶斯垃圾邮件识别:从理论到实现
朴素贝叶斯垃圾邮件识别:从理论到实现
摘要
本文从理论推导角度,完整解释朴素贝叶斯模型做垃圾邮件识别的可行性,包括:为什么文字需要向量化、贝叶斯公式如何推导出分类规则、"朴素"假设为什么不严格但仍然好用、训练集拆分的细节,以及这套流程是否适用于其他分类任务。
一、问题定义
给定一封邮件,判断它是垃圾邮件(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 里跑一下这段,把词汇表和矩阵都打印出来看,比文字描述直观很多。
参考资料
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)