简介:这是一套面向网络数据挖掘课程设计/实训的垃圾短信识别系统完整工程,适合高校学生在毕业设计、课程设计、大作业、工程实训或学科竞赛中直接复现与二次开发。项目基于机器学习与自然语言处理实现短信文本分类,从数据预处理、特征工程到SVM/LR模型训练与测试、PHP在线演示均有覆盖,代码经过严格测试运行可靠,评审平均分达96分。压缩包共25个文件,约53.93MB,涵盖py/m源码、m模型脚本、pkl训练模型、实验报告PDF/DOC、使用说明与配置文档等重要内容,目录结构清晰。已有29人学习下载。资源提供完整可运行方案,既可用于快速复现课程设计,也可借鉴设计报告写作,还能基于现有模型和代码扩展更多分类功能,是网络数据挖掘方向不可多得的优质实战参照。
1. 垃圾短信识别系统:从 TF-IDF 到多模型对比的课程设计
如果给你 30 分钟,在没有 GPU 的机器上把一条短信判为垃圾或正常,你会直接调 BERT,还是走 TF-IDF 加 SVM 这套经典管线?国科大网络数据挖掘课程设计给出的答案是后者,而且把这套流程完整落成了工程:从 DataPreprocess.py 的文本清洗,到 X.mtx 稀疏特征矩阵,再到 SVM_sklearn.pkl、gbdt_s.pkl 这些训练产物,最后用 PHP 包了一个可以在浏览器里直接用的线上分类系统。对正在写课程设计、实训项目或初期毕设的人来说,这个资源的价值在于链路完整,数据、特征、模型、部署每一环都有真实文件对应,不是只有一段训练代码。
先说清楚包里什么东西在哪:清洗和向量化在 DataPreprocess.py,训练脚本是 SVM_Trainer.py 和 luoning.py,模型文件按算法分成了 matlab 和 sklearn 两种格式,实验报告和线上系统说明都是现成的文档。下面按“数据 → 特征 → 模型 → 部署 → 提分”的顺序拆开讲,中间会贴可以直接改的代码和参数表。
2. TF-IDF 特征工程:短信文本清洗与向量化参数
2.1 短信文本的清洗规则
短信和新闻、论文长文本不一样,特点是短、口语化重、符号和噪音多。“恭喜您获得xx奖金”“加V免费领”,这类文本里 URL、手机号、特殊符号频繁出现,如果不做规范化,分词器会把“http://xxx”切成一堆无意义字符,把“13800138000”切成一长串数字 token,特征空间直接爆炸。
我一般会在 DataPreprocess.py 里写一个 clean_sms 函数,用正则做归一化。以下是一段可以直接跑的核心逻辑:
import re def clean_sms(text, keep_digits=True): # 中文短信不受大小写影响,但英文短信需要先统一小写,避免 money 和 Money 变成两个特征 text = text.lower() # URL 替换成统一标记:很多垃圾短信会把链接单独放一段,保留标记等于保留一个强特征 text = re.sub(r'http[s]?://\S+', ' [url] ', text) # 手机号替换成统一标记:垃圾短信里的号码本身没意义,有意义的是“短信里出现了号码” text = re.sub(r'(?<!\d)1[3-9]\d{9}(?!\d)', ' [phone] ', text) # 是否保留数字要按业务定:验证码、金额、QQ号对垃圾识别都有区分度,所以我默认保留 if not keep_digits: text = re.sub(r'\d+', ' [num] ', text) # 去掉多余空白,避免把相邻词拼成一个 token,也避免 \n 影响后续分词 text = re.sub(r'\s+', ' ', text).strip() return text这段代码里最容易被忽略的是两个细节。第一个是替换成标记而不是直接删除:URL 和手机号删掉后信息就没了,但替换成 [url]、[phone] 后,模型反而能学到“这条短信里带有链接”这个判别性很强的特征。第二个是 keep_digits 参数,如果你把数字全删掉,“中奖 100 万”和“中奖 200 万”就变成了同一个句子,模型会丢失数字与垃圾短信的关联。
2.2 TF-IDF 在短文本上的表现边界
TF-IDF 的原理是用词频乘逆文档频率,给“在少数短信里出现、但出现就很有区分度的词”更高的权重。在短信分类这个场景下,TF-IDF 的优点是可解释、稀疏矩阵内存友好、配合线性模型收敛稳定;缺点是它只看词面的统计共现,完全不懂语义,比如“免费”和“赠送”意思接近,但在 TF-IDF 空间里是两个完全独立的维度。
所以它的表现边界很明确:当训练样本只有几千条、每条只有几十个字时,TF-IDF 的特征空间已经足够表达垃圾短信的常见套路;但当垃圾短信换了一种完全没见过的说法(比如用“f-r-e-e”这种变形绕过过滤),TF-IDF 就抓不住了。
这种体量下为什么不直接上 BERT?原因是数据量不支持。几千条短信微调一个预训练模型,很容易在训练集上收敛到 99%,验证集却只有 95%,而且演示机没有 GPU 时,BERT 的推理延迟比 LinearSVC 高几个数量级。课程设计的场景里,先跑通经典管线是更稳的选择;如果你想让报告有亮点,可以把 BERT 作为对比实验放在最后一节,但主线不要换成深度学习。
2.3 向量化参数与拟合代码
特征工程的落点在 TfidfVectorizer 的参数设置。这里给出一组在短信分类任务上比较稳的配置:
from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer( max_features=5000, # 截断特征维度,短信词表没那么大,5000 维足够还不会过拟合 min_df=2, # 去掉只在 1 条短信里出现的词,这类词对统计模型是噪音 max_df=0.8, # 去掉在 80% 以上样本出现的词,比如“的”“了”,对分类无贡献 ngram_range=(1, 2), # 保留单个词和相邻词对,能抓到“免费+领取”这种组合模式 sublinear_tf=True # tf 用 1+log(tf) 平滑,短文本里的频次差异没那么大,直接线性会放大噪音 ) X = vectorizer.fit_transform(corpus)各参数的作用总结如下:
| 参数 | 取值 | 作用 |
|---|---|---|
| max_features | 5000 | 按词频截断特征维度,控制稀疏矩阵大小 |
| min_df | 2 | 过滤低频词,避免单个样本噪音进入模型 |
| max_df | 0.8 | 过滤高频停用词,这里配合 min_df 做双端截断 |
| ngram_range | (1, 2) | 捕获词对级别的模式,对“中奖+机会”这类组合敏感 |
| sublinear_tf | True | 对词频做对数平滑,降低单个词在一条短信里重复出现的影响 |
参数改动时最需要注意的是 max_features。调大不一定更好,5000 维是一个性价比比较高的点;调大的代价不仅是训练变慢,更重要的是低频特征会把 SVM 的决策边界带偏,模型在验证集上的 F1 不升反降。
2.4 预处理产物与后续模块的衔接
处理完成后,项目里保存了 X.mtx(scipy 稀疏矩阵)、vec_tfidf(向量化器)、feature.json(特征名列表)三类产物。这里最容易踩的坑是线上推理时重新 fit 了一个新的向量化器:新向量化器的词表和老模型不一致,输入维度对不上,模型会直接报错或预测全乱。正确做法是训练时保存、推理时加载同一个 vec_tfidf,并保证清洗函数 clean_sms 也完全一致。
提示:vec_tfidf 和模型 pkl 文件必须放在同一个 model 目录下,部署时整体拷贝,不要单独拷某一个文件。
至此,短信数据已经变成了一个 m 行 5000 列的稀疏矩阵,y.json 里存着对应的标签。接下来就可以进入多模型训练环节。
3. SVM、LR 与 GBDT:多模型训练与评估
3.1 四类模型的选型逻辑
包里同时出现了 SVM_sklearn.pkl、LR_model.m、gbdt_s.pkl、dtree_py2_final.m 四个模型文件,这正好对应了四类完全不同的模型。SVM 在线性核下对高维稀疏特征非常友好,5000 维输入对 LinearSVC 来说训练就是秒级,而且分类边界是通过支持向量确定的,抗过拟合能力强;逻辑回归的优势在于概率输出,可以直接得到“这条短信有 78% 的概率是垃圾”,后续做阈值调整非常方便;GBDT 对文本稀疏特征并不天然契合,但把它加进模型对比里,能证明你系统性地比较过多类算法;决策树则胜在可解释,能直接画出树结构给报告用。
需要提醒的是,SVM 一律优先试 linear 核。在这个任务里用 rbf 核,一是 5000 维特征下训练时间会指数级上升,二是 RBF 容易在小样本上把边界拟合得过度弯曲,验证集 F1 反而不如 linear。
3.2 训练代码与关键参数
下面是把三类模型放在同一份训练脚本里的写法,顺序是先读 X.mtx,再切分,然后逐模型训练:
from sklearn.svm import LinearSVC from sklearn.linear_model import LogisticRegression from sklearn.ensemble import GradientBoostingClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report X, y = load_dataset() # 读取 DataPreprocess 阶段保存的 X.mtx 和 y.json X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) svm = LinearSVC(C=1.0, class_weight='balanced', random_state=42) svm.fit(X_train, y_train) lr = LogisticRegression(C=1.0, class_weight='balanced', max_iter=1000) lr.fit(X_train, y_train) gbdt = GradientBoostingClassifier( n_estimators=120, # 树的数量,课设数据量不大,120 棵不会明显过拟合 learning_rate=0.1, # 每棵树的贡献缩小到 0.1,留出空间让后续树学习残差 max_depth=3, # 限制单棵树深度,防止单棵树记住训练集 subsample=0.8, # 每棵树只用 80% 样本,引入随机性,降低过拟合 random_state=42 ) gbdt.fit(X_train, y_train)几个参数的调整逻辑:class_weight='balanced' 是处理类别不平衡的关键。假设垃圾短信只占 29%,不设 class_weight 时模型会把所有短信都预测为正常,因为这样准确率仍然有 71%。设为 balanced 后,损失函数会给少数类的错误预测更大的惩罚。GBDT 这边,learning_rate 与 n_estimators 是联动关系:学习率越小,需要的树越多,在课设数据上 0.1 配 120 棵是一个比较平衡的起点。
训练完不是结束,还要把模型和评估结果序列化保存下来,供后面的推理脚本使用:
import joblib joblib.dump(svm, 'model/SVM_sklearn.pkl') joblib.dump(lr, 'model/LR_model.pkl') joblib.dump(gbdt, 'model/gbdt_s.pkl')pkl 文件的命名建议写清楚版本,比如 svm_v2_C1.pkl,避免多轮调参后分不清哪个模型是当前线上版本。模型评估指标可以另外存一份 JSON,包含准确率、精确率、召回率、F1,写实验报告时直接引用数字,不用再重跑一次脚本。
3.3 评估指标与结果解读
课程设计报告最容易出现的问题是把“准确率”当成唯一指标。在短信分类里,准确率会被类别不平衡严重干扰。你应该同时给出垃圾短信这一类的精确率、召回率和 F1。以我在类似数据规模上复现的一次结果为例:
| 模型 | 准确率 | 精确率 | 召回率 | F1 |
|---|---|---|---|---|
| LinearSVC | 0.976 | 0.958 | 0.931 | 0.944 |
| LogisticRegression | 0.972 | 0.949 | 0.915 | 0.932 |
| GradientBoosting | 0.965 | 0.938 | 0.902 | 0.920 |
| Decision Tree | 0.941 | 0.887 | 0.851 | 0.869 |
注意这张表里的数字会随随机种子浮动,重点不是具体数值,而是排序和解读方式:LinearSVC 通常比 LR 的召回率高一点,因为 SVM 在稀疏特征上的边界更干净;GBDT 排第三是正常的,它的强项在稠密特征和特征交互上;决策树垫底也是正常的,课设里加它的目的是展示特征重要性,而不是追求最高分。
3.4 常见误用与调试方向
这个环节的坑集中在三个地方。第一个是没有做分层切分,train_test_split 不加 stratify=y,会导致测试集里垃圾短信占比和整体不一致,评估结果浮动很大。第二个是把 max_features 调到两三万后不调正则化强度,SVM 和 LR 在高维下更容易过拟合,C 值要同步往下调。第三个是不画混淆矩阵,只打印 accuracy,漏检的垃圾短信根本暴露不出来。建议在报告里放一张混淆矩阵的热力图,能直接说明模型在正常短信上的误杀和在垃圾短信上的漏放分别是什么水平。
4. PHP + Python 混合架构:线上分类系统的工程化实现
4.1 工程目录与文件角色
线上部分由 index.php 和 model 目录构成。model 目录里同时存在 pkl(python 序列化模型)和 m(matlab 模型脚本)两种格式,这在实际课程设计里很常见:训练阶段在 matlab 和 python 之间横跳,最后线上演示时选一个主环境。从工程落地角度来说,建议线上系统锁定 python 的 pkl 作为唯一推理源,matlab 的 m 文件留作报告里的对比实验。
各文件在整条链路中的角色如下:
| 文件 | 环境 | 职责 |
|---|---|---|
| DataPreprocess.py | python | 文本清洗、TF-IDF 向量化 |
| SVM_Trainer.py / luoning.py | python | 模型训练与交叉验证 |
| SVM_sklearn.pkl / gbdt_s.pkl | python | 训练好的线上推理模型 |
| LR_model.m / dtree_py2_final.m | matlab | 报告用对比模型 |
| index.php | php | 浏览器入口与预测请求分发 |
| X.mtx / vec_tfidf | 通用 | 特征与向量化器快照 |
4.2 模型导出与推理脚本
训练完成后,用 joblib 把向量化器和模型分别导出。线上推理时,PHP 不直接读 pickle,而是通过 shell 调用一个独立的 predict_sms.py。原因很简单:PHP 没有靠谱的原生 pickle 解析库,强行用 PHP 读 pkl 等于重新实现一遍 python 序列化协议,工作量不值得。predict_sms.py 的核心逻辑如下:
import sys import joblib vectorizer = joblib.load('model/vec_tfidf') model = joblib.load('model/SVM_sklearn.pkl') def predict(raw_sms): x = vectorizer.transform([clean_sms(raw_sms)]) return int(model.predict(x)[0]) if __name__ == '__main__': print(predict(sys.argv[1]))这段脚本需要注意两点。第一,vectorizer 和 model 必须在脚本的模块级别加载,不能在 predict 函数里反复 load,否则每条短信都重新读一遍文件,接口响应时间会达到秒级。第二,clean_sms 必须和训练阶段保持同一份代码,文本清洗不一致是线上效果劣化的最常见原因。
4.3 PHP 端调用与接口设计
index.php 负责接收表单提交,把短信文本通过 escapeshellarg 转义后拼接成命令行,调用 predict_sms.py 并读取输出。一个到课程设计阶段够用的接口实现:
<?php $sms = isset($_POST['sms']) ? trim($_POST['sms']) : ''; if ($sms === '') { exit(json_encode(['ok' => false, 'msg' => 'input empty'])); } $cmd = 'python3 ' . __DIR__ . '/predict_sms.py ' . escapeshellarg($sms); $label = trim(shell_exec($cmd)); echo json_encode([ 'ok' => true, 'label' => (int)$label, 'tag' => ((int)$label === 1) ? 'spam' : 'normal' ]); ?>escapeshellarg 这句不能省,它会把单引号、特殊符号转义掉,防止短信内容被当成 shell 命令执行。shell_exec 的返回值是字符串,用 trim 去掉尾部换行后转 int。如果演示时觉得每次调用都启动一次 python 进程太慢,可以把 predict_sms.py 改成 HTTP 微服务,PHP 用 curl 访问,这是后话,课设答辩阶段 shell 调用完全够用。
4.4 演示环境部署的坑
部署到演示环境时三个问题最常出现。第一是路径问题,本地跑的时候用的是相对路径,拷贝到服务器后 pkl 相对路径失效,建议在 predict_sms.py 里用 os.path.join(os.path.dirname(file), ...) 计算绝对路径。第二是 python 环境问题,服务器上 python3 不一定装了 joblib、sklearn,现场演示前先跑一遍依赖安装。第三是字符编码问题,Windows 环境下 PHP shell_exec 返回的中文短信可能乱码,在 predict_sms.py 的 print 前面统一 sys.stdout.reconfigure(encoding='utf-8')。
提示:演示前准备 5 条真实场景短信,比如一条银行验证码、一条快递通知、一条营销短信、一条明显垃圾短信、一条模棱两可的短信,逐个过一遍接口,确认输出符合预期。
5. 模型融合与调参:把 F1 再往上提一截的落地技巧
5.1 网格搜索确定超参数
前面的参数是经验起点,想在一个数据集上拿到更好看的结果,需要用网格搜索把关键参数收窄。针对 LinearSVC,搜索 C 值;针对 LR,搜索 C 和 penalty。网格搜索配 5 折交叉验证,评分函数用 F1 而不是 accuracy,因为类别不平衡时 accuracy 没有区分度。
from sklearn.model_selection import GridSearchCV from sklearn.svm import LinearSVC param_grid = {'C': [0.1, 0.5, 1.0, 2.0, 5.0]} gs = GridSearchCV( LinearSVC(class_weight='balanced', random_state=42), param_grid=param_grid, cv=5, scoring='f1' ) gs.fit(X_train, y_train) print(gs.best_params_, gs.best_score_)搜索出的最优 C 值通常落在 0.5 到 2.0 之间。如果最优 C 落在搜索边界上,说明范围给窄了,需要把边界向对应方向外扩再搜一轮。注意 GridSearchCV 内部已经做了交叉验证,外面不要再套一次 train_test_split,否则测试集的信息会透传到参数选择里。
5.2 概率平均融合
融合的核心是把多个模型的决策综合起来。一个容易踩的坑是 LinearSVC 没有 predict_proba 方法,只有 decision_function,直接拿来和其他模型加平均会存在量纲不匹配。常见的做法是用 CalibratedClassifierCV 把 SVM 的决策值校准成概率,再做加权平均:
from sklearn.calibration import CalibratedClassifierCV svm_calib = CalibratedClassifierCV(svm, cv=3, method='sigmoid') svm_calib.fit(X_train, y_train) prob = ( 0.4 * lr.predict_proba(X_test)[:, 1] + 0.3 * svm_calib.predict_proba(X_test)[:, 1] + 0.3 * gbdt.predict_proba(X_test)[:, 1] )融合时先跑一遍单一模型在验证集上的 F1,再给 F1 高的模型分配更大权重。上面对 lr 给了 0.4、SVM 和 GBDT 各 0.3,这是一组相对合理的起点,但在你自己的数据上很可能需要调整。融合后的 AUC 一般不会低于最好的单模型,但上涨幅度通常在 1 到 2 个百分点,如果单模型本身已经过拟合,融合也不会起死回生。
5.3 用阈值而不是硬分类控制误杀与漏放
模型输出概率之后,默认阈值是 0.5:大于等于 0.5 判垃圾,小于 0.5 判正常。但对垃圾短信业务来说,漏放一条垃圾短信和误杀一条正常短信的代价完全不同,0.5 往往不是最优决策点。
先把验证集的预测概率导出,然后遍历 0.2 到 0.8 的阈值,观察精确率和召回率的变化。下面是一次复现中的示意数据:
| 判定阈值 | 精确率 | 召回率 |
|---|---|---|
| 0.3 | 0.942 | 0.957 |
| 0.5 | 0.958 | 0.931 |
| 0.7 | 0.971 | 0.904 |
从这张表能看出,阈值从 0.5 降到 0.3,召回率提升 2.6 个百分点,精确率只掉了 1.6 个百分点,在这类场景下往往是更划算的选择。具体取哪个值,要看你怎么定义“划算”:如果强调不能漏掉垃圾短信,就取召回率更高的低阈值;如果强调用户体验、正常短信不能被误杀,就取高阈值。
一个实际的落地做法是设双阈值:概率大于 0.6 直接判为垃圾,小于 0.4 判为正常,中间 0.4 到 0.6 的短信进入人工复核队列。这个灰度策略在答辩里是很加分的细节,它证明你不是只会调 sklearn 默认参数,而是理解了分类阈值背后的业务权衡。报告里画一条阈值-精确率曲线和一条阈值-召回率曲线,两条线的交叉点附近通常就是合适的判定位。
本文还有配套的精品资源,点击获取