尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

甲状腺结节图像分类实战:7000张标注数据从基线到避坑

发布时间:2026/9/30 1:34:30

资讯中心
01
ARTICLE

甲状腺结节图像分类实战:7000张标注数据从基线到避坑

甲状腺结节图像分类实战:7000张标注数据从基线到避坑
简介本资源为甲状腺结节图像分类数据集面向医学影像分析、计算机视觉与深度学习方向的研究者及工程师用于训练和评估正常与结节两类的自动识别模型。压缩包共2000个文件以1998张jpg图像为主体另含1个json标注文件和1个py可视化脚本整体约324.26MB训练集与测试集已按类别分目录存放便于直接接入分类网络。运行包内show脚本可快速浏览样本分布与图像质量json文件则给出两类标签的具体定义。资源同时附有图像分类网络改进与计算机视觉完整项目的延伸链接方便读者在数据预处理、模型设计、训练测试到部署的全流程中对照实践。目前已有372人学习适合希望快速验证分类算法或开展医学影像课题的中高级开发者。1. 甲状腺结节图像分类数据集7000张已标注数据能跑出什么结果手里有一份约 7,000 张已标注的甲状腺结节图像分类数据集第一反应不应该是“拿去训个 ResNet 看看准确率”而是先搞清楚这批数据的标注粒度、类别分布和图像来源。甲状腺超声图像分类在临床上对应的是 TI-RADS 分级辅助判断落到机器学习任务上通常是良恶性二分类或者更细的 46 类分级。7,000 张的量级不算大但在医学图像领域已经够做一轮完整的迁移学习实验。问题在于很多团队拿到数据直接切分训练集验证集跑出一个 95% 的准确率就收工结果换一台超声设备采集的图像上线就崩。这份数据集的真正价值不在于“有多少张”而在于标注是否一致、类别是否均衡、是否有患者级别的划分信息。适合谁用做医学图像入门实验的研究生、需要快速验证分类模型原型的算法工程师、以及想搭建甲状腺结节辅助筛查流程的产品团队。接下来的内容按“先看清数据、再跑通基线、最后避开翻车点”的顺序展开。2. 拿到 7000 张标注图先做三件事类别分布、图像尺寸、患者泄漏检查2.1 类别分布统计与不均衡处理决策医学图像数据集最常见的问题就是类别不均衡。良性结节在人群中的检出率远高于恶性如果这份 7,000 张的数据集也是按真实分布采集的良恶性比例可能达到 3:1 甚至更高。直接训练会导致模型倾向于预测多数类准确率看起来不错但召回率惨不忍睹。先跑一段统计脚本把每个类别的样本数、图像尺寸分布、文件格式全部拉出来import os import json from collections import Counter from PIL import Image DATA_ROOT thyroid_dataset # 假设目录结构为 DATA_ROOT/类别名/图片文件 class_counts {} size_dist Counter() corrupt_files [] for cls_name in sorted(os.listdir(DATA_ROOT)): cls_dir os.path.join(DATA_ROOT, cls_name) if not os.path.isdir(cls_dir): continue files [f for f in os.listdir(cls_dir) if f.lower().endswith((.png, .jpg, .jpeg, .bmp))] class_counts[cls_name] len(files) for fname in files: fpath os.path.join(cls_dir, fname) try: with Image.open(fpath) as im: size_dist[im.size] 1 except Exception as e: corrupt_files.append((fpath, str(e))) print(类别分布:, json.dumps(class_counts, ensure_asciiFalse, indent2)) print(图像尺寸 Top10:, size_dist.most_common(10)) print(损坏文件数:, len(corrupt_files))这段脚本的逻辑很直接遍历每个类别目录统计文件数量同时用 PIL 打开每张图记录尺寸顺便捕获损坏文件。参数方面DATA_ROOT换成你实际的挂载路径即可。如果类别目录名是中文os.listdir返回的也是中文不影响后续处理但建议在生成标签映射时统一转成英文或数字索引。跑完之后重点看两个信号第一最大类与最小类的比值。如果超过 5:1必须做重采样或损失加权。第二尺寸分布是否集中。如果大部分图像是 512×512 而少数是 1024×1024统一缩放到 256×256 或 320×320 是常规操作但要注意小结节在缩放后可能只剩几个像素这时候需要考虑裁剪 ROI 而不是整图缩放。提示类别不均衡的处理优先级是——先确认标注质量再做数据增强最后才考虑加权损失。如果少数类本身标注就有问题加权只会放大噪声。2.2 患者级别划分避免同一患者的图像同时出现在训练和验证集这是医学图像分类里最容易被忽视、后果最严重的坑。如果同一个患者的多个结节图像被随机切分到训练集和验证集模型实际上在“背”这个患者的图像特征验证准确率会虚高 1020 个百分点。7,000 张图如果来自 2,000 个患者平均每人 34 张随机切分的泄漏概率非常高。检查方法是看文件名或附带的元数据里有没有患者 ID。常见做法是文件名包含患者编号比如P00123_lesion1.png、P00123_lesion2.png。如果有按患者 ID 做 GroupShuffleSplitimport re import numpy as np from sklearn.model_selection import GroupShuffleSplit def extract_patient_id(filename): # 根据实际命名规则调整正则 match re.search(r(P\d), filename) return match.group(1) if match else filename all_files [] all_labels [] all_groups [] for cls_name in sorted(os.listdir(DATA_ROOT)): cls_dir os.path.join(DATA_ROOT, cls_name) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): if fname.lower().endswith((.png, .jpg, .jpeg, .bmp)): all_files.append(os.path.join(cls_dir, fname)) all_labels.append(cls_name) all_groups.append(extract_patient_id(fname)) gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(gss.split(all_files, all_labels, groupsall_groups)) print(f训练集: {len(train_idx)} 张, 验证集: {len(val_idx)} 张) print(f训练集患者数: {len(set(np.array(all_groups)[train_idx]))}) print(f验证集患者数: {len(set(np.array(all_groups)[val_idx]))})GroupShuffleSplit的核心参数是groups它保证同一组的样本只会出现在训练集或验证集其中之一。test_size0.2表示验证集占 20%。random_state固定后结果可复现。如果文件名里没有患者 ID那就只能退而求其次用图像感知哈希做近似去重但效果不如真正的患者级别划分。注意如果数据集提供方明确说了“已按患者划分”仍然建议自己验证一遍。我见过不止一次标注文档写的是患者级别划分实际文件命名根本区分不出来。3. 从零跑通甲状腺结节分类基线迁移学习、数据增强与训练配置3.1 为什么选 EfficientNet-B0 而不是 ResNet50 做基线医学图像数据集通常不大7,000 张图在 ImageNet 预训练模型面前属于小样本场景。ResNet50 参数量 2,500 万在这种量级上容易过拟合。EfficientNet-B0 参数量约 530 万在 ImageNet 上的精度与 ResNet50 相当但推理速度快 3 倍以上更适合做第一轮基线验证。另一个选择是 ConvNeXt-Tiny 或 Swin-Tiny但这两个架构对输入分辨率更敏感甲状腺超声图像本身分辨率参差不齐用 EfficientNet 系列容错率更高。如果后续要做模型集成可以把 ResNet50 和 EfficientNet-B0 的输出做加权平均但在基线阶段没必要。迁移学习的标准做法是冻结 backbone 前几层只训练分类头和最后几个 stage。具体冻结多少层取决于数据集大小。7,000 张图我一般会先冻结全部 backbone 训练 5 个 epoch 分类头再解冻最后两个 stage 微调 20 个 epoch。学习率方面分类头用 1e-3微调阶段用 1e-4。3.2 训练脚本与关键参数配置下面是一个基于 PyTorch 的完整训练脚本包含数据加载、增强、模型定义和训练循环import torch import torch.nn as nn import torchvision.transforms as T from torch.utils.data import Dataset, DataLoader from torchvision.models import efficientnet_b0, EfficientNet_B0_Weights from PIL import Image import os class ThyroidDataset(Dataset): def __init__(self, file_list, label_list, transformNone): self.file_list file_list self.label_list label_list self.transform transform # 构建类别到索引的映射 self.classes sorted(set(label_list)) self.class_to_idx {c: i for i, c in enumerate(self.classes)} def __len__(self): return len(self.file_list) def __getitem__(self, idx): img Image.open(self.file_list[idx]).convert(RGB) label self.class_to_idx[self.label_list[idx]] if self.transform: img self.transform(img) return img, label # 训练集增强超声图像慎用垂直翻转和大幅旋转 train_transform T.Compose([ T.Resize((256, 256)), T.RandomHorizontalFlip(p0.5), T.RandomRotation(degrees10), # 小角度旋转模拟探头轻微偏移 T.ColorJitter(brightness0.2, contrast0.2), # 模拟不同增益设置 T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_transform T.Compose([ T.Resize((256, 256)), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) # 构建 DataLoader train_dataset ThyroidDataset( [all_files[i] for i in train_idx], [all_labels[i] for i in train_idx], transformtrain_transform ) val_dataset ThyroidDataset( [all_files[i] for i in val_idx], [all_labels[i] for i in val_idx], transformval_transform ) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) # 模型定义 num_classes len(train_dataset.classes) model efficientnet_b0(weightsEfficientNet_B0_Weights.IMAGENET1K_V1) model.classifier[1] nn.Linear(model.classifier[1].in_features, num_classes) model model.cuda() # 第一阶段冻结 backbone只训练分类头 for param in model.features.parameters(): param.requires_grad False criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max25) def train_one_epoch(model, loader, criterion, optimizer): model.train() total_loss, correct, total 0, 0, 0 for imgs, labels in loader: imgs, labels imgs.cuda(), labels.cuda() optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * imgs.size(0) correct (outputs.argmax(1) labels).sum().item() total imgs.size(0) return total_loss / total, correct / total torch.no_grad() def evaluate(model, loader, criterion): model.eval() total_loss, correct, total 0, 0, 0 for imgs, labels in loader: imgs, labels imgs.cuda(), labels.cuda() outputs model(imgs) loss criterion(outputs, labels) total_loss loss.item() * imgs.size(0) correct (outputs.argmax(1) labels).sum().item() total imgs.size(0) return total_loss / total, correct / total # 训练循环 for epoch in range(25): if epoch 5: # 解冻最后两个 stage for param in model.features[-2:].parameters(): param.requires_grad True optimizer torch.optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max20) train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer) val_loss, val_acc evaluate(model, val_loader, criterion) scheduler.step() print(fEpoch {epoch1:02d} | Train Loss {train_loss:.4f} Acc {train_acc:.4f} | Val Loss {val_loss:.4f} Acc {val_acc:.4f})这段代码的关键设计点数据增强只用了水平翻转、±10° 旋转和轻微颜色抖动。超声图像和自然图像不同垂直翻转会改变解剖结构的上下关系大幅旋转会让结节形态失真所以增强策略要保守。ColorJitter模拟的是不同超声设备的增益和对比度差异这对提升泛化能力很有帮助。训练策略分两阶段前 5 个 epoch 冻结 backbone学习率 1e-3第 6 个 epoch 开始解冻最后两个 stage学习率降到 1e-4。这个策略在 7,000 张量级的数据集上通常能比端到端微调高出 23 个百分点。CosineAnnealingLR的T_max分别设为 25 和 20对应两个阶段的 epoch 数。提示如果验证集准确率在第 10 个 epoch 后不再提升不要急着加 epoch。先检查数据增强是否过强或者验证集是否存在标注噪声。3.3 评估指标为什么准确率不够用甲状腺结节分类的临床意义在于“不漏诊恶性”。如果模型把恶性预测为良性患者可能错过进一步穿刺检查。所以评估指标必须包含敏感度召回率和特异度而不是只看准确率。在验证集上计算混淆矩阵和各类指标from sklearn.metrics import classification_report, confusion_matrix torch.no_grad() def get_predictions(model, loader): model.eval() all_preds, all_labels [], [] for imgs, labels in loader: imgs imgs.cuda() outputs model(imgs) preds outputs.argmax(1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) return all_preds, all_labels preds, labels get_predictions(model, val_loader) print(confusion_matrix(labels, preds)) print(classification_report(labels, preds, target_namestrain_dataset.classes, digits4))classification_report会输出每个类别的精确率、召回率和 F1。重点关注恶性类的召回率如果低于 0.85需要调整分类阈值或增加恶性样本的权重。混淆矩阵能直观看到误判方向良性被判为恶性可以接受进一步检查恶性被判为良性必须尽量避免。4. 避坑与排查甲状腺结节分类数据集落地时的五个血泪教训4.1 现象验证集准确率 98%换一批数据掉到 60%原因患者级别泄漏。同一患者的图像同时出现在训练和验证集模型记住了患者特有的图像特征而非结节本身的病理特征。解决用 GroupShuffleSplit 按患者 ID 划分确保训练集和验证集的患者集合完全不相交。如果文件名没有患者 ID联系数据提供方补充或者用图像哈希做近似去重。4.2 现象模型训练 loss 正常下降但验证 loss 震荡剧烈原因数据增强过强或 batch size 太小。超声图像本身噪声大如果叠加了垂直翻转、大幅旋转、强颜色抖动增强后的图像分布和原始分布差异过大。解决把增强策略缩减到水平翻转 ±10° 旋转 轻微亮度对比度调整batch size 从 16 提到 32。4.3 现象某些类别的 F1 始终为 0原因类别极度不均衡少数类样本数太少模型直接学会了全部预测多数类。解决先确认少数类是否真的有足够样本至少 200 张以上。如果不够考虑合并类别比如把 TI-RADS 4a 和 4b 合并为“可疑恶性”或者用重采样 加权损失。CrossEntropyLoss的weight参数可以传入各类别的权重权重设为该类样本数的倒数。4.4 现象推理时单张图像预测结果与批量预测不一致原因推理时的预处理和训练时的验证预处理不一致。常见的是推理时忘了做 Normalize或者 Resize 的插值方法不同。解决把验证集的 transform 单独封装成一个函数推理时直接复用同一个函数不要重新写一遍。4.5 现象GPU 显存够但训练速度极慢原因num_workers设为 0 或者数据加载成为瓶颈。超声图像如果是 PNG 格式且分辨率较高解码耗时明显。解决num_workers设为 CPU 核心数的 1/4 到 1/2开启pin_memoryTrue。如果图像格式是 BMP考虑提前转成 JPEG 或 LMDB 格式加速读取。5. 把 7000 张数据的价值榨干进阶技巧与验证习惯基线跑通之后7,000 张数据的上限取决于你怎么用它。我一般会做三件事第一用五折交叉验证替代单次划分每折都按患者 ID 分组最终报告五折的平均敏感度和特异度而不是单次验证集的准确率。第二用 Test Time AugmentationTTA做推理增强对每张验证图像做水平翻转和 ±5° 旋转把多次预测的概率取平均通常能提升 12 个百分点的召回率。第三如果数据集中有 TI-RADS 分级标注不要只做良恶性二分类尝试序数回归ordinal regression把分级信息用起来模型输出的分级概率分布比二分类概率更有临床参考价值。验证习惯方面我坚持每轮实验都保存混淆矩阵和各类别的 PR 曲线而不是只看一个总体指标。甲状腺结节分类的临床底线是恶性召回率不低于 0.90在这个约束下再优化特异度。如果某次实验恶性召回率达标但特异度很低说明模型把太多良性判为恶性需要调整分类阈值或增加良性样本的多样性。最后一个习惯每次换模型架构或调整数据增强策略后固定用同一个验证集患者列表做对比。我见过太多实验因为验证集换了导致指标不可比白白浪费几周时间。把验证集的患者 ID 列表存成 JSON 文件每次实验前加载同一个列表这个习惯帮我省了无数次后悔药。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。