简介:本资源为甲状腺结节图像分类数据集,面向医学影像分析、计算机视觉与深度学习方向的研究者及工程师,用于训练和评估正常与结节两类的自动识别模型。压缩包共2000个文件,以1998张jpg图像为主体,另含1个json标注文件和1个py可视化脚本,整体约324.26MB,已按训练集与测试集划分,各类别图片分目录存放,便于直接接入分类网络。运行包内show脚本可快速预览样本分布与图像质量,json文件则给出类别定义与标注对应关系。资源同时附有图像分类网络改进与计算机视觉完整项目的延伸链接,方便读者在数据预处理、模型设计、训练测试到部署的全流程中参考优化思路。目前已有372人学习下载,适合作为医疗影像分类任务的练手与对比基准。
1. 甲状腺结节图像分类数据集:7000 张已标注数据能跑出什么结果
手里有一份约 7,000 张、已完成标注的甲状腺结节图像分类数据集,这件事本身就值得认真对待。甲状腺超声图像在临床上属于高噪声、低对比度的典型场景,结节边界模糊、钙化点细小、不同设备成像差异大,公开可用的标注数据一直不算充裕。7,000 张这个量级,如果类别划分合理、标注质量过关,足够支撑一个从数据清洗到模型微调的完整闭环,也足够让一个团队在两周内判断出「这条路值不值得继续投入」。
这份数据能解决的问题很具体:结节良恶性二分类、TI-RADS 分级辅助、或者更细的结节亚型区分。适合的人群也很明确——做医学影像算法落地的工程师、需要快速验证分类方案的研究者、以及想拿真实标注数据练手但不想从零标注的开发者。真正决定成败的不是模型选得多新,而是你有没有先把这批图像的标注结构、类别分布和图像质量摸清楚。下面按「先看懂数据、再跑通基线、最后避坑」的顺序展开。
2. 先摸清数据底细:7,000 张标注图像的类别分布与质量核查
拿到一份标注数据集,最忌讳的就是直接ImageFolder一把梭开始训练。甲状腺结节图像有几个天然特点:阳性样本往往集中在某些设备或某些采集批次,阴性样本里混着大量正常腺体组织,两者在灰度分布上可能高度重叠。不先做分布核查,训练出来的模型很可能只是学会了「分辨设备」而不是「分辨结节」。
2.1 目录结构与标注格式的三种常见形态
已标注的甲状腺结节分类数据集,落地时通常以三种形态出现,先确认你手里是哪一种,后面的读取代码完全不同。
| 形态 | 典型结构 | 标注载体 | 读取方式 |
|---|---|---|---|
| 文件夹分类 | train/benign/、train/malignant/ | 目录名即标签 | ImageFolder直接读 |
| CSV 索引 | 图像统一存放 +labels.csv | 文件名到标签的映射 | pandas 读表后自定义 Dataset |
| 多标签 JSON | 图像 +annotations.json | 含类别、位置、TI-RADS 等 | 解析 JSON 后按需取字段 |
我一般会先跑一段统计脚本,把类别数、每类样本量、图像尺寸分布一次性打出来。这一步花十分钟,能省掉后面几小时的返工。
import os from collections import Counter from PIL import Image root = "thyroid_dataset" split_info = {} for split in ["train", "val", "test"]: split_dir = os.path.join(root, split) if not os.path.isdir(split_dir): continue counter = Counter() sizes = Counter() for cls in os.listdir(split_dir): cls_dir = os.path.join(split_dir, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): if not fname.lower().endswith((".png", ".jpg", ".jpeg", ".bmp")): continue counter[cls] += 1 with Image.open(os.path.join(cls_dir, fname)) as im: sizes[im.size] += 1 split_info[split] = {"counts": dict(counter), "sizes": dict(sizes)} for split, info in split_info.items(): print(split, "类别分布:", info["counts"]) print(split, "尺寸 Top5:", info["sizes"].most_common(5))这段脚本做三件事:遍历每个 split 下的类别目录、统计每类图像数量、统计图像原始尺寸分布。counter用来暴露类别不平衡,sizes用来判断是否需要统一 resize。如果发现某一类只有几百张而另一类有三千张,那后面必须上加权采样或 focal loss,否则模型会偏向多数类。尺寸分布如果集中在两三种分辨率,说明数据来源相对统一,预处理可以简化;如果尺寸五花八门,就要考虑是否保留长宽比做 padding。
2.2 标注质量核查:三类必须提前发现的脏数据
标注数据不等于干净数据。甲状腺结节图像里,我踩过最多的三类脏数据是:重复图像、标签矛盾、以及边界样本被硬塞进某一类。
重复图像用感知哈希(pHash)查最快。同一张图被不同文件名重复收录,会让训练集和验证集产生泄漏,验证指标虚高,上线就翻车。标签矛盾指的是同一张图或几乎相同的图在两个类别里都出现,这种必须人工裁决。边界样本则是那些良恶性特征都不明显的结节,标注者本身可能就犹豫,这类样本如果占比高,会显著拉低模型上限。
import imagehash from PIL import Image import os def find_duplicates(folder, hash_size=8, threshold=5): hashes = {} dup_pairs = [] for fname in os.listdir(folder): path = os.path.join(folder, fname) try: with Image.open(path) as im: h = imagehash.phash(im, hash_size=hash_size) except Exception: continue for exist_h, exist_name in hashes.items(): if h - exist_h <= threshold: dup_pairs.append((exist_name, fname, h - exist_h)) hashes[h] = fname return dup_pairs dups = find_duplicates("thyroid_dataset/train") print(f"发现疑似重复对: {len(dups)}") for a, b, dist in dups[:20]: print(a, "<->", b, "距离", dist)hash_size=8生成 64 位哈希,threshold=5表示汉明距离小于等于 5 视为疑似重复。阈值调小更严格,调大更宽松。实际用的时候我会把距离在 3 以内的直接判重,3 到 8 之间的拉出来人工看。这一步做完,通常能从 7,000 张里揪出几十到上百张重复或近重复图,对最终指标的诚实度影响很大。
注意:查重一定要在划分训练/验证集之前做,否则重复图会跨 split 泄漏,验证集准确率能虚高十几个点。
3. 从零跑通基线:用迁移学习在 7,000 张图上拿到可信指标
数据摸清之后,下一步是尽快跑出一个可信的基线。医学图像数据量普遍不大,7,000 张从头训一个 CNN 基本没戏,迁移学习是标准答案。选 backbone 的时候不要迷信最新,ResNet50 和 EfficientNet-B0 在这个量级上依然是性价比最高的起点,前者稳、后者省显存。
3.1 数据划分与增强策略:分层抽样加医学图像专用增强
划分不能随机切。甲状腺结节数据里良恶性比例往往不均,随机切会导致验证集类别分布抖动。用分层抽样(stratified split)保证每个 split 的类别比例一致,是基本操作。
import os import shutil import random from sklearn.model_selection import train_test_split random.seed(42) src = "thyroid_dataset/all" classes = ["benign", "malignant"] paths, labels = [], [] for idx, cls in enumerate(classes): cls_dir = os.path.join(src, cls) for fname in os.listdir(cls_dir): paths.append(os.path.join(cls_dir, fname)) labels.append(idx) train_p, temp_p, train_y, temp_y = train_test_split( paths, labels, test_size=0.3, stratify=labels, random_state=42 ) val_p, test_p, val_y, test_y = train_test_split( temp_p, temp_y, test_size=0.5, stratify=temp_y, random_state=42 ) def dump(items, targets, split): for p, y in zip(items, targets): dst = os.path.join("split", split, classes[y]) os.makedirs(dst, exist_ok=True) shutil.copy(p, dst) dump(train_p, train_y, "train") dump(val_p, val_y, "val") dump(test_p, test_y, "test") print(len(train_p), len(val_p), len(test_p))stratify=labels是关键参数,它保证切分后每类的比例和原始一致。test_size=0.3先切出 30% 做临时集,再从临时集里对半分成验证和测试,最终得到 70/15/15 的划分。random_state=42固定随机种子,保证可复现。切完之后打印三个集合的大小,确认没有某一类在验证集里只剩个位数。
增强策略上,甲状腺超声图像和自然图像差别很大。水平翻转、小角度旋转、亮度对比度微调是安全的;垂直翻转要谨慎,因为超声探头方向有解剖学含义;强烈的颜色抖动基本没用,因为图像本身是灰度的。我一般用 torchvision 的RandomHorizontalFlip、RandomRotation(10)、ColorJitter(brightness=0.1, contrast=0.1)三件套,够用且不容易引入伪影。
3.2 训练脚本与关键超参:batch size、学习率、冻结策略怎么定
基线训练用 PyTorch 写一个最小闭环。backbone 用 torchvision 自带的 ResNet50 预训练权重,替换最后的全连接层,前几轮先冻结卷积层只训分类头,再解冻做小学习率微调。
import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms, models device = torch.device("cuda" if torch.cuda.is_available() else "cpu") train_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ColorJitter(brightness=0.1, contrast=0.1), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) eval_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) train_ds = datasets.ImageFolder("split/train", transform=train_tf) val_ds = datasets.ImageFolder("split/val", transform=eval_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4) model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) for p in model.parameters(): p.requires_grad = False model.fc = nn.Linear(model.fc.in_features, 2) model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.AdamW(model.fc.parameters(), lr=1e-3, weight_decay=1e-4) for epoch in range(5): model.train() for x, y in train_loader: x, y = x.to(device), y.to(device) optimizer.zero_grad() loss = criterion(model(x), y) loss.backward() optimizer.step() print(f"epoch {epoch} done")batch_size=32在 224 分辨率、单张 12G 显存卡上比较稳,显存不够就降到 16 并同步把学习率减半。lr=1e-3是只训分类头时的常用值,解冻卷积层后要降到1e-4甚至1e-5,否则预训练权重会被快速破坏。weight_decay=1e-4抑制过拟合。冻结策略上,先冻结训 5 轮让分类头收敛,再解冻最后两个 stage 微调 10 到 15 轮,是医学图像上比较稳的节奏。
评估阶段不要只看 accuracy。甲状腺结节分类里,恶性样本漏检的代价远高于误检,所以 recall 和 AUC 比 accuracy 更值得盯。用sklearn.metrics的classification_report和roc_auc_score各跑一遍,把混淆矩阵打出来看假阴性有多少。
3.3 类别不平衡的处理:加权损失与采样器的取舍
如果第 2 章统计出来良恶性比例超过 2:1,就要处理不平衡。两条路:改损失函数,或者改采样方式。CrossEntropyLoss(weight=...)给少数类更高权重,实现简单;WeightedRandomSampler让少数类被采到的概率更高,效果通常更直接但会改变每个 epoch 的有效样本数。
from torch.utils.data import WeightedRandomSampler import numpy as np targets = [y for _, y in train_ds.samples] class_count = np.bincount(targets) class_weight = 1.0 / class_count sample_weight = [class_weight[t] for t in targets] sampler = WeightedRandomSampler(sample_weight, num_samples=len(sample_weight), replacement=True) train_loader = DataLoader(train_ds, batch_size=32, sampler=sampler, num_workers=4)class_weight取类别频次的倒数,replacement=True允许重复采样少数类。这样每个 epoch 里两类样本大致均衡。代价是少数类被反复采样,过拟合风险上升,所以配合早停和验证集监控一起用。我的习惯是:比例在 2:1 以内用加权损失,超过 3:1 才上采样器,两者不叠加,避免矫枉过正。
4. 避坑与排查:甲状腺结节分类训练里最容易翻车的五件事
这一章全是血泪经验。甲状腺结节图像分类看起来是个标准二分类任务,但医学图像的坑和自然图像完全不是一个量级。下面五条是我和身边同行反复踩过的,按「现象 → 原因 → 解决」写清楚。
4.1 验证集 AUC 0.95,测试集掉到 0.7
现象:训练时验证集 AUC 一路涨到 0.95 以上,换测试集一跑只有 0.7 出头,差距大得离谱。
原因:最常见的是数据泄漏。同一患者的多个切面图被分到了训练和验证集,模型记住了这个患者的纹理特征,验证集等于开卷考试。其次是重复图像没查干净,近重复图跨了 split。
解决:划分必须按患者 ID 分组,而不是按图像随机切。如果数据里没有患者 ID,就用感知哈希把近重复图聚成簇,整簇一起分到同一个 split。这一步做完,验证和测试的差距通常会收敛到 3 个点以内。
4.2 模型把图像边缘的标尺和文字当成了特征
现象:模型在内部测试集上表现很好,但换一批新设备采集的图就崩了,混淆矩阵显示大量误判。
原因:超声图像角落常有设备自带的标尺、参数文字、时间戳。这些内容在不同设备上完全不同,模型很容易学到「某类结节恰好都来自某台设备」这种伪相关。
解决:预处理阶段做边缘裁剪,把四周 5% 到 10% 的边框去掉,或者用固定 ROI 掩膜遮住角落区域。裁剪比例根据实际图像里标尺的位置定,宁可多裁一点也别让文字进模型。裁完再训练,跨设备泛化会明显改善。
4.3 学习率没降,解冻后模型直接发散
现象:冻结阶段 loss 稳步下降,一解冻卷积层,loss 突然飙升甚至变成 nan。
原因:解冻后所有参数一起更新,用的还是训分类头时的1e-3,预训练权重被大梯度冲垮。
解决:解冻时把学习率降到原来的十分之一到百分之一,并且用分层学习率——卷积层用小学习率,分类头用大学习率。PyTorch 里给optimizer传不同param_group即可。另外解冻后前两轮可以先用 warmup 把学习率从极小值线性升上来,稳定性更好。
4.4 图像灰度范围不统一,归一化参数用错
现象:训练 loss 震荡,收敛慢,同一份代码换个数据集就正常。
原因:不同来源的超声图像位深不一样,有的是 8 位灰度,有的是 16 位,直接ToTensor()后数值范围差异巨大。用 ImageNet 的均值方差去归一化灰度图,本身就不太匹配。
解决:先统计整个训练集的像素均值和标准差,用实际统计值做归一化,而不是照搬 ImageNet 的[0.485, 0.456, 0.406]。如果图像是 16 位,先线性映射到 0 到 255 再处理。这一步在医学图像上经常被忽略,但影响不小。
4.5 早停看 accuracy,错过了最佳 recall 点
现象:训练到后期 accuracy 还在微涨,但恶性类的 recall 已经开始下降,最终模型漏检偏多。
原因:早停指标选了 accuracy,而类别不平衡时 accuracy 会被多数类主导,少数类的表现被掩盖。
解决:早停和模型选择都改用验证集的 AUC 或恶性类 recall。保存 checkpoint 时按这个指标存最优,而不是按最后一个 epoch。临床上漏检一个恶性结节的代价,远高于多报一个良性,指标选择要跟业务代价对齐。
5. 把 7,000 张数据的价值榨干:难例挖掘与置信度校准的实操技巧
基线跑通、坑也避过之后,真正拉开差距的是怎么把这 7,000 张数据的价值用到极致。数据量固定,能提升的空间主要在难例利用和输出可信度上。这里讲两个我实际用过、收益明确的技巧。
第一个是难例挖掘(hard example mining)。做法是:用基线模型对训练集全量推理,把预测概率在 0.4 到 0.6 之间的样本挑出来,这批就是模型最犹豫的边界样本。对这批样本做两件事——人工复核标签,以及在下轮训练里给它们更高的采样权重。甲状腺结节里那些良恶性特征都不典型的样本,往往就藏在这个区间。我做过一次,从 7,000 张里挑出约 400 张高不确定性样本,复核后发现其中约 15% 的标签确实存疑,修正后重训,验证集 AUC 提升了约 2 个点。代码上就是在WeightedRandomSampler的sample_weight里给这批样本乘一个大于 1 的系数。
import torch import numpy as np model.eval() uncertain_idx = [] with torch.no_grad(): for i, (x, y) in enumerate(train_ds): x = x.unsqueeze(0).to(device) prob = torch.softmax(model(x), dim=1)[0, 1].item() if 0.4 <= prob <= 0.6: uncertain_idx.append(i) print(f"高不确定性样本数: {len(uncertain_idx)}") base_weight = [class_weight[t] for t in targets] for i in uncertain_idx: base_weight[i] *= 2.0 sampler = WeightedRandomSampler(base_weight, num_samples=len(base_weight), replacement=True)0.4到0.6这个区间可以按需调整,区间越窄挑出的样本越少但越难。base_weight[i] *= 2.0是给难例加权,倍数在 1.5 到 3 之间试,太高会让模型过拟合这几百张图。
第二个是置信度校准。医学场景里,模型输出「恶性概率 0.73」这种数字,如果没校准过,这个 0.73 是没有实际意义的。用温度缩放(temperature scaling)在验证集上拟合一个温度参数 T,把 softmax 输出重新标定,让预测概率和真实频率对齐。做法很简单:在验证集上以最小化 NLL 为目标优化一个标量 T,然后推理时把 logits 除以 T 再过 softmax。校准后,概率阈值卡在 0.5 时,模型说「恶性」的样本里实际恶性比例会更接近 50%,下游做阈值决策时心里有底。
| 技巧 | 输入 | 关键参数 | 预期收益 |
|---|---|---|---|
| 难例挖掘 | 基线模型 + 训练集 | 概率区间、加权倍数 | AUC +1~2 点 |
| 温度缩放 | 验证集 logits | 温度 T | 概率可信度提升 |
| 分层学习率 | 解冻阶段 | 卷积层 lr 比例 | 收敛更稳 |
最后说个我自己的习惯:每次拿到一份新的医学图像数据集,我都会先花半天只做数据核查,不碰模型。这半天里把类别分布、重复图、尺寸、灰度范围、患者分组全部过一遍,写成一份简短的核查记录。看起来慢,但后面每一次训练的可信度都建立在这份记录上。7,000 张标注数据不算多,但用对了,足够支撑一个能拿出手的分类方案。希望帮到你。
本文还有配套的精品资源,点击获取