尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

虚假新闻检测系统实战:机器学习、深度学习与BERT三条路线解析

虚假新闻检测系统实战:机器学习、深度学习与BERT三条路线解析 简介一份基于机器学习、深度学习与BERT方法的虚假新闻检测项目完整源码属于自然语言处理分类实战适合作为课程设计、期末大作业或毕业设计参考。项目数据来自中文微信消息需根据标题文本判断真假并输出精确率、召回率、F1分数和AUC等评估指标。压缩包共七十六个文件含四十五个Python脚本、十四个XML工程配置、Jupyter Notebook示例及说明文档整体大小仅一百六十三KB结构按机器学习、深度学习、BERT三个模块划分。传统机器学习部分覆盖词袋模型与TF-IDF两种向量化方式配合朴素贝叶斯、支持向量机、随机森林和逻辑回归支持网格搜索与交叉验证调参深度学习与BERT部分提供独立训练评估脚本便于对比不同技术路线的分类效果。已有一千一百三十八人学习浏览代码可直接运行适合有一定Python基础并想深入理解NLP建模流程的学习者二次开发。1. 虚假新闻检测项目为什么值得复制一套代码三条路线有人拿着这份源码包问我的时候我一般会先问一句你是要交作业还是要真做一个能用的虚假新闻检测系统这两个目标差很远。前者跑通一个 BERT 微调脚本就够后者得把机器学习、深度学习和 BERT 三条路线摆在一起对比着选还要处理数据泄漏、类别不均衡和上线的长尾误报。这个项目的价值在于把三条路线都搭好了你可以先复现再替换成自己的数据。它适合两类人一类是刚入门 NLP、想看文本分类完整流程的开发者另一类是做内容审核、舆情监测的从业者需要一个能落地的基线系统。2. 先定数据与标签训练集怎么选测试集怎么划2.1 标签要怎么定真假二分类没有想象中简单虚假新闻检测的标签体系网上能搜到好几种。最常见的是真假二分类也就是 0 和 11 表示虚假。但实际项目里我见过三分类和四分类的做法真实、部分虚假、完全虚假再加一个“无法判断”。部分虚假是最难标的一条新闻可能只有某一段数据造假其余都是真的。众包标注的主观性也很大同一条新闻在不同标注者手里可能得到两个标签。如果直接二分类会把这种噪声放大训练出来的模型会在边界样本上表现得很“倔”给谁都不服。所以拿到源码包之后第一件事不是打开模型文件而是看一眼它默认的标签结构是不是适合你的场景。LIAR 数据集的标签有六档 pants-fire、false、barely-true、half-true、mostly-true、true这种细粒度标签可以后来合并成 0/1但合并的时候要想想边界放哪边。是“barely-true”算假还是“half-true”算假这直接决定数据集量和模型能力的天花板。2.2 数据集怎么选公开数据集对比与适用场景数据集规模与语言标签类型适合做什么Kaggle Fake News约 2 万条英文二分类真/假快速跑通流程、对比模型LIAR约 1.28 万条短句英文六档谎言量表细粒度标注研究FakeNewsNet政治领域为主英文真假标签 源信息带传播路径的研究自建采集数据取决于预算自定义上线前必须做的事我的建议是复现阶段用 Kaggle Fake News因为字段干净、干扰少研究阶段用 LIAR细粒度标签能测试模型对程度差异的敏感度真要上线一定要自建一套领域数据。泛新闻领域的模型换到垂直行业比如金融、医疗会明显掉点这不是玄学是词表分布变了。BERT 类模型虽然迁移能力强但领域特征比如医疗术语、财报措辞还是得靠领域数据去拉回来。2.3 划分数据集时间切分布局优先于随机切分很多教程直接用train_test_split随机打乱这在虚假新闻检测里是个隐患。新闻有强时间属性今天的模型去预测昨天的新闻还行但去预测明天的新闻才是真实业务场景。随机切分会让模型“偷看”到未来数据线下指标很好看上线后立刻翻车。我一般按发布时间排序前 80% 做训练后 20% 做测试验证集从训练集尾部切。import pandas as pd df pd.read_csv(news.csv, parse_dates[publish_date]) df df.sort_values(publish_date).reset_index(dropTrue) train_size int(len(df) * 0.8) train_df df.iloc[:train_size] test_df df.iloc[train_size:] # 从训练集尾部再切出验证集模拟最近一周的数据做调参 valid_size int(len(train_df) * 0.1) valid_df train_df.iloc[-valid_size:] train_df train_df.iloc[:-valid_size] print(ftrain{len(train_df)}, valid{len(valid_df)}, test{len(test_df)})这段代码的关键在于先按时间排序再切分。注意pandas的parse_dates参数要确保日期列解析成时间类型否则字符串排序会出错。还有一个细节验证集要从训练集尾部切而不是从训练集头部或中间切这样调参时看到的验证分布才接近线上场景——线上遇到的都是“未来”的新闻。如果数据里没有明确的发布日期字段就用抓取时间再不行才考虑随机切分。2.4 数据清洗与字段拼接清洗必须在划分之后清洗策略会影响模型输入而任何基于全量数据的清洗都会引入泄漏。最常见的错误是先对整份数据做 TF-IDF 词表统计或去掉低频词再划分训练和测试。这样做会让测试集的信息通过词表渗透进训练过程线下 F1 会虚高。安全做法是先按上一节做完时间切分然后只在训练集上拟合计数的词表、TF-IDF 的 IDF 值。文本字段的拼接也值得注意。很多新闻标题和正文分属不同列BERT 输入需要拼成一个序列常见做法是title [SEP] body。但要注意BERT 有 512 token 上限新闻正文动不动超长直接拼接会被截断。我常用的策略是标题永远保留正文按 token 数从两头截取保证开头、结尾都在。开头有导语结尾有结论这是新闻文体的特征。def build_input(title, body, max_len512, split_ratio0.4): title title.strip() body body.strip() # 按 token 估算中文按字英文按词 title_len min(int(max_len * split_ratio), len(title)) body_len max_len - title_len - 1 # 减去 [SEP] title_part title[:title_len] body_part body[: body_len // 2] body[-body_len // 2:] return title_part [SEP] body_partsplit_ratio控制标题在输入里占多少比例我一般设 0.4因为标题往往包含事件主体和态度倾向。正文截取哪一半也要看领域政治谣言往往结论在文末所以头尾都保留比较稳妥。这个拼接函数可以复用到机器学习的 TF-IDF 路线和 BERT 路线保证两套模型的输入口径一致不然后面做模型融合时特征根本对不上。3. 机器学习基线TF-IDF 逻辑回归先拿到 90% 的分数3.1 为什么要先跑机器学习路线而不是直接上 BERT拿到数据先跑 BERT 是新手最容易犯的错。BERT 微调一次要几分钟到几十分钟调参空间又大跑完一个版本发现数据有问题改完又要重跑一遍时间全砸在等待上。机器学习基线的价值是快速验证数据、标签和评估流程几十秒出结果。周志华《机器学习》里讲的偏差-方差分解思路在这里很适用先用一个高偏差低方差的模型探底把数据和评估流程搞对再上复杂模型。另一个现实因素是资源。BERT-base 有 1.1 亿参数推理一次要算力如果做实时接口又扛不住高并发最后可能还是得用机器学习模型兜底。我做过一个内容审核接口上线用的是 TF-IDF 逻辑回归因为 QPS 要顶到几千BERT 得走 GPU 批处理延迟和成本都压不下来。所以这条基线不是用来陪跑的它本身就可能成为线上的主力。3.2 TF-IDF 逻辑回归一套能直接跑的 Pipeline机器学习路线的核心是特征抽取和分类器。TF-IDF 把文本变成稀疏向量逻辑回归在这个稀疏高维空间上表现稳定而且输出概率可以直接用来排序和调阈值。TF-IDF 的 n-gram 范围、词表大小、是否去停用词这些参数直接决定模型能看到的“词汇现象”。只抽单词ngram_range(1,1)会漏掉“不是真的”这种否定表达抽到三元组(1,3)又会把特征维度拉得很高。我一般从(1,2)开始试。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline model Pipeline([ (tfidf, TfidfVectorizer( ngram_range(1, 2), max_features50000, min_df2, max_df0.95, sublinear_tfTrue )), (clf, LogisticRegression( C1.0, class_weightbalanced, max_iter1000, solverliblinear )) ]) model.fit(train_df[text], train_df[label]) y_prob model.predict_proba(test_df[text])[:, 1]这里Pipeline保证 TF-IDF 的词表只在fit时构建测试集不会污染训练。min_df2表示至少在 2 条文本里出现过的词才保留过滤掉拼写错误和超低频噪声max_df0.95去掉在 95% 文本里都出现的词这些词通常是停用词级别的对区分真假没帮助。sublinear_tfTrue把词频做对数缩放防止长新闻因为词多而占便宜。分类器用class_weightbalanced是因为虚假新闻数据集经常正负样本比例失衡这个参数自动按类别频率加权。solverliblinear适合稀疏高维特征C默认 1.0如果验证集过拟合就调小到 0.5 或 0.3。3.3 评估指标准确率会骗人F1 和混淆矩阵才是真相二分类任务最容易盯着准确率看但虚假新闻数据集里如果 80% 是真实新闻模型全预测“真实”也有 80% 准确率看起来很美实则什么都没学到。必须看 F1 分数、精确率和召回率。该检测的场景漏报一条假新闻比误报一条真新闻后果严重得多所以业务上往往要求召回率高哪怕牺牲一点精确率。from sklearn.metrics import classification_report, confusion_matrix y_pred (y_prob 0.5).astype(int) print(classification_report(test_df[label], y_pred, target_names[real, fake])) print(confusion_matrix(test_df[label], y_pred))如果只看classification_report里的F1在 0.9 左右先别高兴去翻confusion_matrix。看到假新闻这一类召回率低说明模型偏向预测“真实”。这就是class_weightbalanced还要叠加阈值调整的原因——类别权重只是训练时的事推理时的判定阈值是另一回事。我会在验证集上遍历 0.3 到 0.7 的阈值找 F1 最大值对应的点。3.4 提高机器学习基线的几个手工特征TF-IDF 只捕捉词汇捕捉不到语气和写作风格这类信息对识别虚假新闻很关键。虚假新闻标题常用惊叹号、全大写、夸张数字正文里情感极端词占比高引用来源不明确。这些特征我一般通过拼接向量加进去。做法是先抽取手工特征标准化之后跟 TF-IDF 的稀疏矩阵水平拼接再喂给逻辑回归。注意手工特征的统计比如均值、标准差同样只能在训练集上计算否则又是一次泄漏。标题长度、正文长度、标题/正文字数比感叹号和问号数量全大写单词占比英文场景数字出现次数夸张数字如“100%”“一夜暴富”情感极性强弱用现成的词典打正负分把这份增强后的管线在验证集上跟纯 TF-IDF 对比F1 一般能涨 1 到 3 个点。我的经验是如果这个差异小于 1 个点说明数据集里真假新闻的词汇差异已经不大了这时候继续堆特征是浪费时间该考虑深度学习路线。4. 深度学习与 BERT 微调从 TextCNN 到 BertForSequenceClassification4.1 词向量怎么选从 word2vec 到预训练模型深度学习的起点是词向量。虚假新闻检测里用 word2vec 或 fastText 做平均池化拿句子向量的做法效果其实不如 TF-IDF因为平均池化把词序信息和关键词权重全抹平了。《动手深度学习》里讲过一个观点简单模型被复杂模型超越的前提是足够的数据语料不够时预训练才是更靠谱的路径。BERT 的优势在于它预训练阶段见过海量文本知道“据知情人士透露”这种表达的语境含义这些很难靠自己的小语料学出来。所以现在的常见做法是直接跳过了 word2vec 这一站。除非你的数据非常特殊比如某行业术语占比极高需要自己训练领域词向量否则用 BERT 预训练权重是性价比最高的选择。BERT 参数下载这块常见做法是从 Hugging Face Hub 拉取国内可以从镜像站下载模型文件大约 400MB 上下下载后会自动缓存到本地目录后面离线也能用。4.2 TextCNN理解卷积核尺寸和池化的意义TextCNN 是深度学习路线里最容易理解和调优的模型。它的思路是把句子看作一个“二维图像”用多个尺寸的卷积核提取 n-gram 特征。卷积核高度 3 对应三元组高度 5 对应五元组多个高度并行相当于同时看不同粒度的词组。import torch import torch.nn as nn import torch.nn.functional as F class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim300, num_filters128): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv2d(1, num_filters, (k, embed_dim)) for k in [2, 3, 4] ]) self.dropout nn.Dropout(0.5) self.fc nn.Linear(num_filters * 3, 2) def forward(self, x): x self.embedding(x) # (batch, seq_len, embed_dim) x x.unsqueeze(1) # (batch, 1, seq_len, embed_dim) pooled [] for conv in self.convs: out conv(x).squeeze(3) # (batch, num_filters, seq_len - k 1) out F.relu(out) pooled.append(F.max_pool1d(out, out.size(2)).squeeze(2)) x torch.cat(pooled, dim1) x self.dropout(x) return self.fc(x)代码里卷积核宽度等于embed_dim这样可以沿着词向量维度做完整卷积输出每个位置的 n-gram 响应。max_pool1d取每个特征图的最大值保留最强的信号。num_filters128表示每种 n-gram 尺寸用 128 个卷积核这个值太小学不到特征太大容易过拟合。全连接前加Dropout(0.5)是防止过拟合的关键尤其数据量只有一两万条的时候。这些原理懂了调起参来就有方向不然就是瞎试。4.3 BERT 微调最小可运行的训练脚本BERT 微调是最消耗时间也是效果最好的环节。这里用transformers库加载预训练权重直接接一个分类头。微调的核心是预训练权重已经学到了通用语言知识我们要保留它只能小幅调整学习率要小迭代轮次要少。from transformers import BertTokenizer, BertForSequenceClassification, get_linear_schedule_with_warmup import torch tokenizer BertTokenizer.from_pretrained(bert-base-chinese) model BertForSequenceClassification.from_pretrained(bert-base-chinese, num_labels2) model.train() optimizer torch.optim.AdamW(model.parameters(), lr2e-5, weight_decay0.01) total_steps len(train_loader) * epochs scheduler get_linear_schedule_with_warmup(optimizer, num_warmup_stepsint(total_steps * 0.1), num_training_stepstotal_steps) for epoch in range(3): for batch in train_loader: outputs model(**batch) loss outputs.loss loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() scheduler.step() optimizer.zero_grad()lr2e-5是一个安全起点BERT 微调的学习率超过 5e-5 很容易让权重崩溃loss 直接跳到天文数字。weight_decay0.01只对非 bias 和 LayerNorm 参数生效AdamW 就是这么设计的不要省。clip_grad_norm_把梯度范数限制在 1.0防止长文本样本产生异常梯度。warmup_steps设为总步数的 10%让模型在小学习率下先跑热再进入稳定区间。epochs 设为 3超过 4 个 epoch验证集 F1 往往开始下滑这是过拟合的典型信号。从“bert 参数下载”到微调这一套流程跑完大概十几分钟比从头训 word2vec 快得多。4.4 长文本和显存不够两个绕不开的 BERT 问题BERT 的最大输入长度是 512 token新闻正文动不动一两千字直接截断会丢信息。除了前文的build_input策略还有一个办法是滑窗把正文切成多个片段每个片段分别过 BERT再把输出的[CLS]向量做平均或取最大。这个做法会在推理时增加几十倍计算量我一般只在段落明显表达不同主题时用。另一个常见做法是用长文本模型替换 BERT-base但显存和推理时间又会翻倍。显存不够是另一个高频翻车现场。batch size 设 32 在 BERT 上会直接显存溢出OOM常见做法是从 8 或 16 开始试。还不行就开梯度累积每 4 个 batch 累积一次梯度再更新相当于把 batch size 扩大了但不会增加显存占用。这里有个坑梯度累积后要同步调整学习率batch size 翻倍学习率也可以相应调高一点点但不能直接翻倍。5. 避坑数据泄漏、截断不一致、阈值乱调我踩过的四个坑5.1 数据泄漏线下 F1 0.94上线直接掉到 0.8现象模型在测试集上表现极好换了新数据立刻垮掉。原因清洗和特征构建用了全量数据测试集信息提前泄漏给模型。最典型的是先做全量 TF-IDF、全量去低频词然后再切分数据。解决先按时间切分再在训练集上做所有拟合操作。验证集也只能用来调参不能用来做任何特征拟合。这条我从一开始就写了代码但你得检查管线里是不是所有fit都只发生在训练集上。5.2 类别不均衡准确率 85%假新闻召回率只有 30%现象accuracy还不错一查classification_reportfake 这一类召回率极低。原因数据里真实新闻占大头模型学到“全猜真实”损失最小。解决用class_weightbalanced再配合阈值搜索。具体做法是我在验证集上遍历 0.3 到 0.7 的判定阈值画 P-R 曲线找业务可接受的点——我们这边宁可多拦截不能漏掉假新闻。5.3 训练和推理的文本预处理不一致现象线下验证 F1 0.9上线后线上接口效果只有 0.7排查发现是同一个模型。原因训练代码里先对标题和正文做了拼接和清洗但线上推理脚本没有执行同样的步骤数据库里字段拼接方式和训练时不一样。解决把build_input这类预处理函数放进同一个模块训练和推理共用不要在两处重复实现。加载模型前先拿几条第训练数据跑一遍预处理比对输出是否一致。5.4 时间分布偏移用旧新闻训练预测新热点就抓瞎现象模型对突发新闻比如新政策的谣言完全没有判断力。原因训练数据里的实体、措辞都是旧领域的新事件的词汇不在词表分布里。解决机器学习路线里模型要定期用新数据重训BERT 路线里可以把新数据做增量微调。我习惯每周或者每两周用本周新增标注数据重新微调一次同时保留一部分旧数据参与训练防止灾难性遗忘。5.5 BERT 训练玄学loss 突然变 NaN现象训练到一半损失变成nan后面全部无效。原因学习率过大、某个 batch 里出现了极端长度的文本或者梯度爆炸。解决先把lr降到 2e-5 以下给max_grad_norm设 1.0这两条能解决 90% 的情况。检查数据里有没有空文本、全标点文本BERT 处理这些输入时容易产生异常梯度。先跑几十步看 loss 曲线再跑完整训练能省很多返工时间。6. 上线前的验证与融合把三类模型拼成一套可用系统到了这一步手里有 TF-IDF 逻辑回归、TextCNN、BERT 三个模型。单独选一个最省事的是 BERT但要上线我的习惯是三者做融合。融合的方式不是投票而是软融合三个模型都输出“是虚假新闻”的概率然后加权平均。权重怎么定在验证集上用网格搜索范围从 0.2 到 0.5 之间固定权重。通常 BERT 权重最大逻辑回归作为兜底TextCNN 是廉价跑的并行基线。阈值调整我放在融合之后。模型融合完输出的概率分布往往偏向中间值不能直接用 0.5 切。我的做法是在验证集上按业务需要对阈值做扫描找到“拦截 90% 的假新闻”时对应的阈值再看这个阈值下的误报率能不能接受。这个流程必须写成脚本固化下来因为数据更新后最优阈值也会变。验证方法上时间切分是底线。真正的上线评估还要做滚动回测把测试集按时间切成多个窗口比如每周一个窗口分别计算 F1看模型的表现在时间维度上是稳定的还是逐渐衰减的。如果衰减明显就说明模型依赖的特征随时间漂移需要重新训练了。最后再花半天时间做错误分析从误报和漏报里各抽 50 条人工看一眼错例。你会发现漏掉的假新闻往往偏长、偏灰色地带误报的真新闻则多是标题夸张的“标题党”。这个观察比任何指标都更能指导下一步改进方向比如要不要单独识别标题党、要不要引入发布来源特征、要不要给不同的新闻分类建不同的模型。我的习惯是每做一个新数据集先做完错误分析再看指标指标只告诉你“差多少”错例才告诉你“差在哪”。希望帮到你。本文还有配套的精品资源点击获取
返回列表