ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

手写分类器实战:中文垃圾短信识别从分词到模型全流程解析

2026/9/24 18:04:33 拓冰建站 浏览量
手写分类器实战:中文垃圾短信识别从分词到模型全流程解析 简介这是一份面向计算机相关专业毕业设计或课程作业的Python中文垃圾短信识别项目核心为手写分类器实现涵盖朴素贝叶斯、逻辑回归、感知机等经典模型可用于短信文本分类与垃圾信息过滤。项目附带完整文档说明和可直接运行的源码压缩包共23个文件包含12个Python脚本、7个模型序列化文件pkl、2个txt标注数据及说明文档整体大小47.94MB目录结构清晰便于按模型训练、测试和接口调用模块快速上手。目前已有52人学习/下载。通过该资源读者可掌握中文文本分词、特征向量构建与分类器训练调优的完整流程也能直接复用代码完成毕设中的功能演示、实验对比与论文撰写支撑适合需要快速落地项目的学生与Python学习者。1. 手写分类器识别中文垃圾短信这个毕业设计项目到底值不值得复现中文垃圾短信识别是自然语言处理领域一个特别适合当毕设的题目数据好找、流程完整、可视化也容易出效果。这个项目最大的特点是手写分类器——朴素贝叶斯、逻辑回归、感知机三个算法全部自己实现不直接调sklearn同时又把sklearn版本作为对照组一并训练保存。这意味着你拿到的不是一份调包代码而是能讲清楚原理、能应付答辩追问的完整实现。适合正在做课程设计、期末大作业或毕业设计的计算机相关专业学生也适合想快速跑通一个文本分类全流程的Python学习者。项目评审分99分代码结构清晰纯Python实现跑通不难。2. 数据准备与中文分词带标签短信怎么变成可训练的模型输入2.1 原始数据到底长什么样带标签与不带标签两份短信项目压缩包里有两个核心数据文件都在data目录下带标签短信.txt和不带标签短信.txt。前者是训练用的每条短信前面有类别标记后者是待预测或者做增量测试用的。这种数据组织形式在中文短信分类里很常见因为垃圾短信的标签获取成本高通常只有一小部分数据是人工标注过的。垃圾短信:恭喜您获得××公司抽奖资格请点击链接领取奖品 正常短信:明天下午三点老地方见别忘了带合同 垃圾短信:您的银行卡在境外消费xxxx元如有疑问请拨打xxx上面三行只是一个格式示意实际文件里每行是一条完整记录用冒号或制表符把标签和正文分开。第一次拿到数据时我建议先用文本编辑器打开看一眼编码和分隔符Python的open()函数读文件时90%的乱码问题都出在编码上这个项目里txt文件基本是UTF-8或GBK编码读取时统一指定encodingutf-8最省事。2.2 jieba分词与token_and_save_to_file.py的预处理逻辑中文文本分类的第一步永远是分词。英文按空格切就行中文不行你好世界究竟是你好/世界还是你/好世界机器不知道。这个项目用的是jieba分词库它基于前缀词典实现高效的词图扫描再通过动态规划找出最大概率路径对未登录词用HMM模型和Viterbi算法处理。token_and_save_to_file.py就是负责把原始短信切词并保存成中间结果的脚本。import jieba import os def tokenize_file(input_path, output_path, encodingutf-8): 对短信文件逐行分词保留标签前缀 with open(input_path, r, encodingencoding) as f_in: lines f_in.readlines() with open(output_path, w, encodingutf-8) as f_out: for line in lines: line line.strip() if not line: continue # 按冒号切分标签与内容 parts line.split(:, 1) if len(parts) 2: label, content parts[0], parts[1] else: label, content , line # 精确模式分词返回list words jieba.lcut(content) f_out.write(f{label}\t{ .join(words)}\n) if __name__ __main__: tokenize_file(data/带标签短信.txt, data/tokenized_train.txt)这段代码的逻辑是逐行读入原始短信用冒号切开标签和正文然后对正文调用jieba.lcut()做精确模式分词最后把标签空格分词结果写入新文件。split(:, 1)里的第二个参数1表示只切第一刀防止短信正文本身包含冒号导致标签切错。分词模式这里用的是精确模式它适合文本分类这种需要稳定词边界的场景搜索引擎用的全模式和提取关键词用的搜索引擎模式在这里都不合适因为全模式会产生冗余词对干扰分类器训练。2.3 分词结果怎么验证test_jieba.py与常见分词坑分词脚本跑完之后先别急着喂给分类器一定要先肉眼检查分词质量。test_jieba.py是一个独立的小脚本专门用来测试jieba在当前数据上的分词效果它接收一句话输出分词结果方便你调试自定义词典。import jieba def test_segmentation(sentence, user_dictNone): 测试单句分词效果可加载自定义词典 if user_dict: jieba.load_userdict(user_dict) seg_list jieba.lcut(sentence) print(分词结果: | .join(seg_list)) if __name__ __main__: test_segmentation(恭喜您获得抽奖资格请点击www.xxx.com领取)运行这个脚本你会发现抽奖领取恭喜这些词会被正确切开但www.xxx.com这类URL可能被切成散碎的字母和符号。这是中文分词最常见的问题——英文、数字、URL混合内容需要单独处理。一般做法是在分词前用正则把URL替换成URL占位符把手机号替换成PHONE占位符这样分类器学到的不是某个具体链接而是这条短信包含链接这个抽象特征。垃圾短信里链接是强特征做占位符替换后识别效果明显提升。分词这一步直接决定了后续向量化和分类的上限词切得不好后面做再多优化都是白费。我一般会抽样看50条分词结果确认没有系统性错误再往下走这个习惯帮我躲过不少预处理层面的翻车。3. 特征工程与模型管理vsm.pkl和model_manage.py如何把文本变成数字3.1 VSM向量空间模型的构建思路与参数细节分词完成之后每条短信还是一串词不能直接喂给分类器。你需要把文本转成固定维度的数值向量这里的核心工具是词袋模型也叫向量空间模型VSM。整个项目的特征工程核心产出是model/vsm.pkl它保存的其实就是词典和词汇到索引的映射关系。import pickle import math from collections import Counter class VSM: def __init__(self, max_features5000): self.vocab {} self.idf {} self.max_features max_features def build_vocab(self, corpus): corpus是分词后的文本列表统计词频并构建词典 counter Counter() for text in corpus: words text.split() counter.update(set(words)) # 用set去重,统计文档频率 # 取词频最高的前max_features个词 top_words [w for w, c in counter.most_common(self.max_features)] self.vocab {w: idx for idx, w in enumerate(top_words)} def transform(self, text): 将文本转成tf-idf向量 words text.split() vec [0.0] * len(self.vocab) word_count len(words) if word_count 0: return vec for w in words: if w in self.vocab: vec[self.vocab[w]] 1 # tf归一化并乘以idf for idx, tf in enumerate(vec): if tf 0: vec[idx] (tf / word_count) return vec这段代码展示了VSM构建的核心逻辑统计词频、按词频截断、建立词到索引的映射、转换文本为向量。注意counter.update(set(words))用的是词集合而非词列表目的是计算文档频率——一个词在一篇文档里出现10次和1次对文档频率的贡献一样。max_features5000是个重要参数中文短信的词表通常在几万量级但低频词对分类贡献很小甚至带来噪声截断到5000~10000词是常见的做法既能控制向量维度又能保留强区分度的特征。3.2 model_manage.py的模型管理逻辑训练、保存、加载一条线model_manage.py是这个项目里承上启下的模块它把特征工程、模型训练、模型持久化串到一起。训练好的分类器统一用pickle序列化保存到model目录下次预测时直接加载不需要重新训练。import pickle import os from classifier.NaiveBayesian import NaiveBayesian from classifier.LogisticRegression import LogisticRegression from classifier.Perceptron import Perceptron class ModelManager: def __init__(self, model_dirmodel): self.model_dir model_dir os.makedirs(model_dir, exist_okTrue) def save_model(self, model, name): 把模型对象序列化保存到pkl文件 path os.path.join(self.model_dir, f{name}.pkl) with open(path, wb) as f: pickle.dump(model, f) print(f模型已保存: {path}) def load_model(self, name): 从pkl文件加载模型 path os.path.join(self.model_dir, f{name}.pkl) with open(path, rb) as f: return pickle.load(f) def train_and_save_all(self, X, y): 训练三个手写分类器并保存 models { NaiveBayesian: NaiveBayesian(), LogisticRegression: LogisticRegression(learning_rate0.1, epochs50), Perceptron: Perceptron(learning_rate0.05, epochs30) } for name, model in models.items(): model.fit(X, y) self.save_model(model, name)这段代码把整个训练流程封装成了一个管理器。save_model用pickle.dump把内存中的模型对象写到磁盘load_model反向加载。注意pickle保存的是整个对象状态包括模型参数和内部数据结构所以加载后可以直接调用predict()方法不需要重新训练。这也是为什么项目能提供NaiveBayesian.pkl、LogisticRegression.pkl、Perceptron.pkl这些预训练模型文件——它们就是训练完成后序列化的结果在你不想等待训练的时候可以直接加载来跑预测。3.3 sklearn对照模型的训练与保存为什么要做对照组项目里还有三个带sklearn标识的pkl文件Logistic_sklearn.pkl、SVM_sklearn.pkl、Bayes_sklearn.pkl。这三个是标准库实现用来和手写分类器做效果对比的。在毕业设计答辩里这个对照组非常加分——它能直接证明你手写的分类器达到了和不调库版本相近的准确率说明你确实理解了算法本质。from sklearn.naive_bayes import MultinomialNB from sklearn.linear_model import LogisticRegression from sklearn.svm import LinearSVC import pickle def train_sklearn_baseline(X, y, model_dirmodel): 训练sklearn基线模型用于和手写分类器对比 models { Bayes_sklearn: MultinomialNB(alpha1.0), Logistic_sklearn: LogisticRegression(C1.0, max_iter200), SVM_sklearn: LinearSVC(C1.0, max_iter200) } for name, model in models.items(): model.fit(X, y) path f{model_dir}/{name}.pkl with open(path, wb) as f: pickle.dump(model, f) print(f{name} 训练完成, 精确率{model.score(X, y):.4f})这段代码训练三个sklearn模型并保存。MultinomialNB是多项式朴素贝叶斯适合离散的TF向量LogisticRegression(C1.0)用L2正则防止过拟合LinearSVC是线性支持向量机在文本分类上通常表现很好。X是前面VSM转换出来的向量矩阵model.score(X, y)输出的是在训练集上的精确率这个数字只能说明模型收敛了不能当最终指标。真正要看的指标是在测试集上的准确率、精确率、召回率和F1值这个在后面的测试脚本里会体现。sklearn模型跑出来的效果通常比手写版本好一点点这是正常的因为标准库在数值稳定性、收敛条件上做了很多工业级优化。你不需要让手写分类器全面超越sklearn只要差距在5个点以内答辩老师就认可你的实现是有效的。4. 手写分类器的实现细节朴素贝叶斯、逻辑回归、感知机三个算法的代码解读4.1 朴素贝叶斯先验概率与条件概率怎么算朴素贝叶斯是文本分类最经典的算法它的核心假设是特征条件独立——在已知类别的情况下每个词出现的概率互不影响。这个假设在现实中不成立但分类效果出奇地好。classifier/NaiveBayesian.py实现了这个算法。import numpy as np class NaiveBayesian: def __init__(self, alpha1.0): self.alpha alpha # 拉普拉斯平滑系数 self.class_prior {} # 类别先验概率 self.cond_prob {} # 条件概率 P(词|类别) self.classes None def fit(self, X, y): self.classes np.unique(y) for c in self.classes: X_c X[y c] # 先验概率 类别样本数 / 总样本数 self.class_prior[c] len(X_c) / len(X) # 条件概率: 每个特征在该类别下的频率 平滑 total_count X_c.sum(axis0) self.alpha total total_count.sum() self.cond_prob[c] np.log(total_count / total) def predict(self, X): results [] for x in X: probs {} for c in self.classes: # 对数概率求和,避免小数下溢 score np.log(self.class_prior[c]) np.sum(x * self.cond_prob[c]) probs[c] score results.append(max(probs, keyprobs.get)) return np.array(results)这里的实现有几个关键细节。第一total_count X_c.sum(axis0) self.alpha是拉普拉斯平滑alpha1.0意味着每个词在计算条件概率时至少贡献一次计数避免某些词在训练集中没出现导致概率为0。第二np.log(total_count / total)取了对数这是因为朴素贝叶斯计算的是所有词概率的乘积几百个词乘下来数值会小到浮点数无法表示取对数可以把乘法变成加法在数学上等价但数值稳定得多。第三predict阶段对每个类别计算对数得分取最大值对应的类别作为预测结果。4.2 逻辑回归手写梯度下降与参数更新逻辑回归本质上是线性回归加了一个sigmoid函数把输出压缩到0到1之间输出值被解释为正类的概率。classifier/LogisticRegression.py实现了这个算法核心是梯度下降优化。import numpy as np class LogisticRegression: def __init__(self, learning_rate0.1, epochs100): self.lr learning_rate self.epochs epochs self.weights None self.bias 0 def _sigmoid(self, z): return 1 / (1 np.exp(-np.clip(z, -500, 500))) def fit(self, X, y): n_samples, n_features X.shape self.weights np.zeros(n_features) y_binary np.array([1 if label 垃圾短信 else 0 for label in y]) for epoch in range(self.epochs): # 线性得分 sigmoid linear np.dot(X, self.weights) self.bias predictions self._sigmoid(linear) # 梯度计算 dw (1 / n_samples) * np.dot(X.T, (predictions - y_binary)) db (1 / n_samples) * np.sum(predictions - y_binary) # 参数更新 self.weights - self.lr * dw self.bias - self.lr * db if epoch % 10 0: loss -np.mean(y_binary * np.log(predictions 1e-9) (1 - y_binary) * np.log(1 - predictions 1e-9)) print(fEpoch {epoch}, Loss: {loss:.4f}) def predict(self, X): linear np.dot(X, self.weights) self.bias prob self._sigmoid(linear) return np.array([垃圾短信 if p 0.5 else 正常短信 for p in prob])这段代码里有几个工程细节值得注意。np.clip(z, -500, 500)防止输入过大导致sigmoid溢出——当z很大时exp(-z)会变成0导致除零错误。损失函数用的是交叉熵而不是均方误差交叉熵在分类问题里梯度更稳定收敛更快。learning_rate0.1和epochs100是需要调节的超参数学习率太大损失会震荡太小收敛慢特征维度高的时候epochs可以适当调大。predict阶段阈值默认0.5如果你想减少漏报可以把阈值降到0.4想减少误报就提到0.6这是一个很实用的调整杠杆。4.3 感知机最简单的神经网络原型感知机是神经网络的基础原型它的思想极为朴素给每个特征一个权重预测时计算加权和如果超过阈值预测为正类否则为负类每次预测错了就更新权重和偏置。import numpy as np class Perceptron: def __init__(self, learning_rate0.05, epochs30): self.lr learning_rate self.epochs epochs self.weights None self.bias 0 def fit(self, X, y): n_samples, n_features X.shape self.weights np.zeros(n_features) y_binary np.array([1 if label 垃圾短信 else 0 for label in y]) for epoch in range(self.epochs): wrong_count 0 for idx in range(n_samples): # 线性得分 linear np.dot(X[idx], self.weights) self.bias # 阶跃函数预测 y_pred 1 if linear 0 else 0 # 预测错误时更新参数 error y_binary[idx] - y_pred if error ! 0: self.weights self.lr * error * X[idx] self.bias self.lr * error wrong_count 1 if wrong_count 0: print(fEpoch {epoch} 收敛, 误分类数0) break def predict(self, X): linear np.dot(X, self.weights) self.bias return np.array([垃圾短信 if v 0 else 正常短信 for v in linear])感知机的更新策略和逻辑回归不同逻辑回归每次训练都会更新参数感知机只在预测错误的时候更新更新公式是w w lr * error * x如果样本被正确分类error为0不更新如果正类被误判为负类error为1权重朝向x增加的方向调整如果负类被误判为正类error为-1权重反向调整。感知机的收敛前提是数据线性可分如果数据本身线性不可分它永远不会收敛到零误差这时候设置epochs30相当于做最大迭代次数的约束防止无限循环。感知机无法输出概率只能给出硬分类这是它和逻辑回归的本质区别。4.4 模型持久化与加载边界pickle文件直接用的注意事项model/目录下除了三个手写分类器的pkl还有vsm.pkl。这里有个特别容易踩的坑vsm.pkl保存的是词表映射如果训练特征和预测时用的VSM版本不一致向量维度对不上轻则预测结果全错重则直接报维度不匹配的错误。正确的预测流程必须先加载vsm.pkl用它对输入短信做transform再喂给分类器的pkl。加载顺序反了或者漏了VSM加载是使用这个项目最常见的错误。import pickle def load_models(model_dirmodel): 按正确顺序加载特征工程和分类器 vsm pickle.load(open(f{model_dir}/vsm.pkl, rb)) classifier pickle.load(open(f{model_dir}/LogisticRegression.pkl, rb)) return vsm, classifierpkl文件的加载要注意Python版本兼容问题。Python 3.7到3.10之间的pickle协议基本兼容但如果项目是在Python 3.6下训练的你拿3.11去加载大概率会报UnpicklingError或ModuleNotFoundError。如果遇到这个问题最常见的原因是classifier目录不在sys.path里因为pickle加载自定义类实例时需要能import这个类。解决方法是确保你在项目根目录运行脚本或者手动把项目根目录加到sys.path里。5. 避坑记录中文短信分类里最容易翻车的五个环节5.1 现象读取txt文件后中文全部变成乱码用open(data/带标签短信.txt)直接读文件输出的中文变成了鐑棬这类乱码字符。原因是Windows下的txt文件默认可能是GBK或ANSI编码而Python 3的open()函数默认按UTF-8解码。解决方法是读取时显式指定编码Windows下通常是encodinggbk如果GBK报错再试encodingutf-8。最稳妥的做法是用编辑器打开txt文件看右下角编码提示然后写代码时和你读取时保持一致。从那以后我每次处理中文数据都会在代码里加一行with open(..., encodingutf-8)并准备一个GBK的兜底分支。5.2 现象训练时准确率很高预测时准确率断崖式下跌我在第一次跑这个项目时就遇到这个问题训练集准确率95%多用新短信测试时感觉完全不对。仔细对比后发现是训练分词和预测分词不一致造成的——训练时用了自定义词典预测时没有加载。比如中奖在训练时被切成一个词预测时被jieba切成中和奖特征对不上。解决方法是把分词逻辑封装成同一个函数训练和预测都调用它确保分词配置完全一致。项目里token_and_save_to_file.py和test_judge.py如果各自维护一套分词逻辑就容易出这个问题。5.3 现象内存直接爆掉或训练极其缓慢特征向量维度设置太高比如max_features50000每条短信变成一个50000维的稀疏向量几千条样本训练逻辑回归时内存开销大且训练速度极慢。中文短信实际有效的分类特征通常在几百到几千个词之间max_features5000已经足够覆盖绝大多数情况。你可以先跑一次统计词频分布看看按频率降序取多少个词能覆盖90%以上的词例token occurrence用这个数作为特征维度比盲目拉满高效得多。这个调参思路不只在短信分类里有效任何中文文本分类项目都能用。5.4 现象加载pkl文件报ModuleNotFoundError或AttributeError加载LogisticRegression.pkl时报错说找不到classifier模块或者找不到LogisticRegression类。pickle序列化自定义类实例时会记录类的完整模块路径比如classifier.LogisticRegression.LogisticRegression。如果你把pkl文件拷到另一个目录下运行那个目录没有classifier包加载就会失败。解决方法是保持项目目录结构完整运行的时候从项目根目录调用脚本。如果你非要单独拷pkl文件可以像4.4节那样在加载前手动sys.path.insert(0, classifier所在目录)但这个做法有点玄学不同Python版本表现不一样我还是建议老老实实跑项目根目录。5.5 现象预测结果全是正常短信或者全是垃圾短信模型输出单一类别没有任何区分度。最常见的原因是训练数据极度不平衡——比如带标签短信里95%都是正常短信分类器学到最优策略就是全都预测正常短信因为这样准确率能到95%。解决方法是查看训练集中两个类别的比例如果垃圾短信占比明显低于30%就要考虑过采样、欠采样或在损失函数里加类别权重。对于这个项目最简单的办法是扩充垃圾短信样本让正负样本比例控制在1:1到1:2之间。另外检查向量化是否正确也很重要——如果VSM向量全是零向量模型学不到任何信息输出结果也会是单一类别。6. 端到端验证用test_judge.py跑通一条短信的完整判定链路项目里的test_judge.py和judgeSpamMessage.py是最终的判定脚本它们把VSM加载、模型加载、预处理、预测串成了一条完整链路。我习惯先用一条已知的垃圾短信测试确认链路的每个环节都通了再批量测试其他样本。import pickle import jieba def judge_message(message, vsm, classifier): 对单条短信做垃圾判定 words jieba.lcut(message) text .join(words) vec vsm.transform(text) result classifier.predict([vec]) return result[0] if __name__ __main__: # 加载模型 vsm pickle.load(open(model/vsm.pkl, rb)) model pickle.load(open(model/LogisticRegression.pkl, rb)) # 测试已知垃圾短信 test_msg 恭喜您获得苹果手机一部请点击链接尽快领取逾期作废 print(f预测结果: {judge_message(test_msg, vsm, model)})这一步跑通后你就掌握了这个项目从数据到预测的完整链路。更进一步你可以在judgeSpamMessage.py里把单条预测扩展成批量文件预测把不带标签短信.txt里的每条短信跑一遍输出每条的判定类别和置信度这样的可视化结果放在毕业设计里很加分。我建议你拿到项目后的第一件事不是去读README而是按这个顺序跑一遍先python token_and_save_to_file.py做分词再python model_manage.py训练模型最后python test_judge.py验证单条短信。三个脚本跑通整个项目的脉络就清晰了。之后再去替换自己的数据集改改max_features和learning_rate看看效果变化系统性感受一下参数对模型的影响。这些用最原始的方式手工一次就理解透的细节比背十遍算法原理都管用。希望帮到你。本文还有配套的精品资源点击获取