简介:这套基于深度学习的虚假新闻检测系统Python项目,主要面向高校毕业设计、课程设计及自然语言处理方向的技术实践者。项目以Python为核心,通过TensorFlow/PyTorch搭建模型,综合应用RNN、LSTM、BERT等文本特征提取方法,完整覆盖新闻数据加载、文本清洗、分词、模型训练、效果评估与结果可视化等环节,模型中可根据数据集规模切换网络结构,训练时支持学习率调整与早停策略。压缩包共137个文件,整体大小约49.64MB,含Python源码(.py)、前端交互界面(JS/JSX/TS)、配置文件(JSON)、模型权重(.hdf5)、CSV训练测试数据集以及说明文档(.md/.txt)等,既能查看算法实现,也能直接运行验证,项目结构完整,便于二次开发。已有100人学习下载,适合参考完整的项目工程结构或进行论文复现。随包提供的文档详细说明环境搭建与运行步骤,代码经过验证可直接运行,能帮助读者快速上手虚假新闻检测的深度学习方法。
1. 基于深度学习的虚假新闻检测系统Python项目源码:这条赛道真正拼的是数据与评估
一条带着明显谣言色彩的图文,几分钟就能在社交平台收获上千次转发,人工审核的速度远远跟不上传播速度。基于深度学习的虚假新闻检测系统Python项目源码与实现,要解决的就是这件事:把新闻标题和正文作为输入,经过文本清洗、模型推理,输出一个“真实/虚假”的概率。
先给你一个反直觉的判断:这类项目最容易翻车的不是深度学习模型,而是数据和评估。随便拉一个预训练模型可能把准确率跑到八成,可一旦新闻来源更换、时间跨度过大,分数立刻跳水。原因通常不在网络结构,而在标签噪声、时间穿越、类别不平衡这类很工程化的细节。
所以这篇笔记按一条能稳定跑通的完整pipeline来讲:先选数据集、清洗文本、立住baseline,再用TextCNN把第一个像样分数跑出来,接着讲早停与调参顺序,最后分享避坑经验和上线接口。适合正在做NLP毕设的同学、需要快速给团队交付文本分类Demo的工程师,以及从机器学习往深度学习切换时想少踩坑的开发者。
2. 数据工程先行:公开数据集选型与文本清洗的落地顺序
2.1 公开数据集怎么选:FakeNewsNet、LIAR、ISOT的取舍
公开渠道能拿到的新闻数据集,我接触比较多的是FakeNewsNet、LIAR和ISOT。选哪个,取决于你的项目源码里文本字段长什么样,而不是哪个名气大。一张表说清差异:
| 数据集 | 文本粒度 | 标签体系 | 适合场景 | 注意点 |
|---|---|---|---|---|
| FakeNewsNet | 标题、正文、推文ID、用户信息 | real/fake 二分类 | 想同时做内容与传播特征 | 额外字段多,需要联网补全社交数据,离线实验偏重 |
| LIAR | 一句话断言或短句 | 六档可信度标签,常见做法合并为二分类 | 想快速做短文本验证 | 需要合并标签并做类别平衡 |
| ISOT | 新闻文章全文 | real/fake 二分类 | 直接做文本分类跑通Pipeline | 文本偏长,训练和推理耗时更高 |
如果只是要把源码先跑通,我一般推荐ISOT或LIAR——字段少、表格化,不需要额外请求外部接口。FakeNewsNet的字段最丰富,但很多字段要再拿推文ID去社交平台补数据,在离线实验里容易劝退。它更适合后续做“文本+传播结构”的进阶方向。
2.2 清洗与切分:一份能直接喂给PyTorch的DataFrame
选定数据集后,先统一文本格式。我习惯写一个极简清洗函数,只去掉URL、HTML残留和多余空白,不对标点和大小写做激进处理。
import re import pandas as pd from sklearn.model_selection import train_test_split def clean_text(text: str) -> str: if not isinstance(text, str): return "" text = text.lower() text = re.sub(r"http\S+", "", text) # 去掉 URL text = re.sub(r"<[^>]+>", "", text) # 去掉残留 HTML 标签 text = re.sub(r"\s+", " ", text).strip() # 多空格合并 return text df = pd.read_csv("news.csv", encoding="utf-8") df["clean_text"] = df["text"].map(clean_text) df = df[df["clean_text"].str.len() > 20] # 过滤过短的残片 label_map = {"real": 0, "fake": 1} df["label"] = df["label"].map(label_map) df = df.dropna(subset=["label"]) X_tr, X_test, y_tr, y_test = train_test_split( df["clean_text"], df["label"], test_size=0.1, stratify=df["label"], random_state=42 ) X_tr, X_val, y_tr, y_val = train_test_split( X_tr, y_tr, test_size=0.1 / 0.9, # 让最终比例约为 8:1:1 stratify=y_tr, random_state=42 )逻辑说明:第一步把噪声字符清掉,得到干净的纯文本;第二步过滤过短样本,新闻正文如果短于20个字符,大概率是采集残片,留着只会增加标签噪声;第三步做标签映射,把字符串标签转成0/1;最后切出训练、验证、测试三份数据。
参数说明:stratify保证切分前后正负样本比例一致,对类别不平衡的数据极其重要;random_state=42固定切分结果,让实验可复现;test_size=0.1先切出测试集,再用0.1 / 0.9切验证集,这样最终训练集约占80%。如果你第一次跑不想写这么绕,可以先用train_test_split切出测试集,再对剩余数据按10%切验证集,效果相同。
提示:不要在清理阶段做停用词删除和词干化。深度学习模型需要停用词来捕捉语气和句式,词干化又会破坏预训练词向量的匹配,这两步在后期调优时往往得不偿失。
2.3 文本表示与最强baseline:TF-IDF先跑一趟,哪怕你打算用BERT
我见过不少同学拿到数据后直接上LSTM,连baseline都没有。这不是错的,但你至少需要一个“下限分数”来告诉自己:模型有没有学到位。对虚假新闻检测来说,TF-IDF加逻辑回归通常能到70%到80%的F1。如果你的深度学习模型连这个线都没过,问题多半不在模型,而在数据或训练回路。
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.metrics import f1_score, accuracy_score vec = TfidfVectorizer( max_features=20000, min_df=2, max_df=0.95, ngram_range=(1, 2) ) X_vec_tr = vec.fit_transform(X_tr) X_vec_val = vec.transform(X_val) clf = LogisticRegression(max_iter=500, C=1.0) clf.fit(X_vec_tr, y_tr) y_pred = clf.predict(X_vec_val) print("acc:", accuracy_score(y_val, y_pred)) print("f1:", f1_score(y_val, y_pred))逻辑说明:fit_transform在训练集上完成词表构建和向量化,transform只在验证集上做同一套映射,不重新拟合。ngram_range=(1, 2)让线性模型能看见“not fake”“hoax alert”这类双词短语,这是虚假新闻里很常见的判别信号。
参数说明:max_features=20000限制词表规模,避免稀疏矩阵过大;min_df=2去掉只出现过一次的词,减少噪声;max_df=0.95去掉几乎所有文本都有的高频词,比如无信息量的通用词;C=1.0是逻辑回归正则强度的默认值,数据量不大时不用急着调。
顺手记录一下句子长度分布也有价值:打印训练集中文本长度的中位数和90分位数,后面TextCNN和BERT的max_len就按这个来定,而不是拍脑袋填128。
3. 模型实现:在PyTorch里跑通TextCNN虚假新闻分类器,记住这些参数
3.1 为什么TextCNN是虚假新闻检测的稳妥起点
虚假新闻检测本质上是对短文本做二分类。新闻正文平均长度在几十到几百个词,标题更短。TextCNN通过多个宽度的卷积核并行扫描句子,相当于同时看bigram、trigram和4-gram三种长度的局部短语。虚假新闻的破绽往往藏在局部表达里:反问句、夸张形容词、全大写标题、连续感叹号。CNN天然擅长这种局部模式抽取。
相比之下,LSTM更强调长期依赖,对短文本来说训练成本高、收益有限;BERT更强,但要先下载预训练权重,显存不够时很容易卡在第一轮。所以,如果项目源码的目标是快速产出可复现的分数,TextCNN是合理的第一选择,等整体Pipeline稳定后再迁移到更强的模型不迟。
3.2 先建词表再写模型:TextCNN的最小实现
动手写模型之前,先把词表建好,它决定Embedding层的第一维大小。我一般用Counter扫一遍训练集,出现次数少于2的词统一映射为<UNK>。
from collections import Counter counter = Counter() for text in X_tr: counter.update(text.split()) vocab = {"<PAD>": 0, "<UNK>": 1} for word, cnt in counter.items(): if cnt >= 2: vocab[word] = len(vocab) def encode(text, max_len=128): ids = [vocab.get(w, 1) for w in text.split()][:max_len] return ids + [0] * (max_len - len(ids))逻辑说明:<PAD>用来补齐短句,<UNK>用来替换低频和未见词。encode先截断到max_len,再用0补齐,保证批次内所有样本长度一致。
参数说明:cnt >= 2是经验值,过滤掉只出现一次的词,能明显减少Embedding层的参数量和训练噪声;max_len建议取训练集文本长度的90分位数,新闻标题可以小一点,正文通常取128或256。
接下来是TextCNN核心类。我用nn.Conv2d实现,输入形状是(batch_size, 1, seq_len, embed_dim),卷积核宽度等于embed_dim,这样卷积只沿序列方向滑动。
import torch import torch.nn as nn import torch.nn.functional as F class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim=128, num_filters=100, filter_sizes=(2, 3, 4), num_classes=2, dropout=0.5): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.convs = nn.ModuleList([ nn.Conv2d(1, num_filters, (fs, embed_dim)) for fs in filter_sizes ]) self.dropout = nn.Dropout(dropout) self.fc = nn.Linear(len(filter_sizes) * num_filters, num_classes) def forward(self, x): # x: [batch_size, seq_len] emb = self.embedding(x) # [bs, seq_len, embed_dim] emb = emb.unsqueeze(1) # [bs, 1, seq_len, embed_dim] convs = [] for conv in self.convs: c = F.relu(conv(emb)) # [bs, num_filters, seq_len - fs + 1, 1] c = c.squeeze(3) c = F.max_pool1d(c, c.size(2)) # 取序列维最大值 convs.append(c.squeeze(2)) out = torch.cat(convs, dim=1) # [bs, num_filters * len(filter_sizes)] out = self.dropout(out) return self.fc(out)逻辑说明:Embedding层把词索引转成稠密向量,padding_idx=0让<PAD>位置的向量始终为0,不参与梯度更新。三个并行的Conv2d分别用宽度为2、3、4的卷积核扫描文本,相当于同时提取bigram、trigram和4-gram特征。max_pool1d取整个序列维的最大值,意思是“只要文本某处出现强烈信号,就把它当作整条新闻的特征”。
参数说明:embed_dim=128对多数新闻语料足够,再大收益有限;filter_sizes=(2,3,4)是保守配置,改大不一定更好,但参数量会涨;num_filters=100是表达能力和参数量的平衡点;dropout=0.5是文本CNN的常用防过拟合配置,如果验证集loss迟迟不降,可以先降到0.3试一轮。
3.3 升级路径:BERT模块怎么接进来
如果TextCNN在验证集上已经稳定,再往上冲,下一步自然想到BERT。只需要把分词和编码换成Transformer接口,分类头从[CLS]向量接出来。
from transformers import AutoTokenizer, AutoModel tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased") model = AutoModel.from_pretrained("bert-base-uncased") encoded = tokenizer( texts, padding=True, truncation=True, max_length=128, return_tensors="pt" ) outputs = model(**encoded) cls_vec = outputs.last_hidden_state[:, 0, :] logits = cls_head(cls_vec) # cls_head 是你定义的一个 Linear 分类头逻辑说明:BERT把每条文本编码成[batch, seq_len, hidden],其中[CLS]位置的向量被设计为整句的语义聚合,所以分类头通常只接这一个向量,不需要再做池化。
参数说明:BERT的默认学习率比CNN低一个数量级,常见做法是2e-5到3e-5;max_length=128会在输入前把长文本截断,否则attention矩阵过大,显存很容易爆;如果显存不够,batch_size压到8或16,不要硬撑。这条路径适合你完整跑通过一次TextCNN之后,再把它当作动手深度学习阶段的升级练习。
4. 训练与调参:早停、类别不平衡与泛化调优的实操顺序
4.1 训练回路:早停、梯度裁剪和最佳权重保存
训练回路是项目源码里最容易写散的部分。我的做法是代码里同时包含三个机制:早停、梯度裁剪、只保存验证集loss最优的权重。
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) criterion = nn.CrossEntropyLoss() best_val_loss = float("inf") patience = 0 for epoch in range(epochs): model.train() for batch_texts, batch_labels in train_loader: logits = model(batch_texts) loss = criterion(logits, batch_labels) optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() model.eval() val_loss_total, val_correct, val_count = 0, 0, 0 with torch.no_grad(): for batch_texts, batch_labels in val_loader: logits = model(batch_texts) loss = criterion(logits, batch_labels) val_loss_total += loss.item() preds = logits.argmax(dim=1) val_correct += (preds == batch_labels).sum().item() val_count += len(batch_labels) val_loss = val_loss_total / len(val_loader) val_acc = val_correct / val_count if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), "best_textcnn.pth") patience = 0 else: patience += 1 if patience >= 3: break逻辑说明:训练阶段用clip_grad_norm_限制梯度的L2范数不超过1.0,防止单条长文本产生的大梯度把Embedding层带偏;验证阶段不计算梯度,统计loss和准确率;只有当验证集loss创新低时才保存权重,连续3轮没有改善就停止。
参数说明:lr=1e-3是Adam配TextCNN的默认起点,验证集震荡时可降到5e-4或3e-4;patience=3是偏激进的早停配置,想更稳妥可以调到5;max_norm=1.0不是越大越好,调大后训练初期收敛更快但更容易在后期震荡。这些参数应该在你第一次跑通后再细调,不要一开始就追求最优。
4.2 处理不平衡标签:当准确率0.89但F1只有0.62时
虚假新闻数据集里,真实新闻和虚假新闻的比例往往不是1:1。特别是从LIAR的六档标签合并而来时,小样本类别会被稀释。遇到这种情况,最忌讳只看accuracy。一个模型如果对全部样本都预测多数类,在90:10的数据上准确率也有0.90,但对少数类的召回是0。
labels = torch.tensor(y_train_labels) neg = (labels == 0).sum().float() pos = (labels == 1).sum().float() pos_weight = neg / pos # 方案一:单输出 logits 时用 BCEWithLogitsLoss criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight) # 方案二:两输出 logits 时用 CrossEntropyLoss + class weight class_weights = torch.tensor([1.0, neg / pos]) criterion = nn.CrossEntropyLoss(weight=class_weights)逻辑说明:pos_weight是少数类相对于多数类的权重,值为2.0时,模型把样本预测为少数类时受到的惩罚会加倍,从而抑制“无脑输出多数类”的行为。两种方案针对不同输出层结构,选择一个用即可。
参数说明:如果模型最后一层只有一个神经元,用BCEWithLogitsLoss;如果是两个神经元输出二维logits,用CrossEntropyLoss加class_weights。不推荐手动把少数类样本复制多份来平衡,那样会引入重复样本相关性,早停和验证都会失真。
评估时把混淆矩阵打印出来:
from sklearn.metrics import confusion_matrix, f1_score print(confusion_matrix(y_val, y_pred)) print("f1 on fake:", f1_score(y_val, y_pred, pos_label=1))你需要检查的不是整体准确率,而是“真实新闻被误判为虚假”和“虚假新闻被漏掉”各占多少。如果漏掉的假新闻远多于误伤的真新闻,说明类别权重还不够大,可以继续向上调。
4.3 参数调整顺序:先过拟合、再谈泛化,最后动正则
调参的顺序比具体数值更重要。我调这类项目基本是三步走:第一轮先拿少量训练样本跑过拟合,比如随机抽500条,如果loss能降到接近0,说明模型结构和数据没问题;第二轮拉全量数据训练,主要看验证集loss走势;第三轮只有验证集明显回升、训练集继续猛降时,才去动dropout、L2和学习率衰减。
多数人跳过了第一步,直接把dropout调到0.8、L2开到0.01,结果验证集和训练集一起震荡,还误以为是模型不行。其实那只是正则过猛。
| 现象 | 第一个动作 | 第二个动作 |
|---|---|---|
| 训练loss不降 | 检查数据、词表、学习率 | 先用小批量样本跑过拟合 |
| 训练loss降、验证loss先降后升 | 把dropout从0.3往0.5调 | 加权重衰减或L2正则 |
| 训练和验证都不稳 | 降低学习率 | 调小梯度裁剪阈值并检查batch size |
dropout作用于特征向量,对短文本更敏感;L2作用于全连接层和Embedding输出,对这类二分类任务影响相对温和,所以应先调dropout,再动L2。
5. 虚假新闻检测项目避坑清单:5个最容易被翻车点与排查路径
5.1 数据泄露:把测试集信息带进训练
现象:验证集F1高得离谱,模型一换成全新新闻来源就崩。原因:构建词表或TF-IDF时用了全部数据,测试集文本已经参与拟合。解决:词表构建、TF-IDF拟合、标准化参数的计算都只在训练集完成,验证集和测试集只用transform。这个错误我在不少开源项目源码里看到过,复盘时很容易被忽略。
5.2 时间穿越:训练集和测试集新闻来自同一时期
现象:模型在测试集上分数虚高,但实际部署后对新近新闻无效。原因:新闻数据集按时间流划分时,同一个热点事件的不同报道很容易同时落在训练集和测试集里,模型记忆的是事件实体,而不是“虚假表达”本身的模式。解决:如果数据带有发布时间字段,按时间点切分,让测试集在时间上完全晚于训练集;如果没时间字段,至少要做标题去重,把重复报道从测试集剔除。
5.3 标签噪声:把讽刺类内容当成虚假新闻
现象:模型对讽刺或戏仿类文本特别容易判成fake,但翻原文会发现它并没有直接撒谎。原因:不少公开数据集的标注标准是“来源可疑”而非“文本为假”,采集时把政治讽刺文的标签也带进去了。解决:跑完模型后,人工抽查预测置信度最高的20条错误样本,看输出是文本语义还是来源偏见。如果讽刺类占大头,把它从当前测试集剔除并在实验日志里记录,不要默默吞掉。
5.4 类别不平衡:准确率好看,召回崩盘
现象:accuracy 0.89,F1只有0.62,混淆矩阵里少数类一行全是0。原因:类别比例失衡,模型学会了无脑预测多数类,损失函数没有加权。解决:按4.2的pos_weight或class_weights做加权,并且用F1和混淆矩阵做早停依据,不要只盯准确率。准确率只告诉你“预测对的占比”,不告诉你“假新闻被抓到多少”。
5.5 清洗过度:把语气痕迹洗没了
现象:清洗后文本很干净,同一模型反而比清洗前F1更低。原因:我把全部标点、大写、重复字符都删了,而虚假新闻的卖点往往集中在这类格式上:全大写标题、连续感叹号、夸张标点。解决:清洗只处理URL、HTML标签和换行,大小写和标点尽量保留。如果想去掉多余感叹号,用正则把连续三个以上压缩成一个,保留“有”和“无”的区别。
这五条不一定每条都会出现,但出现时往往会让分数看起来“很对”、实际上很脆。排查时先把跑通的那版结果固化保存,再逐个修改,不要一次性全改,否则你根本不知道是哪一步把分数修回来的。
6. 部署与验证:把训练好的模型封装成推理接口,再做一轮错误样本审阅
训练完成后的最后一步,是把模型从Jupyter Notebook里搬到可调用的接口。我用FastAPI写一个最小推理服务,加载训练好的权重,接收新闻文本,返回虚假概率。
from fastapi import FastAPI, Body import torch from model import TextCNN, vocab, encode app = FastAPI() model = TextCNN(vocab_size=len(vocab)) model.load_state_dict(torch.load("best_textcnn.pth", map_location="cpu")) model.eval() def predict(text: str, max_len: int = 128): ids = encode(text, max_len) x = torch.tensor([ids], dtype=torch.long) with torch.no_grad(): logits = model(x) prob = torch.softmax(logits, dim=1).squeeze(0).tolist() return { "fake_prob": prob[1], "label": "fake" if prob[1] > 0.5 else "real" } @app.post("/predict") def api_predict(text: str = Body(..., embed=True)): return predict(text)逻辑说明:加载模型后一定要记得model.eval(),否则dropout仍会生效,每次推理结果都不一样;encode和训练时保持同一套词表和截断逻辑,前端传入的文本先转成词索引,再补齐到max_len。Body(..., embed=True)让文本以JSON字段传入,避免URL长度限制。
验证方法是:每次在验证集上跑完,把预测结果按概率排序,人工看高置信度但预测错误的样本。这是唯一能快速让你意识到模型在学什么的动作。如果高置信错误样本全部集中在某类来源,大概率是数据分布偏了,而不是模型不够复杂。
我早期做这类项目,经常被验证集的高F1冲昏头脑,结果把接口交出去后才发现,线上文本里带着表情符号、URL和长段落,模型完全没有准备好。所以我现在习惯把流程卡得很死:先完整跑通一次端到端,再谈调优;先看错误样本,再谈分数;先固化基线,再动任何参数。这套顺序帮我避开了很多看似简单、实际代价高昂的坑,希望帮到你。
本文还有配套的精品资源,点击获取