ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python实战:多模态虚假新闻检测模型从搭建到调优

Python实战:多模态虚假新闻检测模型从搭建到调优 简介这份资源是面向高校学生与Python开发者的虚假新闻检测多模态识别项目代码适用于毕业设计、期末大作业与课程设计等场景帮助读者快速搭建可运行的多模态检测方案。压缩包共39个文件约352KB以16个py脚本为核心涵盖模型训练、预测与集成等环节另含4个md说明、4个txt配置、3个sh运行脚本、3个tsv数据文件及json、ipynb、checkpoint等辅助内容结构清晰便于按模块查阅。目前已有246人学习下载。代码注释较为完整新手也能理解整体流程下载后简单部署即可使用读者可从中获得多模态特征融合与虚假新闻识别的完整实现思路、训练与推理脚本、依赖与运行配置以及可参考的目录组织方式便于在此基础上二次修改或直接用于项目答辩与作业提交。1. 从一条假新闻说起多模态检测到底在做什么一条“某地突发山体滑坡”的消息在群里疯传配图是泥石流冲垮房屋的现场照文字写得有鼻子有眼。你把它丢给纯文本模型它可能判“真”因为措辞像新闻通稿你把它丢给纯图像模型它也可能判“真”因为图确实是灾害现场。但把图文放一起看破绽就出来了图片是两年前另一场灾害的旧图文字里的时间地点和图片元数据对不上。这就是虚假新闻检测里最典型的场景——单模态看不出的问题多模态交叉验证能看出来。这篇要讲的是用 Python 搭一套虚假新闻多模态识别代码。核心思路不复杂文本走一路编码图像走一路编码两路特征在中间层做融合最后输出“真/假”二分类。适合谁看有 Python 基础、做过一点深度学习、想找一个能跑通、能改、能写进简历或课程设计的完整项目的人。我不会只给你一个“调包就完事”的脚本而是把数据怎么组织、模型怎么搭、融合层怎么设计、训练时哪里会翻车一条条拆开讲。你照着做能在本地跑出一个可复现的 baseline再往上加自己的改进。2. 多模态虚假新闻检测的模型骨架文本和图像怎么各走一路再合流2.1 为什么不能直接把文本和图像拼成一个向量很多人第一反应是文本用 TF-IDF 抽个向量图像用 CNN 抽个向量直接 concat 起来丢进分类器。这个做法能跑但效果通常很差原因是两个模态的特征空间、尺度、语义密度完全不一样。文本特征可能是 768 维的语义嵌入图像特征可能是 2048 维的视觉特征直接拼接会让分类器偏向维度大、数值范围大的那一侧。更合理的做法是两路各自先降维到同一空间再做融合。常见做法是文本用 BERT 类模型取 [CLS] 向量图像用 ResNet 或 ViT 取全局池化向量各自接一个线性层映射到 256 或 512 维然后做注意力融合或门控融合。我一般会选“双塔 交叉注意力”的结构文本塔和图像塔各自独立编码然后在融合层用文本特征去 query 图像特征再用图像特征去 query 文本特征得到双向的跨模态表示。这样做的好处是模型能学到“文本里的哪个词对应图像里的哪个区域”对虚假新闻检测特别有用因为假新闻的破绽往往就在图文不一致的局部。2.2 文本分支从 BERT 到轻量级替代方案文本分支最省事的做法是直接用 HuggingFace 的 transformers 加载预训练 BERT。但如果你机器显存不够或者想跑得快一点可以用 distilbert 或者甚至用 TextCNN 预训练词向量。下面是一个文本编码器的最小实现用 BERT 做 backbone输出一个固定维度的文本特征向量。import torch import torch.nn as nn from transformers import BertModel, BertConfig class TextEncoder(nn.Module): def __init__(self, pretrained_namebert-base-chinese, out_dim256, freeze_bertFalse): super().__init__() # 加载预训练 BERT中文新闻场景用 bert-base-chinese self.bert BertModel.from_pretrained(pretrained_name) if freeze_bert: # 冻结底层参数只训练顶层省显存 for p in self.bert.parameters(): p.requires_grad False hidden self.bert.config.hidden_size # base 版是 768 # 映射到统一融合维度 self.proj nn.Sequential( nn.Linear(hidden, out_dim), nn.ReLU(), nn.Dropout(0.3) ) def forward(self, input_ids, attention_mask): # 取 [CLS] 位置的输出作为整句表示 outputs self.bert(input_idsinput_ids, attention_maskattention_mask) cls_vec outputs.last_hidden_state[:, 0, :] # (B, hidden) return self.proj(cls_vec) # (B, out_dim)这段代码里out_dim是融合层的统一维度我一般设 256显存紧张可以降到 128。freeze_bert在数据量小于一万条时建议设 True否则 BERT 容易过拟合。Dropout(0.3)是经验值虚假新闻数据集普遍偏小dropout 低了会翻车。注意attention_mask必须传否则 padding 位置会污染 [CLS] 向量。2.3 图像分支ResNet 够用ViT 看数据量图像分支用 ResNet50 是最稳的选择预训练权重在 ImageNet 上迁移到新闻配图场景效果不差。如果你数据量超过五万张可以换 ViT-B/16但小数据集上 ViT 容易欠拟合。下面用 torchvision 的 ResNet50 做图像编码去掉最后的全连接层取全局平均池化后的向量。import torch.nn as nn from torchvision import models class ImageEncoder(nn.Module): def __init__(self, out_dim256, freeze_backboneFalse): super().__init__() # 加载 ImageNet 预训练 ResNet50 resnet models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) # 去掉最后的 fc 层保留卷积部分 self.backbone nn.Sequential(*list(resnet.children())[:-1]) # 输出 (B, 2048, 1, 1) if freeze_backbone: for p in self.backbone.parameters(): p.requires_grad False self.proj nn.Sequential( nn.Flatten(), nn.Linear(2048, out_dim), nn.ReLU(), nn.Dropout(0.3) ) def forward(self, images): feat self.backbone(images) # (B, 2048, 1, 1) return self.proj(feat) # (B, out_dim)freeze_backbone在图像数量少于两万时建议 True。weights参数用新版 torchvision 的写法旧版是pretrainedTrue注意版本差异。图像输入尺寸统一 resize 到 224x224归一化用 ImageNet 的均值和方差这个后面数据加载部分会写。2.4 融合层交叉注意力比简单拼接强在哪融合层是整个模型的关键。简单 concat 的问题前面说了这里给一个双向交叉注意力的实现。思路是文本特征作为 query图像特征作为 key 和 value得到文本视角的跨模态表示反过来再做一次得到图像视角的跨模态表示最后把两个表示拼接后分类。class CrossModalFusion(nn.Module): def __init__(self, dim256, num_heads4): super().__init__() # 文本 query 图像 self.t2i_attn nn.MultiheadAttention(embed_dimdim, num_headsnum_heads, batch_firstTrue) # 图像 query 文本 self.i2t_attn nn.MultiheadAttention(embed_dimdim, num_headsnum_heads, batch_firstTrue) self.norm1 nn.LayerNorm(dim) self.norm2 nn.LayerNorm(dim) # 分类头 self.classifier nn.Sequential( nn.Linear(dim * 2, dim), nn.ReLU(), nn.Dropout(0.3), nn.Linear(dim, 2) # 二分类真/假 ) def forward(self, text_feat, image_feat): # text_feat: (B, dim) - 扩展成 (B, 1, dim) 作为 query t text_feat.unsqueeze(1) # (B, 1, dim) i image_feat.unsqueeze(1) # (B, 1, dim) # 文本 query 图像 t2i, _ self.t2i_attn(t, i, i) # (B, 1, dim) t2i self.norm1(t2i t) # 残差连接 # 图像 query 文本 i2t, _ self.i2t_attn(i, t, t) i2t self.norm2(i2t i) # 拼接两个视角的表示 fused torch.cat([t2i.squeeze(1), i2t.squeeze(1)], dim-1) # (B, 2*dim) return self.classifier(fused)num_heads设 4 是因为 dim256 时每个头 64 维再大就太碎。残差连接不能省否则注意力层在训练初期容易梯度消失。分类头最后输出 2 维配合 CrossEntropyLoss 使用。如果你要做“真/假/存疑”三分类把最后的 2 改成 3 即可。3. 数据准备与训练流程从原始图文对到能跑的 DataLoader3.1 数据集怎么组织一个文件夹 一个 CSV 就够虚假新闻多模态数据集常见的有 Weibo 和 Twitter 的两个版本但很多人拿不到。实际做项目时我一般会自己组织成最简单的结构一个images/文件夹放所有图片一个data.csv记录每条样本的文本、图片文件名、标签。CSV 至少三列text、image_id、label。label 用 0 表示真新闻1 表示假新闻。图片统一用image_id.jpg命名避免路径混乱。# 目录结构示例 dataset/ ├── images/ │ ├── 0001.jpg │ ├── 0002.jpg │ └── ... └── data.csvdata.csv里不要放图片的绝对路径只放文件名代码里拼路径。这样换机器、换系统都不用改 CSV。如果一条新闻有多张图要么取第一张要么把多张图的特征做平均后者更稳但代码复杂一点新手先用单图。3.2 用 Dataset 和 DataLoader 把图文对齐PyTorch 的 Dataset 要同时返回文本的 token ids、图像的 tensor、以及标签。文本用 BERT 的 tokenizer 做截断和 padding图像用 torchvision 的 transforms 做 resize 和归一化。下面是一个完整的 Dataset 实现。import os import pandas as pd from PIL import Image from torch.utils.data import Dataset, DataLoader from transformers import BertTokenizer from torchvision import transforms class FakeNewsDataset(Dataset): def __init__(self, csv_path, img_dir, tokenizer, max_len128): self.df pd.read_csv(csv_path) self.img_dir img_dir self.tokenizer tokenizer self.max_len max_len # 图像预处理统一尺寸 ImageNet 归一化 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] text str(row[text]) # 文本编码截断 padding 到 max_len enc self.tokenizer( text, max_lengthself.max_len, paddingmax_length, truncationTrue, return_tensorspt ) input_ids enc[input_ids].squeeze(0) attention_mask enc[attention_mask].squeeze(0) # 图像加载 img_path os.path.join(self.img_dir, str(row[image_id]) .jpg) image Image.open(img_path).convert(RGB) image self.img_transform(image) label int(row[label]) return input_ids, attention_mask, image, label # 使用示例 tokenizer BertTokenizer.from_pretrained(bert-base-chinese) dataset FakeNewsDataset(dataset/data.csv, dataset/images, tokenizer) loader DataLoader(dataset, batch_size16, shuffleTrue, num_workers2)max_len128对新闻标题和短正文够用长文可以加到 256但显存会涨。batch_size16是 8G 显存的保守值12G 以上可以上 32。num_workers在 Windows 上设 0 更稳Linux 上设 2 到 4。注意Image.open如果遇到损坏图片会抛异常生产环境要加 try-except 跳过或返回一张全黑图。3.3 训练循环损失函数、优化器和学习率怎么设训练循环本身不复杂关键是几个超参。损失用 CrossEntropyLoss优化器用 AdamW学习率文本分支和图像分支分开设BERT 部分用 2e-5图像分支用 1e-4融合层用 1e-3。这是因为预训练模型对学习率很敏感设大了会灾难性遗忘。import torch from torch.optim import AdamW from transformers import get_linear_schedule_with_warmup device torch.device(cuda if torch.cuda.is_available() else cpu) model FullModel().to(device) # 把 TextEncoder、ImageEncoder、Fusion 组装进去 # 分组设置学习率 bert_params list(model.text_encoder.bert.parameters()) other_params [p for n, p in model.named_parameters() if text_encoder.bert not in n] optimizer AdamW([ {params: bert_params, lr: 2e-5}, {params: other_params, lr: 1e-4} ], weight_decay0.01) criterion torch.nn.CrossEntropyLoss() epochs 10 total_steps len(loader) * epochs scheduler get_linear_schedule_with_warmup(optimizer, num_warmup_stepsint(0.1*total_steps), num_training_stepstotal_steps) for epoch in range(epochs): model.train() total_loss 0 for input_ids, attention_mask, images, labels in loader: input_ids input_ids.to(device) attention_mask attention_mask.to(device) images images.to(device) labels labels.to(device) optimizer.zero_grad() logits model(input_ids, attention_mask, images) loss criterion(logits, labels) loss.backward() # 梯度裁剪防止 BERT 微调时梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() scheduler.step() total_loss loss.item() print(fEpoch {epoch1}, Loss: {total_loss/len(loader):.4f})weight_decay0.01是 AdamW 的常规值。warmup设总步数的 10%这是微调 BERT 的标准做法。梯度裁剪max_norm1.0一定要加否则某些 batch 的梯度会突然变大导致 loss 变 NaN。如果显存不够把 batch_size 降到 8同时把学习率按比例降到 1e-4 和 1e-5。4. 避坑与排查多模态虚假新闻检测里最容易翻车的 5 个地方4.1 图文对不齐导致模型学了个寂寞现象训练 loss 正常下降但验证集准确率一直在 50% 到 60% 之间晃跟随机猜差不多。原因CSV 里的image_id和images/文件夹里的文件名对不上或者图片加载失败返回了全黑图模型实际上只学到了文本分支图像分支输出恒定值。更隐蔽的情况是图片和文本在数据预处理时被 shuffle 打乱了对应关系。解决写一个检查脚本遍历 CSV 每一行确认图片文件存在且能正常打开。再抽 20 条样本人工看一眼文本和图片是不是同一条新闻。训练前打印一个 batch 的图片 tensor 均值和方差如果方差接近 0说明图片没加载对。4.2 BERT 微调时 loss 变 NaN现象前几个 batch loss 正常突然跳到 NaN之后再也降不下来。原因学习率设太大或者没有做梯度裁剪某个 batch 的梯度范数爆炸。中文 BERT 在 batch 里有超长文本时更容易出现这个问题。解决学习率从 2e-5 降到 1e-5加clip_grad_norm_(max_norm1.0)并且在 tokenizer 里设truncationTrue和max_length128。如果还不行检查输入里有没有空字符串空文本经过 BERT 会产生全零的 attention mask导致除零。4.3 图像分支过拟合训练集 99%验证集 60%现象训练准确率很快到 99%验证集卡在 60% 左右loss 开始上升。原因图像数据量太少ResNet50 参数量太大模型把训练集的图片全背下来了。虚假新闻数据集通常只有几千条图像分支很容易过拟合。解决冻结 ResNet 的前面层只训练最后两个 block 和投影层加数据增强比如 RandomHorizontalFlip、ColorJitter把 dropout 从 0.3 提到 0.5。如果还不行换更小的 backbone比如 ResNet18 或 EfficientNet-B0。4.4 融合层注意力权重全是均匀分布现象把交叉注意力的权重可视化出来发现每个位置的权重都差不多没有聚焦在任何区域。原因融合层的学习率设得太小或者训练轮数不够注意力还没学到东西就停了。另一个可能是文本和图像特征在进入融合层之前没有做 LayerNorm尺度差异太大softmax 之后趋近均匀。解决在文本和图像投影层后面各加一个 LayerNorm融合层的学习率单独设大一点比如 1e-3训练轮数至少 10 轮观察注意力权重的熵是否在下降。4.5 验证集准确率波动大每次跑结果不一样现象同样的代码跑三次得到三个不同的准确率差距能到 5 个百分点。原因数据量小batch 顺序敏感没有设随机种子DataLoader 的 shuffle 和 dropout 引入了随机性。解决在代码开头固定所有随机种子包括 torch、numpy、random 和 cuda。用 5 折交叉验证代替单次划分报告平均准确率和标准差。如果标准差超过 3 个百分点说明模型不稳定需要简化模型或增加数据。import torch import numpy as np import random def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) # 下面两行让 cudnn 确定性运行会慢一点但结果可复现 torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False set_seed(42)5. 把准确率再往上推一截三个我实际用过的改进技巧5.1 用图像元数据做辅助特征新闻配图里藏着很多文本看不到的信息图片的 EXIF 时间戳、分辨率、是否被 PS 过。我一般会额外抽三个特征图片的宽高比、文件大小、以及用 ELAError Level Analysis算出的压缩痕迹均值。这三个特征归一化后拼到图像编码器的输出上再进融合层。实测在 Weibo 数据集上能涨 1.5 到 2 个百分点的 F1。ELA 的实现不复杂用 PIL 重新保存图片一次和原图做差取差值图的均值。from PIL import Image, ImageChops import numpy as np def ela_feature(img_path, quality90): original Image.open(img_path).convert(RGB) # 重新保存一次引入压缩差异 temp_path /tmp/_ela_temp.jpg original.save(temp_path, JPEG, qualityquality) compressed Image.open(temp_path).convert(RGB) diff ImageChops.difference(original, compressed) return np.array(diff).mean() / 255.0 # 归一化到 0-1这个特征单独看很弱但和图像 CNN 特征互补。注意 ELA 对 PNG 图片不适用因为 PNG 是无损格式重新保存不会产生差异。所以数据里如果有 PNG要么转成 JPEG 再算要么直接跳过这个特征。5.2 文本侧加一个“情绪极性”分支假新闻的文本往往带有更强的情绪色彩比如大量感叹号、极端形容词。我一般会在文本编码器旁边并行加一个轻量分支用 SnowNLP 或预训练的情感分类模型抽一个情感极性分数再统计感叹号密度、大写字母比例英文场景、以及疑问句比例。这几个标量拼成一个 8 维向量和 BERT 的 256 维文本特征拼接后一起进融合层。这个改动很小但在我做过的几个项目里都能稳定涨点因为情绪特征和语义特征是正交的。5.3 用对比学习做预训练再微调分类如果数据量超过两万条可以先用对比学习做一轮预训练把同一条新闻的文本和图像作为正样本对不同新闻的图文作为负样本对用 InfoNCE 损失训练文本和图像编码器。这一步不需要标签纯用图文对应关系。预训练完再加载权重做分类微调通常比直接微调高 2 到 3 个百分点。代价是训练时间翻倍适合有 GPU 资源的情况。def info_nce_loss(text_feat, image_feat, temperature0.07): # 归一化 text_feat torch.nn.functional.normalize(text_feat, dim-1) image_feat torch.nn.functional.normalize(image_feat, dim-1) # 计算相似度矩阵 logits text_feat image_feat.T / temperature # (B, B) labels torch.arange(logits.size(0), devicelogits.device) # 对称损失文本找图像 图像找文本 loss_t2i torch.nn.functional.cross_entropy(logits, labels) loss_i2t torch.nn.functional.cross_entropy(logits.T, labels) return (loss_t2i loss_i2t) / 2temperature0.07是 SimCLR 里的经典值小数据集可以调到 0.1 让梯度更稳。batch_size 越大对比学习效果越好至少 64能上 256 更好。如果显存不够用梯度累积模拟大 batch。5.4 验证时看混淆矩阵别只看准确率虚假新闻检测里假新闻的召回率比准确率更重要。一个模型准确率 85%但假新闻召回只有 60%意味着四成假新闻漏掉了实际不可用。我一般会在验证时打印混淆矩阵并且单独看假新闻那一类的 F1。如果假新闻召回低把分类损失里的假新闻类权重调高或者把分类阈值从 0.5 降到 0.4。from sklearn.metrics import confusion_matrix, classification_report def evaluate(model, loader, device): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for input_ids, attention_mask, images, labels in loader: input_ids input_ids.to(device) attention_mask attention_mask.to(device) images images.to(device) logits model(input_ids, attention_mask, images) preds logits.argmax(dim-1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(confusion_matrix(all_labels, all_preds)) print(classification_report(all_labels, all_preds, target_names[真新闻, 假新闻]))跑完这个你会看到假新闻那一列的 recall 和 f1。如果 recall 低于 0.7优先调损失权重而不是换模型。我自己的习惯是每次改完模型先跑一遍混淆矩阵确认假新闻召回没掉再看准确率。这个习惯帮我省了很多“准确率涨了但模型变废了”的后悔药。希望帮到你。本文还有配套的精品资源点击获取
返回列表