尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

垃圾邮件识别实战:朴素贝叶斯与SVM文本分类全流程

垃圾邮件识别实战:朴素贝叶斯与SVM文本分类全流程 简介这是一份基于朴素贝叶斯与支持向量机SVM的垃圾邮件识别系统Python工程面向机器学习初学者、NLP应用开发者及需要课程设计参考的高校学生。项目通过对垃圾邮件与正常邮件数据集进行训练完成词汇词频统计与分类建模涵盖数据模块、模型构建与附加功能三大模块可用于理解文本分类、特征提取及两类经典算法的实际落地。压缩包共含约2000个文件主要类型包括80个jpg图像样本、5个Python源码文件、pkl模型文件、txt说明文档以及大量数字命名的数据文件整体约28.64MB目录结构便于按模块查阅。已有1357人学习下载。除完整源码与数据集外资源附带环境配置说明支持Windows下Anaconda配置及Linux虚拟机运行并整合pytesseract文字识别与百度云图像识别接口可作为扩展功能参考。读者可对照工程代码与数据文件系统掌握邮件文本预处理、词频统计、朴素贝叶斯与SVM模型训练及评估流程为课程设计或实际项目提供可复用的基础方案。1. 垃圾邮件识别一个能同时跑通朴素贝叶斯和SVM的机器学习分类工程垃圾邮件识别是机器学习分类算法里最适合亲手跑通一遍的场景数据不用标注、正负样本明显朴素贝叶斯和SVM都能跑出像样的结果。这个Python工程把两条路线做在同一套流程里——jieba分词、停用词过滤、TF-IDF特征、MultinomialNB和LinearSVC训练、混淆矩阵评估、joblib持久化全链路源码完整。朴素贝叶斯胜在轻快、几乎不吃参数SVM胜在边界干净、抗噪强对比着调很快能建立什么场景选什么分类器的判断直觉。适合课程设计、算法入门、以及想把邮件过滤需求落成可调用脚本的从业者。这篇笔记按拆完工程的实际顺序来写预处理怎么做、两个算法的参数怎么设、评估为什么不能只看准确率以及踩过的几个坑。2. 邮件文本预处理与特征工程从原始正文到TF-IDF矩阵2.1 中文分词与停用词表为什么不直接把文本喂给分类器朴素贝叶斯和SVM都属于不吃字符串的分类算法。sklearn的TfidfVectorizer虽然能直接接收原始文本但它在内部是按空格切词的。英文天然有空格分词中文一整句没有空格直接传进去会把恭喜您获得iPhone一部请点击链接当成一个词特征完全失效。所以这个工程的第一步是接入jieba分词把每封邮件切成词序列再用空格拼回一句话交给TfidfVectorizer处理。分词完成之后还有一个直接影响分类效果的动作停用词过滤。像的了在是这类高频虚词在垃圾邮件和正常邮件里出现的比例几乎一样它们只会给TF-IDF特征矩阵增加大量权重接近的维度顺带拉高SVM的训练耗时。停用词表的选择常见做法是下载哈工大停用词表拿到后再针对邮件业务补几个词。我一般会在跑完第一版模型后把特征权重最高的词拉出来人工看一眼凡是垃圾和正常邮件里都大量出现的词就手动加进停用词集合里。这比重换一个更大的通用词表更有效因为你清理的是这个数据集自己的噪声而不是别人数据集的噪声。注意分词要在特征化之前做而且训练集和验证集必须用同一套分词逻辑。这个工程把分词函数封装成独立模块后续换停用词表或加自定义词典都不用动模型代码。2.2 从原始邮件到TF-IDF矩阵特征构造的代码与参数工程里的数据通常是CSV格式两列text为邮件正文label为spam或ham。如果手头是spam/ ham目录结构遍历目录把文件名当标签、文件内容当正文拼成DataFrame即可这里按CSV版本展开。import pandas as pd import jieba from sklearn.feature_extraction.text import TfidfVectorizer # 假设数据是CSV两列text 为邮件正文label 为 spam/ham df pd.read_csv(emails.csv, encodingutf-8-sig, dtypestr) df[label] df[label].map({spam: 1, ham: 0}) df[label] df[label].fillna(0).astype(int) # 加载停用词表一行为一个词 stop_words set() with open(stopwords.txt, r, encodingutf-8) as f: for line in f: word line.strip() if word: stop_words.add(word) # 分词并过滤停用词返回空格拼接的分词串 def cut_mail(text): words jieba.lcut(str(text)) return .join(w for w in words if w.strip() and w not in stop_words) df[cutted] df[text].apply(cut_mail) # max_features 控制特征数量ngram_range 决定是否组合相邻词 vectorizer TfidfVectorizer(max_features5000, ngram_range(1, 2), min_df2) X vectorizer.fit_transform(df[cutted]) y df[label] print(特征矩阵形状:, X.shape)这段代码有几个参数值得单独说。encodingutf-8-sig是为了兼容带BOM的CSV文件用记事本或Excel保存过CSV的同学都遇到过首行第一个字段莫名多个\ufeff字符的情况这个字符会混进词表变成一个特征词。min_df2过滤掉只在1封邮件里出现过的词这些词提供不了统计区分度还会给矩阵增加噪声列。max_features5000对几千封邮件的规模够用如果数据集到几万封可以提到10000以上但不是越大越好——特征维度上去后SVM训练时间近似线性上涨而分类效果很快进入平台期。ngram_range(1, 2)把中和奖、发票这类双字组合也纳入特征对中文垃圾邮件尤其重要因为很多垃圾词是双字词单字特征容易被停用词淹没。2.3 划分训练集与验证集stratify是分层抽样的便宜售后特征矩阵构造好后下一步是切训练集和验证集。这里有一个新手最容易忽略的细节train_test_split默认是纯随机切分垃圾邮件在样本里占比只有30%的话验证集里垃圾比例可能变成20%或40%每次跑评估结果都不同你根本没法判断模型改动到底有没有效果。from sklearn.model_selection import train_test_split # stratifyy 保证正负样本比例在切分前后一致 X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, random_state42, stratifyy ) print(训练集正样本占比:, round(y_train.mean(), 4)) print(验证集正样本占比:, round(y_val.mean(), 4))stratifyy参数按y的类别比例做分层抽样切分后训练集和验证集的正样本占比基本保持一致。random_state42固定随机数种子让每次切分结果可复现——这是保证实验可重复的前提不然你调了一个参数重新跑一遍代码数据分布变了结论就不可信了。有一点要注意stratify必须配合分类标签使用如果y是连续数值sklearn会直接报错。这一步做完数据就可以进模型了。3. 朴素贝叶斯与SVM建模用sklearn把分类器跑起来并调出可复现参数3.1 朴素贝叶斯家族MultinomialNB与BernoulliNB的适用边界朴素贝叶斯的基本原理是用贝叶斯定理计算P(类别|特征)它假设特征之间条件独立虽然这个假设在文本上严格说不成立但实际效果出奇的好。sklearn里朴素贝叶斯有三个常用变体GaussianNB用于连续数值特征文本场景基本不用MultinomialNB假设特征服从多项式分布直接接收词频计数或TF-IDF值是文本分类默认选项BernoulliNB假设特征只有0和1只关心词出现或没出现。垃圾邮件场景里中奖出现1次和出现10次对判定结果的影响完全不同——同一封邮件里反复出现的词往往就是这封邮件的主体意图所以MultinomialNB比BernoulliNB更贴合。工程里如果想用BernoulliNB做对照需要把TF-IDF矩阵做一个二值化即所有大于0的值都变成1这一步可以用sklearn.preprocessing.Binarizer完成。我的经验是BernoulliNB在邮件场景通常比MultinomialNB低1到2个百分点的F1但训练速度几乎没差别作为基准模型跑一份对照数据还是值得的。另一个容易被忽略的问题是TF-IDF矩阵是scipy.sparse稀疏矩阵MultinomialNB和LinearSVC都原生支持直接传入就行。有人习惯性调用.toarray()转成稠密矩阵几千封邮件的5000维特征矩阵转出来就是上亿个浮点数内存直接爆掉。这个坑我在早期项目里踩过一次从那以后凡是给sklearn传稀疏矩阵我都会先看一眼矩阵类型。3.2 SVM核选择为什么垃圾邮件场景默认线性核SVM在文本分类里用的是LinearSVC也就是线性核SVM。原理上SVM找一个超平面把两类样本分开超平面到两侧最近样本的间隔最大化间隔越大泛化能力越强。文本特征经过TF-IDF变换后维度高且稀疏绝大多数特征维度的值是0样本在这个高维空间里往往是线性可分的——线性核已经足够不需要RBF核再往更高维映射。RBF核在文本场景的问题在于两个样本的TF-IDF向量里共同出现的词通常很少核函数计算出来的相似度趋近于0整个核矩阵的信息量非常低模型花大量时间在算一堆接近常数的相似度上调gamma参数更像是在翻车边缘试探。如果一定要上RBF核第一步应该对TF-IDF矩阵做L2归一化再做交叉验证调整C和gamma但文本分类场景里这么折腾的收益通常不如直接换LinearSVC来得实在。这个工程的SVM分支就是线性核路线也符合主流做法。3.3 训练与对比代码Pipeline封装与关键参数训练阶段我把向量化和分类器装进同一个Pipeline这样做最大的好处是预测新邮件时不用手动记住先transform再predict的调用顺序一个pipeline.predict()全搞定。import joblib from sklearn.pipeline import Pipeline from sklearn.naive_bayes import MultinomialNB from sklearn.svm import LinearSVC from sklearn.model_selection import cross_val_score # 复用上一章的vectorizer封装成两个Pipeline nb_pipeline Pipeline([ (tfidf, vectorizer), (clf, MultinomialNB(alpha1.0, fit_priorTrue)), ]) svm_pipeline Pipeline([ (tfidf, vectorizer), (clf, LinearSVC(C1.0, losssquared_hinge, class_weightbalanced)), ]) # 五折交叉验证看的是稳定性和泛化能力不是单次切分的运气 for name, pipe in [(MultinomialNB, nb_pipeline), (LinearSVC, svm_pipeline)]: scores cross_val_score(pipe, X, y, cv5, scoringf1) print(name, F1均值:, round(scores.mean(), 4), 标准差:, round(scores.std(), 4))这里几个参数是文本分类场景的常用起点。alpha1.0是拉普拉斯平滑系数防止某个词在训练集的某个类别里没出现过、导致条件概率算成0一般从1.0起步验证集结果波动大再在0.1到5之间搜索。fit_priorTrue让模型从训练数据学习先验概率如果样本是重新人工采样过的、正负比例已经改了可以设成False用均匀先验。LinearSVC的C1.0是正则化强度的逆参数C越大越容易过拟合训练数据垃圾邮件场景下C从1调高到10验证集指标通常变化不大训练耗时却明显上涨。class_weightbalanced让模型按类别频率自动加权专门应付正负样本比例不均衡这条在垃圾邮件数据集上几乎必开。调参不用一上来就上大网格搜索先用小范围确定参数量级from sklearn.model_selection import GridSearchCV param_grid { clf__alpha: [0.1, 0.5, 1.0, 2.0], } grid GridSearchCV(nb_pipeline, param_grid, cv5, scoringf1) grid.fit(X_train, y_train) print(最优alpha:, grid.best_params_)Pipeline参数名里双下划线的含义是子步骤的参数入口clf__alpha表示Pipeline中名为clf的步骤的alpha参数。alpha超过2.0后平滑过度特征之间的差异会被抹平模型反而变笨。4. 模型评估与对比只看准确率会得出完全相反的结论4.1 垃圾邮件识别里精确率与召回率为什么那么重要如果样本中垃圾邮件占20%写一个每次都返回正常邮件的模型准确率也有80%——这就是准确率在类别不均衡场景下失效的原因。垃圾邮件识别要看的指标是精确率和召回率精确率Precision是模型判为垃圾的邮件里真的是垃圾的比例召回率Recall是所有真正的垃圾邮件里被模型拦下来的比例。两个指标在业务上是有冲突的。放到邮件场景里误杀一封正常询盘邮件精确率下降比漏掉一封广告邮件召回率下降代价大得多。所以工程实践里我会更偏重精确率宁肯放走几封广告也不要让客户的重要邮件进了垃圾箱。这个偏好不需要改模型后面用阈值就能控制。工程里如果报告只给了一个准确率那这份评估基本等于白做。4.2 从混淆矩阵到ROC曲线读结果的具体代码评估代码要一次把三个东西都打印出来混淆矩阵、分类报告、ROC曲线。混淆矩阵看错误类型分类报告看精确率和召回率ROC-AUC看模型整体区分能力。from sklearn.metrics import confusion_matrix, classification_report, roc_curve, auc y_pred svm_pipeline.predict(X_val) cm confusion_matrix(y_val, y_pred) print(混淆矩阵 [TN FP; FN TP]:, cm.tolist()) print(classification_report(y_val, y_pred, target_names[ham, spam])) # LinearSVC有decision_function可以直接拿置信分数画ROC scores svm_pipeline.decision_function(X_val) fpr, tpr, thresholds roc_curve(y_val, scores) print(ROC-AUC:, round(auc(fpr, tpr), 4))混淆矩阵的排列顺序是[TN, FP; FN, TP]。如果FN很大说明垃圾邮件大量漏网业务上垃圾邮件泛滥如果FP很大说明正常邮件被误杀业务上客户投诉爆表。ROC-AUC和阈值无关它衡量的是模型把所有垃圾邮件的分数排在正常邮件前面的能力适合用来比较朴素贝叶斯和SVM谁的整体区分力强。注意LinearSVC有decision_function方法返回的是每个样本到超平面的带符号距离画ROC直接用这个分数不需要调predict。MultinomialNB没有decision_function要改用predict_proba取正类概率列。4.3 最终选型与对比表两个模型都训练完成后用表格综合对比模型训练速度预测速度稀疏矩阵支持典型场景关键参数MultinomialNB极快极快原生支持词频统计类文本、快速基线alpha平滑系数LinearSVC较快快原生支持高维稀疏特征、需要边界清晰C、class_weightBernoulliNB快快原生支持只关心词是否出现的场景binarize阈值选型结论不是哪个算法赢了而是场景决定算法优先级。如果业务偏向不能漏掉垃圾邮件优先调SVM的C值并观察召回率如果偏向不能误杀正常邮件用朴素贝叶斯配合调高阈值更省心。SVM在这个工程里的定位是给朴素贝叶斯做交叉验证对照两个模型的F1差异通常在一个百分点以内关键区别在训练速度和推理速度——LinerSVC在样本量上万后优势明显朴素贝叶斯在数据集小时更稳。5. 常见问题与避坑预处理、样本不均衡与过拟合排查5.1 训练准确率99%新来的垃圾邮件一条没拦住现象验证集上F1高得吓人模型上线后新到的垃圾邮件几乎全部漏判。原因最常见的两个原因——停用词表过重把垃圾邮件的高频特征词全部滤掉了或者训练集和验证集里有完全重复的邮件验证时等于在背答案。工程里如果爬取邮件时没做去重同一封被转发多次的邮件会同时出现在训练集和验证集准确率虚高超标。解决先对所有邮件正文做哈希去重完全相同的文本只保留一条然后检查停用词表里是不是误加了点击免费活动这类在垃圾邮件里具有强区分度的词最后如果样本带时间戳按时间排序做前80%后20%的切分这样验证集更贴近用过去预测未来的真实场景。5.2 验证集分数虚高一上真实邮件就露馅现象交叉验证F1有0.95部署后对实时邮件预测效果很差。原因向量化环节在验证集上又做了一次fit_transform。fit_transform会重新学习词表和IDF权重等于模型变相偷看了验证集的文本分布这在机器学习里叫数据泄露是最容易犯的错误之一。工程里特征构造那章的代码如果写成X vectorizer.fit_transform(df[cutted])后面预测新邮件时又写X_new vectorizer.fit_transform(X_new)就中招了。解决坚持Pipeline流程——训练阶段只fit_transform一次之后所有预测统一用transform方法。Pipeline封装的另一个好处就是把这两步绑定在一起减少手动误操作的空间。检查方式很简单在验证集上分别跑一次fit_transform和transform两份特征矩阵如果差异很大就说明泄露已经发生了。5.3 中文邮件大量乱码特征矩阵里全是问号现象词表里出现一堆??或者jieba分词切出来的全是乱码字符模型准确率跌到50%附近接近瞎猜。原因邮件来源不同编码也不同。旧系统导出的邮件可能是GBK或GB18030编码新版爬虫抓的是UTF-8pd.read_csv默认按UTF-8读遇到GBK内容的行就解成乱码或直接报解码错误。编码不统一在邮件数据集里特别常见因为垃圾邮件发送方经常用不同编码逃避过滤。解决读取时统一指定编码工程里我建议读取CSV时用encodingutf-8-sig碰到解码失败的行用errorsignore跳过并在日志里记录数量更稳的做法是先抽几行文本用chardet检测编码确认数据集的主编码后再全量读取。乱码样本占比超过5%时不要硬跑模型先修数据。5.4 正常邮件误杀率高阈值却不知道往哪调现象模型把大量正常邮件丢进垃圾箱业务方直接找过来。原因默认分类边界是0即决策函数值大于0判为垃圾小于0判为正常。这个0是数学上的最优分界但不一定是业务上的最优分界。如果实际业务更讨厌误杀正常邮件就应该把阈值往上提。解决用ROC曲线选阈值而不是拍脑袋。用第4章的roc_curve返回的thresholds数组逐个看每个阈值下的精确率和召回率找到精确率大于0.95的那个阈值点存成配置文件。写代码时不要把阈值硬编码在模型里而是作为predict函数的一个参数传进去。5.5 SVM训练超过十分钟换LinearSVC三秒结束现象SVM训练时间长得能去泡杯咖啡模型文件还特别大。原因用了sklearn.svm.SVC而不是LinearSVC且默认核是RBF。SVC的复杂度在样本数平方到立方之间几千封邮件勉强能跑上万封就开始让人怀疑人生RBF核在高维稀疏特征上算出来的核函数值趋向常数训练效率低。解决文本分类的第一选择永远是LinearSVC它底层是liblinear实现专门优化线性SVM在大规模稀疏数据上的求解速度。两个API虽然都叫SVM但SVC和LinearSVC的默认行为完全不同。工程里如果遇到训练慢优先检查是不是把这两个类搞混了。6. 部署成命令行检测工具模型持久化与单封邮件打分6.1 持久化Pipeline而不是单独保存向量化器模型训练完后我会把整个Pipeline用joblib.dump持久化成一个文件而不是分别保存向量化器和分类器。分开保存意味着预测时要手动保证词表顺序和模型权重对应一旦词表改动过新邮件向量化的特征列和模型期望的顺序错位预测结果就是一个静默错误比重训练更难排查。Pipeline打包保存后这些都是内部状态预测时不需要关心。import joblib import jieba # 训练完成后一次保存预测时一次性加载 joblib.dump(svm_pipeline, models/spam_pipeline.joblib)6.2 命令行检测脚本与阈值自检下面这个脚本是部署的核心读取一封邮件正文输出判定结果和置信分数。import sys import joblib import jieba pipe joblib.load(models/spam_pipeline.joblib) with open(stopwords.txt, r, encodingutf-8) as f: stop_words set(line.strip() for line in f if line.strip()) def predict(text, threshold0.0): words .join(w for w in jieba.lcut(text) if w not in stop_words) score pipe.decision_function([words])[0] label spam if score threshold else ham return label, score if __name__ __main__: mail 恭喜您获得iPhone一部请点击链接填写收货信息活动长期有效 label, score predict(mail) print(判定:, label, 置信分:, round(score, 4))score来自decision_function正值偏向垃圾邮件负值偏向正常邮件绝对值越大置信度越高。上面这个例子恭喜点击活动这些特征词会直接把分数推到正区间。如果用的是MultinomialNBPipeline里没有decision_function要改成predict_proba(text)[0][1]取垃圾邮件的概率。脚本里的threshold参数可以直接从外部传入部署到邮件服务器时按实际投诉率调整比如把阈值调到0.5只拦截那些高置信度的垃圾邮件误杀率会明显下降代价是部分边缘垃圾邮件漏网。这个权衡没有标准答案取决于你的业务更怕哪种代价。我现在的习惯是每次改了特征或调了参数都拿一批真实的邮件文本跑一遍这个脚本把输出存成日志人工扫一遍判定结果再上线从不直接拿交叉验证的分数当最终结论。自己拆这个工程时踩过最深的坑就是验证集上好看、真实环境翻车从那以后每换一个数据集我都强制走一遍数据清洗、分层抽样、阈值自检这三个步骤虽然多花半小时但上线后的安全感完全不一样。希望这套流程和代码对你有用。本文还有配套的精品资源点击获取
返回列表