news 2026/10/7 2:57:09

虚假新闻检测源码实战:TF-IDF到BERT三级技术栈解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
虚假新闻检测源码实战:TF-IDF到BERT三级技术栈解析

简介:一份整合机器学习、深度学习与BERT模型的虚假新闻检测项目源码,面向自然语言处理文本分类任务,适用于计算机、电子信息、数学等专业学生的课程设计、期末大作业或毕业设计参考。项目源自南开大学Python语言程序设计课程,以中文微信消息标题为训练数据,系统对比了朴素贝叶斯、支持向量机、随机森林、逻辑回归等传统算法与神经网络、BERT微调方案的性能,并给出准确率、召回率、F1与AUC等评估指标,便于理解不同方法论在文本分类中的差异。压缩包共76个文件,包含45个Python脚本、XML配置、Jupyter Notebook及Markdown说明,整体仅163KB,按机器学习、深度学习、BERT等目录模块组织,并配有README与调试记录,方便对照学习与二次开发。目前已有1138人学习下载,源码可直接运行,适合有一定Python基础并希望深入理解自然语言处理分类与模型调优的读者。

1. 虚假新闻检测项目源码,先建立三级技术栈的全局观

拿到这份基于机器学习+深度学习+bert方法的虚假新闻检测项目源码.zip,先别急着解压点 train。这类源码包我接过不止一个,名字越全,越容易在“能打开”到“能复现”之间翻车:数据标签口径不统一、机器学习基线和 BERT 的结果对不上、显存一上来就爆。这篇笔记把整条链路拆开——先用 TF-IDF 加逻辑回归拿到及格线,再用 TextCNN、BiLSTM 做深度学习阶段的对比,最后微调 BERT 冲上限,每一级都配上代码、参数和退出条件。适合三类人:拿它做毕业设计的新手、要跑比赛 baseline 的队伍、以及公司内容风控想快速冷启动的工程师。

不要指望一个压缩包解压完就能跑出漂亮结果。它给你的是一套可运行的框架,真正耗时间的永远是数据清洗、标签审核和踩坑排查。

2. 数据与预处理:标签口径和文本清洗决定后面所有模型的上限

2.1 数据集选型:别让模型在脏标签上学“假动作”

做虚假新闻检测,模型结构再花哨,数据不行全都白搭。业界有个不成文的说法:数据决定了模型的上限,模型只是在逼近这个上限。所以拿到源码后,第一件事不是读模型代码,而是搞清楚两个问题:数据从哪里来,标签怎么定义的。

常见做法是直接用公开数据集。中文场景下,微博谣言相关集合、疫情期间的谣言集都是经常被用的;英文场景用 FakeNewsNet、LIAR 这一类。选数据集时我一般卡三个硬指标:数据量至少一万条起步,太少的话 BERT 微调根本学不出泛化能力;标签必须是明确的真假二分类或多分类,而不是“可转发”“已辟谣”这种含混口径;数据要有时间跨度,否则你没法做按时间切割的验证集。

这里有个很容易被忽略的坑:划分训练集和验证集时,不要随机切,要按发布时间切。新闻的特点是同一个事件会衍生出一堆改写稿,随机切很容易让同一事件的不同报道同时出现在训练集和验证集里,验证分数会虚高得离谱。按时间切虽然会让验证集更难(模型没见过新事件),但那才是真实上线后的情况。

2.2 文本清洗与去重:一个脚本清掉八成脏数据

源码包里一般会有一个 preprocess 或 clean 之类的脚本,没有的话自己写一个也就几十行。我会在清洗阶段做三件事:统一列名、去空白与特殊符号、按文本相似度去重。第一件事特别现实,因为不同的数据集源的列名五花八门,有的叫 label,有的叫 tag,有的叫 is_fake,统一成 label 能省掉后面所有模型脚本的麻烦。

import pandas as pd import re import hashlib def norm_text(text: str) -> str: """归一化:去空白、去特殊符号、统一小写""" if not isinstance(text, str): return "" text = re.sub(r"[\s\u3000]+", "", text) text = re.sub(r"[^\w\u4e00-\u9fa5]", "", text) return text.lower() def text_hash(text: str) -> str: """基于归一化文本的哈希,用于去重""" norm = norm_text(text) return hashlib.md5(norm.encode("utf-8")).hexdigest() def load_and_deduplicate(csv_path: str): df = pd.read_csv(csv_path) # 列名统一:源码里可能是 label/tag/is_fake,统一下 label_col = [c for c in df.columns if c.lower() in ("label", "tag", "is_fake", "labels")] if not label_col: raise KeyError("找不到标签列,请检查数据集的列名") df["label"] = df[label_col[0]] # 去掉过短的文本,短文本对真假判断没有信息量 df = df[df["text"].map(lambda x: len(norm_text(x)) > 30)].copy() # 哈希去重:同一事件改写稿只保留一条 df["_hash"] = df["text"].map(text_hash) df = df.drop_duplicates(subset="_hash", keep="first").drop(columns="_hash") # 标签统一为 0/1:0 真,1 假 df["label"] = df["label"].map(lambda x: 1 if str(x) in ("1", "真", "fake", "True") else 0) return df[["text", "label"]]

这个脚本的逻辑很简单,但参数值得说两句。len(norm_text(x)) > 30是过滤短文本,这个阈值可以按你的数据调,如果新闻源本身就有很多短消息,降到 20 也行。哈希去重有个特点:它只去“完全一样”的文本,改写稿改两个字就对不上了。想抓近似重复,可以用 SimHash 或 MinHash,但效果往往不值得它引入的复杂度,我先用精确 hash 兜底。

2.3 跑通预处理后,先看分布再动手训练

清洗完,不要急着进模型阶段。先花两分钟看类别分布和文本长度分布,这两个数字几乎决定了后面所有的模型选型。

python -c " import pandas as pd df = pd.read_csv('clean_data.csv') print(df['label'].value_counts(normalize=True)) print(df['text'].str.len().describe()) "

类别分布的意义在于:如果假新闻只占 5%,那么无脑预测“真”就有 95% 准确率,后面所有模型的评估指标都得围着少数类转。文本长度分布的意义在于:如果大部分文本在 200 字以内,TextCNN 这种短文本模型就够用;如果动辄上千字,BERT 的输入截断策略就要单独设计。这一步跑完,你应该已经能预判哪个模型会出什么效果了。我一般会在笔记本上记下这三行输出,作为后面每一轮实验的对标基准。

3. 机器学习基线:TF-IDF + 逻辑回归,先拿到一个能对比的及格线

3.1 为什么先跑基线而不是直接上 BERT

很多人拿到这种源码包,习惯性地跳过机器学习部分,直接冲 BERT,理由是“BERT 效果最好”。这是典型的把手段当目标。BERT 是效果好,但它是个黑匣子,训练慢、显存贵、出了问题很难定位。而 TF-IDF 加逻辑回归跑一遍只要几秒钟,你得到的不是一个最终方案,而是一条参照系:如果后面 TextCNN 和 BERT 连逻辑回归都没跑赢,那不是模型的问题,是数据或训练流程出了问题。

这也是一条完整的机器学习应用流程:先定评估口径,再选候选模型,最后对比筛选。作为一线工程师,我几乎不会在没有基线的情况下直接上大模型。逻辑回归给出的概率是校准过的(相对而言),它还能直接输出每个词对决策的贡献权重,这在排查标签噪声时特别有用——如果模型对某个词给了极高权重,你去看那些样本,十有八九是标注错了。

3.2 本地跑通 TF-IDF + 逻辑回归的最小代码

把这套代码用起来,不需要 GPU,一台普通笔记本就够了。核心就三段:文本向量化、训练、交叉验证。

import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.model_selection import StratifiedKFold from sklearn.metrics import classification_report, f1_score df = pd.read_csv("clean_data.csv") X, y = df["text"], df["label"] # 文本向量化:词频 -> TF-IDF vectorizer = TfidfVectorizer( max_features=100000, # 词表上限,防过拟合 ngram_range=(1, 2), # 保留相邻词组合,能抓住“不实”“谣言”这种短语 sublinear_tf=True, # 用 1+log(tf) 平滑词频,削弱高频词 min_df=2, # 至少在 2 篇文档出现过的词才保留 stop_words=[] # 不单独做停用词表,TF-IDF 本身会降权高频词 ) # 逻辑回归:C 越小正则越强,balanced 自动调类别权重 clf = LogisticRegression(C=1.0, class_weight="balanced", max_iter=1000, n_jobs=-1) # 5 折交叉验证 skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) macro_f1s = [] for train_idx, val_idx in skf.split(X, y): X_tr, X_val = X.iloc[train_idx], X.iloc[val_idx] y_tr, y_val = y.iloc[train_idx], y.iloc[val_idx] X_vec = vectorizer.fit_transform(X_tr) X_val_vec = vectorizer.transform(X_val) clf.fit(X_vec, y_tr) y_pred = clf.predict(X_val_vec) macro_f1s.append(f1_score(y_val, y_pred, average="macro")) print(classification_report(y_val, y_pred, digits=3)) print("Macro-F1:", sum(macro_f1s) / len(macro_f1s))

这段代码里的参数都不是玄学,每个都有明确目的。ngram_range=(1, 2)单独用 unigram 抓不到“毫无根据”这种包含否定词的短语,加上 bigram 就能把“不实消息”保留成一个特征。sublinear_tf=True是为了削弱“的”“了”这种高频无意义词的绝对数量优势,比硬砍停用词表更稳。class_weight="balanced"处理类别不平衡的方式是给少数类更高的错分代价,比下采样省事,数学上也等价于给损失函数加权。

3.3 别只盯着 accuracy:混淆矩阵和 F1 才是紧要的

用这份源码的人在评估阶段最容易犯的错就是只看 accuracy。假设数据集里真新闻占 95%,模型全部预测“真”,accuracy 是 95%,但这个模型毫无用处。虚假新闻检测的本质是少数类识别,所以必须看少数类的 precision 和 recall,以及综合两者的 F1。

精确率在意的“我抓出来的假新闻里有几成是真的假新闻”,召回率在意“真正的假新闻里我抓到了几成”。业务侧通常更看重召回率,因为漏掉一条假新闻的代价远大于误伤一条真新闻。这时候分类报告里的 macro-F1 就比 accuracy 可靠得多,它把两个类别同等对待,不会被大头类别带偏。

如果跑完基线发现少数类 F1 很低,先别急着换模型。把预测错误的样本捞出来看,大概率能看到两类问题:一类是标签本身就是错的,一类是这条“假新闻”用词和真新闻没有区别、人类都分不出来。这两类问题,换任何模型都解决不了,必须回数据层修。

4. 深度学习与 BERT:从 TextCNN、BiLSTM 到微调 BERT 的完整链路

4.1 选型原则:文本长度和算力约束决定用哪一级

机器学习阶段看的是“词有哪些”,深度学习阶段看的是“词的顺序和上下文”,BERT 阶段看的是“全文语义关联”。这三个层次不是替代关系,是递进关系。实际选型时,我一般用两条硬约束来卡:第一是文本长度,第二是算力。

如果数据绝大部分在 200 字以内,TextCNN 是性价比之王,训练速度快、参数少、不容易过拟合。如果句子之间有长距离依赖,比如“他先声称 X,三天后又说 Y,但 Z 证明他在撒谎”,BiLSTM 能捕捉这类序列关系。如果这两者都跑完了,还想继续提升效果,再上 BERT。动手深度学习的第一步不是调参,而是先把这三者的边界画清楚,省得后面用 BERT 跑短文本,既浪费算力又提升有限。

4.2 TextCNN 的 PyTorch 实现与关键参数

TextCNN 的思路是把文本看成一行像素,用不同宽度的卷积核去抓局部 n-gram 特征。三个不同尺寸的卷积核并行,相当于同时看 2-gram、3-gram、4-gram 的短语。代码实现很稳定,几乎不用改。

import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim=100, num_filters=256, num_classes=2): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.convs = nn.ModuleList([ nn.Conv1d(embed_dim, num_filters, kernel_size=ks, padding=ks // 2) for ks in (2, 3, 4) # 三种窗口并行 ]) self.dropout = nn.Dropout(0.5) self.fc = nn.Linear(num_filters * 3, num_classes) def forward(self, x): # x: (batch, seq_len) emb = self.embedding(x).transpose(1, 2) # (batch, embed_dim, seq_len) pooled = [torch.max(nn.functional.relu(conv(emb)), dim=2).values for conv in self.convs] out = self.dropout(torch.cat(pooled, dim=1)) return self.fc(out)

这里padding=ks // 2是为了让卷积输出长度不变,避免 maxpool 之后信息丢失;nn.MaxPool用的是全局最大池化,取序列维度上最强的特征;dropout=0.5是 TextCNN 的标准配置,加太低容易过拟合。训练参数我一般给 batch_size=64、Adam 学习率 1e-3、epochs 上限 10 轮,配合早停 patience=3。

TextCNN 的局限性在于它只抓局部短语,抓不到“开头说 A、结尾说 B”这种跨段落的矛盾。这时候就需要 BiLSTM。

4.3 BiLSTM 适合长新闻,但训练更慢

BiLSTM 的核心思路是让网络从头到尾和从尾到头各读一遍文本,然后把两个方向的信息拼起来,这样每个位置的表示都同时包含左邻和右邻的上下文。

class BiLSTM(nn.Module): def __init__(self, vocab_size, embed_dim=100, hidden_size=128, num_layers=2, num_classes=2): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.lstm = nn.LSTM( embed_dim, hidden_size, num_layers=num_layers, bidirectional=True, # 双向就是两份隐藏层 batch_first=True, dropout=0.5 if num_layers > 1 else 0 ) self.fc = nn.Linear(hidden_size * 2, num_classes) # 双向拼接 def forward(self, x): emb = self.embedding(x) # (batch, seq_len, embed_dim) out, (h_n, c_n) = self.lstm(emb) # out: 所有时刻输出 # 对长新闻,mean pooling 比取最后时刻更稳 pooled = torch.mean(out, dim=1) # (batch, hidden*2) return self.fc(pooled)

参数上比较关键的是hidden_size=128和num_layers=2。单层 LSTM 对新闻这种长文本往往欠拟合,三层以上在数据量不够时又容易过拟合。双向拼接意味着最后的特征维度是hidden_size * 2,全连接层的输入维度别写错。取最后时刻的输出是很多教材里的写法,但对长文本不稳定——最后一步的隐状态可能被尾部几个词主导,所以这里用 mean pooling,把全序列的信息平均下来。

代价就是训练速度明显比 TextCNN 慢,尤其在序列长度超过 500 时,LSTM 的循环展开会让反向传播非常耗时。这时候先把序列截断到 300 以内是明智的。

4.4 微调 BERT:模型选型、max_length 与学习率

BERT 阶段是这份源码的压轴戏。中文场景下最常见的做法是加载bert-base-chinese,如果追求更高上限,用哈工大的roberta-wwm-ext,它全词掩码对中文更友好,但模型文件更大,加载更慢。两者在评估指标上的差距通常在 1-2 个点以内,数据量少时这个差距都可以忽略。

BERT 模型实操的关键不是模型结构,而是微调参数。

from transformers import AutoTokenizer, AutoModelForSequenceClassification, AdamW model_name = "bert-base-chinese" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2) # 训练参数按显存来:16G 显卡可以 batch=16、max_length=256 batch_size = 16 max_length = 256 lr = 2e-5 epochs = 4 # tokenize:截断到 max_length,别舍不得 def encode(texts, labels): enc = tokenizer( texts, max_length=max_length, padding="max_length", truncation=True, return_tensors="pt" ) enc["labels"] = torch.tensor(labels) return enc

参数上,learning_rate=2e-5是 BERT 微调的惯例,高于 5e-5 很容易把预训练特征冲掉,俗称“灾难性遗忘”。weight_decay=0.01可以加在 AdamW 里,只对非偏置和层归一化参数生效。warmup比例设 0.1,前 10% 的 step 让学习率从 0 线性爬到 2e-5,避免刚开始训练就大步走崩。

显存不足时,先降max_length而不是先降batch_size。把 512 截到 256,显存占用直接减半,而且新闻这种文体的关键信息通常集中在前 200 字内。截断之后如果 F1 反而更低,再来检查是不是关键信息真的落在后半段。

5. 避坑指南:跑这份源码最容易翻车的 5 个现场

5.1 数据与源码包自身的三个坑

现象:解压后直接跑train.py,立刻报KeyError: 'label'。

原因:数据集的列名不统一。源码里写死的列名是label,但实际数据可能是labels、tag、is_fake,甚至两列都有但含义不同。这是这种打包源码最常见的粗糙点,因为作者自己用的数据集和大家下载到的数据集往往不是同一个。

解决:在数据加载后立刻统一列名,打印前五行确认。不要直接改源码里的列引用,统一在入口处做一次映射,后面所有脚本就都不用动了。

现象:清洗完之后,正负样本比例悬殊到 1:20,模型把整片验证集都预测成“真”。

原因:数据本身类别不平衡,而模型没有感知到这种不平衡。

解决:逻辑回归加class_weight="balanced",深度模型在损失函数里给少数类加权,或者在采样时做正样本过采样。最彻底的办法是在数据收集阶段补充更多假新闻样本,但从源码起步的话,先调类别权重最现实。

现象:BERT 训练到第 3 个 epoch,loss 突然变成 NaN。

原因:学习率太高,或者有个别样本包含极端长串数字/特殊字符,BERT 的 tokenizer 把它切成一堆碎片后,数值不稳定。

解决:先降到 1e-5 试跑,如果还在 NaN,把清洗脚本里的特殊字符过滤加强。千万不要忽略这条,BERT 一旦 NaN,前面两个 epoch 的训练成果全部作废。

5.2 训练和评估阶段的坑

现象:验证集 F1 比训练集还高,高得离谱,比如训练集 0.87、验证集 0.94。

原因:时间泄漏。同一个新闻事件的多篇改写稿,一篇在训练集、一篇在验证集,模型其实是记住了事件而不是学到了真假判别。

解决:回到第 2 章的按时间切分。随机切分看到的高分都是自欺欺人,按时间切以后分数会掉 5-10 个点,那个数字才接近线上真实水平。

现象:整体准确率 95%,但少数类(假新闻)的 recall 只有 18%。

原因:准确率被大类别的 97%正确率撑起来了,少数类几乎没被识别出来。

解决:评估报告只看classification_report里第一行的 precision/recall/f1,以及 macro-F1。如果少数类 F1 低于 60%,这个模型上线等于摆设。另一个技巧是看模型输出概率的分布,假新闻类的平均概率如果低于 0.6,说明模型根本没学会把这类样本和真新闻分开。

6. 把模型从能跑推到能信:坏例复盘与阈值搜索

6.1 先看坏例再调阈值:每个模型都要过这一关

模型训练完,第一件事不是看测试集分数,而是把预测错误的样本全部捞出来,逐条读。这一步能过滤掉一半以上的“虚假高分”。一个阈值搜索脚本就能做这件事:

# 在验证集上导出错误样本,并按模型置信度排序 import pandas as pd val = pd.read_csv("val_data.csv") val["pred"] = preds val["prob"] = probs[:, 1] # 假新闻类的概率 bad = val[val["pred"] != val["label"]].sort_values("prob", ascending=False) print(bad[["text", "label", "pred", "prob"]].head(50)) # 阈值搜索:不固定 0.5,找 F1 最大的阈值 from sklearn.metrics import f1_score best_th, best_f1 = 0.5, 0 for th in [i / 100 for i in range(30, 80)]: yp = (probs[:, 1] >= th).astype(int) f1 = f1_score(val["label"], yp, average="macro") if f1 > best_f1: best_th, best_f1 = th, f1 print("best threshold:", best_th, "macro-F1:", best_f1)

读坏例时有一个经验:如果错误样本里大量出现“某明星微博辟谣”这种明显可以靠关键词判别的文本,说明模型没用上这些特征,回数据层加特征。如果错误样本人类都分不清,那就放过模型,降低预期。阈值搜索是免费的提升手段,但注意要在验证集上调,调完再用测试集确认一次,否则阈值本身也会过拟合。

6.2 再往上走:半监督伪标签与多模型集成

如果坏例复盘做完还想提点,常见做法是用训练好的 BERT 去预测大量无标注新闻,挑置信度高于 0.95 的样本作为伪标签,补进训练集再微调一轮。这个操作对长尾的“新事件句式”有帮助,但也可能把模型自己的偏见放大——只取高置信度就是给模型自己当复读机,所以只适合作为最后一搏。另一个是集成:把 TF-IDF 逻辑回归、TextCNN 和 BERT 三个模型的结果做投票或概率平均,通常能再涨 1-2 个点,代价是推理速度变成原来的三倍。

我自己现在拿到任何一个虚假新闻检测源码,第一件事永远是先跑基线、再存下预测错误的样本,逐条读一遍再谈调参。这个习惯帮我挡掉了太多次“分数好看、上线翻车”的尴尬。希望帮到你。

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

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

普通摄像头实现Windows Hello人脸解锁:零成本软件方案实操

去年年底我把手头一台老笔记本的摄像头重新利用起来,装了个能用的 Windows Hello 人脸解锁。折腾了大概一个周末,从“普通 USB 摄像头根本不被系统认”到每天早上开机刷脸进桌面,中间踩了不少坑。这个 V0.1.1 版本算不上什么成熟产品&#xf…

作者头像 李华
网站建设 2026/10/7 2:55:23

纯前端JavaScript实现的LIMS系统:样品管理到PDF报告全流程

简介:这是一套面向实验室信息化建设人员、高校教学开发者及Java/JavaScript全栈学习者的LIMS系统完整源码,专为市场监督检验所等检测机构定制,解决样品登记、任务分配、报告编制与设备管理等核心业务的数字化协同难题。资源包共922个文件&…

作者头像 李华
网站建设 2026/10/7 2:55:20

SnowNLP情感分析实战:批量处理微博评论与自定义模型优化

简介:基于SnowNLP库的新浪微博评论情感分析工具,面向需要掌握中文文本情感分析的Python开发者、NLP学习者以及课程设计项目人群。工具覆盖新浪微博评论获取、文本预处理、分词、情感得分计算与可视化展示的完整流程,能够判断评论的正负面与中…

作者头像 李华
网站建设 2026/10/7 2:54:25

Unity触摸屏物体识别桌开发:从坐标映射到C#架构

简介:这份资源是一套基于 Unity 引擎与 C# 语言、结合 Lean Touch 插件实现触摸屏物体识别桌的完整项目源码与工程文件,面向需要开发互动式桌面、AR/VR 触控交互的 Unity 开发者。资源以 zip 压缩包形式提供,共 3640 个文件,包含 …

作者头像 李华
网站建设 2026/10/7 2:54:16

Python二手交易网站实战:Django完整MVP搭建指南

简介:这是一套基于Django框架开发的B/S架构二手交易市场网站完整源码,面向Python Web初学者与Django项目实践者,适用于课程设计、毕业设计及中小型电商系统学习场景。资源包含565个文件,主体为86个核心Python业务逻辑文件、26个HT…

作者头像 李华
网站建设 2026/10/7 2:53:51

Spring Boot 与微信小程序搭建药店管理系统:从建表到避坑全流程

简介:这套基于Spring Boot、SSM与uniapp开发的药店管理系统源码,面向Java开发者、小程序学习者以及需要完成课程设计或毕业设计的学生。系统覆盖用户注册登录、退出、密码重置、药品分类与信息管理、收货地址、购物车、留言板、文件上传下载等业务模块&a…

作者头像 李华