资讯详情 新闻文本分类双模型实战:朴素贝叶斯+BERT全解析
📅 2026/10/10 15:33:37
简介面向机器学习课程设计与期末大作业场景这套基于BERT与朴素贝叶斯算法的新闻文本分类项目提供了从数据预处理、特征工程到模型训练与评估的完整解决方案。资源共收录23个文件包括8个ipynb交互式分析脚本、3个txt结果日志、2个csv数据集、2个split_dataset与split_testset切分目录以及实验报告docx、依赖说明md和1个Python脚本压缩包整体约67.39MB。其中BERT训练脚本与朴素贝叶斯对照实验并列清晰展示深度模型与传统算法的效果差异。已有542人学习下载适合希望快速复现高分项目、理解文本分类全流程的本科生与入门开发者。相较于其他零散代码本包额外提供新闻预处理步骤、数据加载脚本dataload.py及result_bayes.txt与result_bert.txt结果文件便于验证实验结论撰写实验报告或课程设计文档时可直接引用。1. 机器学习大作业里的新闻文本分类双模型方案才是省力答案期末选题表里躺着“新闻文本分类”那一刻多数人的第一反应是朴素贝叶斯——因为课堂上就讲了这个代码也好抄。但交过作业的都知道只交一个朴素贝叶斯报告没有任何对比实验老师问起来也答不出“为什么不试试深度学习”。这个压缩包给的是两条腿走路的方案朴素贝叶斯当基线和解释性素材BERT做精度冲刺数据集和源码都配齐了属于拿到手能直接跑的机器学习大作业套路。适合正在做课程设计、期末大作业、或者想在生产里快速搭一个文本分类基线的人。不适合一点不想读代码、指望双击就出结果的人——任何分类项目都要先搞懂数据格式和训练入口这不是一个能靠直觉绕过去的工具包。2. BERT 和朴素贝叶斯双轨选型为什么这条路线比单模型稳2.1 朴素贝叶斯老算法在大作业里承担的角色不光是凑数朴素贝叶斯在新闻文本分类里用的最多的是 MultinomialNB配合 TF-IDF 特征。它的核心是贝叶斯定理加一个很强的条件独立性假设——每个词在给定类别下独立出现。新闻文本里“国际”“财经”“体育”这种类别词分布差异很大条件独立性假设虽然粗略但在短文本上往往比很多线性模型还稳因为它把每个词当作独立证据不会被词组间的共线关系干扰。在大作业场景里朴素贝叶斯承担的其实是三个任务给 BERT 的结果当基线、在报告里写出“经典方法 vs 深度方法”的对比、以及当机器跑不动大规模预训练模型时保住基本盘。用 scikit-learn 跑一个 TF-IDF MultinomialNB核心代码量很小但参数有三个值得动alpha拉普拉斯平滑系数默认 1.0。新闻分类里如果某个词在某个类别从没出现过平滑系数能避免概率算成 0。我一般会在 0.1 到 1.0 之间试两个值。fit_prior是否学习类别的先验概率。新闻类别如果本身不均匀比如“体育”样本量是“国际”的两倍保留先验会更合理设为True。ngram_rangeTF-IDF 向量化时是否用(1, 2)二元词。加了二元词准确率通常会涨一两个点代价是特征维度暴涨。一个常见误用是把 CountVectorizer 的min_df设得太小。新闻数据里大量只出现一两次的生僻词进特征矩阵后全是稀疏噪音。我一般会设min_df2也就是至少在两个文档里出现过的词才保留跑出来的模型更干净。2.2 BERT把预训练模型当特征提取器而不是黑匣子BERT 在新闻文本分类里的做法很标准用bert-base-chinese做特征编码取[CLS]位置的输出向量再接一个全连接分类头。[CLS]这个位置是 Transformer 编码器最后层对整个序列的总结向量隐含了全文的信息接一个 Linear 层做分类是 HuggingFace 里最常见的下游任务结构。为什么说它不是黑匣子因为你需要理解两个关键参数max_len输入序列的最大长度。新闻文本动辄几百上千字BERT 的 self-attention 计算复杂度是序列长度的平方塞满 512 个 token 不仅慢而且长文本后半段的 attention 分布早就稀疏了。处理新闻这种场景max_len128或256基本够用标题加前几个段落已经把信息集中在前面了。batch_size预训练模型微调时对显存非常敏感。16 的 batch size 在 6G 显存的卡上跑bert-base-chinese12 层、768 维、约 1.1 亿参数刚好擦边再大就 OOM。在选型上BERT 负责的是“把准确率往上推”这件事。同一个新闻分类数据集TF-IDF 朴素贝叶斯能到 90% 上下的话BERT 微调后能到 95% 以上。差距主要来自两个方面一是 BERT 的上下文建模能处理一词多义比如“苹果”在“苹果公司发布新品”和“苹果削皮后容易氧化”里的含义完全不同朴素贝叶斯把同一个词当成同一个特征二是预训练阶段已经学到了大规模中文语料的语法和语义知识即使新闻数据只有几千条也能借到外部知识。2.3 两条路怎么在同一个项目里分工协作我拿到一个文本分类项目第一件事永远是跑一个朴素贝叶斯基线第二件事才是决定要不要上 BERT。这不是因为我保守而是因为基线模型能告诉你数据集本身难不难。如果 TF-IDF 特征下朴素贝叶斯准确率不到 85%那大概率是数据切分有问题、标签噪音大、或者类别严重不均衡——这些问题不解决直接上 BERT 也只是让模型把噪音记住而不是学到真正的模式。我整理一下在这个新闻分类项目里两条路的参数边界可以保存下来对照环节朴素贝叶斯路线BERT 路线特征输入jieba 分词 TF-IDFBertTokenizer 分字模型容量几十万维稀疏特征1.1 亿参数稠密向量训练资源CPU 几分钟GPU 几十分钟到几小时典型准确率85%-90% 区间95% 以上区间报告价值基线、可解释、对比精度冲刺、体现深度主要风险词表噪音、未登录词显存溢出、过拟合这个对比表不是定死的。如果数据集特别大比如十几万条新闻朴素贝叶斯和 BERT 的差距可能缩到 3 个点以内这时要不要花几小时跑 BERT 就看你报告需要什么。如果数据集只有两三千条BERT 很容易过拟合训练集 99%、测试集 90% 的情况很常见需要加重 Dropout 或者减小学习率。机器学习实战那套 scikit-learn 套路在中小型数据集上的表现往往会颠覆你“深度学习一定更强”的直觉。3. 把压缩包跑起来从解压到第一次看到分类报告3.1 目录结构先搞清楚哪些文件需要你动手改拿到压缩包第一件事不是看代码而是看目录结构。这个项目典型的布局是这样的news_classifier/ ├── data/ │ ├── 财经/ # 每个类别一个文件夹内部是 .txt 新闻正文 │ ├── 体育/ │ ├── 娱乐/ │ └── 科技/ ├── src/ │ ├── train_nb.py # 朴素贝叶斯训练入口 │ ├── train_bert.py # BERT 微调训练入口 │ └── predict.py # 加载模型推理 ├── output/ # 模型权重和报告输出 └── README.md这种“每个类别一个文件夹”的数据组织方式是最常见的新闻分类数据集格式解压后一眼就能确认数据量。我建议你坐下来数一下每个文件夹里的文件数量然后用wc -l看一眼新闻平均长度——这两个数字决定了后面所有参数怎么设。# 统计每个类别的样本数量和平均长度 for dir in data/*/; do count$(ls $dir | wc -l) avg_len$(cat $dir/*.txt | awk {sumlength($0)} END {print sum/NR}) echo $dir: $count 篇, 平均长度 $avg_len 字符 done注意看avg_len。如果平均长度在 300 字符以下BERT 的max_len设 128 就够了如果到了 1000 以上max_len就得提到 256否则后文被截断会损失信息。这一步不做后面跑出来的准确率差两个点你都找不到原因。3.2 环境准备torch、transformers 与 sklearn 的最小匹配这个项目依赖三个核心库scikit-learn负责朴素贝叶斯和评估指标transformers负责加载 BERT 模型和分词器torch是 BERT 的后端。版本匹配是这类项目最容易翻车的地方transformers 4.x 和 torch 1.10 以上的组合比较稳定我自己一般会新建一个虚拟环境避免和系统里的旧版本打架。python -m venv venv source venv/bin/activate pip install scikit-learn jieba torch transformers这里有个细节transformers会顺便装tokenizers和numpy的依赖但不会主动装torch所以要显式指定。如果显卡是 NVIDIA 的建议提前装好 CUDA 版的 torch否则训练时device会检测成 CPUBERT 跑起来慢到你怀疑人生。检查环境是否就绪可以跑一段很短的验证代码import torch import transformers from sklearn.naive_bayes import MultinomialNB print(torch:, torch.__version__) print(cuda available:, torch.cuda.is_available()) print(transformers:, transformers.__version__)如果cuda available输出False后面 BERT 训练默认用 CPU一个 epoch 可能要跑几十分钟这是项目里最大的坑之一。解决办法是在第 5 章避坑里展开这里先记住BERT 路线默认认为你有可用 GPU。3.3 跑通训练入口两个脚本的入口参数怎么调环境就绪后先跑朴素贝叶斯。这个脚本不依赖 GPU两三分钟就能看到结果能帮你验证数据读取和标签映射有没有问题。python src/train_nb.py \ --data_dir data \ --output_dir output \ --ngram_range 1 2 \ --min_df 2脚本内部的核心逻辑是一个 jieba 分词 TF-IDF MultinomialNB 的流水线。分词这一步对中文特别关键直接拿原文做 TF-IDF每个汉字都是独立特征“机器学习”会被拆成“机器”“学习”两个互不相关的特征。加了 jieba 分词后机器学习才会作为一个整体特征出现这也是朴素贝叶斯在中文本上表现差异最大的一个处理。import jieba from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.pipeline import make_pipeline def tokenize(text: str) - list: # 自定义分词函数交给 TfidfVectorizer 的 tokenizer 参数 return [w for w in jieba.cut(text) if w.strip()] pipeline make_pipeline( TfidfVectorizer(tokenizertokenize, ngram_range(1, 2), min_df2), MultinomialNB(alpha0.3) ) pipeline.fit(train_texts, train_labels)这段代码里TfidfVectorizer接收tokenizer参数直接用 jieba 分词结果建词表ngram_range(1, 2)同时保留单个词和相邻双词的特征MultinomialNB(alpha0.3)是平滑系数。make_pipeline把向量化和分类器包装成一个整体后面predict时只需调用同一个 pipeline 对象不需要手动对文本做向量化。朴素贝叶斯跑通后再跑 BERTpython src/train_bert.py \ --data_dir data \ --output_dir output/bert_model \ --max_len 128 \ --batch_size 16 \ --epochs 3 \ --learning_rate 2e-5BERT 的训练脚本比朴素贝叶斯长得多核心结构是BertForSequenceClassification加一个训练循环。这里有一个值得注意的参数learning_rate用 2e-5 而不是常见的 0.01这不是笔误BERT 微调时预训练权重已经收敛得很好了用大学习率会把学到的特征冲掉。epochs设 3 也是经验值新闻分类场景下 3 轮训练已经足够再多就会过拟合。4. 把项目变成自己的换数据集、改类别、设计对比实验4.1 数据格式把不整齐的文件夹转成结构化 CSV拿到项目后如果你打算换自己的数据集或者想调整类别体系第一步是把“文件夹里散落的 txt”切分成结构化数据。我一般会写一个转换脚本统一输出成两列 CSV后面无论喂给 sklearn 还是 HuggingFace 数据集都方便。这个步骤的核心价值是在做任何模型实验前把数据验证逻辑固定下来避免每次训练前都要检查十几个文件夹。import os import csv import random data_dir data output_csv news_processed.csv label_map {财经: 0, 体育: 1, 娱乐: 2, 科技: 3} rows [] for label, label_id in label_map.items(): folder os.path.join(data_dir, label) for filename in os.listdir(folder): filepath os.path.join(folder, filename) with open(filepath, r, encodingutf-8) as f: # 新闻文本可能有多行读完后统一去换行符 content f.read().replace(\n, ).strip() if content: # 跳过空文件 rows.append((content, label_id)) random.seed(42) random.shuffle(rows) with open(output_csv, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([text, label]) writer.writerows(rows)这里有两个容易踩的细节。第一读取 txt 时encoding必须显式指定为utf-8很多 Windows 上生成的新闻数据是 GBK 编码不指定的情况下默认编码在某些环境里会直接抛UnicodeDecodeError。第二random.seed必须在 shuffle 前固定否则每次跑出来的数据顺序都不一样后面模型对比时无法判断准确率差异到底是模型带来的还是数据切分带来的。4.2 分层抽样与种子让你的实验可复现从 CSV 读回来以后划分训练集和测试集这一步要用分层抽样。新闻分类的类别分布通常不均如果直接随机切有可能测试集里某个类别的样本特别少导致准确率波动巨大。train_test_split的stratify参数就是干这个的。import pandas as pd from sklearn.model_selection import train_test_split df pd.read_csv(news_processed.csv) X_train, X_test, y_train, y_test train_test_split( df[text], df[label], test_size0.2, stratifydf[label], random_state42 )stratifydf[label]会让训练集和测试集里的类别比例保持与原全集一致。random_state42是固定随机种子任何可复现实验的第一个要求。如果你做完实验换了随机种子重新跑准确率从 91% 变成 89%那不是算法的错是数据切分变了。两个模型做对比实验时必须保证它们用的是同一份训练集和测试集否则报告里的对比表格完全没有意义。4.3 评估指标别只盯着 accuracy新闻分类大作业里最容易拿高分但最常被忽略的环节是评估。很多人只输出一个accuracy就结束了但老师想看到的是分类报告和混淆矩阵这两个东西能直观展示模型在每个类别上的表现。classification_report里的 precision、recall、f1-score 是按类别计算的宏平均和加权平均的区别也要能讲清楚。from sklearn.metrics import classification_report, confusion_matrix import seaborn as sns import matplotlib.pyplot as plt y_pred_nb nb_pipeline.predict(X_test) print(classification_report(y_test, y_pred_nb, target_names[财经, 体育, 娱乐, 科技])) cm confusion_matrix(y_test, y_pred_nb) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabels[财经, 体育, 娱乐, 科技], yticklabels[财经, 体育, 娱乐, 科技]) plt.savefig(output/confusion_matrix_nb.png)在新闻分类里准确率相同但混淆矩阵不同说明的问题完全不一样。比如“娱乐”和“科技”经常混淆因为科技新闻里会出现大量人名和公司名而“体育”类别因为术语独特通常 precision 很高。报告里如果能写一句“体育类 precision 高但科技类被误分为娱乐的比例较大推测原因是科技新闻中包含了艺人代言、影视剧推送等内容”这就是一个 80 分报告和一个 95 分报告的差距。混淆矩阵正是帮你发现这个规律的。5. 避坑记录这个新闻分类项目里最容易翻车的四个问题5.1 中文乱码UnicodeDecodeError 或训练出满屏乱码现象读取 txt 时报错UnicodeDecodeError: gbk codec cant decode byte 0x...或者训练完成后预测结果全是同一个类别。原因新闻数据集来源复杂有的 txt 是 UTF-8 保存有的是 GBK 保存混在一个文件夹里。Python 的open()默认编码跟随系统Windows 中文系统下默认 GBK读 UTF-8 文件就会报错。乱码反向问题也常见——用 UTF-8 读 GBK 文件不报错但内容全乱模型得到的是无意义的字符序列。解决所有读取文件的地方显式指定encodingutf-8并在读取后做一次异常兜底。我一般会在数据加载函数里写一个编码探测def read_text(filepath: str) - str: for enc in (utf-8, gbk, gb18030): try: with open(filepath, r, encodingenc) as f: return f.read() except UnicodeDecodeError: continue return gb18030是 GBK 的超集覆盖了更多生僻字符。这样即使一个文件夹里混着不同编码也能全部读出来不至于训练数据量忽大忽小。从那以后我每次处理中文语料第一步永远是统计read_text返回空字符串的文件数量超过 5 个就说明数据本身有问题。5.2 BERT 训练时显存溢出CUDA out of memory现象跑train_bert.py到第一个 batch 时直接报CUDA out of memory或者训练了几个 step 之后才炸掉。原因bert-base-chinese约 1.1 亿参数每个 batch 要同时保存所有层的中间激活值。max_len128和batch_size16在 6G 显存上已经非常极限如果后台还开着浏览器或 Jupyter显存直接被占满。解决先确认train_bert.py里有没有把模型和梯度都放到同一个设备上然后按顺序调参。优先降batch_size到 8如果还炸就把max_len降到 128 以下最后再考虑用梯度累积。torch.cuda.empty_cache()只能在 Python 层面释放缓存对显存碎片没有帮助真正的解决路径是缩小 batch 对应的计算量。# 失败的梯度累积替代方案 trainer Trainer( modelmodel, argsTrainingArguments( per_device_train_batch_size8, gradient_accumulation_steps2, # 实际等效 batch_size16 fp16True, # 半精度训练显存几乎减半 ), )fp16True用半精度浮点数训练显存占用接近减半但前提是你的显卡支持。RTX 20 系及以上基本都支持老一点的卡可能不认。gradient_accumulation_steps2的含义是每 2 个 step 才做一次参数更新等效于 batch_size16但显存只占用 8 的规模。5.3 损失不下降BERT 训练几个 epoch 后准确率纹丝不动现象训练 loss 从 0.7 降到 0.3 后就不再变化验证集准确率停留在 85% 左右比朴素贝叶斯高不了多少。原因一种情况是学习率太大BERT 预训练权重被破坏模型在乱学另一种情况是新闻类别本身有重叠比如科技类新闻里大量出现娱乐内容单靠文本前 128 个 token 不足以区分。我见过最离谱的一次是标签映射写错了——数据预处理里label_map把“科技”和“娱乐”的编号调反了模型学了半天loss 低是因为在记噪音。解决先用 20 条样本做一次过拟合测试如果 loss 能降到接近 0说明模型和数据管道没问题如果 20 条都降不下去先检查标签映射和数据读取。这种排查顺序能帮你区分是模型问题还是数据问题。# 用极小数据集验证管道是否通畅 mini_train X_train[:20] mini_labels y_train[:20] model BertForSequenceClassification.from_pretrained(bert-base-chinese, num_labels4) training_args TrainingArguments(output_dir./debug, learning_rate2e-5, num_train_epochs10) # 训练完毕后看 training_loss 是否趋近于 020 条样本 10 个 epoch只要数据管道没毛病模型一定能完美记住这 20 条loss 降到 0.01 以下。如果这步都不通过后面所有实验都是徒劳。5.4 测试集信息泄露报告里准确率高得离谱现象BERT 在测试集上准确率 99%远超正常水平但我检查数据时发现训练集和测试集里有完全相同的新闻。原因原始文件夹切分时没有做去重。新闻爬虫经常把同一篇稿子放在多个类别文件夹里或者同一篇新闻被稍加修改后重复出现。如果不做去重直接划分训练集和测试集共享了大量相同文本模型相当于开卷考试测试准确率高但没有实际意义。解决在数据预处理阶段对所有新闻正文求哈希值按哈希值去重后再划分数据集。这一步对报告的说服力至关重要因为老师一旦抽查测试集里某条新闻并发现它和训练集重复整份报告的可信度就清零了。import hashlib df[hash] df[text].apply(lambda x: hashlib.md5(x.encode(utf-8)).hexdigest()) df df.drop_duplicates(subsethash, keepfirst)md5在这里不是加密用途只是快速判断文本是否完全相同的指纹。去重后你会发现训练集样本量掉一两成这是正常的准确率也会恢复到合理区间。6. 交付前最后一步用一个独立推理脚本证明模型真的能用大作业交付前我最怕的翻车不是训练跑不出来而是答辩现场换了一台机器你发现自己只能在 Jupyter 里跑训练没有一个干净的单文件推理脚本。训练代码里充满了数据加载、标签映射、评估逻辑混在一起 200 行现场根本来不及解释。我后来养成一个习惯任何分类项目训练完必须写一个独立的predict.py从硬盘加载模型权重输入一段字符串直接输出类别整个过程不超过 10 秒。import torch from transformers import BertTokenizer, BertForSequenceClassification label_map {0: 财经, 1: 体育, 2: 娱乐, 3: 科技} # 加载分词器和模型注意路径必须指向保存权重的目录 tokenizer BertTokenizer.from_pretrained(output/bert_model) model BertForSequenceClassification.from_pretrained(output/bert_model) model.eval() def predict(text: str) - str: inputs tokenizer(text, truncationTrue, max_length128, return_tensorspt) with torch.no_grad(): logits model(**inputs).logits pred_id torch.argmax(logits, dim-1).item() return label_map[pred_id] # 测试样例 print(predict(国足今晚在客场以2比0战胜对手晋级下一轮预选赛)) print(predict(某科技公司发布新一代智能手机搭载自研处理器))这段代码里有三个关键点。第一model.eval()必须调用关闭 Dropout 和 BatchNorm 的训练行为否则同一条新闻每次预测结果都可能不同。第二with torch.no_grad()关闭梯度计算推理时少占内存且更快。第三truncationTrue和max_length128必须和训练时保持一致否则该截断的没截断预测结果会和训练指标对不上。这个推理脚本同时也是报告里的“模型应用展示”章节素材。老师问“这个模型能干嘛”你直接现场敲两行命令输入「某球队主场大胜」输出「体育」再输入「某厂商发布旗舰手机」输出「科技」。效果比任何流程图都直观。我也遇到过 BERT 权重文件和脚本代码不在同一路径的场景写脚本时用了硬编码的相对路径换目录跑就报FileNotFoundError。所以现在交付时我会强制走一遍把output/bert_model文件夹和predict.py拷贝到一台干净机器从头跑一次推理确认路径没有依赖当前目录结构。从那以后这个大作业项目我再也没在答辩现场翻过车。希望帮到你。本文还有配套的精品资源点击获取