☰
基于COCO格式X光片的肺炎识别:迁移学习与目标检测实战
2026/10/1 3:06:57 网站建设 项目流程

简介:这份新冠肺炎检测数据集以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或jpg
annotations/一个或几个COCO格式的json标注文件
README.md 或 data_description.txt类别定义、标签来源、数据划分说明
train.txt / val.txt部分打包者会附带划分好的文件列表

如果只有一个总标注json,说明需要自己做训练集和验证集划分;如果已经拆成train.json和val.json,可以直接跳到第4章。另外一定用脚本数一下图片文件数量和json里images数组的长度是否相等。常见做法是把不完整的数据和标注文件混在一起,数量对不上时先排查,不要带着脏数据往下走。

2.2 COCO标注里的三个核心数组:images、annotations、categories

COCO格式本质上是一整套以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", encoding="utf-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, mode="cls", size=224, transform=None): super().__init__() with open(coco_path, "r", encoding="utf-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, dtype=torch.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_size=0.2, random_state=42, stratify=labels )

stratify=labels保证训练集和验证集里三个类别的比例和原始数据集一致。这个细节很关键,如果直接随机切分且原始数据里新冠样本只占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_ok=True) with open(coco_path, "r", encoding="utf-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(weights=models.ResNet18_Weights.IMAGENET1K_V1) model.fc = nn.Linear(model.fc.in_features, 3) # 三分类:新冠/正常/肺炎 transform = transforms.Compose([ transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(degrees=5), transforms.ColorJitter(brightness=0.1, contrast=0.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", mode="cls", size=224, transform=transform) val_dataset = CXRDataset("annotations/val.json", "images", mode="cls", size=224, transform=transforms.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_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4) optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=1e-4) criterion = nn.CrossEntropyLoss(weight=class_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(dim=1) == y).sum().item() total += y.size(0) print(f"epoch={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", classes=classes, y=np.array(labels)) class_weights = torch.tensor(weights, dtype=torch.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(f"early 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值钱得多,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询