简介:面向计算机相关专业正在准备期末大作业、课程设计或毕业设计的学生,这份基于Python的新闻文本分类项目融合了BERT预训练模型与朴素贝叶斯算法,是一个评审得分99分的高分设计,代码完整且易于运行,零基础学习者也能快速上手。压缩包共23个文件,整体约67.38MB,包含多个Jupyter Notebook交互式实验文档、Python辅助脚本与数据加载模块、CSV训练测试数据集、txt结果输出、Log日志文件、Markdown说明以及一份DOCX实验报告,目录结构清晰,从数据划分、预处理、模型训练到结果对比各环节均有对应文件,便于按步骤复盘。目前已有71人学习下载,在期末冲刺和算法实战场景中有不错的参考价值。资源不仅提供了BERT与朴素贝叶斯两条完整的实现流程,还细致展示了文本清洗、特征构造、训练调参与分类效果对比,能够帮助读者理解两种方法在新闻文本任务上的差异,掌握可复用的解决思路,直接迁移到自己的项目中。
1. 需求与选型:期末大作业的新闻文本分类,为什么不只跑一个模型
如果你正在为 Python 期末大作业发愁,大概率见过这类题目:给一批新闻文本,要求做自动分类。最省事的思路是拿一个朴素贝叶斯模型跑完,画个混淆矩阵就交差。但这套资源没有走捷径,而是同时实现了 BERT 和朴素贝叶斯两套模型,并把两者结果做了对比。这么设计不是为了炫技,而是为了让大作业从“能跑”变成“能讲”:评审看到的是两条技术路线、一组对比数据、一张结果分析表,工作量一目了然。另一个现实原因是算力不可控——没 GPU 的电脑可以跑朴素贝叶斯,有 GPU 或 Kaggle 环境就能跑 BERT。适合正在做期末大作业、课程设计或需要项目实战练习的学生,也适合想快速复用一个完整文本分类流程的从业者。整套资源包含源码、数据集和实验报告,拿到手之后按顺序跑通就能作为项目交付。
2. 数据集切分与预处理:从 train.csv 到 split_dataset 的完整链路
2.1 原始数据组织:train.csv 与 test.csv 的分工
data 目录下放的是原始数据,train.csv 是带标签的新闻训练集,test.csv 是待预测的测试集。第一次拿到这套资源,不建议直接拿 test.csv 去提交,因为它的标签可能是空的或者不公开,你没法本地评估效果。所以项目里专门放了一个 split_dataset 目录,里面存放经过切分的训练子集和测试子集,用于自己验证模型。
切分逻辑由 split_dataset/data.ipynb 完成,核心动作是把 train.csv 按比例拆成新的训练集和测试集。我一般会采用分层采样,而不是简单随机切分。新闻分类数据通常存在类别不平衡的问题,比如体育类和科技类样本多,金融类样本少。如果随机切分,可能出现某类新闻在测试集中极少甚至消失,导致模型评估结果失真。分层切分能保证每个类别在切分前后的比例基本一致,这样得到的 F1 分数才有参考意义。
# split_dataset/data.ipynb 中的分层切分核心逻辑 import pandas as pd from sklearn.model_selection import train_test_split df = pd.read_csv("../data/train.csv") # stratify 参数按 label 列作分层,保证训练/测试集中类别比例一致 train_df, test_df = train_test_split( df, test_size=0.2, stratify=df["label"], random_state=42 ) train_df.to_csv("../split_dataset/train.csv", index=False) test_df.to_csv("../split_dataset/test.csv", index=False)train_test_split 的 stratify 参数必须传入原始 DataFrame 的 label 列,而不是切分后的结果。random_state 固定为 42,是为了让复现结果稳定。如果随机种子不固定,每次跑出的数据集不同,后面所有模型的结果都不可比。实际做课设时,遇到结果忽高忽低,很多时候不是模型问题,而是数据切分没固定。
2.2 预处理流程:清洗、分词、去停用词与标签编码
News_preprocessing_steps.ipynb 把预处理拆成了四个步骤。第一步是清洗,去掉网页标签、URL、数字和特殊符号。新闻数据是从网上抓来的,混杂着大量噪声,比如“相关阅读”“点击查看”这类导航文案。第二步是分词,英文按空格切分,中文需要使用分词工具,教学环境里最常用的是 jieba。第三步是去停用词,把“的”“了”“是”这类对分类没有贡献的词删掉。第四步是标签编码,将新闻类别从字符串映射成数字,方便 sklearn 和 PyTorch 读取。
# News_preprocessing_steps.ipynb 中的文本清洗片段 import re import jieba def clean_text(text): # 去掉 HTML 标签 text = re.sub(r'<.*?>', '', text) # 去掉 URL text = re.sub(r'http\S+', '', text) # 去掉数字和特殊字符,保留中文和英文 text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z]', ' ', text) return text.strip() def tokenize(text): return list(jieba.cut(text)) def filter_stopwords(tokens, stopwords): return [w for w in tokens if w not in stopwords and len(w) > 1]这里有几个容易踩的细节:正则表达式里 \u4e00-\u9fa5 是每个中文的 Unicode 区间,如果你处理的语料包含中文标点,清洗时要一并去掉。分词之后过滤长度小于 1 的词,可以顺手删掉空格和单字符噪声。stopwords 集合通常放在一个 txt 文件里,用 set 类型装载,in 判断速度更快。资源里的 .gitignore 和 Log.txt 分别用来管理版本忽略项和记录运行日志,预处理过程如果出错,日志会提示是哪一步崩的。
2.3 dataload.py:统一数据加载与批次生成
朴素的处理方式是每个 Notebook 各自写一套读取逻辑,但那样会产生重复代码。dataload.py 把数据加载封装成了公共模块,两个模型的训练脚本都从它这里取数据。
# dataload.py 的简化结构 import pandas as pd from sklearn.preprocessing import LabelEncoder class NewsDataset: def __init__(self, csv_path): self.df = pd.read_csv(csv_path) self.texts = self.df["text"].tolist() self.labels = self.df["label"].tolist() def encode_labels(self): # 将类别字符串转为 0,1,2,... 的整数标签 self.encoder = LabelEncoder() self.labels = self.encoder.fit_transform(self.labels) return self.labels def get_texts_labels(self): return self.texts, self.labelsLabelEncoder 的 fit_transform 会把字符串类别编码成从 0 开始的整数。这里有一个隐蔽的问题:训练集和测试集必须用同一个 LabelEncoder 去 encode,否则同一个类别在两个集合里的编号不一致。dataload.py 的写法是让训练集先 fit,然后保存 encoder,测试集只做 transform。这个模块同时服务了朴素贝叶斯和 BERT 两条线,比如 BERT 需要把文本转成 input_ids,这部分逻辑会在 Bert_train.ipynb 里再套一层 Dataset 类,dataload.py 只负责最底层的 CSV 读取和标签映射。
3. 朴素贝叶斯基线实现:特征工程、训练评估与结果约定
3.1 为什么用朴素贝叶斯做基线
文本分类的入门算法里,朴素贝叶斯是风险最低的一个。它的计算基于贝叶斯定理,假设特征之间相互独立。新闻文本里词和词当然不是完全独立的,比如“中国”后面常跟着“经济”,但即便假设不成立,朴素贝叶斯在高维稀疏文本特征上依然表现得不错,而且训练速度极快,几秒就能跑完一个几万样本的数据集。
把朴素贝叶斯放在第一个做,还有一个好处:给后面的 BERT 做参照。先拿到一个可解释的基线准确率,再用 BERT 尝试超越它。如果 BERT 结果比朴素贝叶斯还差,说明训练配置有问题,而不是模型不行。这种“先基线后复杂模型”的顺序,也是实验报告里比较讨喜的结构,评审能看出你在做工作,而不是盲目套模型。
3.2 NaiveBayes.ipynb 的训练主流程
训练朴素贝叶斯最常用的特征是 TF-IDF,它比单纯的词频更能体现词语在文档中的重要程度。NaiveBayes.ipynb 里先加载训练集,接着用 TfidfVectorizer 做特征提取,然后喂给 MultinomialNB。
# NaiveBayes.ipynb 训练主流程 from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.metrics import classification_report vectorizer = TfidfVectorizer( max_features=20000, ngram_range=(1, 2), sublinear_tf=True ) X_train = vectorizer.fit_transform(train_texts) X_test = vectorizer.transform(test_texts) clf = MultinomialNB(alpha=0.5) clf.fit(X_train, train_labels) pred = clf.predict(X_test) print(classification_report(test_labels, pred, target_names=class_names))max_features 参数限制只保留出现频率最高的 20000 个词,防止向量维度过高导致内存爆炸。ngram_range=(1,2) 同时保留了单词和二元词组,比如“北京”和“北京大学”会成为不同特征,但维度会大不少。sublinear_tf=True 用 1+log(tf) 代替原始词频,可以降低高频词的主导作用。MultinomialNB 的 alpha 是拉普拉斯平滑系数,alpha=0.5 是我在这种数据量下常用的起始值,如果准确率偏低,可以试着调成 1.0 或 0.1。
这里要特别注意:vectorizer 必须在训练集上 fit,然后只是 transform 测试集。如果对测试集也调用 fit_transform,会引入测试集信息,属于典型的数据泄漏,会让结果虚高,最终在真实环境中翻车。
3.3 评估指标与 result_bayes.txt 怎么读
训练结束后,分类报告会写进 result_bayes.txt。这个文件存在的意义是存档,避免 Notebook 关闭后结果丢失。看这份报告时不要只盯准确率,新闻分类里类别不平衡很常见,准确率可能被大类别拉高,小类别表现差却被掩盖。正确做法是看每个类别各自的 precision、recall、f1-score,再看 macro avg 和 weighted avg。
一般我会按这个顺序分析:先看哪个类别 recall 最低,说明该类别样本被分成别的类了;再看哪个类别 precision 最低,说明其他类的新闻被误分进来了。比如科技类 recall 低,大概率是“手机”“芯片”这些词和体育类中的“球队”“比赛”没有足够区分度。result_bayes.txt 是朴素贝叶斯的最终成绩单,后面 BERT 的结果会以同样的格式输出,这样两份报告可以直接对比。
| 指标 | 关注点 |
|---|---|
| precision | 预测为该类的样本有多少是真该类 |
| recall | 真实该类的样本有多少被找回来 |
| f1-score | precision 和 recall 的调和平均 |
| macro avg | 所有类别指标的简单平均,不受样本数量影响 |
| weighted avg | 按各类样本量加权平均,更适合不均衡数据 |
实验报告里建议放一张这种格式的指标表,把朴素贝叶斯和 BERT 的加权 F1 并列,一眼就能看出差异。
4. BERT 分类模型实现:Bert_train.ipynb 与 Kaggle 环境的适配
4.1 模型结构:BERT + 全连接分类头
BERT 部分使用的是预训练模型权重,核心思路是拿预训练好的参数初始化,然后在新闻数据上做微调。输入一段新闻文本,BERT 编码后得到每个 token 的向量,取 [CLS] 位对应的向量作为整句话的语义表示,再接一个 Dropout 层和 Linear 分类层。Linear 层输出维度就是类别数,最后经过 softmax 得到每个类别的概率。
这种做法比从零训练一个文本分类网络高效得多,因为 BERT 已经在大规模语料上学会了通用语言表示,微调只需要让它适应新闻领域的表达习惯。在实际实现中,Dropout 一般设 0.3,太小容易过拟合,太大则模型学不进去。
4.2 训练参数与核心训练代码
BERT 的训练代码集中在 Bert_train.ipynb,核心是把文本转成模型需要的三样东西:input_ids、attention_mask、token_type_ids。input_ids 是每个 token 在词表中的索引,attention_mask 标记哪些位置是真实内容,token_type_ids 在单句分类里全为 0。
# Bert_train.ipynb 中数据集封装与训练循环的简化写法 from transformers import BertTokenizer, BertForSequenceClassification import torch from torch.utils.data import DataLoader tokenizer = BertTokenizer.from_pretrained("bert-base-chinese") model = BertForSequenceClassification.from_pretrained( "bert-base-chinese", num_labels=num_classes ) def encode_texts(texts): return tokenizer( texts, padding=True, truncation=True, max_length=128, return_tensors="pt" ) # 这里省略 dataset 类,训练时每批取出 encodings 和 labels optimizer = torch.optim.AdamW(model.parameters(), lr=2e-5) model.train() for epoch in range(3): for batch in train_dataloader: outputs = model(**batch, labels=batch["labels"]) loss = outputs.loss loss.backward() optimizer.step() optimizer.zero_grad()max_length=128 是一个平衡点。新闻平均长度在 200 字左右,超过 128 的部分被截断会损失部分信息,但大多数分类线索集中在开头。如果你的机器显存比较紧张,可以改成 64;如果数据确实长,再考虑 256。学习率用 2e-5 是微调 BERT 的常见起点,太大容易让预训练权重偏移,太小则收敛缓慢。训练轮次 3 轮在这个数据集规模下足够,超过 5 轮通常会过拟合,表现为训练集 loss 持续降低但验证集不再提升。
4.3 kaggle_bertversion 与本地版的差异
本地训练 BERT 最大的障碍是硬件。BERT-base 参数量过亿,CPU 跑一个 epoch 可能几十分钟到几个小时,显存不够更是直接 OOM。所以资源里单独放了 kaggle_bertversion.ipynb,这个版本是为 Kaggle Notebook 的 GPU 环境准备的。它和本地版的主要差异有三处:数据读取路径改为 Kaggle 的 /kaggle/input 目录,训练时启用混合精度 fp16,保存结果时把分类报告写入 result_bert.txt。
# kaggle_bertversion.ipynb 中启用混合精度的关键片段 from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() model.cuda() for batch in train_dataloader: batch = {k: v.cuda() for k, v in batch.items()} with autocast(): outputs = model(**batch, labels=batch["labels"]) loss = outputs.loss scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() optimizer.zero_grad()混合精度让部分计算用半精度浮点数完成,能显著降低显存占用并加速训练,代价是精度轻微下降,但对分类任务基本无感。kaggle_bertversion 不能在工作目录直接跑,需要先上传数据到 Kaggle 数据集,再把 Notebook 附加过去。如果你没有 GPU 环境,直接用这个版本是最保险的。
5. 常见问题与排查:五个容易翻车的点,从报错到结果异常
5.1 缺依赖与环境不一致
现象:运行 Bert_train.ipynb 时提示 ModuleNotFoundError: No module named 'transformers',或者 torch 版本过低导致某些算子不支持。
原因:当前 Python 环境没有安装项目依赖,或者版本不匹配。同类问题在朴素贝叶斯部分也会出现,比如 sklearn 版本差异导致 API 行为不同。
解决:在项目目录先安装依赖,然后重启 Jupyter Kernel 再运行。
pip install torch transformers scikit-learn jieba pandas如果是在 Kaggle 环境,基础镜像已经内置大部分依赖,通常只需要补装 transformers。安装完一定重启 kernel,否则已加载的模块仍是旧版本,报错不会消失。
5.2 读取 CSV 时中文乱码
现象:pd.read_csv 之后看到中文变成乱码,或者直接报 UnicodeDecodeError。
原因:数据文件保存编码不是 UTF-8,新闻数据经常来自不同渠道,有些是 GBK 编码。
解决:读取时显式指定编码,先试 utf-8,再试 gbk。
df = pd.read_csv("../data/train.csv", encoding="utf-8")如果还报错,可以把 encoding 参数改成 "gbk" 或 "gb18030"。我一般会用记事本打开 csv 看右下角编码,不确定时写一个小循环轮询常见编码,避免反复手改。
5.3 朴素贝叶斯准确率偏低
现象:训练完朴素贝叶斯,准确率只有 0.6,甚至不到 0.5。
原因:最常见的是特征包含太多噪声,比如没去停用词、ngram 范围不合适、或者是向量化时把测试集也 fit 了一次。还有一种情况是类别标签编码不一致,训练集和测试集的类别编号错位。
解决:回到预处理 Notebook 重新清洗,确认 vectorizer 只 fit 训练集;单独打印几条测试文本,看看清洗后是否还残留无意义符号;检查 dataload.py 里 LabelEncoder 是否沿用训练集的映射。这种问题按“数据泄漏—特征质量—标签映射”的顺序排查,通常很快能定位。
5.4 BERT 训练时 OOM 或卡死
现象:运行到某一步提示 CUDA out of memory,或者进程直接崩溃。
原因:batch size 太大、max_length 太长、显存不足。Kaggle 的 GPU 显存 16G,但本地显卡可能只有 6G,直接套同样的参数就会翻车。
解决:把 batch size 从 32 降到 16 或 8,把 max_length 从 128 降到 64,并开启 fp16。如果还是没有显存,就减少数据量,先用 5000 条样本跑通流程,确认无误后再全量训练。不要一上来就全量训,先小规模做 smoke test,是处理这类问题的通用经验。
5.5 Notebook 结果和实验报告对不上
现象:实验报告里写的准确率是 0.93,自己跑出来只有 0.87。
原因:Notebook 中的 cell 没有按顺序全部执行。Jupyter 的 kernel 状态是累积的,可能你在某个 cell 里重新定义了变量,后面用到了旧结果,或者训练日志是最早试出来的一次,后续改动后没重新记录。
解决:验收时强制点击 Kernel -> Restart & Run All,让每个 cell 从干净状态重新执行,跑完后对比 result_bayes.txt 和 result_bert.txt 是否与报告一致。我发现大多数结果对不上都是因为没做这一步,而不是模型问题。做实验报告前,一定要重新生成一次最终结果文件。
6. 验收与答辩:从复现结果到讲清楚双模型对比
拿到这套代码,验收顺序应该固定为三遍。第一遍,先把朴素贝叶斯从数据集切分到 result_bayes.txt 完整跑通,记录准确率;第二遍,跑 BERT 的本地版或 kaggle_bertversion,得到 result_bert.txt;第三遍,从头重启 kernel 再跑一次,确认两个结果文件数值稳定。这套流程保证你在答辩现场不会翻车,评审看一眼你的 Notebook 执行历史是完整的,就会认可工作量。
答辩时不要只讲准确率,要讲对比结论。直接说“BERT 比朴素贝叶斯高了多少个点”太单薄,正确讲法是拆成三点:一是朴素贝叶斯在短文本和高频词上很有优势,训练只要几秒,适合做实时分类;二是 BERT 对语义理解更强,尤其在区分“电竞比赛”和“电子产业”这种易混类别上有明显优势;三是 BERT 的训练成本大约是朴素贝叶斯的几十倍,如果实际部署到低算力环境,朴素贝叶斯反而是更稳妥的选择。把这些话组织成一段两分钟的回答,评审问到的概率非常高。
我自己的习惯是,交任何大作业前强制走一遍“Restart & Run All”,哪怕多花十分钟,也不让现场跑出错。从那以后确实少了很多手忙脚乱。希望帮到你。
本文还有配套的精品资源,点击获取