ARTICLE DETAIL

资讯详情

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

基于COCO格式X光片的肺炎识别:迁移学习与目标检测实战

基于COCO格式X光片的肺炎识别:迁移学习与目标检测实战 简介这份新冠肺炎检测数据集以X光胸透光片为素材使用COCO格式对一千七百六十五张原始影像进行精细标注可识别新冠肺炎、正常与肺炎三种状态覆盖医学影像分类与目标检测的主流应用场景。图片包含多种拍摄角度与清晰度贴近真实临床条件能有效检验模型的泛化能力。压缩包共一千七百七十个文件核心为jpg图像、json标注文件与txt说明文件整包大小六十一点四一兆字节标注与图像一一对应拿来即可用于训练。json文件提供边界框和类别信息txt文件用于数据集划分或类别定义免去格式转换目前已有九百零六人学习下载可直接接入YOLO、MMDetection等检测框架快速完成数据准备与模型验证。整体结构清晰上手简单可显著缩短环境搭建与数据预处理时间。1. 用1765张COCO格式X光片做肺炎识别先想清楚这套数据能训练一个什么样的模型第一次拿到这个“新冠肺炎检测数据集”时很多人会下意识把它当普通图片分类任务处理——解压、丢进训练脚本、等准确率。但1765张原始图片这个规模并不大再加上COCO格式的标注比单纯放文件夹多出一层解析逻辑真正动手时才发现光是把数据读对就能消耗掉半天时间。这个数据集的实际价值在于用COCO格式对1765张X胸透光片完成标注让模型能区分新冠肺炎、正常、肺炎三种状态同时保留检测框的位置信息为后续从图像分类升级到病灶定位留好接口。本文面向正在做医疗影像识别训练的算法工程师、参加竞赛的学生以及想验证迁移学习落地效果的开发者我按自己处理类似数据集的经验把从解压到验证这条链路完整拆开讲。2. 先看数据再谈训练这个肺炎检测数据集的目录结构与COCO标注怎么读2.1 拿到zip先做三件事解压、读说明、核对图片和标注数量别急着写训练代码。这类压缩包解压后通常是一个比较规整的目录但不同来源打包习惯差异很大我第一次拿到类似数据集时没看目录就直接写路径结果脚本跑了一半才发现图片藏在嵌套子目录里。先解压再看结构成本最低。unzip 新冠肺炎检测数据集.zip -d ./cxr_dataset cd ./cxr_dataset find . -maxdepth 2 -type f | head -30# 如果目录层级比较深用这条把所有图片路径和json路径打出来核对 find . -name *.json -o -name *.png -o -name *.jpg -o -name *.jpeg | head -50解压后先看README或说明文档再看图片目录和标注文件是否齐全。按这类COCO数据集的通例目录布局大致是下面这样具体以你拿到的包里实际结构为准路径常见内容images/ 或 JPEGImages/1765张X胸透光片格式多为png或jpgannotations/一个或几个COCO格式的json标注文件README.md 或 data_description.txt类别定义、标签来源、数据划分说明train.txt / val.txt部分打包者会附带划分好的文件列表如果只有一个总标注json说明需要自己做训练集和验证集划分如果已经拆成train.json和val.json可以直接跳到第4章。另外一定用脚本数一下图片文件数量和json里images数组的长度是否相等。常见做法是把不完整的数据和标注文件混在一起数量对不上时先排查不要带着脏数据往下走。2.2 COCO标注里的三个核心数组images、annotations、categoriesCOCO格式本质上是一整套以json为核心的标注协议理解它不需要读完整文档只需要抓住三个顶层数组。第一个是images数组描述每张图片的id、文件名、宽高第二个是annotations数组记录每个标注框挂在哪张图上、属于哪个类别、框的坐标和面积第三个是categories数组定义类别id与类别名的映射。{ images: [ {id: 0, file_name: covid_001.png, width: 512, height: 512}, {id: 1, file_name: normal_001.png, width: 512, height: 512} ], annotations: [ {id: 0, image_id: 0, category_id: 1, bbox: [128.5, 96.2, 300.1, 340.8], area: 102274.6, iscrowd: 0} ], categories: [ {id: 1, name: covid-19}, {id: 2, name: normal}, {id: 3, name: pneumonia} ] }这里面最容易被忽略的是bbox的坐标系。COCO规定bbox是绝对值像素坐标格式为[x, y, width, height]x和y是框左上角的像素位置width和height是框的宽度和高度不是归一化值。area字段对矩形框而言等于width乘以height如果是带多边形分割的标注则按多边形面积计算。iscrowd为0表示这个框是单个实例为1表示一群重叠物体X光片标注里几乎不会用到。还要注意categories里的id不一定是连续的0、1、2很多数据集为了语义清晰会从1开始甚至跳号。训练时需要自己重映射成连续标签这块在第三章给代码。有个现实问题也在这里埋着有些数据集会把“正常”类表达成空标注也就是图片存在但没有对应的annotations条目。这种情况按COCO标准读法会把正常类全部丢掉所以下一步要先做个完整性检查。2.3 用一段脚本验证每张X光片都有对应的标注框在正式写Dataset之前我习惯先跑一个最小检查脚本确认标注框和图片的对应关系以及三类样本的数量分布。这一步能帮你提前发现漏标、空标注、类别比例严重失衡这些问题。import json from collections import Counter with open(annotations/instances_all.json, r, encodingutf-8) as f: coco json.load(f) images coco[images] annotations coco[annotations] # 统计每个类别的标注框数量 cat_count Counter(a[category_id] for a in annotations) print(标注框数量:, len(annotations)) print(各类别框数:, dict(cat_count)) # 找出没有标注框的图片 ann_img_ids {a[image_id] for a in annotations} missing [img[id] for img in images if img[id] not in ann_img_ids] print(没有标注框的图片数量:, len(missing)) print(无标注图片id示例:, missing[:10])这段脚本的核心逻辑是拿annotations里的image_id集合去比对images数组。缺失的情况分两种一种是“正常”类图故意不画框另一种是标注人员漏标。区分方法是抽查missing里的图片用labelimg或CVAT打开看一眼如果图里明显有肺部区域但没有任何框就是漏标。漏标样本如果直接进训练会被跳过等于变相把类别比例改掉了后面模型会学偏最好统计清楚再决定是过滤还是手动补标。注意如果“正常”类是以空标注表达的后面训练分类模型时要从images数组取label而不是从annotations里取。不要把这种情况当作脏数据删掉。3. 从COCO标注到可训练的识别样本分类和检测两条路怎么走3.1 先判断任务是分类还是检测同一份COCO标注的两种读法标题里写的是“识别三种状态”这句话在模型层面其实有歧义。图像分类是给整张X光片一个标签模型输出的是“这张片子是新冠肺炎/正常/肺炎”的概率分布目标检测则是输出“图里哪里有病变、病变属于哪一类”模型同时做定位和分类。COCO格式原本是为检测设计的但很多X光片数据集会退化成分类来用——把整张图或整个肺野框起来框内内容就是一个样本。怎么判断这个数据集的设计意图看bbox的尺寸占比。写一个小脚本统计所有框的面积除以对应图片的面积如果绝大部分框的占比超过0.7说明标注者把整个肺野或整张图框了起来本质是服务分类任务如果框只覆盖局部区域那就要按检测任务来训练。我见过不少项目在这个环节判断错误把全图框当目标检测训练模型学了半天只能输出一个覆盖全图的大框没有实际定位意义。avg_ratio 0.0 cnt 0 for ann in annotations: img_id ann[image_id] img_info next(img for img in images if img[id] img_id) x, y, w, h ann[bbox] box_area w * h img_area img_info[width] * img_info[height] avg_ratio box_area / img_area cnt 1 print(平均框面积占比:, avg_ratio / cnt)从实践看1765张图的规模对检测任务偏小直接训练检测器容易过拟合对分类任务则完全够用配合迁移学习能做到不错的效果。所以下面先按分类任务给完整流程再给一个COCO转YOLO格式的脚本方便你想尝试检测路线时直接切换。3.2 把COCO转换成PyTorch Dataset裁剪、标签重映射与数据划分有了清晰的标注理解接下来把它变成一个能直接喂给PyTorch的数据集类。这里有一个关键设计无论任务最终是分类还是检测统一在Dataset层做解析上层训练代码保持干净。import json import os from PIL import Image import torch from torch.utils.data import Dataset from torchvision import transforms class CXRDataset(Dataset): def __init__(self, coco_path, img_dir, modecls, size224, transformNone): super().__init__() with open(coco_path, r, encodingutf-8) as f: self.coco json.load(f) self.img_dir img_dir self.mode mode self.size size self.transform transform # 从categories动态构建标签映射不硬编码 self.cat2label {} for idx, cat in enumerate(self.coco[categories]): self.cat2label[cat[id]] idx self.items self._build_items() def _build_items(self): items [] # 建立 image_id - annotations 的映射 ann_map {} for ann in self.coco[annotations]: ann_map.setdefault(ann[image_id], []).append(ann) for img in self.coco[images]: img_path os.path.join(self.img_dir, img[file_name]) anns ann_map.get(img[id], []) if self.mode cls: # 分类任务取第一个框的类别作为整图标签 if len(anns) 0: label self.cat2label[anns[0][category_id]] else: # 空标注图是否保留由外部参数控制这里先跳过 continue items.append((img_path, label, None)) else: # 检测任务保留所有框 labels [self.cat2label[a[category_id]] for a in anns] boxes [a[bbox] for a in anns] # coco格式: [x, y, w, h] items.append((img_path, labels, boxes)) return items def __len__(self): return len(self.items) def __getitem__(self, idx): img_path, label, boxes self.items[idx] image Image.open(img_path).convert(RGB) if self.mode cls: image image.resize((self.size, self.size)) if self.transform: image self.transform(image) return image, label else: # 检测模式返回图片和原始标注collate_fn需自行处理 return image, torch.tensor(boxes, dtypetorch.float32), torch.tensor(label)这个Dataset类的核心逻辑是_build_items方法先把annotations按image_id聚合避免到__getitem__里每次遍历全表然后动态构建类别映射不管json里的categories id是0还是1开头都能对齐。classification模式只取每个图像的第一个框作为标签前提是框覆盖肺野或全图如果一张图有多个小框且分属不同类别就不能简单取第一个这种情况需要改为多标签或多实例聚合属于边界场景。数据划分也要在这里做不能用默认的随机切分。一个实用做法是先按类别标签做分层抽样from sklearn.model_selection import train_test_split # 先把所有样本的路径和标签取出 paths [item[0] for item in dataset.items] labels [item[1] for item in dataset.items] train_paths, val_paths, train_labels, val_labels train_test_split( paths, labels, test_size0.2, random_state42, stratifylabels )stratifylabels保证训练集和验证集里三个类别的比例和原始数据集一致。这个细节很关键如果直接随机切分且原始数据里新冠样本只占10%验证集里很可能只剩几十张甚至分裂缺失评估结果方差极大这也是为什么我反复强调划分要在早期脚本里就固化下来。3.3 如果后面要换YOLOv8把COCO转成归一化txt标签很多人习惯用YOLO系列跑检测但YOLO不认COCO的json需要转成自己的txt格式。这个转换坑不少最典型的是归一化错误。YOLO的标签格式是类别id、归一化后的中心点x、中心点y、归一化后的宽w、高h全部是0到1之间的浮点数。import json import os def coco_to_yolo(coco_path, out_dir): os.makedirs(out_dir, exist_okTrue) with open(coco_path, r, encodingutf-8) as f: coco json.load(f) # YOLO类别id必须从0连续编号 cat2yolo {} for new_id, cat in enumerate(coco[categories]): cat2yolo[cat[id]] new_id # 建立 image_id - image信息 的映射 img_map {img[id]: img for img in coco[images]} # 按图片维度聚合标注 ann_map {} for ann in coco[annotations]: ann_map.setdefault(ann[image_id], []).append(ann) for img_id, img in img_map.items(): lines [] for ann in ann_map.get(img_id, []): x, y, w, h ann[bbox] # COCO格式左上角宽高 cx x w / 2.0 cy y h / 2.0 nx cx / img[width] ny cy / img[height] nw w / img[width] nh h / img[height] yolo_class cat2yolo[ann[category_id]] lines.append(f{yolo_class} {nx:.6f} {ny:.6f} {nw:.6f} {nh:.6f}) if lines: txt_name os.path.splitext(img[file_name])[0] .txt with open(os.path.join(out_dir, txt_name), w) as f: f.write(\n.join(lines)) coco_to_yolo(annotations/instances_all.json, yolo_labels)这个脚本做完后把生成的txt文件和对应图片放同一个目录再写一个data.yaml声明类别名和路径就能直接跑yolov8 train命令这就是训练YOLOv8自定义数据集的标准喂法。坐标系转换细节都写在注释里COCO的x,y是左上角YOLO要的是中心点所以先加半宽半高再除以图片尺寸归一化。img[width]和img[height]取值来自json里的宽度字段如果json的尺寸和实际图片不一致转换后坐标就全偏了这个坑在第5章具体展开。4. 用1765张片子跑通一个三分类基线迁移学习参数与训练细节4.1 微调ResNet18的最小训练脚本单卡也能跑完1765张图片对深度学习来说是很小的数据集从头训练一个卷积网络基本不可能收敛到有用的效果。标准做法是加载ImageNet预训练权重做迁移学习把最后一层全连接换成3类输出。ResNet18在这个规模下是性价比比较高的选择单张消费级显卡几分钟就能跑一个epoch调试成本低。下面是一个能直接改着用的最小训练脚本。import torch import torch.nn as nn from torchvision import models, transforms from torch.utils.data import DataLoader # 使用ImageNet预训练权重 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, 3) # 三分类新冠/正常/肺炎 transform transforms.Compose([ transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees5), transforms.ColorJitter(brightness0.1, contrast0.1), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_dataset CXRDataset(annotations/train.json, images, modecls, size224, transformtransform) val_dataset CXRDataset(annotations/val.json, images, modecls, size224, transformtransforms.Compose([transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225])])) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-4) criterion nn.CrossEntropyLoss(weightclass_weights.to(device)) for epoch in range(30): model.train() for x, y in train_loader: x, y x.to(device), y.to(device) optimizer.zero_grad() logits model(x) loss criterion(logits, y) loss.backward() optimizer.step() # 每个epoch后在验证集上评估一次 model.eval() val_loss, correct, total 0.0, 0, 0 with torch.no_grad(): for x, y in val_loader: x, y x.to(device), y.to(device) logits model(x) val_loss criterion(logits, y).item() * x.size(0) correct (logits.argmax(dim1) y).sum().item() total y.size(0) print(fepoch{epoch} val_loss{val_loss/total:.4f} val_acc{correct/total:.4f})这个脚本里几个参数是相对稳妥的起点数据增强用了水平翻转和5度以内的随机旋转X光片的肺部结构是左右大致对称的水平翻转可以安全使用但旋转角度不能太大否则会把纵隔和横膈膜的位置关系破坏掉ColorJitter的亮度和对比度扰动对X光片是合理的因为不同设备拍出来的片子灰度范围本身就有差异。训练部分用AdamW配合3e-4的学习率是个迁移学习里很少翻车的组合。4.2 三个必须调对的参数类别权重、学习率、输入尺寸第一个参数是类别权重。JSON统计出来三个类别数量大概率不是均匀的尤其是肺炎或新冠类样本可能明显偏少。常见做法是手动计算每个类别的样本数反比作为权重或者直接用sklearn的compute_class_weight函数。这个参数直接影响损失函数对少数类的惩罚力度不设置的话整个模型会倾向把所有样本预测成多数类验证集准确率可能虚高到80%以上但新冠类召回率几乎为0。from sklearn.utils.class_weight import compute_class_weight import numpy as np # labels是训练集所有样本的标签列表 classes np.array([0, 1, 2]) weights compute_class_weight(balanced, classesclasses, ynp.array(labels)) class_weights torch.tensor(weights, dtypetorch.float32)第二个是学习率。预训练模型的底层特征已经比较通用刚开始不适合用太大的学习率猛冲建议从3e-4开始。如果发现训练损失前几个epoch降得很慢再上调到1e-3如果损失震荡不收敛降到1e-4重来。很多从分类任务转过来的同学习惯用SGD加1e-3在小数据集上也能跑但AdamW省去手动调整动量的麻烦调试成本更低。第三个是输入尺寸。X胸透光片的原始分辨率常见的有512x512、1024x1024缩到224会丢失大量纹理细节。但ResNet18在224输入下感受野能覆盖到肺部整体结构对“弥漫性阴影”这类大尺度病灶特征已经够用。我的经验是如果验证集结果接近但差一口气把size改成320或384重训一次往往有可感知的提升代价是显存占用和训练时长大致翻倍。不要一开始就用太高分辨率先把pipeline跑通再优化。这三个参数是互相牵连的改了输入尺寸可能要同步改batch size改了类别权重要重新看验证集表现调参时需要整体权衡。4.3 早停与模型保存按验证损失留检查点别只看训练曲线在1765张图这种规模下过拟合发生得很快可能第15个epoch训练准确率就逼近99%但验证集准确率反而在往下掉。所以训练脚本里必须在每个epoch记录验证集损失并且只保存验证损失最低的那个检查点。只保存最后一个epoch的模型是小数据集训练中最常见的失误之一等你想回去找中间那个泛化最好的权重时已经找不回来了。best_val_loss float(inf) patience 0 for epoch in range(30): # ... 训练和验证代码 ... if val_loss best_val_loss: best_val_loss val_loss torch.save({ epoch: epoch, model_state_dict: model.state_dict(), val_loss: val_loss, }, best_cxr_model.pt) patience 0 else: patience 1 if patience 8: print(fearly stop at epoch {epoch}) break这里做了两件事一是只保存验证损失更低的权重二是当验证损失连续8个epoch没有刷新时提前终止训练。模型保存用dict结构把epoch和val_loss一起存下来方便事后溯源。load回来推理时要记得先构建一个同样结构的模型实例再load_state_dict如果发现key对应不上检查是否保存了整个模型而没有只保存state_dict。这些看起来琐碎但实际跑项目时一个保存路径写错导致半天训练白费的情况并不少见这类细节积累起来就是一套靠谱的工作流。5. 避坑X光片数据集训练常见的5个翻车现场5.1 现象训练集98%、验证集70% —— 小数据量直接全量微调必过拟合这个现象几乎是每个用1765张图训练的人都必经的一段。原因很明确模型容量相对于数据量来说太大了ImageNet预训练权重可以快速拟合训练集但完全没有泛化能力。解决办法是分阶段训练先冻结backbone只训练最后的分类头等分类损失降到一定程度再解冻全部层并用更低的学习率微调同时叠加更强的正则化比如在分类头前加Dropout把weight_decay从1e-4调到1e-3。还有一种常见做法是把增强强度加上去比如引入cutout或random erasing。但X光片不是自然图像过强的人工遮挡可能把肺野关键纹理遮住我一般用亮度对比度扰动和轻微旋转就够了cutout这类要小剂量试。5.2 现象验证集比训练集还高0.5个点 —— 划分时没按病人ID隔离同患者影像泄漏我第一次跑这个数据集时就遇到过这个怪现象一开始还以为是模型特别好后来抽了一张验证集图片发现训练集里有同一病人的另一张片子。X光片数据集的规范做法是按病人ID划分训练验证集一个病人的所有影像只能出现在一边。如果打包者文件名里带了患者编号要先提取出来做group级别的划分。import re def extract_patient_id(filename): # 常见格式如 covid_001_01.png取中间的数字段作为病人id parts re.split(r[_\-.], filename) for p in parts: if p.isdigit() and len(p) 3: return p return None patient_ids [extract_patient_id(os.path.basename(p)) for p in paths]用group划分后验证集的分数通常会降几个点这才是真实水平。数据泄漏在医疗影像里是非常严重的问题直接影响论文和产品的可信度所以拿到数据集第一步就应该判断文件名里有没有可提取的病患标识。5.3 现象模型永远不预测“新冠肺炎”那一类 —— 类别不平衡和稀疏标注的双重干扰在不少类别的X光片数据集中新冠样本本身就是少数派假设1765张里只有200多张新冠片直接用普通交叉熵训练模型学到的最优策略就是永远预测“正常”或“肺炎”这两类。单纯加类别权重有时候不够因为少数类的特征在深层特征空间里本身就被多数类压制了。我的处理顺序是先用分层划分保证验证集里三类比例合理再训练时用WeightedRandomSampler让每个batch里三类数量相对均衡最后在验证时重点看新冠类的召回率。如果加了权重后训练集准确率下降是正常的不要慌关键是验证集的各类别指标是否均衡了。5.4 现象COCO转成YOLO后标注框全部错位 —— 坐标归一化与读图通道的坑转成YOLO格式后可视化发现框全部偏到左上角或右边绝大多数原因是归一化时用的图片宽高和实际图片宽高不一致。COCO的json里存的width和height是标注时使用的尺寸有些图片实际被二次压缩过但json没有同步更新。另一个容易错的地方是把COCO的xywh直接当中心点坐标用了要先加半宽半高再归一化。还有一个隐蔽坑用OpenCV读图片再转PIL时通道顺序从BGR变RGB虽然检测框不受影响但如果你同时做图像预处理模型输入的颜色分布就变了可能让训练出来的模型在推理时掉点。转格式后用可视化脚本画框抽查20张图是必须的验证步骤。5.5 现象“正常”类里混着老病灶 —— X光片分类的一个标注边界问题X光片的“正常”定义在实际标注中很容易出现分歧。有的标注者把只有陈旧性钙化灶、小结节的片子归为“正常”有的归为“肺炎”因为这类片子没有急性感染征象但确实有影像异常。边界案例的存在直接拉低了分类任务的准确率上限。处理办法是先做一次数据清洗把训练集里拿不准的片子抽出来人工复查规模不大时甚至可以全量过一遍。如果确认类别边界确实模糊比较务实的做法是只保留置信度高的干净样本训练类别的定义松散会导致模型在真实场景里对边界样本的输出不稳定。这种清洗工作很费时间但X光片数据集本身就是小样本场景数据质量对最终模型的影响远超模型结构的选择。6. 验证模型而不是信任准确率用混淆矩阵和Grad-CAM检查三分类结果6.1 用混淆矩阵检查漏报方向医疗场景里宁可多报疑似训练结束看到85%的验证集准确率还远远不够。医疗影像识别中漏掉新冠的代价远高于把正常误判为肺炎所以评估时必须看三类各自的查全率和查准率混淆矩阵比单一准确率能提供更多信息。from sklearn.metrics import confusion_matrix, classification_report # y_true: 真实标签列表, y_pred: 模型预测标签列表 cm confusion_matrix(y_true, y_pred) print(混淆矩阵:\n, cm) print(classification_report(y_true, y_pred, target_names[covid, normal, pneumonia]))看混淆矩阵时我习惯先看对角线之外最严重的错误方向。如果“新冠真实样本”被大量预测成“正常”说明模型对新冠特征的敏感度不够需要调整类别权重或阈值如果“正常”被误判成“肺炎”可能说明“正常”数据里混入了边界病例。实际部署时可以把模型输出的概率存成CSV手动调节判断阈值比如将预测为新冠的阈值从0.5降到0.3牺牲一部分精度换取更高的召回率。6.2 用Grad-CAM确认模型在看肺野而不是在看边框和字母准确率达标也只是第一步。X光片里往往带着设备品牌水印、病人信息遮罩、甚至扫描仪的边框标记模型很可能学到的是这些东西而不是肺部的病理特征。用Grad-CAM把模型关注的区域可视化叠回到原图上如果高亮区域落在图像角落或边框上说明模型找到了一个数据集的捷径特征换个来源的数据就完全失效。实际操作时把ResNet18最后一个卷积层作为目标层计算类别得分对该层输出梯度的平均作为权重对特征图加权求和再经过ReLU得到一个与输入同尺寸的注意力热力图缩放到0到255后和原图做alpha叠加。观察标准只有一个高亮区域是否主要落在肺野内部。如果热力图集中在心脏轮廓或纵隔区域需要怀疑模型是否在依赖软组织对比度差异而忽略了肺野纹理。这个检查我建议在模型调优阶段每个epoch结束做一次比盯着loss曲线有价值得多。当年我第一次跑通这类数据集时也是只看准确率后来可视化才发现模型在看图片角落的字母水印。把验证做扎实再谈上线比多训20个epoch值钱得多希望帮到你。本文还有配套的精品资源点击获取
返回列表