ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SVM垃圾短信识别实战:从TF-IDF特征工程到模型调优全流程

SVM垃圾短信识别实战:从TF-IDF特征工程到模型调优全流程 简介一套面向计算机相关专业的课程设计资源包基于支持向量机SVM实现垃圾短信自动识别与分类适合计科、大数据、人工智能、物联网等方向的学生、教师或企业开发者用于课程作业、毕业设计或初期项目演示。资源包为约107.89MB的zip压缩包主要由Python源码、项目说明与设计报告组成内部按model、Data、code三个模块组织model保存训练好的模型Data目录包括带标签与无标签数据、TF-IDF特征及预处理中间文件code目录涵盖数据预处理、SVM训练与在线分类预测脚本整体结构清晰便于查阅和调试。借助说明文档和设计报告使用者可快速理解并复现完整的SVM文本分类流程由于代码模块划分清晰也便于替换数据集或调整特征工程可二次开发用于其他文本分类场景。目前已有289人学习下载在同类课程设计资源中具有一定参考价值。1. 垃圾短信识别为什么拿SVM做课程设计一个小而完整的机器学习闭环每年课程设计答辩都有人被导师一句“你的SVM到底在分什么”问得下不来台。垃圾短信识别恰好是一个不用造数据集、不用上GPU、又能把「文本预处理→特征工程→模型训练→评估调参→部署演示」整个机器学习流程走通的小项目这也是它常年出现在Python课程设计选题里的原因。这套方案要干的事很直接输入一条短信模型输出它是正常短信ham还是垃圾短信spam。技术栈是Python scikit-learn里的SVM分类器配合TF-IDF把文本变成向量。读完这篇你能跑通一整套SVM垃圾短信识别流程知道每个参数为什么这么设也能应对答辩时最容易被追问的细节。适合正在做机器学习课程设计、或者想用一份完整代码把SVM原理落到实际数据上的同学。2. 从数据集到特征向量先把短信文本变成SVM能读的矩阵SVM是个数学分类器输入必须是数值矩阵而短信是变长的非结构化文本。这一步解决的核心问题就是把几十万个字符变成几千维的稀疏向量同时不丢失「中奖」「免费」「验证码」这类判别词的信息。2.1 数据集选型UCI SMS Spam Collection与格式解析课程设计最稳妥的数据集是UCI机器学习仓库里的SMS Spam Collection学术用途下被引用了很多年公开、体积小、标注规范。它一共约5574条短信正常短信4827条、垃圾短信747条比例大概是6.5比1。这个比例本身就是后面要处理的类别不平衡问题的雏形。数据格式是标准的TSV第一列是标签第二列是短信原文。加载代码很常规import pandas as pd # 数据集是 TSV 格式每行第一列是标签第二列是短信内容 df pd.read_csv( SMSSpamCollection, sep\t, # 用 Tab 分隔不是逗号 headerNone, # 原始文件没有表头 names[label, message], encodingutf-8, ) # 看一眼类别分布确认数据能读进来 print(df[label].value_counts()) print(df.head())sep\t是这里最容易出错的地方如果你用默认的逗号分隔所有字段都会挤进第一列。headerNone告诉pandas数据集没有列名否则第一行数据会被当成表头。读进来之后先看value_counts()是习惯标签分布ham/spam比例如果和你预期差得远说明解析出了问题不要急着往下做。如果你做的是中文垃圾短信识别UCI这个英文数据集只能用来跑通流程不能直接当交付数据。常见做法是自建几百条中文短信按「spam、ham」两个类别自己标注或者用学校课程平台提供的中文短信语料。流程完全一样但后面分词环节要换成中文分词器。2.2 文本预处理清洗、分词、去停用词三步走SVM不关心标点符号和停用词这些内容只会稀释真正的判别特征。英文短信的处理相对简单转小写、去标点数字、按空格切词、去掉停用词。中文短信没有天然空格分隔必须先用分词器切词否则一个句子会被当成一个token后面TF-IDF算出来毫无意义。import re import jieba # 中文停用词示例实际使用可以按需扩充 STOP_WORDS {的, 了, 是, 我, 你, 他, 在, 有, 和, 就, 不, 人, 都, 一} def clean_text(text: str, language: str zh) - str: # 把非中英文、数字的字符统一替换成空格避免残留标点干扰 text re.sub(r[^\w\u4e00-\u9fa5], , text.lower()) if language zh: # 中文短信需要先分词jieba 会把「恭喜您中奖」切成有意义的词 words [w for w in jieba.lcut(text) if w.strip() and w not in STOP_WORDS] else: # 英文直接按空格切分即可 words [w for w in text.split() if w.strip() and w.isalpha()] return .join(words) df[message_clean] df[message].apply(lambda s: clean_text(s, languagezh))正则[^\w\u4e00-\u9fa5]意思是匹配所有不是字母数字、不是中文的字符把它们替换成空格。这一步把标点、表情符号、特殊符号统一清掉避免“免费”和“免费”被当成两个不同特征。中文走jieba.lcut分词英文走split加一个isalpha()过滤纯数字串。注意去停用词这步是可选的但对短信这种短文本帮助明显——“的”“了”“是”这类词在垃圾短信和正常短信里都高频出现留着只会增加噪声。2.3 TF-IDF向量化为什么比词袋模型更适合短文本词袋模型统计的是词频但词频有个致命缺陷“明天”“公司”这类正常短信高频词会拿到很高权重而真正区分垃圾短信的“中奖”“贷款”因为出现次数相对少权重反而不够。TF-IDF的做法是先算词频TF再乘以逆文档频率IDF在越少文档里出现的词IDF越高说明这个词的区分度越强。from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer( max_features5000, # 只保留词频最高的5000个特征防止维度爆炸 ngram_range(1, 2), # 同时考虑单个词和相邻两个词的组合 sublinear_tfTrue, # tf 用 1log(tf) 平滑抑制高频词 # stop_wordsenglish, # 如果前面已经做过停用词清洗这里不要重复设置 ) X vectorizer.fit_transform(df[message_clean]) print(X.shape) # 结果是稀疏矩阵例如 (5574, 5000)max_features5000是短文本场景下的经验值。短信长度有限实际有效词也就几百个5000维足够覆盖大多数判别词又不会让训练集过拟合。ngram_range(1, 2)让模型同时看到“免费”和“免费领取”两类特征能捕捉动词搭配。sublinear_tfTrue是一个容易被忽略但有效的参数将词频做对数平滑避免某个词在一条超长短信里反复出现导致权重失真。这里先fit_transform全量数据是为了快速看特征维度正式训练时必须先切分再fit具体原因在第5章讲。3. 训练SVM分类器线性核还是RBF核先看样本量SVM的核心思想是找一个超平面让两类样本的间隔最大化。落在间隔边界上的样本就是支持向量。课程设计报告里最常被追问的就是「为什么选SVM」和「为什么用这个核函数」这一章把这两个问题的答案讲透。3.1 类别不平衡是第一个坑重采样与类别权重747条垃圾短信对上4827条正常短信如果直接训练SVM会倾向把所有样本都判成多数类因为这样整体损失最小。你的准确率可能达到87%但垃圾短信的召回率趋近于零——模型形同虚设。三种常见处理方式在SVC里设置class_weightbalancedSVM会根据类别样本量自动加大少数类的误分类惩罚。最省事推荐优先试。用imbalanced-learn做SMOTE过采样合成新的少数类样本。效果通常更好但会破坏文本向量的稀疏性SVM训练变慢。对多数类做欠采样。速度快但丢弃大量正常短信损失信息严重。对短信文本这种高维稀疏向量我一般只设置class_weightbalanced不再动采样。少数类只有几百条样本SMOTE在几千维空间里合成样本很容易造出和真实文本分布脱节的向量得不偿失。from sklearn.svm import SVC # 类权重会让 SVM 对少数类样本的误判更敏感 model SVC(kernellinear, C1.0, class_weightbalanced, random_state42)3.2 第一个可运行版本SVC训练与评估的最小代码训练之前先切分数据集再在训练集上做特征工程这是防止数据泄漏的铁律。下面的代码是一个能完整跑通的版本from sklearn.model_selection import train_test_split from sklearn.svm import SVC from sklearn.metrics import classification_report # 先切分数据集stratify 保证训练集和测试集的 spam 比例一致 X_train, X_test, y_train, y_test train_test_split( X, df[label_binary], test_size0.2, random_state42, stratifydf[label_binary] ) # 线性核文本特征维度高样本量小线性可分性足够 model SVC( kernellinear, C1.0, # 误分类惩罚越大越容易过拟合 class_weightbalanced, random_state42, ) model.fit(X_train, y_train) # 分类报告里重点看 spam 行的精确率、召回率、F1 print(classification_report(y_test, model.predict(X_test), target_names[ham, spam]))train_test_split里的stratifydf[label_binary]很关键如果不分层切分可能测试集里恰好没有垃圾短信或者垃圾短信占比和总体差别很大评估结果完全不可信。SVM选线性核的理由是TF-IDF向量化后文本特征通常是几千维的稀疏向量特征数量远大于样本数量这种情况下面向高维空间的线性核往往表现更好训练也比RBF核快一个数量级。C1.0是起步值C越小模型泛化越好但容易欠拟合这组代码里先不动它第4章再调。3.3 评估指标怎么选准确率会骗人F1更可靠垃圾短信识别里如果模型把所有短信都判为ham准确率照样有大约87%。所以只看准确率是课程设计答辩的大忌导师一句「召回率多少」就能让你露馅。正确做法是看混淆矩阵和类别维度上的精确率、召回率、F1。指标在垃圾短信场景的含义为什么重要精确率Precision被判为垃圾短信的样本里真垃圾的比例防止把正常短信误杀召回率Recall真垃圾短信里被正确找出来的比例防止垃圾短信漏网F1精确率和召回率的调和平均类别不平衡下比准确率可靠得多准确率Accuracy所有样本里判断正确的比例班级不平衡时严重失真课程设计阶段我习惯要求spam类别的F1不低于0.9且召回率不能比精确率低太多。如果召回率明显偏低说明大量垃圾短信被放进了收件箱这是违背项目目的的。答辩时主动汇报这组指标比被导师逐项追问体面得多。4. 参数调优与模型保存把F1从0.9推到0.97第一个版本跑通后线性核SVC的F1通常在0.9到0.93之间。想再往上推主要靠两个手段用网格搜索调C以及把特征提取放进Pipeline里做交叉验证。4.1 GridSearchCV搜索C和gamma网格别太大线性核SVC只有一个关键参数CRBF核则多一个gamma。文本场景下我一般先只搜C如果核函数选的是RBF再追加gamma。网格搜索的代价是组合数乘以交叉验证折数再乘以训练时间一上来就放一个大的参数网格可能在答辩前夜让你干等两小时。from sklearn.model_selection import GridSearchCV param_grid [ {kernel: [linear], C: [0.1, 1, 10]}, {kernel: [rbf], C: [0.1, 1, 10], gamma: [scale, 0.1, 0.01]}, ] grid GridSearchCV( SVC(class_weightbalanced), param_grid, cv5, scoringf1, n_jobs-1, verbose1, ) grid.fit(X_train, y_train) print(grid.best_params_, grid.best_score_)注意scoringf1而不是默认的准确率——如果把准确率当评分网格搜索会全力优化那个87%的「全判成ham」方案。n_jobs-1会调用所有CPU核心RBF核在大网格下会把内存吃满如果机器只有8G内存建议先只搜{kernel: [linear], C: [0.1, 1, 10]}这一组。verbose1的好处是能看到每轮任务进度心里有数。4.2 交叉验证的正确姿势先切分再fit第2章里我为了看特征维度在全量数据上做了fit_transform。真正的训练流程不能这么干一旦vectorizer先在全量数据上拟合测试集里每个词是否进词表、IDF值是多少都会被训练过程用到的统计信息污染。这就是数据泄漏。正确姿势是把vectorizer和SVM放进同一个Pipeline让GridSearchCV在每一折交叉验证里独立地重新拟合vectorizer。from sklearn.pipeline import Pipeline from sklearn.svm import SVC from sklearn.feature_extraction.text import TfidfVectorizer pipe Pipeline([ (tfidf, TfidfVectorizer(max_features5000, ngram_range(1, 2))), (svm, SVC(kernellinear, class_weightbalanced)), ]) # 注意参数名前缀svm__C 表示 C 属于 pipeline 里的 svm 组件 grid GridSearchCV( pipe, param_grid{svm__C: [0.1, 1, 10]}, cv5, scoringf1, ) grid.fit(df[message_clean], df[label_binary])Pipeline的价值在于交叉验证的每一折里TfidfVectorizer只看到训练部分的文本测试折的语料统计不会混进词表或IDF计算评估结果才真实可信。网格搜索时调参路径要用svm__C这种双下划线写法漏掉前缀会直接报参数错误。整个网格搜索跑完后grid.best_estimator_就是一份完整的「清清洗文本→向量化→分类」链路。4.3 用joblib保存模型课程设计演示时不用重训调优结束后最怕的事情是答辩时现场重训或者关了终端又忘记参数。用joblib把整个Pipeline保存下来加载后一条消息就能直接出结果。import joblib # 把清洗、向量化、SVM 一条链路整体保存 joblib.dump(grid.best_estimator_, sms_svm_pipeline.joblib) # 加载后直接预测不需要再带入 vectorizer loaded joblib.load(sms_svm_pipeline.joblib) print(loaded.predict([恭喜您中奖啦点击链接领取大礼包]))保存整个Pipeline而不是只保存SVC模型是很多人忽略的细节如果只存模型预测时你还要手动保留一份vectorizer并且要保证两者的特征维度完全一致。joblib.dump对带大量数组参数的scikit-learn对象有专门优化比Python自带的pickle更稳。加载时如果报错常见原因是sklearn版本不一致这个坑在第5章展开。5. 踩坑记录SVM垃圾短信识别里最容易翻车的5个细节这部分是课程设计里最常见的血泪经验。每条都按「现象→原因→解决」排列遇到报错直接对号入座。5.1 标签列带空格或空字符评估结果全乱现象print(df[label].unique())输出里看到spam和ham长得正常但训练时报错说标签类型不是binary或者F1低到离谱。再仔细看发现有的标签其实是spam 尾随空格有的行是\tspam。原因数据集从网页复制或者用Excel编辑过隐藏字符混进了标签列。pandas读进来后这些不可见字符导致字符串匹配失败标签编码后变成乱序。解决加载后立刻做两步清洗。df[label] df[label].str.strip() df[label_binary] df[label].map({spam: 1, ham: 0}) print(df[label_binary].value_counts())strip()去掉首尾空格和Tabmap把字符串标签转为0和1。不要在map之前省略检查value_counts()看一眼是最低成本的保险。5.2 全量数据fit之后再切分造成数据泄漏现象训练集F1高到0.98测试集F1只有0.91而且无论怎么调参都无法缩小差距。网上的老教程常让人fit_transform全部数据再做train_test_split照抄就踩坑。原因vectorizer提前在全量语料上拟合测试集里的词频和IDF信息已经参与了训练SVM的决策边界在评估时「偷偷开卷」测试结果虚高。更麻烦的是这种虚高不会报错只会让你对自己的模型产生错误自信。解决坚持先切分再做特征工程或者直接用4.2的Pipeline。正确的是X_train vectorizer.fit_transform(df_train)、X_test vectorizer.transform(df_test)。如果你已经全量fit了立刻修改流程不要让模型带病答辩。5.3 中文短信没分词TF-IDF算了个寂寞现象中文短信数据集训练出的模型精确率还行召回率极低打印几个向量特征看到的是整句原文而不是词。原因TfidfVectorizer默认按空格分词中文短信没有空格一整段话被当成一个token。单条短信里这个token只出现一次TF为1而IDF的分母是包含该「词」的文档数整句重复的概率极低导致所有特征值趋近于0SVM学不到任何模式。解决在2.2的cleaning函数里用jieba.lcut分词但要注意如果你保存了Pipeline分词逻辑是写在clean_text函数里还是在vectorizer的tokenizer参数里预测时必须走同一条路径。我习惯把清洗函数定义在单独模块里训练和predict都import它避免两处逻辑不一致。5.4 类别不均衡不处理把87%准确率当成好结果现象spam行召回率0.3以下但模型整体准确率有0.87。报告里只贴了accuracy答辩时被追问就翻车。原因没有设置class_weightbalancedSVM为了最小化整体误判率学会了「全部判成ham」这种偷懒解法。而只看准确率的人根本发现不了因为少数类错误被多数类的正确淹没了。解决在SVC里加上class_weightbalanced评估时强制看spam行的精确率、召回率、F1。我最常用的验收标准是spam召回率不低于0.9。5.5 用pickle保存模型换个环境加载失败现象在自己电脑上joblib.load正常把文件拷到另一台电脑或课程设计服务器上报ModuleNotFoundError: No module named sklearn或奇怪的AttributeError。原因sklearn的类定义和Python模块路径绑定pickle序列化的是类的引用路径而不是类本身。本地装的是sklearn 1.2目标机器是0.24内部类结构不同加载时找不到对应属性直接报错。解决用joblib保存在项目目录里放一个requirements.txt至少写死scikit-learn你的版本号。最稳妥的做法是答辩演示用自己的笔记本或者现场用训练脚本重新跑一遍。课程设计最忌讳的就是演示时现装包提前把环境和模型一起准备好这是基本礼仪。6. 让它变成能演示的成品命令行预测与验收清单模型调好只是项目的一半课程设计验收看的是「能不能用」。最后把模型封装成一个直观可用的预测入口并列出答辩前的自检项。6.1 封装一个可复用的预测函数def predict_sms(text: str) - str: # loaded 是第4章保存的完整 Pipeline内部包含清洗之外的向量化与SVM cleaned clean_text(text, languagezh) # 必须和训练时用同一个函数 result loaded.predict([cleaned])[0] return spam if result 1 else ham # 自测几条 print(predict_sms(恭喜您中奖啦点击链接领取大礼包)) print(predict_sms(晚上一起吃饭))预测函数的输入是原始短信字符串内部先做清洗再由Pipeline完成向量化和分类。loaded.predict接收的是列表返回的也是列表取[0]得到单个结果。6.2 验收时必备的输出与截图点课程设计报告和答辩PPT里至少要有四张可复现的图或表分类报告classification_report、混淆矩阵热力图、5条典型短信的预测结果、网格搜索最优参数表。混淆矩阵用sklearn自带的ConfusionMatrixDisplay画比你贴一堆数字直观得多。截图时不要把训练过程的日志截进去只截最终指标和预测demo。6.3 一个可选的再训练习惯如果答辩前你拿到了新的已标注短信不要只在旧模型上跑预测。把新数据合并进原数据集重新执行一遍第4章的网格搜索把新的最优参数和F1记录在设计报告的附录里。这个动作看起来费时间但它证明你理解「模型交付不是终点数据更新后需要重新验证」这一工程常识比堆砌理论更能说服导师。我自己的习惯是项目代码里只有数据清洗和评估脚本允许「跑一遍就完」模型训练和预测必须封装成函数随项目一起提交。这样导师临时塞一条短信过来你不用尬在终端前重新调参。这个SVM垃圾短信识别做完收获最大的不是调包技能而是把数据泄漏、类别不平衡、特征稀疏这几个机器学习通用问题在几千条真实数据上体验了一遍。希望帮到你。本文还有配套的精品资源点击获取
返回列表