简介面向人工智能与机器学习方向的本科毕业设计项目以新闻标题分类为场景完整呈现从数据清洗、特征提取、模型训练到前端可视化展示的自然语言处理流程适合计算机相关专业学生开展毕设、课设或求职项目复现。压缩包内共六十三个文件容量约十一兆字节。核心为七个Python脚本配合十六个HTML、十三个CSS及八个JS构成前端可视化界面九个txt文件内置哈工大、四川大学等多套中文停用词表与训练、验证、测试数据集并包含敏感词过滤与词表映射等预处理组件另有Jupyter Notebook用于探索式数据预处理、SQL文件提供后台数据库结构、Word与PDF文档便于阅读项目说明与结论。已有88人学习下载可按项目说明与依赖清单快速搭建环境通过pipeline脚本串联数据处理至模型推理的完整链路也可将界面、接口与模型解耦后移植到其他文本分类任务中。对于需要参考完整毕设工程组织、理解机器学习项目落地的同学这份资源具备较高的直接使用价值。1. 新闻标题分类系统一个能跑通全流程的机器学习毕设长什么样毕业设计最怕的不是模型跑不出数字而是“demo能点论文没得写”。这个基于机器学习的新闻标题分类系统把中文文本分类从数据清洗、分词、特征工程、模型训练到 Flask Web 展示完整串了一遍属于典型的“麻雀虽小五脏俱全”。它解决的核心问题是给一句新闻标题自动判断属于娱乐、体育、财经、科技还是其他类别背后是一套可复现的传统机器学习流程。适合两类人一是做人工智能或机器学习方向课程设计、本科毕设的学生想找一个结构完整、能讲清楚原理又能演示的项目二是刚开始接触中文 NLP、想搞明白训练和预测全链路怎么落地的从业者。接下来我从项目文件结构开始拆一路拆到模型参数和部署坑点。2. 先拆骨架再谈模型6 类文件把项目的职责边界划清楚拿到一个压缩包不要急着跑代码先把文件按职责归类。这个系统里几十个文件实际上可以分为配置依赖、数据、预处理、算法、Web 展示、数据库脚本六个层面每一层解决一个独立问题层与层之间通过文件和函数接口衔接。先把边界摸清后边调参和排错才有方向。2.1 配置与依赖requirements.txt 和 config.py 决定了复现成本任何机器学习项目第一步永远是把环境锁死。requirements.txt是这个项目的依赖清单常见内容会包含flask、scikit-learn、jieba、pandas、numpy、joblib这类库。安装命令是pip install -r requirements.txtconfig.py管的是全局参数比如数据文件路径、模型保存路径、停用词表路径、标签映射文件路径等。我一般会建议把这类参数集中到一个文件里而不是散落在各个模块中。这样在做实验时只需要改一个文件就能切换数据集和模型配置不需要在filter.py、pipeline.py里到处翻常量。2.2 数据文件train、dev、test 三件套和标签映射表train.txt是训练集dev.txt是验证集test.word和test_with_label.word是测试集。注意后缀.word说明这份数据已经做过初步分词标题文本按空格或者/分隔。实际使用时需要对着id2tag.txt去还原每个数字标签对应的真实类别名比如 0 对应的可能是“科技”1 对应的可能是“体育”。这里有一个值得留意的细节vocab.txt是词表快照文件里每一行是一个词或者编号。它存在的意义是让你能检查训练时模型到底看到了哪些词排查词表漂移问题——比如训练时某个词在词表里预测时加载的模型里却没有结果就会莫名分错。2.3 算法与流程文件preprocess.ipynb、filter.py、pipeline.py 的铁三角preprocess.ipynb是 Jupyter Notebook 形式的探索性分析脚本主要看数据分布、类别数量、样本长度等等相当于正式建模前的体检报告。filter.py负责过滤包括敏感词过滤和无效样本清洗。pipeline.py是训练主流程把分词、向量化、分类器训练串成一条流水线最终产出一个可持久化的模型文件。这三个文件的分工逻辑是先做数据探索notebook再做规则过滤filter最后进入特征与模型训练pipeline。如果你在自己项目里也是这个结构后续换数据集、换模型、调参数都会很顺手。下表把项目的核心文件职责列清楚文件类型职责main.py / app.py / view.pyWeb 层Flask 路由与页面渲染提供标题输入和分类结果展示filter.py预处理敏感词过滤、无效样本清洗、文本规则处理preprocess.ipynb预处理数据分布探索、可视化、样本体检pipeline.py算法构建 TF-IDF 分类器的训练流水线config.py配置路径、参数、模型保存位置等全局配置train.txt / dev.txt / test.word数据训练集、验证集、分词后的测试集tables.py / Bachelor_Graduation.sql数据持久化数据库表结构与历史预测记录存储2.4 从 README 开始复现的标准路径README.md是这个压缩包的入口文档里面通常会写明运行顺序。一般流程是先安装依赖然后跑预处理脚本再训练模型最后启动 Web 服务pip install -r requirements.txt python preprocess.ipynb # 实际使用时转为 .py 再执行 python pipeline.py python app.py跑完最后一条命令Flask 会在本地起一个 Web 服务浏览器访问对应端口就能看到新闻标题分类页面输入一句“某球队主场大胜对手”系统返回“体育”类。到这里整个系统的“可用性”就已经验证完了接下来需要深入每一层的实现细节搞清楚参数为什么这么设、换一个场景要改哪里。3. 预处理决定分数上限停用词合并、敏感词过滤与数据划分很多人在文本分类上有个误区把精力全放在模型调参上却忽略了预处理。实际上对中文短文本分类来说预处理的好坏直接影响分数上限。模型再强喂进去的词是脏的结果也不可能干净。这一章我把预处理拆成三个可执行的模块来讲停用词表合并、敏感词过滤策略、数据划分与样本体检。3.1 三份停用词表怎么合并哈工大、川大、中文停用词表的取舍这个项目内置了三份停用词表分别是哈工大停用词表、四川大学机器智能实验室停用词库、中文停用词表。为什么不直接用一份因为不同来源的停用词表覆盖场景不同哈工大偏通用词覆盖面广川大停用词库偏学术和机器智能领域中文停用词表则包含不少网络常见词。单用任何一份都有覆盖盲区合并去重是常见的工程做法# merge_stopwords.py # 把三份停用词表合并去重再做空行清洗 import os stopword_files [ 哈工大停用词表.txt, 四川大学机器智能实验室停用词库.txt, 中文停用词表.txt, ] merged set() for path in stopword_files: if not os.path.exists(path): print(f[warn] 缺少停用词表: {path}) continue with open(path, encodingutf-8) as f: for line in f: word line.strip() if word and word not in merged: merged.add(word) with open(stopwords_merged.txt, w, encodingutf-8) as f: for word in sorted(merged): f.write(word \n) print(f合并完成去重后共 {len(merged)} 个停用词)这段逻辑的核心是set去重。三份表里有些词会重复出现比如“的”“了”“在”几乎每份都有用set天然去重最后再按字典序写出方便日后审查。写入时逐行追加\n避免最后一行和下一行粘连。这里有一个我踩过的坑部分停用词表文件里带着\ufeff之类的 BOM 头直接用strip()去不掉所以我在读文件时显式指定encodingutf-8如果遇到 BOM 需要改成encodingutf-8-sig否则第一个词会带不可见字符。3.2 sensitive_words.txt 的过滤逻辑业务级清洗还是硬删除filter.py里加载了sensitive_words.txt这个敏感词表的职责不是学术预处理而是业务层面的内容安全清洗。常见的做法是先把敏感词加载到内存集合里然后对每条新闻标题做一次短路匹配命中任意一个词就判定为不合法样本。实现如下# filter.py 的常见写法把敏感词表加载成集合命中即整条过滤 class SensitiveFilter: def __init__(self, pathsensitive_words.txt): self.words set() with open(path, encodingutf-8) as f: for line in f: w line.strip() if w: self.words.add(w) def is_legal(self, text: str) - bool: # 命中任意敏感词就判为不合法工程上先做快速短路 return not any(w in text for w in self.words)这里有两层考虑。第一加载到set而不是每次打开文件读取避免在每条样本上重复做磁盘 IO词表几千条时这个优化感知明显。第二匹配用的是w in text子串匹配适合词表规模中等、对性能要求不极端的场景如果敏感词量到几万级别就必须换成 AC 自动机否则预测接口的延迟会高到不可接受。注意一个边界问题敏感词过滤在训练集上做硬删除是合理的但在线上预测时如果直接对用户输入做整条拒绝会出现“输入里带一个敏感词整句话被拦掉”的体验问题。在该项目的毕业设计场景里训练集硬删除没问题但代码迁移到生产环境时这一层需要改成提示而不是阻断。3.3 数据划分与样本不均衡的初检先看类别分布再谈训练预处理阶段必须做的另一件事是检查训练集的类别分布。train.txt每一行通常是一对“标签 标题”标签可能是中文也可能是数字编号。如果类别分布严重不均衡比如“科技”类样本占 60%“体育”类只占 5%那么模型会倾向于把所有样本都判成科技准确率虚高但没有实用价值。最直接的体检方式是用 pandas 统计# 体检脚本统计每个类别的样本数看一眼分布 import pandas as pd df pd.read_csv(train.txt, sep\t, headerNone, names[label, title]) print(df[label].value_counts()) print(总样本数:, len(df))这段代码把训练集按 Tab 分隔读进来然后对标签列做计数。加headerNone是因为原始 txt 里通常没有列名手动指定names便于后续引用。跑完后如果发现某个类别占比超过 40%说明数据存在不均衡风险后续要么做类别加权、要么做欠采样/过采样或者干脆报告时以 macro-F1 而不是 accuracy 作为核心指标。在这个项目里dev.txt的划分一般也是同一分布采样出来的验证时要注意保持和训练集同源。4. 训练闭环的四个关键环节从 TF-IDF 到分类器参数数据清洗完毕就进入算法核心。这个系统的模型侧并不复杂走的是传统机器学习路线TF-IDF 做文本向量化朴素贝叶斯或逻辑回归做分类。选择这两个模型是有理由的——在几千条样本规模的毕业设计数据集上深度模型容易过拟合且训练周期长传统机器学习模型收敛快、可解释性强、论文里还能画学习曲线。这一章我拆开讲四个环节分词选择、向量化参数、pipeline 组装、模型评估。4.1 中文分词的选型为什么默认是 jieba中文文本不像英文天然有空格分隔必须分词。这个项目里常见做法是用jieba因为它是纯 Python 实现、安装简单、词典覆盖面够用恰好匹配课程设计和本科毕设的工程强度。分词这一步通常在预处理脚本里完成产出分词后的文本再喂给 TF-IDFimport jieba def tokenize_title(title: str) - str: # 精确模式分词用空格连接方便 TF-IDF 直接按空格切分 words jieba.cut(title.strip(), cut_allFalse) return .join([w for w in words if w.strip()])cut_allFalse是精确模式这也是默认模式不建议用全模式cut_allTrue因为会产生大量冗余词组合。分词后做一次空字符串过滤避免连续空格干扰后续 TF-IDF 的解析。这里的输出格式要和test.word保持一致否则训练和预测时文本格式不一致会导致向量维度异常。4.2 TF-IDF 的五个关键参数ngram_range、min_df、max_df、sublinear_tf、tokenizer向量化是整个项目里参数最密集的环节。TfidfVectorizer有大量参数但这个场景下真正影响结果的是五个分词器、ngram 范围、最低文档频率、最高文档频率、词频压缩方式。核心 pipeline 组装代码如下# pipeline.py 的训练主流程精简版 from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.linear_model import LogisticRegression from sklearn.pipeline import make_pipeline from sklearn.metrics import classification_report def build_model(use_lrFalse): vectorizer TfidfVectorizer( tokenizertokenize_title, # 复用上面的 jieba 分词函数 ngram_range(1, 2), # 保留单词和相邻词对 min_df2, # 至少在 2 条样本中出现过滤低频噪声 max_df0.8, # 超过 80% 样本都出现的词对分类无贡献 sublinear_tfTrue, # 用 1log(tf) 压缩词频差异 ) clf LogisticRegression(max_iter1000, C1.0) if use_lr else MultinomialNB(alpha1.0) return make_pipeline(vectorizer, clf) # 训练并评估 pipe build_model(use_lrTrue) pipe.fit(train_titles, train_labels) print(classification_report(dev_titles, pipe.predict(dev_titles), target_nameslabel_names))参数含义逐一说清。tokenizertokenize_title让 TF-IDF 内部每次解析文本时都调用 jieba这样从原始文本到词频矩阵一步到位不需要先产出一个中间分词文件。ngram_range(1, 2)是同时保留单个词和相邻两个词的组合对新闻标题这种短文本非常关键——比如“中国”和“中国队”语义不同bigram 能捕捉到这种局部搭配但三元以上在短文本里出现过少意义不大。min_df2把只在一条样本中出现的词丢弃这些词通常是人名拼写错误或噪声对分类没有泛化价值。max_df0.8把超过 80% 样本都出现的词丢弃因为太常见的词比如“今天”“记者”在所有类别里分布均匀没有区分度。sublinear_tfTrue用 1log(tf) 压缩高频词的词频差异防止“一个词在一篇文档里重复 20 次”的状况主导整个向量。4.3 朴素贝叶斯和逻辑回归选哪个、参数怎么调这个场景下朴素贝叶斯和逻辑回归是两种风格完全不同的选择。多项式朴素贝叶斯MultinomialNB假设特征之间条件独立训练极快在小样本和离散特征上表现稳定尤其适合 TF-IDF 这种计数型输入但它对特征相关性敏感新闻标题里“疫情”和“防控”经常一起出现贝叶斯会把这种关联重复计算。逻辑回归LogisticRegression则显式建模特征权重能学出“哪些词强烈指向某个类别”所以在这个项目里逻辑回归的 F1 通常会高一点。代价是训练时间稍长但几千条样本也就在秒级。alpha1.0是朴素贝叶斯的拉普拉斯平滑系数默认值就是 1.0一般不需要调。逻辑回归里两个值得动的参数是C和max_iter。C是正则化强度的倒数值越小正则越强越不容易过拟合文本分类特征维度动辄几万我一般从 1.0 起步如果训练集 F1 远高于验证集就调低到 0.1 甚至 0.01。max_iter1000是迭代上限默认 100 在几千维度特征上经常不收敛会报ConvergenceWarning这个参数要舍得给大。4.4 评估不是只看准确率打印完整 classification_report训练完第一件事不是看 accuracy而是打印分类报告。单一准确率在类别不均衡时是骗人的如果科技类占 50%模型全判科技也有 50% 准确率。classification_report会输出每个类别的精确率precision、召回率recall和 F1 值这三列信息量远大于一个总分。在这个项目里我见过一个典型情况娱乐类召回率只有 0.4意味着有一半的娱乐标题被分到了别的类别。这就不是调参能解决的要回到预处理环节去看这些分错的样本长什么样——通常会发现娱乐标题里包含大量明星人名而人名在 TF-IDF 里是低频稀疏特征贡献极弱。遇到这种问题下一步要么扩充分词词典要么引入额外的特征。5. 避坑记录毕业设计最容易翻车的 5 个现场这一章集中记录我在拆这个项目、以及自己做同类系统时踩过的坑。每条都是“现象 → 原因 → 解决”的结构你可以对照自己的项目逐条排查。5.1 网页上预测结果全分到同一个类现象验证集 F1 有 0.85模型看起来一切正常但启动 Flask 后在网页上随便输什么标题返回都是同一个类别比如全是“科技”。原因预测时没有加载训练保存的 pipeline而是重新fit了一个空的TfidfVectorizer导致词表完全是空的所有标题向量化后全是零向量模型只能按先验概率猜自然全分到样本最多的类。解决训练完用joblib.dump(pipe, model.pkl)把整个 pipeline包括向量器和分类器一起保存预测时只load不fitimport joblib # 训练阶段保存整个 pipeline joblib.dump(pipe, news_title_clf.pkl) # 预测阶段直接加载不要再碰 TfidfVectorizer model joblib.load(news_title_clf.pkl) label model.predict([某球队主场大胜对手])[0]这个坑的根源在于对 scikit-learn pipeline 的机制理解不深。TfidfVectorizer在fit时构建内部词表这个词表是模型的一部分丢掉它等于换了语言。从那以后凡是文本分类项目我只保存完整 pipeline从不单独保存分类器权重。5.2 三份停用词表合并后反而掉分现象加了停用词过滤后 F1 不升反降尤其是“科技”类掉得最明显。原因停用词表里把“中国”“美国”“国家”这类词也包含了这些词对财经和科技类别是有区分度的被过滤后模型丢失了有效信号。哈工大停用词表偏学术通用里面有些词在新闻标题场景里并不该停用。解决合并停用词表之后做一次反向审查把高频且对类别有区分度的词从停用表里捞回来。具体做法是统计每个词在各类别中的占比差异差异越大越不该进停用表。这是给所有文本分类项目的一条通用忠告没有任何一份公开停用词表可以直接照搬必须基于自己的训练集做一次“召回审查”。5.3 训练集和预测集的分词结果不一致现象脚本里跑模型结果正常但 Web 接口返回的分词结果和训练时完全对不上同一句话在训练脚本和 Flask 里的词序列不同。原因jieba的词典在不同版本或不同机器上可能加载了不同的自定义词更常见的是 Flask 的view.py里没有调用同一个分词封装函数而是自己重新写了一遍分词逻辑参数对不上。解决把分词函数封装在独立模块里训练和预测都引用同一个函数# text_utils.py import jieba def tokenize_title(title: str) - str: words jieba.cut(title.strip(), cut_allFalse) return .join([w for w in words if w.strip()])然后在pipeline.py和view.py里都from text_utils import tokenize_title。这样能保证训练和预测走的是同一套代码路径。这个坑在复杂项目里非常隐蔽因为两处代码单独看都“正确”只有放在一起比对才能发现不一致。5.4 random_state 不设置每次跑出来的指标都不一样现象同样的train.txt连着跑三次pipeline.pyF1 分别是 0.83、0.86、0.81完全无法确定模型真实水平。原因train_test_split或者ShuffleSplit没有设置random_state每次划分的数据子集不同评估指标自然漂移。如果是边训练边调参你甚至无法判断某个参数改动是真正有效还是数据划分带来的随机波动。解决所有涉及随机划分的地方显式固定随机种子from sklearn.model_selection import train_test_split X_train, X_dev, y_train, y_dev train_test_split( titles, labels, test_size0.2, random_state42, stratifylabels )这里stratifylabels是做分层采样让训练集和验证集的类别分布一致避免某个小类在验证集里恰好没有样本。random_state42是工程惯例具体数字不重要重要的是固定下来保证实验可复现。这是机器学习八股里最常见的检查项也是写论文时最容易被怼的问题。5.5 dev 集达标但真实输入命中不了敏感词逻辑现象新输入的标题里明明有敏感词但系统没有触发过滤仍然返回了正常分类结果。原因web 预测链路里加载的sensitive_words.txt路径写错了。view.py里用的是相对路径Flask 启动时当前工作目录可能不在项目根目录于是加载了一个不存在的文件异常被try...except静默吞掉过滤逻辑失效。解决所有文件路径统一基于__file__计算绝对路径import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) SENSITIVE_PATH os.path.join(BASE_DIR, sensitive_words.txt)相比直接写sensitive_words.txt这种方式不管从哪个目录启动 Flask 都能正确定位文件。排查这类问题的方法是启动时打印实际加载的路径而不要想当然。这个坑在毕业设计答辩演示时最容易暴露——换了一台电脑、换了一个目录路径全乱系统直接崩溃。5.6 类别不均衡导致准确率虚高现象准确率 0.9但分类报告显示“体育”类的召回率是 0.0。原因训练集里“体育”样本占比不到 5%模型为了总体准确率把所有样本都判成了“科技”准确率虚高小类完全被忽略。解决换用 macro-F1 作为核心评估指标它会对每个类别的 F1 求平均类别不均衡时能真实反映模型对每个类的判断能力。同时可以尝试class_weightbalanced给少数类更高的惩罚权重model LogisticRegression(max_iter1000, class_weightbalanced)这句话让逻辑回归根据类别频率自动调整样本权重少数类的样本在优化时获得更大贡献。代价是整体准确率可能略降但每个类别的召回率会趋于均衡这才是文本分类系统真正要的东西。6. 模型上线前再验证一轮用留出集和错误 case 反推问题训练完成、Web 能跑不等于系统合格。在毕业设计答辩前我习惯强制做一轮“上线前验证”包含两个动作留出集验证和错误 case 抽样分析。留出集验证不用重新训练直接把dev.txt喂给已保存的模型逐条打印 prediction 和真实 label 的对比错误 case 分析是把预测失败的样本抽出来打印人眼扫一遍通常能发现系统性规律。比如连续 10 条分错的娱乐标题都包含“剧”字说明模型没学到“剧”和娱乐的关联可能这个词在预处理时被停用词表误删了也可能训练集里这类样本太少。验证逻辑可以写成一个独立脚本把错误样本和模型预测概率一并输出model joblib.load(news_title_clf.pkl) wrong [] for title, true_label in zip(dev_titles, dev_labels): pred model.predict([title])[0] if pred ! true_label: proba max(model.predict_proba([title])[0]) wrong.append((title, true_label, pred, round(proba, 3))) for item in wrong[:20]: print(f标题: {item[0]} | 真实: {item[1]} | 预测: {item[2]} | 置信度: {item[3]})看这些输出时重点盯两件事。第二件是置信度分布如果大部分错误样本的置信度都在 0.9 以上说明模型极度自信地犯错往往是特征没有覆盖到关键判别词如果置信度都在 0.5 附近说明类别本身边界模糊可以考虑增大ngram_range或者引入外部知识库特征。这件是第一件看错误样本里有没有规律性的用词模式是停用词误杀、分词不合理还是样本标签本身标错了。我在实际项目里遇到过最典型的案例dev 集里有一条“某地发生地震救援队已抵达”真实标签是“科技”但任何人看这句话都应该归“社会”或“时事”——标签错了模型学到的规则自然乱。这类脏数据在真实场景中占比通常有 3% 到 5%会影响模型训练时的梯度信号需要在预处理阶段做一轮去重和人工抽检。进阶路径方面如果这个项目要继续扩展我会建议在现有 pipeline 上做三个方向第一把 TF-IDF 换成 Word2Vec 或 FastText 预训练向量能缓解低频稀疏问题但需要额外引入大规模语料第二引入模型融合把朴素贝叶斯和逻辑回归的结果做加权投票通常能提升一个百分点的 F1第三把templates里的前端页面加一个历史查询记录展示配合Bachelor_Graduation.sql把每次预测结果持久化到数据库这样演示时能展示“输入标题 → 输出分类 → 记录入库”的完整业务闭环。最后一个方向对答辩加分最直接因为它把机器学习模型落到了实际业务系统里而不是一个孤立的 Python 脚本。从接到这个项目到完全跑通、再到排查完上面五个坑我最大的体会是本科毕设的机器学习项目难点从来不在模型算法本身而在数据路径的一致性管理和环境复现。分词函数、停用词表、模型保存路径、随机种子任何一个环节在训练和预测之间出现不一致都会导致结果不可复现。从那以后我每次做文本分类项目都强制走一遍“训练前打印配置 → 训练结束后即 dump → 预测时重新 load 源码路径”的检查流程确保训练链路和预测链路是同一套代码、同一个词表、同一次转换。这套习惯帮我省掉了大量返工时间希望帮到你。本文还有配套的精品资源点击获取
