基于PyTorch的多模态虚假新闻检测:BERT+ResNet+对比学习实战
发布时间:2026/9/26 19:45:17来源:尧图网络
简介一套基于PyTorch实现的多模态虚假新闻检测系统源码面向深度学习研究者、NLP方向学生及舆情分析开发者。方案融合BERT预训练模型与ResNet卷积神经网络分别提取文本深层语义与图像视觉特征并在微博谣言数据集上完成训练与评估同时引入对比学习机制强化真实新闻与虚假新闻的判别能力适用于社交媒体虚假信息识别等任务。压缩包共收纳21个文件整体仅1.48MB以12个Python脚本为主体涵盖模型结构、训练流程、数据预处理与工具函数另有4个文本说明、3个CSV数据及docx、md格式的配套资料方便对照阅读与二次开发。目前已有96人浏览/学习。从实战角度看整套代码不仅提供完整的模型结构与可运行训练入口还通过说明文档梳理了参数配置、数据组织方式与扩展思路适合作为多模态分类项目的起步模板通过阅读源码可学习BERT文本特征与ResNet图像特征的融合方式以及对比学习如何优化表征并复现微博谣言数据上的实验结果。1. 多模态虚假新闻检测为什么值得复现先立住技术选型再谈落地难度拿到一个“基于PyTorch的多模态虚假新闻检测系统”代码包很多人的第一反应是先把环境配好再跑通训练脚本。但这个方向真正值得花时间的点不在训练本身而在“文本和图像特征怎么对齐”和“对比学习到底给分类任务带来了什么”。这条技术路线在PyTorch生态里已经非常成熟BERT负责文本语义ResNet负责图像内容两者各出一个特征向量再通过对比学习让同一篇博文的图文表征靠得更近。它作为论文复现、毕业设计或企业舆情系统的原型性价比都很高。适合对PyTorch有基础、想把多模态模型从概念落到代码的工程师也适合想拿公开数据集做消融实验的研究者。这篇笔记按“数据处理 - 模型搭建 - 对比学习训练 - 排障 - 评估验证”的顺序展开所有代码都按可运行的标准来写。2. 微博谣言数据集预处理从原始标注到BERT与ResNet都能吃的样子2.1 数据集字段与标注含义先看清JSON里有什么公开的微博不实信息数据集一般以JSON或CSV形式发布每条样本包含“微博文本内容”“图片URL列表”“标注谣言/非谣言”“发布时间”“转发数”“点赞数”等字段。做多模态检测时真正用到的只有文本、图片和标签三样其余字段可以作为辅助特征但前期建议先不放避免引入过多噪声。不同版本的微博数据集在字段命名上不太统一有的用weibo_text有的用text图片字段有的是image_url有的是pic_list。拿到数据的第一件事不是写训练代码而是写一个探查脚本把字段名、类型、缺失情况一次性打印出来。import json from collections import Counter with open(weibo_dataset.json, r, encodingutf-8) as f: data json.load(f) print(样本总数:, len(data)) print(字段名:, list(data[0].keys())) print(标签分布:, Counter([item[label] for item in data])) # 检查文本和图片字段的缺失率 missing_text sum(1 for item in data if not item.get(text)) missing_img sum(1 for item in data if not item.get(image_url)) print(缺失文本:, missing_text, 缺失图片:, missing_img)这段代码做了三件事确认数据量、确认字段名、确认标签分布。标签分布这一步很重要微博谣言数据集的谣言与非谣言比例通常不是1:1有的版本甚至接近1:3这个比例会直接影响后面评估指标的选择。缺失文本和缺失图片的数量也要先摸清如果缺失比例超过10%处理策略要相应调整。2.2 文本清洗与BERT Tokenizer处理统一到128个token以内BERT的输入是token序列但原始微博文本里有很多对语义没帮助的内容HTML转义符、用户昵称、URL、表情符号的文本表示。这些东西如果不清理tokenizer会把一个长URL拆成几十个碎token白白占用序列长度BERT根本看不完正文内容。常见的清洗顺序是先去HTML实体再替换URL和用户最后保留中文字符和基本标点。清洗做完后用HuggingFace的BertTokenizer把文本转成input_ids、attention_mask、token_type_ids三件套。from transformers import BertTokenizer tokenizer BertTokenizer.from_pretrained(bert-base-chinese) MAX_LEN 128 def clean_text(text): import re text re.sub(r[^], , text) # 去HTML标签 text re.sub(rhttps?://\S, 【网页链接】, text) # URL替换为占位符 text re.sub(r[\w\-\u4e00-\u9fa5], 【用户】, text) # 用户替换 text re.sub(r\s, , text).strip() return text def encode_text(text, max_lenMAX_LEN): cleaned clean_text(text) encoded tokenizer( cleaned, max_lengthmax_len, paddingmax_length, truncationTrue, return_tensorspt ) return { input_ids: encoded[input_ids].squeeze(), attention_mask: encoded[attention_mask].squeeze() }这段代码里的关键参数是max_length128。微博文本大多比较短128个token足够覆盖绝大多数内容更重要的是序列长度直接决定BERT前向推理的显存占用从512降到128显存能省下近一半。return_tensorspt让tokenizer直接返回PyTorch张量省去手动转换。注意这里没有返回token_type_ids因为单文本分类任务用不到返回了也只能增加数据体积。2.3 图片解码与缩放ResNet不喜欢原图分辨率ResNet的输入要求是固定尺寸的RGB张量torchvision的标准是224×224。但我们从网上下载的微博图片分辨率参差不齐有的大图5000×3000有的小图只有80×80直接把原图送入网络会导致两个问题一是数据加载变慢二是不同样本经过ResNet的池化层后输出尺寸不一致无法对齐特征。图片处理的基本流程是下载或读取图像文件后用PIL解码转换成RGB模式按比例缩放使短边到256再中心裁剪出224×224。这个“先缩放再裁剪”的顺序比直接拉伸更保真不会让图像变形。from PIL import Image from torchvision import transforms img_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def load_image(img_path): try: img Image.open(img_path).convert(RGB) return img_transform(img) except Exception: # 图片损坏或下载失败时返回全零张量后续由Dataset层处理 return None这里有两个参数值得注意Resize((256, 256))的256是经验值比目标尺寸224稍大一点给随机裁剪留出空间Normalize用的均值方差是ImageNet的统计值因为我们要加载在ImageNet上预训练的ResNet权重输入分布与预训练分布保持一致才能发挥迁移学习的效果。如果直接使用没有预训练的随机初始化ResNet归一化参数用默认的0.5也能跑但效果会明显变差。2.4 Dataset封装与训练集划分把数据读取从训练循环里解放出来数据清洗和编码做完后要把所有逻辑封装进torch.utils.data.Dataset子类。多模态数据集的难点在于文本和图像要成对返回而且图像存在下载失败、文件损坏的可能这部分必须在Dataset里做容错处理不能在训练循环里临时判断。import torch from torch.utils.data import Dataset class MultimodalDataset(Dataset): def __init__(self, data, tokenizer, img_root, max_len128): self.samples [] for item in data: text encode_text(item[text], max_len)[input_ids] mask encode_text(item[text], max_len)[attention_mask] img_path f{img_root}/{item[img_id]}.jpg self.samples.append({ input_ids: text, attention_mask: mask, img_path: img_path, label: torch.tensor(item[label], dtypetorch.long) }) def __len__(self): return len(self.samples) def __getitem__(self, idx): sample self.samples[idx] img load_image(sample[img_path]) if img is None: img torch.zeros(3, 224, 224) # 坏图补零而不是跳过样本 return { input_ids: sample[input_ids], attention_mask: sample[attention_mask], image: img, label: sample[label] }这个Dataset有两个设计细节。第一文本编码在__init__里提前算好训练时直接查表取用避免每个epoch都重复做tokenize第二图像加载失败时返回全零张量而不是丢弃该样本因为如果测试集里有一张坏图就跳过会导致评估集不完整指标对比失去意义。全零张量会让ResNet输出一个无意义特征但配合后面的对比学习模型会学会对此类样本不可靠实测中比直接跳过更安全。3. 搭建双塔特征提取网络BERT与ResNet的接入方式与输出对齐3.1 文本塔BERT的CLS向量与最后一层Hidden State怎么取舍BERT作为文本编码器最常见的接入方式是取[CLS]位置的输出向量作为整条文本的表示。这个向量经过12层Transformer的交互之后理论上聚合了全局语义信息。另一种做法是取最后一层所有token的向量做平均池化或最大池化在某些短文本任务上平均池化比CLS表现更稳定。在微博谣言检测这个任务上我一般保留CLS向量理由是对话立场和语气等信号在CLS里编码更充分而且CLS向量是后续对比学习投影头最自然的输入。实现上用transformers库直接构建模型不额外写BERT结构省心也稳定。from transformers import BertModel class TextEncoder(torch.nn.Module): def __init__(self, pretrainedbert-base-chinese, freezeFalse): super().__init__() self.bert BertModel.from_pretrained(pretrained) self.feat_dim self.bert.config.hidden_size # 768 if freeze: for param in self.bert.parameters(): param.requires_grad False def forward(self, input_ids, attention_mask): outputs self.bert(input_idsinput_ids, attention_maskattention_mask) return outputs.last_hidden_state[:, 0, :] # [batch, 768]outputs.last_hidden_state[:, 0, :]取的就是每个序列的第一个token即CLS位置的向量。self.bert.config.hidden_size是768这个数字后面做特征对齐时要用到建议写成从config读取而不是硬编码。关于是否冻结BERT参数下面搭配整体训练策略时再细说。3.2 图像塔去掉ResNet分类头的特征提取写法torchvision里的ResNet模型默认带着一个1000类的全连接分类头用作特征提取时要把fc层去掉或替换成恒等映射。ResNet18的最后一个卷积块输出512维特征图经过全局平均池化得到512维向量ResNet50则是2048维。选哪个主要看显存和精度要求ResNet18在微博图片这类场景下已经够用显存占用也友好。import torchvision.models as tv_models class ImageEncoder(torch.nn.Module): def __init__(self, baseresnet18, freezeFalse): super().__init__() if base resnet18: self.backbone tv_models.resnet18(weightstv_models.ResNet18_Weights.IMAGENET1K_V1) self.feat_dim 512 else: self.backbone tv_models.resnet50(weightstv_models.ResNet50_Weights.IMAGENET1K_V2) self.feat_dim 2048 # 去掉最后的分类层保留特征提取部分 self.backbone.fc torch.nn.Identity() if freeze: for param in self.backbone.parameters(): param.requires_grad False def forward(self, image): return self.backbone(image) # [batch, feat_dim]把fc换成Identity()是最省事的做法ResNet前向计算到全局平均池化后直接输出特征向量不会多算一个1000维的线性层。这里用了torchvision.models的预训练权重枚举比旧版传字符串的方式可读性更好。weights参数在PyTorch 2.x里是推荐写法传IMAGENET1K_V1或IMAGENET1K_V2都能自动下载权重。3.3 特征对齐与融合768和512怎么拼到一起双塔输出的维度不一致这是必须面对的第一个问题。融合方案有几种最简单的concat除此之外还有加权求和、cross-attention等方式。在微博谣言检测这个任务规模上concat到一个维度后接全连接层是最稳妥的做法信息损失最小也方便后面做消融实验对比单模态基线。class MultimodalModel(torch.nn.Module): def __init__(self, text_encoder, image_encoder, num_classes2, fusion_dim256, use_projectionTrue): super().__init__() self.text_encoder text_encoder self.image_encoder image_encoder self.fusion_dim fusion_dim self.use_projection use_projection total_dim text_encoder.feat_dim image_encoder.feat_dim self.classifier torch.nn.Sequential( torch.nn.Linear(total_dim, fusion_dim), torch.nn.ReLU(), torch.nn.Dropout(0.3), torch.nn.Linear(fusion_dim, num_classes) ) # 对比学习用的投影头把融合特征映射到低维对比空间 self.projection_head torch.nn.Sequential( torch.nn.Linear(total_dim, 128), torch.nn.ReLU(), torch.nn.Linear(128, 128) ) def forward(self, input_ids, attention_mask, image, return_featureFalse): text_feat self.text_encoder(input_ids, attention_mask) img_feat self.image_encoder(image) fused_feat torch.cat([text_feat, img_feat], dim-1) logits self.classifier(fused_feat) if return_feature: proj_feat self.projection_head(fused_feat) return logits, proj_feat return logits这里的核心参数是fusion_dim256既不过大导致过拟合也不太小导致信息瓶颈。Dropout(0.3)是针对融合层的经验值多模态场景下两个特征来源差异大融合层比单模态分类器更容易过拟合。projection_head输出128维向量这是给对比学习专门预留的特征空间与分类分支共享底层特征提取器但各用各的输出头。3.4 冻结策略BERT和ResNet到底要不要一起微调一个完整的多模态模型参数量很大BERT base大约1.1亿ResNet18大约1200万全量微调对显存和训练时间都是考验。常见做法分三种全部冻结只训练融合层、部分冻结微调深层、全部微调。在微博数据集规模几万到十几万样本下我一般选择冻结BERT的前8层微调后4层和ResNet的最后一个stage这个策略在公开数据集上表现接近全量微调但显存占用降低约40%。def set_partial_freeze(model, freeze_text_layers8): # 冻结BERT前8层 for name, param in model.text_encoder.bert.named_parameters(): layer_idx int(name.split(.)[2]) if layer in name else -1 if 0 layer_idx freeze_text_layers: param.requires_grad False层编号解析依赖transformers的内部命名规则bert.encoder.layer.0.attention.self.query.weight这种格式里split(.)[2]取到的就是层序号。冻结浅层非常关键BERT浅层编码的是词法和句法特征通用性强深层才编码任务相关语义微博这种短文本场景尤其如此。ResNet则建议冻结前三个stage只微调最后一个stage和全局池化前的卷积块。4. 用对比学习拉近同源样本训练策略与关键参数设置4.1 对比学习在这个任务里解决什么问题单纯用交叉熵训练多模态分类器模型可能学到“文本说转发、图片是风景”这类表面关联鲁棒性不足。对比学习的核心目标是拉近同一个样本图文表征的距离推远不同样本的图文表征让模型学会“这条微博的图和文在说的是同一件事”这个语义对齐关系。具体到实现每个训练样本天然的图文对就是正样本对同一个batch里其他样本的图文特征就是负样本。这样不需要额外构造数据也不依赖标注信息直接利用多模态数据的配对结构。这比用同一条文本在不同数据增强下的视图做正对更自然。import torch.nn.functional as F def info_nce_loss(features, temperature0.07): features: [batch, 2, proj_dim]0是文本特征1是图像特征 text_feat features[:, 0, :] img_feat features[:, 1, :] text_feat F.normalize(text_feat, dim-1) img_feat F.normalize(img_feat, dim-1) logits torch.matmul(text_feat, img_feat.T) / temperature batch_size text_feat.size(0) labels torch.arange(batch_size, devicefeatures.device) loss F.cross_entropy(logits, labels) return losstemperature0.07是对比学习里常用的起点值它控制相似度分布的尖锐程度。温度太低会让训练不稳定温度太高会让正负样本的区分度变弱。labels用的是对角线索引因为第i个样本的图文特征正确配对时text_feat[i]应该和img_feat[i]最相似对角线就是监督信号。4.2 图文联合训练主分类损失和对比损失怎么加权对比学习只是辅助约束不能让它的 loss 压过分类 loss 太多否则模型会把精力全放在对齐图文上反而忽略了分类任务。实践上一般让分类损失做主导对比损失作为正则项。这里的关键参数就是权重系数 lambda从0.1开始调如果分类指标提升不明显就往上加如果训练震荡就往下减。def train_step(model, batch, optimizer, lambda_cl0.1, temperature0.07): input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) image batch[image].to(device) labels batch[label].to(device) logits, proj_feat model(input_ids, attention_mask, image, return_featureTrue) cls_loss F.cross_entropy(logits, labels) # 构造对比学习输入让投影头分别处理文本和图像特征 text_feat model.text_encoder(input_ids, attention_mask) img_feat model.image_encoder(image) fused_text torch.cat([text_feat, torch.zeros_like(img_feat)], dim-1) fused_img torch.cat([torch.zeros_like(text_feat), img_feat], dim-1) proj_text model.projection_head(fused_text) proj_img model.projection_head(fused_img) features torch.stack([proj_text, proj_img], dim1) cl_loss info_nce_loss(features, temperature) total_loss cls_loss lambda_cl * cl_loss optimizer.zero_grad() total_loss.backward() optimizer.step() return total_loss.item(), cls_loss.item(), cl_loss.item()这段代码里对比学习分支的做法是把文本特征和图像特征分别拼进完整特征向量再送入投影头。理由是投影头接收的是融合特征的维度不能直接对768维文本特征和512维图像特征分别投影否则对比空间的维度不一致。用零填充补位虽然有点浪费计算但保证了投影头输入的语义空间与分类分支一致。4.3 训练循环与学习率设置BERT用低学习率新层用高学习率多模态模型的参数更新频率天然不一致BERT层如果和分类层用同一个学习率BERT会被冲乱。常见做法是设置分组学习率BERT和ResNet的backbone用小学习率2e-5到5e-5融合层、分类头和投影头用大学习率1e-4到3e-4。优化器用AdamW这是PyTorch生态里预训练模型微调的事实标准。from transformers import AdamW def build_optimizer(model, lr_backbone3e-5, lr_head1e-4): backbone_params [] head_params [] for name, param in model.named_parameters(): if not param.requires_grad: continue if classifier in name or projection_head in name: head_params.append(param) else: backbone_params.append(param) return AdamW([ {params: backbone_params, lr: lr_backbone}, {params: head_params, lr: lr_head} ])lr_backbone3e-5是BERT类模型的标准起点1e-4对新初始化的层非常合适。训练轮数上微博数据集几万样本的规模5到8个epoch基本收敛再多就会过拟合。训练过程中每半个epoch记录一次验证集准确率如果连续两个epoch没有提升就把学习率降到当前值的0.1倍这是最省心的调度策略。5. 多模态训练避坑指南从显存OOM到评估指标虚高5.1 显存溢出OOM的锅不只在batch size现象训练刚开始就报CUDA out of memory把batch size从32降到8还是崩。原因BERT的序列长度、ResNet输入分辨率、数据加载的pin_memory设置、多个张量同时驻留显存这些因素叠加在一起单靠调batch size收效甚微。解决先看总显存占用结构用torch.cuda.memory_summary()确认哪些中间变量占空间。最常见的优化是把MAX_LEN从128降到96把 image 的分辨率保持224不变同时给 DataLoader 加pin_memoryTrue但把num_workers调低到2或4。如果还爆启用梯度累积每两个batch更新一次参数。提示微博文本平均长度只有30到40个token96的序列长度极少截断正文优先压缩文本序列而不是图片分辨率。5.2 BERT权重下载失败与参数路径混乱现象本地已经下载过bert-base-chinese但程序每次启动都从HuggingFace Hub重新下载网络不稳时直接报连接错误。原因from_pretrained默认检查的是缓存目录如果之前下载中断或缓存目录环境变量没配对就会重复下载。解决手动指定本地路径加载提前用snapshot_download把模型拉到指定目录或者从HuggingFace的模型页手动下载配置文件、tokenizer文件和权重文件放到项目下的./weights/bert-base-chinese/里from transformers import BertModel, BertTokenizer local_bert_path ./weights/bert-base-chinese tokenizer BertTokenizer.from_pretrained(local_bert_path) text_encoder BertModel.from_pretrained(local_bert_path)这段代码的关键在“一次性指定本地路径”。BERT相关的文件有5个config.json、pytorch_model.bin、vocab.txt、tokenizer_config.json、special_tokens_map.json缺任何一个都会加载失败。另一个隐蔽的坑是文件名不一致比如权重叫pytorch_model.bin但代码里期望model.bin所以下载后先检查文件名再写路径。5.3 ResNet预训练权重与PyTorch版本不匹配现象加载ResNet时提示Parameter fc.weight of size (1000, 2048) doesnt match the size (2048, 2048)之类的错误。原因旧代码里用models.resnet50(pretrainedTrue)新版本torchvision已经改为weights参数而本地的解决办法是用不符合版本的下载方式。解决明确用枚举指定权重版本不要用布尔值。如果本地已有历史权重文件可以用torch.load手动载入后过滤掉fc层对应的键值再load_state_dict。import torchvision.models as tv_models state_dict torch.load(./weights/resnet50_imagenet.pth) state_dict.pop(fc.weight, None) state_dict.pop(fc.bias, None) backbone tv_models.resnet50(weightsNone) backbone.fc torch.nn.Identity() backbone.load_state_dict(state_dict, strictFalse)strictFalse是这里的关键参数它允许状态字典缺失或多余部分不会报错。如果直接strictTrue就会因为fc层被移除而报key不匹配。注意weightsNone表示不加载任何预训练权重完全由手动方式注入这也绕开了PyTorch新版本对下载来源的校验。5.4 微博数据集的图片缺失与字段不一致现象训练进度到某个epoch突然变慢或者准确率一直在50%上下跳动。排查后发现数据里图像的img_id和实际文件名对不上有的图片在下载过程中损坏PIL直接抛异常。原因公开数据集的图片URL经常失效尤其那些多年以前的微博配图源站已经删除或防盗链。解决除了在Dataset里对坏图返回全零张量之外还要在建Dataset之前做一次全量图片存在性检查统计坏图比例。如果坏图比例超过5%建议把对应样本的文本塔输出用作主特征对图像分支做置零处理而不是直接剔除样本保持训练集和测试集的分布一致。5.5 对比学习不收敛或拉低分类指标现象加了对比学习之后分类准确率反而下降或者对比损失降不下去一直维持在0.5以上。原因最可能是温度参数和lambda权重不匹配。温度太低时负样本相似度梯度几乎为零投影头学不动lambda太大时模型只顾着对齐图文对忽略分类标注。另一个隐蔽问题是对比学习里的正负样本构造错误比如把同一个样本的文本与文本当成正对那就没有意义了。解决先固定分类损失单独训练3个epoch再开启对比学习分支。温度从0.07开始lambda从0.05开始观察对比损失如果10个step内没有下降趋势把温度调大一点温度调大仍然不降就检查特征是否做了L2归一化。6. 评估与验证用消融实验和特征可视化验证系统真的有效6.1 评估指标宏平均F1比准确率更可靠微博谣言数据集中非谣言样本通常多于谣言样本准确率会被多数类抬高。一个模型把所有样本都判为非谣言准确率也能到70%以上这在业务场景里完全不可用。评估时用宏平均F1macro F1和混淆矩阵按谣言类别单独看召回率。实现上直接用sklearn的f1_score(averagemacro)和confusion_matrix不需要绕弯。from sklearn.metrics import f1_score, classification_report preds, gt [], [] model.eval() with torch.no_grad(): for batch in val_loader: logits model(batch[input_ids], batch[attention_mask], batch[image]) preds.extend(torch.argmax(logits, dim-1).cpu().tolist()) gt.extend(batch[label].cpu().tolist()) print(macro F1:, f1_score(gt, preds, averagemacro)) print(classification_report(gt, preds, target_names[非谣言, 谣言]))评估的时候模型要切到eval()模式并关闭梯度计算这两步能明显降低显存占用和推理时间。分类报告里的每一行都要看特别是谣言类的precision和recall如果recall偏低说明模型漏掉了很多谣言样本这是生产场景里最不能接受的失误。6.2 用特征可视化验证对比学习是否真的生效训练完的模型不能只看指标就交差还要验证对比学习有没有真的把图文特征拉到一起。做法是把测试集里的图文对分别过模型编码器取投影特征降维后用颜色标注正负样本观察图文特征是否聚类。from sklearn.manifold import TSNE import matplotlib.pyplot as plt text_feats, img_feats, labels [], [], [] model.eval() with torch.no_grad(): for batch in val_loader: tf model.text_encoder(batch[input_ids], batch[attention_mask]) imf model.image_encoder(batch[image]) text_feats.append(tf) img_feats.append(imf) labels.extend(batch[label].tolist()) text_feats torch.cat(text_feats).cpu().numpy() img_feats torch.cat(img_feats).cpu().numpy() tsne TSNE(n_components2, perplexity30, random_state42) all_feats tsne.fit_transform(np.concatenate([text_feats[:500], img_feats[:500]]))TSNE(n_components2, perplexity30)是可视化常用配置perplexity太大或太小都会让分布变得松散。抽样500个样本是经验值样本太多TSNE的计算时间会急剧增加。如果把对应图文对的点距离画出来正样本对的距离比负样本对小说明对比学习确实起作用了这时候模型的泛化能力才有保障。6.3 消融实验的最小配置要证明多模态和对比学习的价值需要跑三组消融只用BERT文本、只用ResNet图像、双塔不加对比学习、双塔加对比学习。这四组配置可以在同一个模型框架里通过开关切换每组在相同的数据划分和随机种子下跑记录macro F1和谣言类recall。做消融时最忌讳的是各跑各的随机种子最后指标差异分不清是模型贡献还是数据波动。固定随机种子是必须的def set_seed(seed42): torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) import numpy as np, random np.random.seed(seed) random.seed(seed)6.4 从复现到改进值得尝试的进阶方向模型跑通之后如果想往业务或论文方向走有几个性价比高的改进点。第一个是把ResNet18换成ResNet50并对比效果确认是不是图像特征容量拖了后腿。第二个是在融合层加一层简单的cross-attention让文本特征和图像特征在融合前互相加权。第三个是用torch.onnx.export把训练好的模型导出为ONNX格式做推理加速验证这对上线部署很有参考意义。我在做完对比学习验证之后习惯把所有实验的指标、参数、随机种子记录在一个表格里再决定下一步动哪里项目里这个“三塔配置”在微博数据集上最稳定的是双塔加对比学习希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网