
简介这是一份基于朴素贝叶斯与支持向量机SVM算法的垃圾邮件识别系统完整工程源码面向机器学习初学者、自然语言处理研究者及需要完成相关课程设计的高校学生。项目通过对垃圾邮件和正常邮件进行训练统计词汇词频并构建分类模型实现邮件自动判别功能。资源包为zip压缩格式总计2000个文件大小28.64MB包含80个jpg图片样本、5个Python脚本、pkl模型文件及txt说明文档数字命名的文件为邮件样本数据Python脚本涵盖数据模块、模型构建和附加功能三大模块。该资源已有1357人浏览学习。读者可获取完整工程代码学习朴素贝叶斯和SVM在文本分类中的实际应用了解pytesseract文字引擎配置、百度云图像识别接口调用等扩展功能为独立开展机器学习项目提供可复用的参考模板。1. 垃圾邮件识别为什么是朴素贝叶斯和 SVM 的主场先泼一盆冷水垃圾邮件识别不需要深度学习。把一封中文邮件交给 BERT 能拿到更好看的准确率但一台没有 GPU 的机器、一份几千条的标注数据、一个下班前要交的 Python 工程朴素贝叶斯和 SVM 反而是最可靠的选择。这个标题对应的系统本质是一条完整的机器学习应用流程先用分词和 TF-IDF 把原始邮件变成高维稀疏向量再用朴素贝叶斯和 SVM 两个分类器做判别最后输出正常/垃圾的二分类结果。它适合正在做机器学习入门练手项目的人也适合需要交课程设计、毕业设计的学生。你不需要读懂全部数学推导但需要知道每个参数在真实数据上会带来什么后果——这正是下面要展开的。2. 把邮件变成特征向量中文分词与 TF-IDF 预处理上手做这个系统第一步不是选模型而是先让一封邮件能被计算机“读懂”。英文邮件按空格 split 就能得到单词列表中文没有天然分隔符必须先分词。这一步决定了特征数量、模型训练速度和最终准确率。做这个工程时前提是 Python 环境别装错——我见过新手把 32 位和 64 位版本搞混装完 scikit-learn 一 import 就报错。用 PyCharm 或 VSCode 配好解释器pip install jieba scikit-learn就能继续往下走。2.1 中文分词与停用词jieba 的使用边界分词工具可选 jieba、哈工大 LTP、pkuseg。LTP 和 pkuseg 精度更高但安装重、依赖多在一个以落地为主的分类工程里jieba 的精确模式已经能覆盖绝大多数邮件文本。下面是完整的分词函数。import jieba import re STOP_WORDS set(的 了 您 请 在 是 我 你 他 它 和 就 都 而 及 与 等 啊 吧 吗 呢.split()) def is_pure_punct(word): # 去掉中文字符和字母数字后什么都不剩就是纯标点 return not re.sub(r[\w\u4e00-\u9fff], , word) def normalize_text(text): # 垃圾邮件里 URL 和邮箱是强信号整体替换成占位符防止被切碎 text re.sub(rhttps?://\S, URL , text) text re.sub(rwww\.\S, URL , text) text re.sub(r[\w.][\w.], EMAIL , text) return text def tokenize(text): # 1. 去掉 HTML 标签邮件正文经常混入 htmlbody 之类残留 text re.sub(r[^], , text) text normalize_text(text) # 2. 压缩连续空白 text re.sub(r\s, , text) # 3. 精确模式分词全模式会让特征维度暴涨 return jieba.lcut(text.strip()) def clean_words(words): result [] for w in words: if not w.strip(): continue if w in STOP_WORDS: continue if is_pure_punct(w): continue result.append(w) return result逻辑说明分成几块re.sub(r[^], , text)把标签替换成空格而不是空串是为了避免两个词因为标签删除而黏在一起normalize_text把 URL 和邮箱替换成固定的URL、EMAIL占位符否则一串http://...会被 jieba 切成几十个无意义碎片丢失最关键的信号。\s压缩空白之后再分词。is_pure_punct用[\w\u4e00-\u9fff]把字母、数字、下划线、中文汉字都算作有效字符如果去掉后字符串为空说明原词只剩标点。这里没有把“免费”“点击”加进停用词表因为它们恰恰是垃圾邮件的强信号模型要靠它们做区分停用词表只收“的、了、您”这类无区分度词。2.2 TfidfVectorizer 的四个必调参数分词只是第一步下一步是向量化。sklearn 提供 CountVectorizer 和 TfidfVectorizer。词袋模型只看频次TF-IDF 看的是频次乘以逆文档频率。一个词在 100 封邮件里出现 90 次说明它对分类没什么帮助它只在垃圾邮件里高频出现、正常邮件几乎不见才是关键特征。所以垃圾邮件识别这个场景直接选 TfidfVectorizer。from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer( tokenizerclean_words, ngram_range(1, 2), min_df2, max_df0.9, sublinear_tfTrue, norml2 )参数怎么定说下实际调参的经验。tokenizer接收我们刚写的 clean_words注意它必须接收字符串、返回 list。ngram_range(1, 2)表示保留单个词和相邻双词像“恭喜您”和“获得iPhone”这样的固定搭配对垃圾邮件是强特征不要轻易上三词训练慢了不说新邮件里出现同样三连词的概率极低只会过拟合。min_df2表示一个词至少要出现在两封邮件里只出现一次的错别字、网址碎片、人名都当成噪声丢掉。max_df0.9则反过来超过 90% 邮件都含有的词比如“邮件”“您好”区分度太低直接排除。sublinear_tfTrue用 1log(tf) 替代原始频次避免一封垃圾邮件把“免费”写 20 次就把该特征权重拉爆。norml2让每封邮件的向量长度归一避免长邮件天然比短邮件有更高模长。向量化之后得到的是 scipy 稀疏矩阵不要随手转成 numpy 数组。两万封邮件、两万维特征直接.toarray()会瞬间吃掉几个 G 内存。训练和预测都直接在稀疏矩阵上进行sklearn 内部能正确处理。2.3 训练集划分先切再洗防止数据泄露数据集准备阶段有一个经典坑。拿到 raw_data.csv 后如果第一反应是全量向量化再做 train_test_split训练集和测试集的 F1 会差一大截。原因在后面避坑章节展开这里先说正确顺序先清洗、去重再切分最后在训练集上做 fit_transform。import pandas as pd from sklearn.model_selection import train_test_split df pd.read_csv(spam_data.csv, encodingutf-8) df df.dropna(subset[label, content]) df df.drop_duplicates(subset[content]) df[label] df[label].map({ham: 0, spam: 1}) X_train, X_test, y_train, y_test train_test_split( df[content], df[label], test_size0.2, random_state42, stratifydf[label] )stratifydf[label]很关键。当垃圾邮件只占样本量的 10%随机划分可能把这小部分全分进训练集或测试集分层抽样保证两边正负比例一致。drop_duplicates(subset[content])把同一封群发邮件删到只剩一条。如果不删同一封垃圾邮件在训练集和测试集各出现一份模型等于背题上线后遇到真实新邮件准确率会明显回落。这个阶段的判断标准是数据干净、无重复、标签比例清楚。准备好这一步后面两个分类器都只是几行代码的事。3. 朴素贝叶斯分类器从贝叶斯公式到 sklearn 实现特征向量准备好了进入标题里的第一个主角。朴素贝叶斯是监督学习里最容易实现的分类器之一训练的本质是统计“每个类别下每个词的条件概率”。很多人从吴恩达的机器学习课里看过贝叶斯但真上手做文本分类时常常在三种变体里选错。3.1 三种朴素贝叶斯变体怎么选朴素贝叶斯不是“一种算法”的固定代码sklearn 里按特征分布假设分成三类。先看选型表。变体假设特征服从典型场景GaussianNB高斯分布连续数值特征比如年龄、收入MultinomialNB多项分布词频或 TF-IDF 向量文本分类首选BernoulliNB伯努利分布短文本只看词是否出现关键区别在输入形式。MultinomialNB 期望非负的计数或 TF-IDF 值它统计每个特征在每个类别上的平均概率BernoulliNB 把特征转成 0/1只关心“免费”出现没出现不关心出现 5 次还是 1 次。垃圾邮件往往靠“免费”“点击”“特价”反复轰炸频次本身是信号所以默认用 MultinomialNB。如果邮件很短比如短信类垃圾内容只有十几个字可以换 BernoulliNB 配合二元特征减少长尾频次噪声。3.2 MultinomialNB 最小训练代码与 alpha 参数下面这段是完整闭环把第 2 章得到的训练矩阵喂给模型输出分类报告。from sklearn.naive_bayes import MultinomialNB from sklearn.metrics import classification_report model_nb MultinomialNB(alpha1.0, fit_priorTrue) model_nb.fit(X_train_vec, y_train) y_pred_nb model_nb.predict(X_test_vec) print(classification_report(y_test, y_pred_nb, target_names[正常邮件, 垃圾邮件]))alpha1.0是拉普拉斯平滑。它的意义是如果一个词只在垃圾邮件里出现过、从没在正常邮件里出现过条件概率算出来是 0连乘后整个类别概率直接归零。加 1 平滑避免这种“一次没见就判死刑”的情况。alpha 越大所有概率越往均匀分布靠拢模型越保守。调参经验是 0.5 到 2 之间波动不大不追求极致准确率时用默认 1.0 就行。fit_priorTrue表示从训练数据学习类别先验也就是 P(垃圾邮件)垃圾样本数/总样本数。如果你的数据集是人工构造的、垃圾和正常各占 50%这里会保留这个比例不会强行按真实先验改。3.3 评估指标准确率会骗人召回率才是关键垃圾邮件识别不能只看 accuracy。假设测试集 900 封正常、100 封垃圾模型一刀切全部判成正常准确率也有 90%。但垃圾邮件一封没拦下来这个系统形同虚设。分类报告里的 precision、recall、f1-score 才是判断依据。以垃圾邮件为正样本recall 表示“100 封真垃圾里拦住了多少”precision 表示“被判成垃圾的邮件里真有垃圾的比例”f1 是两者的调和平均。做威胁类识别优先看垃圾邮件那一行的 recall。如果想细看错在哪看混淆矩阵。from sklearn.metrics import confusion_matrix cm confusion_matrix(y_test, y_pred_nb) print(cm)矩阵第一行是正常邮件cm[0][0] 判对、cm[0][1] 误杀第二行是垃圾邮件cm[1][0] 漏拦、cm[1][1] 命中。垃圾过滤场景里cm[1][0] 是必须压到最低的指标cm[0][1] 可以适当容忍——一封促销邮件被扔进垃圾箱远比一封诈骗邮件出现在收件箱安全。调参时我把“垃圾邮件类别的 f1-score”当唯一 KPI而不是总准确率。如果发现所有样本都偏向正常邮件还可以打印model_nb.class_log_prior_确认先验是否异常这能帮你快速定位标签映射是不是反了。4. SVM 分类器核函数选择与 Pipeline 封装朴素贝叶斯快但特征之间完全独立的假设毕竟不成立。SVM 用间隔最大化的思路找决策边界文本分类里通常比贝叶斯更准代价是调参要细心。这章把核函数、C、类别权重、Pipeline 一次讲完再给一个处理大样本的替代方案。4.1 线性核还是 RBF 核由特征维度决定SVM 核函数的选择不是玄学要回到特征维度说起。TF-IDF 处理之后特征维度常常是几万维且绝大多数值为 0这是高维稀疏空间。在这个空间里样本之间的线性可分性已经足够强再用 RBF 核把数据映射到更高维收益极小反而会引入大量支持向量让训练和推理变慢。所以这个场景我只用 LinearSVC不用 SVC(kernelrbf)。核函数主要参数高维稀疏特征下的表现训练耗时linearC好边界足够快rbfC, gamma容易过拟合且慢慢polyC, degree不常用维度越高越不可控慢如果特征维度只有几百且样本量也不大RBF 才有优势。这个判断原则可以直接套用到其他文本分类项目里。4.2 用 Pipeline 串起分词、向量化和 LinearSVC单独保存 vectorizer 和 model 容易漏最省心是包成一个 pipeline。下面是完整代码。from sklearn.pipeline import make_pipeline from sklearn.svm import LinearSVC from sklearn.metrics import classification_report model_svm make_pipeline( TfidfVectorizer( tokenizerclean_words, ngram_range(1, 2), min_df2, max_df0.9, sublinear_tfTrue ), LinearSVC(C1.0, class_weightbalanced, random_state42) ) model_svm.fit(X_train, y_train) y_pred_svm model_svm.predict(X_test) print(classification_report(y_test, y_pred_svm, target_names[正常邮件, 垃圾邮件]))这里 X_train 直接传原始文本不用再提前做向量化因为 pipeline 内部已经包含 TfidfVectorizer。fit 时它自动对训练集做 fit_transformpredict 时自动对测试集 transform顺序不会再错。这是我把机器学习应用流程落成工程时的标准姿势数据清洗在外部做分词向量化交给 pipeline模型只关心最后一个环节。LinearSVC 的两个参数说明。C1.0是误分类惩罚权重C 越大模型越努力把所有训练点分类正确边界变得复杂容易过拟合C 越小容错越高边界平滑。文本高维场景从 C1 起步用网格搜索试 0.5 到 2.0。class_weightbalanced会自动根据类别频率把少数类权重抬高垃圾邮件只占 20% 时这一项能把垃圾邮件的召回率明显拉起来。LinearSVC 的dual参数无需手动干预新版 sklearn 会根据样本数和特征数自动选择求解方式。另外LinearSVC 训练完成后权重系数可以直接用来审视特征。coef model_svm.named_steps[linearsvc].coef_.toarray()[0] feature_names model_svm.named_steps[tfidfvectorizer].get_feature_names_out() top_words sorted(zip(feature_names, coef), keylambda x: x[1], reverseTrue)[:20] print(top_words)打印出来你会发现排在前面的几乎都是“免费”“点击”“领取”这类词。我习惯拿这个列表做一次特征审核如果发现某个词明显和任务无关说明停用词表或预处理还需要调整。4.3 大数据量下的替代方案SGD 逼近 SVM当样本量到几十万封LinearSVC 的计算也会吃紧。很多人问“svm的梯度下降”能不能用其实 sklearn 的 SGDClassifier 配上losshinge就是对线性 SVM 的梯度下降解法效果非常接近但训练速度快一个量级。from sklearn.linear_model import SGDClassifier model_sgd make_pipeline( TfidfVectorizer(tokenizerclean_words, min_df2), SGDClassifier(losshinge, alpha1e-4, max_iter1000, random_state42) )losshinge让优化目标变成 hinge loss这正是 SVM 的目标函数。alpha1e-4是正则项系数数值越大正则越强。max_iter1000给足收敛轮数数据量小时 100 轮就到平台期但保险起见设大。SGDClassifier 还支持partial_fit新邮件积累到一定量后可以增量训练不用全量重来。先跑朴素贝叶斯当基线再上 LinearSVC 提精度数据涨到大规模再切 SGD这是我常用的三阶段进阶路径。5. 避坑与排查垃圾邮件识别最容易翻车的四个位置这个系统本身不复杂但实践中翻车大多不在模型公式而在数据和工程细节。下面几条是血泪换来的排查手册按“现象、原因、解决”的顺序写。另外加一条编码问题很多人被它卡在第一步。5.1 数据泄露先 fit_transform 再切分测试集被剧透现象训练集 f1 高到 0.99测试集只到 0.6差距大得不合理。原因数据准备阶段对全量邮件调用了一次TfidfVectorizer.fit_transform然后才 train_test_split。测试集的词频已经参与了 idf 逆文档频率计算模型在训练时通过特征权重“偷看”了测试集分布评估结果虚高线上完全不成立。解决严格遵循先划分、再 fit_transform 的顺序。训练集用fit_transform测试集只能用transform后者只做映射不做拟合。最省心的做法是放进 Pipeline让模型自己处理顺序从根源上堵住这个坑。5.2 特征维度爆炸一次分词出来 30 万列现象X_train_vec.shape打出来是 (8000, 300000)内存占用飙升LinearSVC 跑十几分钟没结束。原因jieba 用了全模式又把ngram_range(1, 3)打开min_df1于是人名、网址碎片、错别字、三词组合全成了特征。全模式产生的词比精确模式多 30% 到 50%三词组合更是指数级膨胀。解决分词统一用jieba.lcut精确模式ngram_range 控制在 (1,1) 或 (1,2)。再加两道闸min_df2过滤低频max_features50000让 sklearn 只保留词频最高的 5 万维。加上这三项之后特征维度能从 30 万降到 7 万左右训练时间从十几分钟降到两分钟F1 反而上涨因为噪声特征被清掉了。5.3 模型保存后 predict 报错pickle 与环境的坑现象本地训练完用 joblib.dump 保存换一台机器 load 之后调用 predict 报 ValueError提示特征数量对不上。原因分词 tokenizer 是定义在主脚本里的函数pickle 只存函数名和模块路径换机器后模块路径变了TfidfVectorizer 的分词器引用失效。另外 scikit-learn 版本升级后部分内部结构不兼容也会导致向量器恢复后输出的特征矩阵维度不一致。解决分词函数放到独立模块比如text_utils.py确保路径一致。把 vectorizer 和分类器作为一个整体 pipeline 保存不要分开关。部署环境锁定依赖pip freeze requirements.txt。现在我的习惯是只保存model_svm.pkl一个文件加载后直接 predict 原始文本不碰中间状态。5.4 准确率虚高但漏信用类别权重和阈值找回召回现象分类报告里整体 accuracy 有 96%垃圾邮件那一行的 recall 只有 60%测试集里四成垃圾邮件漏进了收件箱。原因样本里垃圾邮件占比太低模型把几乎所有样本判成正常类就能拿到很高的准确率。默认阈值 0.5 在类别不平衡时会对多数类有利。解决两步走。第一步给模型加类别权重把少数类识别错误的代价调高第二步手动调判决阈值。LinearSVC 不支持 predict_proba但可以取 decision_function 的得分当作置信度再找一个更合适的分界。scores model_svm.decision_function(X_test) y_pred_custom (scores -0.2).astype(int)贝叶斯模型有概率输出调起来更直观。proba model_nb.predict_proba(X_test_vec)[:, 1] y_pred_custom (proba 0.3).astype(int)阈值设到 0.3表示模型有 30% 的把握就判成垃圾误杀会增加但召回会明显提升。具体调多少建议打印不同阈值下的召回率和误杀率挑业务能接受的点。垃圾过滤场景我一般让误杀率控制在 1% 以内召回尽量提到 90% 以上。5.5 编码问题GBK 和 UTF-8 搞出乱码现象pd.read_csv(spam_data.csv)直接报 UnicodeDecodeError或者代码能跑但分词结果全是乱码。原因邮件数据集很多是从旧系统导出的文件是 GBK 编码而 Python 默认按 UTF-8 读。解决读取时显式指定编码并容忍个别坏字节。df pd.read_csv(spam_data.csv, encodingutf-8, errorsignore)如果确定是 GBK 文件把 encoding 换成gbk。这个坑不涉及模型但卡住后前面所有代码都跑不动值得最先排查。6. 进阶网格搜索调参与模型上线前的验证技巧到这一步系统已经能跑通、能出报告。再往前走不是换模型而是把参数和评估流程规范化。6.1 用 GridSearchCV 一次找到 C 与 ngram_range手动试参是玄学换成网格搜索直接搜。Pipeline 场景下参数名用双下划线分层指定。from sklearn.model_selection import GridSearchCV param_grid { tfidfvectorizer__ngram_range: [(1, 1), (1, 2)], tfidfvectorizer__min_df: [1, 2], linearsvc__C: [0.5, 1.0, 2.0] } search GridSearchCV( model_svm, param_grid, cv3, scoringf1, n_jobs-1, verbose1 ) search.fit(X_train, y_train) print(search.best_params_) print(search.best_score_)scoringf1是这里的关键。如果评分函数选 accuracy网格搜索同样会被类别不平衡骗到选 f1 会让搜索过程直接面向垃圾邮件的识别能力。cv3 用 3 折交叉验证比单次切分更稳定。n_jobs-1 让所有 CPU 核一起跑组合数不大时几分钟内出结果。6.2 上线前做一次速度对比贝叶斯和 SVM 谁更适合实时过滤调完参还要确认推理延迟。垃圾邮件过滤通常是边收信边预测单封邮件预测时间太长用户收信体验会很差。import time model_nb_pipeline make_pipeline( TfidfVectorizer(tokenizerclean_words, min_df2), MultinomialNB() ) model_nb_pipeline.fit(X_train, y_train) for name, model in [(NaiveBayes, model_nb_pipeline), (SVM, model_svm)]: t0 time.time() model.predict(X_test[:500].tolist()) print(name, round(time.time() - t0, 3))注意这组对比里两个模型都是 pipeline接口一致都能直接接收原始文本。实际测下来朴素贝叶斯通常比 SVM 快一个数量级因为 SVM 的决策依赖支持向量即使线性核计算已经在优化单条样本仍要多算一次特征映射。我自己的做法是先拿朴素贝叶斯当基线把召回率撑到可接受区间再用 SVM 顶精度。全套流程跑顺后把分词器、向量器、模型一起用 joblib 保存成单文件写一个predict_one(text)函数对外只暴露原始邮件文本的输入接口。如果你正在做机器学习相关的课程设计或者生产项目这套“贝叶斯打底、SVM 提精度、网格搜索收尾”的路径足够把结果做扎实。希望帮到你。本文还有配套的精品资源点击获取