简介:这套基于机器学习支持向量机(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(path='stopwords.txt'): stopwords = set() with open(path, 'r', encoding='utf-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(r'http[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_paddle=True,这个参数需要安装 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_features=5000, # 只保留最重要的5000个词 ngram_range=(1, 2), # 保留单个词和相邻两个词 norm='l2', # 对每个文档向量做L2归一化 sublinear_tf=True # 对词频做log(1+tf)压缩 ) train_vectors = vectorizer.fit_transform(train_texts)max_features=5000不是拍脑袋。短信分类场景里,5000 个词足够覆盖 95% 的有用信息。如果你把max_features调成 10000,你会发现模型准确率提升不到 0.5%,但训练时间拉长一倍。ngram_range=(1, 2)的作用是把“免费+领”这种连续共现词对也纳入特征,因为“免费领”比单独的“免费”和“领”更有判别力。sublinear_tf=True是一个容易被忽略的参数,它把原始词频用log(1+tf)压缩,防止一条被重复多次的垃圾短信在向量空间里获得过大的 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_size=0.2, random_state=42, stratify=y ) pipeline = Pipeline([ ('tfidf', TfidfVectorizer( max_features=5000, ngram_range=(1, 2), sublinear_tf=True, preprocessor=lambda s: s # 我们已经预处理过,这里只接受原始字符串 )), ('svm', SVC( kernel='linear', C=1.0, class_weight='balanced', probability=True, random_state=42 )) ]) pipeline.fit(X_train, y_train) accuracy = pipeline.score(X_test, y_test) print(f'Test accuracy: {accuracy:.4f}')这里有两个细节值得说。第一,TfidfVectorizer的preprocessor参数被设为一个恒等函数,因为我们已经提前把分词结果用空格拼成了字符串,向量化器内部会自动调用jieba吗?不会。TfidfVectorizer自带的分词是英文左空格的,对中文无效,所以必须在外部完成分词,再把词用空格串起来。这步经常有人在代码里写错,结果发现模型效果很差。
第二,SVC里的class_weight='balanced'。如果垃圾短信只占 10%,SVM 默认是偏向多数类的,用balanced让模型按样本比例放大少数类惩罚,F1 分数会明显改善。probability=True是为了后面画 ROC 曲线,因为decision_function也能用,但概率更直观。如果你不画 ROC,这个参数可以不写,反而能省一点训练时间。
3.3 C 和 gamma 怎么调:网格搜索的落地范围
线性核只有一个超参数C,它的作用是控制误分类惩罚。C越大,训练时越不能容忍分错,边界贴合训练集越紧,容易过拟合;C越小,边界越平滑,但可能欠拟合。对于文本特征,C在 0.1 到 10 之间通常比较合适。不要用C=1000,那会让模型把训练集里的噪声都背下来。对于 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, cv=5, scoring='f1_macro', n_jobs=-1, verbose=1 ) 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。另外verbose=1会在控制台打印进度,答辩演示时很好看。
4. 拿到 90% 准确率后,还要做的四件事:评估指标、混淆矩阵与过拟合排查
4.1 准确率骗人的地方:垃圾短信数据天生不平衡
你把模型跑完,打印出accuracy=0.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/F1
scikit-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)输出形态一般是这样的:
| 类别 | precision | recall | f1-score | support |
|---|---|---|---|---|
| 正常 | 0.99 | 0.97 | 0.98 | 800 |
| 垃圾 | 0.87 | 0.94 | 0.90 | 120 |
解释这两个指标:precision 表示模型预测为垃圾的短信里,真的是垃圾的比例;recall 表示真正的垃圾短信里,被模型找出来的比例。课程设计最好的结果是两个都高,但如果二选一,我建议优先保证 recall,因为垃圾短信漏掉比正常短信误封更让用户恼火。如果 recall 低,先把class_weight='balanced'加上,这个改动通常能提升 5 个百分点。你在报告里一定要写清楚support,表示每一类的真实数量,这样评价指标才可信。
4.3 交叉验证与学习曲线:验证你的模型不是背答案
仅仅在 train_test_split 的测试集上跑一次准确率高,不意味着模型泛化。老师最常问的问题是“你做交叉验证了吗”。Cross-validation 能评价模型在不同子集上的稳定性。上面网格搜索里已经用了cv=5,但你可以单独跑一次交叉验证来报告稳定性:
from sklearn.model_selection import cross_val_score scores = cross_val_score(pipeline, X, y, cv=5, scoring='f1_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')时,编码没指定,换一台机器跑数据就乱了。解决:所有读取和写入文件的地方,统一显式指定encoding='utf-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 是同一条短信的轻微变体,比如“【短信】”标记不同,向量化器把它们映射成同一向量,导致测试集被污染。解决:在切分数据之前,先对原始文本做去重,再按stratify=y切分。如果文本里有用户 ID、时间戳这类不要当作特征。
5.4 网格搜索跑太久,直接把笔记本干没电
现象:param_grid 里有 RBF 核、C和gamma各取了 10 个值,总共有 100 组,再乘 5 折交叉验证,就是 500 次 SVM 训练,笔记本风扇狂转,两个小时后还在跑。原因:网格搜索没有考虑特征维度和样本量,盲目扩大参数组合。解决:先用线性核和少量C值跑通流程,再考虑 RBF 核。使用GridSearchCV的verbose=1跟踪进度,并且给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 加PYTHONUTF8=1环境变量,或者直接写sys.stdin.reconfigure(encoding='utf-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 核,就画不出这个词权重图,所以报告里我建议至少训练一个线性核模型来展示特征重要性。这三张图放进设计报告,整篇的工程感会强很多。你在保存模型时,记得把probability=True加上,不然predict_proba会报错说probability未设定。这是我自己的血泪经验,每次跑完新模型都先测试一下predict_proba能不能调用,免得答辩现场临时改参数重训,耗时又心慌。希望帮到你,课程设计顺利。
本文还有配套的精品资源,点击获取