ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

1765张X胸透光片三分类数据集实战:从COCO标注到PyTorch训练全流程

1765张X胸透光片三分类数据集实战:从COCO标注到PyTorch训练全流程 简介这份资源面向医学影像分析、深度学习入门与计算机视觉方向的开发者及研究者提供一套可直接用于训练与验证的新冠肺炎X胸透光片检测数据集覆盖新冠肺炎、正常、肺炎三种状态适合分类模型训练、目标检测实验及教学演示等场景。压缩包共1770个文件其中1765张jpg胸透图像构成主体样本另含3个json标注文件与2个txt说明文件json采用COCO格式记录图像中病灶区域的位置与类别信息便于直接接入主流检测框架整体包体约61.41MB体积轻量下载与本地部署都较为方便。目前已有906人学习下载具备一定的使用参考基础。借助COCO格式标注读者可快速完成数据加载、类别映射与训练流程搭建并在此基础上开展模型对比、数据增强与迁移学习等实验为医学影像识别项目提供可复用的数据支撑。1. 1765 张 X 胸透光片的三分类数据集从拿到压缩包到跑通第一轮训练上周有个做医疗 AI 的朋友甩给我一个压缩包说里面是 1765 张 X 胸透光片用 COCO 格式标好了能区分新冠肺炎、正常、肺炎三种状态。我第一反应是「又是个玩具集」结果解压一看文件名全是COVID19-8-_jpg.rf.d1e42a6f...这种哈希串标注文件倒是规规矩矩的 COCO JSON。这类数据集在新冠高峰期之后其实沉淀了不少但真正能直接拿来跑通训练流程、标注质量还过得去的并不多。这份资源适合两类人一是想快速验证胸部 X 光三分类模型效果的算法工程师二是需要一份带标注的医学影像数据来做教学或课程设计的同学。它解决的核心问题就一个——省掉你自己从零标注 1765 张图的体力活直接进入模型调参和评估环节。下面我按实际拆包、转换、训练、排错的顺序把这份数据集怎么用、哪里容易翻车讲清楚。2. COCO 标注结构拆解1765 张图里到底存了什么2.1 COCO JSON 的字段含义与三分类映射拿到数据集先别急着写 DataLoader把标注文件打开看一眼结构。COCO 格式的核心就三个数组images、annotations、categories。这份数据集里categories应该只有三个类别对应新冠肺炎、正常、肺炎。但这里有个血泪经验——很多医学影像数据集虽然号称 COCO 格式实际上annotations里存的是整图级别的 bbox也就是每张图一个覆盖全图的框而不是病灶区域的精细标注。你得先确认这一点否则后面做目标检测还是分类会走错路。import json with open(annotations/instances_train.json, r, encodingutf-8) as f: coco json.load(f) # 看类别定义 for cat in coco[categories]: print(cat[id], cat[name]) # 看一张图的标注数量 img_id coco[images][0][id] anns [a for a in coco[annotations] if a[image_id] img_id] print(f图片 {img_id} 有 {len(anns)} 个标注) for a in anns: print(a[bbox], a[category_id])这段代码干的事很简单先打印类别映射再抽一张图看它的标注数量和 bbox 坐标。如果bbox是[0, 0, width, height]这种全图框说明这份数据本质上是分类数据集只是借了 COCO 的壳。参数上注意category_id不一定从 0 开始常见的是从 1 开始写映射字典的时候别想当然。2.2 图片文件名哈希化带来的路径问题文件名全是COVID19-数字-_jpg.rf.哈希.jpg这种格式好处是唯一性强不会重名坏处是你没法从文件名直接判断类别。类别信息完全藏在 JSON 的annotations里所以构建 Dataset 类的时候必须走「图片 ID → 文件名 → 类别」这条链路。我一般会先做一步预处理把 COCO 转成文件名,类别的 CSV后面无论是用 PyTorch 还是 TensorFlow 都省事。import os import csv img_dir images/train id2file {img[id]: img[file_name] for img in coco[images]} id2cat {} for ann in coco[annotations]: id2cat[ann[image_id]] ann[category_id] with open(train_labels.csv, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([filename, category_id]) for img_id, fname in id2file.items(): cat id2cat.get(img_id, -1) writer.writerow([fname, cat])逻辑说明id2file建立图片 ID 到文件名的映射id2cat建立图片 ID 到类别的映射最后合并写出 CSV。参数上cat id2cat.get(img_id, -1)里的-1是兜底值如果某张图没有标注就会被标成 -1后面训练前记得过滤掉。这一步做完你手里就有一份干净的「文件名-类别」对照表比每次去解析 JSON 快得多。2.3 训练集与验证集的划分策略1765 张图不算多三分类平摊下来每类五百多张。划分的时候千万别用随机划分就完事医学影像数据有个坑——同一患者的多次拍摄可能都在里面随机划分会导致训练集和验证集出现同一患者的片子验证指标虚高。常见做法是先按文件名里的数字前缀分组再做 GroupShuffleSplit。如果文件名没有患者 ID 信息那就退而求其次用分层抽样保证每类比例一致。from sklearn.model_selection import train_test_split import pandas as pd df pd.read_csv(train_labels.csv) df df[df[category_id] ! -1] train_df, val_df train_test_split( df, test_size0.2, stratifydf[category_id], random_state42 ) print(train_df[category_id].value_counts()) print(val_df[category_id].value_counts())stratifydf[category_id]保证训练集和验证集里三个类别的比例和原始数据一致random_state42固定随机种子方便复现。如果后面发现验证集准确率波动很大优先检查是不是划分没做好而不是急着换模型。3. 从 COCO 到训练张量PyTorch Dataset 与增强管道3.1 自定义 Dataset 类的三个关键方法PyTorch 的 Dataset 类必须实现__init__、__len__、__getitem__三个方法。这份数据集因为文件名哈希化__getitem__里要做一次 CSV 查询拿到类别标签。另外 X 光片是灰度图但很多预训练模型要求三通道输入所以要在 transform 里做复制通道的操作。import torch from torch.utils.data import Dataset from PIL import Image import pandas as pd class ChestXrayDataset(Dataset): def __init__(self, csv_file, img_dir, transformNone): self.df pd.read_csv(csv_file) self.df self.df[self.df[category_id] ! -1].reset_index(dropTrue) self.img_dir img_dir self.transform transform def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img_path os.path.join(self.img_dir, row[filename]) image Image.open(img_path).convert(RGB) label int(row[category_id]) - 1 # 类别 ID 从 1 开始转成 0 起始 if self.transform: image self.transform(image) return image, label逻辑说明__init__里读 CSV 并过滤无效标注__getitem__里拼路径、开图、转 RGB、标签减一。参数上label int(row[category_id]) - 1这行很关键因为 COCO 的category_id通常从 1 开始而 PyTorch 的 CrossEntropyLoss 要求标签从 0 开始不减这个 1 训练时不会报错但结果全错属于典型的玄学翻车点。3.2 医学影像增强的边界哪些增强能用哪些不能用X 光片的增强和自然图像不一样。水平翻转通常可以因为左右肺对称但垂直翻转要谨慎倒过来的胸片在解剖上不成立。颜色抖动基本没用因为原图就是灰度的。我一般用 RandRotate 小角度、RandomResizedCrop 配合适的缩放范围、再加一点亮度对比度扰动。from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees10), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_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]) ])RandomRotation(degrees10)限制在正负 10 度再大就可能把肺尖切掉。ColorJitter的 brightness 和 contrast 都设 0.2再大就失真了。Normalize 用的是 ImageNet 的均值和方差因为后面要加载预训练权重。验证集只做 Resize 和 Normalize不做任何随机增强保证评估结果可复现。3.3 类别不平衡与 WeightedRandomSampler三分类数据里如果某一类明显偏少训练时模型会偏向多数类。先统计一下每类数量如果最大类和最小类差距超过 2 倍就上 WeightedRandomSampler。这个采样器让每个 batch 里各类别出现的概率大致均衡。from torch.utils.data import WeightedRandomSampler import numpy as np labels train_df[category_id].values - 1 class_counts np.bincount(labels) class_weights 1.0 / class_counts sample_weights class_weights[labels] sampler WeightedRandomSampler( weightstorch.DoubleTensor(sample_weights), num_sampleslen(sample_weights), replacementTrue )class_weights 1.0 / class_counts给少数类更高的权重sample_weights把每个样本的权重映射出来replacementTrue表示有放回采样。用这个 sampler 的时候 DataLoader 的shuffle要设成 False否则会冲突。4. 训练配置与评估让三分类模型真正跑起来4.1 迁移学习选型ResNet18 还是 EfficientNet1765 张图属于小样本场景从零训练肯定过拟合。常见做法是加载 ImageNet 预训练权重冻结 backbone 先训分类头再解冻全部微调。ResNet18 参数量小、训练快适合快速验证EfficientNet-B0 精度通常高两三个点但训练慢一些。我一般先用 ResNet18 跑通流程确认数据管道没问题再换大模型。import torchvision.models as models import torch.nn as nn model models.resnet18(pretrainedTrue) num_features model.fc.in_features model.fc nn.Linear(num_features, 3) # 三分类 # 先冻结 backbone for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad TruepretrainedTrue加载 ImageNet 权重model.fc替换成输出 3 类的全连接层。冻结 backbone 后只训练fc层学习率可以设大一点比如 1e-3。训几个 epoch 后解冻全部学习率降到 1e-4 做微调。4.2 训练循环里的三个监控指标训练时别只看 loss要同时盯准确率、各类别召回率和混淆矩阵。医学影像里「把新冠判成正常」和「把正常判成新冠」的代价完全不同前者是漏诊后者是误诊。所以评估时要把混淆矩阵打出来看模型在哪个类别上容易混淆。from sklearn.metrics import confusion_matrix, classification_report def evaluate(model, loader, device): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for images, labels in loader: images images.to(device) outputs model(images) preds outputs.argmax(dim1).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[COVID19, Normal, Pneumonia]))model.eval()关掉 dropout 和 batchnorm 的训练模式torch.no_grad()省显存。classification_report会输出每个类别的 precision、recall、f1-score重点看 COVID19 那一列的 recall漏诊率高的话要调整类别权重或采样策略。4.3 学习率调度与早停小数据集训练容易在验证 loss 上震荡用 ReduceLROnPlateau 在验证 loss 不下降时自动降学习率配合早停防止过拟合。耐心值设 3 到 5 个 epoch 比较合适。scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience3 ) best_loss float(inf) patience_counter 0 for epoch in range(50): train_loss train_one_epoch(model, train_loader, optimizer, device) val_loss, val_acc validate(model, val_loader, device) scheduler.step(val_loss) if val_loss best_loss: best_loss val_loss torch.save(model.state_dict(), best_model.pth) patience_counter 0 else: patience_counter 1 if patience_counter 7: print(fEarly stop at epoch {epoch}) breakfactor0.5表示每次降一半学习率patience3是连续 3 个 epoch 验证 loss 不降才触发。早停的耐心值设 7 比调度器的 3 大保证学习率降了之后还有机会再观察几个 epoch。5. 避坑与排查这份数据集最容易翻车的五个地方5.1 现象训练 loss 正常下降但验证准确率始终在 33% 左右原因标签映射错了。COCO 的category_id从 1 开始如果 Dataset 里没做减一操作模型学到的标签是 1、2、3而 CrossEntropyLoss 期望 0、1、2导致所有样本的标签都偏移了一位模型实际上在学一个错位的分类边界。解决在 Dataset 的__getitem__里强制label int(row[category_id]) - 1并且训练前打印一个 batch 的标签确认范围是 0 到 2。5.2 现象验证集准确率比训练集还高原因数据泄漏。同一患者的 X 光片同时出现在训练集和验证集里模型在训练时已经「见过」验证集的图。这份数据集文件名是哈希化的没法直接按患者分组但可以检查是否有文件名数字前缀相同的图片被分到了不同集合。解决如果发现泄漏改用 GroupShuffleSplit 按文件名前缀分组划分或者接受这个现实并在报告里说明验证指标偏乐观。5.3 现象训练到一半 loss 突然变成 NaN原因学习率太大或者数据里有损坏图片。X 光片数据集里偶尔会有全黑或全白的异常图归一化后产生极端值导致梯度爆炸。解决先用脚本遍历所有图片检查均值和方差过滤掉均值接近 0 或 255 的图。学习率从 1e-4 开始试配合梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)。5.4 现象模型把大部分样本预测成同一类原因类别不平衡加上没有用加权采样。如果正常类占了 60% 以上模型直接全预测正常就能拿到 60% 准确率loss 也会降得很快但新冠和肺炎的召回率接近零。解决上 WeightedRandomSampler 或者在 loss 里设weight参数训练后看混淆矩阵确认每个类别都有被预测到。5.5 现象验证集图片加载报 FileNotFoundError原因COCO JSON 里的file_name和实际磁盘上的文件名不一致。哈希化文件名容易在解压或复制过程中被截断或改名尤其是跨平台操作时。解决写一个校验脚本遍历 JSON 里的所有file_name逐个检查文件是否存在缺失的打印出来人工核对。别等到训练循环跑到一半才报错。6. 进阶技巧用混淆矩阵反推数据质量与模型改进方向跑完第一轮训练后别急着调参先把混淆矩阵仔细看一遍。我一般会把混淆矩阵归一化后画出来重点看非对角线上的数值。如果 COVID19 和 Pneumonia 之间的混淆特别严重说明这两类的影像特征在模型看来太接近可能需要更精细的预处理或者换更高分辨率的输入。1765 张图里如果某些类别的图片质量参差不齐比如有的曝光过度有的曝光不足可以按图片亮度做一次聚类看看是不是某个亮度区间的样本特别容易错。另一个实用技巧是用 Grad-CAM 可视化模型关注的区域。如果模型盯着肺外的区域做判断比如肩膀或者文字标记说明数据里有捷径特征这时候要么裁剪掉无关区域要么在增强里加随机裁剪强制模型看肺。我习惯在验证集上抽 20 张错分样本做 Grad-CAM如果热力图集中在肺部说明模型学到了合理特征如果散在图像边缘就得回头查数据。最后说一个我踩过的坑这份数据集只有 1765 张做三分类可以但别指望它能训出临床可用的模型。它的定位是教学、原型验证和算法对比的基准集。我一般会用它快速验证一个新架构或新增强策略有没有效果确认方向对了再上更大的私有数据。从那以后我每次拿到新数据集都强制先跑一遍「标签范围检查 → 文件完整性校验 → 混淆矩阵基线」这三步省得后面调半天参发现是数据本身的问题。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进