多模态虚假新闻检测实战:Python实现图文融合与特征提取
2026/9/23 1:19:36 网站建设 项目流程

简介:面向毕业设计、期末大作业和课程设计的基于Python的虚假新闻检测多模态识别项目,适合具有一定Python基础、希望直接获取可运行完整方案的学习者。资源共39个文件,以16个Python代码文件为主,涵盖BERT模型处理、特征融合、LightGBM与CatBoost多模型结果混合、预测与训练脚本等核心模块;同时包含Markdown与TXT说明文档、Shell运行脚本、TSV数据文件及配置信息,压缩包大小仅353KB,结构清晰,便于快速部署与二次开发。代码注释详细,并经严格调试,可直接作为高分项目参考。目前已有498人学习使用,可帮助读者理解多模态虚假新闻检测的完整流程,也适合在答辩或展示中作为落地成果。

1. 多模态虚假新闻检测:为什么单看文本拦不住谣言

你在刷新闻时看到一张“某地发生爆炸”的现场照片,配文言之凿凿,转发量几十万。大多数人的第一反应是去读那行文字,而事实核查员会先做另一件事:把图片丢进反向搜索。为什么?因为虚假新闻的生产成本正在降低——文本可以用大语言模型批量生成,图片可以用扩散模型伪造,但把两路信息放在一起交叉验证,破绽就会露出来。这就是多模态识别对虚假新闻检测的核心价值:它不只判断“这句话像不像真的”,还要判断“这段话和这张图是不是真的匹配”。

基于Python做这套检测系统,技术栈非常成熟:文本侧用预训练语言模型提取语义向量,图像侧用卷积网络或CLIP提取视觉特征,最后用融合层判断真伪与图文一致性。网上流传的“源代码+文档说明”项目大多是这条路线,区别只在数据集的规模、融合方式的选择和训练细节的打磨。本文不依赖任何特定开源包,从模型选型、特征提取、融合分类到训练参数,梳理出一套可以直接复现的最小可行实现。适合正在做毕业设计、竞赛或内容安全相关工程的读者,对已有单模态检测经验的人,也能在融合层与训练策略上获得可落地的参考。

2. 检测框架与数据准备:先分清模态,再谈融合

2.1 多模态检测任务建模:图文一致性、语义真伪与情感偏离

把“多模态虚假新闻检测”拆成可计算的问题,通常涉及三个子任务。第一个是图文一致性判断:给定一张图片和一段文本,判断它们是否真实相关。虚假新闻最常见的模式就是“旧图新用”或“图文无关”——图片是真的,但与文本描述的事件毫无关系。第二个是单模态语义真伪:仅从文本措辞、夸张程度、信息来源等特征判断可信度;仅从图片的拍摄参数、压缩痕迹、异常拼接等判断是否为伪造。第三个是情感与立场的偏离检测:虚假新闻往往在情感上刻意煽动,而图像内容却可能平静如常,这种“情感温差”本身就是一个信号。

工程实现上,我会把这三个子任务统一到一个多任务学习框架里,共享底层的特征提取器,在任务头分别输出三类分数:

# 多任务输出头示意 import torch.nn as nn class MultiTaskHead(nn.Module): def __init__(self, feat_dim=512, num_classes=2): super().__init__() self.consistency_head = nn.Sequential( nn.Linear(feat_dim, 128), nn.ReLU(), nn.Linear(128, 1)) self.fakeness_head = nn.Sequential( nn.Linear(feat_dim, 128), nn.ReLU(), nn.Linear(128, num_classes)) self.sentiment_head = nn.Linear(feat_dim, 3) # 负向/中立/正向 def forward(self, x): return { "consistency": self.consistency_head(x), "fakeness": self.fakeness_head(x), "sentiment": self.sentiment_head(x), }

多头输出的好处是训练时可以用加权和的方式把三个损失合并,推理时则主要看fakenessconsistency的分数。如果你的项目标题里强调“文档说明”,这一结构往往会被当作架构图放进说明文档,因为它能清楚表达多模态模型的设计边界:哪些模块共享、哪些模块独立。注意num_classes一般设 2(真/假)或 3(真/假/不确定),后者更贴近现实标注难度,但训练时需要更多人工标注成本。

2.2 数据集选择:Fakeddit、微博数据集与自定义采集的三条路线

数据决定了多模态检测的上限。公开可用的英文数据集首选 Fakeddit,它有超过 100 万条样本,按 2-way、3-way、6-way 三种粒度标注,其中 3-way 标注(真/假/争议)最常用,每篇帖文同时包含标题、正文、图片 URL 和评论区。缺点是图片 URL 有大量失效链接,下载后需要做一轮去重和破损过滤。中文场景下可用的开源多模态虚假新闻数据集较少,常见做法是从微博收集带有“辟谣”标签的帖子,用“@微博辟谣”或“谣言”关键词过滤,再人工复核,此路线适合做小规模实验。

我一般会优先做一个约 1 万条样本的“基准训练集”,划分方式如下表格所示,其中验证集与测试集按时间切分而不是随机切分,目的是模拟模型上线后遇到的“未来新闻”。

数集(子集)用途建议规模划分原则
训练集模型参数学习8000时间线上较早的样本
验证集调参与早停1000时间线中段,贴近训练分布
测试集最终指标评估1000时间线最新,避免时间泄漏

时间切分是很多刚上手的人会忽略的点:如果随机划分,模型可能学到“某段时间的帖子整体偏假”这样不稳定的特征,测试指标虚高。数据准备的最后一步是写一个统一的Dataset类,用__getitem__同时返回文本原始字符串、图像路径和标签,后续特征提取只需要在这一层替换即可。

3. 特征提取:文本语义与图像视觉编码

3.1 文本特征提取:从 TF-IDF 到预训练语言模型的取舍

文本模态的技术选型,取决于你手里的计算资源与数据量。两块 RTX 3090 或单块 A100 以下的环境,微调 BERT-base 是性价比最高的方案;数据量少于 3 万条时,不建议直接微调更大的模型如 RoBERTa-large,过拟合风险高且收益有限。我更常用的替代方案是冻结 BERT 的底层参数、只微调最后 4 层,这种做法既保住预训练知识,又显著降低显存占用。

一个不可忽略的细节是文本的截断策略。虚假新闻的文本长度分布极不均匀:标题通常 20 字以内,正文可能长达上千字。BERT 的输入长度上限是 512 token,直接截断会丢失关键证据(造假者常在正文中隐藏可验证的细节)。我用的是“头部优先 + 尾部摘要”策略:取前 400 token 和后 112 token,而非单纯取前 512 token。原因是虚假新闻往往首段煽情、末段呼吁转发,两端信息密度最高。

# 用 transformers 加载 BERT 并实现双端截断 from transformers import BertTokenizer, BertModel import torch tokenizer = BertTokenizer.from_pretrained("bert-base-uncased") bert_model = BertModel.from_pretrained("bert-base-uncased") def encode_text(text: str, max_len=512): tokens = tokenizer.tokenize(text) if len(tokens) <= max_len - 2: # 留出 [CLS] 和 [SEP] return tokenizer(text, max_length=max_len, padding="max_length", truncation=True) head = tokens[:400] tail = tokens[-(max_len - 402):] # 减掉 head 和特殊符号 merged = ["[CLS]"] + head + tail + ["[SEP]"] ids = tokenizer.convert_tokens_to_ids(merged) # 补 padding 到 max_len ids = ids + [tokenizer.pad_token_id] * (max_len - len(ids)) return torch.tensor([ids])

提取特征时,代码里bert_model的输出要取last_hidden_state[:, 0]那个向量,即[CLS]token 的隐状态,它被训练为聚合整个序列的语义。后面的融合层不一定非要接在这一个向量上;如果你发现模型对局部细节不敏感,可以改成取last_hidden_state所有位置的均值池化,或做 max-pooling 与 mean-pooling 的拼接,这两者在短文本场景下经常比[CLS]更稳定。

参数层面的经验值是:学习率 2e-5 到 5e-5,batch size 16 到 32,warmup 比例 0.1。如果你的项目文档里写的是“使用 RoBERTa”,注意它与 BERT 的最大差异在分词器——RoBERTa 使用 Byte-level BPE 且没有 token_type_ids,加载时就要去掉token_type_ids参数,否则会报维度不匹配的 bug。

3.2 图像特征提取:ResNet 预训练权重与 CLIP 的边界在哪

图像侧的老牌方案是用在 ImageNet 上预训练的 ResNet50 做特征提取,输出 2048 维向量。这个方案的优势是成熟稳定,在 CPU 上也能做推理,缺点是对“语义级”的图文匹配不够敏感。举个具体例子:一张真实的火灾现场照片,配文“某市化工园区发生爆炸”,ResNet 可以识别出“火”和“建筑”,但很难判断“这个建筑是否位于某市化工园区”。

近两年的实际工程项目更多转向 CLIP 及其变体。CLIP 将文本与图像映射到同一个语义空间,直接产出一个相似度分数,天然适合检测图文错配。在我搭建的检测流程里,CLIP 有两处用法:一是替代 ResNet 作为图像特征提取器,取 image encoder 输出的 512 维向量;二是与本文的 BERT 文本向量做 cosine 相似度,单独计算一个“图文匹配分数”,作为特征拼接到融合层。第二种用法更稳,因为你保留了 BERT 的语义表征,同时引入跨模态对齐信号。

import clip from PIL import Image device = "cuda" if torch.cuda.is_available() else "cpu" clip_model, preprocess = clip.load("ViT-B/32", device=device) def extract_image_feature(image_path: str): image = Image.open(image_path).convert("RGB") arr = preprocess(image).unsqueeze(0).to(device) with torch.no_grad(): feat = clip_model.encode_image(arr) # shape: [1, 512] feat /= feat.norm(dim=-1, keepdim=True) return feat

norm归一化这一步不要省。CLIP 预训练时是用余弦相似度对齐图文对的,向量不归一化会导致相似度分布偏移。ViT-B/32ViT-L/14的选择逻辑是:后者精度高出约 4 到 6 个点,但显存需求翻倍,输入分辨率也从 224 提升到 336(ViT-L/14@336px)。训练阶段建议用较小的 ViT-B/32 把流程跑通,最后做精度收尾时再换大模型。

3.3 图文对齐的坑:图片下载失败、损坏与重复样本的清洗

多模态项目里最容易被低估的是数据清洗的时间占比。真实数据集中,“图片 URL 已失效”、“图片只有 1×1 白底”、“图片被二次压缩导致 EXIF 信息丢失”这三类问题占比可以达到总样本数的 20%。部署下载脚本时,我用opencv-python做三重校验:文件头是否为JPEG/PNG/WEBP之一、cv2.imread是否返回非空数组、图像尺寸是否大于 64×64。校验不通过的样本直接丢弃,而不是保留为空图。

重复图片是另一个隐蔽问题,未处理的重复项会让验证集指标虚高。计算所有图片的感知哈希(pHash),汉明距离小于 8 即视为近似重复,只保留最早发布的那一条。但要注意,这条规则的副作用是会把“同图不同文的谣言样本”全部去重,而这些样本恰恰是多模态检测的重要训练信号。我采取的折中方案是:重复图片只删除标签完全一致的样本,若标签不一致则全部保留并记录“该组图片被多次复用”作为辅助特征。

4. 模态融合与分类器训练:交叉注意力是性价比最高的融合方式

4.1 三种融合策略对比:早融合、晚融合与交叉注意力

多模态融合的选型直接决定模型上限。早融合(Early Fusion)将文本和图像向量拼接后直接送入分类器,实现最简单,但也最容易出现模态主导问题——实验里常见的是文本特征强时模型完全忽略图像。晚融合(Late Fusion)分别对每个模态训练一个分类器,最后加权平均分数,稳定性好但忽略了模态间的交互信息。交叉注意力(Cross-Attention)让文本向量作为 Query、图像向量作为 Key/Value,在深度融合中可以动态决定“关注图像的哪个区域”,是目前性价比最高的方案。

实际工程里我通常的做法是三条路并行:先训练早融合和晚融合两个模型作为 baseline,再看交叉注意力版在验证集上是否能稳定提升 2 个点以上。如果提升不显著,项目只追求上线可用,那我保留晚融合的简单方案;如果目标是竞赛名次或论文实验,则必须上交叉注意力。

# 单层交叉注意力实现 import torch.nn as nn import math class CrossAttention(nn.Module): def __init__(self, d_model=512, num_heads=8): super().__init__() self.q_proj = nn.Linear(d_model, d_model) self.kv_proj = nn.Linear(d_model, d_model * 2) self.out_proj = nn.Linear(d_model, d_model) self.num_heads = num_heads self.dim_per_head = d_model // num_heads def forward(self, text_feat, image_feat): # text_feat: [B, L_text, D], image_feat: [B, L_img, D] batch = text_feat.size(0) Q = self.q_proj(text_feat).view(batch, -1, self.num_heads, self.dim_per_head).transpose(1, 2) KV = self.kv_proj(image_feat).view(batch, -1, 2, self.num_heads, self.dim_per_head) K, V = KV[:, :, 0].transpose(1, 2), KV[:, :, 1].transpose(1, 2) scores = torch.matmul(Q, K.transpose(-1, -2)) / math.sqrt(self.dim_per_head) attn = torch.softmax(scores, dim=-1) out = torch.matmul(attn, V).transpose(1, 2).contiguous().view(batch, -1, self.dim_per_head * self.num_heads) return self.out_proj(out)

上边的CrossAttention关键在Q来自文本、K/V来自图像,这会让最终的输出向量带上“文本主动去图像里找证据”的倾向。如果调换过来(图像作为 Query、文本作为 K/V),模型捕捉到的就是“图像内容如何被文本解释”,两个方向的信息不同,建议都试一下,然后在验证集上选更优的。融合层的最终输出向量通常接一个带 Dropout 的 MLP 分类器,Dropout 比例 0.3 起步。

4.2 训练循环中的关键参数:学习率分层、梯度裁剪与早停策略

融合模型训练与纯文本或纯图像模型有一个显著区别:收敛速度快的预训练模型部分(如 BERT 底层)和随机初始化的融合层在梯度量级上差距巨大。不对学习率做分层处理的效果是:要么融合层训不动,要么 BERT 的参数被破坏导致灾难性遗忘。我用transformersget_linear_schedule_with_warmup加上手动参数分组解决这个问题。

# 分层学习率设置:BERT 低学习率、融合层高学习率 from transformers import AdamW, get_linear_schedule_with_warmup def configure_optimizer(model, bert_params, fusion_params): optimizer = AdamW([ {"params": bert_params, "lr": 2e-5}, {"params": fusion_params, "lr": 1e-4}, ], weight_decay=0.01) scheduler = get_linear_schedule_with_warmup( optimizer, num_warmup_steps=200, num_training_steps=total_steps) return optimizer, scheduler

total_steps的计算方式是训练轮数乘以每轮 batch 数,其中 warmup 比例取 0.05 到 0.1。梯度裁剪的最大范数设置为 1.0,防止单条异常样本把整个损失函数顶穿。早停我习惯只看验证集的F1分数而不是lossacc,因为虚假新闻检测场景中类别不平衡比想象中严重,即使训练集做了重采样,验证集上的准确率也可能被多数类主导。

超参数推荐值调整方向
BERT 层峰值学习率2e-5过拟合时降至 1e-5
融合层峰值学习率1e-4训练慢时升至 2e-4,不可更高
梯度裁剪 max_norm1.0损失震荡时降至 0.5
Dropout(融合层)0.3训练集 F1 高而验证集低时升至 0.5
早停 patience3 个 epoch验证集 F1 连续不升即停止

4.3 多任务损失组合,避免一致性分数“淹没”真伪判断

三个任务头的损失合并方式容易踩坑:consistency输出是单数值、fakeness是分类、sentiment是三分类,损失的数值范围天然不在一个量级。如果简单相加,数值大的那个任务主导梯度更新。经验做法是给每个损失加一个可学习的权重或手工设定的固定权重,我一般用固定权重起步:loss = 0.6 * fakeness_loss + 0.3 * consistency_loss + 0.1 * sentiment_loss

另外,consistency标签的可靠性依赖原始数据的标注方式。Fakeddit 这类数据集的图文一致性标签并不总是人工标注的,很多来自规则推导(比如判断图片是否出现在正文链接里),噪声较大。因此给一致性任务设置更低的权重是合理的。若发现验证集中“图文无关却被标为真实”的样本过多,可以将consistency权重进一步下调到 0.2,以fakeness为主任务。

5. 排错与进阶:证明多模态确实有用,而不只是“能跑”

5.1 结果不涨的常见原因:先查数据泄漏,再查模态主导

验证集指标卡住不动时,我大部分时间花在数据泄漏排查上。多模态项目中最隐蔽的泄漏是图片层面的重复:同一张图片可能既出现在训练集又出现在测试集,模型实际记住的是图片的“身份”而非任务规律。除了训练开始前做 pHash 去重外,验证集的构建应该按图片去重而非按文本去重。另一个泄漏点是文本中的转发链接含时间戳,如果测试集时间更新,时间戳特征可以直接泄漏标签,需要做预处理时剔除 URL 参数。

模态主导问题是“模型没坏但学偏”的典型表现。排查方法是:在测试集上分别计算纯文本模型的准确率与纯图像模型的准确率,再对比多模态模型。如果多模态模型的准确率低于单模态模型的最高值,多半是融合策略不当或学习率设置不合理,而不是数据太少。这时可以先固定特征提取器,只训练融合层,等融合层收敛后再解冻全部参数微调。

5.2 做一次消融实验:把多模态模型的得分拆开看

反驳“多模态对这项任务没用”的唯一方式是用消融实验证明每个模态都有贡献。最小化可用的消融矩阵如下:完整模型(文本 + 图像 + 融合)、仅文本(丢弃图像输入,文本特征直接接分类头)、仅图像(对称操作)、文本 + 图像简单拼接(无交叉注意力)。四组实验使用相同的种子、相同的预训练权重和相同的数据划分,运行 5 次取平均。如果文本加图像的拼接已经显著优于单模态,说明两个模态的信息互补;如果交叉注意力进一步优于拼接,说明跨模态交互确实被有效利用。

5.3 进阶技巧:用对抗扰动测试模型鲁棒性,顺便爆出一个需求

虚假新闻的制造者会在图片上叠加噪声水印、对文字做同义改写来规避检测。用对抗训练提升鲁棒性有两种落地方式:对文本向量做 FGSM 扰动(向损失增大的方向移动一个小步长),对图像做随机透视变换与色彩扰动,作为数据增强而非针对性攻击。实验表明,这两种增强的组合能让模型在对抗样本上的 F1 提升约 10 个点,而干净样本上的 F1 仅下降约 1 个点,算是一笔划算的交易。

最后提一个容易忽略的文档化习惯:把模型每一层的输入输出张量形状写进说明文档,而不是只贴架构图。排查“维度对不上”类型错误时,一份记录了[batch, 512][batch, 8, 64]流转过程的文档价值,远高于一行一行的注释。照着这个基座改数据、换融合、调参数,你的多模态检测器才算真正进入可用状态。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询