简介:面向网络数据挖掘课程设计/实训场景的垃圾短信识别系统完整工程,涵盖数据预处理、特征构建、模型训练与分类等环节,配有实验报告、操作说明及可运行的Python和MATLAB代码,适合高校学生用于课设、大作业或毕业设计参考复现。压缩包共25个文件,类型包括Python源码、MATLAB脚本、SVM/GBDT等模型文件(pkl)、文本语料(txt/json/mtx)以及PDF/doc说明文档,整体大小53.93MB,结构清晰便于按模块查阅。已有29人学习浏览。该课程设计答辩平均分达96分,代码经实际测试可运行,可直接复现实验效果;基础较好的读者还可基于现有框架替换算法或扩展功能,用于学科竞赛或进一步研究。
1. 垃圾短信识别不只是分类:一个课程设计背后的数据挖掘闭环
提到垃圾短信识别,多数人第一反应是“用朴素贝叶斯分个类”,代码跑完准确率九十几,课设就算交差了。但如果你真拿这份思维去做网络数据挖掘课程设计,多半会在答辩时被问住:准确率高是因为测试集里垃圾短信占比本来就不低,还是模型真的学到了“转账”“中奖”“退订回T”这些信号?换一批真实短信,精确率掉多少?这个问题的核心其实不在分类器,而在于你是否把网络数据挖掘的完整闭环走通了——从数据采集、标注、清洗、分词,到特征表示、模型训练、评估、调优,再到一个能被老师或评委现场点开的演示界面。这篇文章按“理论到落地”的顺序,把这个闭环里的每一步拆开讲,所有代码都基于 Python 和 sklearn,可以直接对着跑。适合正在做课设、实训、大作业的学生,也适合想快速搭一套短信识别基线系统的工程师。
2. 把短信识别建模成文本分类:标签、噪声与评估口径
2.1 垃圾短信识别本质上是什么问题
从网络数据挖掘的角度看,垃圾短信识别属于典型的短文本二分类任务:给定一条短信内容,预测它属于“正常短信(ham)”还是“垃圾短信(spam)”。但和通用文本分类不同的是,短信有三个天然约束:长度短(通常不超过100字)、噪声多(大量数字、链接、特殊符号、火星文)、口语化严重(“亲,在吗”“【XX银行】”)。这些约束直接决定了后续预处理和特征工程的策略。
常见的误区是一上来就调模型,先跑个深度学习再说。但课程设计和工程落地的区别在于:你要能解释每一步为什么这么做。短信分类的基线方案是:分词 + TF-IDF + 线性分类器(朴素贝叶斯/逻辑回归/SVM),这个组合在公开数据集上通常能到97%以上的F1。深度学习(如TextCNN、FastText)在短文本上确实可以再涨零点几个点,但训练时间和调参成本高,且不容易在答辩时讲清楚特征重要性。我的建议是:先跑通基线,再考虑是否用深度模型做对比实验。
2.2 数据标注:多数课设死在这一步
模型效果的上限由数据决定。网上能找到的公开短信数据集不少,但存在两个问题:一是年代久远,垃圾短信的语义已经变了——以前是“恭喜您中奖”,现在是“【京东】您的验证码为XXXX”“加V信领免费课程”;二是类别分布极度不平衡,正常短信远多于垃圾短信。
如果你打算自己爬取或收集数据,标注规范要提前定好。以下是我常用的标注口径:
- 含营销推广链接且用户无法一键退订的,标为垃圾
- 银行、运营商、政务的验证码和通知短信,即使带链接也标为正常
- 包含“退订回T”但内容为营销推广的,标为垃圾
- 含辱骂、色情、赌博等明显违法内容的,标为垃圾
- 同一号码的短信内容重复度极高(如刷屏式广告),标为垃圾
代码层面,数据格式统一成两列 CSV:
import pandas as pd df = pd.read_csv('sms_raw.csv') # 只保留标签和文本两列,其余列丢弃 df = df[['label', 'text']] # 标签统一为0(正常)和1(垃圾),避免字符串映射出错 df['label'] = df['label'].map({'ham': 0, 'spam': 1}) # 去重:同一文本重复出现多次(如批量群发)保留一条即可 df = df.drop_duplicates(subset='text', keep='first') print(df['label'].value_counts())逻辑说明:先读入原始数据,只保留标签和文本两列;再用map把字符串标签转为数值,这是 sklearn 训练前的必要步骤;最后按文本内容去重——群发短信在原始数据里往往出现很多次,不去重会让模型把“高频”误学成“垃圾”的特征。
2.3 评估指标:只看准确率会翻车
当数据不平衡时,准确率是欺骗性最强的指标。假设数据里 90% 是正常短信,模型什么都不学,全预测成正常短信,准确率也有 90%。所以垃圾短信识别必须看三个指标:
- 精确率(Precision):预测为垃圾的短信里,真的是垃圾的比例。精确率低意味着误杀正常短信,代价很高
- 召回率(Recall):真实垃圾短信里,被正确找出来的比例。召回率低意味着漏放
- F1 值:精确率和召回率的调和平均,综合衡量
在线下评估时,我一般把数据按 7:3 划分训练集和测试集,并用分层抽样保证两边的类别比例一致:
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( df['text'], df['label'], test_size=0.3, random_state=42, stratify=df['label'] # 分层抽样,保证训练/测试的类别比例相同 ) print(X_train.shape, X_test.shape)逻辑说明:stratify参数按标签比例分层抽样,避免随机划分时测试集里恰好没有垃圾短信。random_state=42固定随机种子,保证每次跑出来的结果可复现,这在你写实验报告时要贴多次实验结果时会很有用。
提示:答辩时被问“为什么用F1不用准确率”,就回答“类别不平衡下准确率无法反映少数类(垃圾短信)的识别效果,而F1同时惩罚误杀和漏放”。
3. 数据清洗与中文分词:垃圾短信识别的第一道槛
3.1 中文短信为什么必须清洗
短信数据里有三类东西对分类是纯噪声:HTML实体( 、&)、URL 链接、以及“【】”包裹的签名。URL 本身不能说明短信是否垃圾——银行短信也带链接——但 URL 的特征(短链、免费域名)是有用的。所以在清洗时,我通常把 URL 归一化成URL占位符而不是直接删掉,让模型自己学习带链接的短信和垃圾之间的相关性。
3.2 清洗规则和代码实现
import re def clean_sms(text): # 去除HTML实体 text = re.sub(r'&[a-zA-Z]+;', ' ', text) # 统一把URL替换为占位符 text = re.sub(r'http[s]?://\S+|www\.\S+', ' URL ', text) # 去除@和#话题符号(微博类数据常见) text = re.sub(r'[@#][\w\u4e00-\u9fa5]+', ' ', text) # 数字统一替换:手机号、QQ号、验证码都可能是垃圾短信特征 text = re.sub(r'\d+', ' NUM ', text) # 连续重复的标点符号合并,如!!!变成! text = re.sub(r'([!?。!?])\1+', r'\1', text) # 去除多余空白 text = re.sub(r'\s+', ' ', text).strip() return text df['clean_text'] = df['text'].apply(clean_sms)参数说明:\S+匹配不包含空格的一串字符,能覆盖大多数 URL 形态;[\w\u4e00-\u9fa5]同时匹配中英文和数字;数字归一化是短信场景的关键——验证码短信和诈骗短信都大量出现数字,如果保留原始数字会引入海量稀疏特征。
3.3 分词:jieba 在短信上要加自定义词典
中文分词是特征工程的前置步骤。jieba.lcut是最常见的做法,但默认词典有两个短板:一是互联网新词(如“加V”“秒回”“tg”)分不出来;二是短信里大量品牌名(如“支付宝”“京东”)会被切碎。解决方案是维护一个自定义词典,把短信语料里反复出现的高频词加进去。
import jieba # 自定义词典,每行一个词,可带词频和词性:加V 100 n jieba.load_userdict('sms_dict.txt') # 开启并行分词,适合大批量预处理 jieba.enable_parallel(4) def tokenize(text): # jieba.lcut返回词列表,过滤单字符词和无意义符号 words = [w for w in jieba.lcut(text) if len(w.strip()) > 1] return words df['tokens'] = df['clean_text'].apply(tokenize)逻辑说明:load_userdict加载自定义词典后,词典里的词在分词时会被优先识别为完整词;enable_parallel(4)用4个进程并行分词,几万条短信数据能明显提速。过滤单字符词的目的是减少特征维度——“的”“了”“在”这类词既不携带分类信息,又会把TF-IDF矩阵撑大。
停用词表要不要用?我的结论是:在短信场景下,不要用通用中文停用词表。因为“退订”“回复”“免费”这些词在通用语料里不算停用词,但在短信里恰恰是强特征。真正需要过滤的是标点符号和单个数字占位符(NUM),以及“的、了、吗、呢”这类纯语气词。如果用了完整停用词表,有可能把“免费”这种词给误删——你需要对照停用词表确认一下再决定。
4. 特征工程与TF-IDF:让模型看见“转账”“中奖”“退订”的信号
4.1 TF-IDF 为什么适合短文本
词袋模型(Bag of Words)会给每个词赋予一个计数,但“的”“了”这类高频词会淹没真正有区分度的词。TF-IDF 的核心思想是:一个词在一条短信里出现次数越多越重要(TF大),但在所有短信里出现次数越少越有区分度(IDF大)。垃圾短信识别里,“转账”在几万条正常短信里几乎不出现,所以在垃圾短信中出现一次,TF-IDF 值会被放大;而“的”几乎每条短信都有,IDF 趋近于0,自然被压下去。
4.2 特征向量化的参数怎么调
在 sklearn 中,TfidfVectorizer是标准入口。下面这组参数是我的默认配置,经过多个数据集验证效果稳定:
from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer( tokenizer=tokenize, # 使用jieba分词函数 ngram_range=(1, 2), # 保留单个词和相邻两个词的组合 max_features=20000, # 限制最大特征数,防止维度爆炸 min_df=2, # 至少在2条短信中出现过才保留 sublinear_tf=True # TF用1+log(DF)替代原始计数 ) X_train_vec = vectorizer.fit_transform(X_train) X_test_vec = vectorizer.transform(X_test) print('特征维度:', X_train_vec.shape[1])参数说明:ngram_range=(1, 2)让“免费”和“免费 领取”都成为特征,能捕捉短语级的语义信息,短文本上通常比纯单个词提升1-2%的F1;max_features=20000截断最不重要的低频词,显著降低内存占用;sublinear_tf=True用对数缩放词频,防止某个词在一条短信里反复出现时权重被过度放大。
4.3 从向量到可视化的特征筛选
做完向量化后,有个非常实用的诊断方法:把权重最高的特征打印出来,肉眼判断模型学到了什么。
import numpy as np # 计算每个特征在所有垃圾短信中的平均TF-IDF值 spam_indices = np.where(y_train == 1)[0] mean_tfidf = X_train_vec[spam_indices].mean(axis=0).A1 top_features = np.argsort(mean_tfidf)[::-1][:20] feature_names = vectorizer.get_feature_names_out() for idx in top_features: print(f'{feature_names[idx]}: {mean_tfidf[idx]:.4f}')逻辑说明:先从训练标签中找出所有垃圾短信的行索引,再对TF-IDF矩阵按列求均值,得到每个特征在垃圾短信中的平均权重,最后取Top20。如果打印出来的词是“中奖、领取、加V、免费、点击、链接、退款、保险”,说明特征工程是合理的;如果出现“今天、你好、我们”这类无意义词,说明清洗环节可能出了问题。
提示:这一步和 final 答辩时的“如何验证模型学到了有效特征”密切相关。把这个Top20特征列表截图放到报告里,说服力远大于一张ROC曲线图。
5. 模型选型与阈值调优:朴素贝叶斯、逻辑回归和SVM的实际差异
5.1 三种线性模型的定位与选择
TF-IDF 矩阵的特点是稀疏、高维、离散。适合这种数据分布的经典模型有三个:
- 朴素贝叶斯(MultinomialNB):假设特征之间相互独立,训练极快,在短文本分类上是强基线,对稀疏数据友好
- 逻辑回归(LogisticRegression):不假设特征独立,可解释性强,能输出概率值,适合做阈值调整
- 线性SVM(LinearSVC):在文本分类上通常优于朴素贝叶斯,但默认只输出类别,不输出概率
我的建议是:课程设计至少要跑朴素贝叶斯和逻辑回归两个模型做对比,因为它们在sklearn里都是几行代码的事。SVM可以作为进阶对比项,但没必要在上面花太多时间调参,线性核就够了。
5.2 训练与评估的完整代码
from sklearn.naive_bayes import MultinomialNB from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, confusion_matrix # 朴素贝叶斯:alpha是拉普拉斯平滑系数 nb = MultinomialNB(alpha=1.0) nb.fit(X_train_vec, y_train) y_pred_nb = nb.predict(X_test_vec) # 逻辑回归:max_iter要调大,否则可能不收敛;C控制正则化强度 lr = LogisticRegression(C=2.0, max_iter=1000, solver='liblinear') lr.fit(X_train_vec, y_train) y_pred_lr = lr.predict(X_test_vec) for name, y_pred in [('Naive Bayes', y_pred_nb), ('Logistic Regression', y_pred_lr)]: print('=' * 20, name, '=' * 20) print(classification_report(y_test, y_pred, target_names=['正常', '垃圾']))参数说明:alpha=1.0是朴素贝叶斯的平滑参数,防止某个词在训练集中未出现导致概率为0;solver='liblinear'适合小数据集和二分类问题,线性SVM相关的方法也可以直接用;C=2.0是正则化系数的倒数,C越大正则化越弱,模型越容易过拟合——短信特征维度很高,C不建议超过10。
5.3 混淆矩阵:找到误杀还是漏放是关键
F1 只能告诉你好不好,混淆矩阵才能告诉你怎么改:
import matplotlib.pyplot as plt import seaborn as sns cm = confusion_matrix(y_test, y_pred_lr) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=['正常', '垃圾'], yticklabels=['正常', '垃圾']) plt.xlabel('预测标签') plt.ylabel('真实标签') plt.savefig('confusion_matrix.png', dpi=150)如果右下角(真实垃圾预测垃圾)远大于右上角(真实垃圾预测正常),说明漏放很少;如果左下角(真实正常预测垃圾)不为0,说明存在误杀。实际业务中误杀一条正常短信的代价远高于漏放一条垃圾短信——被误杀的可能是一条银行验证码。所以下一步要用概率阈值来调节误杀和漏放的平衡。
6. 概率校准与阈值选择:把精确率召回率调到你想要的点
6.1 使用 predict_proba 而不是 predict
predict默认以0.5为阈值,但业务上往往需要更保守或更激进的拦截策略。这时要改用predict_proba输出每条短信属于垃圾的概率,然后自己设定阈值:
prob_lr = lr.predict_proba(X_test_vec)[:, 1] # 取垃圾短信的概率 threshold = 0.6 # 只有概率超过0.6才判定为垃圾 y_pred_tuned = (prob_lr >= threshold).astype(int) # 对比不同阈值下的精确率和召回率 for t in [0.3, 0.4, 0.5, 0.6, 0.7]: pred_t = (prob_lr >= t).astype(int) tn, fp, fn, tp = confusion_matrix(y_test, pred_t).ravel() precision = tp / (tp + fp) if tp + fp > 0 else 0 recall = tp / (tp + fn) if tp + fn > 0 else 0 print(f'阈值={t:.1f} 精确率={precision:.3f} 召回率={recall:.3f} F1={2*precision*recall/(precision+recall):.3f}')逻辑说明:阈值提高,只有模型非常有把握时才判定为垃圾,误杀减少但漏放增加;阈值降低则相反。打印出来的表格可以直接放进实验报告里,是“阈值调优”这个环节最有力的证据。
6.2 从离线到线上:用 joblib 保存模型和向量器
模型训练结束后,向量器和分类器必须一起保存,因为线上预测时要用同一个词表和IDF值去变换输入数据:
import joblib # 保存向量器和模型到一个pipeline from sklearn.pipeline import make_pipeline pipeline = make_pipeline(vectorizer, lr) pipeline.fit(df['text'], df['label']) # 用全量数据再训练一次 joblib.dump(pipeline, 'sms_spam_pipeline.pkl') # 加载后预测新短信 loaded = joblib.load('sms_spam_pipeline.pkl') new_sms = ['恭喜您被选为幸运用户,点击链接领取iPhone15'] print('垃圾概率:', loaded.predict_proba(new_sms)[0][1])参数说明:make_pipeline把向量化和分类器组装成一个完整对象,避免预测时忘记做分词或IDF变换;predict_proba返回的是一个二维数组,第一个索引是样本序号,第二个索引是标签为垃圾的概率。
6.3 最后一公里:给课设加一个命令行交互界面
课程设计和实际系统的差别在于交付形态。哪怕不上Web框架,一个最简单的命令行循环就能让评委现场输入短信、看到识别结果:
while True: sms = input('输入短信(回车退出): ') if not sms: break prob = loaded.predict_proba([sms])[0][1] label = '垃圾短信' if prob >= 0.6 else '正常短信' print(f'分类: {label} | 垃圾概率: {prob:.2%}')这一段代码加上前面的离线评估结果,已经可以构成网络数据挖掘课程设计的完整交付:数据清洗、分词、特征工程、模型训练、评估、阈值调优、成果落地。如果评委问“垃圾短信识别系统的核心难点是什么”,可以回答“短文本噪声大、特征稀疏、类别不平衡导致评估困难”而不是“模型越复杂越好”。最后记得把vectorizer、模型、混淆矩阵图、阈值对比表全部打包进报告,这套体系足够撑起一次高分答辩。
本文还有配套的精品资源,点击获取