首页 / 资讯中心 / 文章详情

Python垃圾短信分类实战:从数据到模型全流程

Python垃圾短信分类实战:从数据到模型全流程 ★ FEATURED ARTICLE
简介本资源为基于Python的垃圾短信分类课程设计完整资料包面向正在学习机器学习、模式识别或自然语言处理的高校学生与自学者可帮助解决文本分类入门实践中从数据预处理到建模预测的全流程问题。包内共17个文件以m脚本、csv数据集、py源码、docx实验报告为主另含md说明与license文件压缩包约6.19MB目录结构清晰便于按模块查阅与复现。目前已有347人学习下载。资源围绕lintcode垃圾短信分类题目展开完整呈现了单词标准化、Snowball词干提取、TF-IDF特征向量转换以及逻辑回归建模预测的技术链路并附有模式识别作业报告与可视化脚本读者可据此理解文本分类的核心思路对照代码完成实验复现、撰写课程报告或迁移到相似的自然语言处理任务中。1. 垃圾短信分类到底在分什么从一条“中奖通知”说起你手机里大概率躺着这样一条短信“尊敬的客户您已获得XX平台幸运用户资格点击链接领取……”它和正常验证码、快递通知混在同一个收件箱里肉眼一眼能分辨但要让程序自动分辨就没那么简单了。基于Python的垃圾短信分类做的就是这件事把一条短信文本喂给模型输出“垃圾”或“正常”两个标签。它属于文本二分类任务核心难点不在模型多深而在中文短信短、口语化、夹杂符号和变体词特征稀疏得厉害。适合谁做刚学完Python基础语法、想找一个端到端NLP小项目练手的人也适合需要给业务系统加一道短信过滤兜底策略的工程师。这篇笔记按“数据怎么来→特征怎么提→模型怎么选→怎么评估→怎么避坑”的顺序讲每一步都给可复现的代码和参数说明你照着跑就能得到一个能用的分类器。2. 数据准备与中文分词把原始短信变成模型能吃的格式2.1 短信数据集长什么样从哪来公开的中文垃圾短信数据集不算多常见做法是找一份带标签的CSV两列label和message。label一般是ham正常和spam垃圾也有用0/1的。如果你手头没有现成数据我一般会先用几百条自己标注的短信跑通流程再逐步扩量。数据量上二分类任务起步建议至少3000条正负样本比例别太悬殊垃圾短信占比在15%到40%之间比较健康。如果垃圾样本太少后面评估指标会失真。拿到数据先做一次体检看缺失值、重复值和标签分布。重复短信在真实数据里很常见尤其是营销短信模板化严重不去重会导致训练集和测试集泄漏评估分数虚高。import pandas as pd # 读取数据假设文件是 utf-8 编码的 csv df pd.read_csv(sms.csv, encodingutf-8) # 统一列名方便后续处理 df.columns [label, message] # 看基本信息 print(df.shape) print(df[label].value_counts()) print(df.isnull().sum()) # 去重完全相同的短信只保留一条 df df.drop_duplicates(subset[message]).reset_index(dropTrue) # 去掉空短信 df df[df[message].str.strip().astype(bool)].reset_index(dropTrue) print(去重后:, df.shape)这段代码做了四件事统一列名、看标签分布、按短信内容去重、剔除空白行。drop_duplicates的subset参数指定只按message列判重因为同一条短信可能被标了不同标签那种情况要单独处理。去重后如果样本量掉得厉害说明原始数据模板化严重需要考虑数据增强或者换数据源。2.2 中文分词jieba的三种模式和自定义词典英文短信按空格切词就行中文不行。“免费领取”是一个词还是四个字直接影响特征质量。Python里做中文分词jieba是最常用的选择安装就一句pip install jieba。jieba有三种模式精确模式、全模式、搜索引擎模式。短信分类我一般用精确模式因为它切分结果最干净不会产生大量冗余碎片。但jieba的默认词典对网络新词和营销话术覆盖不够“薅羊毛”“秒杀”“返现”这类词可能被切碎。解决办法是加载自定义词典把领域词加进去。import jieba # 加载自定义词典每行一个词可带词频和词性可选 jieba.load_userdict(user_dict.txt) # 精确模式分词示例 text 恭喜您获得秒杀资格点击链接返现50元 words jieba.lcut(text, cut_allFalse) print(words) # 输出类似[恭喜, 您, 获得, 秒杀, 资格, , 点击, 链接, 返现, 50, 元] # 停用词表过滤掉对分类无意义的词 stopwords set() with open(stopwords.txt, encodingutf-8) as f: for line in f: stopwords.add(line.strip()) # 过滤停用词和单字 cleaned [w for w in words if w not in stopwords and len(w) 1] print(cleaned)load_userdict加载的词典文件格式是每行一个词可以只写词也可以写“词 词频 词性”。停用词表网上有很多现成的但建议自己过一遍把“的”“了”“啊”这类去掉同时注意别把“不”“没”这种否定词误删它们在短信里可能携带关键语义。过滤单字是因为单个汉字在短信里噪声太大保留双字及以上词效果通常更好。分词之后每条短信就变成了一个词列表。接下来要把词列表转成数值特征才能喂给模型。3. 特征工程TF-IDF和词袋模型怎么选、参数怎么调3.1 词袋模型与TF-IDF的差别词袋模型Bag of Words只统计每个词在短信里出现了几次完全不考虑词序。TF-IDF在词频基础上乘了一个逆文档频率降低那些在所有短信里都高频出现的词的权重。举个例子“您”在正常短信和垃圾短信里都常见TF-IDF会给它较低权重“返现”只在垃圾短信里高频出现TF-IDF会给它较高权重。所以短信分类我默认用TF-IDF效果通常比纯词袋好一截。scikit-learn的TfidfVectorizer把分词、过滤、向量化串在一起但中文需要先自己分词再传进去或者传一个自定义的tokenizer。我一般先分好词存成一列再用TfidfVectorizer的analyzer参数配合lambda处理。from sklearn.feature_extraction.text import TfidfVectorizer # 假设 df[words] 是分好词后用空格拼接的字符串 df[words] df[message].apply(lambda x: .join(jieba.lcut(x))) vectorizer TfidfVectorizer( max_features5000, # 最多保留5000个词控制维度 min_df2, # 至少在2条短信里出现才保留 max_df0.9, # 出现在超过90%短信里的词丢掉 ngram_range(1, 2), # 同时考虑单字词和双字词组合 sublinear_tfTrue # 对词频做对数平滑抑制超高频词 ) X vectorizer.fit_transform(df[words]) y df[label].map({ham: 0, spam: 1}) print(X.shape) # (样本数, 特征维度)max_features5000是维度和信息量的折中短信短5000个特征通常够用。min_df2过滤掉只出现一次的词减少噪声。max_df0.9去掉近乎全量的词比如“我”“你”。ngram_range(1,2)让模型能捕捉“点击链接”这种双词组合对短信分类有正向帮助但会增大特征维度配合max_features一起用。sublinear_tfTrue把词频从线性变成对数避免某个词重复出现十几次就主导权重。3.2 参数调整的实操判断调参不是盲调要看验证集表现。我一般先固定max_features5000然后单独调ngram_range从(1,1)到(1,2)看F1有没有提升。如果提升不明显就退回(1,1)省内存。min_df从1调到2再调到3观察过拟合有没有缓解。max_df一般不动0.9是个安全值。还有一个容易忽略的点TF-IDF必须在训练集上fit然后对测试集只做transform。如果全量数据一起fit测试集的词频信息就泄漏到训练过程里了评估分数会偏高。正确做法是先切分数据再分别处理。from sklearn.model_selection import train_test_split X_train_text, X_test_text, y_train, y_test train_test_split( df[words], y, test_size0.2, random_state42, stratifyy ) # 只在训练集上 fit vectorizer TfidfVectorizer(max_features5000, min_df2, max_df0.9, ngram_range(1, 2), sublinear_tfTrue) X_train vectorizer.fit_transform(X_train_text) X_test vectorizer.transform(X_test_text)stratifyy保证切分后正负样本比例和原始一致短信分类里垃圾样本少的时候尤其重要。random_state42固定随机种子方便复现。切分比例8:2是常规起点数据量过万可以调到9:1。4. 模型训练与评估朴素贝叶斯、SVM和逻辑回归的实战对比4.1 三个基线模型怎么选短信分类属于高维稀疏文本分类朴素贝叶斯、线性SVM和逻辑回归是三个最常用的基线。朴素贝叶斯假设特征独立虽然这个假设在文本里明显不成立但它计算快、对小数据友好经常能给出不错的基线。线性SVM在高维空间找最大间隔文本分类里表现稳定。逻辑回归输出概率方便后续调阈值。我一般三个都跑一遍用交叉验证看F1再决定用哪个。不要一上来就上深度学习几千条短信的数据量BERT微调未必比TF-IDF加线性模型好而且训练成本高得多。from sklearn.naive_bayes import MultinomialNB from sklearn.svm import LinearSVC from sklearn.linear_model import LogisticRegression from sklearn.model_selection import cross_val_score import numpy as np models { NB: MultinomialNB(alpha1.0), SVM: LinearSVC(C1.0, max_iter5000), LR: LogisticRegression(C1.0, max_iter1000) } for name, model in models.items(): scores cross_val_score(model, X_train, y_train, cv5, scoringf1) print(f{name}: F1{scores.mean():.4f} (/- {scores.std():.4f}))MultinomialNB的alpha是平滑参数默认1.0数据稀疏时可以调到0.1到0.5。LinearSVC的C控制正则强度C越大越容易过拟合短信分类我一般从1.0开始试。LogisticRegression的C同理max_iter要设大一点否则可能不收敛。cross_val_score的cv5做五折交叉验证scoringf1关注正类垃圾短信的F1因为业务上更怕漏判垃圾短信。4.2 评估指标为什么准确率会骗人如果垃圾短信只占10%一个把所有短信都判为正常的模型准确率也有90%但它一条垃圾短信都抓不到。所以短信分类不能只看准确率要看精确率、召回率和F1。精确率是“判为垃圾的里面有多少真的是垃圾”召回率是“真的垃圾里面有多少被找出来了”。业务上如果不想误拦正常短信就优先保精确率如果不想漏掉垃圾短信就优先保召回率。from sklearn.metrics import classification_report, confusion_matrix # 用SVM做示例 svm LinearSVC(C1.0, max_iter5000) svm.fit(X_train, y_train) y_pred svm.predict(X_test) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred, target_names[正常, 垃圾]))confusion_matrix输出四个数真正常、假垃圾、假正常、真垃圾。classification_report给出每个类的精确率、召回率、F1和支持度。重点看“垃圾”那一行的召回率如果低于0.85说明漏判较多需要调整模型或特征。如果精确率低说明误拦多可以调高分类阈值或增加正常短信的样本权重。4.3 模型持久化与推理封装训练完的模型和向量器要存下来不然每次推理都重新训练不现实。用joblib保存加载后封装成一个预测函数。import joblib # 保存 joblib.dump(vectorizer, tfidf.pkl) joblib.dump(svm, svm_model.pkl) # 加载并推理 vectorizer joblib.load(tfidf.pkl) model joblib.load(svm_model.pkl) def predict_sms(text): words .join(jieba.lcut(text)) vec vectorizer.transform([words]) pred model.predict(vec)[0] return 垃圾 if pred 1 else 正常 print(predict_sms(恭喜您中奖了点击领取))推理时注意分词和向量化必须和训练时完全一致包括自定义词典和停用词表。如果训练时用了user_dict.txt推理时也要加载同一个词典否则分词结果不一致特征对不上预测就会出错。这个坑我踩过模型离线评估很好上线后效果差一截排查半天才发现是词典没同步。5. 避坑与排查短信分类项目里最容易翻车的五个地方5.1 数据泄漏去重没做导致分数虚高现象交叉验证F1到0.98上线后实际只有0.7左右。原因训练集和测试集里有大量重复短信模型记住了这些样本评估时等于开卷考试。解决切分数据之前先按短信内容去重切分时用stratify保持标签比例并且确保同一条短信不会同时出现在训练集和测试集。如果数据本身模板化严重可以考虑按模板分组切分。5.2 分词不一致训练和推理用了不同词典现象离线评估正常线上预测结果随机。原因训练时加载了自定义词典推理服务没加载同一个词被切成了不同片段TF-IDF特征对不上。解决把分词配置词典路径、停用词表、jieba版本固化到配置文件里训练和推理共用同一份。更稳妥的做法是把分词结果和向量化打包成一个Pipeline用joblib整体保存。5.3 类别不平衡垃圾样本太少导致召回率低现象模型把大部分短信判为正常垃圾短信召回率不到0.5。原因垃圾样本占比太低模型倾向于预测多数类。解决三种做法——对垃圾样本过采样、对正常样本欠采样、或者在模型里设class_weightbalanced。逻辑回归和SVM都支持class_weight参数设置后模型会自动调整权重。过采样可以用imblearn的RandomOverSampler但要注意只在训练集上做别动测试集。5.4 新词和变体词模型没见过就抓不住现象垃圾短信里出现“薇信”“扣扣”这种谐音变体模型判为正常。原因训练数据里没有这些变体TF-IDF词典里也没有模型完全没见过。解决维护一个变体词映射表在分词前做归一化替换比如把“薇信”替换成“微信”。另外定期用新数据增量训练让词典覆盖新出现的营销话术。这个没有一劳永逸的办法只能持续迭代。5.5 阈值默认0.5业务需求不同阈值要调现象模型输出的概率在0.4到0.6之间时判为垃圾还是正常很模糊。原因默认分类阈值是0.5但业务上可能更怕误拦或更怕漏判。解决用predict_proba拿到概率后自己设阈值。比如宁可误拦也不漏判就把阈值降到0.3宁可漏判也不误拦就升到0.7。阈值要在验证集上按业务指标调不能拍脑袋。# 以逻辑回归为例调整阈值 lr LogisticRegression(C1.0, max_iter1000, class_weightbalanced) lr.fit(X_train, y_train) proba lr.predict_proba(X_test)[:, 1] for thresh in [0.3, 0.4, 0.5, 0.6, 0.7]: y_pred_thresh (proba thresh).astype(int) print(f阈值{thresh}) print(classification_report(y_test, y_pred_thresh, target_names[正常, 垃圾]))这段代码遍历几个阈值分别输出评估报告你可以根据业务需求选一个最合适的。注意class_weightbalanced会让模型更关注少数类配合阈值调整效果更明显。6. 进阶技巧用Pipeline串起全流程和增量更新策略把分词、向量化、模型训练串成一个Pipeline能避免很多手动同步的坑。scikit-learn的Pipeline支持自定义转换器你可以把jieba分词封装成一个Transformer。from sklearn.base import BaseEstimator, TransformerMixin from sklearn.pipeline import Pipeline class JiebaTokenizer(BaseEstimator, TransformerMixin): def __init__(self, user_dictNone): self.user_dict user_dict def fit(self, X, yNone): if self.user_dict: jieba.load_userdict(self.user_dict) return self def transform(self, X): return [ .join(jieba.lcut(text)) for text in X] pipe Pipeline([ (tokenizer, JiebaTokenizer(user_dictuser_dict.txt)), (tfidf, TfidfVectorizer(max_features5000, min_df2, ngram_range(1, 2), sublinear_tfTrue)), (clf, LogisticRegression(C1.0, max_iter1000, class_weightbalanced)) ]) pipe.fit(X_train_text, y_train) print(pipe.score(X_test_text, y_test)) # 整体保存推理时直接加载 joblib.dump(pipe, sms_pipeline.pkl)Pipeline的好处是fit和predict的输入都是原始短信文本分词和向量化在内部自动完成不会出现训练和推理不一致的问题。保存时整个Pipeline一起序列化加载后直接predict原始文本即可。增量更新方面短信话术变化快模型不能一劳永逸。我一般每季度用新标注的数据重新训练一次或者用partial_fit做在线学习。但partial_fit对TF-IDF不友好因为词典会变。更实际的做法是定期全量重训把新数据合并进训练集重新fit向量器和模型。重训前记得在新数据的验证集上评估确认没有退化再上线。最后说一个我自己的习惯每次模型上线前我会手动构造20条“刁钻”短信——包含谐音变体、夹杂符号、中英混合、超短文本——跑一遍预测看有没有明显翻车。这个习惯帮我提前发现过好几次词典缺失和阈值不合理的问题。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站