简介这是一套基于Python的多模态虚假新闻检测完整项目源码与文档面向计算机、人工智能、通信工程等专业的在校学生、教师及企业开发者可用于课程设计、毕业设计、竞赛备赛或项目初期立项演示。项目以文本与图像双模态为切入点采用BERT完成文本特征提取结合卷积神经网络处理图像特征并设计了一种有效的多模态融合方法在相关比赛中取得第一名检测准确率表现突出。压缩包共41个文件约400KB包含16个py源码、5个txt说明、4个md文档、3个sh脚本及tsv、json、checkpoint、ipynb等配置与训练文件覆盖模型定义、特征提取、训练预测与结果记录等环节目录结构清晰。已有391人学习关注。代码均经测试运行成功附文档说明与远程答疑支持读者可据此掌握多模态融合思路、复现实验流程并迁移到自身课题中。1. 从一份「虚假新闻检测多模态识别源码」说起它到底能跑出什么你手上如果拿到一个叫「基于Python的虚假新闻检测多模态识别源码文档说明.zip」的包第一反应大概率是解压、装依赖、跑main.py然后看它能不能识别出哪条是假新闻。但真正跑过的人都知道这类项目最容易翻车的地方不在模型而在数据对齐——文本和图片对不上号标签和样本错位跑出来的准确率看着挺高实际全是玄学。这个标题讲的是用 Python 做虚假新闻检测而且强调「多模态识别」意思是模型不只看文字还要看配图、排版甚至传播结构。它解决的是单模态检测的盲区纯文本模型遇到「图不对文」的假新闻会漏判纯图像模型遇到「文不对图」的也会翻车。适合谁适合已经会 Python 基础、想找一个能跑通的多模态入门项目练手的人也适合做内容审核、舆情分析方向、需要快速验证多模态方案可行性的工程师。但别急着把它当成生产级系统。这类源码包通常是一个「能跑通的最小闭环」不是「能上线的工程」。你要做的是先把它跑起来理解数据怎么组织、模型怎么拼接、评估怎么做然后再决定要不要往自己的业务里搬。下面我按实际复现的顺序把这条路拆开讲。2. 多模态虚假新闻检测的数据管线文本、图像和标签怎么对齐2.1 先搞清楚「多模态」在这类项目里通常指什么大多数基于 Python 的虚假新闻检测源码多模态指的是「文本 图像」两条输入。文本可能是新闻标题加正文图像可能是配图、截图或者视频封面。有些项目还会加「社交上下文」比如转发数、评论情感但那属于进阶入门包一般不带。为什么非要两个模态因为假新闻的套路往往是「真图配假文」或者「假图配真文」。单看文本模型可能被流畅的措辞骗过去单看图像模型可能被真实照片迷惑。只有把两者放在一起才能捕捉到「图文不一致」这个强信号。常见做法是文本走 BERT 或 RoBERTa 提取 [CLS] 向量图像走 ResNet 或 ViT 提取全局池化向量然后拼接或者做跨模态注意力。我一般会先确认源码里用的是哪种融合方式因为这决定了你后面能不能换 backbone、能不能加自己的数据。2.2 数据目录结构和标签格式先别改代码先看数据拿到包之后第一件事不是装环境而是把数据目录翻一遍。典型的目录长这样dataset/ ├── train/ │ ├── text/ │ │ ├── 001.txt │ │ └── 002.txt │ ├── image/ │ │ ├── 001.jpg │ │ └── 002.jpg │ └── labels.csv ├── val/ │ ├── text/ │ ├── image/ │ └── labels.csv └── test/ ├── text/ ├── image/ └── labels.csvlabels.csv一般至少有两列id和label。id对应文本文件名和图像文件名的主干label通常是 0/10 表示真新闻1 表示假新闻。有些数据集会多一列source或者category那是用来做细粒度分析的入门阶段可以先忽略。这里最容易踩的坑是文本文件名和图像文件名不一致。比如文本叫001.txt图像叫001.png或者图像多了个后缀_img。你如果直接按id拼路径就会报FileNotFoundError。我一般会先写个脚本扫一遍确认两边文件名能对上。import os text_dir dataset/train/text image_dir dataset/train/image text_ids {os.path.splitext(f)[0] for f in os.listdir(text_dir)} image_ids {os.path.splitext(f)[0] for f in os.listdir(image_dir)} print(文本有但图像没有:, text_ids - image_ids) print(图像有但文本没有:, image_ids - text_ids) print(两边都有:, len(text_ids image_ids))这段代码做的是集合差集运算。os.path.splitext去掉扩展名只留主干。如果差集不为空说明数据本身有问题要么补文件要么改配对逻辑。参数上没什么好调的重点是看输出——如果「两边都有」的数量远小于总数那这个数据集可能被人为切过你得找文档说明确认。2.3 用 PyTorch Dataset 把文本和图像绑在一起源码里一般会有一个Dataset类继承torch.utils.data.Dataset实现__getitem__和__len__。核心逻辑是根据id读文本、读图像、读标签然后返回一个字典或者元组。import torch from torch.utils.data import Dataset from PIL import Image from transformers import BertTokenizer from torchvision import transforms class FakeNewsDataset(Dataset): def __init__(self, data_dir, labels_file, tokenizer_namebert-base-uncased, max_len128): self.data_dir data_dir self.tokenizer BertTokenizer.from_pretrained(tokenizer_name) self.max_len max_len self.samples [] with open(labels_file, r, encodingutf-8) as f: next(f) # 跳过表头 for line in f: parts line.strip().split(,) self.samples.append((parts[0], int(parts[1]))) self.img_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def __len__(self): return len(self.samples) def __getitem__(self, idx): sample_id, label self.samples[idx] text_path os.path.join(self.data_dir, text, f{sample_id}.txt) image_path os.path.join(self.data_dir, image, f{sample_id}.jpg) with open(text_path, r, encodingutf-8) as f: text f.read().strip() image Image.open(image_path).convert(RGB) encoding self.tokenizer( text, max_lengthself.max_len, paddingmax_length, truncationTrue, return_tensorspt ) return { input_ids: encoding[input_ids].squeeze(0), attention_mask: encoding[attention_mask].squeeze(0), image: self.img_transform(image), label: torch.tensor(label, dtypetorch.long) }逻辑说明__init__里读标签文件把id和label存成列表__getitem__里按id拼路径文本用 BERT tokenizer 转成input_ids和attention_mask图像做 Resize、ToTensor、Normalize。参数上max_len128是文本截断长度新闻标题加摘要一般够用如果正文很长可以调到 256 或 512但显存会涨。图像尺寸 224 是 ResNet 的标准输入换成 ViT 可能要改 224 或 384。注意paddingmax_length会让所有样本长度一致方便 batch 拼接但短文本会浪费计算。如果数据里文本长度差异很大可以改成paddingTrue做动态 padding不过要在 collate_fn 里处理。2.4 标签噪声和类别不平衡先看分布再决定怎么采样虚假新闻数据集经常有类别不平衡真新闻多、假新闻少或者反过来。你如果直接跑模型可能全预测成多数类准确率看着不低但 F1 很难看。我一般会先统计一下训练集标签分布from collections import Counter labels [label for _, label in dataset.samples] print(Counter(labels))如果 0 和 1 的比例超过 3:1就要考虑加权采样或者改损失函数。常见做法是WeightedRandomSampler给少数类更高的采样权重或者用CrossEntropyLoss(weight...)把少数类的损失权重调大。源码里如果没带这些你可以自己加不影响主体结构。3. 模型搭建文本分支、图像分支和融合层怎么接3.1 文本分支BERT 还是 TextCNN选哪个入门源码里文本分支最常见的是 BERT因为 HuggingFace 的transformers库太方便了三行代码就能加载预训练模型。但也有用 TextCNN 或 BiLSTM 的那种更轻量适合没有 GPU 的环境。BERT 的优势是语义表征强对反讽、隐喻这类假新闻常用手法更敏感。缺点是显存占用大bert-base大概 110M 参数batch size 开到 16 就可能爆 8G 显存。TextCNN 参数量小训练快但语义理解弱一些。我一般会先看源码用的是哪个。如果是 BERT就确认transformers版本和模型名称如果是 TextCNN就检查词表大小和 embedding 维度。下面是一个 BERT 文本分支的典型写法import torch.nn as nn from transformers import BertModel class TextBranch(nn.Module): def __init__(self, bert_namebert-base-uncased, hidden_dim768): super().__init__() self.bert BertModel.from_pretrained(bert_name) self.dropout nn.Dropout(0.3) self.fc nn.Linear(hidden_dim, 256) def forward(self, input_ids, attention_mask): outputs self.bert(input_idsinput_ids, attention_maskattention_mask) cls_vec outputs.last_hidden_state[:, 0, :] # [CLS] 向量 return self.fc(self.dropout(cls_vec))hidden_dim768是bert-base的隐藏层维度如果换bert-large要改成 1024。Dropout(0.3)是防止过拟合数据量小的时候可以调到 0.5。fc把 768 维降到 256 维是为了和图像分支拼接时维度匹配。3.2 图像分支ResNet 够用ViT 看数据量图像分支用 ResNet50 的最多因为torchvision自带预训练权重加载方便224 输入也成熟。ViT 这两年火但对数据量要求高小数据集上不如 ResNet 稳。import torch.nn as nn from torchvision import models class ImageBranch(nn.Module): def __init__(self, backboneresnet50, out_dim256): super().__init__() if backbone resnet50: self.cnn models.resnet50(pretrainedTrue) in_dim self.cnn.fc.in_features self.cnn.fc nn.Identity() # 去掉原始分类头 elif backbone vit: self.cnn models.vit_b_16(pretrainedTrue) in_dim self.cnn.heads.head.in_features self.cnn.heads.head nn.Identity() self.fc nn.Linear(in_dim, out_dim) self.dropout nn.Dropout(0.3) def forward(self, image): feat self.cnn(image) return self.fc(self.dropout(feat))pretrainedTrue会下载 ImageNet 预训练权重第一次跑需要联网。如果环境不能联网要提前把权重文件放到~/.cache/torch/hub/checkpoints/。out_dim256和文本分支对齐后面拼接就是 512 维。3.3 融合层拼接、注意力还是门控融合方式决定了多模态到底有没有用。最简单的就是拼接class ConcatFusion(nn.Module): def __init__(self, text_dim256, image_dim256, num_classes2): super().__init__() self.classifier nn.Sequential( nn.Linear(text_dim image_dim, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, num_classes) ) def forward(self, text_feat, image_feat): combined torch.cat([text_feat, image_feat], dim1) return self.classifier(combined)拼接的优点是简单、可解释缺点是假设两个模态同等重要。如果图像质量差、文本质量好拼接反而会拖后腿。进阶做法是跨模态注意力让文本去「查询」图像或者反过来。但入门阶段拼接够用了先跑通再优化。我一般会先跑拼接看验证集 F1。如果 F1 比单文本模型还低说明图像分支在捣乱可能是图像预处理有问题或者图像和标签根本不对应。这时候要回去检查数据对齐而不是急着换融合方式。3.4 训练循环和评估指标别只看准确率训练循环没什么特别的就是标准的 PyTorch 流程。但评估指标要特别注意虚假新闻检测里准确率很容易被多数类带偏我一般会同时看 F1、精确率和召回率。from sklearn.metrics import f1_score, precision_score, recall_score def evaluate(model, dataloader, device): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for batch in dataloader: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) image batch[image].to(device) labels batch[label].to(device) logits model(input_ids, attention_mask, image) preds torch.argmax(logits, dim1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) return { f1: f1_score(all_labels, all_preds, averagebinary), precision: precision_score(all_labels, all_preds, averagebinary), recall: recall_score(all_labels, all_preds, averagebinary) }averagebinary适用于二分类如果是多分类要改成macro或weighted。如果假新闻是少数类召回率比精确率更重要因为漏判假新闻的代价通常比误判真新闻高。这个取舍要根据业务场景定没有标准答案。4. 避坑与排查跑通多模态虚假新闻检测最常见的 5 个翻车点4.1 现象训练 loss 不降准确率卡在 50% 左右原因最常见的是标签和样本错位。比如labels.csv里的id顺序和文件遍历顺序不一致或者文本和图像用了不同的id映射。另一个可能是 tokenizer 的padding和truncation没开导致 batch 里张量形状不一致PyTorch 默默广播成了错误计算。解决先写个最小测试拿一个 batch 出来手动检查input_ids解码后是不是原文图像shape是不是[B, 3, 224, 224]标签是不是和文件内容对应。确认无误后再跑全量。4.2 现象验证集 F1 比训练集低 20 个点以上原因过拟合。多模态模型参数量大如果训练集只有几千条很容易记住训练样本。另一个可能是数据泄露——训练集和验证集有重复样本或者同一新闻的不同截图被分到了两边。解决先查重用文本哈希或图像感知哈希去重。然后加正则Dropout 调到 0.5加 weight decay或者冻结 BERT 前几层。如果还不行就减模型容量把 BERT 换成 TextCNN。4.3 现象图像分支输出全零或全同一个值原因图像预处理出错。比如Normalize的 mean/std 用错了或者ToTensor之前没convert(RGB)导致灰度图变成单通道和预训练模型的 3 通道不匹配。还有一种可能是图像路径拼错Image.open打开的是同一个默认图。解决随机抽几张图打印image.mean()和image.std()正常应该在 0 到 1 之间。如果全是 0 或全是 1说明归一化反了。再检查Image.open的路径打印出来确认。4.4 现象GPU 显存爆了batch size 降到 1 还是 OOM原因BERT 的max_len设太大比如 512加上图像分支的 ResNet显存占用是叠加的。另外如果paddingmax_length每个 batch 都按最大长度算短文本也占满显存。解决先把max_len降到 128图像尺寸从 224 降到 160batch size 设 8 试试。如果还不够用梯度累积模拟大 batch或者把 BERT 换成蒸馏版如distilbert。实在不行就上混合精度训练torch.cuda.amp能省不少显存。4.5 现象换了数据集之后模型完全不能用原因新数据集的文本语言、图像风格、标签定义和原数据集不一致。比如原数据集是英文新闻新数据集是中文社交媒体tokenizer 词表对不上图像也从专业摄影变成了用户随手拍。解决不要直接迁移。先在新数据集上重新训练分类头冻结 BERT 和 ResNet 的底层只训融合层和分类层。等 F1 稳定了再解冻顶层做微调。如果语言不同要换对应的 BERT 模型比如中文用bert-base-chinese。5. 从跑通到用起来多模态虚假新闻检测的进阶技巧跑通源码只是第一步。如果你真想把多模态虚假新闻检测用到实际业务里下面几个技巧是我踩过坑之后总结出来的。第一别迷信端到端。多模态模型看起来很美但实际业务里文本和图像的质量往往不对等。我一般会先分别跑单文本和单图像模型看哪个模态的 F1 更高。如果文本模型已经到 0.9图像模型只有 0.6那多模态融合的收益可能很小甚至因为图像噪声拖累整体。这时候更好的做法是「文本为主图像做一致性校验」——只有当文本模型置信度低的时候才调用图像分支。第二数据增强要分模态做。文本可以用同义词替换、回译图像可以用随机裁剪、颜色抖动。但要注意虚假新闻检测里图像增强不能改变语义。比如你把一张「火灾现场」的图裁得只剩烟雾模型可能就学不到关键特征了。我一般只做轻微的几何变换不做大幅裁剪。第三评估要分场景。整体 F1 高不代表每个类别都好。我习惯按新闻主题政治、娱乐、科技或者按传播平台微博、微信、新闻网站分组算 F1。如果某个主题的 F1 明显低说明模型在这个场景下有盲区可能要补数据或者加规则。第四模型可解释性比准确率更重要。内容审核场景里审核员需要知道「为什么这条被判定为假」。我一般会用注意力权重可视化看模型主要关注文本的哪些词、图像的哪些区域。如果模型关注的是「震惊」「速看」这类标题党词汇那说明它学到了捷径换个数据集可能就失效了。下面是一个简单的注意力可视化代码片段用于检查文本分支的关注点from transformers import BertTokenizer, BertModel import torch tokenizer BertTokenizer.from_pretrained(bert-base-uncased) model BertModel.from_pretrained(bert-base-uncased, output_attentionsTrue) model.eval() text Breaking news: scientists confirm that drinking coffee cures all diseases inputs tokenizer(text, return_tensorspt) with torch.no_grad(): outputs model(**inputs) # 取最后一层注意力平均所有头 attention outputs.attentions[-1].mean(dim1).squeeze(0) tokens tokenizer.convert_ids_to_tokens(inputs[input_ids][0]) for token, weight in zip(tokens, attention[0]): print(f{token}: {weight:.4f})这段代码输出每个 token 对[CLS]的注意力权重。权重高的词就是模型认为重要的词。如果「cures」和「diseases」权重很高说明模型在抓「绝对化表述」这是合理的。如果「the」「that」权重很高那模型可能没学到东西。最后说一个我自己的习惯每次跑完一个多模态模型我都会把文本分支和图像分支单独拿出来评估一遍记录三个 F1——单文本、单图像、多模态。如果多模态比单文本高不到 2 个点我就会考虑是不是不值得上多模态直接用文本模型加规则更省事。这个习惯帮我省了很多无效的工程投入。希望帮到你。本文还有配套的精品资源点击获取