资讯详情

资讯详情

用SVM构建垃圾短信识别系统:中文文本分类从入门到实战

简介这套基于机器学习支持向量机SVM的垃圾短信识别系统源码面向计算机相关专业学生与开发者用于解决短信自动分类与过滤问题适合作为课程设计、毕业设计或大作业的完整参考。压缩包约107.89MB以zip格式封装内含数据预处理、SVM训练、消息预测等Python模块并附带带标签训练数据、无标签测试数据以及详细的设计报告覆盖从文本特征提取、TF-IDF计算到模型训练与在线预测的完整流程。项目代码经验证可运行目录结构清晰model、Data、code及SPAM_CLASSIFY_online等功能模块划分明确可直接部署于Apache环境进行在线预测便于读者按模块学习或二次开发。已有289人学习使用既能帮助快速入门机器学习文本分类也可支撑课程答辩与项目拓展是一份兼顾教学演示与实际应用的优质课程设计资源。1. 垃圾短信识别为什么选 SVM一份能交差的课程设计核心不在模型很多人在课程设计里一上来就想着用深度学习但拿到“垃圾短信识别”这个题目SVM支持向量机反而是更稳的选择。它不需要 GPU、不需要海量数据只要把短信处理成向量用 scikit-learn 就能跑出 90% 以上的准确率。你要交的是一份能答辩、能复现、能讲清原理的完整项目而不是一个跑不动的黑匣子。这个项目方向之所以经典是因为它把中文分词、特征提取、分类器训练、模型评估这几个机器学习核心环节全串起来了每个环节都有明确的输出物。我接下来给你拆一条可以直接照做的落地路径从数据处理到模型打包重点讲参数选择和踩坑点确保你拿到源码后改一改就能跑通。2. 把短信变成向量中文分词、停用词与 TF-IDF 特征的正确姿势2.1 不做分词直接喂字频是新手最常见的翻车点文本不能直接喂给 SVM这是基础常识。但很多课程设计代码里为了省事直接拿字频当特征结果就是“中奖”“中奖啦”“恭喜您中奖”这三条短信被切成完全不同的特征向量模型学不到“中奖”这个关键信号。中文不像英文有天然空格必须先做分词。常见做法是使用 jieba 分词库它是目前课程设计里最常见的方案。注意这里要区分字符级特征和词级特征字符级特征在短文本场景下偶尔有效但对“免费”“贷款”“加微信”这类垃圾短信里的强词词级特征更符合人的判断逻辑SVM 的稀疏高维特征空间也能更好地利用词频统计。分词之后还要去停用词像“的”“了”“吗”这类词在垃圾短信分类里几乎没有区分度。网络上流传的停用词表很多但你需要针对短信场景做加减法。比如“”“【】”这类符号在垃圾短信里反而是强特征不能从停用词表里删掉甚至要单独保留。“免费”这个词在很多通用停用词表里没有但它是垃圾短信的高频词。所以我会先加载一个基础停用词表然后手动把短信语料里出现频率高但和分类无关的词加进去。这个过程要写在代码注释里答辩时老师问起来你也能说清楚。2.2 基于 jieba 的中文预处理脚本与参数说明下面这段代码是文本预处理的最小实现。你可以直接放到项目的preprocess.py里后面训练脚本直接 import 它。import re import jieba # 加载停用词表按行读取去掉空白字符 def load_stopwords(pathstopwords.txt): stopwords set() with open(path, r, encodingutf-8) as f: for line in f: w line.strip() if w: stopwords.add(w) return stopwords # 清洗短信文本去掉URL、数字、连续标点但保留中文和感叹号 def clean_text(text): # 去除链接 text re.sub(rhttp[s]?://\S, , text) # 去除手机号、QQ号等连续数字 text re.sub(r\d, , text) # 只保留中文、英文字母、感叹号、问号、空格 text re.sub(r[^\u4e00-\u9fa5a-zA-Z], , text) return text # 分词并过滤停用词和单字 def tokenize(text, stopwords): text clean_text(text) words jieba.lcut(text) return [w for w in words if w not in stopwords and len(w.strip()) 1]这里的逻辑说明clean_text先处理噪声。垃圾短信里经常混着链接、电话、验证码这些数字和 URL 对判断“是不是垃圾”没有帮助反而会增加特征维度。正则里\u4e00-\u9fa5是 Unicode 中文范围被显式保留因为很多垃圾短信用连续感叹号制造紧迫感。tokenize里过滤单字是因为单字在短文本里噪声很大比如“你”“我”“是”这类字即使不在停用词表里也会让向量变得稀疏。参数层面的关键在于 jieba 的三种分词模式。默认的jieba.lcut是精确模式适合短文本分类不要用jieba.cut_for_search它会多分出很多冗余词导致特征维度膨胀。如果你的电脑上跑起来很慢可以给jieba.lcut加use_paddleTrue这个参数需要安装 paddlepaddle一般课程设计不推荐因为会引入额外依赖。用默认的就够了。2.3 TF-IDF 里两个必须调的参数max_features 与 ngram_range分词完之后下一步就是把词列表转成 TF-IDF 特征。这里最容易翻车的是直接调用TfidfVectorizer默认参数不管文本长度和语料规模。常见做法是限制特征数量因为短信语料本身不大如果特征维度超过几万SVM 训练会变慢模型也容易过拟合。下面这段代码是标准流程from sklearn.feature_extraction.text import TfidfVectorizer # 使用预处理后的分词结果用空格连接成句子 def corpus_to_text(corpus): return [ .join(seg) for seg in corpus] vectorizer TfidfVectorizer( max_features5000, # 只保留最重要的5000个词 ngram_range(1, 2), # 保留单个词和相邻两个词 norml2, # 对每个文档向量做L2归一化 sublinear_tfTrue # 对词频做log(1tf)压缩 ) train_vectors vectorizer.fit_transform(train_texts)max_features5000不是拍脑袋。短信分类场景里5000 个词足够覆盖 95% 的有用信息。如果你把max_features调成 10000你会发现模型准确率提升不到 0.5%但训练时间拉长一倍。ngram_range(1, 2)的作用是把“免费领”这种连续共现词对也纳入特征因为“免费领”比单独的“免费”和“领”更有判别力。sublinear_tfTrue是一个容易被忽略的参数它把原始词频用log(1tf)压缩防止一条被重复多次的垃圾短信在向量空间里获得过大的 L2 范数。这几个参数是你在答辩时能讲出“为什么”的地方老师一听就知道你不是只会调包。我一般还会在训练前做一次类别统计。如果垃圾短信和正常短信的比例小于 1:5就要考虑要不要做欠采样或过采样。后面避坑部分会单独说。3. 训练 SVM 分类器线性核还是 RBF 核课程设计怎么选3.1 为什么总说 SVM 适合小样本高维文本很多人的课程设计报告写着“用了深度学习”但实际代码里跑的是逻辑回归因为深度学习在 CPU 上训练文本分类需要的时间让人崩溃。SVM 在小样本高维稀疏数据上表现好这是理论课上的结论。放在垃圾短信识别这个场景里语料通常只有几千条特征维度却是几千维正好落在 SVM 的优势区。SVM 的目标是找一个超平面让最近的正负样本点到它的距离最大化。对于文本分类这个超平面在高维空间里往往能把两类分得特别开即使原始特征不是线性可分的。线性核和 RBF 核的选择是课程设计里最容易让新手纠结的地方。线性核的决策边界是一条直线训练快、模型参数少、不容易过拟合对于特征维度远大于样本量的文本数据效果通常已经很好。RBF 核能拟合非线性边界但有两个超参数C和gamma调不好就会过拟合训练时间也成倍增加。我的建议是如果你的训练数据少于 5000 条优先用线性核把C调到一个合适的值就能交差如果你一定要在报告里展示 RBF 核那就要跑网格搜索否则不要用。3.2 用 scikit-learn 跑通最小训练流程下面是一个可以直接跑的训练脚本使用了流水线把向量化和分类器绑在一起避免测试数据再次调用 fit 时发生特征不一致的玄学错误。from sklearn.pipeline import Pipeline from sklearn.svm import SVC from sklearn.model_selection import train_test_split # 假设 X 是原始短信文本列表y 是标签列表0为正常1为垃圾 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) pipeline Pipeline([ (tfidf, TfidfVectorizer( max_features5000, ngram_range(1, 2), sublinear_tfTrue, preprocessorlambda s: s # 我们已经预处理过这里只接受原始字符串 )), (svm, SVC( kernellinear, C1.0, class_weightbalanced, probabilityTrue, random_state42 )) ]) pipeline.fit(X_train, y_train) accuracy pipeline.score(X_test, y_test) print(fTest accuracy: {accuracy:.4f})这里有两个细节值得说。第一TfidfVectorizer的preprocessor参数被设为一个恒等函数因为我们已经提前把分词结果用空格拼成了字符串向量化器内部会自动调用jieba吗不会。TfidfVectorizer自带的分词是英文左空格的对中文无效所以必须在外部完成分词再把词用空格串起来。这步经常有人在代码里写错结果发现模型效果很差。第二SVC里的class_weightbalanced。如果垃圾短信只占 10%SVM 默认是偏向多数类的用balanced让模型按样本比例放大少数类惩罚F1 分数会明显改善。probabilityTrue是为了后面画 ROC 曲线因为decision_function也能用但概率更直观。如果你不画 ROC这个参数可以不写反而能省一点训练时间。3.3 C 和 gamma 怎么调网格搜索的落地范围线性核只有一个超参数C它的作用是控制误分类惩罚。C越大训练时越不能容忍分错边界贴合训练集越紧容易过拟合C越小边界越平滑但可能欠拟合。对于文本特征C在 0.1 到 10 之间通常比较合适。不要用C1000那会让模型把训练集里的噪声都背下来。对于 RBF 核还要关注gamma它决定了单个样本对决策边界的影响力范围gamma越大每个样本的影响半径越小边界越复杂过拟合风险越高。我用的是GridSearchCV参数范围尽量收敛避免跑几个小时。可直接抄下面的代码from sklearn.model_selection import GridSearchCV import numpy as np param_grid [ {svm__kernel: [linear], svm__C: [0.1, 1, 10]}, {svm__kernel: [rbf], svm__C: [1, 10], svm__gamma: [0.001, 0.01]} ] grid GridSearchCV( pipeline, param_grid, cv5, scoringf1_macro, n_jobs-1, verbose1 ) grid.fit(X_train, y_train) print(grid.best_params_) print(grid.best_score_)网格搜索的scoring不要用默认的accuracy原因在后面评估章节讲。n_jobs-1表示使用所有 CPU 核心课程设计电脑如果是四核笔记本线性核的 3 组参数加上 RBF 核的 4 组参数总共 7 组5 折交叉验证也就是 35 次训练通常几分钟内完成。如果超过 10 分钟还没结束说明特征维度太大回头检查max_features。另外verbose1会在控制台打印进度答辩演示时很好看。4. 拿到 90% 准确率后还要做的四件事评估指标、混淆矩阵与过拟合排查4.1 准确率骗人的地方垃圾短信数据天生不平衡你把模型跑完打印出accuracy0.97打开报告就开始写“准确率 97%”。但如果原始数据里正常短信占 95%垃圾短信只占 5%那模型只要永远输出“正常”准确率就是 95%。这种情况下单看准确率没有任何意义。课程设计的评分老师一定会问“你的模型对垃圾短信的识别率多少”而不是“总准确率多少”。因此模型评估必须看混淆矩阵而且要重点关心“垃圾短信被召回的比例”也就是recall。在爱情公寓相关的公开短信数据集或者 UCI 的 SMS Spam Collection 里垃圾短信占比大概在 13% 左右还不是极端不平衡。但你自己扩充数据后比例可能变得很难看。所以第一步是在训练前用y.value_counts()统计类别分布如果垃圾短信少于 10%就要考虑class_weight、过采样 SMOTE、或者对垃圾短信做复制/改写来扩充。SVM 对类别不平衡本身就不太鲁棒硬训只会让模型偏向多数类。4.2 用 classification_report 看 precision/recall/F1scikit-learn 提供了现成的报告函数你不需要自己手写公式。在测试集上跑完预测后执行下面代码from sklearn.metrics import classification_report, confusion_matrix y_pred pipeline.predict(X_test) print(classification_report(y_test, y_pred, target_names[正常, 垃圾])) # 手动输出混淆矩阵用于报告插图 conf confusion_matrix(y_test, y_pred) print(混淆矩阵) print(conf)输出形态一般是这样的类别precisionrecallf1-scoresupport正常0.990.970.98800垃圾0.870.940.90120解释这两个指标precision 表示模型预测为垃圾的短信里真的是垃圾的比例recall 表示真正的垃圾短信里被模型找出来的比例。课程设计最好的结果是两个都高但如果二选一我建议优先保证 recall因为垃圾短信漏掉比正常短信误封更让用户恼火。如果 recall 低先把class_weightbalanced加上这个改动通常能提升 5 个百分点。你在报告里一定要写清楚support表示每一类的真实数量这样评价指标才可信。4.3 交叉验证与学习曲线验证你的模型不是背答案仅仅在 train_test_split 的测试集上跑一次准确率高不意味着模型泛化。老师最常问的问题是“你做交叉验证了吗”。Cross-validation 能评价模型在不同子集上的稳定性。上面网格搜索里已经用了cv5但你可以单独跑一次交叉验证来报告稳定性from sklearn.model_selection import cross_val_score scores cross_val_score(pipeline, X, y, cv5, scoringf1_macro) print(F1 macro scores:, scores) print(Mean:, scores.mean().round(4), Std:, scores.std().round(4))如果五次交叉验证的得分波动超过 0.05就说明你的模型在某一折上表现特别差通常是数据分布不均或者预处理时泄露了测试集信息。另一种排查过拟合的方法是画学习曲线用小部分训练数据训练时训练准确率很高但测试准确率很低随着训练数据增加两者逐渐靠近这是典型的过拟合。反之如果两条线始终靠拢但准确率都不高说明模型欠拟合。课程设计里画学习曲线能把报告厚度撑起来代码用sklearn.model_selection.learning_curve就能出数据。5. 避坑垃圾短信识别项目里最常见的 5 个坑现象→原因→解决5.1 中文乱码导致模型准确率骤降现象训练时准确率有 92%测试时只有 60%打开控制台发现预测输出全是空白、错别字。原因文本文件用 GBK 编码读取而 Python 的默认字符串处理在 Windows 上是 GBK在 Linux 上是 UTF-8。当你在源码里写open(data.csv, r)时编码没指定换一台机器跑数据就乱了。解决所有读取和写入文件的地方统一显式指定encodingutf-8。如果原始数据是 GBK先转换成 UTF-8。我一般会在项目根目录放一个convert_encoding.py一次性处理原始数据。5.2 jieba 自定义词典没生效专业词被切碎现象短信里的“代开发票”被切成“代开”“发票”“加微信”被切成“加”“微信”模型很难把“代开发票”当成一个整体。原因jieba 自带词库里没有这些垃圾短信专用词默认 HMM 模型对未知词会按单字或双字组合。解决在项目里建一个user_dict.txt每行写一个词然后调用jieba.load_userdict(user_dict.txt)。这个词表要你自己根据语料积累比如“退订”“TD”“免费领取”“中奖”“转账”“客服”等等。注意自定义词典的优先级最高但不要放入过于泛化的词否则会影响歧义词切分。5.3 训练集和测试集混入了同一批短信样本现象你在网格搜索里跑出的 best_score 很高但拿新数据预测时效果极差。原因很可能你使用了全局变量在预处理时直接把整个数据集做了清洗没有用分层抽样切分训练和测试或者重复跑了fit_transform和transform时没有严格区分。更隐蔽的是训练集里 A 和测试集里 B 是同一条短信的轻微变体比如“【短信】”标记不同向量化器把它们映射成同一向量导致测试集被污染。解决在切分数据之前先对原始文本做去重再按stratifyy切分。如果文本里有用户 ID、时间戳这类不要当作特征。5.4 网格搜索跑太久直接把笔记本干没电现象param_grid 里有 RBF 核、C和gamma各取了 10 个值总共有 100 组再乘 5 折交叉验证就是 500 次 SVM 训练笔记本风扇狂转两个小时后还在跑。原因网格搜索没有考虑特征维度和样本量盲目扩大参数组合。解决先用线性核和少量C值跑通流程再考虑 RBF 核。使用GridSearchCV的verbose1跟踪进度并且给max_features降到 3000 先跑一次小规模验证确定参数合理后再用 5000 特征跑全量。如果还是慢把n_jobs设为 4 而不是 -1避免内存被吃满。5.5 保存模型后加载预测报错pickle 与 joblib 的区别现象训练结束后用pickle.dump(pipeline, open(model.pkl, wb))保存换到别的脚本里pickle.load后执行predict报错TypeError: tuple object is not callable或者AttributeError: TfidfVectorizer object has no attribute vocabulary_。原因pickle 按引用序列化对象如果你把TfidfVectorizer单独保存了但加载环境中没有正确导入 scikit-learn 类就会出问题。另外模型里保存的jieba.lcut是外部函数加载时如果没有import jieba反序列化的preprocessor调不出分词函数。解决不使用 pickle而是用joblib.dump(model, model.joblib)它对大对象和包含 external 函数的对象处理更稳。加载时写import joblib import jieba # 必须提前加载虽然不直接调用但反序列化需要这个模块存在 model joblib.load(model.joblib)注意保存模型时也要把向量化器一起保存在 pipeline 里不要单独保存。每次预测前确保输入文本经过和训练时完全一样的清洗和分词逻辑。6. 把模型变成能演示的东西一个 50 行左右的命令行预测器与验证技巧6.1 最小可用的预测函数与编码对齐课程设计答辩最怕现场输入一条短信模型输出乱码或者直接抛异常。你最好做一个最简单的命令行脚本把之前所有流程串起来。我自己常用的是一个predict.py从标准输入读取短信调用已保存的 pipeline 输出“垃圾/正常”和置信度。核心代码只有这么一点import sys import joblib import jieba from modules.preprocess import clean_text, tokenize, load_stopwords model joblib.load(model.joblib) stopwords load_stopwords() def predict_message(raw_text): # 必须完全复现训练时的预处理 tokens tokenize(raw_text, stopwords) processed .join(tokens) proba model.predict_proba([processed])[0][1] label 垃圾 if proba 0.5 else 正常 return label, proba if __name__ __main__: for line in sys.stdin: label, proba predict_message(line.strip()) print(f{label} | 垃圾概率: {proba:.2%})这里最容易被忽略的是编码对齐。如果你在 Windows 命令行运行要给 Python 加PYTHONUTF81环境变量或者直接写sys.stdin.reconfigure(encodingutf-8)否则从控制台输入的汉字会被歪曲。我在代码里显式 recompile 过clean_text里的正则保证去除链接和数字的顺序一致。你要记住任何一条新的短信在喂给模型之前必须用和训练集一模一样的 pipeline 处理包括clean_text里的所有替换规则。我遇到不少人自己改了一下清洗逻辑然后说模型不准其实是数据分布变了。6.2 用你自己的手机短信做盲测验证模型的泛化性不能只看测试集。找三个人的手机短信每人凑几十条正常短信再人工标注出其中的垃圾短信。如果无法直接导出就在输入框里手工输入典型垃圾短信比如“恭喜您获得一等奖回复 TD 退订”“加微信领红包”。盲测的核心在于这些数据不在训练集里。做一个简单表格记录真实标签和预测标签算一下小样本上的混淆矩阵。如果发现新短信被误判为垃圾大概率是清洗逻辑删掉了模型依赖的特征例如你把“【”符号从短信里过滤了而模型训练时认为方括号是垃圾短信特征。这类矛盾要调清洗规则而不是重训模型。6.3 给设计报告用的三张图ROC 曲线、混淆矩阵、特征权重最后把模型表现可视化。第一张是 ROC 曲线横轴假正率纵轴真正率曲线越靠近左上角越好计算曲线下的面积 AUC。第二张是混淆矩阵热力图用matplotlib加seaborn画红色表示误判。第三张是把线性核 SVM 的系数取出来得到每个词对决策的贡献权重。下面代码可以抽出权重vectorizer model.named_steps[tfidf] svm model.named_steps[svm] feature_names vectorizer.get_feature_names_out() # 线性核的 coef_ 形状是 [1, n_features] weights svm.coef_[0] top_positive weights.argsort()[-10:] # 最支持垃圾短信的10个词 top_negative weights.argsort()[:10] # 最支持正常短信的10个词 for i in top_positive: print(垃圾短信特征:, feature_names[i], round(weights[i], 4))如果 SVM 用的是 RBF 核就画不出这个词权重图所以报告里我建议至少训练一个线性核模型来展示特征重要性。这三张图放进设计报告整篇的工程感会强很多。你在保存模型时记得把probabilityTrue加上不然predict_proba会报错说probability未设定。这是我自己的血泪经验每次跑完新模型都先测试一下predict_proba能不能调用免得答辩现场临时改参数重训耗时又心慌。希望帮到你课程设计顺利。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →