
简介面向医学影像分析与深度学习入门者的肺炎诊断工具包基于Transformer架构并结合ResNet34预训练权重完成胸部X光图像的肺炎分类任务。模型经过400轮训练批量大小32学习率0.0001并内置混淆矩阵评估模块便于直观查看各类别诊断准确率与误判情况。整套系统基于PyTorch实现代码结构清晰适合作为医疗AI课题的参考基线或教学案例。压缩包共13个文件核心为9个Python脚本涵盖模型定义、数据加载、训练、预测及混淆矩阵绘制等完整流程另含json类别索引、md说明、txt配置说明及docx附赠文档可辅助环境搭建与参数理解。资源包仅55KB轻量易用。目前已有55人学习下载适合希望快速上手Transformer医学影像分类或需要一套可复现评估流程的研究者与开发者。1. 用ResNet34Transformer做胸片肺炎诊断400轮训练背后真正值得关注的设计点胸部X光肺炎诊断这两年已经从“有没有肺炎”升级到“要区分细菌性还是病毒性”再进一步就是给病灶定位。纯CNN模型比如ResNet34对局部磨玻璃影和间质性纹理很敏感但胸片上的病灶常常是分散的、跨肺野的CNN的局部感受野容易漏掉全局上下文纯Transformer又需要海量数据和很长的训练时间在几千张胸片这种小规模数据集上直接翻车是常事。所以这个标题的工程思路很聪明用ResNet34预训练权重先把图像咬成稳定的局部特征再接Transformer做全局交互让模型同时拿到细节和位置关系。400轮、batch_size32、lr0.0001这套配置看似常规但每个数字都要跟模型架构、数据量、预训练权重状态配合否则要么200轮过拟合要么跑完400轮验证集还在震荡。这篇文章按“模型结构 → 数据与训练 → 混淆矩阵评估 → 排坑 → 验证”的顺序把整个方案的复现路径讲透。适合手里有胸片数据集、想用PyTorch做可解释分类系统的算法工程师或医工交叉方向的研究生。下面所有代码都按PyTorch 1.13及以上版本书写如果你还在用旧版本注意TransformerEncoderLayer的激活函数参数名要改成activationgelu这种写法。2. 模型主干设计为什么是ResNet34预训练权重而不是纯Transformer把“基于Transformer架构”和“ResNet34预训练权重”放在同一个标题里很多人第一反应是“这俩怎么拼”。其实这在医学影像分类里是过去两年最稳的混合架构CNN负责提取局部纹理Transformer负责建模长距离依赖。下面对模型每一层做拆解包括张量流、权重复用和冻结策略。2.1 混合架构的张量尺寸变化从512通道特征图到64个token序列胸部X光原图分辨率通常很大但医学影像数据集样本量有限直接整图喂Transformer完全不现实。常见做法是先让ResNet34把图像压缩成语义特征图再把每个空间位置当作token送入Transformer。假设输入是256×256的灰度胸片复制成3通道ResNet34主干一直保留到conv5_x输出得到[batch, 512, 8, 8]的特征图。把这8×864个位置展开每个位置有512维特征相当于一句话有64个“词”每个词的嵌入维度是512。接着加上可学习的位置编码因为胸片的肺野区域、心影位置、肋膈角在解剖学上固定位置信息跟病灶定位一样重要。最后通过TransformerEncoder后取CLS token分类。完整定义如下import torch import torch.nn as nn import torchvision.models as models class ResNetWithTransformer(nn.Module): def __init__(self, num_classes3, transformer_layers4, nhead8): super().__init__() # 加载ResNet34预训练权重去掉全局池化和全连接层 resnet models.resnet34(weightsmodels.ResNet34_Weights.IMAGENET1K_V1) self.features nn.Sequential(*list(resnet.children())[:-2]) # 输出 [B, 512, 8, 8] # Transformer编码器d_model必须等于特征图通道数512 encoder_layer nn.TransformerEncoderLayer( d_model512, nheadnhead, dim_feedforward2048, dropout0.1, activationgelu ) self.transformer nn.TransformerEncoder(encoder_layer, num_layerstransformer_layers) # 可学习位置编码1行对应batch共享64个空间位置512维 self.pos_embed nn.Parameter(torch.randn(1, 64, 512)) # CLS令牌用于聚合全局信息 self.cls_token nn.Parameter(torch.zeros(1, 1, 512)) self.classifier nn.Linear(512, num_classes) def forward(self, x): x self.features(x) # [B, 512, 8, 8] B, C, H, W x.shape x x.flatten(2).permute(0, 2, 1) # [B, 64, 512] x x self.pos_embed # 位置编码加到每个token上 cls self.cls_token.expand(B, -1, -1) # [B, 1, 512] x torch.cat([cls, x], dim1) # [B, 65, 512] x self.transformer(x) cls_out x[:, 0] # 取CLS token return self.classifier(cls_out)逻辑说明self.features取自resnet34的卷积段conv1到layer4list(resnet.children())[:-2]去掉了最后的avgpool和fc。输入经过ResNet34下采样32倍256×256得到8×8特征图。每个空间位置的512维向量就是一个token这就把图像从像素空间转换成了语义空间。位置编码用randn初始化让模型在训练中自己去学相对位置这样比固定的正弦编码更适应胸片的解剖位置。CLS token在训练后可以理解为模型的全局决策向量它的注意力权重对应着模型对每个区域的重视角。参数说明transformer_layers4是经验值64个token很短4层编码器足够捕获跨肺野依赖堆到8层会在小数据集上过拟合。nhead8要求d_model能被8整除512/864每个头的维度合适。dim_feedforward2048是前馈网络中间维度如果显存紧张可以降到1024收敛速度会慢一些精度通常影响不大。2.2 预训练权重的加载与通道适配灰度图如何复用ImageNet权重胸片是灰度图而ResNet34官方预训练权重是在ImageNet上训练的第一层卷积期望3通道输入。两种常见适配方式一是把灰度图复制成三通道继续用官方权重二是把第一层卷积改成1通道将预训练卷积核在通道维取平均。工程上推荐第一种因为ImageNet预训练模型对边缘、纹理的低层响应本来就是跨颜色空间泛化的复制通道不会破坏特征。代码如下import torchvision.models as models def build_model(num_classes3, pretrainedTrue): resnet models.resnet34(weightsmodels.ResNet34_Weights.IMAGENET1K_V1 if pretrained else None) # 截取到layer4不要avgpool和fc backbone nn.Sequential(*list(resnet.children())[:-2]) model ResNetWithTransformer(num_classesnum_classes) # 将官方backbone的权重复制到我们模型的特征提取器 model.features.load_state_dict(backbone.state_dict()) return model逻辑说明load_state_dict要求两边的层名和shape完全一致。这里直接把backbone.state_dict()装进model.features因为二者来自同一个官方结构的前半部分。真正在训练循环里输入图像会经过一个repeat(3,1,1)操作把单通道复制成三通道这一步放在Dataset里我们会在下一章数据部分看到。参数说明pretrainedTrue会从PyTorch官方hub下载权重到本地缓存。如果你的训练机没有外网需要提前在有网的机器上下载好resnet34-333f7ec4.pth拷贝到缓存目录否则程序会在第一步卡住报“Connection error”。使用预训练权重是本项目能400轮收敛的核心原因从头训练ResNet34Transformer在这个规模的数据上至少要2000轮而且稳定性差很多。2.3 冻结与解冻策略浅层冻结、深层与Transformer全量微调医学图像与自然图像差异很大但ResNet34的底层卷积仍然能提取通用边缘、纹理。训练中如果把整个backbone全部解冻胸片小数据集很容易让浅层卷积被噪声带偏如果完全冻结深层特征又无法适配胸片的特异性纹理。一个可复制的策略是前5轮冻结整个ResNet只训练Transformer和分类头让Transformer先适应ImageNet特征分布从第6轮开始解冻layer3和layer4浅层保持冻结。这样既防止灾难性遗忘又能让高层特征适应肺炎病灶。# 初始冻结backbone只训练Transformer和classifier for name, param in model.named_parameters(): param.requires_grad False for param in model.transformer.parameters(): param.requires_grad True for param in model.classifier.parameters(): param.requires_grad True # 训练到第6轮时解冻layer3(索引6)和layer4(索引7) def unfreeze_deep_layers(model, start_epoch, current_epoch): if current_epoch start_epoch: for i in [6, 7]: # ResNet34的layer3和layer4 for p in model.features[i].parameters(): p.requires_grad True逻辑说明model.features里的索引0-7分别对应conv1、bn1、relu、maxpool、layer1、layer2、layer3、layer4。冻结前5轮后Transformer已经学会了把特征图中的信息聚合到CLS token此时再解冻layer3和layer4梯度可以同时调整高层卷积和Transformer避免低层特征被破坏。参数说明如果你的数据集有1万张以上可以从第1轮就全量微调如果只有2000张左右建议把冻结轮数延长到10轮。解冻太晚会让Transformer学习的特征分布与最终backbone输出不匹配验证集表现会出现一次抖动这是正常的接着训下去会回升。3. 数据流程与400轮训练配置从DICOM到batch_size32、lr0.0001的工程落地模型结构定下来之后真正决定成败的是数据管道和超参数。胸部X光片的数据量通常不大类别分布也极不均衡稍不注意就会得到“看起来loss很低但实际是在瞎猜”的模型。3.1 数据加载与预处理灰度图、resize、增强和归一化原始胸片可能是DICOM或PNG/JPG。这里以PNG为例读取为灰度图后做基础变换。注意输入尺寸选256而不是常见224因为224下采样32倍是7×749个token256则是8×864个token后者对肺部小病灶的定位粒度更好Transformer计算量增加不大。代码中的预处理做了灰度转三通道与预训练权重匹配。import torch from torch.utils.data import Dataset from PIL import Image import torchvision.transforms as T class PneumoniaXrayDataset(Dataset): def __init__(self, image_paths, labels, trainTrue): self.paths image_paths self.labels labels self.train train self.base T.Compose([ T.Resize((256, 256)), T.ToTensor(), ]) self.augment T.Compose([ T.RandomRotation(10), T.RandomHorizontalFlip(), T.ColorJitter(brightness0.2, contrast0.2), ]) def __len__(self): return len(self.paths) def __getitem__(self, idx): img Image.open(self.paths[idx]).convert(L) # 灰度 img self.base(img) if self.train: img self.augment(img) img img.repeat(3, 1, 1) # [1,256,256] - [3,256,256] mean torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1) std torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1) img (img - mean) / std return img, self.labels[idx]逻辑说明convert(L)将RGB转成单通道灰度。repeat(3,1,1)将单通道沿着通道维度复制成3份这样即使原图是暗色调的低对比度胸片也能复用ImageNet的BN统计量。归一化用的均值和标准差是ImageNet的标准值这与预训练权重保持一致是迁移学习里最容易漏但最重要的一步。参数说明RandomRotation(10)旋转角度控制在±10度。胸片的肺尖、心影、肋膈角有解剖约束旋转超过15度模型会学到不真实的形态。ColorJitter(brightness0.2, contrast0.2)模拟X光机曝光差异。ColorJitter对灰度图操作时只影响亮度和对比度不会引入异常色偏。3.2 类别不均衡与数据划分按患者级别拆分而不是按图像随机拆分很多人在这一步翻车直接对所有胸片做随机train_test_split同一个患者的多张片子被同时分到训练集和验证集。模型其实是在“认患者”而不是在认病灶验证精度虚高。正确做法是先把患者ID聚合按患者分层划分。from sklearn.model_selection import train_test_split import pandas as pd df pd.read_csv(metadata.csv) # 字段patient_id, image_path, label # 每个患者只取一行的label作为该患者的类别标签用于分层 first_label df.groupby(patient_id)[label].first() patients first_label.index labels first_label.values train_patients, val_patients train_test_split( patients, test_size0.2, stratifylabels, random_state42 ) train_df df[df[patient_id].isin(train_patients)] val_df df[df[patient_id].isin(val_patients)]逻辑说明groupby(patient_id)[label].first()确保在分层时每个患者只贡献一个样本的标签。stratifylabels让训练集和验证集里各类别比例接近原始数据集避免某个类别只在验证集出现。划分之后再把原始csv过滤成train_df和val_df之后Dataset加载时就只用这两个子集。参数说明test_size0.2假设你总患者数在数百到数千。如果患者数少于200验证集会太小改成0.15会好一些如果每个患者都有多张视图这样可以保证同一患者的所有片子在同一个partition里。3.3 400轮训练循环分阶段解冻、梯度裁剪与余弦退火400轮指的是epoch数而不是iteration。用固定学习率0.0001跑400轮几乎一定会震荡到loss不降。正确的做法是用余弦退火把学习率从1e-4平滑降到1e-6并在训练初期对backbone做分阶段解冻。下面是可运行的训练骨架import torch import torch.nn as nn from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR model build_model(num_classes3, pretrainedTrue) device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) # 先冻结backbone所有参数 for param in model.features.parameters(): param.requires_grad False optimizer AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr1e-4, weight_decay1e-4 ) scheduler CosineAnnealingLR(optimizer, T_max400, eta_min1e-6) loss_fn nn.CrossEntropyLoss() for epoch in range(400): # 从第6轮开始解冻layer3和layer4 if epoch 5: for i in [6, 7]: for p in model.features[i].parameters(): p.requires_grad True optimizer.add_param_group({params: model.features[6].parameters(), lr: 1e-5}) optimizer.add_param_group({params: model.features[7].parameters(), lr: 1e-5}) model.train() total_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() logits model(imgs) loss loss_fn(logits, labels) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() scheduler.step() if (epoch 1) % 10 0: print(fEpoch {epoch1:3d}/400 | loss {total_loss/len(train_loader):.4f})逻辑说明filter(lambda p: p.requires_grad, model.parameters())一开始只给Transformer和分类器优化器参数。到第6轮解冻layer3和layer4时用add_param_group给新解冻的参数单独设置更小的学习率1e-5避免大梯度冲击。clip_grad_norm_对Transformer很重要注意力层偶尔会产生异常大的梯度不裁剪一个step就可能让loss变成nan。CosineAnnealingLR调度器在400轮内将lr平滑降到1e-6后期模型在损失平面平坦区域稳定下来。参数说明batch_size32时学习率1e-4是比较稳的起点。如果显存不足改到16学习率建议先降到5e-5因为批量变小梯度噪声变大相同的lr会导致更新方向不稳定。weight_decay1e-4是Transformer微调的常规值太大容易欠拟合太小后期会过拟合。如果你用的是冻结策略优化器里一开始没有backbone的权重天然会减少正则化压力。4. 混淆矩阵评估真正用来指导临床决策的指标怎么算训练400轮之后报告里不能只写“accuracy 92%”这种话。在肺炎诊断任务中漏诊一个细菌性肺炎的后果比把正常人错判成肺炎严重得多。混淆矩阵能揭示每一类错误具体是怎么分布的也能帮你发现模型是不是在靠类别先验猜答案。4.1 从验证集到多分类混淆矩阵归一化与可视化以三类为例Normal正常、Bacterial细菌性肺炎、Viral病毒性肺炎。我们需要跑一遍验证集将所有预测结果和真实标签收集起来用sklearn生成混淆矩阵并按行归一化。因为类别样本数不均衡归一化后才能看出每一类的召回率差异。from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt import numpy as np def evaluate_confusion(model, dataloader, device, class_names): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in dataloader: imgs imgs.to(device) logits model(imgs) preds logits.argmax(dim1).cpu() all_preds.extend(preds.numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) cm_norm cm.astype(float) / (cm.sum(axis1, keepdimsTrue) 1e-8) plt.figure(figsize(8, 6)) sns.heatmap(cm_norm, annotTrue, fmt.2f, cmapBlues, xticklabelsclass_names, yticklabelsclass_names) plt.ylabel(True Label) plt.xlabel(Predicted Label) plt.savefig(confusion_matrix_normalized.png, dpi200) print(classification_report(all_labels, all_preds, target_namesclass_names, digits4))逻辑说明cm.sum(axis1)得到每行真值类别的总样本数用行归一化后每一行代表“真实为该类别时被模型预测成各个类别的比例”。这样即使Viral类只有50张Normal类有500张也能清楚看到Viral类的召回率是否偏低。classification_report会输出每个类别的precision、recall和f1这些值对医生来说比accuracy更有临床意义。参数说明fmt.2f显示两位小数足够看出0.85和0.88的差别。digits4让报告保留4位小数避免在类别样本少的时候显示成0.0000导致误判。dpi200保证输出图片在投影或论文里依然清晰。如果你要回到二分类Pneumonia vs Normal同样的代码可以直接跑只是class_names换成两个元素。4.2 从混淆矩阵计算敏感度、特异度与F1得分多分类任务里敏感度就是每个类别的召回率特异度需要额外计算。医学报告通常需要这几个数字下面这段代码直接给出一份可粘贴的表。def compute_clinical_metrics(cm, class_names): recall_each np.diag(cm) / (cm.sum(axis1) 1e-8) precision_each np.diag(cm) / (cm.sum(axis0) 1e-8) f1_each 2 * precision_each * recall_each / (precision_each recall_each 1e-8) print(f{Class:12}{Precision:10}{Recall:10}{F1-score:10}) for i, name in enumerate(class_names): print(f{name:12}{precision_each[i]:10.4f}{recall_each[i]:10.4f}{f1_each[i]:10.4f}) # 多分类特异度对类别iTN是非i样本中预测为非i的数量FP是非i样本中预测为i的数量 tn cm.sum() - cm.sum(axis1) - cm.sum(axis0) np.diag(cm) fp cm.sum(axis0) - np.diag(cm) spec_each tn / (tn fp 1e-8) print(f\nMacro-Precision: {precision_each.mean():.4f}) print(fMacro- Recall: {recall_each.mean():.4f}) print(fMacro-Specificity: {spec_each.mean():.4f}) print(fMacro-F1: {f1_each.mean():.4f})逻辑说明敏感度反映“漏诊率”特异度反映“误诊率”。在多分类里对类别i所有真实不是i的样本总数是cm.sum() - cm.sum(axis1[i])其中预测为i且真实不是i的数量是cm.sum(axis0[i]) - cm_diag[i]。这段代码在类别不均衡时依然有效。宏平均的F1将三个类别同等对待比加权F1更能暴露少数类上的Performance。参数说明1e-8防除零。如果某个类别的precision或recall为0宏平均会立刻拉低F1这样比只看准确率诚实得多。如果你的类别定义是二分类特异度公式仍然成立只是输出的宏平均等于整体特异度。5. 训练与评估中的常见坑这几处让多少人白白跑完400轮踩坑经验比模型结构更重要。下面五条都是实际训练中反复出现的典型问题每条都按“现象 → 原因 → 解决”写清楚。5.1 预训练权重加载报错fc层或第一层卷积shape不匹配现象model.features.load_state_dict(backbone.state_dict())报错提示“size mismatch for fc.weight”或者“Missing key(s): fc.weight”。原因官方resnet34的state_dict里包含fc.weight和fc.bias而我们自定义的model.features没有fc层。另外如果修改了第一层卷积的输入通道conv1.weight的shape也会对不上。解决截取backbone时使用list(resnet.children())[:-2]这样state_dict里就已经没有fc层了。如果改了第一层卷积就不要用load_state_dict加载conv1.weight或者改用复制三通道输入的方式彻底绕开这个问题。还有一种保险做法是加载时带一个strictFalse过滤掉不匹配的key但建议只在你知道自己在改什么时才用。5.2 训练loss在下降但验证集准确率一直在50%左右现象每个epoch打印的loss都稳步下降看起来模型在学习但验证集accuracy只有五成且分类报告里某一类的recall为0。原因数据划分没有按患者分层导致同一个人不同拍摄角度的胸片出现在训练集和验证集模型记住的是患者特征。另一个常见原因是类别极不均衡且CrossEntropyLoss没有设置类别权重模型把所有样本都预测为多数类Normalloss看起来低但少数类全部被忽略。解决按患者ID分层划分具体见3.2节代码。对于类别不均衡在损失函数里传入weight权重与样本频次成反比例如nn.CrossEntropyLoss(weighttorch.tensor([1.0, 3.0, 5.0]).to(device))。或者使用torch.nn.functional.focal_loss需要自己实现。先在控制台打印每个类别的样本数根据比例设定weight通常能让少数类的recall有明显提升。5.3 显存不足把batch_size从32改到16后loss发散现象原配置batch32跑得好好的为了塞进单卡改到16前几个step loss就冲到2.0以上之后持续震荡不下降。原因学习率与批量大小高度相关。批量变小意味着每个step的梯度噪声变大同样的学习率会让参数更新方向偏离真实梯度。直接改batch而不动lr是典型翻车操作。解决按线性缩放规则调整学习率常见做法是lr_new lr_old * (new_batch / old_batch)即从1e-4降为5e-5。如果你用的是AdamW可以先降到7e-5观察10个epoch。更稳妥的是在优化器上单独给Transformer和backbone不同lrbackbone建议始终比Transformer低一个数量级。如果必须用batch8学习率降到2e-5并配合前20个step的warmup。5.4 混淆矩阵对角线“好看”但仔细看某一类敏感度极低现象整体accuracy在90%以上但看归一化混淆矩阵时“Bacterial”这一行的召回率只有0.55有0.3被错判成了“Viral”。原因类别特征之间存在语义重叠或者该类别训练样本过少。如果三个类别的样本数差距很大多数类会主导梯度更新少数类的决策边界被挤压。另一个原因是数据增强过度比如旋转角度过大导致肺纹理方向失真。解决先按患者分层划分并设置class weight。若Bacterial和Viral混淆严重可以考虑合并成统一的Pneumonia再进行二分类然后在二分类基础上再分亚型形成两阶段模型。这样主诊断的敏感度会高很多亚型分类再用单独的模型负责。5.5 Transformer注意力权重趋于平均模型退化成线性特征拼接现象训练结束后可视化CLS token在最后一层的注意力权重发现所有位置几乎均匀分布没有明显的热点区域。验证集loss虽然正常但Grad-CAM热力图非常分散看不出对病灶的聚焦。原因位置编码没被正确加入或者Transformer的dropout设得过高比如0.5导致注意力被随机化。另一个常见原因是backbone在训练早期就被全量解冻CNN把信息压到少数字符里Transformer偷懒学会了忽略注意力。解决将Transformer的dropout降到0.1。检查self.pos_embed是否参与梯度更新它应当在训练中发生变化。另外严格按照2.3节的冻结策略执行让训练前5轮只调整Transformer迫使它学会利用位置信息。如果问题依然存在可尝试把nhead降到4提升每个注意力头的维度让注意力分配更有区分度。6. 进阶验证用Grad-CAM和注意力可视化确认模型在看肺尖还是背景训练完400轮后准确率和混淆矩阵只回答了“分得对不对”回答不了“医生凭什么信你”。胸片诊断要落地必须让人看到模型在做决策时关注哪些像素。这里推荐同时做两件验证一是用Grad-CAM看CNN特征对分类的贡献区域二更简单——直接取Transformer最后一层的注意力权重映射到原图上看CLS token在呼应哪些空间位置。具体做法是在模型forward过程中注册一个hook取Transformer输出的attention map。PyTorch的TransformerEncoderLayer在forward时会返回attention权重但需要稍微侵入代码。如果你不想改模型可以在forward里把self.transformer换成自写的编码器层并收集每层的attn_output_weights。拿到权重后对CLS token那一行的权重做平均或取最后一层归一化到0-1用matplotlib叠加在原图上。我习惯把结果和Grad-CAM并排放一起如果两个热力图的中心都不在肺野而在纵膈区域那说明模型学到的线索是背景伪影需要回到数据层面去修复预处理、裁剪或更严格的标注。对于患者级别的效果验证还有一个很实用的技巧对同一张胸片分别做原图预测、左右翻转预测、以及遮挡四个象限后的预测观察logits变化。如果翻转后类别翻转、或者遮挡某块背景后预测概率明显变化说明模型对胸片的解剖结构理解不足。我会在训练结束前用验证集里三张最容易误判的片子跑一组这种“稳定性测试”把结果作为模型能否提交给医生review的硬性门槛。经过这么一轮你会对“400轮到底够不够”有自己的判断。我的习惯是每次调完一个超参数就把混淆矩阵、注意力热图、几张典型误判样本存到一个以日期命名的文件夹里下次迭代直接对比。这套流程看起来繁琐但可以省掉很多重复跑400轮的时间。希望帮到你。本文还有配套的精品资源点击获取