☰
基于朴素贝叶斯算法实现垃圾邮件分类:从原理到Python实战
2026/10/3 2:53:46 网站建设 项目流程

简介:本资源为基于机器学习朴素贝叶斯算法实现垃圾邮件分类的Python项目完整包,面向计算机相关专业正在做大作业、课程设计或期末项目的学生,以及需要项目实战练习的学习者。项目经导师指导并认可,评审分98分,在400封正常与垃圾邮件各半的测试集上分类准确率达95.15%,仅靠词频统计计算概率即可取得不错效果。包内共约2000个文件,以大量无后缀邮件样本数据为主,另含3个py源码文件、5个pyc编译文件、txt说明、md项目文档及prefs、gitattributes等配置项,压缩包整体约17.78MB,目录结构清晰,便于按模块查阅。源码覆盖Python3.4环境搭建、结巴分词工具调用、贝叶斯公式推导与词向量条件概率计算等关键环节,并附项目说明与数据集,可帮助读者理解从文本预处理到分类预测的完整流程。目前已有310人学习下载,适合作为入门机器学习与文本分类的实战参考。

1. 垃圾邮件分类为什么成了贝叶斯算法的经典练兵场

邮箱里每天躺着几十封“恭喜您中奖”“发票代开”“低息贷款”,手动删到手软。基于机器学习贝叶斯算法实现垃圾邮件分类,本质上就是让程序学会自动判断一封邮件是正常邮件还是垃圾邮件。这件事看起来简单,但它是自然语言处理领域最经典的入门任务之一,也是理解朴素贝叶斯分类器的最佳场景。你拿到一份标注好的邮件数据集,用 Python 把文本转成特征向量,训练一个贝叶斯模型,就能得到一个准确率相当可观的分类器。整套流程不依赖 GPU,普通笔记本几分钟跑完,适合机器学习入门练手,也适合需要快速搭建邮件过滤模块的开发者。下面从原理到代码到踩坑,把这条路走通。

2. 朴素贝叶斯做文本分类:原理与选型理由

2.1 为什么文本分类偏偏选中朴素贝叶斯

文本分类的候选算法不少:SVM、逻辑回归、随机森林、甚至微调 BERT。但如果你的场景是邮件过滤,朴素贝叶斯往往是第一个该试的方案。原因有三。

第一,文本数据的特征维度极高。一封邮件经过分词和词袋模型处理后,特征数轻松上万。朴素贝叶斯在这个维度下计算量线性增长,训练和预测都极快,而 SVM 在高维稀疏数据上虽然表现不错,但调参和核函数选择会拖慢迭代速度。

第二,朴素贝叶斯的“条件独立假设”在文本场景下虽然不成立(词与词之间显然有上下文关系),但这个假设带来的偏差反而让它对小数据集有更好的鲁棒性。邮件分类通常几千到几万条样本,深度模型容易过拟合,朴素贝叶斯反而稳。

第三,它天然支持增量学习。新来的邮件可以快速更新概率表,不需要重新训练整个模型。这对在线邮件过滤系统非常友好。

常见做法是先用朴素贝叶斯跑一个基线,如果准确率不够再考虑上 TF-IDF 加权或换用线性 SVM。我一般会先看朴素贝叶斯的混淆矩阵,确认漏报和误报的比例,再决定要不要加特征工程。

2.2 贝叶斯定理在邮件分类中的具体计算过程

贝叶斯定理的核心公式:

P(垃圾|邮件) = P(邮件|垃圾) × P(垃圾) / P(邮件)

在分类任务中,P(邮件) 对所有类别相同,可以忽略。我们只需要比较:

P(垃圾|邮件) ∝ P(垃圾) × P(邮件|垃圾) P(正常|邮件) ∝ P(正常) × P(邮件|正常)

其中 P(垃圾) 和 P(正常) 是先验概率,直接从训练集里统计。P(邮件|垃圾) 是似然,在“朴素”假设下,认为邮件中每个词独立出现,于是:

P(邮件|垃圾) = P(词1|垃圾) × P(词2|垃圾) × ... × P(词n|垃圾)

每个 P(词i|垃圾) 用训练集中该词在垃圾邮件里出现的次数除以垃圾邮件总词数来估计。

这里有个关键细节:如果某个词在训练集的垃圾邮件里从没出现过,P(词i|垃圾) = 0,整个乘积就变成 0。所以实际实现必须做拉普拉斯平滑,分子加 1,分母加词汇表大小。这个参数在 sklearn 的 MultinomialNB 里由 alpha 控制,默认是 1.0。

2.3 从原始邮件到特征向量:文本预处理流水线

原始邮件是纯文本,不能直接喂给模型。需要经过以下步骤:

第一步,读取邮件内容。如果是英文邮件,通常已经分好词;如果是中文邮件,需要分词。常见做法是用 jieba 分词,英文则直接按空格和标点切分。

第二步,去除停用词和标点。像“的”“了”“is”“the”这类词对分类贡献很小,去掉能降维。但注意,垃圾邮件里“免费”“中奖”“发票”这些词恰恰是关键特征,不能误删。

第三步,构建词袋模型或 TF-IDF 向量。词袋模型只统计词频,TF-IDF 会降低常见词的权重。对于朴素贝叶斯,词袋模型通常就够了,因为朴素贝叶斯本身对特征缩放不敏感。

第四步,划分训练集和测试集。一般 8:2 或 7:3。注意要先划分再做特征提取,避免数据泄露。

下面是一个完整的预处理和训练代码示例:

import jieba import os import numpy as np from sklearn.feature_extraction.text import CountVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, confusion_matrix # 读取邮件数据,假设目录结构为 data/spam/ 和 data/ham/ def load_emails(data_dir): texts, labels = [], [] for label, folder in enumerate(['ham', 'spam']): folder_path = os.path.join(data_dir, folder) for filename in os.listdir(folder_path): filepath = os.path.join(folder_path, filename) with open(filepath, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() # 中文分词,英文邮件可改为 content.split() words = jieba.lcut(content) texts.append(' '.join(words)) labels.append(label) return texts, labels # 加载数据 texts, labels = load_emails('data') print(f'总样本数: {len(texts)}, 垃圾邮件数: {sum(labels)}') # 划分训练集和测试集,stratify 保证类别比例一致 X_train, X_test, y_train, y_test = train_test_split( texts, labels, test_size=0.2, random_state=42, stratify=labels ) # 词袋模型,max_features 限制词汇表大小,防止维度爆炸 vectorizer = CountVectorizer(max_features=5000) X_train_vec = vectorizer.fit_transform(X_train) X_test_vec = vectorizer.transform(X_test) # 朴素贝叶斯分类器,alpha 是拉普拉斯平滑参数 clf = MultinomialNB(alpha=1.0) clf.fit(X_train_vec, y_train) # 预测和评估 y_pred = clf.predict(X_test_vec) print(classification_report(y_test, y_pred, target_names=['正常邮件', '垃圾邮件'])) print('混淆矩阵:') print(confusion_matrix(y_test, y_pred))

这段代码的逻辑说明:load_emails函数遍历两个文件夹,分别打标签 0 和 1,用 jieba 分词后拼接成空格分隔的字符串。train_test_split的stratify参数确保训练集和测试集中垃圾邮件的比例与原始数据一致,避免随机划分导致某一类样本过少。CountVectorizer的max_features=5000只保留出现频率最高的 5000 个词,既能降维又能过滤掉大量只出现一次的低频词。MultinomialNB的alpha=1.0是拉普拉斯平滑的默认值,如果发现模型对某些词过于敏感,可以调大到 2.0 或 3.0。

参数说明:max_features建议从 3000 到 10000 之间试,太小会丢失关键特征,太大则训练变慢且容易过拟合。alpha越小模型越“自信”,但容易过拟合;越大则越保守。一般从 1.0 开始调。

3. 用 Python 跑通垃圾邮件分类:从数据到模型评估

3.1 数据集的组织方式与读取脚本

拿到一个垃圾邮件分类数据集,通常有两种格式:一种是每个邮件一个 txt 文件,按文件夹分好类;另一种是一个 CSV 文件,两列分别是标签和邮件正文。两种都要能处理。

如果是文件夹结构,目录长这样:

data/ ├── ham/ │ ├── 001.txt │ ├── 002.txt │ └── ... └── spam/ ├── 001.txt ├── 002.txt └── ...

如果是 CSV,用 pandas 读取:

import pandas as pd df = pd.read_csv('spam.csv', encoding='latin-1') # 假设列名为 label 和 message df = df[['label', 'message']] df['label'] = df['label'].map({'ham': 0, 'spam': 1}) print(df.head()) print(df['label'].value_counts())

注意编码问题。很多公开邮件数据集是 latin-1 或 ISO-8859-1 编码,直接 utf-8 会报错。用errors='ignore'或指定正确编码。

读取后先看类别分布。如果垃圾邮件只占 5%,那准确率这个指标就没意义了——全预测为正常也有 95%。这时候要看召回率和 F1。

3.2 特征工程:词袋模型与 TF-IDF 的取舍

词袋模型和 TF-IDF 是文本分类最常用的两种向量化方式。词袋模型只统计词频,简单直接。TF-IDF 在此基础上乘以逆文档频率,降低“的”“是”这类在所有邮件里都高频出现的词的权重。

对于朴素贝叶斯,我一般先用词袋模型跑基线。如果发现分类器被“免费”“中奖”这类词主导,但误报率偏高(正常邮件里也有“免费”),再换 TF-IDF 试试。TF-IDF 的代码只需把CountVectorizer换成TfidfVectorizer:

from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer(max_features=5000, ngram_range=(1, 2)) X_train_vec = vectorizer.fit_transform(X_train) X_test_vec = vectorizer.transform(X_test)

ngram_range=(1, 2)表示同时考虑单个词和相邻两个词的组合。比如“免费 领取”作为一个特征,比单独的“免费”和“领取”更有区分度。但 n-gram 会让特征数平方级增长,max_features要相应控制。

实际对比:在大多数邮件数据集上,词袋模型和 TF-IDF 的准确率差距在 1% 到 3% 之间。如果追求快速上线,词袋模型足够;如果误报率是核心指标,TF-IDF 加 bigram 通常能降几个百分点。

3.3 模型训练、预测与评估指标解读

训练完模型后,不能只看准确率。邮件分类场景下,误报(把正常邮件判成垃圾)和漏报(把垃圾邮件判成正常)的代价完全不同。误报可能导致用户错过重要邮件,代价更高。所以评估时要重点看混淆矩阵和分类报告。

from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score print('准确率:', accuracy_score(y_test, y_pred)) print('垃圾邮件精确率:', precision_score(y_test, y_pred)) print('垃圾邮件召回率:', recall_score(y_test, y_pred)) print('垃圾邮件F1:', f1_score(y_test, y_pred))

精确率高说明被判为垃圾的邮件里真正垃圾的比例高,误报少。召回率高说明垃圾邮件被抓住的比例高,漏报少。如果业务要求“宁可放过不可错杀”,就优先保精确率,可以调高alpha或调整分类阈值。

另外,classification_report会输出每个类别的精确率、召回率和 F1,直接看“垃圾邮件”那一行即可。

3.4 模型持久化与批量预测脚本

训练好的模型要保存下来,不然每次预测都重新训练太慢。用 joblib 保存模型和向量化器:

import joblib # 保存模型和向量化器 joblib.dump(clf, 'spam_classifier.pkl') joblib.dump(vectorizer, 'vectorizer.pkl') # 加载并预测新邮件 clf_loaded = joblib.load('spam_classifier.pkl') vec_loaded = joblib.load('vectorizer.pkl') def predict_email(text): words = jieba.lcut(text) text_vec = vec_loaded.transform([' '.join(words)]) pred = clf_loaded.predict(text_vec)[0] proba = clf_loaded.predict_proba(text_vec)[0] return '垃圾邮件' if pred == 1 else '正常邮件', proba # 测试 test_email = "恭喜您获得一等奖,请点击链接领取" result, proba = predict_email(test_email) print(f'预测结果: {result}, 概率: {proba}')

注意:加载模型后,新邮件必须用同一个向量化器转换,不能重新 fit。predict_proba返回两个类别的概率,可以设置阈值来调整判定策略。比如概率大于 0.9 才判为垃圾,能进一步降低误报。

4. 避坑与排查:垃圾邮件分类中容易翻车的五个地方

4.1 中文分词把关键特征切碎了

现象:模型准确率始终在 70% 左右上不去,检查发现“中奖”“免费”这些词被 jieba 切成了“中”“奖”“免”“费”。

原因:jieba 默认词典对垃圾邮件里的新词、网络词覆盖不够,导致关键特征词被切碎,失去区分度。

解决:加载自定义词典,把垃圾邮件常见词加进去。代码:

jieba.load_userdict('spam_words.txt') # spam_words.txt 每行一个词,如:中奖 100 n

或者直接用jieba.add_word('中奖')动态添加。另外,分词后可以把单字过滤掉,只保留长度大于等于 2 的词,减少噪声。

4.2 训练集和测试集划分时数据泄露

现象:模型在测试集上准确率 99%,上线后实际效果只有 80%。

原因:先对整个数据集做了向量化,再划分训练测试集。向量化器在 fit 时已经“看到”了测试集的词汇分布,导致信息泄露。

解决:严格按“先划分,再 fit 训练集,再 transform 测试集”的顺序。上面的代码示例已经遵循这个原则。如果做交叉验证,要用 Pipeline 把向量化和分类器串起来,让 sklearn 自动处理。

from sklearn.pipeline import Pipeline pipeline = Pipeline([ ('vectorizer', CountVectorizer(max_features=5000)), ('classifier', MultinomialNB(alpha=1.0)) ]) pipeline.fit(X_train, y_train)

4.3 类别极度不平衡导致模型“偷懒”

现象:垃圾邮件只占 3%,模型把所有邮件都判为正常,准确率仍有 97%,但召回率为 0。

原因:朴素贝叶斯在训练时,先验概率 P(垃圾) 极低,导致后验概率被先验压制,模型倾向于预测多数类。

解决:三种方法。一是用class_prior参数手动设置先验,比如MultinomialNB(class_prior=[0.5, 0.5])。二是对少数类过采样,用 imblearn 的 SMOTE 生成合成样本。三是调整预测阈值,不取 argmax,而是手动设一个概率阈值。

# 手动设置先验 clf = MultinomialNB(alpha=1.0, class_prior=[0.5, 0.5])

4.4 停用词表把垃圾邮件特征词误删了

现象:加了停用词过滤后,准确率反而下降了 5%。

原因:通用停用词表里可能包含“免费”“中奖”“发票”等词,这些恰恰是垃圾邮件的强特征。删掉后模型失去了关键判断依据。

解决:不要直接用网上的通用停用词表。先统计训练集中垃圾邮件和正常邮件词频差异最大的词,人工检查这些词是否在停用词表里。如果在,从停用词表里移除。或者干脆不做停用词过滤,让模型自己通过特征权重学习。

4.5 新邮件里出现训练集从未见过的词

现象:预测一封新邮件时报错ValueError: dimension mismatch。

原因:新邮件经过分词后,包含训练集词汇表里没有的词,导致向量化后的维度与模型输入维度不一致。

解决:用同一个向量化器 transform,CountVectorizer会自动忽略未知词,不会报错。报错通常是因为重新 fit 了向量化器,或者手动构建了特征向量。确保加载的是训练时保存的vectorizer.pkl,并且只调用transform,不调用fit或fit_transform。

5. 进阶技巧:让贝叶斯分类器更懂你的邮件

朴素贝叶斯跑通之后,如果想把准确率和实用性再往上提一截,有几个方向值得试。

第一个是特征选择。CountVectorizer的max_features是粗暴截断,更好的做法是用卡方检验或互信息挑出与类别最相关的词。sklearn 的SelectKBest配合chi2可以做到:

from sklearn.feature_selection import SelectKBest, chi2 from sklearn.pipeline import Pipeline pipeline = Pipeline([ ('vectorizer', CountVectorizer(max_features=10000)), ('selector', SelectKBest(chi2, k=3000)), ('classifier', MultinomialNB(alpha=1.0)) ])

这样先保留 10000 个词,再用卡方挑出 3000 个最相关的,通常比直接截断效果好。

第二个是模型融合。把朴素贝叶斯和逻辑回归的预测概率做加权平均,能互补短板。朴素贝叶斯对小样本稳,逻辑回归对特征相关性建模更好。用VotingClassifier几行代码就能实现:

from sklearn.ensemble import VotingClassifier from sklearn.linear_model import LogisticRegression clf1 = MultinomialNB(alpha=1.0) clf2 = LogisticRegression(max_iter=1000) ensemble = VotingClassifier( estimators=[('nb', clf1), ('lr', clf2)], voting='soft' )

第三个是处理邮件头信息。真实邮件除了正文,还有发件人、主题、时间等元数据。把这些字段拼接到正文前面一起向量化,往往能提升几个百分点。比如主题里的“发票”“贷款”比正文里的词更有指示性。

第四个是定期更新模型。垃圾邮件的用词变化很快,上个月的特征词这个月可能就失效了。建议每月用新标注的数据重新训练一次,或者用partial_fit做增量更新。MultinomialNB支持partial_fit,可以流式学习:

clf = MultinomialNB() clf.partial_fit(X_train_vec, y_train, classes=[0, 1]) # 后续新数据 clf.partial_fit(X_new_vec, y_new)

注意partial_fit第一次调用必须传classes参数,后续不用。

最后说一个我自己的习惯:每次训练完模型,我都会把特征权重最高的 20 个词打印出来看一眼。如果看到“的”“是”这种词排在前列,说明预处理有问题;如果看到“免费”“中奖”“发票”排在前列,说明模型学到了正确的东西。这个习惯帮我省了很多排查时间。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询