
简介一套基于机器学习和深度学习的虚假新闻检测源码包面向高校期末大作业与毕业设计场景融合了传统机器学习、深度学习和BERT三种技术路线帮助读者完成新闻文本分类任务适合有一定Python基础的学生直接参考或二次开发。资源共76个文件以45个Python脚本为核心承载数据处理、模型训练与评估逻辑另含14个XML配置文件、1个Word手册、1个Jupyter Notebook示例及若干IDE辅助文件压缩包仅263KB结构紧凑。目前已有162人浏览学习项目代码注释细致部署门槛低按文档调整路径即可运行。内容覆盖机器学习与深度学习两大模块机器学习部分提供多种对比算法深度学习部分包含BERT训练与评估流程并附有说明文档方便理解实现细节与调参思路。对于需要完成课程设计或期末项目并冲刺高分的读者可直接将项目作为一套文档完整、运行稳定的参考实现。1. 虚假新闻检测到底在检测什么数据集、标签体系和三条技术路线做课程设计最怕的不是代码写不出来而是代码跑完不知道该怎么写文档、答辩时说不清“你做的这件事到底难在哪”。这套基于机器学习、深度学习和 BERT 的虚假新闻检测项目恰好把这个问题拆成了三段叠进先用 TF-IDF 加传统分类器跑一个基线再用 TextCNN / BiLSTM 做深度模型最后用 BERT 微调刷上限。同一份数据、同一个评估口径三种方法的结果摆在一起训练开销和效果差异就都成了文档里的现成素材。项目正文里配的源码和文档说明覆盖了从数据清洗到实验对比的完整链路适合正在做 Python 期末大作业、机器学习课程设计的人也适合想系统性入门文本分类但不想从零攒代码的读者。核心就一句话帮你快速产出一套能复现、能解释、能答辩的完整实验而不是只丢给你一个跑不出数字的孤立模型。2. 数据先行清洗、分词与训练/验证/测试集划分的落地细节2.1 一份可用的虚假新闻数据集长什么样字段、标签和规模先明确一件事这类项目的数据集不是像 CIFAR-10 那样现成标好的图像而是散落在 CSV 或 JSON 里的新闻文本。常见的公开数据集通常包含几个字段title 是新闻标题text 是正文label 是真实或虚假的判断。规模一般在几千到几万条之间课程设计这个量级正好合适——太少模型学不出区分度太多训练时间扛不住。拿到源码包之后第一步别急着跑模型先看数据长什么样。我的习惯是先用 pandas 读进来打印 shape 和前几行确认 label 的取值分布。这里有个很容易被忽略的点真实新闻和虚假新闻的比例往往不均衡虚假类经常只占三成左右。如果后面直接按默认的 train_test_split 切分极端情况下某个类可能在训练集里只剩几百条模型很容易“学歪”。所以标签分布的检查要在预处理开头就做掉而不是等模型跑完之后再回头找原因。2.2 预处理去 URL、去 HTML 标签与标签二值化新闻文本的预处理比一般文本要脏得多。标题和正文里会夹着超链接、HTML 残留标签、emoji 和大量标点。这些噪声对传统机器学习影响很大因为 TF-IDF 会把 URL 里的字符当成正常词白白占用特征空间。下面这段是我常用的清洗流程import pandas as pd import re df pd.read_csv(news.csv, usecols[title, text, label]) df[label] (df[label] FAKE).astype(int) # 虚假记为1真实记为0 def clean_text(s): s re.sub(rhttp\S, , s) # 去掉URL s re.sub(r[^], , s) # 去掉HTML标签 s re.sub(r[^\w\s], , s) # 去掉标点符号 s s.lower() return s.strip() df[clean] df[title].fillna() df[text].fillna() df[clean] df[clean].map(clean_text) print(df[label].value_counts())这段代码的逻辑是先做标签二值化再把标题和正文拼接成一整条文本。fillna()的作用是防止标题或正文为空时直接把整条记录变成 NaN这在新闻数据里很常见。清洗顺序有个讲究先去 URL 和 HTML再去标点。如果顺序反过来URL 里的等号和斜杠会先把标点正则“消费”掉剩下的英文单词残片仍然留在文本里。有一点要特别注意这段字符级清洗只服务于 TF-IDF 和深度学习模型的词表构建。BERT 有自己独立的 tokenizer它会把单词切成子词标点也会被特殊处理所以跑 BERT 分支时不需要走这套清洗直接喂原始文本即可。源码包里通常会把两条预处理管线分开写别混用。2.3 划分数据集分层抽样与随机种子固定数据划分是整个实验能复现的根基。我给课程设计定的标准做法是 6:2:2 的比例拆成训练集、验证集、测试集训练集用来拟合参数验证集用来调超参和决定早停测试集只在最后评估一次。中间任何一次“我看测试集指标不好调个参再跑”的操作都会让测试集失去公正性。from sklearn.model_selection import train_test_split X df[clean] y df[label] X_train, X_temp, y_train, y_temp train_test_split( X, y, test_size0.3, random_state42, stratifyy) X_val, X_test, y_val, y_test train_test_split( X_temp, y_temp, test_size0.5, random_state42, stratifyy_temp)这里两个关键参数值得在文档里大写加粗stratifyy表示按原始标签比例分层抽样保证切出来的训练集和测试集里真实/虚假的比例跟全量数据一致random_state42让每次运行得到完全相同的划分。答辩时被问到“为什么我跑出来跟你不一样”八成就是随机种子没固定。还有一个顺序问题我踩过坑一定要先划分数据集再做特征工程比如 fit TF-IDF 向量化器。如果先做特征再划分TF-IDF 的词表统计就把测试集的信息泄露进训练过程了测试指标会虚高答辩时被追问很容易露馅。这个顺序问题在文档里也要写清楚老师看到你能主动提数据泄漏印象分会好很多。3. 传统机器学习基线TF-IDF 与多模型对比的参数和调优3.1 为什么先跑传统机器学习基线不可跳过课程设计里最常见的一个错误是拿到数据直接上 BERT文档里除了“效果很好”什么都写不出来。传统机器学习的作用不是跟 BERT 争高低而是提供一个可解释的基线。TF-IDF 的核心思想其实很朴素一个词在单篇新闻里出现得越多越重要但在整个语料库里出现得越频繁越没区分度。比如“the”“的”“了”这类词几乎每篇都有TF-IDF 会把它们的权重压到很低而“阴谋”“辟谣”“目击”这类词只在部分新闻里出现权重就会很高。这个性质让它成为虚假新闻检测的天然适配器——虚假新闻往往在措辞上有明显的模式比如情绪化词汇和绝对化表达。而逻辑回归这类线性模型给出的权重可以直接对应到每个词上打印出权重最高的 Top 20 词就能在文档里写“虚假新闻的高频特征词是哪些”。这种可解释性是深度学习模型很难给你的。3.2 TF-IDF 特征工程max_df、min_df、ngram_range 怎么设from sklearn.feature_extraction.text import TfidfVectorizer tfidf TfidfVectorizer( lowercaseTrue, max_df0.8, # 超过80%文档都出现的词视为停用词 min_df5, # 少于5篇文档出现的词视为噪声 max_features20000, # 词表上限防止向量爆炸 sublinear_tfTrue, # 词频用log(1tf)弱化长文本偏置 ngram_range(1, 2) # 保留单个词和相邻双词组合 ) X_train_tf tfidf.fit_transform(X_train) X_val_tf tfidf.transform(X_val) X_test_tf tfidf.transform(X_test)max_df0.8和min_df5是一对配合使用的参数前者删掉全语料高频词后者删掉只在极少数文档里出现的生僻词。实操中max_df的取值范围在 0.7~0.9min_df在 2~10 之间数据量小就取小值。max_features20000是给词表规模兜底防止内存被撑爆。ngram_range(1, 2)保留了“虚假新闻”这种双词组合因为很多判别信息藏在短语里而不是单个词里代价是特征维度明显变大。sublinear_tfTrue是很多教程不会提但实际很有用的参数。它把词频从线性计数换成对数计数防止一篇特别长的事件报道因为某个词反复出现而获得不公平的权重。这套参数基本是个万能起点课程设计的数据量级下不需要做网格搜索直接跑就行。3.3 三模型对比逻辑回归、朴素贝叶斯与线性 SVM传统机器学习阶段我一般同时跑三个模型不是为了炫技而是为了让文档里的对比表有内容。逻辑回归适合做可解释分析朴素贝叶斯在短文本上经常有意外的好表现线性 SVM 在稀疏文本特征上通常精度最高。三者配合classification_report一起输出验证集上的 precision、recall、F1 一次看全。from sklearn.linear_model import LogisticRegression from sklearn.naive_bayes import MultinomialNB from sklearn.svm import LinearSVC from sklearn.metrics import classification_report models { lr: LogisticRegression(max_iter1000, C1.0), nb: MultinomialNB(alpha0.1), svm: LinearSVC(C1.0, max_iter2000) } for name, model in models.items(): model.fit(X_train_tf, y_train) pred model.predict(X_val_tf) print(f--- {name} ---) print(classification_report(y_val, pred, target_names[real, fake]))逻辑回归有个参数max_iter很容易踩坑默认 100 在 TF-IDF 特征上经常不收敛直接改成 1000 能省掉一个莫名其妙的报警。C是正则化强度的倒数C 越小正则越强。文本稀疏特征的量级很小C 在 0.3~2 之间微调即可C1.0 作为默认值不会出大问题。LinearSVC 在上面的代码里只能输出硬分类如果要预测概率需要包一层CalibratedClassifierCV但课程设计阶段直接用决策边界做评估问题不大。跑完之后把三个模型的验证指标整理成一张表就是我常说的基线对比表模型AccuracyPrecisionRecallF1训练耗时秒LogisticRegression0.9120.8840.8980.8913.2MultinomialNB0.8970.8620.8790.8701.1LinearSVC0.9190.8950.9060.9005.8表格里的数字我建议跑完真实数据之后自己填不要直接抄任何参考值因为不同数据集差异很大。线性 SVM 通常比逻辑回归高 1 到 2 个点但速度更慢这个结论在大多数新闻数据集上都成立可以作为文档里“选型理由”的素材。4. 深度学习路线TextCNN 与 BiLSTM 的构建、训练与观察点4.1 结构选型为什么是 TextCNN 和 BiLSTM文本分类的深度模型里TextCNN 和 BiLSTM 是最典型的两条路线也是课程设计里最能体现“我理解原理”的组合。TextCNN 的核心是用多个不同宽度的卷积核去抓局部 n-gram 模式比如宽度为 3 的卷积核对应抓连续 3 个词的组合信号宽度为 5 的抓 5 个词。它训练快、参数少、在小数据集上不容易过拟合缺点是感受野有限抓不到相距很远的词之间的关联。BiLSTM 的思路则完全不同它按顺序读完整条新闻通过门控机制把前文信息一路传递下去再用反向序列再读一遍。双向的设计让每个位置的输出同时包含上文和下文的信息对“前文提到某个事件、后文出现质疑”这种长距离依赖更敏感。缺点也很明显训练速度比 TextCNN 慢一个量级小数据下更容易过拟合。文档里把这俩模型一对比理论深度就出来了。4.2 TextCNN 实战卷积核宽度与池化策略下面的代码用 PyTorch 实现一个三通道 TextCNN卷积核宽度分别取 3、4、5每个宽度 128 个卷积核import torch import torch.nn as nn import torch.nn.functional as F class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim128, num_filters128): super().__init__() self.embed nn.Embedding(vocab_size, embed_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv1d(embed_dim, num_filters, kernel_sizek) for k in (3, 4, 5) ]) self.dropout nn.Dropout(0.3) self.fc nn.Linear(num_filters * 3, 2) def forward(self, x): x self.embed(x).permute(0, 2, 1) pooled [] for conv in self.convs: c F.relu(conv(x)) p F.max_pool1d(c, c.size(2)) pooled.append(p.squeeze(2)) out torch.cat(pooled, dim1) out self.dropout(out) return self.fc(out)nn.Embedding在这段代码里就是一个可训练的查表操作每个词的 token id 对应一行向量训练过程中向量会围绕“语义相近的词”慢慢聚拢。padding_idx0标记 padding 位置的向量恒为 0不参与梯度更新避免填充符引入噪声。permute(0, 2, 1)这步容易绕晕原因是 Conv1d 期望的输入形状是(batch, channels, length)而 Embedding 输出是(batch, length, embed_dim)必须交换后两维。F.max_pool1d是 TextCNN 的设计精髓对卷出来的每个特征图只保留最大响应值。这样做的效果是不管这条新闻是 80 个词还是 200 个词卷积之后每个 kernel 的输出都被压成一个标量全连接层的输入维度就固定了。训练时用一个pad_sequence把 batch 内所有样本补齐到相同长度即可常见配置是 100 或 128不要设太大。4.3 BiLSTM 实战隐藏层维度与过拟合调节class BiLSTM(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_dim64): super().__init__() self.embed nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_dim, num_layers2, batch_firstTrue, bidirectionalTrue) self.dropout nn.Dropout(0.3) self.fc nn.Linear(hidden_dim * 2, 2) def forward(self, x): x self.embed(x) out, _ self.lstm(x) out out[:, -1, :] return self.fc(self.dropout(out))hidden_dim64表示每个方向的隐藏状态维度是 64双向之后拼接成 128 维所以全连接层的输入维度是hidden_dim * 2。out[:, -1, :]取最后一个时间步的隐藏状态在双向结构里这个位置同时聚合了正序的结尾信息和逆序的开头信息是整条新闻的一个紧凑摘要。num_layers2让 LSTM 堆两层理论上能建模更复杂的语义层次但小数据集上两层很容易过拟合。我的判断标准是如果验证集准确率从第 6 个 epoch 开始停滞训练集却还在涨优先把num_layers降回 1。训练超参方面两个模型的通用配置是Optimizer 用 Adam学习率 1e-3batch size 64最大序列长度 128Padding 用batch_firstTrue跑 15~20 个 epoch 加早停。学习率是这些参数里最敏感的1e-2 基本会直接震荡1e-4 则收敛太慢。textcnn 通常 8~10 个 epoch 就能到最佳点BiLSTM 要慢一些这是正常的不是代码出了 bug。4.4 训练循环和观察点Loss 不降先看哪里训练循环本身的代码各家实现差异不大但观察点值得单独说。每个 epoch 打印训练 loss、训练准确率、验证准确率三个数字比只打印一个总 loss 有用得多。如果训练 loss 稳定下降而验证准确率不动是过拟合的前兆如果 loss 震荡上蹿下跳先看学习率是不是太高再看 batch 是不是太小——batch 太小会导致梯度估计噪声过大32 和 64 的差距在这种小项目里就能体现出来。词向量的初始化也是个可选优化点。课程设计要求不高时随机初始化完全够用。想要效果更好可以用预训练 word2vec 或 GloVe 向量初始化 Embedding 层但这时候要把padding_idx0对应位置手动置零否则 padding 部分会带上随机向量参与池化稍微污染结果。这个细节很多人忽略也是答辩时能拿出来说的加分项。5. BERT 微调常见问题权重加载、序列长度与过拟合排查5.1 加载预训练模型tokenizer 必须和模型版本配对BERT 分支是整个项目里效果最好的部分也是坑最多的地方。源码包里的 BERT 代码通常会基于 HuggingFace Transformers 库下面这段是标准写法from transformers import BertTokenizer, BertForSequenceClassification model_name bert-base-uncased tokenizer BertTokenizer.from_pretrained(model_name) model BertForSequenceClassification.from_pretrained( model_name, num_labels2 )BertForSequenceClassification是一个封装好的分类模型内部结构是 BERT 主干加一个全连接分类头。第一次运行会自动把预训练权重下载到本机缓存目录。如果是在离线环境或内网做实验需要先在能联网的机器上把权重下载完整然后把整个缓存目录拷到目标机器上model_name改成指向本地路径即可路径写法是./models/bert-base-uncased这样的相对目录。一个更常见也更隐蔽的坑是语言版本不匹配。英文新闻要用bert-base-uncased中文新闻标题和数据必须换成bert-base-chinese。混用最直接的症状是分词后大量输出[UNK]等于模型看到的是一堆未知符号效果自然崩到跟随机猜差不多。检查方法很简单把 tokenizer 处理后的 input_ids 打印前几条如果[UNK]占比超过 1%基本可以断定用错了预训练模型。5.2 现象排查一训练 Loss 不降或震荡学习率的锅BERT 微调和前面深度学习模型最大的区别就是学习率完全不是一个量级。用 TextCNN 那套 1e-3 的学习率去微调 BERT训练 loss 大概率会在前几个 step 直接冲高震荡看起来像模型在“抽搐”。原因是 BERT 预训练权重已经是收敛状态太大的学习率等于在最优解附近横冲直撞把已经学好的参数一步就顶飞了。正确做法是把学习率设在 2e-5 到 5e-5 之间优化器用AdamW配合线性 warmup。warmup 的意思是在前几百个 step 让学习率从很小的值线性升到目标值避免模型一上来就被大步长撞碎。源码包里如果有这组参数直接沿用如果没有在训练代码里补上这样一段from transformers import AdamW, get_linear_schedule_with_warmup optimizer AdamW(model.parameters(), lr2e-5) scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_steps200, num_training_stepslen(train_loader) * epochs )num_warmup_steps我一般取总训练步数的 5% 到 10%课程设计场景下固定 200 步是一个不容易出错的保守值。判断学习率是否合适的标准很简单前两个 epoch 训练 loss 应该在震荡中缓慢下降而不是一路上冲。BERT 在新闻文本分类这种任务上收敛通常很快3~5 个 epoch 就能见分晓不需要像传统深度学习那样跑十几个 epoch。5.3 现象排查二显存溢出或训练极慢先砍序列长度把新闻文本直接塞给 BERT 遇到的第一个现实问题就是显存。BERT 的注意力机制计算复杂度跟序列长度的平方成正比默认最大长度 512 的配置在课程设计的机器上很容易 OOM。网上常见的解决方法是改 batch size但更立竿见影的是压缩序列长度。新闻标题加正文的完整长度经常超过 256 个 token但实际实验里截断到 96 或 128 通常对精度影响很小。原因是在“标题正文开头”这个范围内判别信息已经足够充分。下面的 tokenizer 配置是项目的常见做法train_encodings tokenizer( list(X_train), truncationTrue, paddingTrue, max_length128, return_tensorspt )truncationTrue自动截断超长序列paddingTrue把同一批次内的所有样本补齐到该 batch 中最长样本的长度。max_length128是课程设计下显存和精度的平衡点。如果 128 仍然 OOM先降 batch size 到 16再考虑降到 96。这套顺序比一味调模型结构要实际得多。5.4 现象排查三训练准确率高但验证分数上不去这是所有分类项目都会遇到的经典过拟合症状BERT 微调尤其明显因为 BERT 参数量大而新闻数据集通常只有几千到一两万条。表现是训练集准确率能到 98%验证集只有 80% 出头且验证指标从第 3 个 epoch 就开始停滞。这时不要盲目加数据或换模型先按这个顺序排查一遍。第一检查验证集和训练集的文本分布是否一致。如果数据按时间顺序排列先划分再打乱或先打乱再划分会得到不同的结果时间上靠后的新闻可能有新词和新事件导致验证集偏难。第二检查 dropout 是否被关掉。HuggingFace 的模型默认有 dropout但如果你手动加载了检查点并在评估时设了model.eval()这个一般不会出问题反而有些从零改写的代码会忘了在训练时开启 dropout导致模型学到的全是表层模式。第三接受一个事实新闻真假判断本身就存在模糊边界有些新闻连人都难以判断F1 在 0.85 以上已经是一个拿得出手的成绩不必死磕到 0.95那是模型在记忆训练集而不是理解语义。5.5 一份不会让答辩老师挑出毛病的实验配置表跑完所有模型之后把下面的表格填满放进文档是我见过的课程设计里性价比最高的一个动作。它同时交代了实验环境、超参数和最终指标答辩时老师的问题基本都会从这里面出模型学习率Batch Size序列长度验证 F1训练耗时TF-IDF LR———0.891约3秒TextCNN1e-3641280.923约2分钟BiLSTM1e-3641280.918约8分钟BERT2e-5321280.948约25分钟表格中的耗时按个人 GPU 或 CPU 条件填写填真实数据就行性能差的机器上 BERT 跑 40 分钟也正常这本身也是讨论“模型成本”的素材。填准确率和 F1 时必须用验证集数据测试集指标留到最终评估那一章再写这是实验规范。6. 验证方法混淆矩阵、误判样本分析与部署前检查6.1 用 sklearn 四件套生成评估指标与混淆矩阵模型训练完不能只报一个准确率就收工。虚假新闻检测这种二分类任务里准确率会被类别不均衡掩盖如果数据集里 70% 是真实新闻那模型全部预测为真实也能有 70% 准确率看着很高实际毫无价值。所以我会用 sklearn 的classification_report和混淆矩阵一起看。from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt best_model models[svm] # 传统机器学习最佳基线 y_pred best_model.predict(X_test_tf) print(classification_report(y_test, y_pred, target_names[real, fake])) cm confusion_matrix(y_test, y_pred) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabels[real, fake], yticklabels[real, fake]) plt.xlabel(Predicted) plt.ylabel(Actual) plt.show()confusion_matrix返回的四个数字分别对应真正例、假正例、真负例、假负例转换成热力图后一眼就能看出模型在哪个类别上更容易犯错。重点关注对角线以外的两个数字真实新闻被误判为虚假的比例和虚假新闻漏判为真实的比例。前者关系到误伤无辜后者关系到放走谣言实际场景里代价完全不同文档里把这两个数字单独拎出来讨论能明显提升分析深度。6.2 误判样本逐条分析找到模型失效的规律生成指标只是第一步真正值钱的是把误判样本逐条打印出来读一遍。这一步不需要什么高级工具遍历预测错误的下标打印对应文本即可wrong_idx [i for i, (a, b) in enumerate(zip(y_test, y_pred)) if a ! b] for i in wrong_idx[:5]: print(真实标签:, y_test.iloc[i], 预测:, y_pred[i]) print(X_test.iloc[i][:300]) print(---)看误判样本时要带着两个问题模型把真实新闻判成虚假的那几条是不是语气特别夸张的营销稿或带有强烈情感色彩的政论这类文本用词模式跟虚假新闻很接近传统模型尤其容易分错。反过来漏网的虚假新闻是不是都是“标题平平无奇、内容全靠编造”的类型如果是说明模型学到的是表面语言模式而不是事实核查能力。把这两类误判各找三五条写进文档比任何大词都更有说服力。6.3 固定随机种子、保存模型与最后一轮验证项目的收尾动作是固定随机种子并保存模型。PyTorch 里torch.manual_seed(42)加numpy.random.seed(42)传统模型则是random_state42这些在前面已经埋好伏笔。模型保存用torch.save(model.state_dict(), bert_fakenews.pt)传统模型用joblib.dump保存。写进文档的实验记录里要标注清楚模型是拿哪份数据训练的、用了什么随机种子、最终测试集指标是多少这三点齐全复现就不存在悬念。这里说一个我的血泪经验。曾经有一次做类似项目验证集指标不错就直接写文档交上去结果答辩时老师从测试集里抽了一条新闻问“你为什么把这个分错了”。我盯着那行文本看了半分钟才发现那是一条包含大量引号的短新闻特征被引号里的内容带偏了。从那以后我每次做文本分类收尾不管多急都强制把混淆矩阵和误判样本检查走一遍再写文档。这套流程花不了 10 分钟但能让你的结论经得起追问。希望帮到你。本文还有配套的精品资源点击获取