简介:这份资源面向深度学习与虚假新闻检测方向的学习者和研究者,提供一套基于PyTorch框架的多模态检测系统实现。系统以BERT预训练模型提取文本深层语义特征,以ResNet卷积神经网络提取图像特征,并引入对比学习技术增强真实与虚假新闻的判别能力,支持在微博谣言数据集上完成训练与评估,适合作为课程设计、毕业设计或相关课题的参考方案。压缩包共21个文件,约1.48MB,以12个Python脚本为核心,涵盖模型定义、数据处理、训练与配置等模块,另含4个txt说明、3个csv数据文件及docx、md文档,便于快速理解项目结构与运行流程。目前已有96人学习下载。通过该资源,读者可掌握多模态特征融合的完整实现思路,理解BERT与ResNet的协同建模方式,并借助对比学习提升检测精度,为社交媒体虚假信息识别提供可复用的工程范例。
1. 拆开这个多模态虚假新闻检测包:BERT+ResNet 到底怎么配合干活
微博上的假新闻有个特点,配图往往比文字更有迷惑性。一张移花接木的现场图,配上一段情绪拉满的文字,单看文本分类器很容易被骗过去。这个资源包解决的就是这个问题:用 BERT 抽文本特征、ResNet 抽图像特征,再通过对比学习把两个模态拉到同一个语义空间里做真假判断。技术栈是 PyTorch,数据集走的是微博谣言数据集,训练和评估脚本都封装好了。
适合谁用?如果你正在做多模态相关的课程设计、毕设,或者想找一个能跑通的跨模态融合 baseline,这个包的门槛不算高——只要你会配 PyTorch 环境、能读懂 Dataset 和 DataLoader 的基本写法,剩下的就是调参和看指标。但如果你指望开箱即用、不碰任何配置就能出论文级结果,那得先做好踩坑的心理准备,多模态的坑比单模态多得多。
2. 环境搭建与数据准备:从零把训练管线跑起来
2.1 PyTorch 环境与依赖版本选择
这个项目对 PyTorch 版本不算特别挑剔,但有几个依赖的版本组合会直接影响能不能跑通。我一般会先确认 CUDA 版本,再倒推 PyTorch 的安装命令。如果你用的是 CUDA 12.1 及以上,直接走官方推荐命令就行;如果是 11.8,注意 torchvision 的版本要对应上,否则 ResNet 的预训练权重加载会报 key 不匹配。
# 创建独立环境,避免和已有项目冲突 conda create -n fake_news python=3.9 -y conda activate fake_news # CUDA 11.8 对应的 PyTorch 安装(常见做法) pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cu118 # 文本侧依赖 pip install transformers==4.30.2 pip install scikit-learn pandas numpy tqdm tensorboard这里有几个参数值得说清楚。transformers版本不要低于 4.28,因为 BERT 的AutoModel接口在旧版本里对attention_mask的处理有差异,会导致对比学习阶段 loss 异常。torchvision必须和torch主版本号对齐,0.15.x 配 2.0.x 是验证过的组合。装完之后跑一句python -c "import torch; print(torch.cuda.is_available())",返回 True 才算环境就绪。
提示:如果你在 WSL 里配环境,CUDA 驱动走 Windows 侧就行,WSL 内部只需要装 CUDA Toolkit 的运行时,不用重复装驱动。
2.2 微博谣言数据集的目录结构与加载逻辑
这个包默认的数据组织方式是「文本文件 + 图片文件夹 + 标签 CSV」三件套。常见做法是把数据根目录设成data/weibo_rumor/,下面分texts/、images/、labels.csv。labels.csv 至少要有三列:text_id、image_id、label,label 用 0/1 表示真/假。
import os import pandas as pd from torch.utils.data import Dataset from PIL import Image class WeiboRumorDataset(Dataset): def __init__(self, root_dir, label_file, tokenizer, transform, max_len=128): self.root = root_dir self.df = pd.read_csv(os.path.join(root_dir, label_file)) self.tokenizer = tokenizer self.transform = transform self.max_len = max_len def __len__(self): return len(self.df) def __getitem__(self, idx): row = self.df.iloc[idx] # 文本侧:BERT tokenizer 输出 input_ids 和 attention_mask text_path = os.path.join(self.root, 'texts', f"{row['text_id']}.txt") with open(text_path, 'r', encoding='utf-8') as f: text = f.read().strip() enc = self.tokenizer(text, max_length=self.max_len, padding='max_length', truncation=True, return_tensors='pt') # 图像侧:ResNet 要求 224x224 输入 img_path = os.path.join(self.root, 'images', f"{row['image_id']}.jpg") image = Image.open(img_path).convert('RGB') image = self.transform(image) return { 'input_ids': enc['input_ids'].squeeze(0), 'attention_mask': enc['attention_mask'].squeeze(0), 'image': image, 'label': row['label'] }逻辑说明:max_len=128是微博文本的合理截断长度,微博正文普遍短,128 个 token 覆盖 95% 以上的样本。padding='max_length'保证 batch 内张量对齐,不然 DataLoader 的 collate 会报错。图像统一转 RGB 是因为数据里可能混有灰度图或 PNG 带 alpha 通道的情况,不转的话 ResNet 第一层卷积会直接崩。
参数怎么改:如果你的显存吃紧,把max_len降到 64,batch_size 设成 16,基本能在 8G 显存上跑起来。图像 transform 里记得加Resize(256)再CenterCrop(224),这是 ResNet 预训练时的标准预处理,直接 Resize 到 224 会损失边缘信息。
3. 双塔特征提取与对比学习融合:模型结构拆解
3.1 BERT 文本编码器与 ResNet 图像编码器的接法
模型主体是双塔结构:文本塔用bert-base-chinese,图像塔用resnet50(pretrained=True)。关键改动在两处——BERT 的输出取pooler_output(768 维),ResNet 去掉最后的全连接层,取全局平均池化后的 2048 维特征。两个维度不一致,所以中间要加投影头把维度对齐到同一个空间。
import torch import torch.nn as nn from transformers import BertModel from torchvision import models class DualEncoder(nn.Module): def __init__(self, proj_dim=256): super().__init__() # 文本塔:加载中文 BERT self.text_encoder = BertModel.from_pretrained('bert-base-chinese') # 图像塔:ResNet50 去掉 fc 层 resnet = models.resnet50(pretrained=True) self.image_encoder = nn.Sequential(*list(resnet.children())[:-1]) # 投影头:把两个模态映射到同一维度 self.text_proj = nn.Sequential( nn.Linear(768, 512), nn.ReLU(), nn.Linear(512, proj_dim) ) self.image_proj = nn.Sequential( nn.Linear(2048, 512), nn.ReLU(), nn.Linear(512, proj_dim) ) def forward(self, input_ids, attention_mask, image): text_out = self.text_encoder(input_ids=input_ids, attention_mask=attention_mask) text_feat = text_out.pooler_output # (B, 768) img_feat = self.image_encoder(image) # (B, 2048, 1, 1) img_feat = img_feat.flatten(1) # (B, 2048) text_emb = self.text_proj(text_feat) image_emb = self.image_proj(img_feat) # L2 归一化,方便后续算余弦相似度 text_emb = nn.functional.normalize(text_emb, dim=-1) image_emb = nn.functional.normalize(image_emb, dim=-1) return text_emb, image_emb为什么用pooler_output而不是last_hidden_state的均值?pooler_output经过一层 tanh 变换,对分类任务更友好,而且维度固定,省去手动池化的麻烦。ResNet 那边用flatten(1)把 (B, 2048, 1, 1) 压成 (B, 2048),比用view更安全,不会因为 batch 维度顺序问题翻车。
投影维度proj_dim=256是个经验值。太小会丢信息,太大对比学习收敛慢。如果你数据量超过 5 万条,可以提到 512;数据量小就保持 256,防止过拟合。
3.2 对比学习损失函数与训练循环
对比学习的核心思路是:同一个样本的文本和图像特征应该靠近,不同样本的特征应该远离。这里用的是 InfoNCE 损失的变体,温度系数设 0.07。
class ContrastiveLoss(nn.Module): def __init__(self, temperature=0.07): super().__init__() self.temp = temperature def forward(self, text_emb, image_emb): # 计算相似度矩阵 (B, B) logits = text_emb @ image_emb.T / self.temp labels = torch.arange(logits.size(0), device=logits.device) # 对称损失:文本找图像 + 图像找文本 loss_t2i = nn.functional.cross_entropy(logits, labels) loss_i2t = nn.functional.cross_entropy(logits.T, labels) return (loss_t2i + loss_i2t) / 2训练循环里有个细节容易被忽略:对比学习阶段和分类微调阶段最好分开。先冻结 BERT 和 ResNet 的主干,只训投影头 5 个 epoch,让两个模态先对齐;然后解冻全部参数,接一个分类头做端到端微调。这样做比一上来就端到端训练稳定得多,loss 不会在前期剧烈震荡。
# 阶段一:只训投影头 for param in model.text_encoder.parameters(): param.requires_grad = False for param in model.image_encoder.parameters(): param.requires_grad = False optimizer = torch.optim.AdamW( list(model.text_proj.parameters()) + list(model.image_proj.parameters()), lr=1e-3, weight_decay=1e-4 ) # 阶段二:解冻全部,小学习率微调 for param in model.parameters(): param.requires_grad = True optimizer = torch.optim.AdamW(model.parameters(), lr=2e-5, weight_decay=1e-4)学习率从 1e-3 降到 2e-5 是 BERT 微调的标准操作,主干网络经不起大学习率的折腾。weight_decay=1e-4就是常说的 L2 正则化,在 PyTorch 里通过 AdamW 的 weight_decay 参数实现,比手动加 L2 惩罚项更干净。
4. 训练与评估实操:指标怎么看、参数怎么调
4.1 训练脚本的关键参数与日志监控
训练脚本一般会暴露这几个参数:batch_size、lr、epochs、temperature、proj_dim。我习惯用 argparse 管理,方便做消融实验。
import argparse parser = argparse.ArgumentParser() parser.add_argument('--batch_size', type=int, default=32) parser.add_argument('--lr', type=float, default=2e-5) parser.add_argument('--epochs', type=int, default=10) parser.add_argument('--temperature', type=float, default=0.07) parser.add_argument('--proj_dim', type=int, default=256) parser.add_argument('--data_root', type=str, default='data/weibo_rumor') args = parser.parse_args()batch_size 设 32 是个平衡点。对比学习依赖 batch 内的负样本,batch 太小负样本不够,对比效果打折扣;batch 太大显存扛不住。如果你只有一张 8G 卡,降到 16,同时把 temperature 提到 0.1,补偿负样本减少带来的影响。
日志方面,除了 loss,重点盯三个指标:文本-图像相似度的均值(应该随训练上升)、分类准确率、F1。如果相似度均值一直不涨,说明投影头没学好,检查一下归一化是不是漏了。
4.2 评估指标与混淆矩阵的读法
虚假新闻检测不能只看准确率。微博谣言数据集通常正负样本不均衡,假新闻占比可能只有 30% 左右。这时候准确率会被多数类带偏,F1 和 AUC 才是靠谱指标。
from sklearn.metrics import classification_report, confusion_matrix 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) images = batch['image'].to(device) labels = batch['label'].to(device) text_emb, image_emb = model(input_ids, attention_mask, images) # 融合方式:拼接后过分类头 fused = torch.cat([text_emb, image_emb], dim=-1) logits = model.classifier(fused) preds = logits.argmax(dim=-1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) print(classification_report(all_labels, all_preds, digits=4)) print(confusion_matrix(all_labels, all_preds))classification_report里重点看假新闻类(label=1)的 recall。如果 recall 低,说明模型漏检多,假新闻被放过了,这在实际场景里比误判更危险。混淆矩阵里如果 FP 高、FN 低,可以适当调低分类阈值;反过来就调高。
注意:评估时一定要用
model.eval()切到推理模式,否则 BatchNorm 和 Dropout 还在训练状态,指标会飘。
5. 避坑与排查:多模态训练里那些血泪经验
5.1 常见问题排查
现象一:loss 变成 NaN。原因通常是对比学习阶段温度系数太小,相似度矩阵除以 0.07 后数值爆炸。解决:把 temperature 提到 0.1 或 0.2,或者在计算 logits 前对 embedding 做 clamp。
现象二:ResNet 预训练权重加载报错。报错信息里出现unexpected key或missing key,多半是 torchvision 版本和权重文件不匹配。解决:确认 torchvision 版本,或者用pretrained=False先跑通流程,再换回预训练。
现象三:显存溢出但 batch_size 已经很小。检查是不是在__getitem__里做了图像增强的多次复制,或者 BERT 的max_len设太大。解决:把max_len降到 64,图像 transform 里去掉 RandomRotation 这类耗显存的操作。
现象四:训练集 loss 降但验证集不降。典型过拟合。解决:加大 weight_decay 到 1e-3,或者在投影头里加 Dropout(0.3),再不行就减少 proj_dim。
现象五:文本和图像特征相似度始终在 0.5 附近。说明两个模态根本没对齐。解决:检查投影头后面有没有做 L2 归一化,以及对比学习的 labels 是不是torch.arange(batch_size),这个最容易写错。
5.2 数据层面的隐蔽坑
微博谣言数据集的图片里,有一部分是截图或长图,直接 Resize 到 224 会糊成一片。常见做法是先做短边 Resize 到 256,再 CenterCrop 224,保留主体区域。另外,文本里如果有大量 @ 和话题标签,建议在预处理阶段去掉,这些 token 对真假判断没有信息量,反而干扰 BERT 的注意力。
还有一个容易翻车的地方:训练集和验证集的划分要按事件划分,不能随机划分。同一个事件的文本和图片如果同时出现在训练集和验证集里,指标会虚高。这个包默认是按随机划分的,如果你要发论文,记得改成按事件 ID 分组划分。
6. 进阶技巧:把对比学习权重用出额外价值
跑通基础流程之后,这个包最值钱的部分其实是对比学习训出来的双塔编码器。它不只能做真假分类,还能直接拿来做跨模态检索——用文本搜图片,或者用图片搜相关报道。具体做法是:把训练好的text_proj和image_proj单独抽出来,对所有样本的文本和图像分别编码,存成一个特征库,然后用余弦相似度做近邻搜索。
# 构建跨模态检索特征库 def build_retrieval_index(model, dataloader, device): model.eval() text_feats, image_feats, ids = [], [], [] with torch.no_grad(): for batch in dataloader: t_emb, i_emb = model( batch['input_ids'].to(device), batch['attention_mask'].to(device), batch['image'].to(device) ) text_feats.append(t_emb.cpu()) image_feats.append(i_emb.cpu()) ids.extend(batch['id']) text_feats = torch.cat(text_feats, dim=0) image_feats = torch.cat(image_feats, dim=0) # 保存成 npy,后续用 faiss 或 sklearn 做近邻检索 torch.save({'text': text_feats, 'image': image_feats, 'ids': ids}, 'retrieval_index.pt') return text_feats, image_feats这个特征库的用法很灵活。比如你拿到一条新微博,想找历史上有没有相似谣言,就用文本特征去检索;如果只有一张图,想找相关报道,就用图像特征去搜。相似度阈值我一般设 0.75,低于这个值的结果噪声太大,不如不返回。
验证对比学习有没有真正学到东西,有个简单办法:把同一事件的文本和图像特征算相似度,再随机抽不同事件的文本图像对算相似度,如果前者显著高于后者(差距在 0.2 以上),说明对齐是有效的。如果差距很小,回去检查投影头的归一化和温度系数。
从那以后我每次训完对比学习模型,都会强制走一遍跨模态检索验证,不跑这一步就不敢说模型真的把两个模态对齐了。希望帮到你。
本文还有配套的精品资源,点击获取