简介:一套基于朴素贝叶斯算法的垃圾邮件过滤系统完整源码,面向计算机科学、信息安全、数据科学与大数据技术、人工智能等专业学生,可直接用于毕业设计、课程设计、大作业或初期项目立项。项目以Python实现,涵盖选择训练集训练、控制台实时查看每封邮件判断过程、精确度评估、屏蔽词自定义及邮件号测试等完整流程,提供test-ham与test-spam目录结构,并预留get_mail.py接口,可接入个人邮箱进行联网过滤测试。资源包共407个文件,以20个Python源码与txt操作说明、md文档为核心,其余为训练集邮件样本;压缩包仅995KB,体量轻便、易部署。已有481人学习下载。对需要快速搭建邮件过滤原型或研究朴素贝叶斯文本分类的开发者,这份资源提供了可直接运行、便于二次扩展的完整方案。
1. 毕设答辩前能演示的朴素贝叶斯垃圾邮件过滤系统:先从五分钟跑通训练开始
做毕设最怕的不是算法难,而是答辩演示时分类器当场翻车。这套基于 Python 实现朴素贝叶斯的垃圾邮件过滤系统,是我见过少数“拿到就能演示”的完整工程:带 GUI 操作界面,选训练集、看精确度、查屏蔽词、输邮件号判别,控制台还会把每封邮件的判断过程逐行打出来。
它面向计算机、数据科学、人工智能等专业的学生,可直接作为毕设、课程设计、大作业或初期项目立项演示。价值在于不是一个孤立的分类脚本,而是把训练、评估、自定义词表、在线收件串成了一个闭环。
下面从原理讲到踩坑,按我拆项目时执行的顺序来写。
2. 朴素贝叶斯为什么总被拿来过滤垃圾邮件:三个必懂参数与特征边界
2.1 从贝叶斯公式到“垃圾、正常”二分类
邮件过滤本质上是一个二分类问题。预测一封新邮件时,我们计算的是两个条件概率:
P(垃圾 | 邮件) = P(邮件 | 垃圾) × P(垃圾) / P(邮件) P(正常 | 邮件) = P(邮件 | 正常) × P(正常) / P(邮件)两个式子分母相同,比较时可以直接丢掉。P(垃圾) 和 P(正常) 是训练集中垃圾邮件与正常邮件占比得出的先验概率。而 P(邮件 | 垃圾) 在朴素贝叶斯里被拆成所有特征词条件概率的乘积,因为这里“朴素”指的就是词与词之间互相独立。
到了代码层面,第一个坑是浮点下溢。一封正常邮件至少几十个词,每个词的概率往往在 0.001 量级,几十个连乘后结果直接变成 0.0,两类得分比不出来。成熟实现都会转成 log 加法:先对每个概率取对数,再累加,最后比大小。你在这个项目的控制台里看到类似“+0.0021 / -0.0008”的日志,本质上就是在逐词累加对数得分。
# nb_classifier.py —— 朴素贝叶斯训练与预测的核心 import math from collections import defaultdict # 垃圾邮件与正常邮件的词频表 spam_word = defaultdict(int) ham_word = defaultdict(int) spam_total = 0 # 垃圾邮件总数,用于计算先验 ham_total = 0 alpha = 1.0 # 拉普拉斯平滑参数,默认 1.0 def train(email_words, label): global spam_total, ham_total if label == 1: # 1 表示垃圾 spam_total += 1 for w in email_words: spam_word[w] += 1 else: # 0 表示正常 ham_total += 1 for w in email_words: ham_word[w] += 1 def predict(email_words): vocab = set(spam_word) | set(ham_word) # 全部去重词表 denom_spam = sum(spam_word.values()) + len(vocab) denom_ham = sum(ham_word.values()) + len(vocab) # 用 log 累加替代连乘,防止浮点下溢 log_spam = math.log(spam_total / (spam_total + ham_total)) log_ham = math.log(ham_total / (spam_total + ham_total)) for w in email_words: # 分子加 1 就是拉普拉斯平滑,未登录词不再是 0 概率 log_spam += math.log((spam_word[w] + alpha) / denom_spam) log_ham += math.log((ham_word[w] + alpha) / denom_ham) return 1 if log_spam > log_ham else 0这段代码的核心逻辑就是:训练时按类别累加词频,预测时把每个词的贡献累加进两个对数得分。这里有两个参数要留意。第一是alpha,默认 1.0 对应 sklearn 中MultinomialNB(alpha=1.0),如果改成 0.1,未登录词的概率会变得非常小,模型对陌生特征更敏感;改成 2.0 则所有概率被拉向均值,分类边界更钝。第二是vocab的大小,分母里的len(vocab)必须用训练集整体去重后的词表,不能用某一类单独的词表,否则平滑后两类的分母不可比。
spam_total / (spam_total + ham_total)是先验概率,数据不平衡时它会明显偏向样本多的一方。这也是后面踩坑章节里“全判成垃圾”的一个源头。
2.2 拉普拉斯平滑:一个参数决定新词是否“翻车”
如果某封测试邮件里出现训练集没见过的词,而这个词恰好是特征,直接按朴素贝叶斯公式计算,P(词|垃圾)和P(词|正常)都是 0,整封邮件的概率会被乘成 0,分类器直接失效,界面上表现就是“怎么测都是同一类”。
解决方法是拉普拉斯平滑,也叫加一平滑。分子加alpha,分母加词表大小 × alpha,让每个词在每个类别下都有一个非零概率:
P(词 | 垃圾) = (垃圾邮件中该词出现次数 + alpha) / (垃圾邮件总词数 + 词表大小 × alpha)| 情况 | 不平滑时的概率 | alpha=1.0 平滑后 |
|---|---|---|
| 某词训练期出现过 1 次 | 1 / N | (1+1) / (N + V) |
| 某词从未出现 | 0 | 1 / (N + V) |
| 某词出现 100 次 | 100 / N | (100+1) / (N + V) |
V 是词表大小,N 是当前类别邮件总词数。从这个表能看出,平滑对高频词影响很小,对低频词和未登录词的影响最明显。你在实践里如果发现某封邮件因为一个陌生词被错误分类,不要怀疑算法,先看alpha是不是被改成了接近 0 的数值。
提示:调试这类项目时,优先检查平滑参数和停用词表,而不要一上来重写分类逻辑。绝大多数分类异常都出在数据预处理而不是贝叶斯公式本身。
2.3 中英文分词与停用词表的边界
上一节代码的email_words是已经切好的词列表。这一层直接决定特征质量。常见做法是:中文用 jieba 分词,英文按正则把字母数字串切开,再把大写折成小写。如果再讲究一点,会把数字、URL、邮箱地址各归为一个特殊 token,比如把https://...统一替换成__URL__,避免每个链接都成为独立特征。
提交课程设计时可以用默认的 jieba 分词,但如果想提高区分度,建议在分词后加一个维度:把中文 2-gram 也带上。比如“点击领取”这个词组拆成“点击”和“领取”之后,特征信息比整词组更稀疏,这时候 2-gram 会保留“点击领取”的共现关系。
停用词表要小心处理。以下是一份常见的中文停用词文件格式:
# stopwords_zh.txt —— 一行一个词,UTF-8 无 BOM 的 了 和 是 在 我 你 也 就 都停用词表的边界在于:删得太狠,会把强特征词一起删掉。我拆过几个类似项目,有人为追求“干净”把“点击”“领取”“现金”也放进了停用词表,结果垃圾邮件识别率直线下降。这些词恰恰是垃圾邮件的高频信号。所以停用词表只删结构助词和虚词,不要删实词。宁可让词表小一点,也不要让强特征被误杀。
3. 项目结构与数据集组织:test-ham、test-ham3、test-spam 怎么摆才不翻车
3.1 根目录下的文件与文件夹清单
这个项目不是单文件脚本,而是按“训练样本目录 + GUI 主程序 + 在线收件模块”组织在一起的完整工程。核心文件和作用如下:
| 路径 / 文件 | 作用 | 使用时机 |
|---|---|---|
| get_mail.py | 在线收件模块 | 想测试自己邮箱里的真实邮件时用到 |
| GUI 主程序 | 界面入口 | 训练、精确度、屏蔽词、单封测试都在这里操作 |
| test-ham | 正常邮件样本目录 | 中等训练规模,放类似 test-ham1 格式的纯文本邮件 |
| test-ham3 | 正常邮件样本目录 | 大量训练规模,数量更多 |
| test-spam | 垃圾邮件样本目录 | 与 test-ham 对应,放垃圾样本 |
拿到的压缩包解压后,第一件事不是运行,而是检查test-ham里是否真的有文件。项目操作说明里特别强调“在 test-ham 中放置类似 test-ham1 中的邮件格式的文件,达到中等训练规模”,言下之意是原始压缩包可能只带少量范例样本,真正能跑到 80% 以上精确度,需要你自己往这三个目录里补数据。
3.2 邮件样本格式与命名规则
test-ham1 这个文件就是单封邮件的文本样例。训练脚本扫描目录时通常按后缀.txt读取整个文件内容,因此你的样本必须保持纯文本格式,不要用 Word 文档改扩展名,也不要在正文里塞 HTML 源码。
命名上我建议统一成“数字编号.txt”,比如ham_001.txt、spam_001.txt。拆项目时遇到过有同学把文件名改成“中奖通知邮件.txt”,文件读写时中文名加空格在某些编码环境下直接报错,程序把文件当作 0 字节跳过,训练集等于白放。如果不想在命名上费心思,就全部用 ASCII 文件名。
编码是另一个隐蔽问题。邮件文本可能是 UTF-8,也可能是 GBK,如果训练脚本只用一种编码读取,另一半文件会乱码。拿到项目后先在train函数入口把读到的文本打印前 100 字,快速确认编码是否正常。
3.3 训练规模与类别均衡
操作说明里把样本规模分成两档:test-ham 中等规模、test-ham3 大量规模。按我的经验,中等规模大约每类 300 到 500 封,大量规模建议每类 1500 封以上。低于这个量级,精确度波动会很大,尤其是垃圾邮件种类一多,单个类别样本太少时特征稀疏的问题立刻暴露。
类别均衡比总体数量更重要。垃圾邮件 2000 封、正常邮件 300 封,训练出的分类器会有明显的偏置,预测阶段大概率把正常邮件也判成垃圾。一个简单做法是复制少量样本把两类凑平,但更好的做法是去真实邮箱里多收集被拦截邮件来补足垃圾样本。
同时注意训练集和测试集要分开。这个项目里点击“精确度”时,如果它读的目录刚好是训练目录,结果会虚高到 95% 以上,因为模型在用自己的训练数据回判。我一般会把原始邮件先按 8:2 切开,用 8 成做训练,2 成做精确度评估,这样得到的数字才有参考意义。第 6 章会给一个自动分层脚本,直接复用即可。
4. 从训练到单封测试的完整操作流程:五步走加 get_mail.py 邮箱配置
4.1 选择训练集:先训练,再关闭选择窗口
项目操作说明的第一步写得很明确:一切开始之前先进行训练,点击“选择训练集开始训练”。这里有一个容易忽略的动作顺序——选择好训练集后,要关闭选择训练集的窗口,再继续后续操作。
为什么要把“关闭窗口”单独提出来?因为这个项目的训练是在事件回调里触发的,选择窗口回传目录路径后,如果窗口还开着,程序可能处于等待状态,后续精确度、屏蔽词等按钮的事件不会正常响应。正确的顺序是:
- 启动 GUI,点击“选择训练集开始训练”;
- 在文件对话框里选中
test-ham或混合样本目录; - 等待控制台输出训练完成的提示;
- 手动关闭选择训练集的窗口;
- 再进行下一步操作。
训练完成后不要急着改目录里的文件。训练阶段读进内存的是文件快照,之后你往目录里加文件,当前模型不会感知,必须重新走一遍训练流程。
4.2 精确度按钮:它在评估什么、为什么可能慢
点击“精确度”按钮后,程序会对测试目录里的邮件逐封预测,并把每封邮件的判断过程打印到控制台,这几行日志就是前面 2.1 节说的对数得分累加过程。因为要对每个文件做读入、分词、特征转换、概率累加,再叠加 print 输出,整体速度会比想象中慢。
如果按下去十几分钟都没有响应,先检查测试目录里是不是混入了附件、图片或超大文本文件。训练脚本如果对目录里所有文件统一按文本解析,遇到二进制文件会把整段乱码读进来,分词耗时暴增。先把测试目录清到只剩文本邮件,再重新点按钮。
精确度计算的是测试目录上的正确率,不是训练集回判。如果结果高得不正常,比如 99% 以上,先怀疑测试目录选成了训练目录。这个数字也不是越高越好,垃圾邮件场景里更该关注的是“正常邮件被误判为垃圾”的比例,因为误杀一封正常邮件,用户感知比漏掉一封垃圾邮件更强烈。
4.3 屏蔽词:内置两种词表与自定义格式
点击“屏蔽词”按钮后,可以在内置词表和自定义词表之间切换。内置的两组词表覆盖了两种典型垃圾场景:一组偏营销推广,包含“优惠、秒杀、扫码”这类词;另一组偏欺诈链接,包含“中奖、加QQ、验证码”这类词。
自定义屏蔽词是文本文件格式,一行一个词:
# custom_words.txt —— 自定义屏蔽词表,UTF-8 无 BOM 中奖 加QQ 点击领取 贷款 刷单参数上有三个注意点。第一,编码必须是无 BOM 的 UTF-8,Windows 记事本默认存的 UTF-8 带 BOM,程序按 UTF-8 读取时 BOM 字符会被留在第一个词前面,匹配直接失败。第二,一行只能放一个词,不要用逗号分隔,否则程序很可能按整行匹配。第三,屏蔽词是“命中即加强垃圾判定”还是“命中即强制判垃圾”,取决于源码实现,我建议先用内置词表对比测试,再决定是否自定义。
4.4 输入邮件号测试单封与 get_mail.py 在线收件
“输入想要测试的邮件号即可判断”,这里的邮件号对应数据集中单封邮件的编号。测试前先确认该编号的文件确实存在,否则程序会报“文件不存在”之类的错误。
如果想把训练好的模型用到自己邮箱里的真实邮件上,就需要 get_mail.py 这个模块。常见实现是 IMAP 协议收件,配置项大致如下:
# get_mail.py —— 在线收件配置模板 IMAP_SERVER = "imap.qq.com" # 邮箱服务商地址,163/outlook 各不同 IMAP_PORT = 993 # SSL 加密端口,不要用 143 明文端口 MAIL_USER = "yourname@qq.com" # 改成你自己的邮箱地址 MAIL_AUTH = "your_auth_code" # 授权码,不是登录密码 USE_SSL = True # 固定走 SSL 加密连接邮箱服务商一般都要求先在网页设置里开启 IMAP 服务,再生成一个独立授权码,登录密码本身通常不允许第三方客户端直接使用。配置好后,程序会拉取收件箱里的邮件,进入训练好的分类流程判断。
这里要强调“需要联网不是一句空话”。IMAP 连接必须能访问到邮箱服务器,校园网或公司内网经常有端口策略限制,993 端口连不通时先换手机热点验证网络,再排查代码配置。如果网络确实不可用,就用本地编号文件测试,不要硬等在线收件结果。
5. 避坑清单:训练顺序、授权码与样本均衡的五个常见翻车点
5.1 现象一:点击训练后界面没有任何反应
现象:点击“选择训练集开始训练”,文件选择器弹出来又关掉,但控制台没有任何输出,界面像卡死了一样。
原因:最常见的是没有按操作说明关闭选择训练集的窗口。窗口未关闭状态下,训练线程还在等待或持有目录句柄,后续事件进不来。另一个原因是选择的目录为空,或者目录里文件编码异常,训练逻辑直接卡在读取阶段。
解决:严格按“选目录 → 等待训练完成 → 关闭选择窗口”的顺序操作。如果已经卡住,关闭整个程序重开,先选一个小目录验证流程,再放大目录训练。
5.2 现象二:点击精确度后等了十几分钟没有输出
现象:精确度按钮点下去,界面无响应,控制台也不打印邮件判断过程,持续很长时间。
原因:测试目录里混入了非文本文件或超大文件。程序把所有文件按文本解析,遇到二进制内容后分词时间急剧增加。还有一个原因是精确度逻辑是单线程的,遍历文件和打印日志串行执行,样本量大了之后耗时成倍增长。
解决:把测试目录拆成小批量,先放 200 封以内验证流程,确认每封邮件都能在两秒内输出判断日志,再逐步扩大样本量。不要一次性丢几千个文件进去。
5.3 现象三:get_mail.py 一直连接超时或认证失败
现象:运行在线收件模块,报 timeout 或者 authentication failed,邮箱邮件拉不下来。
原因:三个常见原因。第一,邮箱的 IMAP 服务没有在网页设置里开启;第二,MAIL_AUTH填成了登录密码而不是授权码;第三,IMAP_PORT配成了 143 明文端口,服务商拒绝明文登录。
解决:去邮箱设置页开启 IMAP 服务并生成授权码,确认端口是 993,USE_SSL保持 True。如果网络环境有端口限制,先用手机热点确认能连上服务器,再回到原网络排查。
5.4 现象四:测试结果明显偏向某一类
现象:所有邮件都判成垃圾,或者所有邮件都判成正常,精确度数字异常偏高或偏低。
原因:训练集里两类样本数量严重不均,先验概率偏向样本多的一侧。另一个原因是“精确度”测试目录被误选成了训练目录,模型在自己做过的题上考试,分数自然虚高。
解决:统计test-ham和test-spam的文件数,把两类样本调整到接近 5:5 再重新训练。同时确认精确度读的是独立的测试目录。想快速验证,可以复制少数样本凑平类别,再跑一次训练对比。
5.5 现象五:自定义屏蔽词没有生效
现象:在自定义词表里加了“刷单”“贷款”,测试时这些词出现了,但邮件还是被判成正常。
原因:词表文件编码带了 BOM,第一行第一个词前面多了不可见字符;或者一行放了多个词,程序按整行匹配失败;再或者自定义词表的文件名和程序内置的默认文件名不一致,程序加载的还是内置词表。
解决:用 VS Code 或 Notepad++ 把词表另存为 UTF-8 无 BOM 格式,确认一行一个词、无标点、无多余空格。然后在程序里打印加载的词表内容,确认自定义词真的进了内存,再重新测试。
6. 进阶技巧:精确度自查加一个自动分层脚本,接手就能二次开发
6.1 数据集自动分层脚本
拿到这个项目后,我最先做的事情不是改算法,而是写一个脚本把原始邮件统一按 8:2 切开,训练归训练,测试归测试,两类样本保持均衡。这个脚本直接决定后面的毕设结果有没有说服力。
# split_mail.py —— 按比例切分邮件数据集,保证类别均衡 import os import random import shutil def split_mail_dir(src_dir, train_out, test_out, ratio=0.8, seed=42): files = [f for f in os.listdir(src_dir) if f.lower().endswith(".txt")] random.seed(seed) # 固定随机种子,每次运行结果可复现 random.shuffle(files) split_at = int(len(files) * ratio) for f in files[:split_at]: shutil.copy(os.path.join(src_dir, f), os.path.join(train_out, f)) for f in files[split_at:]: shutil.copy(os.path.join(src_dir, f), os.path.join(test_out, f)) # 对正常邮件目录和垃圾邮件目录分别执行 split_mail_dir("raw_ham", "train_ham", "test_ham", ratio=0.8) split_mail_dir("raw_spam", "train_spam", "test_spam", ratio=0.8)ratio=0.8表示 80% 做训练、20% 做测试,如果样本总量小,可以改成 0.7。seed=42是固定随机种子,同一批数据在任何机器上运行,切分结果都一样,这在答辩演示时很重要——昨天 85%,今天换个机器变成 78%,评审会直接怀疑实验不可控。切分完先用 Excel 或文件管理器统计四个目录的文件数,确认两类样本接近均衡,再开始训练。
从那以后,我每次拿到分类器项目,都强制先走一遍这套检查顺序:先做小样本训练确认路径和编码,再配授权码验证在线收件,最后才动算法参数。这套朴素贝叶斯系统本身不复杂,真正决定最终效果的是数据组织得整不整。你先用分层脚本把数据整理干净,再点一次精确度,会看到比在原始文件夹上乱点稳定得多的结果。希望帮到你。
本文还有配套的精品资源,点击获取