视觉问答系统实战:从跨模态特征融合到PyTorch训练避坑

发布时间:2026/10/10 12:37:55
视觉问答系统实战:从跨模态特征融合到PyTorch训练避坑
简介面向计算机专业毕业设计场景的深度学习视觉问答系统完整资料包聚焦图像理解与自然语言问答融合这一典型多模态任务。资源特别适合正在准备毕设、课程设计或期末大作业的高校学生也适用于需要完整项目实战的初中级学习者。整套代码基于Python实现覆盖数据预处理、模型搭建、训练评估与预测推理全流程包括基于ResNet与VGG的图像特征提取、MFH与CSF等多模态融合方法以及VQA数据集的加载处理脚本目录结构清晰便于二次扩展。压缩包共69个文件以33个py源码文件为核心辅以实验日志、配置说明、项目文档和答辩PPT整体体积仅2.38MB轻量易获取。该项目为经导师认可的高分毕设代码经严格调试可稳定运行目前已吸引315人学习查看对希望快速搭建VQA实验环境、理解多模态融合实现细节的读者有较高参考价值。1. 从一个跑偏的毕设开始视觉问答系统到底在做什么两年前我带过一个本科毕设题目就是“基于深度学习的视觉问答系统”。学生一开始以为它是“图像识别文本匹配”简单粗暴把图片特征和问题特征拼起来过个全连接就完事。结果训练完模型对“图里有几只猫”这类问题准确率只有 31%比瞎猜高不了多少。这篇项目的价值就在这儿它不是一个能靠拼接特征糊弄过去的 Demo演示程序而是一条需要同时吃透视觉特征、文本编码和跨模态融合的完整链路。源码给出的是可运行的基线文档说明补上设计思路答辩 PPT 负责把黑匣子讲成故事。适合三类人正在做这个题目的学生、想入多模态方向的新手工程师、以及需要快速评估 VQA 技术栈能不能落地的产品侧同行。2. VQA 基线拆开看图像编码、文本编码与融合方式怎么选2.1 图像特征CNN 输出的特征图与全局向量是两种用法视觉问答系统的输入不是原始图片而是图片的稠密向量表示。初次接触特征提取的人容易默认“主干网络最后一层最好用”但 VQA 场景里这个结论不成立。最后一层全连接输出的 512 维或 2048 维向量是面向 ImageNet 分类任务训练的全局语义压缩。图像里有对象、有空间位置、有对象间的相对关系都被压缩进一个向量问答需要的“那只狗在左边的草地里”这类空间细节会严重丢失。我一般会在主干网络的某个中间层取特征图比如 ResNet 的 C4 或者 C5 层输出保留通道维度和空间维度形状大概是[batch, 2048, 7, 7]。这个特征图后续有两种用法一种是把空间维度展平变成 49 个区域特征配合注意力机制让模型自己去找“哪个区域和当前问题最相关”另一种是先做全局池化得到单向量与文本特征做融合。追求答辩指标的话两种都保留特征图喂注意力池化向量做全局信息的兜底。2.2 文本编码LSTM 与 Transformer毕设与工程分别怎么选问题编码的选型直接影响最终效果。经典的 VQA 1.0 和 VQA 2.0 榜单上两层的双向 LSTM 至今仍是性价比最高的基线编码器因为它对短句子的建模能力足够而且参数少。import torch import torch.nn as nn class QuestionEncoder(nn.Module): def __init__(self, vocab_size, embed_dim300, hidden_dim512, num_layers2): super().__init__() # 词向量层用 GloVe 预训练向量初始化效果会明显好于随机初始化 self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_dim, num_layers2, batch_firstTrue, bidirectionalTrue, dropout0.2) self.init_weights() def init_weights(self): for name, param in self.lstm.named_parameters(): if weight_ih in name: nn.init.orthogonal_(param) def forward(self, question_tokens, question_len): emb self.embedding(question_tokens) # pack_padded_sequence 让 LSTM 只处理实际长度不浪费计算在 padding 上 packed torch.nn.utils.rnn.pack_padded_sequence( emb, question_len.cpu(), batch_firstTrue, enforce_sortableFalse) _, (h_n, _) self.lstm(packed) # 拼接最后一层前向和后向的隐状态得到 1024 维问题向量 q_vec torch.cat((h_n[-2], h_n[-1]), dim1) return q_vec这段代码的逻辑是先查词表得到词向量然后送入双向 LSTM取最后一层的前向与后向隐状态拼接成最终的问题向量。参数说明embed_dim建议用 300对应 GloVe 6B 的维度hidden_dim设 512 足够支撑 2 层双向。pack_padded_sequence一定要配合enforce_sortableFalse否则数据加载时未按长度排序会直接报 RuntimeError。如果问题最长不超过 14 个词把 num_layers 降到 1 也能跑推理速度能快 40%。Transformer 编码器在长句上更强但 VQA 数据集的问句平均长度是 6 到 9 个词LSTM 的序列归纳偏置在这个长度下反而是优势。只有你准备上 BERT 做迁移时才值得替换。2.3 融合策略点乘、拼接、双线性注意力差距比想象中大图像特征和问题特征都拿到手之后怎么融合是整个 VQA 系统最核心的设计决策。常见的做法有三种效果差距能到 8 到 12 个百分点的 accuracy。第一种是直接拼接图像特征 2048 维和问题向量 1024 维拼成 3072 维过全连接。这种方式在简化版的 Clevr 数据集上勉强能用但到了 VQA 2.0 这种开放式问答上效果很差因为拼接没有建模两者之间的二阶交互。第二种是逐元素相乘公式是v (W_i * img) * (W_q * q)。乘法能捕捉到“图像中的这个区域与问题中这个词同时出现”的关联信号比拼接好不少。MAC 网络的实验数据表明逐元素相乘比拼接在 VQA 2.0 test-dev 上高出 4% 左右。第三种是双线性注意力也叫 BANBilinear Attention Networks。它对每对图像区域和问题词计算一个注意力分数然后用分数加权融合区域特征。这是闭源论文里最常用的方案跑一个简化版只需要在特征图与问题向量之间引入一个低秩双线性池化模块。class SimpleBAN(nn.Module): def __init__(self, v_dim2048, q_dim1024, glimpse2): super().__init__() self.glimpse glimpse # 注意力头数一般取 1 到 4 # 低秩分解v_dim 降到 512q_dim 降到 512乘积矩阵就是 glimpse 个 self.v_proj nn.Linear(v_dim, 512 * glimpse) self.q_proj nn.Linear(q_dim, 512 * glimpse) self.fusion nn.Linear(512, 1) def forward(self, v, q): # v 的形状为 [batch, num_region, v_dim] # q 的形状为 [batch, q_dim] batch, region, _ v.shape v_t self.v_proj(v).view(batch, region, self.glimpse, 512) q_t self.q_proj(q).view(batch, self.glimpse, 512) # 每个 glimpse 单独算注意力权重 logits torch.einsum(brgk,bgk-brg, v_t, q_t) weights torch.softmax(logits, dim1) # 按注意力权重加权求和 v_attended torch.einsum(brg,brgk-bgk, weights, v_t) # 和问题向量逐元素相乘后压成最终融合特征 out self.fusion((v_attended * q_t.unsqueeze(1)).sum(dim1)) return out这段代码里的glimpse是注意力头数通常取 2。每次前向计算时图像区域特征先投影到低维空间再和问题向量做点积得到注意力分布按权重去加权图像区域。双线性注意力真正强的地方在于它让模型可以同时关注多个区域比如回答“这个人是站着还是坐着”时需要同时看上半身和腿的位置。选型建议代码简洁优先选逐元素相乘效果和实现难度之间最平衡。想在答辩里讲出深度建议上简化版 BAN。3. 数据集与预处理VQA 2.0 不香了但练手还得分得清3.1 COCO-QA 和 VQA 2.0拿哪个跑通第一版跑通第一版代码数据集选择直接决定迭代速度。COCO-QA 是老牌数据集训练集大概 7 万张图、25 万道题答案只有“是/否”和“其他”两类词汇量小模型半小时就能见到收敛趋势。VQA 2.0 是标准基准但完整训练集近 44 万道题光下载图片就要几十 GB单卡训练一轮超过两小时。从源码工程角度先用 COCO-QA 或者 VQA 2.0 的 val 集子集验证代码通路再切完整数据跑正式实验是我最常用的策略。注意一个原则最终答辩的硬指标必须跑在全量 VQA 2.0 的 val 集上否则被问到“训练集验证集是否同分布”很难答。3.2 最小预处理流水线问题词表、答案候选与图像 resizeVQA 数据预处理有三个步骤问题清洗、答案候选统计、图像归一化。问题清洗要把标点、冠词、大小写统一。答案候选统计直接从训练集里数频次选前 3000 个高频答案作为分类候选其他一律标记为__unknown__。import json, re, torchvision.transforms as T def preprocess_question(q_text): # 统一小写并去除非字母字符保留空格和问号 q q_text.lower().strip() q re.sub(r[^a-z0-9\s?], , q) return q def build_answer_vocab(annotation_file, top_k3000): with open(annotation_file) as f: anns json.load(f)[annotations] counter {} for ann in anns: ans ann[multiple_choice_answer] if multiple_choice_answer in ann else ann[answer] counter[ans] counter.get(ans, 0) 1 sorted_ans sorted(counter.items(), keylambda x: -x[1]) # 前 top_k 个作为候选其余答案是 OOV vocab [item[0] for item in sorted_ans[:top_k]] return vocab, counter train_transform T.Compose([ T.Resize((224, 224)), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])第一个坑多个标注者对同一张图同一道题给不同答案统计时要取multiple_choice_answer还是answer。VQA 2.0 官方要求用answer字段做训练分布统计用multiple_choice_answer做测试评估混用会导致答案候选和标签空间不一致。第二个坑答案词表从训练集统计但 val 集里仍有 5% 左右的答案不在词表里这类样本在训练时直接丢弃。全量训练集缩水 5% 对指标影响很小但能省掉一堆边界问题。3.3 DataLoader 这么写才能喂饱 GPUVQA 训练最烦的还不是模型而是数据读取速度跟不上 GPU。常见做法是在 DataLoader 里做三件事图像缓存、问题长度排序、采样权重均衡。from torch.utils.data import Dataset, DataLoader class VQADataset(Dataset): def __init__(self, img_dir, q_json, a_json, vocab, max_q_len14): self.img_dir img_dir self.q_data json.load(open(q_json)) self.a_data json.load(open(a_json))[annotations] self.vocab vocab self.max_q_len max_q_len # 把问题索引和答案索引全部预计算 self.samples self._build_index() def _build_index(self): samples [] for item in self.a_data: q preprocess_question(item[question]) tokens [self.vocab.get(w, 1) for w in q.split()][:self.max_q_len] # 1 是 OOV ans item[answer] if item[answer] in self.vocab else None if ans is None: continue # 跳过不在词表里的答案 samples.append((item[image_id], tokens, self.vocab[ans], len(tokens))) return samples def __getitem__(self, idx): img_id, tokens, ans_idx, q_len self.samples[idx] img_path f{self.img_dir}/COCO_train2014_{str(img_id).zfill(12)}.jpg img Image.open(img_path).convert(RGB) img train_transform(img) q_tensor torch.tensor(tokens, dtypetorch.long) return img, q_tensor, torch.tensor(q_len), torch.tensor(ans_idx) def __len__(self): return len(self.samples) def collate_fn(batch): # 按问题长度降序排列为 LSTM pack 做准备 batch.sort(keylambda x: x[2], reverseTrue) imgs, qs, lens, ans zip(*batch) imgs torch.stack(imgs) qs_padded torch.nn.utils.rnn.pad_sequence(qs, batch_firstTrue, padding_value0) return imgs, qs_padded, torch.tensor(lens), torch.tensor(ans)代码说明_build_index把 JSON 里的问题转成索引序列并且提前过滤掉 OOV 答案这一层非常关键否则训练时每轮都在做字符串匹配慢得让人怀疑人生。collate_fn里按长度降序排列是 LSTMpack_padded_sequence能正常工作的前提。max_q_len设为 14因为 VQA 2.0 里超过 14 个词的问题占比不到 0.1%。数据加载器实例化时记得设num_workers4到8pin_memoryTrue。如果用的是 Windowsnum_workers别超过 2否则容易卡死在数据加载阶段。4. 训练一个能用的精简版 VQAPyTorch 实现与参数解析4.1 模型结构从零手写一个底部融合基线完整结构是把前三章的组件拼起来ResNet 提取特征图LSTM 编码问题融合模块产出分类 logits。这里的手写模型要控制参数量和内存开销让单张 8GB 显存的显卡能跑起来。import torch.nn as nn import torchvision.models as models class SimpleVQA(nn.Module): def __init__(self, q_vocab_size, ans_vocab_size, embed_dim300, hidden_dim512): super().__init__() # 视觉编码器ResNet-50 去掉最后的全连接和池化保留 C5 特征图 resnet models.resnet50(pretrainedTrue) self.v_encoder nn.Sequential(*list(resnet.children())[:-2]) self.q_encoder QuestionEncoder(q_vocab_size, embed_dim, hidden_dim) self.v_proj nn.Linear(2048, 1024) self.q_proj nn.Linear(hidden_dim * 2, 1024) # 融合后接分类头 self.classifier nn.Sequential( nn.Dropout(0.3), nn.Linear(1024, 512), nn.ReLU(), nn.Dropout(0.3), nn.Linear(512, ans_vocab_size) ) def forward(self, images, q_tokens, q_len): v_feat self.v_encoder(images) # [batch, 2048, 7, 7] v_feat v_feat.mean(dim(2, 3)) # 全局平均池化 - [batch, 2048] v_vec torch.relu(self.v_proj(v_feat)) # 降到 1024 维 q_vec self.q_encoder(q_tokens, q_len) # [batch, 1024] q_vec torch.relu(self.q_proj(q_vec)) # 逐元素相乘融合 fused v_vec * q_vec logits self.classifier(fused) return logits代码逻辑说明ResNet 的children()[:-2]去掉了池化和全连接保留卷积输出特征图。全局平均池化把[batch, 2048, 7, 7]压成[batch, 2048]。v_proj和q_proj把两个模态映射到同一个 1024 维空间然后逐元素相乘。这个操作的物理含义是图像全局信息和问题语义信息在相同维度上逐位相乘激活同时出现在两边的维度。参数说明embed_dim用 300 与 GloVe 保持一致hidden_dim512 是效果与内存的折中256 会掉 2 到 3 个点1024 显存直接翻倍但指标提升不到 1%。如果想在答辩时增加注意力机制的讲解把mean(dim(2,3))替换成第 2 章的 SimpleBAN 即可。4.2 训练脚本损失函数、评估指标与学习率调度VQA 本质是一个多分类问题用交叉熵损失。评估指标官方用accuracy但这里有个细节VQA 2.0 的官方指标是“10 个人标注答案预测结果和其中至少 3 个人一致即算对”也就是wu_accuracy。直接用硬标签训练效果会略差于用软标签分布训练但实现简单且足够支撑答辩。import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, total_acc, total_num 0.0, 0.0, 0 for imgs, q_tokens, q_len, ans_idx in loader: imgs imgs.to(device) q_tokens q_tokens.to(device) ans_idx ans_idx.to(device) logits model(imgs, q_tokens, q_len) loss criterion(logits, ans_idx) pred logits.argmax(dim1) acc (pred ans_idx).float().mean().item() optimizer.zero_grad() loss.backward() # 梯度裁剪是 LSTM 训练的必要项不裁剪容易在长句上梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm2.0) optimizer.step() total_loss loss.item() * imgs.size(0) total_acc acc * imgs.size(0) total_num imgs.size(0) return total_loss / total_num, total_acc / total_num model SimpleVQA(q_vocab_sizelen(q_vocab), ans_vocab_sizelen(ans_vocab)) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr2e-4, weight_decay1e-5) scheduler CosineAnnealingLR(optimizer, T_max20, eta_min1e-5)参数说明lr2e-4是视觉文本跨模态模型的常用起点。纯分类任务可以用 3e-4但 VQA 的损失面更复杂调大会震荡。weight_decay1e-5 是经验值太大限制模型表达能力太小起不到正则作用。CosineAnnealingLR配合T_max20表示 20 个 epoch 完成一个完整余弦周期让学习率从峰值平滑降到 1e-5。训练 15 个 epoch 左右COCO-QA 验证集能到 55% 到 60% 的 accuracyVQA 2.0 精简版则能到 48% 到 52%。想快速出结果优先用 COCO-QA 做调试指标稳定后切正式集。4.3 单卡显存不够怎么办梯度累积与尺寸消减八到十年前的论文跑 VQA 用 V100 甚至 A100但手头只有一张 6GB 老显卡的情况更常见。两个技巧能把单 batch 上限从 16 拉到 64梯度累积和图像尺寸消减。accumulation_steps 4 model.zero_grad() for step, (imgs, q_tokens, q_len, ans_idx) in enumerate(loader): logits model(imgs, q_tokens, q_len) loss criterion(logits, ans_idx) / accumulation_steps loss.backward() if (step 1) % accumulation_steps 0: torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm2.0) optimizer.step() optimizer.zero_grad()梯度累积的逻辑是每 4 个 batch 的梯度累加后再更新一次参数效果等同于 batch size 放大 4 倍。注意损失必须除以accumulation_steps否则 loss 会被放大 4 倍学习率需要同步缩小。实际调试中我踩过这个坑不除的话曲线直接飞上天。图片尺寸消减是最原始但有效的办法。VQA 的 COCO 图片原图多在 500 到 640 像素之间加载时统一缩到 224 是标配。显存依然紧张的话把 ResNet 的输入从 224 缩到 160参数量不变但特征图更小能再省 20% 显存。代价是准确率掉 1% 到 2%可以接受。5. 避坑手册视觉问答系统最常见的 5 个翻车现场5.1 现象损失下降但准确率纹丝不动训练到第 10 个 epochloss 从 6.8 降到 4.2但 val accuracy 一直在 38% 到 40% 之间徘徊。原因分析答案候选词表出现了类别严重不平衡。VQA 2.0 里“是/否”占了接近 40% 的答案其他高频答案又集中在少数常见名词上。模型学到了“全猜是”这种偷懒策略loss 因为分类置信度提升而下降但真正的语义理解并没有学会。解决办法把“是/否”单独抽取出来。处理方式有三种一是对答案类别做重采样每轮随机丢弃部分“是/否”样本二是把模型拆成两个头一个判断问题类型是/否题还是开放题另一个输出具体答案三是在 loss 里给低频答案更高的权重直接用CrossEntropyLoss(weightclass_weight)其中class_weight按答案频率的倒数归一化得到。我一般选方案一最简单且不破坏模型的统一性。5.2 现象推理速度慢到答辩没法演示单张图配一个问题的推理耗时要 800 毫秒现场演示时基本是黑屏等待答辩节奏全被打乱。原因分析ResNet 用 224x224 输入在 CPU 上推理序列化路径太长且没有开torch.no_grad()和model.eval()之外的优化。很多人是顺手用了model.eval()但没做推理态优化。解决办法答辩演示前把模型导出为 TorchScript 并用torch.jit.optimize_for_inference做优化推理时间能压缩一半。如果设备允许直接把图像 resize 到 112x112 再做一次推理对比视觉分支的 FLOPs 降为原来的四分之一VQA 2.0 数据上的准确率只降 0.5% 到 1%完全够演示用。注意 batch 设为 1不要太贪心。import torch # 确保在 CPU 上跑导出的脚本化模型避免依赖 GPU libtorch model.eval() with torch.no_grad(): scripted torch.jit.script(model) optimized torch.jit.optimize_for_inference(scripted) optimized.save(vqa_demo.pt)5.3 现象ResNet 预训练权重加载报错或效果反而更差models.resnet50(pretrainedTrue)直接下载失败或加载进来后准确率比随机初始化还低。原因分析有两个可能。一是 torchvision 的版本太新或太旧pretrainedTrue参数在新版本里被替换成weightsResNet50_Weights.IMAGENET1K_V2API 变了。二是没有做正确的图像归一化ImageNet 预训练权重要求输入经过mean[0.485,0.456,0.406]、std[0.229,0.224,0.225]标准化你没做的话特征分布错位跟着冻结骨干网络去训练全连接效果还不如随机权重。解决办法统一用ResNet50_Weights.IMAGENET1K_V2的weights参数来加载并且用官方配套的transforms预处理。另外调试期间建议冻结 ResNet 前 3 个 stage只训练最后 1 个 stage 和融合层这样显存占用小、收敛也快。正式训练再解冻全部层用 1/10 的学习率微调视觉分支。5.4 现象融合层维度对不上shape 错误一天调三次v_vec的 shape 是[64, 2048]q_vec的[64, 1024]逐元素相乘直接报错。原因分析图像分支和文本分支的投影维度不一致。图像v_proj输出 1024文本q_proj输出 512 或者其他数字两个向量没法相乘。解决办法在模型初始化时加一个断言。这类问题想彻底避免就在__init__里写好维度约束assert self.v_proj.out_features self.q_proj.out_features, \ fv_proj 与 q_proj 输出维度必须一致当前分别为 {self.v_proj.out_features} 与 {self.q_proj.out_features}调试时用torch.onnx.export或者直接在 forward 里加 shape 打印到 stderr一眼就能看出哪一层维度对不上。别在融合层之后才发现排查成本至少 20 分钟。5.5 现象复现源码结果和论文对不上指标低 6 到 8 个点文档说明里写 VQA 2.0 验证集 accuracy 55%你复现出来只有 49%。原因分析有三处最常见的变量。第一个是数据划分源码可能在 vq 2.0 的train加上val的子集重新划分而你在全量 val 上评估。第二个是答案词表大小源码统计了全量 5 万类你只取了 3000那本应分到正确答案的数据全被归入__unknown__。第三个是随机种子和 batch size 不同batch size 小会导致 BN 统计量偏差影响最终指标。解决办法先比对答案词表再比对数据划分的文件 id 是否一致。如果文档说明里没有明确写划分方式默认按官方 VQA 2.0 的 train/val 划分复现。随机种子固定成torch.manual_seed(42)并把cudnn.deterministicTrue打开。这还不行从源码的 checkpoint 里直接恢复权重试一次能跑通说明是你的训练策略问题跑不通说明模型结构有差异。6. 答辩 PPT 怎么做从两张图到一条完整的故事线一份能打答辩的 PPT 不需要 40 页真正核心的只有三个部分问题定义与动机、方法设计、实验验证。方法设计部分放一张整体架构图就够按数据流向从左到右画图输入图片和问题中间分两条分支做特征提取融合模块汇合最后是分类输出。实验部分放两张损失曲线和一个可视化案例表格比任何文字都有说服力。第一张图是 backbone 特征图可视化用 Grad-CAM 展示模型对“这只狗是什么品种”的问题关注的是狗头部区域而对“草地上有什么”关注的是草地大面积区域。这说明模型不是瞎猜而是有了注意力。第二张图是答案预测的可视化。挑 8 个问题每个问题下面放图像、预测答案、真实答案、置信度四列做成一个 8 行 4 列的表格。置信度高且错误的案例留下这就是答辩时最值得讲的 failure case主动暴露问题比被评委问出来体面得多。演示 demo 的兜底方案也要提前准备。现场的设备可能是投影仪的电脑显卡性能未知。记住一个原则never demo on live network。把导出好的vqa_demo.pt和配套的数据集样例放到本地目录写一个 30 行的纯 CPU 推理脚本图像用 160 分辨率批大小 1提前录好一份 5 分钟的视频备用。脚本里做一个交互循环用户输入图片编号和问题模型输出 top 3 答案和置信度这个交互界面十行代码就够比在终端里打印结果直观好几倍。同时保留 GPU 版本结果截图万一现场 CPU 也卡直接放截图讲解。最后说个我自己的教训答辩前一周别动模型结构要调就调演示逻辑和 PPT 措辞。我当年在答辩前三天把融合层从拼接换成注意力机制结果指标回不到原来的水平答辩现场演示时模型输出全是“是”当场翻车。从那以后我给自己立了个规矩任何改动至少要留出 72 小时做回归验证否则不叫优化叫赌博。视觉问答系统的源码、文档、PPT 三位一体源码决定你能否跑通文档决定别人能否接手PPT 决定评委能否信服三件事要并行准备别到最后一天才开始写答辩稿。希望帮到你。附一个自查清单源码包里必须有requirements.txt、train.py、eval.py、demo.py四个文件文档说明里必须有数据集下载指引和复现步骤PPT 里必须有至少一张架构图、一条损失曲线、一个可视化案例表。这三样齐了这个项目就算闭环了。本文还有配套的精品资源点击获取