尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

基于朴素贝叶斯的中文新闻分类实战:从分词调参到项目避坑

发布时间:2026/9/26 15:04:59

资讯中心
01
ARTICLE

基于朴素贝叶斯的中文新闻分类实战:从分词调参到项目避坑

基于朴素贝叶斯的中文新闻分类实战:从分词调参到项目避坑
简介基于朴素贝叶斯算法的新闻分类项目源码面向计算机相关专业的期末大作业与课程设计场景适合需要完成新闻分类任务或进行机器学习实战练习的学生参考使用。项目经导师指导、助教审定并获评98分源码均已完成本地编译与调试可直接运行。压缩包共2000个文件约13.33MB包含3个Python核心脚本负责贝叶斯模型训练、分类器构建与URL数据解析、1996个txt频率统计与预处理结果文件以及1个md说明文档便于核对词频分布并快速上手。目前已有152人学习下载读者可结合频率文件理解朴素贝叶斯训练流程并在此基础上调整参数、扩展语料用于作业提交、课程答辩或算法对比实验。1. 新闻分类第一道坎为什么朴素贝叶斯比想象中能打搜索“新闻分类项目源码”的人多半不是来复习概率论公式的而是手里攒了几千条已标注的新闻文本想尽快跑出一个能用的多分类模型。放到五年前这个需求难免要讨论“要不要直接上 BERT”但很多内容平台的第一版新闻分类恰恰就是朴素贝叶斯撑起来的——训练以秒计推理不需要 GPU短文本上的 F1 往往能到 0.85 上下。这个标题对应的项目源码核心是三件事把新闻文本清洗成可训练的表格数据、用朴素贝叶斯拟合一个多分类器、留出清晰可复用的预测接口。它适合刚接触机器学习、想从“跑通代码”走向“能调参数”的人也适合标注数据有限、又不想等深度学习训练时间的团队。2. 项目骨架怎么搭一个能直接跑的中文新闻分类最小结构2.1 目录结构与四个核心文件train、predict、evaluate、utils 谁管什么拿到项目源码先别急着跑先把文件职责分清。我一般会排成下面这样一棵树news_classifier/ ├── data/ │ ├── news_train.csv # 训练集content 列 category 列 │ ├── news_test.csv # 测试集结构与训练集一致 │ └── stopwords.txt # 每行一个停用词 ├── utils/ │ └── text_processor.py # 分词、清洗、停用词过滤都收在这里 ├── train.py # 加载数据 - 向量化 - 训练 - 持久化 ├── evaluate.py # 加载测试集 - 打印分类报告与混淆矩阵 ├── predict.py # 加载模型 - 对单条文本或批量文本预测 ├── models/ │ ├── nb_pipeline.pkl # 训练产物向量器 分类器打成一个包 │ └── label_map.json # 类别名与模型内部标签的映射 └── requirements.txt # jieba / scikit-learn / pandas / joblib这个结构好在职责分离数据、代码、模型产物分开放训练脚本只产出 models/ 下的文件预测脚本只消费 models/ 下的文件。很多人把训练和预测写在一个脚本里测试时还好一上线每个请求都要走一遍训练逻辑接口延迟好几秒就是这么来的。utils/text_processor.py 是整个项目最不能乱动的公共模块。train.py 和 predict.py 都从这里面 import tokenize 函数保证训练和预测走的是同一套分词逻辑。分词不一致是文本分类里最隐蔽的坑训练时“新能源汽车”是一个词预测时被切成了“新能源”“汽车”特征对不上模型效果立刻掉几个点。第一次跑通这个项目的顺序也很固定三条命令就够了pip install -r requirements.txt python train.py python evaluate.pypredict.py 放到最后跑因为它依赖 models/ 下的训练产物。如果 evaluate.py 打印出来的分类报告已经不差再拿 predict.py 做单条文本的冒烟测试这才是合理的推进顺序。2.2 数据接入把任意新闻语料归一化成两列 CSV模型不关心原始数据存在 MySQL、ES 还是 Excel 里它只认两列content 和 category。所以项目源码里第一个要跑通的逻辑一定是数据归一化。不管原始数据是从公开的中文新闻语料导出的还是从业务库里导出的常见做法都是先归一化成两列import pandas as pd from sklearn.model_selection import train_test_split # 原始数据字段名可能五花八门先只挑这两列 df pd.read_csv(raw_news.csv) df df[[content, category]].dropna() # 类型强制转字符串去掉首尾空格和多余空白符 df[category] df[category].astype(str).str.strip() df[content] df[content].astype(str).str.replace(r\s, , regexTrue) # 分层切分让训练集和测试集的类别比例保持一致 train_df, test_df train_test_split( df, test_size0.2, random_state42, stratifydf[category] ) train_df.to_csv(data/news_train.csv, indexFalse) test_df.to_csv(data/news_test.csv, indexFalse) # 顺手打印类别分布类别不平衡在这里就能看出来 print(train_df[category].value_counts())stratifydf[category] 是一个容易忽略但很重要的参数。朴素贝叶斯对先验概率敏感训练集里财经类占 60%、体育类只占 5%学出来的先验本身就是歪的不做分层切分测试集上的结论会进一步失真。random_state42 让切分结果可复现否则同一份数据跑两次报告对不上排查问题时会非常痛苦。dropna() 这里也有个现实问题新闻正文有时整行为空直接 drop 是最省事的。但如果 drop 之后发现数据少了一半问题就不在预处理而在源头的采集逻辑——很多采集源只抓到了标题没抓到正文这时应该回去修爬虫而不是在项目里硬扛。另外读 CSV 时建议统一指定 encodingutf-8。新闻语料里偶尔混着其他编码的文本不指定编码pandas 会默认用系统编码Windows 上经常在 train.py 第一行就报 UnicodeDecodeError。2.3 训练脚本一条 pipeline 把向量化和分类器绑在一起数据准备好了训练脚本可以非常短。不要把“分词、向量化、训练”拆成三步手动串直接用 sklearn 的 Pipeline 一步到位import joblib import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.pipeline import make_pipeline from utils.text_processor import tokenize df pd.read_csv(data/news_train.csv) X, y df[content], df[category] pipe make_pipeline( TfidfVectorizer( tokenizertokenize, ngram_range(1, 2), max_features30000, min_df2, max_df0.8, sublinear_tfTrue, ), MultinomialNB(alpha1.0), ) pipe.fit(X, y) joblib.dump(pipe, models/nb_pipeline.pkl)这段代码的核心逻辑是tokenize 是自定义分词函数返回一个词列表TfidfVectorizer 拿到词列表后统计 IDFMultinomialNB 直接吃 TF-IDF 稀疏矩阵。pipeline 把两步焊在一起predict.py 加载模型后对新文本只需要调 pipe.predict([text])内部会自动完成分词、向量化、预测三个动作。参数方面max_features30000 限制词典规模防止出现几十万维的超稀疏矩阵拖慢训练min_df2 去掉全语料里只出现一次的词这些词大概率是错别字或长尾噪声ngram_range(1, 2) 同时保留单词和相邻词对让“新能源汽车”这种组合词即使被切碎也能以二元词对的形式留下痕迹sublinear_tfTrue 对词频做对数缩放抑制长新闻里重复词被过度放大。这几个参数是新闻分类的稳妥起点不是最优值后面要用交叉验证再搜一轮。3. 朴素贝叶斯原理条件独立假设与拉普拉斯平滑的工程意义3.1 从贝叶斯公式到文本分类先验、似然、后验的三步换算读懂项目源码里的公式才能回答“为什么这么调参”。贝叶斯公式放在新闻分类场景里是这样的给定一篇文本 x它属于类别 y 的概率是 P(y|x) P(y) · P(x|y) / P(x)。其中 P(y) 是类别先验——训练集里财经类比体育类多先验就大P(x|y) 是似然——在财经类语料里“股市”这个词出现的概率分母 P(x) 对所有类别都一样实际计算时可以直接忽略因为我们要比较的是“哪个类别概率更大”不是概率本身。难点在 P(x|y)。一篇新闻有几十个词直接统计“在财经类下出现整句话 x”的概率需要海量数据且根本统计不完。朴素贝叶斯做了一个极强假设词与词之间条件独立。于是 P(x|y) 拆成每个词概率的连乘P(x|y) ≈ P(w1|y) · P(w2|y) · … · P(wn|y)。这个假设在语言学上明显是错的——“美联储”和“加息”根本不是独立出现的——但工程上它让计算变得可行结果还出奇地稳。这就是朴素贝叶斯能成为新闻分类基线的根本原因它不试图理解语言只做词的分布统计。对财经、体育、娱乐这种类别区分度明确的新闻词分布差异足够大即便独立性假设被违反类别的相对大小依然正确。工程上这叫“用有偏估计换可行性”在标注数据不足时这种取舍比追求模型完美更务实。3.2 多项式模型 vs 伯努利模型新闻正文该选哪个项目源码里用的是 MultinomialNB多项式朴素贝叶斯但细心的读者会发现 sklearn 里还有个 BernoulliNB伯努利朴素贝叶斯两者都常用于文本分类区别在特征的取值方式模型特征取值核心假设新闻场景适配度MultinomialNB词频或 TF-IDF 值特征服从多项式分布高正文长度差异大词频信息有价值BernoulliNB0 或 1词是否出现特征服从伯努利分布低只关心词是否出现丢弃了词频新闻正文少则几十字、多则几千字同一个词“经济”在一篇长文里出现 20 次在另一篇短文里出现 1 次两者的信息量显然不同。MultinomialNB 能利用这个差异BernoulliNB 则把所有非零特征一律视为 1等于把词频信息扔了。所以这个项目选 MultinomialNB 是合理的。但有一个边缘场景值得知道如果只拿新闻标题做分类标题通常不超过 30 字词频差异本来就小BernoulliNB 有时反而更稳因为它能抑制长标题里重复词带来的过度放大。如果业务本身就是标题分类可以在项目里保留一个开关两个模型都跑一遍对比用报告说话。3.3 手写一个 40 行的朴素贝叶斯分类器看懂 sklearn 背后在干什么只看 API 调用很难真正理解算法。我习惯在项目里保留一个简化版实现作用是教学和排查。这里给出核心训练与预测逻辑import math from collections import Counter, defaultdict class ManualMultinomialNB: def __init__(self, alpha1.0): self.alpha alpha self.class_counts Counter() # 每个类别的文档数 self.word_counts defaultdict(Counter) # 类别 - 词 - 词频 self.vocab set() def fit(self, X_tokens, y): for tokens, label in zip(X_tokens, y): self.class_counts[label] 1 for w in set(tokens): # 实际工程应按出现次数累加这里简化 self.word_counts[label][w] 1 self.vocab.add(w) def predict(self, tokens): best_label, best_score None, -float(inf) total_docs sum(self.class_counts.values()) for label in self.class_counts: # 先验 P(y)取对数防止下溢 log_prior math.log(self.class_counts[label] / total_docs) # 拉普拉斯平滑的分母该类别总词数 alpha * 词典大小 denom sum(self.word_counts[label].values()) self.alpha * len(self.vocab) log_likelihood 0.0 for w in tokens: count self.word_counts[label].get(w, 0) self.alpha log_likelihood math.log(count / denom) score log_prior log_likelihood if score best_score: best_label, best_score label, score return best_label这段代码演示的正是 sklearn MultinomialNB 的核心计算先验是对数概率似然是每个词的条件概率取对数后累加。用对数是为了防止几十个概率连乘后下溢成 0这是朴素贝叶斯工程实现里最关键的一个细节。alpha1.0 就是拉普拉斯平滑。注意分子 count alpha某个词在训练集的某类别里一次都没出现过count 为 0如果不加 alpha整个连乘直接变成 0这个词所在的文档就永远不可能被分到这个类别。alpha 相当于给每个词在各类别里预置了 1 次虚拟出现次数保证概率永远不会是 0。它在新闻分类里几乎是必开的sklearn 的默认值就是 1.0项目源码里沿用这个默认值是在“不调参也能用”和“给后续调参留空间”之间取的平衡。4. 特征工程决定上限分词、TF-IDF 与停用词的调参点4.1 jieba 分词精确模式加自定义词典别用搜索引擎模式朴素贝叶斯不管中文分词它只认空格分隔的 token所以分词质量直接决定特征质量。项目里一般用 jieba稳定、更新频繁分词速度也够。分词函数建议这样写import jieba # 项目启动时加载自定义词典路径放在配置文件里 jieba.load_userdict(data/userdict.txt) STOPWORDS set() with open(data/stopwords.txt, encodingutf-8) as f: for line in f: w line.strip() if w: STOPWORDS.add(w) def tokenize(text: str) - list[str]: words jieba.cut(text, cut_allFalse) return [ w for w in words if len(w.strip()) 1 and w not in STOPWORDS ]两个参数值得讲。第一cut_allFalse 表示精确模式这是新闻分类的默认选择cut_allTrue 是搜索引擎模式会输出大量冗余词组合让特征空间膨胀好几倍对朴素贝叶斯这种对特征规模敏感的模型很不友好。第二过滤逻辑里 len(w.strip()) 1 把单字词去掉中文单字词多数是“的、了、在”这类虚词以及大量噪声留着只会稀释有效特征。停用词过滤放在分词函数里而不是 TfidfVectorizer 的 stop_words 参数里是因为分词阶段就把停用词丢掉能减小词表规模后面向量化时的 min_df、max_df 统计也更贴合真实分布。userdict.txt 的格式是每行“词语 词频 词性”比如“新能源汽车 1000 n”不写词频和词性也可以只写词就能生效。4.2 TF-IDF 参数ngram_range、max_features、min_df、max_df 的推荐起点TfidfVectorizer 的参数往往比朴素贝叶斯本身的参数更影响最终效果。下面是新闻分类项目里最值得调的四个参数和我的经验起点参数推荐起点调整方向ngram_range(1, 2)调到 (1, 1) 损失词组信息(1, 3) 在数据量大时值得试max_features30000数据量小降到 10000维度太高训练慢且易过拟合min_df2数据量大提到 5去掉低频噪声max_df0.8降到 0.6 可进一步去掉“新闻”“记者”这类高频噪词sublinear_tfTrue用 1log(tf) 缩放词频抑制长文档里重复词的过度权重ngram_range(1, 2) 是新闻分类里最划算的选择。它保留单词同时生成相邻词对比如“新能源汽车”即使被 jieba 切成“新能源”“汽车”两个词二元词组也能捕捉到它们的相邻关系。代价是特征数量会翻几倍所以需要 max_features 做截断。min_df2 的含义是一个词至少在 2 篇文档中出现过才进入词表。全语料只出现一次的词要么是错别字要么是长尾专名对分类决策几乎没有贡献却会占一个特征维度。max_df0.8 表示在超过 80% 的文档里都出现的词会被忽略“新闻、记者、报道、今天”这类词在各类新闻里都高频出现区分度接近零留着只会稀释有效特征的权重。sublinear_tfTrue 是容易被忽略但收益明显的参数它把原始词频 tf 替换为 1log(tf)让词频从线性增长变成对数增长。一个词在长文里出现 100 次和出现 50 次缩放后的差异远小于原始数值的差异避免长文档里高频词主导整个向量。新闻正文长度差异极大这个参数几乎必开。4.3 停用词表通用表打底领域噪词必须自己加网上能下到各种版本的通用中文停用词表覆盖“的、了、是、在”这类功能词。但新闻分类光靠通用表远远不够真正的噪声是领域相关的。做财经新闻分类的人会发现“记者、来源、责任编辑”这些词在各类财经新闻里反复出现需要手动加进去。一个务实的做法是把停用词分成两层。第一层是通用表下载一个几百行的中文停用词表放 data/stopwords.txt第二层是领域词表每次看分类报告时找到那些在各类别里 tf-idf 排名都很高、但和类别语义无关的词追加到同一个文件里。追加之后要重新训练因为停用词影响的是词表构建阶段不只是预测阶段。需要提醒的是停用词不要加得太狠。像“经济、市场”这种词在财经类里出现频率显著高于其他类它们是有效的分类信号不能因为“看起来像背景词”就加进停用词表。判断标准只有一个这个词在各类别里的出现频率是否接近接近才删有明显差异就保留。5. 新闻分类避坑清单5 个最常见的翻车点与排查方法这几条是新闻分类项目里最容易翻车的点每条都是真实项目里攒下来的血泪经验按“现象、原因、解决”写清楚。5.1 翻车点一类别不平衡小类目被大类目吃掉现象训练集里体育类占 40%、文化类只占 4%训练完跑测试集体育类 F1 有 0.9文化类只有 0.3分类报告里文化类的 recall 尤其惨。原因sklearn 的 MultinomialNB 没有 class_weight 参数它只会老老实实按训练集里的类别比例学习先验。少数类先验太低后验被多数类压制预测时几乎不会输出少数类。解决在 fit 时通过 sample_weight 补偿少数类常见做法是按类别反比加权import numpy as np from sklearn.utils.class_weight import compute_class_weight classes np.unique(y) weights compute_class_weight(class_weightbalanced, classesclasses, yy) sample_weight np.array([weights[list(classes).index(label)] for label in y]) pipe.fit(X, y, multinomialnb__sample_weightsample_weight)注意这里传的是 multinomialnb__sample_weight因为 pipe 是 Pipeline参数要带上前缀指定到 MultinomialNB 这一步。排查时先看 classification_report 里每个类别的 support 值support 小于 100 的类别都要警惕。5.2 翻车点二直接照搬英文新闻组案例的预处理流程现象网上很多“朴素贝叶斯文本分类”教程基于英文 20 Newsgroups 数据集照搬预处理text.lower().split()结果中文新闻分类准确率只有 60% 出头甚至更低。原因英文单词天然以空格分词中文没有空格边界必须单独分词。而且英文可以放心做小写化和词形还原中文根本没有大小写却要做英文里不存在的自定义词典加载和领域停用词过滤。解决项目里所有文本处理入口必须经过 utils/text_processor.py 的 tokenize 函数不能直接调 sklearn 默认的 tokenizer。排查方法就是打印一条样本的分词结果如果中文被空格切开之后是一大堆单字说明分词这步压根没接上。5.3 翻车点三jieba 把领域词切碎特征语义丢失现象训练时“新能源汽车”被 jieba 切成“新能源”“汽车”这两个词单独出现在科技类和财经类新闻里都不少见模型很难分辨一条新闻到底在讲环保还是讲汽车市场。原因jieba 的默认词库是通用语料训练的对垂直领域专名覆盖不足。新闻、金融、医疗都有一堆复合词通用词库不会自动识别。解决维护一个 data/userdict.txt把业务领域的核心专名加进去一行一个词新能源汽车 碳达峰 认房不认贷 低空经济加载一次即可jieba.load_userdict 在进程启动时调用。加完词典必须重新训练模型因为分词结果变了整个词表都会跟着变。排查时在 tokenize 里临时打印几个高频专名的切分结果一眼就能看出切没切对。5.4 翻车点四预测阶段对单条文本做 fit_transform特征空间整个错位现象训练时用 fit_transform 得到 30000 维词表预测时为了“省事”对单条文本也调用 fit_transform结果预测概率乱套或者类别永远落在某一个类上。原因fit_transform 会重新拟合词表。单条文本只有几百个词重新拟合出来的词表只有几十个特征和训练时的 30000 维特征空间完全对不上。这是文本分类项目里最经典的翻车现场几乎每个从零写预测脚本的人都会踩一次。解决训练阶段 fit_transform预测阶段只准用 transform。pipeline 打包的意义就在这——模型持久化时把 TfidfVectorizer 和 MultinomialNB 一起存成 nb_pipeline.pkl预测时加载整个 pipeline 调 predict从源头杜绝了单独调用向量器引发错位。这也是我在 2.3 节强调 make_pipeline 的原因。5.5 翻车点五标题和正文等权拼接短标题信号被长正文淹没现象标题“沪指收复 3000 点”被拼进一篇 3000 字的财经正文后标题里那三四个关键词在 TF-IDF 向量中的权重被稀释到几乎不影响判断分类结果和只用正文没区别。原因TF-IDF 里词频是文档级统计标题只有几十个字拼接进正文后词频占比极小标题的强区分信号被长文稀释。解决常见做法是给标题加权。不要直接拼接而是把标题复制三到五份再和正文拼接代码上就是一行字符串乘法train_df[content] 标题 * 3 正文。这样标题里的关键词词频被放大TF-IDF 会给予更高权重。如果数据源本身没有标题只有正文跳过这个处理如果标题质量高这个加权几乎不花成本就能带来一到两个点的提升。提示标题加权倍数不要超过 5 倍否则模型会变成只认标题词正文里真正重要的长尾信息比如导语里的具体数据词会被完全无视。6. 把基线模型做扎实交叉验证、分类报告与模型持久化项目跑到这步说明手里的朴素贝叶斯新闻分类已经能工作了。最后这三个技巧决定它能不能从“能跑”变成“敢上线”。第一个技巧用交叉验证替代单次切分。把 2.3 节的 pipeline 丢给 GridSearchCV搜 alpha 和 ngram_range 的搭配from sklearn.model_selection import GridSearchCV param_grid { tfidfvectorizer__ngram_range: [(1, 1), (1, 2)], multinomialnb__alpha: [0.1, 0.5, 1.0, 2.0], } grid GridSearchCV(pipe, param_grid, cv5, scoringf1_macro, n_jobs-1) grid.fit(X, y) print(grid.best_params_, grid.best_score_)scoring 选 f1_macro 而不是 accuracy因为新闻类别往往不平衡accuracy 会被多数类掩盖。交叉验证的意义是看方差如果五折分数波动超过两个点说明数据量不够或者类别分布不稳这时候调再细的参数都没用应该回去补数据。第二个技巧每次训练后固定看三个数。precision 看模型输出这个类别时有多可靠recall 看这个类别的新闻有多少被找回来了F1 是两者的调和。重点看 macro avg而不是 overall accuracy。如果某一类的 precision 高但 recall 低说明模型很保守只在该类特征极其明显时才肯输出该类这时再用 5.1 节的 sample_weight 方法给该类加权。第三个技巧joblib 存的是整个 pipeline加载后先用一条真实业务文本做冒烟测试。我个人的习惯是训练完随手跑一下 predict.py输入一条“今日 A 股三大指数集体收涨沪指涨 0.78%”确认输出是财经类再收工。模型持久化文件就是后悔药用 pipeline 统一打包之后这条流程多跑几遍踩坑的概率就低很多。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。