恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
垃圾短信识别:NLP文本分类从TF-IDF到TextCNN的完整实践
首页
资讯中心
/
垃圾短信识别:NLP文本分类从TF-IDF到TextCNN的完整实践
垃圾短信识别:NLP文本分类从TF-IDF到TextCNN的完整实践
发布时间:2026/10/12 13:39:44
简介这是一套面向本科毕业设计及NLP初学者的中文文本分类实战项目聚焦垃圾短信识别覆盖从数据预处理、中文分词、TF-IDF特征提取到SVM模型训练评估的完整流程并支持按需替换为其他分类模型。包内共8个文件包含文本数据集与停用词表、训练好的SVM模型及TF-IDF向量化模型、可直接运行的训练脚本、流程图和说明文档压缩包约38MB结构清晰便于快速复现。数据集采用标签与短信文本以制表符Tab分隔的格式正样本标记为1表示垃圾短信负样本为0表示正常短信同时标明了依赖的Python3.6、jieba与Scikit-learn环境运行提供的训练脚本即可完成训练。目前已有4451人浏览学习适合本科毕业设计、课程设计及NLP文本分类入门实战能帮助理解文本分类特征工程与经典SVM算法在短文本上的应用也可作为后续扩展模型对比实验的起点。1. 本科毕设选垃圾短信识别看着简单其实是 NLP 中文文本分类最稳的切入每年春天都能看到一堆 NLP 毕业设计题目最后都堆在“基于 BERT 的情感分析”或者“基于 LSTM 的新闻分类”上。这些题目听起来体面但对本科阶段时间、数据、算力都有限的情况来说很难做出让人信服的结果。垃圾短信识别这条线反而稳任务边界清晰、样本好找、指标直观而且它覆盖了 NLP 中文文本分类从清洗、分词、表示、建模到评估的完整链条。做好这一件事答辩时能拿出的东西比很多盲目套用大模型的项目扎实得多。这个方向的合适人群很明确有一定 Python 基础、想完整跑一遍 NLP 流程但不想一上来碰预训练大模型的同学。它不要求 GPU也不是非得用 BERT 才叫 NLP数据量千条起步就能看到明显区分度。后面的章节我会直接按“选型、数据准备、模型落地、避坑、验收”拆开讲每一步都给出能直接改的代码和参数照着做就能把题目落地。2. 建模前先选型垃圾短信识别该走哪条文本分类路线2.1 统计模型与 TF-IDF为什么朴素贝叶斯和逻辑回归仍是合格基线很多初学者一上来就自我加码觉得自己不做深度学习就不够“NLP”。实际上垃圾短信识别这个场景有一个鲜明特征特征往往是表面的词面信号而不是深层语义。“中奖”“回 T 退订”“领取话费”这类词和短句出现基本就能判定。这类任务用 TF-IDF 加权后的词袋特征再配一个线性模型往往已经能撑到 90% 左右的准确率完全足够作为整个项目的基线。朴素贝叶斯是教科书里最常提到的文本分类模型但放在真实短信数据上有一个明显短处它假设特征条件独立而短信里“领奖”“点击链接”这样的强相关词会被重复计算。逻辑回归没有这个假设对词与词之间的共现关系更宽容在大规模稀疏特征上收敛也快。所以我的习惯是第一版基线上逻辑回归而不是朴素贝叶斯同时用 TF-IDF 的 sublinear_tf 参数把词频压缩成对数形式避免“退订”出现三次就把概率拉满。这个阶段不用急着上深度学习。基线的作用是给后续模型一把尺子它足够简单、可解释、几乎不可能失败。如果某个深度模型在测试集上的 F1 连这个基线都压不过那一定是数据或训练设置出了问题而不是模型不够先进。2.2 FastText 的入门价值把整条短信变成词包与子词的组合基线之后有很多条路但本科毕设我优先推荐 FastText。它虽然带“Fast”和“Text”本质上是一个线性分类器加词向量平均的结构但有两个对中文短文本非常友好的设计一是支持词语层面的 n-gram 特征比如“中奖”会被当作一个整体特征参与训练二是支持字符子词信息拼写变体也能被捕获。对垃圾短信这种偏短、偏口语化、带有大量营销套话的文本来说词序的重要程度很低。“立即点击领取”和“点击立即领取”意思完全相同顺序对判断是不是垃圾没有实质影响词袋和词平均天然忽略顺序反而成为优点。FastText 训练速度极快几千条短信几秒钟就能跑完一轮方便反复调参这点对时间紧张的毕业设计尤其重要。FastText 的另一个优势是结果稳定。它不像深度模型那样对随机种子敏感同参数下跑出来的指标波动很小这在答辩时非常关键——你不会因为“上星期准确率 92%今天变成 89%”这种玄学问题被老师追问。2.3 TextCNN 与 BERT 的取舍边界小数据量下怎么选深度模型如果想让项目里有真正的“深度模型成分”TextCNN 是最合理的第二站。它把短信当作一串词用多个不同宽度的卷积核并行扫过相当于同时提取“单个词”“相邻两词”“相邻三词”的局部特征之后做最大池化取出最重要的信号。相比 FastText它能保留短距离词序信息对“不 是 垃圾”这类否定结构更敏感。但要注意TextCNN 不适合在数据量很小的场景里直接替换 FastText。我见过同学拿着两千条样本硬训 CNN结果卷积层学到的是大量记忆样本噪声的局部模式验证集指标反而低于逻辑回归。更合理的做法是让 TextCNN 和 FastText 并行存在二者用同一验证集对比精确率和召回率最后让老师看到你的选型推理过程。BERT 在国内答辩中很容易被追问。稳妥策略是把它放进“思考过、但有意不做”的位置说明白它需要更大的数据量、更长的训练时间以及知识蒸馏和部署成本同时指出如果数据集扩充到五万条以上BERT 是值得升级的方向。这比写一行代码加载微软中文 BERT 然后跑不动要体面得多。选型验证阶段我一般会建一张记录表按数据量、训练时间、可解释性、答辩风险四栏打分表格本身就是很好的毕设文档素材。3. 数据与基线首跑把短信语料切成能训练的特征3.1 中文短信文本的清洗函数编码、链接、数字与特殊符号公开渠道能拿到的短信语料大多来自用户主动贡献或公开学术资源格式通常是 CSV字段包括标签和短信正文。下载后第一件事不是分词而是打开文件确认编码。很多语料是从 GBK 或 GB18030 转成 UTF-8 的处理不当后面会不断出现乱码报错。清洗环节的目标不是把文本变漂亮而是降低噪声对特征提取的干扰。短信里最常见的噪声是 URL、电话号码、长数字序列和重复符号它们对分类判断有辅助作用但不能让它们产生过于分散的词汇特征。下面是我常用的一套清洗函数针对垃圾短信场景做了折中处理不把全部数字删掉而是把手机号这类长数字串替换成统一占位符保留“5 万”“100 元”这类短数字因为它们对判断营销短信非常关键。import re def clean_sms(text: str) - str: 清洗单条短信文本返回适合分词的干净字符串 if not text: return text text.lower().strip() # 去掉 URL避免 http、www 后面跟的一长串无意义字符 text re.sub(rhttps?://\S, , text) text re.sub(rwww\.\S, , text) # 邮箱地址替换成占位符 text re.sub(r\S\S\.\S, email , text) # 连续 5 位及以上数字视为电话号码或乱码统一成占位符 text re.sub(r\d{5,}, num , text) # 多余空白压成一个空格 text re.sub(r\s, , text).strip() return text清洗逻辑里最关键的是数字截断阈值。太激进会误删“10086”“95588”这类服务号特征太保守会引入大量不重复的长串数字使词典膨胀、模型过拟合。保留一到四位数字并按原样交给分词器是中文短信场景里一个比较平衡的口径。3.2 分词、停用词与候选特征ngram 窗口该怎么设中文文本分类无法绕过分词。分词质量直接影响特征的粒度。以“双十一特惠”为例正确切分让模型学到“双十一”和“特惠”两组强信号切错了变成“双”“十一”“特惠”信号就被稀释了。我一般直接用 jieba 的精确模式不开全模式。全模式会把句子切成所有可能的词组合特征数量成倍增长但绝大多数是无效冗余对小数据集反而带来噪声。停用词表方面不要照搬网上那份英文停用词表直接跑中文。中文短信里最需要停的是语气词、人称代词和连词“的、了、吗、我、你、它、因为、所以”这类词对文本分类几乎没有信息增益。但小心不要把“回”“退”“订”这些字误放进停用词表它们合起来正是垃圾短信的高频信号。建议自己从训练集中统计高频词把前 300 个词里明显无语义的字词加入停用词表比任何公开通用表都贴合任务。分词之后还需要决定特征窗口。TF-IDF 向量化器里的 ngram_range 是少数几个对效果影响极大的旋钮之一。只用单个词会丢失“立即”“领取”之间的组合关系用三连词则让特征空间迅速爆炸。垃圾短信场景用二元组就够它能表达出“免费领”“点击链接”“回 T 退订”这些营销短语。3.3 训练验证切分与第一个基线跑通TF-IDF 逻辑回归的最小代码准备数据时最容易犯的一个错误是直接随机切分。短信数据里同一条群发内容会以几十遍形式重复出现如果不去重就随机切分同一条文本可能同时落在训练集和测试集里测试指标虚高非常严重。所以切分前一定要先按短信原文做去重再切分确保任意一条唯一文本只出现在一侧。这一步在工程上决定了整个验证过程有没有参考价值。完成了清洗、分词和去重之后第一个基线模型就可以上了。下面这段代码把 TF-IDF 和逻辑回归封装成一个 pipeline半小时内就能看到可复现的数字。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import make_pipeline from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # df 里已包含两列label0 正常 / 1 垃圾text已完成清洗和分词 X_train, X_test, y_train, y_test train_test_split( df[text], df[label], test_size0.2, random_state42, stratifydf[label] # 按类别比例分层切分防止测试集里垃圾短信占比失衡 ) pipeline make_pipeline( TfidfVectorizer( ngram_range(1, 2), # 单个词加相邻两词 max_features80000, # 防止特征维度过大导致训练过慢 sublinear_tfTrue # 对词频取 log弱化高频词的影响 ), LogisticRegression(max_iter1000, C1.0) ) pipeline.fit(X_train, y_train) y_pred pipeline.predict(X_test) print(classification_report(y_test, y_pred, target_names[normal, spam]))sublinear_tf 值得单独说明。垃圾短信里“退订”经常连续出现多次如果按原始词频计算它会压过其他有效特征取对数后次数差距从倍数关系变成加数关系特征分布更平稳。C 是逻辑回归的正则化强度默认 1.0 可以先不动。第一次跑完不要只看准确率重点看 spam 那一行的精确率和召回率。4. 三套模型落地FastText、TextCNN 与阈值工程4.1 FastText 训练文本格式、词向量维度与学习率三个必调项FastText 的训练接口很简单但格式很容易栽跟头。它不读 CSV而是要求每行文本以__label__开头标记类别后面跟已经分好词的句子。这一步必须和你前面跑基线的处理方式完全一致顺序是那句清洗、分词、拼标签、写文件。# 将训练集和测试集分别转成 fasttext 文本格式 def write_ft_file(df, path): with open(path, w, encodingutf-8) as f: for label, text in zip(df[label], df[text]): tag __label__spam if label 1 else __label__normal f.write(f{tag} {text}\n) write_ft_file(train_df, train_ft.txt) write_ft_file(test_df, test_ft.txt) import fasttext model fasttext.train_supervised( train_ft.txt, lr1.0, # 学习率过大会梯度震荡过小收敛很慢 dim50, # 词向量维度小语料 50 就够没必要设 300 epoch25, # 轮数配合学习率一起观察训练 loss wordNgrams2, # 开启词级二元组合特征对中文营销短语很关键 minCount1, # 低频词保留策略小数据集建议保留全部 losssoftmax # 二分类类别少softmax 足够 ) result model.test(test_ft.txt) print(fprecision{result[1]:.4f}, recall{result[2]:.4f})FastText 的调参经验与深度模型差异很大。学习率是最敏感的参数lr1.0是官方文档给出的起点但对中文短文本经常表现过冲日志里训练 loss 下降变慢或忽高忽低时优先把它降到 0.3。dim是个性价比很低的参数从 50 涨到 100 一般只提升零点几个点却不必要地拖慢训练和加载速度。真正影响判断的是wordNgrams2缺了它模型就只能看到孤立词垃圾短信里大量的短语记忆能力会被削弱。最后minCount1在小数据集中能保留“发票”“退订”这类低频强信号数据量在五千以下时建议就设成 1。4.2 TextCNN 落地一个可直接运行的 PyTorch 分类器TextCNN 的原理不复杂但本科项目里自己写容易出一堆维度不对的报错。这里给一个稳定的最小实现模型本身不作为重点装饰而是刻意保持紧凑便于把精力放在数据端和调参上。import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_size100, num_filters128, num_classes2, max_len64): super().__init__() self.embedding nn.Embedding(vocab_size, embed_size, padding_idx0) # 三个卷积核分别覆盖相邻 2 词、3 词、4 词 self.convs nn.ModuleList([ nn.Conv1d(embed_size, num_filters, kernel_sizew) for w in (2, 3, 4) ]) self.dropout nn.Dropout(0.5) self.fc nn.Linear(num_filters * 3, num_classes) def forward(self, x): # x 形状: (batch, max_len) emb self.embedding(x) # (batch, max_len, embed_size) emb emb.transpose(1, 2) # 转成 (batch, embed_size, max_len) pooled [] for conv in self.convs: c torch.relu(conv(emb)) p torch.max_pool1d(c, c.size(2)).squeeze(2) pooled.append(p) cat torch.cat(pooled, dim1) # 三个池化结果拼接 cat self.dropout(cat) return self.fc(cat)向模型喂数据之前要做两件事构建词汇表给每个词一个 ID以及把每条短信 padding 到一个固定长度。短信大多不长max_len64能覆盖九成以上样本过长截断、过短补零。padding 时 ID 用 0和nn.Embedding里的padding_idx0对齐池化时这些位置不会贡献有效信号。训练部分的重点是要加入 dropout 和早停。TextCNN 收敛很快通常十来个 epoch 就到顶继续训练很容易从“记住营销短语”滑向“记住某一条短信原文”。我的做法是每个 epoch 结束后在验证集上算一次 F1如果连续三轮没有提升就保存当前最佳模型并停止。省下来的时间和改进空间会直观反映在你的实验记录里。4.3 阈值调整与类别不平衡模型输出概率如何变成最终判断无论是逻辑回归、FastText 还是 TextCNN模型默认判断阈值为 0.5。但短信语料天然不平衡正常短信占比经常达到九成以上此时 0.5 未必是合理分界。把阈值调高到 0.7 会让模型只在非常确信时才判为垃圾短信换来更低的正常短信误伤率调低到 0.3 则会抓得更全面代价是部分活动邀约或物流提醒被误杀。from sklearn.metrics import confusion_matrix y_proba pipeline.predict_proba(X_test)[:, 1] # 垃圾短信类别的概率 for thresh in [0.3, 0.5, 0.7]: y_pred (y_proba thresh).astype(int) tn, fp, fn, tp confusion_matrix(y_test, y_pred).ravel() precision tp / (tp fp) recall tp / (tp fn) print(fthreshold{thresh:.1f} precision{precision:.3f} recall{recall:.3f})这段代码可以直接先跑在基线模型上用来快速了解数据集中两类样本的重叠程度。对垃圾短信拦截场景我更建议优先保证精确率因为一条正常短信被归入垃圾的体验代价远高于放过一条广告。这一点可以在毕业设计论文里写成明确的系统设计取舍比拍脑袋定一个阈值有说服力得多。5. 垃圾短信识别避坑五条把准确率拖下来的踩坑记录5.1 重复短信泄漏训练集和测试集同源导致分数虚高现象测试集准确率超过 96%但换到少量手工标注的新短信上明显下降一查发现测试集中大量文本和训练集重复。原因短信是群发性质的营销内容经常被几百万用户同时上传到语料库。直接随机切分时同一条文本被同时分到两侧模型等于背下了标准答案。解决在切分前先对清洗后的短信原文做哈希去重并保证某条文本只进入训练集、测试集或验证集中任意一侧。我的做法是建立文本到集合的分组键用df.drop_duplicates(subset[text])再加多次分组切分来避免串集。5.2 分词把关键短语切碎营销短句特征被稀释现象模型对“免费领取”“限时秒杀”判断不准但对“免费”和“领取”单独出现的判断比较稳定整体召回率偏低。原因jieba 在遇到“免费领取”这类动词加动词结构时常常切分成两个独立词原本的短语记忆单元消失双词特征又需要更大数据量才能补上这个空缺。解决维护一个自定义词典把短信里重复出现的营销短语直接写进jieba.suggest_freq或自定义词表。像“免费领取”“点击链接”“回复退订”这类词强制合并成分词单元再做一次对比实验召回率通常会有明显提升。5.3 标签不一致同一类短信被标成两个类别现象训练过程中 loss 居高不下垃圾短信的召回率始终在一半附近波动检查错误样本时发现两条几乎相同内容的短信标签相反。原因人工标注或半自动清洗时不同批次之间标准不统一。比如“贷款推广”在第 1 次标注中算垃圾在第 2 次标注中被当成了“金融资讯”放行。解决标完数据后先跑一版基线模型找出模型高置信但预测与标签相反的全部样本逐一复查。这类样本往往就是标注错误修正后模型指标会突然上涨一个台阶属于最划算的提分手段。5.4 短文本特征不足标注太短导致模型无从学起现象测试集里单条短信只有五六个字例如“加 V 送”“凭码领”。模型输出接近五五开判定不稳定。原因这类短文本几乎没有上下文词包特征只有一两个词可以提取无论模型多先进都很难给出置信判断。解决把“短信长度”作为一个特征加入训练同时在预处理阶段把主题明显的高频短文本直接合并到营销模板库交给规则层拦截。模型负责吞掉长文本规则兜底处理极短的确定性样本这种配合在工程上是成熟模式。5.5 只看准确率不看混淆矩阵95% 的准确率可能毫无意义现象训练完成准确率 94%看起来不错但打开混淆矩阵发现垃圾短信被大量漏报模型几乎把所有样本都预测成了正常短信。原因负样本占比超过 90% 时全预测正常的基线也能拿到九成准确率这显然不代表系统有价值。解决从第一版实验开始就只看 F1 和召回率同时强制打印混淆矩阵。如果 F1 持续低于准确率十个点说明模型正在安全地偷懒此时需要做类别重采样、换阈值或调整损失权重。6. 验收比训练更重要用推理脚本和逐条走查结束项目6.1 固定推理脚本模型持久化与预处理对齐毕设答辩时老师不会现场看训练过程但一定会要求演示“输入一条短信、输出判断结果”。很多同学训练出了不错的模型演示当天却在加载时报错原因往往是训练脚本和推理脚本里的预处理不一致。训练时做了清洗和 jieba 分词推理时却没有调用同一套函数。稳妥做法是把所有预处理封装成固定函数模型训练完用 joblib 保存整个 pipeline推理时直接加载。import joblib import jieba pipeline joblib.load(sms_pipeline.joblib) label_map {0: 正常短信, 1: 垃圾短信} def predict_sms(raw_text: str): text clean_sms(raw_text) # 与训练时完全一致的清洗函数 cut .join(jieba.cut(text)) # 与训练时完全一致的分词方式 prob pipeline.predict_proba([cut])[0][1] pred int(prob 0.5) return label_map[pred], prob demo_text 恭喜您获得精品礼包点击链接即可领取回复T退订 print(predict_sms(demo_text))这段代码最核心的动作是把“清洗分词向量化”固定在脚本里。日志里加一条记录是最好的调试方式。6.2 用 30 条真实短信逐条走查模型指标只能说明整体水平毕设验收需要落到具体样本上。从语料外收集 30 条真实场景短信按“劫持诱导、营销广告、金融借贷、正常通知”四类均匀分布逐条用推理脚本预测。检查重点不是准确率而是模型漏掉的垃圾短信长什么样是短文本、新式变体还是高度口语化的说法每一条差异都可以成为论文里“失败案例分析”的素材。这 30 条样本不要从训练集抽而是尽量贴近当下实际收到的短信内容。出现过多次的结论是模型在训练集表现好但面对没见过的句子直接暴露出特征覆盖不足的问题。这一事实本身比好的指标更符合本科毕设的真实性也能引导答辩讨论走向工程问题而不是背题。6.3 用混淆矩阵和阈值策略交代效果最后一步是把方案收口成一块完整证据链数据规模、清洗与分词方式、三个模型的对比表、混淆矩阵、阈值选择依据以及 30 条新样本的逐条走查结果。建议在论文里放一张精度与召回随阈值变化的曲线直观展示这个任务不是“能分出来”而是“在什么容忍度下分出来”。这项工作不复杂但极容易和其他同学拉开差距。我自己的习惯是任何模型在宣布完成之前一定要做完这轮手工走查哪怕指标再漂亮也要先接受这 30 条样本的检验。这样才能让整条链路闭环也让调用你的模型的人敢用。哪怕最后模型只达到业务可接受的水平完整的测试动作也足以证明你掌握了中文文本分类的完整方法论。希望帮到你。本文还有配套的精品资源点击获取