
简介这份资源面向数据挖掘与自然语言处理入门者围绕朴素贝叶斯算法构建真假新闻分类模型帮助读者掌握从数据准备到模型评估的完整文本分类流程。压缩包共4个文件约27.2MB包含两个CSV数据集Fake.csv与True.csv分别存放假新闻与真新闻文本、一份HTML格式实验报告以及一份ipynb交互式代码文件覆盖数据清洗、停用词去除、词干提取、词袋与TF-IDF特征提取、模型训练及准确率、精确率、召回率、F1分数评估等环节。已有162人学习下载。读者可借助现成数据集与可运行代码快速复现朴素贝叶斯分类器理解条件概率计算与泛化能力调优思路并对照报告中的可视化结果与实验记录排查特征工程与参数设置中的常见问题为后续文本挖掘项目提供可复用的实践模板。1. 真假新闻分类模型从一条标题到一套可复现的朴素贝叶斯方案刷到一条“某地突发惊人事件”的推送点进去发现通篇没有时间、地点、信源只有情绪拉满的形容词——这种内容在信息流里越来越常见。真假新闻分类模型要解决的就是让机器自动判断一篇文本更接近真实报道还是虚假编造。这个方向属于典型的数据挖掘文本分类任务核心思路是把新闻正文转成词频特征再用朴素贝叶斯算法算出它属于哪一类的概率。它适合已经会一点 Python、想找一个完整数据集加代码练手的人也适合需要快速搭一个文本二分类基线的从业者。整套流程不依赖 GPU一台普通笔记本就能跑通数据集和代码打包在一起拿到就能复现。下面我按自己实际做这类项目的顺序把选型理由、特征工程、训练评估和踩过的坑一次讲清楚。2. 朴素贝叶斯凭什么适合新闻文本原理与选型理由2.1 从贝叶斯公式到“朴素”两个字的代价朴素贝叶斯分类器的根子是贝叶斯定理给定一篇文档的特征它属于某个类别的概率正比于该类别的先验概率乘以该类别下出现这些特征的概率。放到真假新闻场景里就是比较 P(真实|词1,词2,…) 和 P(虚假|词1,词2,…) 谁更大。“朴素”指的是一个强假设各个特征之间相互独立。放在文本里就是假设“股市”这个词出现与否和“暴跌”这个词出现与否没有关系。现实里这显然不成立但工程上这个假设带来的简化收益极大——只需要统计每个词在每个类别下的出现频率不用建模词与词之间的复杂依赖。文本分类这种高维稀疏场景下这个假设的偏差往往被数据量抵消效果经常出乎意料地稳。我一般会跟人说朴素贝叶斯不是最准的模型但它是最不容易翻车的基线。你拿它跑出来的准确率就是后续换 BERT、换 XGBoost 必须超过的那条线。2.2 三种变体怎么选多项式、伯努利、高斯scikit-learn 里常用的朴素贝叶斯有三个变体新闻文本分类基本只在多项式MultinomialNB和伯努利BernoulliNB之间选。变体特征形式适用场景新闻分类建议MultinomialNB词频 / TF-IDF 权重文档较长、词出现次数有意义首选长新闻正文用它BernoulliNB0/1 是否出现短文本、关键词命中更重要标题分类或短讯可用GaussianNB连续数值特征特征为连续值不适用于原始文本词频新闻正文通常几百到上千字一个词出现多次是有信息量的所以多项式朴素贝叶斯是默认选择。伯努利变体把“出现 3 次”和“出现 1 次”一视同仁会丢掉这部分信息但它对短文本的噪声更鲁棒。我的做法是两个都跑一遍看验证集上的表现再定。2.3 先验平滑为什么必须设 alpha如果某个词在训练集的“真实”类里从没出现过它的条件概率就是 0。由于朴素贝叶斯是连乘一个 0 会让整篇文档属于该类的概率直接归零——这就是零概率问题。拉普拉斯平滑给每个词的计数加一个常数 alpha保证概率不为零。alpha 默认是 1.0相当于假设每个词在每类里都至少出现过一次。数据量小的时候可以调小到 0.1 甚至 0.01让真实统计占更大权重数据量大、词汇表很长时alpha 太小反而容易过拟合。这个参数是后面调参环节第一个要动的。2.4 最小可跑通流程读数据、切分、向量化、训练在讲完整工程之前先给一个能直接跑的最小版本确认环境和数据没问题。假设数据集里是两个文件夹fake 和 real各放对应类别的 txt 文件。import os import jieba from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # 1. 读取数据遍历两个类别文件夹收集文本和标签 def load_data(root): texts, labels [], [] for label, folder in enumerate([fake, real]): folder_path os.path.join(root, folder) for fname in os.listdir(folder_path): with open(os.path.join(folder_path, fname), r, encodingutf-8) as f: texts.append(f.read()) labels.append(label) return texts, labels texts, labels load_data(./news_data) # 2. 中文分词jieba 精确模式空格连接成向量化输入 def cut(text): return .join(jieba.cut(text)) corpus [cut(t) for t in texts] # 3. 划分训练集和测试集stratify 保证两类比例一致 X_train, X_test, y_train, y_test train_test_split( corpus, labels, test_size0.2, random_state42, stratifylabels ) # 4. TF-IDF 向量化限制最大特征数过滤太罕见的词 vec TfidfVectorizer(max_features20000, ngram_range(1, 2)) X_train_vec vec.fit_transform(X_train) X_test_vec vec.transform(X_test) # 5. 训练多项式朴素贝叶斯并评估 clf MultinomialNB(alpha0.1) clf.fit(X_train_vec, y_train) pred clf.predict(X_test_vec) print(classification_report(y_test, pred, target_names[fake, real]))这段代码的逻辑链条是先按文件夹名给数据打标签再用 jieba 把中文切成空格分隔的词序列接着用 TF-IDF 把文本转成数值矩阵最后训练和评估。几个关键参数说明一下。max_features20000是只保留词频最高的两万个词或词组防止特征维度爆炸ngram_range(1,2)表示同时考虑单个词和相邻两个词的组合能捕捉“股市 暴跌”这类搭配alpha0.1是平滑系数比默认值小适合数据量中等的情况stratifylabels保证切分后训练集和测试集里真假两类比例一致避免某一类样本过少导致评估失真。跑通这一步你会得到一份包含 precision、recall、f1-score 的报告。如果 f1 在 0.85 以上说明数据质量不错如果低于 0.7先别急着换模型大概率是数据或分词的问题后面避坑章节会展开。3. 把数据集和代码跑起来从原始文本到特征矩阵3.1 数据集结构确认与标签映射拿到一个 .rar 压缩包第一步不是急着解压跑代码而是先看清楚目录结构。常见的真假新闻数据集有两种组织方式一种是 fake 和 real 两个文件夹文件名就是编号另一种是一个 CSV里面有 text 列和 label 列。两种处理方式不同先确认再动手。如果是文件夹形式用上一节的load_data就行。如果是 CSV读取和标签映射要改成这样import pandas as pd # 读取 CSV假设列名为 text 和 label df pd.read_csv(./news.csv) # 查看标签分布确认没有拼写不一致的情况 print(df[label].value_counts()) # 统一标签把各种写法映射成 0 和 1 label_map {fake: 0, FAKE: 0, false: 0, real: 1, REAL: 1, true: 1} df[label] df[label].map(label_map) # 检查是否有映射失败的样本 print(未映射样本数:, df[label].isna().sum()) df df.dropna(subset[label])这里有个血泪经验标签列里出现 “Fake”“fake”“FAKE” 三种写法是常有的事如果不统一value_counts()会显示成三个类别模型直接学崩。map之后一定要检查isna().sum()确认没有漏网之鱼。另外文本列如果有大量空值或极短文本也要在这一步过滤掉长度小于 10 个字符的样本基本没有分类价值。3.2 中文分词与停用词处理中文不像英文有天然空格必须分词。jieba 是最常用的选择但默认分词会把“的”“了”“是”这类高频虚词也切出来它们对分类没有区分度反而增加噪声。停用词表的作用就是过滤掉这些词。import jieba # 加载停用词表一行一个词 def load_stopwords(path): with open(path, r, encodingutf-8) as f: return set(line.strip() for line in f) stopwords load_stopwords(./stopwords.txt) def cut_with_stopwords(text): words jieba.cut(text) # 过滤停用词和单字单字信息量通常不足 return .join(w for w in words if w not in stopwords and len(w) 1) corpus [cut_with_stopwords(t) for t in texts]停用词表不需要自己从头写网上有现成的中文停用词表几百到一千多个词就够用。这里额外加了len(w) 1的条件把单个汉字也过滤掉。原因是单字在新闻文本里歧义太大“行”可以是银行也可以是行为保留它反而干扰模型。但要注意如果数据集中有大量专有名词是单字加前缀的情况这个条件可能误伤需要根据实际词频观察调整。分词之后建议随机抽几条打印出来看一眼确认没有出现整段没切开或者切得乱七八糟的情况。jieba 对新闻类文本的通用分词效果通常没问题但如果数据里有大量网络新词或专业术语可以加载自定义词典。3.3 TF-IDF 向量化的参数怎么定分词完成后文本还是字符串必须转成数值矩阵才能喂给模型。TF-IDF 是最常用的文本向量化方法它给每个词算一个权重词在本文档中出现越多权重越高但在所有文档中出现越普遍权重越低。这样“的”“是”这种到处都有的词权重自然被压低而“辟谣”“核实”这种有区分度的词权重就高。from sklearn.feature_extraction.text import TfidfVectorizer vec TfidfVectorizer( max_features30000, # 保留权重最高的 3 万个特征 ngram_range(1, 2), # 一元词和二元词组 min_df3, # 至少在 3 篇文档中出现才保留 max_df0.9, # 在超过 90% 文档中出现的词丢弃 sublinear_tfTrue # 用 1log(tf) 替代原始词频 ) X vec.fit_transform(corpus) print(特征矩阵形状:, X.shape)逐个说参数。max_features30000控制特征上限太大训练慢且容易过拟合太小会丢掉有用信息三万在几万条新闻的数据集上是个稳妥值。min_df3过滤掉只出现一两次的罕见词这些词多半是噪声或拼写错误。max_df0.9过滤掉在 90% 以上文档都出现的词相当于自动停用词过滤和前面的停用词表形成双保险。sublinear_tfTrue把词频取对数避免某个词在一篇长文里重复几十次导致权重畸高对长短不一的新闻正文特别有用。向量化之后X.shape会告诉你矩阵有多少行多少列。行数应该等于样本数列数就是实际保留的特征数。如果列数远小于max_features说明min_df和max_df过滤掉了大量词这是正常的。3.4 训练、预测与评估指标解读特征矩阵准备好之后训练本身只有两行代码重点在评估。新闻分类是不平衡场景时比如真实新闻远多于虚假新闻单看准确率会被多数类带偏必须看 precision、recall 和 f1。from sklearn.naive_bayes import MultinomialNB from sklearn.metrics import classification_report, confusion_matrix clf MultinomialNB(alpha0.1) clf.fit(X_train_vec, y_train) pred clf.predict(X_test_vec) # 混淆矩阵看清哪类被错分得多 print(confusion_matrix(y_test, pred)) print(classification_report(y_test, pred, target_names[fake, real]))混淆矩阵的四个格子分别代表真负、假正、假负、真正。如果假负把真实新闻判成虚假特别多说明模型对真实类的 recall 低可能是真实类样本太少或者特征区分度不够。classification_report 里的 f1-score 是 precision 和 recall 的调和平均是判断模型整体好坏最直接的指标。我一般要求两个类别的 f1 都不低于 0.8如果某一类明显偏低就要回头查数据分布和特征。提示评估一定要在测试集上做不要用训练集的结果下结论。训练集准确率 0.99 测试集 0.7 是过拟合的典型信号。4. 避坑与排查真假新闻分类里最容易翻车的五件事4.1 现象测试集准确率 0.95换一批新闻就崩原因训练集和测试集来自同一批数据切分时没有考虑时间或来源分布。如果数据集里真实新闻都来自同一家媒体、虚假新闻都来自同一个网站模型学到的可能是“这家媒体的用词风格”而不是“真假新闻的语言差异”。解决切分时按来源或时间做分组切分确保测试集里的新闻来源在训练集中没出现过。如果数据没有来源字段至少用train_test_split的random_state固定切分并人工检查两边样本是否同质。更严格的做法是留出一整个时间段的数据做测试。4.2 现象模型把带“震惊”“速看”的新闻全判成假原因训练集里虚假新闻大量使用这类标题党词汇模型学到了这个捷径。但真实新闻也可能用“震惊”做标题模型就误判了。解决检查特征权重最高的词如果前几十个全是情绪化词汇说明特征太单一。可以增加 ngram 范围让“震惊 但 核实”这类组合也进入特征或者在数据层面补充不同风格的样本让模型看到“震惊”不必然等于假。另外max_df设低一点把过于普遍的情绪词过滤掉。4.3 现象训练时报错 “empty vocabulary”原因分词后所有词都被停用词表或min_df过滤掉了导致向量化时没有可用特征。常见于数据量很小或者停用词表过大。解决先打印分词结果确认不是空字符串。然后临时把min_df设为 1、停用词表设为空看是否能跑通。如果能再逐步加回过滤条件找到临界点。数据量少于 100 条时min_df不要超过 1。4.4 现象alpha 调小后训练集准确率上升但测试集下降原因alpha 太小平滑不足模型对训练集里的噪声词过度敏感过拟合了。解决alpha 的调参范围建议在 0.01 到 1.0 之间用交叉验证选。不要只看训练集要用 5 折交叉验证在训练集内部评估。如果数据量在几千条以上alpha 取 0.1 到 0.5 通常比较稳数据量几百条时alpha 反而要调大一点比如 0.5 到 1.0用更强的平滑对抗小样本噪声。4.5 现象预测新文本时结果全是同一类原因新文本的分词结果里包含大量训练集没见过的词向量化后特征全为 0模型只能按先验概率输出多数类。解决确保预测时用的是同一个TfidfVectorizer实例不能重新 fit。正确做法是训练时fit_transform预测时只transform。另外如果新文本领域和训练集差异大可以在训练集里加入少量目标领域的样本或者用vec.get_feature_names_out()检查新文本命中了哪些特征确认不是全零向量。5. 让模型更稳的几个进阶技巧从基线到可用5.1 用交叉验证选 alpha 和特征数单次切分的评估有随机性交叉验证能给出更稳的估计。下面这段代码同时搜索 alpha 和 max_features 两个参数用 5 折交叉验证的 f1 作为选择依据。from sklearn.model_selection import GridSearchCV from sklearn.pipeline import Pipeline # 把向量化和分类器串成管道避免数据泄漏 pipe Pipeline([ (tfidf, TfidfVectorizer(ngram_range(1, 2), sublinear_tfTrue)), (nb, MultinomialNB()) ]) # 参数网格注意管道里参数的写法是 步骤名__参数名 param_grid { tfidf__max_features: [10000, 20000, 30000], nb__alpha: [0.01, 0.1, 0.5, 1.0] } grid GridSearchCV(pipe, param_grid, cv5, scoringf1, n_jobs-1) grid.fit(corpus, labels) print(最佳参数:, grid.best_params_) print(最佳交叉验证 f1:, grid.best_score_)用 Pipeline 的好处是向量化只在训练折上 fit避免测试折的信息泄漏到训练中。n_jobs-1用满所有 CPU 核加速。scoringf1在不平衡数据上比准确率更可靠。跑完会输出最佳参数组合直接拿来用就行。如果最佳 alpha 落在网格边界上比如 0.01说明还可以往更小方向再搜一轮。5.2 补充关键词特征提升区分度纯词频特征有时候抓不住“有没有引用具体信源”这类结构信息。可以手工构造几个特征拼进去文本长度、是否包含数字、是否包含引号、是否包含“记者”“报道”“核实”等信源词。这些特征和 TF-IDF 矩阵拼接后一起训练。import numpy as np from scipy.sparse import hstack def extra_features(texts): feats [] for t in texts: feats.append([ len(t), # 文本长度 sum(c.isdigit() for c in t), # 数字个数 t.count(“) t.count(”), # 引号数量 int(记者 in t or 报道 in t), # 是否含信源词 ]) return np.array(feats) # 对训练集和测试集分别提取再与 TF-IDF 矩阵横向拼接 extra_train extra_features(X_train) extra_test extra_features(X_test) X_train_final hstack([X_train_vec, extra_train]).tocsr() X_test_final hstack([X_test_vec, extra_test]).tocsr() clf MultinomialNB(alpha0.1) clf.fit(X_train_final, y_train) print(classification_report(y_test, clf.predict(X_test_final), target_names[fake, real]))注意hstack要求两边行数一致且稀疏矩阵要用tocsr()转成压缩格式。额外特征的值域和 TF-IDF 权重差异很大如果发现加了之后效果变差可以对额外特征做归一化或者给它们乘一个小于 1 的权重避免盖过文本特征。5.3 错误样本分析从混淆矩阵找到改进方向模型跑出来之后别只看一个总分。把预测错误的样本导出来人工读十几条往往能发现系统性问题。下面这段代码把测试集里判错的样本和真实标签、预测标签一起输出。# 找出预测错误的索引 errors np.where(pred ! y_test)[0] for i in errors[:10]: print(真实:, real if y_test[i] 1 else fake, 预测:, real if pred[i] 1 else fake) print(X_test[i][:200]) # 打印原文前 200 字 print(- * 50)我自己的习惯是每次模型迭代后都抽十条错误样本读一遍。常见发现包括某些虚假新闻写得非常像正规报道用词克制、有具体日期模型很难区分或者某些真实新闻因为翻译腔被误判。这些发现会直接指导下一步是补数据、加特征还是换模型。如果错误样本集中在某个主题上说明训练集覆盖不够需要针对性补充。5.4 保存模型与预测新文本的完整流程训练好的模型要能复用不能每次预测都重新训练。用 joblib 把向量化器和分类器一起保存预测时加载。import joblib # 保存 joblib.dump(vec, tfidf_vectorizer.pkl) joblib.dump(clf, nb_model.pkl) # 加载并预测新文本 vec_loaded joblib.load(tfidf_vectorizer.pkl) clf_loaded joblib.load(nb_model.pkl) def predict_news(text): cut_text cut_with_stopwords(text) vec_text vec_loaded.transform([cut_text]) pred clf_loaded.predict(vec_text)[0] proba clf_loaded.predict_proba(vec_text)[0] return (真实 if pred 1 else 虚假), max(proba) label, confidence predict_news(据新华社报道今日上午相关部门召开新闻发布会……) print(f预测: {label}, 置信度: {confidence:.2f})这里的关键是transform而不是fit_transform用训练时保存的向量化器保证特征空间一致。predict_proba返回两个类别的概率取最大值作为置信度。置信度低于 0.6 的样本建议人工复核不要直接采信。这套保存加载流程是让模型从实验走向可用的最后一步也是很多人跑完 notebook 就忘掉的一步。我自己的习惯是每做完一个文本分类项目都会把基线模型和向量化器存下来后面换模型时拿它做对比。朴素贝叶斯可能不是最终上线的模型但它是最可靠的参照物。希望帮到你。本文还有配套的精品资源点击获取