☰
机器学习新闻标题分类系统实战:从Baseline到TextCNN
2026/10/3 3:24:02 网站建设 项目流程

简介:基于机器学习的新闻标题分类系统是一份Python本科毕业设计源码包,评审得分98.5分,主要面向计算机专业毕业生、课程设计及期末大作业场景,也适合需要完整项目实战的初学者。包内共62个文件,涵盖Python后端逻辑(app、pipeline、config、filter等模块)、Web前端页面(16个html、13个css、8个js)、数据预处理ipynb、SQL数据库脚本、多份中文停用词表(哈工大、四川大学等)与词表txt,以及PDF/Word文档说明,整体压缩包约10.93MB,目录结构清晰,便于按模块研读和二次开发。目前已有147人浏览学习。项目提供从数据清洗、特征工程、模型训练到新闻标题分类展示的完整闭环,附带详细文档说明,可快速理解机器学习分类项目的开发脉络;各脚本与配置模块均可直接修改复用,尤其适合毕业设计、课程答辩或项目实战参考。

1. 基于机器学习的新闻标题分类系统到底是什么:把毕业设计从“演示稿”变成“可答辩成果”

一个常见的场景:毕业设计题目发下来,写着“基于机器学习的新闻标题分类系统”,很多同学第一反应是去搜源码,搜到之后跑通一个demo,就开始写文档。但真正答辩的时候,老师问“你用的特征是什么”“为什么选这个模型”“测试集怎么划分的”,一句话接不上,分数立刻掉档。这个题目本身不复杂,核心就一件事:把一段新闻标题文本映射到预定义的类别(比如财经、体育、娱乐、科技),用机器学习算法完成这个映射,并给出可复现的评估结果。它适合Python入门到进阶之间的同学,也适合想低成本接触NLP的从业者——数据不用标注太多,公开数据集就能起步,模型从朴素贝叶斯到TextCNN都有成熟路径,难点全在工程细节上。

2. 新闻标题分类的模型选型与系统架构:从朴素贝叶斯到TextCNN的落地链路

2.1 标题分类的问题本质与主流算法对比

新闻标题和正文最大的区别是短。一条标题通常10到30个汉字,没有上下文依赖,关键词密度极高。这意味着:词袋模型就能拿到大量判别信息,不需要太复杂的语义抽取;类别之间的边界往往由几个强特征词决定(比如“股市”“涨停”指向财经),所以可解释性强的模型更容易在答辩中讲清楚;样本量不会太大,公开数据集通常几万到几十万条,深度学习模型可以训练,但收益相对传统模型有限。

主流算法对比:

模型特征依赖训练速度可解释性适合场景
朴素贝叶斯词频/TF-IDF极快高基线模型、小样本
SVM(线性核)TF-IDF快高中等规模、维度高
TextCNNWord2Vec/预训练词向量中中数据量足够、需要亮点
BERT预训练模型微调慢(需GPU)低追求效果上限,选题加分项

我一般建议毕设项目走“朴素贝叶斯/SVM做基线 + TextCNN做提升”的双轨路线。原因有三:第一,基线模型能让你快速跑通整个pipeline,验证数据没问题;第二,TextCNN代码量不大,PyTorch实现两百行左右,作为“机器学习算法”应用亮点足够;第三,两者对比之后,表格里能写出有意义的实验数据——老师非常吃这一套。

2.2 系统模块划分与数据流向

落地时按模块拆,不要一个脚本跑到底。常见的工程结构如下:

news_title_classifier/ ├── data/ │ ├── raw/ # 原始抓取数据 │ ├── processed/ # 清洗后数据 │ └── stopwords.txt # 停用词表 ├── src/ │ ├── preprocess.py # 清洗、分词、标签编码 │ ├── train.py # 训练与评估 │ ├── predict.py # 单条/批量预测 │ └── model/ │ ├── baselines.py # 朴素贝叶斯、SVM │ └── textcnn.py # PyTorch TextCNN ├── notebooks/ # 探索性分析 ├── docs/ # 毕业论文/设计文档 └── requirements.txt
# requirements.txt 关键依赖 pandas==1.5.3 numpy==1.23.5 scikit-learn==1.2.2 jieba==0.42.1 torch==2.0.1 matplotlib==3.7.1

代码逻辑说明:preprocess.py负责把原始文本转成模型可用的数值特征;train.py负责训练、保存模型和输出评估指标;predict.py加载保存的模型,对新的标题做预测。数据流动方向是:原始文本 — 清洗分词 — 特征向量 — 模型训练 — 评估报告 — 预测接口。

参数说明:requirements.txt锁定版本是为了答辩演示时换一台机器还能一键复现。我在带毕设时要求所有环境依赖锁版本,因为Python生态的兼容性问题非常隐蔽——sklearn版本不同,同一个Pipeline的默认参数可能都有变化。版本锁死是最便宜的后悔药。

提示:毕设环境不建议追求最新版本。torch 2.x 和 sklearn 1.2+ 的组合足够跑通全部代码,老版本在导入某些模块时会踩坑,新版本又可能改变默认行为。

3. 新闻标题分类的数据集构建与清洗:从爬虫原始语料到可训练样本

3.1 数据采集与标签策略

数据是分类系统的粮仓,很多毕设的翻车点不在模型,而在数据。常见做法有两种:

第一种:直接用公开数据集。清华的THUCNews子集、搜狗新闻语料都可以用,前者按类别分好了文件夹,后者需要自己切分。注意:公开数据集里的类别分布和真实新闻平台不完全一致,但作为毕设足够。

第二种:自采数据。用爬虫抓取新闻门户的分类栏目,好处是数据新鲜、类别可控,坏处是清洗成本高,而且爬虫代码要花时间维护。如果选择自采,注意控制抓取频率,另外务必保存原始HTML和解析后的字段,避免反爬导致数据半途失效。

我遇到的实际情况是:很多同学直接下载了一个“新闻分类数据集.csv”,打开一看只有两列:content和category,没有标题字段。做标题分类时只能从content里截取第一句,这会有噪声,但可用。更稳妥的做法是找按标题格式整理的爬虫数据集,或者自己跑一个简单的新闻列表页采集脚本。

标签编码的代码长这样:

import pandas as pd from sklearn.preprocessing import LabelEncoder df = pd.read_csv("news_title_data.csv") # 确保类别是字符串,去除空标题 df = df.dropna(subset=["title", "category"]) df = df[df["title"].str.len() >= 5] le = LabelEncoder() df["label"] = le.fit_transform(df["category"]) # 保存标签映射,预测阶段需要反向解码 label_map = dict(zip(le.classes_, le.transform(le.classes_))) print(label_map)

逻辑说明:LabelEncoder把“财经/体育/娱乐”等字符串类别转成从0开始的整数编号,模型只认数字。保存label_map是为了在预测时把数字转回可读的类别名。这里必须用fit_transform在全部数据上先做编码,再固定映射表供测试集和预测阶段使用。

参数说明:str.len() >= 5过滤掉过短的标题,比如“快讯”“重磅”这类无信息量文本,会干扰特征提取。如果你的数据量很紧张,可以把阈值降到3,但不要低于3——两个字的标题基本无法提供分类特征。

3.2 中文分词、停用词与样本均衡

中文文本不同于英文,词与词之间没有空格,必须先分词。最省事的方案是jieba,精确模式足够。分词后要去停用词——但新闻标题场景有个特殊点:不能照搬通用停用词表,因为“涨停”“下跌”这类词虽然短,却是强特征。

import jieba import re def clean_title(title: str, stopwords: set) -> str: # 去除特殊字符和数字串,只保留中文、英文、数字 text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9]", "", title) # 精确模式分词 words = jieba.lcut(text) # 过滤停用词和单字 words = [w for w in words if w.strip() and w not in stopwords and len(w) > 1] return " ".join(words) # 使用示例 stopwords = set(open("data/stopwords.txt", encoding="utf-8").read().splitlines()) df["clean_title"] = df["title"].apply(lambda x: clean_title(x, stopwords))

逻辑说明:正则[^\u4e00-\u9fa5a-zA-Z0-9]把标点符号和特殊符号替换为空,避免分词器把“。”当成一个独立词。len(w) > 1过滤掉单字词,比如“的”“了”,它们通常不是分类特征。返回的用空格连接的分词结果,是为后面的TfidfVectorizer做准备——sklearn的向量化器默认按空格切分。

参数说明:open()时用encoding="utf-8",Windows环境经常默认GBK,不指定编码会乱码。停用词表建议只用通用虚词(的、了、在、是),不要把领域词放进去——这是最容易犯的错误。

样本均衡是另一个坑。新闻类别天然不均衡,财经和娱乐可能各占30%,体育和科技各占20%。如果不处理,模型会偏向样本多的类别,表现为总体准确率还行、但少数类F1很低。处理方式有两种:

from sklearn.model_selection import train_test_split # 先打乱再按类别划分,保证训练/测试集分布一致 df = df.sample(frac=1, random_state=42).reset_index(drop=True) train_df, test_df = train_test_split( df, test_size=0.2, stratify=df["category"], random_state=42 ) print(train_df["category"].value_counts(normalize=True))

逻辑说明:stratify参数让划分后的训练集和测试集保持和原始数据相同的类别比例,这是防止“测试集里没有体育类”这类低级事故的关键。sample(frac=1)先把数据全量打乱,避免爬虫按时间顺序采集带来的排列偏差。

参数说明:random_state=42固定随机种子,保证每次运行划分结果一致。毕设实验的可复现性全靠这个参数,答辩演示和论文里的数字必须对得上。

4. 新闻标题分类器的训练与评估:用Python复现一个可运行的分类流程

4.1 特征工程与模型训练代码

特征工程部分,最可靠的是TF-IDF。在短文本分类上,TF-IDF的效果通常不逊色于Word2Vec平均向量,而且可解释性强,SVM和朴素贝叶斯都能直接用。

from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.svm import LinearSVC from sklearn.pipeline import Pipeline from sklearn.metrics import classification_report, accuracy_score # 词级TF-IDF,限制特征数量 vectorizer = TfidfVectorizer( max_features=8000, ngram_range=(1, 2), sublinear_tf=True, min_df=2, ) # 朴素贝叶斯基线 nb_pipeline = Pipeline([ ("tfidf", vectorizer), ("clf", MultinomialNB(alpha=1.0)), ]) nb_pipeline.fit(train_df["clean_title"], train_df["label"]) y_pred_nb = nb_pipeline.predict(test_df["clean_title"]) print("朴素贝叶斯准确率:", accuracy_score(test_df["label"], y_pred_nb)) print(classification_report(test_df["label"], y_pred_nb, target_names=le.classes_))

逻辑说明:Pipeline把向量化和分类器串成一个整体,fit时先算TF-IDF再训练模型,predict时自动复用训练时的词表,不需要手动维护vocabulary_。用Pipeline最大的好处是测试集不会“看到”训练集的特征统计量。

参数说明:

  • max_features=8000限制特征维度,防止词表过大导致训练变慢;
  • ngram_range=(1, 2)同时考虑单词和相邻词对,“机器学习”作为一个bigram特征比单独“机器”+“学习”更有区分度;
  • sublinear_tf=True用1 + log(tf)替代原始词频,弱化高频词的权重;
  • min_df=2丢弃只在一条样本里出现的词,这些词往往是噪声。

SVM是第二个模型:

svm_pipeline = Pipeline([ ("tfidf", TfidfVectorizer(max_features=8000, ngram_range=(1, 2), sublinear_tf=True, min_df=2)), ("clf", LinearSVC(C=1.0, class_weight="balanced")), ]) svm_pipeline.fit(train_df["clean_title"], train_df["label"]) y_pred_svm = svm_pipeline.predict(test_df["clean_title"])

参数说明:class_weight="balanced"自动调整类别权重,少数类样本的误分类代价更高。C是正则化参数,默认1.0在大部分新闻标题数据上表现不错,如果出现过拟合可以把C调到0.5,如果欠拟合调到1.5。这个参数在答辩时经常被问到,要能说出调整依据。

4.2 TextCNN模型与PyTorch实现

如果要冲高分,TextCNN是性价比最高的深度模型。它的核心思路是用多个不同窗口大小的卷积核抽取n-gram局部特征,然后池化拼接。

import torch import torch.nn as nn import torch.nn.functional as F class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim, num_classes, num_filters=128): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) # 三种窗口大小,对应bigram/trigram/4-gram self.convs = nn.ModuleList([ nn.Conv1d(embed_dim, num_filters, kernel_size=k) for k 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, seq_len) emb = self.embedding(x) # (batch, seq_len, embed_dim) emb = emb.transpose(1, 2) # (batch, embed_dim, seq_len) pooled = [] for conv in self.convs: out = conv(emb) # (batch, num_filters, seq_len-k+1) out = F.relu(out) pooled.append(out.max(dim=2)[0]) # 全局最大池化 cat = torch.cat(pooled, dim=1) # (batch, num_filters*3) cat = self.dropout(cat) return self.fc(cat)

逻辑说明:nn.Conv1d的输入形状是(batch, channels, length),所以嵌入层之后要transpose把seq_len换到最后一个维度。三种卷积核分别提取2-gram、3-gram、4-gram特征,最大池化把每个卷积核输出的向量压缩成一个值——这个值相当于“这个特征是否在标题中出现过”。

参数说明:

  • embed_dim=128是最常见的词向量维度,再大到256对短文本收益不大;
  • num_filters=128是每种卷积核的数量,增大可以提升表达力,但训练时间和显存占用都上涨;
  • padding_idx=0需要和数据预处理配合,把填充token的索引设为0;
  • dropout=0.5可以明显缓解过拟合,答辩演示时如果训练集准确率接近100%而测试集差很多,第一件事就是检查这个值。

训练循环的骨架:

def train_one_epoch(model, train_loader, optimizer, criterion, device): model.train() total_loss = 0 for inputs, labels in train_loader: inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() return total_loss / len(train_loader)

逻辑说明:criterion用交叉熵损失,optimizer用Adam,学习率设1e-3。每轮结束后在验证集上计算准确率,保存验证集F1最高的模型权重,这是防止后期过拟合的标准操作——也是给你留后悔药的操作。

4.3 评估指标:不要只用准确率

准确率是外行看的指标,答辩时要能讲F1和混淆矩阵。常见的情况是:分类系统准确率90%,但体育类完全没分对——因为测试集里体育类样本太少,准确率被其他类别掩盖了。

from sklearn.metrics import confusion_matrix, f1_score cm = confusion_matrix(test_df["label"], y_pred_svm) print(cm) # 每一行是真实类别,每一列是预测类别,对角线是正确数

评估的正确姿势:报告macro-F1而不是weighted-F1,因为它对每个类别一视同仁。如果macro-F1远低于准确率,说明少数类在翻车。再用混淆矩阵定位是哪些类别互相混淆——体育和娱乐经常混,财经和科技偶尔混。

调参建议:先跑朴素贝叶斯拿到基线准确率(一般在85%到92%),再看SVM能不能提升1到2个百分点,最后上TextCNN。每次只改一个变量,记录到实验表格里,这就是论文实验章节的素材。

5. 新闻标题分类系统避坑指南:五个经典翻车点与排查方法

5.1 现象:准确率很高,但随机输入一条时事新闻就分错

现象:训练集准确率95%,测试集也维持94%,整个项目看起来一切正常。答辩演示时输入一条刚刚发生的新闻标题,模型给出的类别却完全不是那么回事。

原因:数据泄漏。最常见的形式是数据集按时间顺序排布,爬虫抓取时把同一时段的新闻聚在一起,直接用默认参数划分训练集和测试集时,两个集合在时间上重叠度高,模型学到的是“哪段时间”而不是“什么内容”。另一种泄漏是文本预处理在划分前就完成,测试集的信息通过词表间接混进了训练过程。

解决:划分前先打乱数据,df = df.sample(frac=1, random_state=42).reset_index(drop=True),再按stratify=df['category']切分。Pipeline内的fit_transform只允许出现在训练集上,测试集只能transform。

5.2 现象:TextCNN训练时loss震荡,准确率在70%上下徘徊

现象:验证集loss不断震荡,准确率迟迟上不去,训练集准确率却在爬升。

原因:学习率偏大导致参数在最优解附近摆动;序列未固定长度,batch内长短不一,padding后的掩码没有参与计算;类别不均衡使少数类的梯度被多数类淹没。

解决:把学习率降到3e-4,配合torch.optim.lr_scheduler.ReduceLROnPlateau让loss进入平台期后自动下降;统一pad到64或128,用pack_padded_sequence处理真实长度;给CrossEntropyLoss传入weight向量,少数类权重设为1.2到1.5。

import torch from torch.nn.utils.rnn import pack_padded_sequence torch.manual_seed(42) seq_len = 64 def pad_sequence(text_ids, max_len=seq_len): if len(text_ids) >= max_len: return text_ids[:max_len] return text_ids + [0] * (max_len - len(text_ids))

逻辑说明:pad_sequence把不定长的标题统一到64个token,不足补0,超过截断。配合pack_padded_sequence可以让模型跳过padding位置,避免无意义的填充向量参与卷积和池化。

5.3 现象:Windows环境下报UnicodeDecodeError或打印乱码

现象:读取csv、打开停用词文件时抛异常;jieba分词后打印结果为“�”一类乱码。

原因:文本文件是UTF-8,但Windows默认的GBK编码在读取和输出两处分别制造了解码错误和编码错误。

解决:所有open()和pd.read_csv()显式传encoding='utf-8';控制台打印中文前,如果报错就先转GBK:text.encode("gbk", errors="ignore").decode("gbk", errors="ignore");更省事的是在PyCharm设置里把File Encoding和Console Encoding统一改成UTF-8。

5.4 现象:答辩老师问“为什么不用BERT,你这就是调包”

现象:系统能跑、文档写完,但答辩时老师追问模型选型逻辑,回答不上来。

原因:论文和PPT里只有结果没有选型论证;同时没有跑多个基线模型做对比实验,显得作者没有经过思考。

解决:在论文里明确写出“新闻标题属于短文本,词袋模型与线性分类器在小样本下已能较好拟合;BERT需要大规模预训练和较多计算资源,在几万条标题的数据集上收益有限且解释性差”,并附上朴素贝叶斯、LinearSVC、TextCNN的对比实验表,表格里列准确率、macro-F1、单轮训练时间三列。这组数据不能造假,讲的时候要能背出数字。

5.5 现象:换一台机器部署后,预测结果全乱

现象:在开发机一切正常,打包到演示电脑跑,同一句标题分类结果完全不同。

原因:随机种子不固定、模型权重没保存、依赖版本不匹配三个问题叠加。

解决:训练入口固定random.seed(42)、np.random.seed(42)、torch.manual_seed(42);训练完成保存完整模型文件torch.save(model.state_dict(), "./models/textcnn.pth"),预测脚本只加载权重不重新训练;演示前在目标机器执行pip install -r requirements.txt后,先跑一条固定样本做冒烟测试:

python predict.py --title "A股三大指数集体收涨"

这五个坑里,前三个解决的是“项目能不能稳定运行”,后两个解决的是“答辩时能不能扛住追问”。我给学生定的自查清单是:答辩前用这五条逐项检查,全部通过再提交。你也可以先跑一个最小模型验证数据,再逐步加模型和模块——这个顺序能帮你把调包变成真正的工程能力。

6. 把源码和文档说明整理成高分项目:演示设计、结果复现与两个低成本扩展

6.1 源码要能跑通,文档要能对上号

评分人拿到系统时,第一步是看论文是否和代码匹配。我整理交付物的习惯是:论文里的每一个实验表格,旁边都标注“结果由train.py在random_state=42时运行获得”;演示PPT只用三张图——系统架构图、算法对比表、混淆矩阵热力图。这三张图能讲清楚设计思路、实验过程和结果质量,已经覆盖大多数答辩问题。混淆矩阵用matplotlib画:

import matplotlib.pyplot as plt import seaborn as sns sns.heatmap(cm, annot=True, fmt="d", cmap="Blues", xticklabels=le.classes_, yticklabels=le.classes_) plt.xlabel("Predicted") plt.ylabel("True") plt.savefig("confusion_matrix.png", dpi=300)

代码说明:用seaborn封装heatmap,cm是sklearn返回的混淆矩阵,annot=True在格子里显示数字,fmt="d"保证整数显示,class_names来自LabelEncoder的classes。这张图放在论文实验结果章的第一页,比任何文字描述都直观。

6.2 两个低成本高回报的扩展方向

第一个方向:加一个Web演示端。Flask实现一个单页,输入标题返回预测类别和置信度,总共300行代码,却能把系统从“脚本”变成“系统”。评委更愿意看到一个可以交互的作品,而不是一个跑完后就退出控制台的程序。

第二个方向:做细粒度分类的探索性实验。把“体育/财经”扩成“体育-足球、体育-篮球、财经-股市、财经-房产”这样的子类,手动标注几百条数据做初步实验,论文里写成“扩展讨论”,投入产出比很高。

这两个方向不要都做,挑一个。毕业设计的时间预算永远是紧张的,把演示流程做到“不出错、不冷场”,比堆功能重要。我自己早期的教训是:在一门课设里花了两周搭界面,最后模型只用了最简单的贝叶斯,答辩时被问到“你的模型在哪”,场面非常尴尬。后来我调整了策略——先保证模型和评估扎实,再做展示层。这个顺序救了我很多次,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询