资讯动态

多模态虚假新闻检测实战:Python双塔模型与图文特征融合指南

发布时间:2026/10/18 7:13:48 来源:尧图企业网站定制
简介一份基于Python实现的多模态虚假新闻检测完整工程包含可运行的源码与说明文档面向计算机相关专业学生、算法爱好者以及需要课程设计或毕设参考的开发者也适合作为入门进阶的实战样例。方案采用BERT对新闻文本进行特征提取利用卷积神经网络处理配图信息并设计了有效的模态融合方法提升判别准确率该项目曾在相关比赛中获得第一名。压缩包共41个文件大小约400KB包括16个Python脚本覆盖模型构建、训练、微调与预测另有Markdown与txt说明文档、Shell部署脚本、JSON/TSV参数与评测数据、tfevents训练日志以及模型权重和字典文件结构清晰便于复现。目前已有392人学习下载。代码全部测试通过答辩评审平均分96分采用MIT许可证允许自由修改分发下载后如遇运行问题可私聊咨询支持远程教学适合深入理解文本与图像融合检测的完整流程。1. 拆开这个工程包多模态虚假新闻检测在解决什么问题判断一条新闻的真假只看文本经常会翻车。迷惑性新闻最典型的表现是文字一本正经配图却来自别的场合、别的时间图文信息错位。基于 Python 的虚假新闻检测多模态识别正是为这个场景设计的——把标题、正文和配图一起送进模型从文本和图像两个模态各抽特征再在融合层里做综合判决。这份带源码和文档说明的工程包落地目标不是演示一遍“能跑”而是让你换成自己的新闻数据重新训练、评估看清多模态比单模态到底赢在哪、输在哪。想学多模态分类、在做新闻真实性方向的检测或者想验证“图文不一致”这件事能不能被模型捕捉都适合顺着这个工程往下做。2. 先立框架多模态检测的输入组织与数据管线2.1 为什么采用“双塔”结构一条特征通道解决不了图文错位多模态检测最基本的工程形态是双塔文本塔和图像塔各自独立编码最后把两个表示融合起来。为什么不做单塔单模态模型无论多强都只能拿到一边的证据。文本塔看不到配图面对“配图与内容无关”的样本它和人类只看字一样容易被表面上合理的措辞带偏图像塔没有语义上下文一张普通城市街景在真实新闻和虚假新闻里都可能出现单独分类几乎毫无区分力。双塔结构让两个分支在特征层面互相补充这也是这类源码包几乎都采用的结构。选型上还有一条路是“级联”先用文本模型粗筛拿不准的样本再送去图像模型复审。这样做推理阶段能省一部分算力但训练和评估流程复杂阈值要单独调对于要先跑通任务的新手来说双塔更容易调试——两个分支的输出维度清晰、可以单独跑 baseline、换掉一条分支不影响另一边的结构。我一般建议先按双塔把整个流程跑通再根据实际部署需求考虑要不要改成级联。数据是这里面最容易出问题的地方。公开的新闻检测数据通常给的是文本元信息加图片路径的结构很多包用 JSON Lines 存样本、用文件夹存图片。写死绝对路径不是好习惯代码换到别人机器上就没法复用所以我习惯让源码包的入口脚本接收两个相对路径参数一个指 meta 文件一个指图片根目录。2.2 数据管线怎么搭从 meta 文件到可训练的 collate 函数下面这份 Dataset 是这类工程的标准起点。它的职责只是把 meta 里的每一条新闻文本、配图和标签读出来做最基础的合法性检查不负责具体的分词或图像变换。import json import os from torch.utils.data import Dataset from PIL import Image class NewsSampleDataset(Dataset): 读取meta文件并加载对应配图输出文本/图像/标签三元组。 def __init__(self, meta_path, image_dir): super().__init__() self.image_dir image_dir self.samples [] with open(meta_path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue item json.loads(line) # 每一行是一条新闻的元信息 # 过滤缺图或标签异常的样本避免训练中途崩掉 img_path os.path.join(image_dir, item[image_name]) if not os.path.exists(img_path): continue if item[label] not in (0, 1): continue self.samples.append(item) # 统计正负样本比例方便后面调整loss权重 self.label_dist {0: 0, 1: 0} for s in self.samples: self.label_dist[s[label]] 1 def __len__(self): return len(self.samples) def __getitem__(self, idx): item self.samples[idx] # 标题是可靠信号正文爬取质量不稳定可自行决定是否拼接 text item[title] if content in item and item[content]: text text item[content] # 统一转RGB避免灰度图和彩色图混用导致输入维度不一致 image Image.open(os.path.join(self.image_dir, item[image_name])).convert(RGB) return { text: text, image: image, label: int(item[label]), }构造函数里的 meta_path、image_dir 建议都用相对路径传参这样代码迁移不碰路径逻辑。label_dist 不是装饰真实新闻数据正负比例经常失衡这个字典能让你在训练前就发现类别不平衡问题。__getitem__返回的 text 是字符串、image 是 PIL 对象真正转张量在 collate 阶段做。真实抓取的新闻数据里还有三类噪声要提前处理。第一类是重复样本同一条新闻被多家渠道转载meta 里 id 不同但标题和配图完全相同不去重会让 train 和 dev 互相污染把“记住样本”当成“学会泛化”第二类是能打开但已损坏的图片我习惯用 try/except 包住 Image.open初始化阶段就把打不开的图剔除第三类是文本里混入爬虫模板残留比如“正文共 1234 字”这类后缀会影响分词效果。这些清洗逻辑都放在 Dataset 构造函数里而不是训练循环里换新数据时不会漏做。collate_fn 把一批样本转成模型吃得到的张量。文本用预训练模型自带的分词器统一编码图像用 torchvision 的 transform 做缩放和归一化import torch def collate_news_samples(batch, tokenizer, image_transform): texts [b[text] for b in batch] images torch.stack([image_transform(b[image]) for b in batch]) labels torch.tensor([b[label] for b in batch], dtypetorch.long) # padding统一到batch内最长truncation截断到max_length encoded tokenizer( texts, paddingTrue, truncationTrue, max_length128, return_tensorspt, ) return { input_ids: encoded[input_ids], attention_mask: encoded[attention_mask], images: images, labels: labels, }文本截断 max_length128新闻标题加部分正文已经够用塞到 250 以上并不会提效果只会增加显存。tokenizer 的 padding 方式选 batch 内动态 padding固定 padding 到 512 会白占显存。image_transform 需要注意训练和验证要用不同配置训练做随机裁剪和水平翻转验证只做中心裁剪和缩放保证评估结果确定。注意别把新闻配图的原图直接塞进模型。新闻图分辨率差异很大直接使用会让 batch_size 和显存同时失控。先统一缩放到 224×224 或 256×256再做后续处理。DataLoader 参数也有讲究。常见搭配是 num_workers4、pin_memoryTrue配合 CUDA 使用num_workers 不是越大越好超过逻辑核心数后进程切换开销会抵消并行收益。多模态任务的 batch_size 通常被图像分支吃满文本是定长序列、图像是 2048 维向量显存主要花在 CNN 前向阶段。我一般先用单 batch 跑一次前向确认不 OOM 再调大 batch_size。3. 两条特征分支文本怎么抽、配图怎么变成向量的3.1 文本分支的选型从词频向量到预训练序列模型文本通道的选型决定了这个项目的精度下限。最省事的做法是 TF-IDF 加逻辑回归但它是词袋模型抓不到否定、转折这些句法信息对假新闻这种“带着说服语气”的文本很吃亏。更稳妥的起点是用预训练语言模型取它的 CLS 向量作为整句表示也就是下面这个 TextEncoder 的写法。我一般会在工程里留一个开关先跑一个纯文本 baseline 验证数据质量有把握后再把文本编码器换成更大规模的模型。不要一开始就把模型上到最大否则后面排查数据问题时整个流程都被拉慢。下面是个封装好的文本编码器import torch.nn as nn from transformers import AutoModel, AutoTokenizer class TextEncoder(nn.Module): 文本编码器加载预训练模型输出句级CLS向量。 def __init__(self, model_name, freezeTrue): super().__init__() self.tokenizer AutoTokenizer.from_pretrained(model_name) self.backbone AutoModel.from_pretrained(model_name) if freeze: # 冻结全部参数只把CLS向量送给后面的融合层 for p in self.backbone.parameters(): p.requires_grad False def forward(self, input_ids, attention_mask): out self.backbone( input_idsinput_ids, attention_maskattention_mask, ) # last_hidden_state: (batch, seq_len, hidden) return out.last_hidden_state[:, 0, :]freezeTrue 的含义要理解清楚冻结之后预训练模型不参与梯度更新显存占用和训练时间都明显下降但模型不能针对新闻领域微调效果上限会低一截。我把 freeze 做成开关先用冻结版跑通整个流程再在验证集上比较冻结和微调两者的 F1通常微调能带来两个点以上的提升——前提是训练数据量不少于几千条。model_name 按你的数据语言环境选择对应预训练模型即可中文场景用中文语料预训练的版本英文场景换英文版本不要混用。文本塔的前向一次拿到 (batch, hidden) 的 CLS 向量hidden 通常是 768。这个维度后面要和图像特征对齐对齐方式在第 3.3 节说。3.2 图像分支用预训练 CNN 把配图转成固定长度特征图像分支的常见做法是使用预训练 CNN去掉最后的 1000 类分类头把池化后的向量当作配图表示。选 ResNet50 是工程上的折中——它在显存、速度、特征质量之间比较平衡参考实现也多如果只追求效果EfficientNet 系列在同样精度下参数量更少但训练时对 BatchNorm 更敏感环境配置不对容易翻车。代码里有三件事要注意。预训练权重直接用 torchvision 提供的版本不要下载陌生来源的权重文件那既可能有版权问题也可能是被植入恶意代码的权重。归一化参数用 ImageNet 标准值也就是 mean[0.485, 0.456, 0.406]、std[0.229, 0.224, 0.225]做图像加载的 transform 时跟它保持一致。backbone.fc 用 nn.Identity() 替换forward 输出的就是池化特征而不是分类 logits。import torch.nn as nn from torchvision import models from torchvision.models import ResNet50_Weights class ImageEncoder(nn.Module): 图像编码器ResNet50去掉分类头输出2048维视觉特征。 def __init__(self, freezeTrue): super().__init__() # DEFAULT 会加载 ImageNet 预训练权重 self.backbone models.resnet50(weightsResNet50_Weights.DEFAULT) # 把最后的1000类全连接替换成恒等映射保留全局池化结果 self.backbone.fc nn.Identity() if freeze: for p in self.backbone.parameters(): p.requires_grad False def forward(self, x): # x: (batch, 3, 224, 224)输入前必须做ImageNet归一化 return self.backbone(x) # (batch, 2048)freezeTrue 时整个图像塔的梯度不计算显存占用小、训练速度快但视觉特征只能停留在 ImageNet 学到的通用结构上。如果你的配图领域性很强比如全是截图、图表或证件照微调图像塔后半段会有明显收益。输入 x 要求是已经被 image_transform 归一化的张量也就是第 2 章 collate 函数里的 images如果数据里混了灰度图必须在加载时 .convert(RGB) 统一通道否则输入维度直接报错。3.3 维度对齐768 和 2048 怎么拼现在两条分支的输出维度不一样。文本 CLS 是 768图像池化特征是 2048直接把两个向量 concat 起来也不是不行但后面的融合层要同时适应两个不同空间里的分布收敛更慢。我习惯在进入融合层之前各过一个单层 Linear ReLU把两个模态都投影到同一个维度比如 256 或 512。这样做有两个好处。一是门控和注意力机制只需要在一个统一的语义空间里计算权重才有可比性二是投影层本身是可学习参数相当于给每个模态一次降维表达的机会。投影维度不用选太大新闻检测任务没有细粒度分类那么复杂256 到 512 足够支撑后续融合投影太大反而给融合层增加不必要的参数。这一步在第 4 章的融合模块里直接实现不需要单独训练一个投影网络。4. 融合才是上限特征拼接之后门控和注意力差在哪里4.1 三种融合方式的定位拼接、门控、跨模态注意力多模态检测的精度上限往往不在单模态编码器而在融合层。先看三种最常见方案的定位差别。融合方式实现思路复杂度适用情况直接拼接concat 两个模态向量后接分类头最低快速建立 baseline门控融合用 sigmoid 学一组权重给两个模态加权低模态噪声不平衡时稳住下限跨模态注意力让一个模态按注意力查询另一模态的证据中高图文一致性问题为主的任务直接拼接的缺点在于分类器对所有模态一视同仁当图像分支质量差时它会拖累文本分类。门控融合把“该信谁”变成可学习的权重是我在这个项目里推荐的默认起点。跨模态注意力做的是信息交换文本可以去“查看”图像的哪些区域与当前内容相关能力最强也最吃调参具体实现放到第 6 章。这三种写法的实现难度和收益不是线性的。直接拼接的代码只有一行 torch.cat几分钟就能出结果但它暴露问题也慢如果融合后 F1 不如单模态你很难判断是两个编码器的问题还是融合策略的问题。门控融合多了一个可学习权重代码量不大却能让你直观看到模型到底更相信哪个模态这对定位数据问题很有帮助。跨模态注意力留到流程全部跑通之后再替换不要一开始就上。4.2 一份可以直接嵌入训练循环的门控融合模块门控融合的核心是一个 sigmoid 门它拿到两个模态的投影特征输出一组权重决定最终融合向量里文本和图像各占多少。import torch import torch.nn as nn class GatedFusion(nn.Module): 门控融合先投影到统一维度再让门控决定两个模态的贡献比例。 def __init__(self, text_dim, image_dim, proj_dim256, num_classes2): super().__init__() # 两个模态各自投影到同一维度 self.text_proj nn.Linear(text_dim, proj_dim) self.image_proj nn.Linear(image_dim, proj_dim) # 门控输入拼接后的特征输出两个模态的权重 self.gate nn.Sequential( nn.Linear(proj_dim * 2, 2), nn.Sigmoid(), ) self.classifier nn.Sequential( nn.Linear(proj_dim, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, num_classes), ) def forward(self, text_feat, image_feat): # 两个特征先投影再激活 t torch.relu(self.text_proj(text_feat)) # (batch, proj_dim) v torch.relu(self.image_proj(image_feat)) # (batch, proj_dim) gate self.gate(torch.cat([t, v], dim-1)) # (batch, 2) # 归一化约束gate两维相加等于1 gate gate / (gate.sum(dim-1, keepdimTrue) 1e-8) fused gate[:, :1] * t gate[:, 1:] * v logits self.classifier(fused) return logits, gate投影层解决维度不一致问题统一到 proj_dim256 再做门控。对 gate 做归一化保证两个权重相加为 1避免训练时两个权重同时走强导致特征幅度失控。forward 返回 logits 和 gate 两个值训练时用 logits 算交叉熵验证时统计 gate[:, 0] 的均值——如果这个均值始终高于 0.9说明图像分支几乎没有起到作用问题大概率出在图像塔或图像数据质量上。参数调整方面proj_dim 太小丢信息、太大加重融合层负担256 在本任务里比较稳妥。Dropout(0.3) 是针对新闻数据量通常只有几千到几万条这个现实设置的过拟合风险比通用视觉分类任务高得多。如果 label_dist 里正负样本差距超过 5 倍分类器里需要加类别权重不要只靠调整阈值。注意每次训练都要打印 gate 均值和单模态 baseline。这不是调试负担而是确认融合有效的唯一快速手段。4.3 分类头与损失稳定的默认组合融合向量接一个两层 MLP 分类头即可不需要花哨的复杂网络。损失函数默认用交叉熵不需要因为它是多模态就特殊处理——真正的复杂度在数据分布和特征质量不在损失函数。类别不平衡时给 loss 传 weight[1, ratio]ratio 等于负样本数除以正样本数如果肉眼看出误判集中在某一类上可以考虑 Focal Loss但先跑通默认配置再调这些旋钮不要第一次训练就同时动太多地方。训练周期上我习惯按验证集的 macro-F1 做早停而不是固定 epoch 数。新闻数据集规模小模型在 5 到 8 轮之后很容易过拟合早停配合 Dropout 比单纯正则化更省心。优化器选 AdamW学习率按编码器是否冻结来定冻结编码器时融合层可以用 1e-3微调编码器时整体降到 2e-5否则预训练权重会在前几步就被冲乱。5. 训练与评估避坑跑多模态检测的四个经典翻车点5.1 现象训练集准确率走高测试集 F1 惨不忍睹原因新闻数据集正负样本严重不平衡模型把大多数样本判成多数类就能拿到高准确率。交叉熵对多数类梯度占主导少数类召回率一路走低。常见结果是准确率能到 90%F1 却不到 0.6整个评估报告失真。解决先看 Dataset 里的 label_dist在 loss 里做类别加权。我习惯把负类数量除以正类数量作为权重传给交叉熵例如负样本 8000、正样本 2000weight[1.0, 4.0]。同时评估指标改成 macro-F1不再只盯着 accuracy。类别不平衡严重时验证集的切分也要用分层采样保证 dev/test 的标签分布和训练集一致。5.2 现象GPU 利用率只有个位数CPU 内存倒先爆了原因Dataset 的__getitem__里每次实时打开原图图片分辨率大transform 在 CPU 上执行GPU 只能空转等数据这是多模态项目最常见的性能瓶颈不是代码写错而是数据流没通。解决在数据预处理阶段把图片统一缩放到 224×224 并缓存到临时目录Dataset 里只加载缩放后的版本DataLoader 设置 num_workers4、pin_memoryTrue。如果内存还有富余可以把整个缓存图片预加载成一个 list省去每次的磁盘 IO但要注意内存占用几千张图问题不大几万张就要谨慎。5.3 现象接上图像分支之后效果反而不如纯文本原因图像信息与文本信息重叠度高或者配图本身噪声大。拼接后分类面更复杂融合层学到的可能只是文本分支的重复表达图像分支反而引入了额外的分布偏移。解决先单独训练两个单模态 baseline分别记录 F1。只有当两条 baseline 接近时融合才有意义如果文本 F1 明显高于图像说明图像通道要先换更强的模型、做更彻底的数据清洗或者干脆检查是不是 image_transform 归一化配置不对。门控融合在这个场景下尤其有价值——gate 均值会告诉你模型是不是自己学会了忽略图像分支。5.4 现象同一份代码换台机器就复现不了原因深度学习项目里 random、numpy、torch、CUDA 四处都有随机性只固定其中一个种子等于没固定不同版本之间算子实现差异也会带来结果漂移“换机器结果对不上”是运行环境问题不是模型问题。解决训练入口统一固定四个种子random.seed、numpy.random.seed、torch.manual_seed、torch.cuda.manual_seed_all 全都要设同时把 torch.backends.cudnn.benchmark 关掉避免卷积算子选择引入不确定性。requirements.txt 里锁定主版本号并把随机种子和依赖版本写进文档说明方便换机器后排错。这些坑我基本都踩过一遍前三个是数据问题、一个是环境问题没有一个需要改模型结构。跑多模态项目时我给自己定的习惯是先打印数据分布再跑单模态 baseline最后才碰融合层。这套顺序能挡掉大半无头绪的“玄学调参”。6. 进阶方向从门控往跨模态注意力走一步把图文关系真正用起来6.1 跨模态注意力的最小实现与验证门控融合的粒度是整个模态它只能回答“文本和图像各自该信多少”却看不到“文本里哪个词跟图像里哪个区域产生了矛盾”。跨模态注意力能回答后者把文本表示作为 query图像特征作为 key/value让文本去查询图像特征里与它最相关的部分得到带着视觉证据的文本表示。核心代码其实很短import torch def cross_attention(tokens, image_feats): # tokens: (batch, seq_len, dim) 图像查询文本 # image_feats: (batch, num_regions, dim) scale tokens.shape[-1] ** 0.5 attn torch.matmul(tokens, image_feats.transpose(-1, -2)) / scale attn torch.softmax(attn, dim-1) # 用注意力权重加权视觉特征得到对齐后的视觉表示 return torch.matmul(attn, image_feats)从门控升级到注意力有一个前提变化特征不再是一维句向量而是序列级表示。文本塔要输出 token 级特征图像塔要输出类似 num_regions 的位置特征而不是只留一个 2048 维池化向量。实现时还要把文本的 attention_mask 传进来避免 padding 位置参与注意力计算不然效果会倒退。验证方法上替换融合层后保持其他参数不动单独对比三档结果直接拼接、门控、跨模态注意力记录各自的 macro-F1。如果跨模态注意力在新闻数据上不能超过门控两个点以上先别急着上线——注意力头数、mask 传递、投影维度都值得再调数据量太小时注意力机制很容易过拟合。我最初做这个方向时犯过一个错误以为只要把两个模型的特征拼起来就算多模态结果融合后 F1 反而不如纯文本。后来才明白图文错位这个任务真正要学的是“关系”而不是简单的特征堆叠。跨模态注意力是我试过最稳的进阶方向但前提是前面的单模态分支和数据清洗都过关。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑