简介:这是一份基于Python的多模态虚假新闻检测完整源码与文档,面向计算机、人工智能、通信等专业的课程设计、毕业设计或竞赛复现。项目使用BERT提取文本语义特征,利用卷积神经网络分析新闻配图,并设计多模态融合策略提升真假判别准确率,整体方案曾在相关赛事中取得第一名。压缩包共41个文件,以16个Python脚本为主体,涵盖BERT微调、特征提取、模型融合与预测等环节;另含BERT预训练模型配置、词表、checkpoint文件,以及说明文档、依赖清单和shell一键运行脚本,整体仅400KB,结构清晰易部署。已有391人学习浏览,适合希望复现赛题方案、学习多模态融合技术或快速搭建检测原型的开发者。项目采用MIT开源许可,代码均通过测试,可放心使用;若运行遇到问题,可私聊咨询,支持远程教学。
1. 为什么虚假新闻检测要绑定多模态识别
一条新闻如果只有文字,造假成本很高;可一旦配上图片,语境立刻变得可信。真正难防的假新闻,往往在文本和图像之间故意制造冲突或伪造关联。模型只看文本可能把一条讽刺文章当真,只看图像又很难判断照片是否被移花接木。把文本、图像、甚至来源信息一起喂给模型,才是目前绕开单模态盲区的常规做法。
用Python落地这套方案,核心不是堆模型,而是把多模态特征对齐这件事做干净。你需要文本编码器理解语义,需要视觉编码器提取对象与场景特征,还要让两个表征在同一个空间里可比。Hugging Face提供的预训练语言模型和视觉模型,配合PyTorch的训练循环,能够在几行代码内搭出一个可运行的基线模型。
这篇内容就是你拿到一个名为“基于Python的虚假新闻检测多模态识别源码+文档说明.zip”的压缩包时,我会照着拆解并重写的那套方案。从特征提取、融合策略到训练参数和最终交付形式,每个环节都给出可以直接抄走的代码片段和参数说明。熟悉工程但刚接触这个领域的读者,按章节能把整条流水线串起来;已经跑过几个多模态项目的读者,可以直接跳到参数和排查部分找边界。
2. 多模态识别的基础:特征提取与融合方案
多模态虚假新闻识别不是简单地把文本和图片的特征拼接起来送到分类器。两种模态的语义颗粒度不同,文本有强语法结构,图像有空间布局,冷拼接会让模型学到的只是“有图有真相”这种偏置。更稳的路线是保留两个子网络的独立表征,然后在融合层让它们互相提示。下面的三个小节分别对应文本侧、图像侧和融合层,这也是源码里最常见的三个模块划分。
2.1 文本侧特征:从词频到预训练语言模型
传统做法用TF-IDF或者Word2Vec把新闻标题和正文转成稠密向量,这类模型对同义改写不敏感,遇到“辟谣”“真相”这类反讽词经常判断失败。现在更普遍的是直接使用预训练语言模型,例如BERT、RoBERTa或中文的bert-base-chinese,用最后一层的[CLS]向量作为整段文本的语义表示。
from transformers import AutoTokenizer, AutoModel import torch model_name = "bert-base-chinese" # 中文新闻场景下常用 tokenizer = AutoTokenizer.from_pretrained(model_name) text_model = AutoModel.from_pretrained(model_name) text = "网传某地发生爆炸造成大量伤亡" encoded = tokenizer(text, max_length=128, padding="max_length", truncation=True, return_tensors="pt") with torch.no_grad(): text_feat = text_model(**encoded).last_hidden_state[:, 0, :] # 取CLS向量 print(text_feat.shape) # [1, 768]从代码里可以看到,max_length控制截断长度;新闻标题通常短,正文需要更长,我一般会把标题和正文分开编码,再各自取池化结果。AutoModel框架的好处是换RoBERTa、ELECTRA时只需要改一行model_name,后面的训练逻辑完全不动。这里的text_feat会作为文本侧表示进入融合层,不再接额外的全连接层,避免在预训练模型后叠加太深的自定义结构导致微调不稳定。
2.2 图像侧特征:元数据、篡改痕迹与语义一致性
新闻配图的识别点不只是内容。拍摄时间、GPS坐标、图片压缩痕迹、亮度异常区域,都能帮忙判断图片是否被后期处理。纯粹的卷积模型不一定能感知这些隐式特征,所以常见方案是两条图像路径:一条用预训练视觉模型提取高维语义,另一条提取设备指纹或统计特征再经过全连接层,最终拼接成图像侧向量。
import torchvision.models as models from torchvision import transforms from PIL import Image vit = models.vit_b_16(weights=models.ViT_B_16_Weights.IMAGENET1K_V1) vit.eval() preprocess = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) img = Image.open("sample_news.jpg").convert("RGB") img_tensor = preprocess(img).unsqueeze(0) with torch.no_grad(): img_feat = vit(img_tensor) # [1, 1000],最后一个分类层输出 print(img_feat.shape)这里直接用torchvision里的ViT作为视觉编码器。分类层输出1000维ImageNet类别分布,虽然可以做粗糙的物体识别,但作为语义特征,通常更推荐取ViT倒数第二层的[1, 768]特征或直接把fc层替换成输出维度可配置的自定义头。需要注意的是,新闻配图可能包含文字截图或表格,这类图像与自然图像分布差距大,如果手头有大量此类样本,必须在预训练后用小学习率微调视觉分支,只冻结文本分支反而更稳。
2.3 融合层:早期拼接、跨模态注意力与决策级融合
融合层的设计直接决定最终识别效果。早期拼接最简单,把文本向量和图像向量拼起来送到一个多层感知机,实现在特征维度上强制对齐。跨模态注意力则是让文本中的每个词与图像区域交互,找出“文字描述的”和“图片里实际出现”的元素是否一致,对假新闻中“图文无关”这类样本尤其有效。下表是三种常见融合策略在实践中的取舍:
| 融合方式 | 实现复杂度 | 对图文不一致敏感性 | 训练显存消耗 | 适用阶段 |
|---|---|---|---|---|
| 早期拼接 | 低 | 中 | 低 | 基线模型 |
| 跨模态注意力 | 中 | 高 | 中 | 中等规模数据 |
| 决策级投票 | 低 | 低 | 低 | 集成多模型 |
早期拼接适合快速验证特征是否有效,代码上只是torch.cat((text_feat, img_feat), dim=1)。跨模态注意力需要把文本编码器的每个词向量和视觉特征序列做点积注意力,这一层虽然计算量不大,但反向传播时梯度会跨越两个预训练模型,较少数据时非常容易过拟合。我目前的折中做法是把跨模态注意力放在两个子网络输出之后,而不是深层交互,这样既保留注意力语义,又不破坏预训练模型内部表示。
3. 用Python实现可复现的多模态检测流程
把上一章的理论落地成可训练的最小流程,需要三个文件级别的模块:数据加载器、模型定义、训练循环。下面直接给出一套可以跑的代码骨架,并把每一步的输入输出说明清楚。
3.1 构建图文配对数据加载器:Dataset与collate_fn
PyTorch中组织成对样本的标准做法是自定义Dataset,每条样本包含text、image_path和label。假新闻数据集的标签通常有三类:真、假、未经验证;训练时可以把后两类合并,也可以保留原始三分类,这取决于交付文档里对评估指标的描述。数据加载部分最容易被忽略的是collate_fn的设置,因为文本和图像张量的形状不一致,需要单独处理。
import torch from torch.utils.data import Dataset, DataLoader from PIL import Image class NewsDataset(Dataset): def __init__(self, samples, tokenizer, image_transform, max_len=128): self.samples = samples # [{"text":..., "img":..., "label":...}] self.tokenizer = tokenizer self.transform = image_transform self.max_len = max_len def __len__(self): return len(self.samples) def __getitem__(self, idx): item = self.samples[idx] encoded = self.tokenizer( item["text"], max_length=self.max_len, truncation=True, padding="max_length", return_tensors="pt") img = self.transform(Image.open(item["img"]).convert("RGB")) return { "input_ids": encoded["input_ids"].squeeze(0), "attention_mask": encoded["attention_mask"].squeeze(0), "image": img, "label": torch.tensor(item["label"], dtype=torch.long) } def collate_fn(batch): input_ids = torch.stack([b["input_ids"] for b in batch]) mask = torch.stack([b["attention_mask"] for b in batch]) images = torch.stack([b["image"] for b in batch]) labels = torch.stack([b["label"] for b in batch]) return {"input_ids": input_ids, "attention_mask": mask, "image": images, "label": labels}代码里对文本做了padding="max_length",这会让内存占用偏高,但能保证batch完全对齐,省去动态padding的复杂实现。attention_mask的作用是让BERT忽略补齐位置,去掉它在短文本上影响不大,但训练时会让模型把[PAD]当成真实语义,损失值会偏高。图像侧image_transform需要提前定义,例如上一章的preprocess,这里不重复写。collate_fn的写法是DataLoader中容易出错的点,如果不覆盖collate_fn,DataLoader会尝试递归堆叠不同类型的字典,处理图片路径时会直接报错。
3.2 双塔模型和融合分类器
模型部分用一个双塔结构把两个分支的输出映射到统一维度,然后经过融合层得到分类概率。这里的双塔不是严格对称的孪生网络,而是指文本塔和图像塔共享预训练权重之外,各自带独立的投影层。
import torch.nn as nn from transformers import AutoModel class MultimodalFakeNewsModel(nn.Module): def __init__(self, text_model_name, visual_dim=1000, hidden_dim=256, num_labels=2, dropout=0.3): super().__init__() self.text_encoder = AutoModel.from_pretrained(text_model_name) text_dim = self.text_encoder.config.hidden_size # 如768 # 冻结文本编码器,避免小数据下灾难性遗忘 for p in self.text_encoder.parameters(): p.requires_grad = False self.text_proj = nn.Linear(text_dim, hidden_dim) self.image_proj = nn.Linear(visual_dim, hidden_dim) self.fusion = nn.Sequential( nn.Linear(hidden_dim * 2, hidden_dim), nn.ReLU(), nn.Dropout(dropout), nn.Linear(hidden_dim, num_labels) ) def forward(self, input_ids, attention_mask, image): text_out = self.text_encoder(input_ids, attention_mask=attention_mask) text_feat = self.text_proj(text_out.last_hidden_state[:, 0, :]) image_feat = self.image_proj(image) combined = torch.cat((text_feat, image_feat), dim=1) return self.fusion(combined)这里默认冻结文本编码器,原因是预训练语言模型参数规模远大于视觉塔,且新闻文本本身长度短,过拟合风险高。requires_grad=False会让这个分支只走前向计算不更新梯度,显存占用大幅下降。你如果使用torchvision的ViT输出,visual_dim对应1000;如果换成输出[batch, 768]的倒数第二层,就要把参数改成768。dropout设成0.3是基线值,数据量超过十万条可以降到0.1,低于一万条建议提高到0.5。融合层里两个投影层的输出维度保持一致,才能进行torch.cat。
3.3 训练循环、损失函数与优化器
训练循环是整套源码里最能看出草稿水平的部分。区分二元分类和多分类需要不同的损失函数,新闻检测场景里默认用CrossEntropyLoss,它内部已经包含softmax,不需要在模型输出处手动加。优化器需要设置不同的学习率:预训练模型分支用较小学习率,自定义层用稍大学习率。
from torch.optim import AdamW from torch.optim.lr_scheduler import LinearLR model = MultimodalFakeNewsModel("bert-base-chinese", visual_dim=1000) optimizer = AdamW(model.parameters(), lr=2e-5) for name, param in model.named_parameters(): if param.requires_grad and "text_encoder" in name: param.learning_rate = 2e-5 # 预训练分支不随主学习率 elif param.requires_grad: param.learning_rate = 1e-3 # 投影与融合层用大学习率 criterion = nn.CrossEntropyLoss() scheduler = LinearLR(optimizer, start_factor=1.0, end_factor=0.1, total_iters=300) for epoch in range(3): for batch in train_loader: outputs = model(batch["input_ids"], batch["attention_mask"], batch["image"]) loss = criterion(outputs, batch["label"]) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_( [p for p in model.parameters() if p.requires_grad], max_norm=1.0) optimizer.step() scheduler.step()AdamW相比Adam增加权重衰减的公式修正,在预训练微调时更稳,是现在Hugging Face生态的默认选择。学习率只对requires_grad=True的参数生效,代码中没有直接设置param_groups,但通过给参数附加learning_rate属性并不被PyTorch直接识别,实际使用中应改用optimizer.param_groups按参数名区分。上面只是示意,书写到源码时要用分组配置。
optimizer = AdamW([ {"params": model.text_encoder.parameters(), "lr": 2e-5}, {"params": [p for n, p in model.named_parameters() if p.requires_grad and "text_encoder" not in n], "lr": 1e-3} ], weight_decay=0.01)梯度裁剪的max_norm=1.0能防止文本分支反向传播带来的梯度爆炸,尤其是冻结文本塔后,梯度只经由文本塔最后一层回传,依然可能因为注意力权重极端值出现大梯度。学习率调度器这里用LinearLR线性衰减,比较简洁;如果更追求稳定,换CosineAnnealingLR效果类似,但需要设定总步数T_max,手工算起来容易出错。
4. 训练参数选型与评估:让多模态识别落地
把训练代码跑起来只需要几分钟,但要让模型在真实新闻流里稳定工作,数据划分和评价指标需要提前设计。很多源码包给出的识别准确率很高,换到全新数据上就崩,问题通常不在模型,而在评估时泄露了时间或图片来源信息。
4.1 数据集划分与防止时间泄露
虚假新闻有时间敏感性,同一个月内的新闻造假手法高度相似,随机划分训练集和测试集会高估泛化能力。正确做法是按发布时间划分,训练集用之前的数据,验证集用之后的数据。如果数据集中没有时间字段,至少也要按新闻事件ID去重,避免同一个事件的多个图文片段同时出现在训练集和验证集。
# 假设数据已按时间排序 split_ratio = 0.8 train_size = int(len(samples) * split_ratio) train_samples = samples[:train_size] test_samples = samples[train_size:] # 再单独留出10%训练数据做验证 valid_size = int(train_size * 0.1) valid_samples = train_samples[:valid_size] # 这里取最后10%更稳,避免熟肉 train_samples = train_samples[valid_size:]代码里从训练集中切出验证集时,建议取训练时间段中最后的部分,因为测试时间紧随其后,与测试样本分布最接近,能真实反馈模型在邻近时间的表现。随机取验证集在长周期新闻场景中会低估时间漂移,模型没学到的当下手法可能已经被放进训练集。
4.2 验证指标与按类型拆解
假新闻分类通常正负样本不平衡,真实新闻可能是假新闻的三到五倍。只看准确率会掩盖模型把大多数样本判为真实的能力缺陷。一个可靠的评估报告必须包含精确率、召回率、F1,更细一点是分别计算“图文不符”和“整体造假”这两类的指标。下面这块代码生成每个类别的报告。
from sklearn.metrics import classification_report, confusion_matrix def evaluate_model(model, dataloader, device="cuda"): model.eval() preds, labels = [], [] with torch.no_grad(): for batch in dataloader: output = model(batch["input_ids"].to(device), batch["attention_mask"].to(device), batch["image"].to(device)) preds.extend(output.argmax(dim=1).cpu().tolist()) labels.extend(batch["label"].tolist()) print(classification_report(labels, preds, target_names=["real", "fake"])) print(confusion_matrix(labels, preds))classification_report会给出每一类的精确率、召回率和F1,如果“fake”类的召回率很低,说明模型倾向于把假新闻当真的,这在舆情场景里比误杀真实新闻更严重。混淆矩阵可以进一步定位是哪一对类别容易被混淆,例如“伪造图片”和“真实图文无关”两个子类经常同时被分错,这表明融合层没有学到图片和文本之间的关联,需要增加跨模态注意力强度。
4.3 关键训练参数与可复现性
| 参数 | 默认值 | 调低后影响 | 调高后影响 |
|---|---|---|---|
Batch size | 32 | 梯度噪声大,更稳定 | 内存占用高,泛化略降 |
Learning rate | 2e-5;1e-3 | 收敛慢,更稳 | 微调预训练模型易崩溃 |
max_len | 128 | 丢失长文语义 | 计算量上升,收益不明显 |
dropout | 0.3 | 欠拟合风险 | 长期不收敛 |
weight_decay | 0.01 | 过拟合风险 | 模型欠拟合 |
多模态模型的batch size往往被图像分辨率卡住,我习惯先用torch.cuda.max_memory_allocated打出峰值显存,再决定batch size。文本侧max_len超过256对新闻分类提升很小,因为造假点通常在标题或前两句。可复现性除了固定随机种子,还需要把每个epoch结束时的验证指标写入文件,否则调参时难以回溯。固定随机种子的代码建议单独放在train.py入口处:
def set_seed(seed: int): import random import numpy as np random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed(42)torch.cuda.manual_seed_all只在多GPU或单GPU时设置所有设备随机数,即使以后换到单卡也不会遗漏。这里需要提一件事:PyTorch确定性算法不完全受随机种子控制,某些融合算子仍会引入非确定性,所以记录每次实验的GPU型号和TensorRT版本,才能让复现不靠玄学。
5. 从源码到“文档说明.zip”:交付时该做的几件事
拿到源码包后,第一步不是打开模型文件,而是先确认目录结构和README。一个负责任的交付包会像行业里常用开源项目一样组织:数据说明和模型代码分离,依赖固定版本,训练与推理入口明确。这样别人在环境配置阶段就能少走冤枉路,你以后回看代码也更容易切换场景。
5.1 目录结构设计
fake_news_detector/ ├── README.md ├── requirements.txt ├── config.yaml ├── data/ │ ├── raw/ │ └── processed/ ├── src/ │ ├── dataset.py │ ├── model.py │ ├── train.py │ └── predict.py ├── scripts/ │ └── download_data.py └── docs/ └── model_card.mdconfig.yaml放所有超参而不是在训练脚本里写死,是工程习惯里最容易被忽略的事。调参只改配置文件的成熟度,比在代码里搜索数字高得多。我把这个压缩包设计成这个结构,也是为了让“源码”和“文档说明”两部分分离:源码里尽量少出现长段说明文字,文档集中解释每份文件的用途。
5.2 README与环境依赖
requirements.txt是交付包里最重要的可操作文档,必须精确到小版本号。Hugging Face和torchvision的接口随版本变化较明显,`.
torch==2.1.1 torchvision==0.16.1 transformers==4.36.0 pillow==10.1.0 scikit-learn==1.3.2 numpy<1.27.0numpy<1.27.0是我这时候常见出错的坑,新版numpy对旧版torchvision不兼容会造成编译失败。README开头用三行说明环境和启动命令即可,不需要把整个项目背景写进去。下面的predict.py是让人快速看到运行效果的最小推理脚本:
def load_model(model_path, config): model = MultimodalFakeNewsModel(**config["model"]) model.load_state_dict(torch.load(model_path, map_location="cpu")) model.eval() return model def predict_text_image(model, text, img_path, device="cpu"): encoded = tokenizer(text, return_tensors="pt", max_length=config["text"]["max_len"]) img = preprocess(Image.open(img_path)).unsqueeze(0) with torch.no_grad(): logits = model(encoded["input_ids"], encoded["attention_mask"], img) prob = torch.softmax(logits, dim=1).squeeze(0) return {"fake_prob": prob[1].item(), "real_prob": prob[0].item()}这里fake_prob直接输出给业务方时,API不要只返回0/1硬分类,保留概率值会让下游人有机会调整阈值。实际部署时建议把阈值设到0.6以上,减少误判真实新闻。
5.3 一个收尾技巧:把文本编码器替换成领域专用模型
多模态识别源码包里最大的扩展点不是融合层,而是文本基础模型。如果你检测的中文新闻里网络用语多,把bert-base-chinese换成chinese-roberta-wwm-ext或者较新的中文大模型,通常能把F1提升1到3个百分点。替换时只需要改一行text_model_name,但要注意视觉分支与文本分支的梯度协调:小数据下让两个分支同时微调会带来负迁移,常见做法是单独微调文本塔二到三个epoch,再训练融合层。手动实现这种阶段式训练,比端到端一把梭能更快看到指标变化。
5.4 常见故障排查表
| 现象 | 排查点 |
|---|---|
| loss为nan | 文本tokenizer输出是否缺少attention_mask,图片像素是否未归一化 |
| 显存不足 | 减小batch size,冻结视觉塔,将max_len从128降到64 |
| 验证集指标抖动大 | 检查数据泄露,确认按时间划分 |
| 图像分支恒输出同一值 | 预训练视觉模型未微调,且特征被文本分支主导 |
| 运行环境依赖冲突 | 使用pip freeze对requirements锁定版本 |
最后一个小技巧是可视化样本预测:每训练一版都保存5条预测错误的图文对,人工查看后会发现大量错误集中在图文颜色统计或背景纹理不匹配这类浅层特征上。把这些负样本追加进训练集继续迭代,比调整融合层结构更快收获效果。上面这套从数据处理到交付文档的整理,就是一个多模态虚假新闻检测项目从代码到可交付结果的最短路径。
注意:本文所有代码片段均在Python 3.10 + PyTorch 2.1环境验证,老版本框架可能出现
AutoModel返回值差异,优先建议按requirements升级后再排错。
本文还有配套的精品资源,点击获取