ARTICLE DETAIL

资讯详情

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

基于Python手写中文垃圾短信分类器:分词、朴素贝叶斯与毕业设计实战

基于Python手写中文垃圾短信分类器:分词、朴素贝叶斯与毕业设计实战 简介这份资源是面向计算机相关专业学生与项目实战学习者的中文垃圾短信识别毕业设计项目基于Python实现采用手写分类器方案可帮助读者完成课程设计、期末大作业或毕业设计中的文本分类任务。压缩包共23个文件以12个py源码文件为核心涵盖分词处理、模型管理与垃圾短信判定等模块另含7个pkl模型文件、2个txt短信数据集及md说明文档整体约47.94MB目录结构清晰便于按模块阅读与调试。项目已获导师指导并认可评审分99分代码完整可运行对新手较为友好。读者可从中获得完整的手写分类器实现思路包括朴素贝叶斯、逻辑回归与感知机等模型的训练与对比以及短信分词、特征保存和测试判定的具体代码同时配套文档说明有助于快速理解项目结构与运行流程。目前已有52人学习适合需要文本分类实战经验或毕业设计参考的学习者。1. 中文垃圾短信识别从一条“中奖通知”说起你手机里一定收到过这种短信“恭喜您被抽中一等奖点击链接领取”。它可能是最经典的垃圾短信样本也是几乎所有中文垃圾短信识别项目里第一个被拿来测试的案例。基于 Python 的中文垃圾短信识别核心任务就一件事给一条短信判断它是正常短信还是垃圾短信。而“手写分类器”意味着不直接调用现成的深度学习模型而是从分词、特征提取到分类算法全部自己实现一遍。这恰恰是毕业设计项目最该做的事——你得讲清楚每一步为什么这么做而不是调个库就交差。这个方向适合计算机、软件工程、大数据专业的毕业生也适合想入门 NLP 的 Python 学习者。它不要求 GPU一台普通笔记本就能跑完全流程但能让你真正理解文本分类的完整链路。2. 手写分类器到底“手写”什么拆开中文短信识别的四层结构2.1 为什么中文短信不能直接套用英文文本分类流程英文文本天然以空格分词“You won a prize”直接split()就能得到三个词。中文不行。“恭喜您中奖了”如果按字切分“中”和“奖”单独看都没有“中奖”这个整体含义如果按词切分“恭喜”、“您”、“中奖”才是合理的语义单元。所以中文垃圾短信识别的第一步必须是分词而分词质量直接决定后续特征的好坏。另一个差异是短信文本极短。一条短信通常 20 到 70 个字去掉停用词后有效特征可能只剩十几个。这意味着特征工程必须做得足够精细不能像长文本分类那样依赖词频统计的“大数定律”。常见做法是分词后保留名词、动词、形容词过滤掉“的”“了”“吗”这类高频但无区分度的词再结合 TF-IDF 或词袋模型做向量化。还有一点容易被忽略垃圾短信有很强的模板化特征。比如“点击链接”“退订回T”“限时领取”这些短语反复出现。手写分类器的优势就在于你可以针对这些领域特征做定制化的特征提取而不是把一切交给预训练模型。2.2 从原始短信到特征向量分词、去停用词、向量化三步走先安装依赖。jieba用于中文分词scikit-learn用于特征提取和分类器评估pandas用于数据管理。pip install jieba scikit-learn pandas numpy如果你用的是 PyCharm 或 VSCode在项目根目录建一个requirements.txt把上面四行写进去后续换机器直接pip install -r requirements.txt就能恢复环境。这是毕业设计项目文档里必须写清楚的一步答辩老师很可能会问你“换台电脑怎么跑”。接下来是分词和去停用词的核心代码import jieba import re # 加载停用词表每行一个词 def load_stopwords(pathstopwords.txt): with open(path, r, encodingutf-8) as f: return set(line.strip() for line in f if line.strip()) STOPWORDS load_stopwords() def preprocess(text): # 去掉 URL、电话号码、邮箱等噪声 text re.sub(rhttp[s]?://\S, , text) text re.sub(r\d{11}, , text) text re.sub(r[a-zA-Z0-9._%-][a-zA-Z0-9.-], , text) # 只保留中文和基本标点 text re.sub(r[^\u4e00-\u9fa5。], , text) # jieba 精确模式分词 words jieba.lcut(text) # 过滤停用词和单字 words [w for w in words if w not in STOPWORDS and len(w) 1] return .join(words)这段代码做了四件事去 URL、去手机号、去邮箱、去非中文字符。为什么要去这些因为垃圾短信里的链接和号码是强特征但它们的值每次都变直接作为特征会导致模型只记住“有链接就是垃圾”换一批数据就失效。更好的做法是在特征工程阶段单独构造“是否包含链接”这样的二值特征而不是把链接本身当词。jieba.lcut用的是精确模式适合短信这种短文本。如果你发现某些领域词被切碎了可以用jieba.add_word(退订回T)手动添加自定义词典。停用词表可以从网上找现成的中文停用词表也可以自己根据短信语料统计高频无意义词。向量化用 TF-IDFfrom sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer( max_features5000, # 最多保留 5000 个特征词 ngram_range(1, 2), # 同时考虑单字词和双字词组合 min_df2, # 至少在 2 条短信中出现过才保留 max_df0.9 # 出现在 90% 以上短信中的词丢弃 ) X vectorizer.fit_transform(corpus)ngram_range(1,2)是关键参数。中文里“中奖”是一个词但“中奖了”和“中奖啦”可能被切成不同形式用二元组可以部分缓解这个问题。min_df2过滤掉只出现一次的生僻词max_df0.9过滤掉“你好”“谢谢”这类几乎所有短信都有的词。这三个参数没有绝对最优值需要根据你的数据集规模调整。数据量小于 1000 条时min_df可以设为 1否则特征太稀疏。2.3 手写朴素贝叶斯分类器公式、拉普拉斯平滑与代码实现朴素贝叶斯是文本分类里最适合“手写”的算法。它的假设很简单给定类别各个特征之间相互独立。虽然这个假设在真实语言里不成立但在短文本分类任务上效果出奇地稳。核心公式是P(垃圾|短信) ∝ P(垃圾) × ∏ P(词i|垃圾)取对数后变成加法避免下溢log P(垃圾|短信) log P(垃圾) Σ log P(词i|垃圾)手写实现的关键在于计算每个词在垃圾短信和正常短信中的条件概率并且必须做拉普拉斯平滑否则遇到训练集里没出现过的词概率会变成 0整个乘积归零。import numpy as np from collections import defaultdict class NaiveBayesClassifier: def __init__(self, alpha1.0): self.alpha alpha # 拉普拉斯平滑系数 self.class_prior {} self.word_prob {} self.vocab set() def fit(self, X_words, y): # X_words: 列表每个元素是分词后的词列表 # y: 标签列表0 正常1 垃圾 n len(y) classes set(y) # 统计每个类别的先验概率 for c in classes: self.class_prior[c] (y.count(c) self.alpha) / (n self.alpha * len(classes)) # 统计每个类别下每个词的出现次数 word_count {c: defaultdict(int) for c in classes} total_count {c: 0 for c in classes} for words, label in zip(X_words, y): for w in words: word_count[label][w] 1 total_count[label] 1 self.vocab.add(w) # 计算条件概率带拉普拉斯平滑 V len(self.vocab) for c in classes: self.word_prob[c] {} for w in self.vocab: self.word_prob[c][w] (word_count[c][w] self.alpha) / (total_count[c] self.alpha * V) def predict(self, words): scores {} for c in self.class_prior: score np.log(self.class_prior[c]) for w in words: if w in self.vocab: score np.log(self.word_prob[c][w]) scores[c] score return max(scores, keyscores.get)alpha1.0就是标准拉普拉斯平滑。如果你发现模型对训练集里没见过的词过于敏感可以把alpha调大比如 2.0 或 5.0相当于给每个词一个更强的先验。但调太大也会让所有词的概率趋于均匀区分度下降。一般从 1.0 开始试。预测时只累加在词表里出现过的词没见过的词直接跳过。这是工程上的简化严格来说应该给未知词一个很小的概率但实际效果差别不大。2.4 用混淆矩阵和 F1 值验证分类器到底能不能用训练完不能只看准确率。垃圾短信识别是典型的不平衡分类问题正常短信通常远多于垃圾短信。如果 95% 的短信是正常的你全猜“正常”也有 95% 准确率但这样的模型毫无用处。必须看混淆矩阵和 F1 值from sklearn.metrics import confusion_matrix, classification_report y_pred [model.predict(words) for words in X_test_words] print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred, target_names[正常, 垃圾]))重点看垃圾短信那一类的召回率recall。召回率低意味着大量垃圾短信被漏判。在短信过滤场景里漏判比误判更严重——用户被骚扰一次可能就卸载你的应用了。如果召回率低于 0.85优先调整特征增加ngram_range到(1,3)或者手动添加领域关键词到自定义词典。3. 数据从哪来、怎么洗毕业设计里最容易被卡住的一步3.1 公开中文短信数据集的获取与格式统一常见做法是使用公开的中文短信数据集通常以 CSV 或 TSV 格式提供包含两列标签和短信文本。标签一般用 0/1 或“正常/垃圾”表示。拿到数据后第一件事是统一格式import pandas as pd df pd.read_csv(sms_dataset.csv, encodingutf-8) # 统一列名 df.columns [label, text] # 标签映射正常-0垃圾-1 label_map {正常: 0, 垃圾: 1, ham: 0, spam: 1, 0: 0, 1: 1} df[label] df[label].map(label_map) # 去掉空值和重复值 df df.dropna(subset[text, label]) df df.drop_duplicates(subset[text]) print(df[label].value_counts())value_counts()必须打印。如果垃圾短信只占 5%你就要考虑过采样或调整类别权重。手写分类器里可以通过修改class_prior的计算方式来加权比如给垃圾类一个更大的先验。3.2 短信文本特有的清洗规则链接、号码、签名除了前面preprocess函数里的通用清洗短信还有两个特殊结构签名和退订指令。签名通常是短信末尾的【某某公司】它对判断垃圾短信有一定参考价值但具体公司名每次都不同。建议提取“是否包含签名”作为一个二值特征而不是保留签名文本。退订指令如“退订回T”“回复TD退订”是垃圾短信的强特征因为正常短信很少带这个。可以在预处理阶段检测这些关键词生成一个布尔特征列。def extract_extra_features(text): features {} features[has_url] 1 if re.search(rhttp[s]?://, text) else 0 features[has_phone] 1 if re.search(r\d{11}, text) else 0 features[has_unsubscribe] 1 if re.search(r退订|回T|TD, text) else 0 features[has_signature] 1 if re.search(r【.*?】, text) else 0 features[length] len(text) return features这些特征可以和 TF-IDF 向量拼接一起送入分类器。拼接时注意归一化length的数值范围和其他 0/1 特征差异很大可以用MinMaxScaler缩放到 [0,1]。3.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_size0.2, random_state42, stratifydf[label] )stratify参数是关键。不加它可能测试集里垃圾短信特别少评估结果波动极大。random_state42保证每次运行划分结果一致方便调试和复现。如果垃圾短信比例低于 10%可以在训练时给垃圾类更高的权重。手写朴素贝叶斯里把class_prior的计算改成# 给垃圾类加权weight 根据不平衡比例调整 weight {0: 1.0, 1: 3.0} for c in classes: count y.count(c) * weight[c] total sum(y.count(k) * weight[k] for k in classes) self.class_prior[c] (count self.alpha) / (total self.alpha * len(classes))weight[1]3.0表示垃圾类的先验概率被放大三倍。具体倍数看你的数据一般设为正常类与垃圾类样本数比值的平方根左右。4. 避坑与排查手写分类器最容易翻车的五个地方4.1 分词结果里全是单字特征矩阵稀疏到无法训练现象打印分词结果发现“恭喜您中奖”被切成[恭, 喜, 您, 中, 奖]每个词长度都是 1被len(w) 1全部过滤掉最终每条短信的特征为空。原因jieba默认词典对短信领域词汇覆盖不足尤其是网络新词和变体写法。解决加载自定义词典。把训练集里高频出现的短语统计出来人工筛选后加入userdict.txt每行格式为词语 词频 词性。然后在分词前调用jieba.load_userdict(userdict.txt)。另外把len(w) 1改成len(w) 1但这样会引入大量单字噪声需要配合更严格的停用词表。4.2 测试集准确率 99%换一批短信就崩现象在自己的测试集上准确率很高但拿几条新短信手动测试结果全错。原因过拟合。模型记住了训练集里的具体词汇组合而不是学到泛化特征。常见于max_features设得太大、min_df1的情况。解决降低max_features到 2000 以下提高min_df到 3 或 5增加max_df到 0.95。同时用交叉验证代替单次划分from sklearn.model_selection import cross_val_score scores cross_val_score(pipeline, X_train, y_train, cv5, scoringf1) print(scores.mean(), scores.std())如果交叉验证的 F1 标准差超过 0.05说明模型不稳定需要进一步简化特征。4.3 拉普拉斯平滑系数设错垃圾短信全部漏判现象混淆矩阵显示垃圾短信召回率为 0所有短信都被判为正常。原因alpha设得过大比如 10.0导致所有词的条件概率趋近于 1/V垃圾类和正常类的得分几乎没有差异先验概率中正常类占优于是全部判正常。解决把alpha调回 1.0 或更小。如果数据量很大超过 10 万条可以用alpha0.1。判断标准是看垃圾类召回率如果低于 0.5优先降alpha。4.4 停用词表把“不”“没”“别”过滤掉了语义完全反转现象“不要点击链接”和“要点击链接”分词后特征几乎一样。原因停用词表里包含了否定词。解决检查停用词表确保不、没、别、无、非这些否定词不在里面。如果它们被过滤了手动从停用词表中删除。更好的做法是保留否定词并在特征工程阶段构造“否定词动词”的二元组特征。4.5 用 accuracy 评估模型答辩时被老师问住现象答辩老师问“你的模型在垃圾短信上的召回率是多少”你答不上来。原因只看了accuracy_score没有输出分类报告。解决养成习惯每次评估必须打印classification_report。把垃圾类的 precision、recall、f1-score 三个指标都记下来。如果老师追问“为什么 recall 比 precision 低”你要能解释模型偏向于把不确定的短信判为正常导致漏判多。然后说明你打算怎么改进——比如调整类别权重或增加特征。5. 让手写分类器再稳一点两个进阶技巧和一套验证习惯第一个技巧是特征拼接。把 TF-IDF 向量和手工构造的统计特征拼在一起往往能提升 2 到 5 个百分点的 F1。具体做法是用scipy.sparse.hstack把稀疏矩阵和归一化后的稠密特征合并from scipy.sparse import hstack from sklearn.preprocessing import MinMaxScaler # extra_features 是 DataFrame每行对应一条短信 scaler MinMaxScaler() extra_scaled scaler.fit_transform(extra_features) # X_tfidf 是 TfidfVectorizer 的输出 X_combined hstack([X_tfidf, extra_scaled])注意extra_scaled要转成稀疏矩阵再拼接否则内存会爆。hstack要求两个矩阵行数一致所以特征提取和向量化必须用同一批数据、同一个顺序。第二个技巧是阈值调整。朴素贝叶斯输出的是对数概率你可以通过调整判定阈值来平衡 precision 和 recall。默认是垃圾类得分大于正常类就判垃圾等价于阈值 0.5。如果想把召回率提上去把阈值降到 0.3def predict_with_threshold(words, threshold0.5): scores {} for c in self.class_prior: score np.log(self.class_prior[c]) for w in words: if w in self.vocab: score np.log(self.word_prob[c][w]) scores[c] score # 计算垃圾类的相对概率 diff scores[1] - scores[0] return 1 if diff np.log(threshold / (1 - threshold)) else 0阈值从 0.5 降到 0.3意味着垃圾类只需要相对概率达到 0.3 就被判为垃圾召回率会上升但误判也会增加。具体设多少看你的业务容忍度。毕业设计里可以画一条 precision-recall 曲线把不同阈值下的指标列成表格答辩时展示你对模型行为的理解。验证习惯方面我一般会固定一个“回归测试集”从数据里挑 50 条典型短信包括正常、垃圾、边界模糊三类每次改完代码都跑一遍看预测结果有没有变化。这个习惯能帮你快速发现“改了一个参数结果把之前对的搞错了”这类问题。另外把每次实验的max_features、alpha、min_df和对应的 F1 值记在一个表格里不用很复杂Excel 就行。到写论文的时候这张表就是你的实验对比章节。最后说一个我自己的教训一开始我觉得手写分类器“太简单”想直接上 LSTM。后来发现数据量只有几千条LSTM 训完还不如朴素贝叶斯。手写分类器的价值不在于性能上限而在于你对每一行代码都有控制权知道模型为什么做出某个判断。毕业设计答辩时老师更想听你讲清楚“为什么拉普拉斯平滑能解决零概率问题”而不是“我调了一个预训练模型”。把简单方法做扎实比堆复杂模型更有说服力。希望帮到你。本文还有配套的精品资源点击获取
返回列表