简介:面向课程设计、毕业设计及期末大作业场景,这份Python机器学习项目完整实现了基于SVM的垃圾短信识别系统。项目源码按模块组织:DataProcess.py完成中文文本预处理,SVM_Trainer.py负责训练分类模型,Message_Classify.py用于预测单条短信类别,并附带基于Apache/PHP的在线演示模块;model目录保存训练好的模型,Data目录提供带标签与无标签短信数据,以及特征、TF-IDF向量等中间结果,方便复现与二次开发。配套的项目说明和设计报告,系统梳理了从数据清洗、中文分词、TF-IDF特征提取到SVM建模与评估的完整流程,可帮助计算机、人工智能、大数据等专业学生快速上手文本分类项目。资源包整体约107.89MB,以Python源码、数据文件、说明文档等为主,压缩包结构清晰,目前已吸引289人学习下载,适合作为课程设计、大作业、初期项目立项演示及SVM入门进阶的参考资料。
1. 当课程设计遇上垃圾短信:SVM 方案为什么还能打
如果你正在为课程设计或期末大作业找一个「机器学习味够浓、又能跑通演示」的项目,垃圾短信识别几乎是最合适的切入点。它不像图像识别那样吃显卡,也不像推荐系统那样依赖大规模数据,一份几千条的带标签短信就能完成「数据预处理 → 特征工程 → SVM 训练 → 在线预测」的完整闭环,正好覆盖机器学习课程的核心知识点。这套基于 Python 的 SVM 垃圾短信识别系统,就是按这个思路组织的:本地脚本负责训练和评估,Apache + PHP 负责把模型包成一个能访问的网页服务,演示完直接截图写报告,工作量一眼看得见。
我用的时候最直接的感受是:项目结构非常「课程设计友好」。model 目录放训练产物,Data 目录集中管理带标签和无标签数据,code 目录下三个 Python 脚本各管一段——预处理、训练、预测,职责清晰到可以照着画系统架构图。适合计科、大数据、人工智能专业的本科生直接用作课程设计或毕业设计的前置验证。接下来我会按真实复现的顺序,把数据格式、SVM 训练参数、网页部署步骤和几个容易翻车的坑逐一拆开讲。
2. 中文短信的向量化:DataProcess.py 与 TF-IDF 特征工程
2.1 先搞懂数据长什么样:label.txt 与 nolabel.txt
整个项目的起点是 Data 目录下的两个文本文件。label.txt 是带标签数据,每一行是一条短信,标签和内容之间用分隔符切开;nolabel.txt 是不带标签的原始短信,训练完模型后用来做效果检验。从课程设计的角度,这两个文件的组织方式决定了后续脚本不用大改。
首次打开 label.txt 时建议先确认两件事:一是标签用什么符号分隔,二是正负样本比例是否失衡到会影响训练。常见格式是「标签\t短信内容」,比如 1 表示垃圾短信、0 表示正常短信。项目里的 DataProcess.py 就是围绕这种格式写的,如果你手里的数据是 CSV 或者标签在末尾,需要先做一次格式统一。
提示:训练前把 label.txt 的行数记下来,后面拆分训练集和测试集时,比例是否合理直接看这两个数就够了。
2.2 预处理脚本到底做了什么
DataProcess.py 的职责是把原始短信文本转成后续 SVM 能接受的数值特征。中文文本和英文不一样,词与词之间没有天然空格,所以第一步永远是分词。项目采用的方案是把 jieba 分词结果按空格拼接,再用 TF-IDF 向量化。来看核心代码:
# -*- coding: utf-8 -*- import jieba from sklearn.feature_extraction.text import TfidfVectorizer import json import numpy as np def load_labeled_data(filepath): texts, labels = [], [] with open(filepath, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line: continue # 按制表符切分,前半是标签,后半是短信内容 parts = line.split('\t') if len(parts) == 2: labels.append(int(parts[0])) texts.append(parts[1]) return texts, labels def preprocess_text(text): # 去掉多余空白字符,统一小写(对中文影响不大,但能兼容英文短信) text = ' '.join(text.split()) # jieba 精确模式分词,结果用空格连接,方便后续 TF-IDF 处理 seg_list = jieba.cut(text, cut_all=False) return ' '.join(seg_list) texts, labels = load_labeled_data('Data/label.txt') # 对每条短信做分词预处理 corpus = [preprocess_text(t) for t in texts] # TF-IDF 向量化:ngram_range 控制特征粒度,max_features 限制特征维度 vectorizer = TfidfVectorizer(ngram_range=(1, 2), max_features=5000) X = vectorizer.fit_transform(corpus) # 保存向量化结果和标签,供训练脚本复用 from scipy.io import mmwrite mmwrite('Data/X.mtx', X) with open('Data/y.json', 'w', encoding='utf-8') as f: json.dump(labels, f) # 保存特征词表,方便排查特征是否合理 with open('Data/feature.json', 'w', encoding='utf-8') as f: json.dump(vectorizer.get_feature_names_out().tolist(), f, ensure_ascii=False)这段代码把预处理和向量化一次性完成,逻辑上有几个关键点值得注意。ngram_range=(1, 2)意味着同时保留单个词和相邻双词作为特征,像「中奖」「恭喜您」这类短信高频双词会被单独捕获;max_features=5000限制了特征维度,防止稀疏矩阵过大拖慢 SVM 训练。分词后的语料以空格连接,是因为 TfidfVectorizer 默认按空白字符切分 token,这与英文文本的处理方式一致。
保存环节同样有讲究。X.mtx 用 Matrix Market 格式存储稀疏矩阵,SVM_Trainer.py 读取时不需要重新分词和向量化,直接进入训练环节。y.json 保存标签列表,feature.json 保存特征词表,后面排查模型问题时对照词表看权重才有依据。做完这一步,Data 目录下就多出了四个文件,它们之间的依赖关系是:X.mtx + y.json 决定训练输入,feature.json 决定特征含义,vec_tfidf 则会在训练脚本里生成。
2.3 特征工程的边界:什么时候该加自定义词表
跑通默认流程不算难,但课程设计答辩时老师大概率会问一句「你做了什么特征工程」。只靠 jieba 分词 + TF-IDF 是基准方案,想加分可以在 DataProcess.py 里加一个自定义词典。比如短信里常见的「代开票」「信用卡提额」这类词,jieba 可能切成「代开 / 票」「信用卡 / 提额」,语义就散了。
# 加载自定义词典,强制 jieba 按领域词切分 jieba.load_userdict('Data/userdict.txt')userdict.txt 每行一个词,格式是「词 词频 词性」,词频可以随便写,比如「代开票 10 n」。加词典后重新跑 DataProcess.py,再对比 feature.json 里是否出现这些完整词。这一步的成本很低,但能体现出对中文分词和领域知识的理解,属于典型的低成本高回报操作。
注意:TfidfVectorizer 的 max_features 如果设得太小,自定义词典里的词可能因为词频低而被裁掉。改完词典后建议同时检查这个参数,特征空间要给领域词留位置。
3. 训练 SVM 模型:核函数选择与参数调优
3.1 SVM_Trainer.py 的训练流程
预处理产出的 X.mtx 和 y.json 直接喂给 SVM_Trainer.py。项目使用的训练脚本核心逻辑是:读取稀疏矩阵 → 划分训练集和测试集 → 用网格搜索找一组好参数 → 训练并保存模型。代码大致如下:
# -*- coding: utf-8 -*- from scipy.io import mmread import json from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.svm import SVC from sklearn.metrics import classification_report, confusion_matrix import joblib # 读取预处理结果 X = mmread('Data/X.mtx') with open('Data/y.json', 'r', encoding='utf-8') as f: y = json.load(f) # 按 8:2 划分训练集和测试集,stratify 保证正负样本比例一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # 候选参数:线性核 + 一组惩罚系数 C param_grid = { 'C': [0.1, 1, 10, 100], 'kernel': ['linear'] } # 5 折交叉验证,用 F1 分数作为评选标准更贴合类别不平衡场景 grid = GridSearchCV( SVC(), param_grid, cv=5, scoring='f1', n_jobs=-1 ) grid.fit(X_train, y_train) print("best params:", grid.best_params_) print("best score:", grid.best_score_) # 用最优参数重新训练并评估 best_model = grid.best_estimator_ y_pred = best_model.predict(X_test) print(classification_report(y_test, y_pred)) print(confusion_matrix(y_test, y_pred)) # 模型和向量器一起保存,预测脚本才能完整还原特征处理过程 joblib.dump(best_model, 'model/svm_model.pkl')训练脚本里有几个参数值得逐一说清。test_size=0.2是常见的 8:2 划分,random_state=42固定随机种子,保证每次运行划分结果一致——这一点在答辩时要能说出来,否则助教跑你的代码两次结果不一样会很难解释。stratify=y是按标签比例分层抽样,垃圾短信通常只占一小部分,若不分层,测试集里可能一条垃圾短信都没有,评估指标会虚高。
GridSearchCV的候选参数只有C和kernel两组,其中核函数只选了 linear。为什么不用 RBF?因为文本 TF-IDF 特征本身就是高维稀疏的,线性核在这个场景下往往已经够用,RBF 核反而更容易过拟合,训练时间也更长。C 是误分类惩罚系数,C 越大模型对训练集的拟合越强,越小越倾向于宽泛的决策边界。候选范围 0.1 到 100 覆盖了从欠拟合到过拟合的典型区间,5 折交叉验证选出来的 C 基本能落在合理范围里。
3.2 评估指标不能只看准确率
课程设计报告里最容易犯的错误是只写 accuracy。垃圾短信识别属于典型的类别不平衡问题,正常短信可能占 90% 以上,如果模型把所有短信都判为正常,准确率也有 90%。所以脚本里特意用scoring='f1'做网格搜索的评选标准,输出里也包含 precision、recall、F1 和混淆矩阵。
这三个指标在这个场景下的含义分别是:精确率高说明判成垃圾短信的里面真垃圾的比例高,误伤少;召回率高说明真正的垃圾短信被抓住的比例高,漏放少;F1 是两者的调和平均,防止为了压误伤而放过大量垃圾短信。报告里建议把分类报告和混淆矩阵的截图都放进去,矩阵里四个格子分别对应 TN、FP、FN、TP,能直接看出模型在哪个方向上犯错更严重。
注意:如果训练后模型把所有样本都判成正常短信,先别急着调参,回去看 label.txt 里的标签分布。正负样本比例如果超过 9:1,F1 分数会明显低于准确率,这时候优先处理类别不平衡问题,比如用 class_weight='balanced' 或在预处理阶段做欠采样。
3.3 模型保存与加载的完整闭环
训练结束后,脚本用joblib.dump保存了训练好的 sklearn 模型,但只保存模型是不够的。Message_Classify.py 在预测新短信时必须先做分词和 TF-IDF 向量化,而且 TfidfVectorizer 的词表必须和训练时完全一致,否则特征维度对不上直接报错。所以我在写这类项目时习惯把 vectorizer 也一并保存:
joblib.dump(vectorizer, 'model/tfidf_vectorizer.pkl')这样预测阶段就是标准的「加载两个 pkl → 分词 → transform → predict」流程,完整闭环。项目本身的代码结构里,预测部分就是按这个思路组织的,课程设计报告里画系统架构图时,模型文件和向量器文件正好作为中间产物串起全流程。
4. 从命令行到网页:Message_Classify.py 预测与 Apache 部署
4.1 单条短信预测的脚本实现
训练好的模型不能只躺在 pkl 文件里,Message_Classify.py 就是用来干活的。它的输入是一条短信文本,输出是垃圾或正常的判定结果。这里直接给出一个兼容项目结构的预测脚本写法:
# -*- coding: utf-8 -*- import jieba import joblib def load_model(): # 加载训练阶段保存的模型和向量器 model = joblib.load('model/svm_model.pkl') vectorizer = joblib.load('model/tfidf_vectorizer.pkl') return model, vectorizer def predict_message(model, vectorizer, text): # 与训练时保持一致:先清洗再分词 text = ' '.join(text.split()) seg_list = jieba.cut(text, cut_all=False) corpus = [' '.join(seg_list)] # transform 而不是 fit_transform,保证使用训练时的词表 X_vec = vectorizer.transform(corpus) pred = model.predict(X_vec)[0] return '垃圾短信' if pred == 1 else '正常短信' if __name__ == '__main__': model, vectorizer = load_model() test_msgs = [ '恭喜您获得某某公司抽奖资格,点击链接领取奖品', '明天下午三点会议室开会,请准时参加' ] for msg in test_msgs: result = predict_message(model, vectorizer, msg) print(f'短信内容: {msg}') print(f'判定结果: {result}') print('---')预测脚本里最容易忽略的是vectorizer.transform而不是fit_transform。fit_transform会重新学习词表,如果新短信里出现训练时没见过的词,特征维度就变了,模型直接无法 predict。使用transform则保持词表固定在训练时的状态,新词会被忽略,这正是文本分类的标准做法。
单条预测没问题后,下一步就是把它接进网页。项目提供的方案是用 PHP 做后端,调 Python 脚本执行预测,再把结果回显到页面。这个架构在课程设计里足够演示,老师看到的是网页交互,底层原理又能讲清楚。
4.2 Apache 目录结构与 PHP 调用 Python 的细节
项目说明里明确写了:把 SPAM_CLASSIFY_online 文件夹放到 Apache 根目录,index.php 放在 Apache 根目录下。这意味着整个在线模块是一个典型的 LAMP 环境——Linux + Apache + MySQL + PHP,外加 Python 2.7 作为预测引擎。目录结构示意如下:
/var/www/html/ ├── index.php # 前端页面,负责收集输入和展示结果 └── SPAM_CLASSIFY_online/ ├── classify.py # PHP 调用的预测脚本 └── model/ # 模型文件副本PHP 调用 Python 的常见做法是使用shell_exec或exec函数,把短信内容作为参数传给 Python 脚本,脚本打印结果后 PHP 捕获输出。核心 PHP 代码类似这样:
<?php if ($_SERVER['REQUEST_METHOD'] === 'POST') { $msg = $_POST['message']; // 转义特殊字符,防止命令行注入 $msg_escaped = escapeshellarg($msg); // 调用 Python 脚本,第二个参数是待预测短信 $output = shell_exec("python classify.py $msg_escaped 2>&1"); // $output 格式约定为一行文本:垃圾短信|正常短信 $result = trim($output); } ?>这里有两个细节值得注意。第一,escapeshellarg必须加,短信内容如果包含单引号或分号,不转义可能导致命令注入,这在课程设计的代码评审里是大扣分项。第二,2>&1的作用是把 Python 的 stderr 也并入 stdout,这样 Python 脚本报错时 PHP 页面能直接看到错误信息,排查问题不用反复翻 Apache 日志。
4.3 Python 2.7 环境下的兼容性处理
项目标注的运行环境是 Python 2.7,这一点在复现时要格外留意。Python 2.7 和 Python 3 在 print 语法、字符串编码、json.load行为上都有差异,如果直接把项目脚本丢到 Python 3 环境里跑,大概率会报 SyntaxError 或编码错误。
我一般建议在课程设计演示时用项目标注的环境,即 Python 2.7 + Apache + PHP。如果本机实在装不上 Python 2.7,退而求其次的方案是把脚本里的print语句改成print()函数,并在文件头部加上from __future__ import print_function, unicode_literals。但要注意 sklearn 的版本兼容——Python 2.7 能用的 sklearn 最高到 0.20.x,新版 sklearn 早已放弃 Python 2 支持,装新版本会直接提示环境不满足。
5. 避坑指南:复现这个项目最容易翻车的五个地方
5.1 第一次训练报错:维度不匹配
现象:运行 SVM_Trainer.py 时,sklearn 报ValueError: X has 5000 features, but SVC is expecting 5001 features before fitting。
原因:DataProcess.py 跑完之后,又手动改过max_features参数重新生成了 X.mtx,但 model 目录下残留了旧模型文件,或者预测脚本里加载的 vectorizer 与当前训练用的 vectorizer 不是同一份。
解决:把 Data 目录下的 X.mtx、y.json、feature.json 全部删掉,重新运行 DataProcess.py,再跑 SVM_Trainer.py。也就是说,凡是改了特征工程的配置,就必须按「预处理 → 训练 → 保存」的顺序完整重跑一遍,不能只重训模型。
5.2 模型把所有短信都判为正常
现象:混淆矩阵显示模型几乎没有预测出垃圾短信,但准确率却很高。
原因:训练集里正常短信占比过高,SVM 为了最小化整体误差,倾向于把所有样本判为多数类。这是类别不平衡问题的典型表现。
解决:在 SVC 中加class_weight='balanced',让模型根据类别频率自动调整权重;或在 DataProcess 阶段对多数类样本进行随机欠采样,让正负样本比例接近 1:1。改完后重新训练,对比 F1 分数的变化。
5.3 中文乱码:预处理环节的编码陷阱
现象:打印分词结果时显示\u6b63\u5e38之类的内容,或者训练时报UnicodeDecodeError。
原因:数据文件不是 UTF-8 编码,可能是 GBK 或 GB2312,Python 2.7 默认按 ASCII 读取文件导致解码失败。
解决:打开 label.txt 确认编码格式,用 VSCode 或 Notepad++ 另存为 UTF-8 without BOM。如果数据量大不方便转换,在open时指定编码:codecs.open(filepath, 'r', 'utf-8'),Python 2.7 下codecs模块比内置open对中文更友好。
5.4 sklearn 版本与 Python 版本不匹配
现象:安装 sklearn 时提示需要 Python 3.x,或 import sklearn 直接报错。
原因:项目运行环境标注的是 Python 2.7,但部分机器安装的 sklearn 是最新版本,只支持 Python 3.7 以上。
解决:如果坚持使用 Python 2.7,用pip install scikit-learn==0.20.4锁定历史版本。如果是课程设计且老师允许,直接把环境迁移到 Python 3.8 + scikit-learn 1.0 以上,同时修改脚本里的 print 语法,这是更省事的路线。
5.5 Apache 页面无法访问 Python 脚本
现象:浏览器打开 index.php 能显示页面,但点击「识别」按钮后空白,Apache 错误日志里有command not found或permission denied。
原因:PHP 调用python命令时,Apache 的 PATH 环境变量里没有 Python 安装路径;或者脚本文件没有执行权限。
解决:在 PHP 代码里改用 Python 的绝对路径,比如shell_exec("/usr/bin/python /var/www/html/SPAM_CLASSIFY_online/classify.py $msg_escaped")。同时给脚本目录加执行权限:chmod -R 755 SPAM_CLASSIFY_online。Windows 环境下注意 Python 路径通常形如C:\Python27\python.exe。
6. 进阶玩法:从课程设计到能写进简历的完整项目
模型跑通只是第一步,如果想让这个课程设计在答辩和简历上更有分量,可以往三个方向做延伸。第一个方向是特征层,目前只用 TF-IDF 统计特征,可以加入短信长度、数字占比、URL 数量等手工特征,和 TF-IDF 拼接成混合特征向量。SVM 对特征缩放敏感,拼接后记得用StandardScaler做标准化:
from sklearn.preprocessing import StandardScaler from scipy.sparse import hstack # 手工特征向量(假设从原始文本提取了 3 个数值特征) import numpy as np extra_features = np.array([[len(msg), digit_ratio, url_count] for msg in texts]) # 标准化数值特征 scaler = StandardScaler() extra_features_scaled = scaler.fit_transform(extra_features) # 与 TF-IDF 稀疏矩阵横向拼接 X_combined = hstack([X, extra_features_scaled])第二个方向是做错误案例分析。把混淆矩阵里 FP 和 FN 的样本各挑 5 条打印出来,逐个分析模型为什么判错。比如某些正常短信包含「退订」「回复 TD」,反而触发了垃圾短信特征;某些广告短信伪装成「快递通知」绕过了模型。把这些案例整理成表格写进课程设计报告,答辩时讲「我从 10 条误判样本里发现了一个规律,然后针对性加了特征」,效果远比「我调参调到 F1 到了 0.97」更有说服力。
第三个方向是把模型评估做扎实。我刚拿到这个项目时第一反应是直接跑训练,后来发现把评估逻辑理清楚更重要:按 8:2 划分训练测试集后,还需要把所有样本重新训练一遍——训练集里那 20% 的测试样本也是宝贵的有标签数据。最终交付给网页服务的模型,应该是在全部 label.txt 上重训的版本:
# 网格搜索确定参数后,用全部数据重训最终模型 final_model = SVC(C=grid.best_params_['C'], kernel='linear', class_weight='balanced') final_model.fit(X, y) joblib.dump(final_model, 'model/svm_final.pkl')这会让模型的泛化能力比只用训练集训练更强,页面演示时的识别效果也更好。从那以后我每次做文本分类的课程设计,都会在报告里留出一节专门写「测试集之外的验证」,比如从 nolabel.txt 里随机抽出几十条未参与训练的真实短信跑一轮人工评估,确认模型在完全陌生的数据上不会崩盘。这套流程走完,这个项目就不只是「能跑的课程设计」,而是一个能讲清楚特征工程、模型选择、部署链路和评估逻辑的完整案例了,希望帮到你。
本文还有配套的精品资源,点击获取