简介:这份资源面向计算机相关专业的本科生与研究生,提供一套基于Python的虚假新闻检测多模态识别完整项目源码,可用于毕业设计、期末大作业或课程设计场景,帮助解决多模态特征融合与虚假新闻分类的实践难题。压缩包共39个文件,约353KB,以16个py脚本为核心,涵盖BERT微调、LightGBM与CatBoost融合建模、预测测试等流程,另含4个md说明、4个txt依赖与配置、3个sh运行脚本、3个tsv数据集文件,以及json配置、checkpoint权重和ipynb实验笔记,结构清晰便于按模块查阅。目前已有498人学习下载,代码注释详尽,新手也能理解。项目经过严格调试,部署后即可运行,读者可据此掌握多模态虚假新闻检测的完整建模思路、模型融合技巧与调参排错方法,具有较高的参考与复用价值。
1. 虚假新闻检测多模态识别:为什么单看文本已经不够用了
一条配着现场图的突发消息,文本写得有鼻子有眼,图片却来自三年前的另一个事件——这种「图文错配」是当下虚假新闻最典型的形态。只做文本分类的模型,遇到这种样本几乎必翻车,因为文字本身没有明显破绽。基于Python的虚假新闻检测多模态识别源代码+文档说明这类项目,解决的正是这个问题:把文本和图像两条模态的信息联合起来判断真伪。它适合已经会写Python、想从单模态NLP跨到多模态方向的工程师,也适合需要快速搭一套可复现baseline的学生和研究者。核心思路不复杂:文本走Transformer编码,图像走CNN或ViT编码,两路特征对齐融合后接分类头。难的是数据怎么配、融合放哪一层、图文不对齐时怎么兜底。下面按「先立住原理、再跑通代码、最后避坑」的顺序拆开讲。
2. 多模态虚假新闻检测的技术底座:从特征提取到融合分类
2.1 文本与图像两条编码链路怎么选
文本侧,主流做法是用预训练语言模型做编码器。BERT-base是性价比最高的起点,768维输出,中文场景换成本地预训练的中文BERT权重即可。如果算力紧张,可以用蒸馏后的轻量模型,但虚假新闻检测对语义细节敏感,蒸馏过头会掉点。图像侧有两个方向:CNN(ResNet-50)擅长局部纹理,ViT擅长全局关系。虚假新闻里的图片篡改痕迹往往是局部的(拼接边缘、光照不一致),所以ResNet-50在这类任务上并不吃亏,而且参数量小、训练快。我一般会先用ResNet-50跑通全流程,确认融合策略有效后,再换ViT对比。
两条链路的输出维度要对齐。BERT输出[batch, seq_len, 768],ResNet输出[batch, 2048]。常见做法是文本侧取[CLS]向量或做平均池化得到[batch, 768],图像侧接一个线性层投影到768维,这样两路特征在同一空间里才能做注意力或拼接。
2.2 融合策略:早期融合、晚期融合还是交叉注意力
融合位置决定了模型能捕捉什么级别的关联。晚期融合最简单:两路各自分类,最后对logits加权平均。优点是稳、好调,缺点是学不到图文之间的细粒度对应关系。早期融合在输入层就拼接,对虚假新闻检测来说太粗暴,因为文本和图像的原始表示差异太大。
交叉注意力(cross-attention)是当前效果最好的方案:把文本特征作为query,图像特征作为key和value,让每个词去「看」图像的相关区域,反之亦然。这样模型能学到「文中提到的地点」和「图中建筑」是否一致。代价是显存占用高,batch size要压到8或16。实操中我会用一层交叉注意力加残差连接,层数多了容易过拟合,尤其在小数据集上。
2.3 用PyTorch搭一个可运行的双塔融合模型
下面是一个最小可运行的多模态分类模型骨架,文本用BERT、图像用ResNet,交叉注意力做融合。
import torch import torch.nn as nn from transformers import BertModel from torchvision import models class CrossAttentionFusion(nn.Module): def __init__(self, dim=768, num_heads=8): super().__init__() # 文本作为query,图像作为key/value self.cross_attn = nn.MultiheadAttention(embed_dim=dim, num_heads=num_heads, batch_first=True) self.norm = nn.LayerNorm(dim) self.dropout = nn.Dropout(0.1) def forward(self, text_feat, image_feat): # text_feat: [B, L, D], image_feat: [B, 1, D] attn_out, _ = self.cross_attn(query=text_feat, key=image_feat, value=image_feat) out = self.norm(text_feat + self.dropout(attn_out)) # 残差连接 return out class MultimodalFakeNewsDetector(nn.Module): def __init__(self, num_classes=2, freeze_bert=False): super().__init__() self.text_encoder = BertModel.from_pretrained('bert-base-chinese') if freeze_bert: for p in self.text_encoder.parameters(): p.requires_grad = False self.image_encoder = models.resnet50(pretrained=True) self.image_encoder.fc = nn.Identity() # 去掉原分类头,输出2048维 self.image_proj = nn.Linear(2048, 768) # 投影到文本同维度 self.fusion = CrossAttentionFusion(dim=768) self.classifier = nn.Sequential( nn.Linear(768, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, num_classes) ) def forward(self, input_ids, attention_mask, images): text_out = self.text_encoder(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state img_feat = self.image_encoder(images) # [B, 2048] img_feat = self.image_proj(img_feat).unsqueeze(1) # [B, 1, 768] fused = self.fusion(text_out, img_feat) # [B, L, 768] pooled = fused.mean(dim=1) # 平均池化 logits = self.classifier(pooled) return logits逻辑说明:文本编码器输出每个token的表示,图像编码器输出一个全局向量并投影到768维,交叉注意力让每个token根据图像信息更新自己,最后平均池化接分类头。参数方面,freeze_bert=True时只训练融合层和分类头,适合数据量小于1万条的场景;数据充足时解冻BERT全量微调,学习率要降到2e-5。num_heads=8是768维的标准配置,改成4会降低表达能力但省显存。Dropout(0.3)在分类头前,小数据集上可以提到0.5。
2.4 数据管线的三个关键处理
多模态数据最难的不是模型,是配对。每条样本必须同时有文本和图像,缺一不可。常见做法是自定义Dataset,在__getitem__里同时返回tokenized文本和归一化后的图像张量。
from torch.utils.data import Dataset from PIL import Image from torchvision import transforms class FakeNewsDataset(Dataset): def __init__(self, df, tokenizer, max_len=128): self.df = df # 需包含 text, image_path, label 三列 self.tokenizer = tokenizer self.max_len = max_len 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.df) def __getitem__(self, idx): row = self.df.iloc[idx] enc = self.tokenizer(row['text'], truncation=True, padding='max_length', max_length=self.max_len, return_tensors='pt') image = Image.open(row['image_path']).convert('RGB') image = self.img_transform(image) return { 'input_ids': enc['input_ids'].squeeze(0), 'attention_mask': enc['attention_mask'].squeeze(0), 'images': image, 'labels': torch.tensor(row['label'], dtype=torch.long) }max_len=128对新闻标题加正文摘要够用,长文可以提到256但显存翻倍。图像统一224×224是ResNet的标准输入。convert('RGB')必须加,否则遇到灰度图或RGBA图会直接报错,这是血泪经验。归一化参数用的是ImageNet统计值,因为ResNet预训练权重就是在这个分布上学的,不匹配会掉点。
3. 从零跑通训练与评估:命令行、参数与日志
3.1 训练脚本的核心循环与参数设置
训练循环本身不复杂,关键是梯度累积和混合精度。多模态模型显存吃紧,batch size往往只能开到8,这时用梯度累积模拟大batch。
import torch from torch.cuda.amp import autocast, GradScaler from transformers import AdamW, get_linear_schedule_with_warmup def train_one_epoch(model, loader, optimizer, scheduler, scaler, device, accum_steps=4): model.train() total_loss = 0 optimizer.zero_grad() for step, batch in enumerate(loader): input_ids = batch['input_ids'].to(device) attention_mask = batch['attention_mask'].to(device) images = batch['images'].to(device) labels = batch['labels'].to(device) with autocast(): # 混合精度,省显存 logits = model(input_ids, attention_mask, images) loss = nn.CrossEntropyLoss()(logits, labels) / accum_steps scaler.scale(loss).backward() if (step + 1) % accum_steps == 0: scaler.step(optimizer) scaler.update() scheduler.step() optimizer.zero_grad() total_loss += loss.item() * accum_steps return total_loss / len(loader)accum_steps=4配合实际batch size 8,等效batch size 32。学习率用2e-5(BERT微调标准值),warmup比例0.1。混合精度用autocast加GradScaler,能省30%左右显存,但要注意loss必须除以累积步数,否则梯度会放大。评估时用model.eval()加torch.no_grad(),指标至少看准确率和F1,虚假新闻检测里类别不平衡很常见,F1比准确率更能反映真实水平。
3.2 评估指标与阈值调整
二分类默认阈值0.5,但虚假新闻检测往往更怕漏判(把假新闻判成真),所以可以适当降低阈值提高召回。用sklearn算一组指标:
from sklearn.metrics import classification_report, roc_auc_score import numpy as np def evaluate(model, loader, device, threshold=0.4): model.eval() all_probs, all_labels = [], [] with torch.no_grad(): for batch in loader: logits = model(batch['input_ids'].to(device), batch['attention_mask'].to(device), batch['images'].to(device)) probs = torch.softmax(logits, dim=1)[:, 1].cpu().numpy() all_probs.extend(probs) all_labels.extend(batch['labels'].numpy()) preds = (np.array(all_probs) >= threshold).astype(int) print(classification_report(all_labels, preds, digits=4)) print('AUC:', roc_auc_score(all_labels, all_probs))阈值从0.5降到0.4,召回通常能提3到5个点,精确率掉1到2个点。具体调到多少要看业务容忍度,没有万能值。AUC不受阈值影响,用来判断模型整体排序能力,低于0.85说明融合策略或数据质量有问题。
3.3 文档说明该写什么才有用
一份能让人复现的文档说明,至少包含四块:环境依赖(Python版本、PyTorch版本、transformers版本,用requirements.txt固定)、数据格式(每列的字段名和样例)、训练命令(含所有超参的完整命令行)、预期结果(在标准数据集上的指标区间)。我见过太多文档只写「运行train.py即可」,结果别人连数据放哪都不知道。文档里还要标注哪些参数是必须改的(比如数据路径),哪些可以保持默认。如果代码里用了预训练权重,要写清楚权重名称和获取方式,不要假设别人本地就有。
4. 避坑与排查:多模态虚假新闻检测最容易翻车的五个地方
4.1 图文不对齐导致loss不降
现象:训练几个epoch后loss卡在0.69附近(二分类的随机水平),准确率50%上下。原因:数据里文本和图像不是同一条新闻的,配对错了。常见于从不同来源爬取后按文件名或索引拼接。解决:写一个校验脚本,随机抽100条人工核对图文是否对应;或者在Dataset里加断言,检查image_path是否存在且能打开。更隐蔽的情况是图像路径正确但内容被替换过,这种只能靠抽样肉眼查。
4.2 图像编码器冻结后特征太泛
现象:解冻BERT但冻结ResNet时,验证集F1比纯文本模型还低。原因:ImageNet预训练的ResNet提取的是通用物体特征,对「这张图是否被篡改」这种任务不敏感。解决:至少解冻ResNet的layer4和fc层,学习率设成BERT的十分之一(2e-6)。如果数据量超过5万条,全量解冻ResNet效果更好,但要注意过拟合,加强数据增强(随机裁剪、颜色抖动)。
4.3 显存溢出(OOM)的三种触发方式
现象:训练到一半报CUDA out of memory。原因一:max_len设太大,文本序列过长;原因二:batch size没配合梯度累积;原因三:交叉注意力的注意力矩阵是L×L,L=256时显存占用是L=128的四倍。解决:先把max_len降到128,再用accum_steps补batch;交叉注意力层数控制在1到2层;用torch.cuda.empty_cache()在epoch之间清理缓存。如果还不行,把图像分辨率从224降到160,但会掉点。
4.4 类别不平衡导致模型全预测多数类
现象:准确率看着有80%,但F1只有0.3,混淆矩阵显示少数类几乎全错。原因:虚假新闻数据集中真新闻远多于假新闻,模型学会了偷懒。解决:损失函数加class weight,nn.CrossEntropyLoss(weight=torch.tensor([1.0, 3.0])),权重按类别频率反比设;或者用focal loss。重采样也能用,但过采样容易过拟合,欠采样会丢信息,优先调权重。
4.5 验证集指标虚高但测试集崩盘
现象:验证集F1 0.92,换一批数据掉到0.6。原因:验证集和训练集同分布,但测试集来自不同时间或不同来源,存在分布偏移。虚假新闻检测里这个问题特别严重,因为假新闻的写法变化很快。解决:划分数据时按时间切分,用早期数据训练、后期数据测试;或者在训练集里加入不同来源的样本做域增强。文档说明里要明确标注数据划分方式,否则别人复现时指标对不上。
5. 进阶技巧:用对比学习提升图文一致性判别能力
跑通baseline之后,如果想把F1再往上推3到5个点,可以加一个辅助任务:图文对比学习。核心思想是让匹配的图文对在特征空间里靠近,不匹配的远离。具体做法是在融合层之后加一个投影头,把文本池化向量和图像向量映射到128维,用InfoNCE损失训练。
class ContrastiveHead(nn.Module): def __init__(self, in_dim=768, proj_dim=128): super().__init__() self.text_proj = nn.Sequential(nn.Linear(in_dim, proj_dim), nn.ReLU(), nn.Linear(proj_dim, proj_dim)) self.image_proj = nn.Sequential(nn.Linear(in_dim, proj_dim), nn.ReLU(), nn.Linear(proj_dim, proj_dim)) self.temperature = nn.Parameter(torch.tensor(0.07)) def forward(self, text_vec, image_vec): t = nn.functional.normalize(self.text_proj(text_vec), dim=-1) i = nn.functional.normalize(self.image_proj(image_vec), dim=-1) logits = t @ i.T / self.temperature # [B, B] labels = torch.arange(t.size(0), device=t.device) loss_t2i = nn.functional.cross_entropy(logits, labels) loss_i2t = nn.functional.cross_entropy(logits.T, labels) return (loss_t2i + loss_i2t) / 2总损失是分类损失加0.1倍对比损失。温度参数0.07是SimCLR的经验值,可以学但初始值别乱设。对比学习对batch size敏感,batch越大负样本越多效果越好,所以尽量用梯度累积把等效batch推到64以上。这个辅助任务在图文错配样本多的数据集上提升最明显,因为模型被迫学会判断图文是否真的对应,而不是各自独立分类。
验证对比学习是否有效,看两个信号:一是分类F1有没有涨,二是图文检索的Recall@1有没有提升。如果分类没涨但检索涨了,说明对比损失权重太大,压到0.05再试。另外注意,对比学习需要每个batch里没有重复样本,否则正样本对会混淆,DataLoader的shuffle必须开。
我自己的习惯是:任何多模态方案,先用纯文本跑一个baseline,记住它的F1;加上图像后如果提升不到2个点,先别急着调模型,回去查数据配对和图像质量。十次里有七次是数据问题,不是模型问题。希望帮到你。
本文还有配套的精品资源,点击获取