AI学习-朴素贝叶斯垃圾邮件识别:从理论到实现
2026/5/23 16:24:16 网站建设 项目流程

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

摘要

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


一、问题定义

给定一封邮件,判断它是垃圾邮件(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.2random_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 里跑一下这段,把词汇表和矩阵都打印出来看,比文字描述直观很多。

参考资料

  • scikit-learn MultinomialNB 文档
  • scikit-learn CountVectorizer 文档
  • SMS Spam Collection 数据集

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询