朴素贝叶斯新闻分类实战:从语料采集到模型部署的完整流程

发布时间:2026/9/13 18:35:51
朴素贝叶斯新闻分类实战:从语料采集到模型部署的完整流程 简介这是一份基于朴素贝叶斯算法的中文新闻文本分类项目实践资源从理论推导到工程实现适合人工智能初学者或自然语言处理入门者用来理解经典分类模型落地流程。资源围绕财经、科技、汽车、房产、体育、娱乐、其他七类新闻构建语料共7024个文件其中主体为7019个txt新闻样本另有4个Python脚本和一个pyc文件整个压缩包大小约为13.3MB。脚本涵盖URL解析、词频统计、贝叶斯训练等功能模块整体流程清晰可直接运行学习。已有762人学习下载适合用于课堂实验、课程设计或专项练习。通过复现这套案例读者不仅能掌握贝叶斯定理在文本分类中的应用还能了解从网页数据采集到模型训练评估的完整项目链路获得可直接改造使用的分类代码与结构化语料并通过真实新闻数据验证分类效果。1. 为什么朴素贝叶斯还能扛起新闻分类项目实践的大梁深度学习成为默认选项的时代朴素贝叶斯经常被看成“没有技术含量”的旧方案但放到真实项目里它的价值恰恰体现在低成本和高可解释性。新闻分类任务不需要理解语义只需要在给定标签下比较概率大小如果只给你几千篇已标注新闻和一台没有 GPU 的服务器朴素贝叶斯能在半小时内完成从抓取、清洗、训练到分类的全部流程而且每个参数都能说得清依据。本项目以腾讯新闻和凤凰资讯的滚动页为数据源整理出财经、科技、汽车、房产、体育、娱乐、其他共 7 类新闻语料用朴素贝叶斯实现文本分类。对做人工智能大作业、人工智能毕业设计或是在服务端做轻量级文本过滤的人这是最值得先跑通的一条技术路线。2. 训练文本准备parseurl.py 采集新闻、fre.py 输出词频特征2.1 七类新闻源与训练文件的命名约定项目里的语料来自腾讯新闻各频道和凤凰资讯的滚动新闻列表一共分 7 类。每个类别的新闻地址在采集时就已经固定好训练阶段依赖的就是这些新闻正文采集有效性和新闻时效性会影响最终分类效果。下表是项目里约定的类别编号训练文件名也直接用了这个编号。编号类别采集来源1财经腾讯财经滚动新闻页2科技腾讯科技滚动新闻页3汽车腾讯汽车滚动新闻页4房产腾讯房产新闻页5体育腾讯体育滚动新闻页6娱乐腾讯娱乐新闻页7其他凤凰网综合资讯列表页训练语料以编号_bayestraining.txt命名例如7_bayestraining.txt就是“其他”类的全量文本。项目目录里既有1_bayestraining.txt到7_bayestraining.txt还保留了7_bayestraining.txt、2_bayestraining.txt这样多份带编号的训练文件说明语料是按类别拆分保存的而不是一个大文件里混着标签。训练代码读取时取文件名下划线前的数字直接转成 int 作为类别标签# 训练文件读取逻辑 import os def load_category_files(data_dircorpus): corpus {} for fname in os.listdir(data_dir): if not fname.endswith(_bayestraining.txt): continue label int(fname.split(_)[0]) # 从文件名拿到类别编号 with open(os.path.join(data_dir, fname), encodingutf-8) as f: corpus[label] [line.strip() for line in f if line.strip()] return corpus corpus load_category_files() print({k: len(v) for k, v in corpus.items()})这段代码的作用是把 7 个文件读成一个字典key 是类别编号value 是新闻正文列表。注意fname.split(_)[0]这个写法依赖文件名数字必须出现在最前面如果后续有人把文件名改成bayestraining_7.txt这里就要改成fname.split(_)[-1].split(.)[0]。我一般会加上异常保护避免手滑改了文件名后整个训练静默出错。2.2 parseurl.py 的抓取与正文提取逻辑parseurl.py在这个项目里负责从列表页抓取新闻链接再进入详情页提取正文。腾讯新闻滚动页的链接规律比较直接新闻详情基本都是.htm结尾凤凰网部分页面则是.shtml。我第一次跑的时候直接按整页 HTML 去匹配链接结果把导航栏里的大量 URL 也抓了进来。# parseurl.py 采集骨架 import re import urllib.request from html import unescape def get_html(url): req urllib.request.Request(url, headers{User-Agent: Mozilla/5.0}) resp urllib.request.urlopen(req, timeout10) raw resp.read() # 腾讯部分频道使用 GBK 编码 return raw.decode(gbk, errorsignore) def extract_links(list_html): # 只保留带日期特征的新闻页链接过滤栏目首页 links re.findall(rhref([^]?\.s?html), list_html, re.I) news_links [url for url in links if /a/ in url or 2015 in url or 2011 in url] return list(dict.fromkeys(news_links)) # 去重且保持顺序 def extract_content(article_html): title re.search(rh1[^]*(.*?)/h1, article_html, re.S) paragraphs re.findall(rp[^]*(.*?)/p, article_html, re.S) clean lambda s: unescape(re.sub(r[^], , s)).strip() text .join(clean(p) for p in paragraphs) return title.group(1).strip() if title else , text这里有两个工程细节值得注意。第一个是编码问题腾讯新闻频道页很多用 GBK虽然 HTML 标签里可能写着charsetgbk直接用decode(gbk)比依赖requests的自动识别更可靠errorsignore是为了防止某个生僻字让整个页面解析崩溃。第二个是对提取后的正文做标签清除我习惯用正则先抽p再清标签比直接去掉所有script和style更稳因为跑掉的那些空白字符和脚本片段不会再混进词频统计。2.3 fre.py 统计词频与低频词过滤新闻正文提取出来后是纯文本但朴素贝叶斯分类器需要的是词和频次。fre.py做两件事统计词频并过滤掉只出现极少的噪声词。直接处理一行一篇新闻的数据每行按空格分词也就是词袋模型。下面的代码看起来简单但真正决定特征质量的是min_count的选择。# fre.py 词频统计与低频词过滤 from collections import Counter def build_vocab(lines, min_count1): vocab Counter() for line in lines: # 每行是一篇已经分好词的新闻词之间是空格 tokens line.strip().split() vocab.update(tokens) return {w: c for w, c in vocab.items() if c min_count} vocab build_vocab(corpus[1], min_count2) print(财经类词典大小:, len(vocab))min_count2表示词在全类别里至少出现两次才保留用来切掉单次出现的人名、乱码和数字噪声。但这里要敲个警钟把min_count调高到 3 或 5 时低频但具有强类别辨识度的词也会被删掉新闻分类本来就依赖主题词比如“央行”“篮板”“院线”这类词频率不高但一出现基本就能定类别。所以真实项目里我一般先把min_count1跑一遍看误分类样本里的关键词再决定是否加阈值。3. 训练阶段bayestraining.py 的先验概率与平滑条件概率3.1 从贝叶斯定理到可计算的概率表朴素贝叶斯在文本分类里使用的公式是P(c|d) P(c) * P(w1|c) * P(w2|c) * ... * P(wn|c) / P(d)其中P(c)是类别先验概率表示所有新闻里属于财经类的比例P(wi|c)是类条件概率表示在财经类新闻中第 i 个词出现的概率。分母P(d)对同一篇新闻的所有类别都是一样的所以在做类别比较时可以直接约掉训练阶段只需要统计先验和条件概率两张表。P(c) 的计算方式很简单就是该类新闻数除以总新闻数。P(wi|c) 则用词频统计某词在财经类新闻中出现的次数除以财经类新闻的总词数。但这里存在一个致命问题如果一个词只在汽车类训练集出现在财经类新闻里从没出现过那 P(该词|财经) 就是 0连乘后整个财经类的概率直接等于 0。解决办法是拉普拉斯平滑也就是给分子加 1分母加词表大小这样保证每个词在每个类别下都保留一个很小的非零概率。3.2 bayestraining.py 的训练核心代码bayestraining.py做的就是把上面的公式落成数据统计。项目里有7_bayestraining.txt、1_bayestraining.txt等多份语料训练代码遍历目录下所有*_bayestraining.txt。# bayestraining.py 训练核心 import os import json import math from collections import defaultdict class NaiveBayesTrainer: def __init__(self, data_dircorpus): self.data_dir data_dir self.class_doc_cnt defaultdict(int) self.class_word_cnt defaultdict(lambda: defaultdict(int)) self.class_total_words defaultdict(int) def train(self): for fname in os.listdir(self.data_dir): if not fname.endswith(_bayestraining.txt): continue label fname.split(_)[0] path os.path.join(self.data_dir, fname) with open(path, encodingutf-8) as f: for line in f: words line.strip().split() if not words: continue self.class_doc_cnt[label] 1 for w in words: self.class_word_cnt[label][w] 1 self.class_total_words[label] 1 return self._build_model() def _build_model(self): total_docs sum(self.class_doc_cnt.values()) model { prior: {k: v / total_docs for k, v in self.class_doc_cnt.items()}, cond: {}, class_total_words: dict(self.class_total_words) } for cls in self.class_word_cnt: word_num len(self.class_word_cnt[cls]) very_total self.class_total_words[cls] word_num # 平滑分母 model[cond][cls] { w: (c 1) / very_total for w, c in self.class_word_cnt[cls].items() } return model if __name__ __main__: trainer NaiveBayesTrainer(corpus) model trainer.train() with open(nb_model.json, w, encodingutf-8) as f: json.dump(model, f, ensure_asciiFalse)这段代码里的核心是_build_model中的very_total。分子加 1分母加上该类别所有词数量再加上词表大小这就是拉普拉斯平滑。注意这里用的是类别内词表大小而不是全部词汇表因为每个类别的词表长度可能差很多。如果改成全局词表低频类别的所有条件概率都会被压得更低新闻分类中“其他”类本来就稀疏会进一步被压制。训练完成后输出的nb_model.json里包含三个字段prior是类别先验cond是每个类别的条件概率class_total_words保留各类别总词数。后续分类器直接加载这个 JSON 文件不需要再回到原始语料。3.3 模型字段与训练效果之间的关系为了调参时方便对照我整理了训练输出字段的含义和影响模型字段含义对新闻分类的影响prior该类新闻占总语料的比例语料不均衡时直接影响打分结果cond[w]词 w 在该类中的条件概率高频主题词会把分数拉向对应类别class_total_words该类新闻的词总数用于归一化值越大条件概率越小平滑系数分子 1 / 分母词表大小值越大各类别概率越趋同prior是最容易被忽略的一个字段。如果财经类新闻是其他类的 5 倍那 P(财经) 本身就高分类器会偏向把不确定的文本判成财经。所以训练前我会先看每类新闻条数分布如果某类超过总语料的 40%就要考虑对多数类随机抽样而不是直接塞进训练集。4. 分类阶段bayesclassifier.py 的 Log 变换与判别边界4.1 概率连乘的下溢问题训练完成后bayesclassifier.py负责对待分类新闻做预测。朴素贝叶斯的打分公式是把一篇新闻里每个词的 P(wi|c) 全部乘起来一篇新闻少说也有几十个词每个概率都小于 1连乘几十次后浮点数会小到超出 Python 的 float 精度直接变成 0。真实环境中文本越长概率越小甚至所有类别的计算结果都是 0那就永远只能得到第一个类别。解决办法是取对数。乘号变加号log(P(wi|c))是负数连加再取最大值score(c) log(P(c)) Σ log(P(wi|c))取对数后虽然分数变成了负数但比较大小和原来乘法的结果排序完全一致。工程上这是朴素贝叶斯分类器的标配做法训练阶段也可以直接把条件概率先取 log 存到模型里省去预测时的重复对数运算。4.2 bayesclassifier.py 的在线预测代码# bayesclassifier.py 预测核心 import math import json class NewsClassifier: def __init__(self, model_pathnb_model.json): with open(model_path, encodingutf-8) as f: self.model json.load(f) self.vocab_size max(len(t) for t in self.model[cond].values()) def predict(self, tokens, alpha1.0): scores {} for cls, prior in self.model[prior].items(): cond self.model[cond][cls] total_words self.model[class_total_words][cls] log_prob math.log(prior) for w in tokens: cnt cond.get(w, 0) p (cnt alpha) / (total_words alpha * self.vocab_size) log_prob math.log(p) scores[cls] log_prob best max(scores, keylambda k: scores[k]) return best, scores if __name__ __main__: clf NewsClassifier() text 央行 下调 存款 准备金率 银行业 迎来 利好.split() label, scores clf.predict(text) print(预测类别:, label) print(各类得分:, scores)这段代码里使用cond.get(w, 0)对训练时没见过的词都按 0 处理。关键是在预测时也用了平滑系数alpha不是说训练时平滑过预测时就可以直接取cnt / total_words。一旦遇到一个新词cnt 为 0概率直接用 0 乘log 会直接抛数学异常。预测端的alpha和训练端的拉普拉斯平滑必须保持相同的分母结构这里alpha1.0与训练时完全一致。运行上面的例子预测结果通常是类别 1即财经。但如果某个词如“银行”在科技类新闻里也出现较多两个类别的分数差会很小这时就要看新闻里是否出现了“央行”“存款准备金率”这类强财经倾向的词。同一篇文本放的词数越多不确定性反而被稀释这就是朴素贝叶斯把每篇新闻当作词袋统计导致的一个天然弱点。4.3 分类得分与置信度输出工程上做新闻分类往往不止要一个类别标签还要知道模型有多少把握。朴素贝叶斯输出的 log 分数是负值不能直接当概率使用可以做一个简单的 softmax 变换来看置信度分布类别logP 累计得分softmax 置信度1 财经-32.50.612 科技-34.10.217 其他-36.80.06这个例子中财经类置信度明显占优可信度较高。但如果最高两个类别的置信度都只有 0.3 左右说明这两个类别的训练语料中存在相似用词或者新闻本身是跨领域的。遇到这种情况我在生产环境会单独走一条“低置信度转人工”的规则而不是强行输出类别。5. 调优朴素贝叶斯新闻分类的三件具体事5.1 “其他”类语料的比例调整项目里第 7 类是“其他”这是最难处理的一个类别因为它没有固定的主题词。凤凰网综合资讯里什么内容都有如果“其他”类语料太少分类器会把不属于前 6 类的新闻强行归入某一类如果语料太多又会让很多本来能判断主题的文本落到“其他”里。我建议训练前统计每个类别的文档数把“其他”控制在前 6 类平均数的 60% 左右宁可少一点因为它只负责兜底。做法是随机抽样时给定一个随机种子保证每次执行的可复现性。5.2 用混淆矩阵定位类别重叠用sklearn.metrics.confusion_matrix对训练集做 4:1 留出验证是最快的调优方式。把所有类别文档打乱后切分跑完分类器后看对角线的数字分布# 快速十折交叉验证片段 from sklearn.model_selection import StratifiedKFold skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for train_idx, test_idx in skf.split(texts, labels): # 每个类别独立训练、预测统计准确率 pass打印结果时重点看财经与科技、体育与娱乐这两组易混类别。如果交叉验证准确率在 0.85 以下优先怀疑训练文本里混杂了相似主题的新闻而不是急着调平滑系数。体育类新闻中“比赛”“夺冠”等词占比较高但如果娱乐新闻里大量出现“明星比赛”也会把两者搅在一起。5.3 误判样本反查与更新训练集每次模型上线后把预测置信度低于阈值的新闻落盘隔一段时间集中看误判文本。常见做法是每天抽取 200 条低置信度新闻人工打标后追加到对应编号的*_bayestraining.txt文件再重新运行训练和评估。注意追加时要保持数据格式一致一行一篇新闻词之间用空格分隔。模糊边界样本补充得越多分类器的拒绝能力越好而不是单纯提高正确率。真实项目里朴素贝叶斯调一次参数往往只提升零点几个百分点但把误判样本加回去重训一轮效果提升会更明显。本文还有配套的精品资源点击获取