news 2026/10/3 4:18:18

基于Python的虚假新闻检测系统:机器学习、深度学习与BERT模型实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Python的虚假新闻检测系统:机器学习、深度学习与BERT模型实现

简介:本资源是一套面向中文社交媒体虚假新闻检测的完整项目源码,适合自然语言处理初学者、机器学习课程设计者及毕业设计开发者参考。项目以微信文章标题为输入,区分真实与虚假新闻,覆盖传统机器学习、深度学习与BERT预训练模型三条技术路线。压缩包共92个文件,约173KB,以45个Python脚本为核心,辅以zbak备份、xml配置、iml工程文件及ipynb笔记,结构上分为MachineLearning与DeepLearning两大模块。传统方法集成随机森林、支持向量机、朴素贝叶斯与逻辑回归,结合jieba分词、词袋模型和TF-IDF特征,其中逻辑回归加词袋模型准确率达90.48%;深度学习部分提供CNN、RNN、TextRCNN、DPCNN、Transformer及BERT微调等实现,并附训练评估脚本。已有66人学习,读者可获取从数据预处理、特征工程到模型训练与指标评估的完整代码链路,便于复现实验、对比算法差异并在此基础上优化文本清洗与模型集成策略。

1. 虚假新闻检测系统:从标题到可跑通的工程路径

虚假新闻检测这件事,真正动手做过的人都知道,难点从来不在模型本身,而在数据怎么来、标签怎么定、模型选哪一层。基于 Python 的虚假新闻检测系统,机器学习、深度学习与 BERT 模型实现,这个标题拆开看其实是一条完整的工程链路:先用传统机器学习把基线跑通,再用深度学习把特征抽取自动化,最后用 BERT 把语义理解拉到预训练级别。三档方案不是替代关系,而是递进关系,每一档都有它适合的场景和明确的性能天花板。

这套系统适合谁?如果你手上有几千到几万条带标签的新闻文本,想做一个能实际跑起来、能解释、能迭代的检测系统,那这条路径就是为你准备的。新手可以从机器学习那一档跟步骤走,熟手可以直接跳到 BERT 微调,但中间的坑和参数边界,我会一并讲清楚。整条链路用 Python 串起来,不依赖冷门框架,本地一台带 GPU 的机器或者云平台都能跑。

2. 数据准备与机器学习基线:先把能跑的版本做出来

2.1 虚假新闻数据的来源与标签体系

做检测系统,第一件事不是选模型,是搞清楚数据从哪来、标签怎么定。常见做法是找公开的新闻数据集,比如带真假标签的英文新闻语料,或者自己从新闻网站抓取后人工标注。我一般会先确认三件事:文本字段是否完整、标签是否只有真假两类、有没有明显的来源偏置。

来源偏置是虚假新闻检测里最隐蔽的坑。如果假新闻全部来自某几个网站,真新闻全部来自另外几个,模型学到的其实是网站风格而不是新闻真假。处理办法是在数据准备阶段就做来源去偏,比如按来源分层采样,或者把来源信息从特征里剔除。

标签体系上,二分类是最稳的起点。多分类(比如真、假、半真、误导)看起来更细,但标注一致性很难保证,几个人标同一批数据,kappa 系数经常低得没法用。我一般建议先做二分类,把系统跑通,再考虑细粒度。

import pandas as pd from sklearn.model_selection import train_test_split # 读取原始数据,假设字段为 text 和 label df = pd.read_csv("news_raw.csv") # 去掉文本为空或过短的样本 df = df[df["text"].str.len() > 50] # 标签映射:假新闻为 1,真新闻为 0 df["label"] = df["label"].map({"fake": 1, "real": 0}) # 按来源分层采样,减少来源偏置 df = df.groupby("source", group_keys=False).apply( lambda x: x.sample(min(len(x), 500), random_state=42) ) # 划分训练集和测试集,stratify 保证标签比例一致 train_df, test_df = train_test_split( df, test_size=0.2, stratify=df["label"], random_state=42 )

这段代码做了四件事:过滤无效样本、标签二值化、按来源限制单来源样本量、分层划分。min(len(x), 500)这个参数是经验值,目的是防止某个来源的样本过多导致模型偏向该来源的风格。stratify参数保证训练集和测试集里真假比例一致,否则测试集如果假新闻占比过高,准确率会虚高。

2.2 用 TF-IDF 加逻辑回归跑通第一个基线

机器学习这一档,我推荐从 TF-IDF 加逻辑回归开始。原因很简单:训练快、可解释、调参少,而且能给你一个明确的性能下限。如果 BERT 跑出来还不如 TF-IDF,那大概率是数据或训练流程出了问题,而不是模型不够强。

from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report from sklearn.pipeline import Pipeline # 构建 TF-IDF + 逻辑回归的流水线 pipeline = Pipeline([ ("tfidf", TfidfVectorizer( max_features=50000, # 保留词频最高的 5 万个词 ngram_range=(1, 2), # 同时考虑单词和双词组合 min_df=3, # 忽略出现少于 3 次的词 max_df=0.9 # 忽略出现在 90% 以上文档中的词 )), ("clf", LogisticRegression( C=1.0, # 正则化强度的倒数,越大越容易过拟合 max_iter=1000, class_weight="balanced" # 类别不平衡时自动调整权重 )) ]) # 训练 pipeline.fit(train_df["text"], train_df["label"]) # 预测并输出报告 pred = pipeline.predict(test_df["text"]) print(classification_report(test_df["label"], pred))

max_features=50000是平衡内存和效果的常用值,文本量在十万级以下时够用。ngram_range=(1, 2)让模型能捕捉“不实信息”“未经证实”这类双词模式,对虚假新闻检测有明显帮助。min_df=3过滤掉拼写错误和噪声词,max_df=0.9过滤掉几乎每篇都出现的词。class_weight="balanced"在真假样本比例超过 3:1 时一定要开,否则模型会倾向于预测多数类。

这个基线跑完,你大概能拿到 85% 到 92% 的准确率,具体取决于数据集难度。如果低于 85%,先检查数据标签有没有问题,而不是急着换模型。

2.3 机器学习方案的边界在哪里

TF-IDF 加逻辑回归的天花板很明显:它只看词的出现,不理解词序和语义。比如“某地发生地震”和“某地未发生地震”,TF-IDF 看到的词几乎一样,但意思完全相反。再比如反讽和隐喻,词袋模型基本无能为力。

我一般用这个基线做两件事:一是验证数据质量,二是作为后续模型的对照。如果深度学习模型比它高出不到 2 个百分点,那说明数据里的信号主要来自关键词,这时候上 BERT 的性价比就不高。反过来,如果 BERT 能拉开 5 个百分点以上,说明语义理解确实在起作用,这个方向就值得继续投入。

3. 深度学习方案:用 CNN 和 LSTM 自动抽特征

3.1 词向量与嵌入层的选择

深度学习这一档,第一步是把文本变成向量。常见做法是用预训练词向量初始化嵌入层,比如 Word2Vec 或 GloVe,然后在训练中微调。如果数据量不大,也可以直接用随机初始化的嵌入层,让模型自己学。

我一般会先用预训练词向量,因为它在小数据上更稳。嵌入维度通常设 100 到 300,太低表达力不够,太高容易过拟合。词表大小控制在 30000 到 50000 之间,覆盖大部分高频词即可,剩下的用<UNK>处理。

import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim, num_classes, filter_sizes, num_filters): super().__init__() # 嵌入层,padding_idx=0 表示填充符不参与梯度更新 self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) # 多尺寸卷积核,分别捕捉 2、3、4 元词组特征 self.convs = nn.ModuleList([ nn.Conv2d(1, num_filters, (fs, embed_dim)) for fs in filter_sizes ]) self.dropout = nn.Dropout(0.5) self.fc = nn.Linear(num_filters * len(filter_sizes), num_classes) def forward(self, x): # x: (batch, seq_len) x = self.embedding(x) # (batch, seq_len, embed_dim) x = x.unsqueeze(1) # (batch, 1, seq_len, embed_dim) # 每个卷积核做最大池化,得到固定长度向量 x = [torch.relu(conv(x)).squeeze(3) for conv in self.convs] x = [torch.max_pool1d(i, i.size(2)).squeeze(2) for i in x] x = torch.cat(x, dim=1) x = self.dropout(x) return self.fc(x)

filter_sizes一般取[2, 3, 4],对应二元、三元、四元词组。num_filters每个尺寸取 100 到 200。padding_idx=0让填充符不产生梯度,避免影响其他词向量。Dropout(0.5)在全连接层前做正则,这是文本分类里比较稳的设置。

3.2 LSTM 处理长距离依赖的配置要点

CNN 擅长抓局部模式,但新闻文本里真假线索可能隔得很远。比如开头说“据可靠消息”,结尾说“纯属虚构”,中间隔了好几百字。这种长距离依赖,LSTM 比 CNN 更合适。

class TextLSTM(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes, num_layers=2): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.lstm = nn.LSTM( embed_dim, hidden_dim, num_layers=num_layers, bidirectional=True, # 双向,同时看前后文 batch_first=True, dropout=0.3 # 多层 LSTM 之间的 dropout ) self.fc = nn.Linear(hidden_dim * 2, num_classes) # 双向所以乘 2 def forward(self, x): x = self.embedding(x) out, _ = self.lstm(x) # 取最后一个时间步的输出,双向拼接后送入全连接 out = out[:, -1, :] return self.fc(out)

hidden_dim一般取 128 到 256,num_layers取 2 层。bidirectional=True让模型同时看前后文,对检测任务帮助很大。dropout=0.3是多层 LSTM 之间的正则,注意这个参数只在num_layers > 1时生效。取最后一个时间步的输出是一种简化做法,更稳的方式是做注意力池化,但实现复杂度会高一些。

3.3 训练循环与早停策略

深度学习训练最容易翻车的地方是过拟合和训练不稳定。我一般会加早停和梯度裁剪,这两个是性价比最高的稳定手段。

from torch.utils.data import DataLoader, TensorDataset # 假设 train_X, train_y 已经转成 tensor train_loader = DataLoader( TensorDataset(train_X, train_y), batch_size=64, shuffle=True ) model = TextLSTM(vocab_size=50000, embed_dim=200, hidden_dim=256, num_classes=2) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) criterion = nn.CrossEntropyLoss() best_loss = float("inf") patience = 3 wait = 0 for epoch in range(20): model.train() total_loss = 0 for batch_x, batch_y in train_loader: optimizer.zero_grad() logits = model(batch_x) loss = criterion(logits, batch_y) loss.backward() # 梯度裁剪,防止梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0) optimizer.step() total_loss += loss.item() avg_loss = total_loss / len(train_loader) print(f"Epoch {epoch}, Loss: {avg_loss:.4f}") # 早停判断 if avg_loss < best_loss: best_loss = avg_loss wait = 0 torch.save(model.state_dict(), "best_model.pt") else: wait += 1 if wait >= patience: print("Early stopping") break

batch_size=64是文本分类的常用值,显存不够就降到 32。lr=1e-3是 Adam 的默认学习率,如果 loss 震荡明显就降到 5e-4。clip_grad_norm_的max_norm=5.0是经验值,LSTM 容易梯度爆炸,这个参数基本必开。早停的patience=3表示验证损失连续 3 轮不降就停,防止过拟合。

4. BERT 微调:把语义理解拉到预训练级别

4.1 BERT 微调的数据格式与分词器配置

BERT 这一档,核心变化在数据格式。它不用词表索引,而是用 WordPiece 分词器把文本切成子词,再转成 token id。这一步如果配错,后面全白搭。

from transformers import BertTokenizer, BertForSequenceClassification from torch.utils.data import Dataset import torch tokenizer = BertTokenizer.from_pretrained("bert-base-uncased") class NewsDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_len=256): self.texts = texts self.labels = labels self.tokenizer = tokenizer self.max_len = max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): encoding = self.tokenizer( self.texts[idx], max_length=self.max_len, padding="max_length", truncation=True, return_tensors="pt" ) return { "input_ids": encoding["input_ids"].squeeze(0), "attention_mask": encoding["attention_mask"].squeeze(0), "label": torch.tensor(self.labels[idx], dtype=torch.long) }

max_len=256是新闻文本的常用截断长度,超过 256 个 token 的新闻不多,而且 BERT 的 512 上限对显存压力很大。padding="max_length"统一补齐,方便批量训练。truncation=True自动截断超长文本。注意return_tensors="pt"返回的是 PyTorch 张量,后面squeeze(0)去掉多余的批次维度。

4.2 微调脚本与关键超参数

BERT 微调的超参数和传统深度学习不太一样,学习率要小得多,通常在 2e-5 到 5e-5 之间。太大容易把预训练学到的语义打乱,太小又学不动。

from transformers import AdamW, get_linear_schedule_with_warmup from torch.utils.data import DataLoader train_dataset = NewsDataset(train_texts, train_labels, tokenizer) train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True) model = BertForSequenceClassification.from_pretrained( "bert-base-uncased", num_labels=2 ) optimizer = AdamW(model.parameters(), lr=2e-5, weight_decay=0.01) # 线性预热,前 10% 步数学习率从 0 升到 2e-5 total_steps = len(train_loader) * 3 scheduler = get_linear_schedule_with_warmup( optimizer, num_warmup_steps=int(total_steps * 0.1), num_training_steps=total_steps ) model.train() for epoch in range(3): for batch in train_loader: optimizer.zero_grad() outputs = model( input_ids=batch["input_ids"], attention_mask=batch["attention_mask"], labels=batch["label"] ) loss = outputs.loss loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() scheduler.step() print(f"Epoch {epoch}, Loss: {loss.item():.4f}")

lr=2e-5是 BERT 微调的经典值,weight_decay=0.01做正则。num_warmup_steps取总步数的 10%,这是 Transformer 训练的标准做法,能防止初期梯度太大破坏预训练权重。clip_grad_norm_的max_norm=1.0比 LSTM 更严格,因为 BERT 对梯度更敏感。训练轮数一般 2 到 4 轮,超过 4 轮基本都会过拟合。

4.3 用验证集挑最佳轮次而不是最后一轮

BERT 微调有个反直觉的地方:最后一轮的模型往往不是最好的。第 2 轮或者第 3 轮的效果可能更好,后面就开始过拟合了。所以一定要在每轮结束后跑验证集,保存验证集上最好的那个模型。

from sklearn.metrics import f1_score def evaluate(model, loader): model.eval() preds, trues = [], [] with torch.no_grad(): for batch in loader: outputs = model( input_ids=batch["input_ids"], attention_mask=batch["attention_mask"] ) pred = torch.argmax(outputs.logits, dim=1) preds.extend(pred.cpu().numpy()) trues.extend(batch["label"].cpu().numpy()) return f1_score(trues, preds, average="binary") best_f1 = 0 for epoch in range(3): # 训练代码同上 val_f1 = evaluate(model, val_loader) print(f"Epoch {epoch}, Val F1: {val_f1:.4f}") if val_f1 > best_f1: best_f1 = val_f1 model.save_pretrained("best_bert") tokenizer.save_pretrained("best_bert")

用 F1 而不是准确率来挑模型,是因为虚假新闻检测里假新闻通常是少数类,准确率会被多数类拉高。average="binary"表示只算假新闻这一类的 F1,这个指标更贴近实际需求。保存模型时把 tokenizer 一起存,否则推理时对不上分词规则。

5. 避坑与排查:虚假新闻检测里最容易翻车的五件事

5.1 数据泄漏:测试集里混进了训练集样本

现象:模型在测试集上准确率 99%,上线后效果一塌糊涂。

原因:数据划分时没有去重,同一篇新闻的多个版本分别进了训练集和测试集。或者用随机划分时,同一来源的相似文本被分到了两边。

解决:划分前先做文本去重,用 SimHash 或者简单的 MD5 去重。再按来源或时间划分,而不是随机划分。时间划分更贴近真实场景:用旧新闻训练,用新新闻测试。

5.2 标签噪声:标注不一致导致模型学偏

现象:训练 loss 降不下去,验证集准确率在 70% 左右徘徊。

原因:标注人员对“假新闻”的定义不一致,同一篇新闻有人标真有人标假。或者数据里混入了机器标注的低质量标签。

解决:先做标注一致性检查,抽 200 条算 kappa 系数,低于 0.6 就重新定标注规范。再用交叉验证找出模型预测和标签不一致的样本,人工复核。如果噪声比例超过 10%,考虑用噪声鲁棒损失函数,或者先清洗再训练。

5.3 过拟合:训练集 F1 0.99,验证集 F1 0.75

现象:训练集指标远高于验证集,且差距随训练轮数扩大。

原因:模型参数量远大于数据量,或者训练轮数太多。BERT 微调时尤其常见,3 轮以上基本都会过拟合。

解决:先降模型复杂度,比如 BERT 只微调最后两层,或者用 DistilBERT 替代。再加正则,dropout调到 0.3 到 0.5,weight_decay调到 0.01 到 0.1。最后用早停,验证集 F1 连续两轮不升就停。

5.4 类别不平衡:模型全预测为多数类

现象:准确率看起来不错,但假新闻的召回率接近 0。

原因:真假样本比例悬殊,比如 10:1,模型发现全预测真新闻就能拿 90% 准确率。

解决:训练时用class_weight或者重采样。BERT 微调可以用加权损失,给少数类更高的权重。评估时看 F1 和 AUC,不要只看准确率。如果少数类样本太少,考虑用数据增强,比如同义词替换、回译。

5.5 推理速度:BERT 上线后单条延迟超过 500ms

现象:离线评估效果很好,但线上接口响应太慢,用户等不了。

原因:BERT base 模型有 1.1 亿参数,CPU 推理单条就要几百毫秒。如果批量大小是 1,GPU 利用率也很低。

解决:先做模型蒸馏,用 BERT 教一个小模型,比如 BiLSTM 或者 TextCNN,推理速度能快 10 倍以上。再做量化,把 FP32 转成 INT8,速度再快 2 到 3 倍。最后做批处理,把多条请求攒成一个 batch 一起推理,GPU 利用率能拉满。

6. 进阶技巧:用对抗验证挑出真正难分的样本

模型跑通之后,真正拉开差距的不是换更大的模型,而是找到那些模型学不会的样本。我常用的一招是对抗验证:训练一个分类器去区分训练集和测试集,如果它很容易区分,说明两个集合分布不一致,模型在测试集上的表现不可信。

from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score import numpy as np # 构造对抗验证数据集:训练集标 0,测试集标 1 adv_texts = np.concatenate([train_df["text"], test_df["text"]]) adv_labels = np.array([0] * len(train_df) + [1] * len(test_df)) # 用 TF-IDF 加随机森林做对抗验证 adv_pipeline = Pipeline([ ("tfidf", TfidfVectorizer(max_features=20000)), ("clf", RandomForestClassifier(n_estimators=100, random_state=42)) ]) auc = cross_val_score(adv_pipeline, adv_texts, adv_labels, cv=5, scoring="roc_auc") print(f"Adversarial AUC: {auc.mean():.4f}")

如果 AUC 接近 0.5,说明训练集和测试集分布一致,模型评估可信。如果 AUC 超过 0.7,说明两个集合有明显差异,这时候要么重新划分数据,要么用重要性加权把训练集往测试集分布上靠。这个技巧在数据量不大、来源单一的时候特别有用,能提前发现评估虚高的问题。

另一个我常用的习惯是:每次换模型之前,先把当前模型的错误样本导出来看 50 条。虚假新闻检测里,错误往往集中在反讽、隐喻、以及真假混杂的报道上。看多了你会发现,有些错误是数据问题,换模型解决不了;有些错误是语义理解不够,那才是 BERT 的用武之地。这个习惯帮我省了很多无效调参的时间,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 4:17:33

大模型微调全流程:从预训练到指令微调与对齐

接触过大模型微调的人应该都有这种体验&#xff1a;从开源社区拉下一个基座模型&#xff0c;无论偏通用还是偏垂直&#xff0c;直接跑起来对话&#xff0c;你问它“帮我写一封辞职信”&#xff0c;它可能给你续写出一篇散文&#xff1b;你又说“你是一个客服机器人”&#xff0…

作者头像 李华
网站建设 2026/10/3 4:17:29

Linux下MATLAB安装全指南:版本匹配、license与静默安装避坑

在 Linux 下装 MATLAB&#xff0c;说难也难&#xff0c;说简单也简单。难在坑多&#xff1a;权限、依赖库、license 文件、启动器路径&#xff0c;哪一步不顺就能卡你一下午&#xff1b;简单在只要把流程理顺&#xff0c;照着命令敲就行。这篇就按我自己的实操经验来讲&#xf…

作者头像 李华
网站建设 2026/10/3 4:17:12

UniApp家具商城开发复盘:微信小程序从登录到分包避坑指南

其实早在立项之前&#xff0c;我就明确知道要做一套基于微信小程序的家具商城系统&#xff0c;技术栈直接锁定 UniApp。这不是拍脑袋的决定&#xff0c;而是对比过原生小程序、Taro、Flutter 小程序容器后做的取舍。UniApp 的跨端能力、Vue 开发体验、生态里成熟的组件库&#…

作者头像 李华
网站建设 2026/10/3 4:16:51

AI工程从零到一:大模型应用开发落地实践指南

看到ai-engineering-from-scratch这个标题&#xff0c;我第一反应是熟悉——这不就是我自己过去一年半走过的路吗。从完全不会写代码&#xff0c;到能独立把一个大模型应用从零搭到上线&#xff0c;中间踩过的坑、推倒重来的代码、凌晨三点还在调评测集的经历&#xff0c;几乎全…

作者头像 李华
网站建设 2026/10/3 4:16:51

NLP工程师实战指南:RoPE优化与Transformer本地部署

1. 这不是论文目录&#xff0c;而是一份NLP研究者的“季度作战地图”如果你点开过arxiv-cs.CL这个分类页面&#xff0c;大概率会陷入一种熟悉的眩晕感&#xff1a;每天新增30篇论文&#xff0c;标题里塞满RoPE、Fused RoPE、MissFormer、LLM-Pruning、MoE-Router、FlashAttenti…

作者头像 李华
网站建设 2026/10/3 4:16:30

Spring Boot共享图书管理系统毕设:从核心设计到答辩避坑全解析

每到毕设季&#xff0c;总有同学拿着“基于Spring Boot的‘图书森林’共享图书管理系统”这种题目来找我&#xff0c;问我好不好做、源码怎么跑、论文怎么写。说实话&#xff0c;这类系统放在今天并不算新&#xff0c;难点从来不是“用Spring Boot写CRUD”&#xff0c;而是你有…

作者头像 李华