
简介这份资源面向Python初学者、机器学习入门者以及需要完成课程设计的学生围绕垃圾短信分类这一经典文本分类任务提供了一套可复现的完整实现方案。资源包共17个文件包含2个Python脚本、3个CSV数据集、2份Word实验报告、若干MATLAB脚本及说明文档压缩包约6.19MB结构清晰便于对照学习。已有347人学习下载。方案完整覆盖从数据预处理到建模预测的全流程先对所有单词进行标准化再用Snowball方法提取词干接着通过TF-IDF将自然语言转换为数值特征向量最后采用逻辑回归完成分类建模与预测。读者可据此掌握文本清洗、特征工程与分类器调参的核心思路并借助随附的数据集与报告快速复现实验、理解每一步的输入输出适合作为课程设计参考或文本分类入门练手项目。1. 垃圾短信分类项目从数据清洗到模型落地的完整链路手机收件箱里躺着一条“您的话费积分即将清零点击链接兑换现金”另一条是“尊敬的客户您已获得 50 万免息额度”。这类文本短、噪声大、正负样本极度不平衡用关键词黑名单拦截对方把“积分”改成“积 分”就绕过去了。基于 Python 的垃圾短信分类项目编号 100010111要解决的就是这个问题用 TF-IDF 把短信文本转成向量再喂给朴素贝叶斯或线性模型做二分类最终给出可复现的训练与预测脚本。它适合正在做课程设计的学生、想补一个完整 NLP 小项目的入门者以及需要快速搭一个短信过滤原型的后端工程师。整套流程不依赖 GPU一台装了 Python 的普通笔记本就能跑完。2. TF-IDF 与分类器选型为什么不是词袋加关键词2.1 从词袋到 TF-IDF 的向量化逻辑短信分类的第一步是把文本变成数字。最朴素的做法是词袋模型Bag of Words统计每个词在短信里出现没出现、出现几次。但词袋有个明显问题像“的”“了”“您”这种高频词几乎每条短信都有它们对区分垃圾短信毫无帮助却会在向量里占很大权重。TF-IDF 的思路是给词加权——一个词在当前短信里出现越多TF 高同时在整个语料里出现越少IDF 高它就越有区分度。具体计算上TF 通常取词频本身或词频的对数IDF 用log(总文档数 / 包含该词的文档数)两者相乘得到权重。这样“积分”“额度”“点击链接”这类词会拿到高权重而“的”“您”被压到接近零。常见做法是用 scikit-learn 的TfidfVectorizer它把分词、去停用词、计算 IDF、归一化打包在一起几行代码就能完成向量化。需要留意的是中文分词。英文按空格切就行中文必须先用 jieba 之类的工具切词否则“积分兑换”会被当成一个整体泛化能力很差。我一般会在TfidfVectorizer里传入tokenizerjieba.lcut让分词和向量化串起来。2.2 朴素贝叶斯、线性 SVM 与逻辑回归的取舍向量化之后要选分类器。垃圾短信分类属于典型的短文本二分类样本量通常几千到几万条特征维度高但稀疏。这个场景下有三类模型值得考虑。朴素贝叶斯MultinomialNB是最常用的基线。它假设特征之间独立虽然这个假设在文本里明显不成立但实际效果往往不差训练速度极快适合先跑通流程。缺点是它对特征分布敏感如果 TF-IDF 权重没归一化好概率输出会偏。线性 SVMLinearSVC在高维稀疏特征上表现稳定边界清晰适合正负样本可分性较强的数据。它不直接输出概率需要配合CalibratedClassifierCV才能拿到置信度多一步封装。逻辑回归LogisticRegression介于两者之间能输出概率可解释性好系数可以直接看哪些词对判定垃圾短信贡献大。如果课程设计需要展示“哪些词最像垃圾短信”逻辑回归的coef_是最直观的。我的建议是先用朴素贝叶斯跑通全流程拿到基线准确率再用逻辑回归对比如果追求指标就上线性 SVM。三个模型切换只需要改一行代码没必要一开始就纠结。2.3 可复现的训练脚本与参数说明下面是一段完整的训练脚本覆盖读取数据、分词、向量化、训练、评估五个环节。数据格式假设是两列label0 正常1 垃圾和message短信原文。import pandas as pd import jieba from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.model_selection import train_test_split from sklearn.naive_bayes import MultinomialNB from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, confusion_matrix # 1. 读取数据假设文件为 sms.csv含 label 和 message 两列 df pd.read_csv(sms.csv, encodingutf-8) df df.dropna(subset[message]) # 丢掉空文本 df[message] df[message].astype(str) # 2. 中文分词jieba 默认精确模式 def cut(text): return .join(jieba.lcut(text)) df[cut_msg] df[message].apply(cut) # 3. 划分训练集和测试集stratify 保证正负比例一致 X_train, X_test, y_train, y_test train_test_split( df[cut_msg], df[label], test_size0.2, random_state42, stratifydf[label] ) # 4. TF-IDF 向量化限制最大特征数和 n-gram 范围 vec TfidfVectorizer( max_features5000, # 最多保留 5000 个词 ngram_range(1, 2), # 同时考虑单字和双字组合 min_df2, # 至少出现在 2 条短信里才保留 max_df0.9 # 出现在 90% 以上短信里的词丢掉 ) X_train_vec vec.fit_transform(X_train) X_test_vec vec.transform(X_test) # 注意测试集只 transform不 fit # 5. 训练朴素贝叶斯并评估 nb MultinomialNB(alpha0.1) # alpha 是平滑系数 nb.fit(X_train_vec, y_train) pred_nb nb.predict(X_test_vec) print(朴素贝叶斯结果) print(classification_report(y_test, pred_nb)) print(confusion_matrix(y_test, pred_nb)) # 6. 换逻辑回归对比 lr LogisticRegression(max_iter1000, C1.0) lr.fit(X_train_vec, y_train) pred_lr lr.predict(X_test_vec) print(逻辑回归结果) print(classification_report(y_test, pred_lr))这段脚本里几个参数值得展开。max_features5000控制特征维度短信语料通常不大5000 足够覆盖主要词汇设太大反而引入噪声。ngram_range(1, 2)让模型同时看单字和相邻双字“免息”和“免 息”都能被捕捉到。min_df2过滤只出现一次的词降低过拟合。max_df0.9把几乎每条短信都有的词剔除相当于自动停用词。alpha0.1是拉普拉斯平滑防止某个词在训练集没出现导致概率为零值越小越依赖数据值越大越平滑。测试集必须用transform而不是fit_transform否则 IDF 会从测试集重新统计造成信息泄露评估结果虚高。这是新手最容易翻车的地方之一。3. 数据清洗与样本不平衡垃圾短信分类的真实门槛3.1 短信文本的噪声类型与清洗策略真实短信数据远没有教科书里干净。常见噪声包括HTML 标签残留、URL 链接、连续空格和换行、全角半角混用、表情符号、以及“加威信”“V信”这类故意变形的词。如果不清洗TF-IDF 会把http://xxx和https://yyy当成两个完全不同的词浪费特征维度。清洗顺序一般是先去 HTML 标签和 URL再统一全角半角然后去掉多余空白最后做分词。URL 可以用正则替换成一个统一占位符比如__URL__这样模型能学到“含链接的短信更可能是垃圾短信”而不是记住具体域名。import re def clean_text(text): text re.sub(r[^], , text) # 去 HTML 标签 text re.sub(rhttps?://\S|www\.\S, __URL__, text) # URL 统一替换 text re.sub(r\s, , text) # 合并空白 text text.strip() return text df[clean_msg] df[message].apply(clean_text)这里把 URL 替换成__URL__而不是直接删除是因为“是否含链接”本身就是一个强特征。直接删掉等于丢掉了这个信号。同理手机号可以替换成__PHONE__金额替换成__MONEY__让模型学到模式而不是具体数字。3.2 正负样本比例失衡的处理方式垃圾短信数据集里正常短信通常远多于垃圾短信比例可能到 10:1 甚至更高。如果直接训练模型会倾向于把所有短信判成正常准确率看起来有 90%但垃圾短信的召回率极低实际不可用。处理不平衡有三条路。一是调整class_weight让模型对少数类更敏感。逻辑回归和 SVM 都支持class_weightbalanced它会按类别频率自动反比加权。二是过采样少数类比如 SMOTE但在文本高维稀疏空间里 SMOTE 效果不稳定容易生成无意义的插值样本。三是欠采样多数类简单但会丢信息。我一般优先用class_weightbalanced改动最小不引入合成样本。如果效果还不够再考虑对垃圾短信做数据增强比如同义词替换、随机插入但要注意别把语义改跑偏。lr_balanced LogisticRegression( max_iter1000, C1.0, class_weightbalanced # 按类别频率自动加权 ) lr_balanced.fit(X_train_vec, y_train) pred_bal lr_balanced.predict(X_test_vec) print(classification_report(y_test, pred_bal))评估时不能只看准确率。垃圾短信分类更关心垃圾类别的召回率和 F1因为漏掉一条垃圾短信的代价通常比误判一条正常短信高。classification_report里重点看 label1 那一行的 recall 和 f1-score。3.3 交叉验证与超参数搜索的落地写法单次 train_test_split 的结果有随机性换一个 random_state 指标可能波动几个点。要拿到稳定评估应该做交叉验证。TfidfVectorizer和分类器可以串成 Pipeline再用GridSearchCV搜参数这样向量化只在训练折上 fit避免泄露。from sklearn.pipeline import Pipeline from sklearn.model_selection import GridSearchCV pipe Pipeline([ (tfidf, TfidfVectorizer(tokenizerjieba.lcut, max_features5000)), (clf, LogisticRegression(max_iter1000, class_weightbalanced)) ]) params { tfidf__ngram_range: [(1, 1), (1, 2)], tfidf__min_df: [1, 2, 3], clf__C: [0.1, 1.0, 10.0] } grid GridSearchCV(pipe, params, cv5, scoringf1, n_jobs-1) grid.fit(df[message], df[label]) print(最佳参数, grid.best_params_) print(最佳 F1, grid.best_score_)cv5是五折交叉验证scoringf1表示以 F1 为优化目标。n_jobs-1用满所有 CPU 核心加速。注意 Pipeline 里TfidfVectorizer的tokenizer参数在较新版本 scikit-learn 里可能提示需要改用token_pattern或自定义分析器如果报错就换成在外部先分好词再传入。4. 避坑与排查垃圾短信分类最常见的五个翻车点4.1 测试集用了 fit_transform 导致指标虚高现象训练完模型测试集准确率 99%换一批新数据掉到 70%。原因测试集调用了fit_transformIDF 从测试集重新统计相当于提前看到了测试数据的分布。解决训练集fit_transform测试集只transform或者直接用 Pipeline 让框架管理。4.2 中文没分词直接喂给 TfidfVectorizer现象模型跑通了但特征里全是整句get_feature_names_out看到的都是长字符串。原因TfidfVectorizer默认按空格和标点切分中文没有空格整条短信被当成一个 token。解决传入tokenizerjieba.lcut或者提前分好词用空格连接再传入。4.3 停用词表把关键否定词删掉了现象正常短信“这个活动不收费”被判成垃圾短信。原因停用词表里包含“不”被删掉后语义反转。解决中文停用词表要慎用尤其是否定词、程度副词。我一般只删“的”“了”“呢”这类纯语气词保留“不”“没”“无”。4.4 类别标签不是 0/1 导致评估报错现象classification_report报 ValueError提示目标类型不支持。原因标签列是字符串“垃圾”“正常”而不是数字。解决训练前用LabelEncoder或map转成 0/1并记录映射关系预测时再转回去。4.5 模型保存后加载分词器没一起保存现象训练时准确率很高用joblib保存模型后重新加载预测结果全错。原因只保存了分类器没保存TfidfVectorizer新数据向量化方式不一致。解决把 vectorizer 和分类器一起打包保存或者用 Pipeline 整体joblib.dump。import joblib # 保存整个 Pipeline包含向量化和分类器 joblib.dump(grid.best_estimator_, sms_clf.pkl) # 加载后直接预测原始文本 clf joblib.load(sms_clf.pkl) print(clf.predict([您的话费积分即将清零点击链接兑换]))5. 从脚本到可用工具批量预测与置信度过滤的实战技巧训练脚本跑通只是第一步真正要用起来得能对一批新短信做预测并且知道哪些预测可信、哪些需要人工复核。这里分享两个我常用的技巧。第一个是批量预测。把待预测短信读成列表直接调用 Pipeline 的predict再用predict_proba拿概率。逻辑回归和朴素贝叶斯都支持概率输出线性 SVM 需要额外校准。拿到概率后可以设一个阈值区间比如概率在 0.4 到 0.6 之间的判为“不确定”转人工审核两头明确的自动处理。这样能显著降低误判带来的影响。import pandas as pd import joblib clf joblib.load(sms_clf.pkl) new_sms pd.read_csv(new_sms.csv, encodingutf-8) texts new_sms[message].astype(str).tolist() proba clf.predict_proba(texts)[:, 1] # 取垃圾类别的概率 pred clf.predict(texts) result pd.DataFrame({ message: texts, pred: pred, proba: proba, need_review: (proba 0.4) (proba 0.6) # 不确定区间 }) result.to_csv(predict_result.csv, indexFalse, encodingutf-8-sig) print(result[need_review].sum(), 条需要人工复核)第二个技巧是看模型学到了什么。逻辑回归的系数可以直接映射回词正系数越大越像垃圾短信负系数越大越像正常短信。把 top 20 正系数词打印出来能快速判断模型有没有学到合理模式。如果排前面的全是“URL”“PHONE”这种占位符说明数据里链接和号码确实强相关如果排前面的是“的”“了”那说明停用词没处理好。import numpy as np vec clf.named_steps[tfidf] clf_model clf.named_steps[clf] feature_names vec.get_feature_names_out() coefs clf_model.coef_[0] top_spam np.argsort(coefs)[-20:][::-1] print(最像垃圾短信的词) for i in top_spam: print(feature_names[i], round(coefs[i], 3))这两个技巧配合使用基本能覆盖从训练到上线的最后一公里。我自己的习惯是每次换数据集先跑一遍 top 系数检查确认模型没走偏再去看指标。有一次偷懒跳过这步结果发现模型把“退订”学成了垃圾短信的强特征——因为训练集里垃圾短信末尾都带“退订回 T”而正常短信很少带。这种特征在真实场景里会失效因为正常营销短信也会带退订。从那以后我每次训练完都强制走一遍系数检查确认没有这种伪相关特征才继续。希望这些经验帮到你少走几个弯路。本文还有配套的精品资源点击获取