简介这份资源面向计算机相关专业的本科生与研究生以及需要完成毕业设计、期末大作业或课程设计的学习者提供一套基于Python的虚假新闻检测多模态识别完整项目。项目融合文本与图像等多模态特征采用BERT等预训练模型进行特征提取并引入LightGBM、CatBoost等集成学习策略进行模型融合帮助读者理解从数据预处理、模型训练到预测评估的全流程。压缩包共39个文件约353KB包含16个Python源码文件、4个Markdown说明文档、4个TXT配置与依赖清单、3个Shell运行脚本、3个TSV数据文件以及JSON配置、模型检查点、Jupyter Notebook和训练日志等代码注释详尽新手也能快速上手。目前已有498人学习下载。资源不仅提供可直接部署运行的完整代码还附带文档说明与依赖清单方便读者复现实验、调整参数并迁移到自己的课题中具有较高的实际应用与参考价值。1. 虚假新闻检测的多模态路线为什么单看文本已经不够用了一条假消息能骗过文本模型往往不是因为它写得多好而是因为它配了一张真图。图片是真的文字是编的两者单独看都没问题合在一起才是谎言。这就是基于 Python 的虚假新闻检测多模态识别要解决的核心问题把文本、图像甚至传播结构放在同一个判断框架里而不是各判各的。这个方向适合两类人一类是手里已经有新闻数据集、想做内容审核或舆情分析的工程师另一类是想找一个能跑通、能改、能写进文档的多模态入门项目的人。标题里提到的源代码和文档说明本质上是一套可复现的工程骨架重点不在模型多深而在于多模态数据怎么对齐、特征怎么融合、结果怎么解释。下面按我实际搭过的一套方案从数据到推理逐层拆开讲。2. 多模态检测的骨架文本、图像、融合层各自在干什么2.1 为什么不能把文本和图像简单拼接很多人第一反应是把文本向量和图像向量直接 concat然后丢进全连接层。这个做法在早期多模态论文里确实出现过但实际跑起来会发现两个问题一是文本特征维度通常远大于图像特征拼接后图像信息被淹没二是两个模态的语义空间没有对齐模型学到的只是统计相关不是真正的跨模态证据。常见做法是先把两个模态各自编码到同一维度再用注意力机制做交互。文本侧用预训练语言模型抽 token 级表示图像侧用 CNN 或 ViT 抽 patch 级表示然后通过跨模态注意力让文本 token 去查询图像 patch反之亦然。这样模型能学到「这句话在描述图片的哪个区域」以及「这个区域是否支持这句话」。我一般会把融合层设计成两阶段先做模态内编码再做模态间交互。模态内编码负责把原始输入压缩成紧凑表示模态间交互负责找矛盾点。虚假新闻的判别信号往往就藏在矛盾里比如文本说「某地发生爆炸」图片却是一张几年前的旧图模型需要捕捉到这种不一致。2.2 数据管线的搭建从原始新闻到模型输入多模态检测的第一步不是建模而是把数据整理成模型能吃的格式。典型输入包括新闻正文、标题、配图、发布时间、来源。如果做传播结构分析还要加转发关系。下面是一个最小可用的数据组织脚本把文本和图像路径对齐成 JSONL。import json import os from PIL import Image def build_multimodal_records(raw_dir, image_dir, output_path): 把原始新闻目录整理成多模态训练记录。 raw_dir: 每行一个 JSON包含 id/text/title/image_name/label image_dir: 图片存放目录 output_path: 输出 JSONL 路径 records [] for fname in os.listdir(raw_dir): if not fname.endswith(.json): continue with open(os.path.join(raw_dir, fname), r, encodingutf-8) as f: item json.load(f) img_path os.path.join(image_dir, item[image_name]) # 图片缺失时跳过避免训练时反复报错 if not os.path.exists(img_path): continue # 统一转成 RGB防止灰度图或 RGBA 导致后续 transform 失败 img Image.open(img_path).convert(RGB) records.append({ id: item[id], text: item[title] item[text], image_path: img_path, label: int(item[label]), source: item.get(source, unknown) }) with open(output_path, w, encodingutf-8) as f: for r in records: f.write(json.dumps(r, ensure_asciiFalse) \n) print(fbuilt {len(records)} records) if __name__ __main__: build_multimodal_records(./raw, ./images, ./train.jsonl)这段代码的关键点有三个第一文本用标题加正文拼接标题往往比正文更有判别力第二图片统一转 RGB避免通道数不一致导致 DataLoader 崩溃第三缺失图片直接跳过而不是用占位图因为占位图会引入噪声。参数上raw_dir和image_dir按实际目录改output_path建议放在项目根目录下方便后续 Dataset 类读取。2.3 文本编码器的选型BERT 还是轻量模型文本侧的选择取决于你的算力和数据量。如果数据量在几万条以上直接用 BERT-base 或 RoBERTa-base 微调效果稳定。如果数据量小、显存有限可以用 DistilBERT 或中文的轻量预训练模型。我一般会先跑一个基线用预训练模型冻结底层只训练分类头和融合层等管线跑通后再解冻微调。from transformers import AutoTokenizer, AutoModel import torch import torch.nn as nn class TextEncoder(nn.Module): def __init__(self, model_namebert-base-chinese, freezeTrue): super().__init__() self.tokenizer AutoTokenizer.from_pretrained(model_name) self.bert AutoModel.from_pretrained(model_name) if freeze: for p in self.bert.parameters(): p.requires_grad False self.out_dim self.bert.config.hidden_size def forward(self, texts): # texts: list[str] enc self.tokenizer(texts, paddingTrue, truncationTrue, max_length128, return_tensorspt) enc {k: v.to(next(self.bert.parameters()).device) for k, v in enc.items()} out self.bert(**enc) # 用 [CLS] 向量作为整句表示 return out.last_hidden_state[:, 0, :]max_length128是权衡速度和信息量的结果新闻标题加正文前几句通常够用。freezeTrue时只训练后续层显存占用能降一半左右。如果发现验证集效果上不去再把freeze改成False学习率调到 2e-5 左右做全量微调。2.4 图像编码器与跨模态注意力融合图像侧用 ResNet 或 ViT 都行ResNet 更省显存ViT 在小数据上容易过拟合。我一般用 ResNet-50 抽 2048 维特征再投影到和文本相同的维度。融合层用双向跨模态注意力让文本和图像互相查询。import torch.nn.functional as F class CrossModalFusion(nn.Module): def __init__(self, dim768, num_heads8): super().__init__() self.text_proj nn.Linear(768, dim) self.img_proj nn.Linear(2048, dim) self.cross_attn_t2i nn.MultiheadAttention(dim, num_heads, batch_firstTrue) self.cross_attn_i2t nn.MultiheadAttention(dim, num_heads, batch_firstTrue) self.norm1 nn.LayerNorm(dim) self.norm2 nn.LayerNorm(dim) def forward(self, text_feat, img_feat): # text_feat: [B, D_text], img_feat: [B, D_img] t self.text_proj(text_feat).unsqueeze(1) # [B, 1, D] i self.img_proj(img_feat).unsqueeze(1) # [B, 1, D] # 文本查询图像 t2i, _ self.cross_attn_t2i(t, i, i) t self.norm1(t t2i) # 图像查询文本 i2t, _ self.cross_attn_i2t(i, t, t) i self.norm2(i i2t) # 拼接后过分类头 fused torch.cat([t.squeeze(1), i.squeeze(1)], dim-1) return fused这里dim768是为了和 BERT-base 对齐num_heads8是常用配置。跨模态注意力只做一层数据量不大时够用如果数据超过十万条可以堆两层并加残差。融合后的向量维度是 1536接一个两层 MLP 分类头即可输出真假概率。2.5 训练循环与评估指标训练时要注意两个模态的学习率可能不同。文本编码器如果解冻学习率要小融合层和分类头可以大一些。评估不能只看准确率虚假新闻检测里类别通常不平衡F1 和 AUC 更有参考价值。from sklearn.metrics import f1_score, roc_auc_score import numpy as np def evaluate(model, dataloader, device): model.eval() all_preds, all_labels, all_probs [], [], [] with torch.no_grad(): for batch in dataloader: text batch[text] img batch[image].to(device) labels batch[label].to(device) logits model(text, img) probs torch.softmax(logits, dim-1)[:, 1] preds torch.argmax(logits, dim-1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) all_probs.extend(probs.cpu().numpy()) f1 f1_score(all_labels, all_preds, averagebinary) auc roc_auc_score(all_labels, all_probs) return {f1: f1, auc: auc}averagebinary适用于二分类如果是多分类改成macro。AUC 用正类概率计算所以取 softmax 后第二列。实际跑的时候如果 F1 高但 AUC 低说明模型在阈值附近不稳定需要检查融合层是否学到了有效交互。3. 避坑与排查多模态检测里最容易翻车的五个地方3.1 文本和图像不对应模型学到假关联现象训练集准确率很高验证集掉得厉害。原因数据里存在大量文本和图片不匹配的样本模型记住了「某类图片配某类文本」的统计规律而不是真正的语义矛盾。解决在数据管线里加一步图文一致性过滤用 CLIP 之类的模型算相似度低于阈值的样本人工复核或丢弃。3.2 图像预处理不一致导致推理报错现象训练时正常推理时单张图片报通道数错误。原因训练用 DataLoader 统一转 RGB推理脚本直接读原图遇到灰度图或 PNG 透明通道就崩。解决推理入口也强制Image.open(path).convert(RGB)并在文档里写明支持的格式。3.3 融合层参数量过大小数据直接过拟合现象训练 loss 降到 0.1验证 loss 从第 3 轮开始上升。原因跨模态注意力层参数多数据量不够时容易记住训练样本。解决减少注意力头数、加 dropout、冻结文本编码器或者先用简单拼接跑基线确认数据没问题再上融合层。3.4 类别不平衡导致模型偏向多数类现象准确率 90%但假新闻召回率只有 30%。原因真实新闻远多于假新闻模型学会全预测多数类。解决损失函数用带权重的交叉熵或者用 focal loss评估时看 F1 和召回率不看准确率。3.5 文档说明和代码版本脱节现象按文档装依赖跑不起来报 API 不兼容。原因文档写的是旧版 transformers 的用法代码已经升级。解决在文档里固定依赖版本用 requirements.txt 锁死并在 README 里写清楚 Python 版本和 CUDA 版本。我一般会在项目根目录放一个environment.yml或requirements.txt每次改代码同步更新。4. 把检测结果用起来从单条推理到批量审核的落地技巧模型训完只是第一步真正要用起来得解决推理效率和结果解释。单条推理直接调模型就行但内容审核场景往往是批量进来这时候要做一个带批处理的推理服务。下面这个脚本把模型加载、批量推理和结果输出串起来适合接在审核队列后面。import torch from torch.utils.data import DataLoader, Dataset from PIL import Image from torchvision import transforms class InferenceDataset(Dataset): def __init__(self, records, transform): self.records records self.transform transform def __len__(self): return len(self.records) def __getitem__(self, idx): r self.records[idx] img Image.open(r[image_path]).convert(RGB) return { text: r[text], image: self.transform(img), id: r[id] } def batch_inference(model, records, device, batch_size32): 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]) ]) dataset InferenceDataset(records, transform) loader DataLoader(dataset, batch_sizebatch_size, shuffleFalse) model.eval() results [] with torch.no_grad(): for batch in loader: imgs batch[image].to(device) logits model(batch[text], imgs) probs torch.softmax(logits, dim-1) for i, rid in enumerate(batch[id]): results.append({ id: rid, fake_prob: float(probs[i][1]), label: int(torch.argmax(probs[i])) }) return resultsbatch_size32是显存和吞吐的折中显存小就降到 8 或 16。Normalize用的 ImageNet 均值方差如果图像编码器换成了别的预训练权重这里要跟着改。输出里保留fake_prob而不是只给标签是因为审核场景需要按概率排序优先人工复核高风险的。结果解释方面我习惯在输出里加一个简单的证据字段把文本里和图像注意力权重最高的 token 和 patch 对应起来虽然做不到完美解释但能让审核员看到模型「看了哪里」。这个功能不用改模型结构在前向时把注意力权重存下来即可。最后说一个我踩过的坑不要一上来就追求 SOTA。多模态虚假新闻检测的数据集往往有领域偏差公开数据集上跑得好的模型换到自己的业务数据上可能直接崩。先用小样本跑通全流程确认数据管线和评估指标没问题再逐步加模型复杂度。我现在的习惯是每个新项目先写一个「最小可跑通版本」哪怕 F1 只有 0.6也比一个跑不起来的高分模型有价值。希望帮到你。本文还有配套的精品资源点击获取