Python垃圾短信分类实战:从分词到朴素贝叶斯模型

📅 2026/8/27 16:29:29
Python垃圾短信分类实战:从分词到朴素贝叶斯模型
简介文本分类是自然语言处理中最基础也最常用的任务之一其核心在于将非结构化的文本转换为机器可理解的数值特征并通过分类器实现自动判别。在工程实践中常见的完整链路包括数据清洗、中文分词、特征提取、模型训练与效果评估。朴素贝叶斯作为经典概率分类算法凭借其在高维稀疏数据上的稳定表现成为文本分类的黄金选择而TF-IDF特征能有效抑制高频无意义词突出关键词的区分度。这类技术广泛用于垃圾邮件过滤、舆情分析、评论审核等场景。本文基于Python以垃圾短信分类为例深入讲解jieba分词、TF-IDF特征构建及朴素贝叶斯模型的应用细节并分享数据去重、样本不均衡处理、召回率优化等实战经验帮助读者从零跑通一个可落地的文本分类项目。 搞NLP入门项目的人十有八九都拿垃圾短信分类练过手。这个题目看着简单但实际做完你会发现它几乎把文本分类的完整流程全走了一遍数据清洗、中文分词、特征提取、模型训练、效果评估一个都没落下。而且它足够“轻”不需要GPU不需要海量数据一台普通笔记本就能跑通作为Python机器学习入门的综合练手项目非常合适。这篇文章就围绕“基于Python的垃圾短信分类”这个项目把我实际做的时候的思路、踩过的坑、调参的心得完整记录下来。代码不是重点重点是每一步为什么这么做以及遇到问题怎么排查。标题里的【100010111】说白了就是项目的版本号或编号方便自己归档用的真正核心的东西在后面。1. 项目整体思路与方案选型1.1 先把问题定义清楚这是一个标准的文本二分类任务别一上来就急着写代码先想清楚你要解决的问题是什么。垃圾短信分类输入是一条短信文本输出是“垃圾”或“正常”两个标签之一。这在机器学习里叫二分类问题而且是典型的文本分类。文本分类的难点在于计算机不认识文字它只认识数字。所以整个项目的核心链路就是把文本转成计算机能处理的数值特征再把这些特征喂给分类器让分类器学会区分垃圾和正常短信。目标定了后面所有步骤都围绕这条链路展开。数据准备、分词、特征工程、模型训练、评估每一环都是链条上的一环缺一个后面就跑不通。1.2 方案选型为什么我先不考虑深度学习现在一提文本分类很多人第一反应是BERT、LSTM这些深度学习模型。但对于这个项目我建议先把经典机器学习方案跑通原因有三点第一数据集规模小。常用的垃圾短信数据集大概几千到几万条这么小的数据量喂给深度学习模型很容易过拟合效果反而不如传统方法。第二硬件要求低。传统机器学习方案用CPU跑就很快几秒钟就能训练完而深度学习哪怕是小模型也得等一段时间。第三便于理解原理。朴素贝叶斯、逻辑回归这些模型的可解释性强你能清楚地看到哪些词对判断垃圾短信贡献最大。这对入门者理解整个NLP流程非常有帮助。我的选型是用“结巴分词 TF-IDF特征 朴素贝叶斯分类器”。这套组合是文本分类的黄金搭档尤其是朴素贝叶斯它在文本分类上的表现相当出色下一篇会细说原因。2. 数据处理与特征工程实操2.1 数据集获取与清洗别跳过这步数据是项目的灵魂。我用的数据集是公开的短信垃圾数据集主要有两个来源一个是UCI Machine Learning Repository的SMS Spam Collection另一个是一些开源的中文短信数据集。英文数据集处理起来简单中文数据集则需要额外做分词。拿到数据后先别急着做特征工程数据清洗这步非常关键。常见的清洗内容包括去除重复短信。同一个短信可能被多次发送如果不做去重模型会对这些重复样本过拟合。处理缺失值。检查是否有空行、NaN值有的话直接删除或填充。去除无意义字符。比如HTML标签、超链接、特殊符号这些对分类帮助不大可能还会引入噪声。标签统一。把“spam”和“ham”这类英文标签统一成“1”和“0”方便后续计算。我当时踩了一个坑就是没有去重结果模型在训练集上准确率高达99%但测试集上只有85%。后来一查发现大量重复短信同时出现在训练集和测试集里模型“记住”了这些样本而不是“学会”了泛化。去重之后测试集准确率反而稳定在了97%左右这才是真实的水平。2.2 中文分词与停用词处理jieba是个好工具中文文本和英文最大的区别就是没有空格词语之间没有天然的分隔符。所以中文NLP的第一步永远是分词。我用的是jieba分词库它是目前Python生态里最常用的中文分词工具。关于分词有个很重要的细节行业术语和特殊词汇需要手动添加。比如“中奖”、“信用卡”、“代开发票”这类垃圾短信高频词如果词典里没有jieba可能会把它们切成“代开”和“发票”这就破坏了这个词的整体语义。import jieba # 添加自定义词汇让分词更准确 jieba.add_word(信用卡) jieba.add_word(代开发票) jieba.add_word(中奖) text 恭喜您获得信用卡提额资格请点击链接领取 words jieba.lcut(text) print(words) # 输出[恭喜, 您, 获得, 信用卡, 提额, 资格, , 请, 点击, 链接, 领取]分词完成后要做停用词过滤。停用词就是“的”、“了”、“是”、“在”这类没有实际含义的虚词它们对分类没有帮助反而会增加特征维度。需要一个停用词表把分词结果中的停用词过滤掉。这里有一个实际经验停用词表不需要追求大而全够用就行。网上有很多开源的停用词表下载一个常用的再根据垃圾短信的特点补充几个高频干扰词比如“嗯”、“啊”等语气词效果就不错。千万别花大量时间在整理停用词表上收益很低。2.3 TF-IDF特征构建让重要的词浮出水面分词和停用词过滤之后短信文本变成了一串有意义的词语列表。下一步是要把这些词语转成数值向量。最基础的方法是词袋模型就是统计每个词在文档中出现的次数。但词袋模型有一个问题像“的”、“是”这样的词在文本中出现的频率很高但实际区分垃圾短信的能力很弱。而“中奖”、“免费”这类词虽然出现次数少但往往标志着垃圾短信。所以需要一种特征表示方法既能反映词语在文档中的重要性又能抑制高频但不重要的词语。TF-IDF解决的就是这个问题。它由两部分组成TF词频一个词在文档中出现的次数反映词对文档的重要程度。 IDF逆文档频率一个词在所有文档中出现的次数出现次数越多说明这个词越共通区分能力越弱。from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer(max_features5000, ngram_range(1, 2)) X vectorizer.fit_transform(corpus) print(X.shape) # 输出(样本数, 特征数)这里我设置了一两个关键参数max_features5000限制特征维度控制模型复杂度。如果特征太多容易过拟合训练也慢。ngram_range(1, 2)保留单个词和相邻两个词的组合。这样既能兼顾“免费”这种单个关键词又能识别“立即回复”这类词组。3. 模型训练与评估细节3.1 朴素贝叶斯为什么在文本分类上表现这么好说起朴素贝叶斯很多人的第一反应是“它有一个很强的假设特征之间相互独立”。但文本里词与词之间明显是有联系的比如“免费”和“领取”经常出现在一起这种情况下为什么朴素贝叶斯的表现还能这么好几个原因分类任务往往只需要一个“足够好”的决策边界而不需要精确的概率估计。即使概率估计有偏差只要各类别之间的相对大小差不多分类结果依然正确。规则的词汇分布让朴素贝叶斯在小样本下表现稳定不需要大量数据就能学到不错的参数。特征维度高但数据稀疏时朴素贝叶斯那种“各维度独立计算”的方式反而规避了维度灾难问题。当然这个独立假设确实是它的软肋。如果两个特征相关性太强概率估计会被夸大。但在垃圾短信分类这个场景下它的表现依然是王级别的。3.2 实操过程训练、评估与调参的一个完整流程我习惯的做法是把数据集按照70%训练、30%测试的比例切分然后先用一个默认参数的朴素贝叶斯模型跑一遍看看效果再逐步调参优化。from sklearn.model_selection import train_test_split from sklearn.naive_bayes import MultinomialNB from sklearn.metrics import classification_report, confusion_matrix # 数据切分 X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.3, random_state42) # 模型训练 model MultinomialNB() model.fit(X_train, y_train) # 预测与评估 y_pred model.predict(X_test) print(classification_report(y_test, y_pred))注意随机种子random_state的值要固定。我调试的时候试过好几个不同的值发现有些种子切分出来的数据会导致准确率上下浮动1%左右。固定种子之后结果可复现方便你对比不同参数的效果。3.3 评估指标别只看准确率垃圾短信分类要关注召回率二分类任务最直观的指标是准确率但垃圾短信分类场景下准确率高不一定代表模型好。举个例子如果100条短信里只有5条是垃圾短信那么模型只需要把全部短信都预测为正常准确率就有95%。但这个模型毫无用处。所以必须关注精确率、召回率和F1值精确率预测为垃圾短信的样本中真正是垃圾短信的比例。精确率低意味着误报多正常短信被当成垃圾清理了。召回率真正的垃圾短信中被正确找出来的比例。召回率低意味着漏报多垃圾短信漏进了收件箱。F1值精确率和召回率的调和平均综合评估模型性能。在垃圾短信场景中我更看重召回率。因为漏掉一条垃圾短信用户顶多烦一下但把一条正常短信拦下来可能就是错过一条银行验证码或者重要通知。所以宁可多拦几条可疑的也不能放过真正有威胁的。print(confusion_matrix(y_test, y_pred))混淆矩阵能清楚地展示四类情况真正例、假正例、真负例、假负例。我做完第一次实验发现假负例稍微偏多也就是垃圾短信漏判的情况比较多。后来通过调整阈值或者给垃圾短信类别加大权重召回率明显提上来了。4. 常见问题与排查技巧实录4.1 加载CSV数据时中文乱码这是中文NLP里最常遇到的问题。用pandas读取CSV时经常会碰到各种乱码问题。第一次碰到的朋友可能一脸懵其实绝大多数都是编码格式不一致导致的。解决思路# 方法一指定正确的编码格式 import pandas as pd df pd.read_csv(sms_data.csv, encodingutf-8) # 方法二先用chardet检测文件编码 import chardet with open(sms_data.csv, rb) as f: result chardet.detect(f.read(10000)) print(result)注意一点CSV文件保存时选择的是UTF-8但Excel默认用ANSI或GBK编码打开再另存就会把编码悄悄改掉。所以当你拿到别人给的数据文件时先检测编码再读取能省很多麻烦。4.2 样本不均衡垃圾短信占比太少真实场景中垃圾短信的占比远低于正常短信有的数据集里垃圾短信可能只占10%左右。这种情况下模型倾向于把所有样本都预测为正常短信因为这样准确率已经很高了。处理样本不均衡的几种思路过采样复制垃圾短信样本让两类样本数量接近。简单粗暴但容易过拟合。欠采样从正常短信中随机抽一部分减少正常短信数量。简单但浪费数据。调整类别权重在sklearn模型里直接设置class_weightbalanced让模型更关注少数类样本。sklearn的多项式朴素贝叶斯没有直接提供class_weight参数所以我当时用了过采样的方式。如果你用逻辑回归或SVM直接用class_weight简单多了。4.3 分词结果里出现奇怪的词或数字我实际跑出来的分词结果里经常出现“10086”、“188”这类数字以及“QQ”、“VIP”这类英文缩写。这些词本身可能是有意义的比如“10086”是客服号码但垃圾短信也经常伪造客服号码。所以我没有把它们直接过滤掉而是作为特征保留。如果你发现分词结果里很多无意义的单字比如“啊”、“吧”、“呵”说明停用词表不够完整或者你的分词模式选得不对。jieba有全模式和搜索引擎模式但默认的精确模式最适合这种场景。不要为了追求分词速度用全模式会产生大量冗余词。4.4 模型效果很好但线上应用效果差这是很多实际应用的坑。训练集上准确率很高但拿到真实场景一测效果掉了一大截。根本原因很可能是训练数据分布和真实数据分布不一致也就是所谓的“分布漂移”。解决思路只有一个持续收集新的数据重新训练模型。垃圾短信是动态变化的上一轮的中奖诈骗短信过几个月可能就变成新的套路了。我的做法是搭一个简单的反馈闭环用户每标记一条垃圾短信就把它存到一个待标注池子里每周人工审核一次合入训练集重新训练。这个闭环看似简单但能解决很多生产环境的问题。模型不是一次训练完就一劳永逸的要持续迭代。5. 项目扩展与进阶方向5.1 从朴素贝叶斯到深度学习什么时候需要升级经典机器学习方案适合小规模场景和数据量但如果你想把准确率再往上推一个台阶或者面对的是大规模、高维度、口语化极强的短信文本深度学习模型可能更合适。我当时试过用TextCNN和LSTM作为对比实验。在公开数据集上深度学习模型的准确率比朴素贝叶斯高出约1%训练时间却多了几十倍。这个提升能否接受取决于你的场景对准确率的要求有多高。如果业务场景对精度极其敏感不妨使用预训练模型如BERT但在投入之前先想想数据量撑不撑得起这个大模型。数据量不够再好的模型也是白搭。5.2 规则模型工程上最稳的组合策略在很多实际工程中我习惯在模型前面加一层规则前置过滤。比如短信中含“退订回T”的短语或者是“106”开头的服务号发送的短信基本上可以直接标记为垃圾短信或营销短信。规则的好处是白盒、可控、即时生效。模型的输出万一不对规则层能兜底。我的建议是模型负责处理模糊情况规则负责处理明确模式两者结合既能控制误杀风险又能保持较好的准确率。5.3 部署上线用FastAPI包一个短信分类服务模型训练完了总不能一直跑在Jupyter Notebook里。可以写一个简单的FastAPI接口把模型和TF-IDF向量化器加载到内存里对外提供HTTP服务。from fastapi import FastAPI from pydantic import BaseModel import joblib app FastAPI() vectorizer joblib.load(tfidf_vectorizer.pkl) model joblib.load(sms_model.pkl) class TextItem(BaseModel): text: str app.post(/predict) def predict(item: TextItem): text_vec vectorizer.transform([item.text]) label model.predict(text_vec)[0] return {label: int(label)}这样前端只需要调一个接口就能判断短信是否为垃圾短信。整个服务内存占用不到200MB单核CPU就能扛住。5.4 几个可复用的优化小技巧用joblib代替pickle保存模型和向量化器joblib对大数组的序列化效率更高。TF-IDF向量化器一定要和模型一起保存预测新数据时重新fit会报错。短信内容预处理函数清洗、分词、停用词过滤要封装成一个独立模块训练和预测共用一套代码避免“线下准、线上偏”的问题。6. 项目复盘与个人经验做完这个项目有几个感悟跟大家分享一下。第一项目不在大在于把每个环节吃透。垃圾短信分类看似简单但它把NLP的完整链路都串起来了。你做一遍就知道数据不平衡怎么处理、特征怎么设计、模型怎么评估这些东西换个场景照样能用。第二不要迷信深度学习。以前我也是听说深度学习强什么任务都往上套。后来学多了发现选模型要看数据量、算力、场景需求。一个朴素贝叶斯能解决的问题非要用BERT纯属浪费资源。第三评估指标的选择直接决定你的模型方向。同样的数据集优化准确率和优化召回率做出来的模型完全不一样。就算同样是垃圾短信分类在“营销短信”和“诈骗短信”两个细分方向上评估指标侧重点也要有区别。第四工程能力和模型能力同样重要。一个能跑通的模型只是起点怎么把它变成稳定的服务怎么处理线上数据漂移这些才是真正拉开差距的地方。最后再说一点垃圾短信分类是NLP入门的一个很好的起点但它只是一个起点。做完这个项目可以去试试情感分析、主题分类、命名实体识别。换数据集换模型都是一套方法论。基础打牢了后面才能走得更远。本文还有配套的精品资源点击获取