简介面向医学图像分类任务此数据集涵盖23种常见皮肤病类别包括湿疹、肿瘤、真菌感染等典型病种并预先划分训练集与测试集图片按类别文件夹存放可直接用PyTorch的ImageFolder读取省去额外预处理环节。数据总量约967MB压缩包共2000个文件以1998张JPEG图像为主体另附类别字典JSON文件与可视化展示Python脚本脚本无需修改即可随机抽取4张图片生成预览图便于快速核查样本与标注质量。目前已有531人学习适合深度学习初学者、医学影像研究者以及YOLOv5等图像分类任务实践者使用。训练集包含15557张图片、测试集包含4002张图片类别与文件名一一对应配合JSON字典可快速映射标签读者拿到后可直接训练、验证与评估无须自行整理目录结构是一套开箱即用的标准分类数据集。1. 这个“分类数据集”是起点但用错方向训练完才发现 23 类白训了很多做医学图像分类的人第一次拿到“23种皮肤病分类数据集包含训练集、验证集”时第一反应是解压、看文件夹、数图片然后赶紧丢进 ResNet 开训。这个数据集最省事的地方是它连训练集、验证集都划好了你不需要自己纠结按 7:3 还是 8:2 切分也不用担心随机划分把同一病人的图像同时丢进两个集合。但它真正的坑不在数据量而在“23 类”到底按什么口径分是按临床诊断类别分还是按病灶形态分。口径没搞清楚就调参后面每个 epoch 都在为错误的目标付时间。这篇文章把拿到数据集后从目录重组、类别映射、迁移学习训练到验证集评估的完整链路讲清楚顺带给你几条血泪经验。2. 先认清 23 类的真实口径标签划分决定你要训的是分类器还是认图器2.1 按病因诊断分还是按形态学特征分先花十分钟搞懂标签口径皮肤科图像分类和 ImageNet 那种“猫 vs 狗”的天然语义分类不一样同一张皮损照片皮肤科医生可能同时给出“银屑病样皮炎”和“慢性湿疹样改变”两种描述因为肉眼和组织病理经常对不上。所以拿到 23 种皮肤病的训练集之后我做的第一件事不是写代码而是把所有类别名打印出来逐个判断它的标签粒度。如果你的数据集中既有“痤疮”“湿疹”“银屑病”这类临床诊断又有“色素沉着”“红斑鳞屑”这类形态描述说明它并不是一个纯粹的“判别式分类问题”更像一个“多标签多语义”的混合标注。这种情况下硬套 23 类互斥分类器softmax 输出会在验证集上出现一个很奇怪的现象单类准确率很高但整体准确率被混淆对拖垮。常见做法是保持作者给的类别划分不动先训一个 23 类互斥分类器当基线再用混淆矩阵找出高频错对判断是否需要把某些类别合并成上级语义类。另外一个容易忽略的点是标签文件的后缀格式。CSV 格式的标签文件最常见但有的版本里标签不是“文件名 类别”两列而是“文件名 编号 类别全名 细分位置”。我习惯把原始标签先读出来看一眼再切成 DataFrame而不是直接当 csv 读死因为文件里可能混着 BOM 头、中文逗号或者 Windows 换行符这些东西在 Linux 容器里会直接导致第一个类别名错位。2.2 训练集、验证集划分到底省了你什么事按病人分还按图片分标题里明确了数据集包含训练集、验证集这其实是整个数据集最有价值的部分。常见的数据集只给一堆原始图像验证集划分得自己来而这个数据集把验证集独立出来了意味着你直接跳过 train_test_split 这一层。但这里有个隐藏问题划分的单位是图片还是病人。如果同一患者的多张皮损图同时出现在训练集和验证集里模型会通过背景、肤色、拍摄光线等非病灶特征轻松“认出来”导致验证集指标虚高。图片级划分和病人级划分的区别可以这样理解图片级划分验证的是“这张图有没有被记住”病人级划分验证的是“这个病能不能被泛化”。如果数据集目录里图片文件名带有患者编号比如 patient_001_lesion_002.jpg你可以很容易地按患者编号做分组校验。如果文件名只有随机哈希那只能用笨办法把全部文件名拉出来肉眼检查同一拍摄背景的图是否横跨 train 和 val 两个目录。这个检查很费时间但如果验证集指标好得出奇训练 acc 还不到 90%验证 acc 先冲到 95%十有八九是分桶时泄漏了。2.3 图像尺寸、颜色空间与类别不均衡进入训练前必须处理的三件事皮肤病理图像和自然图像最大的差别在于颜色敏感度高。同样的湿疹在偏黄光、偏白光和偏蓝光下拍出来RGB 直方图差异远大于“背景不同”带来的差异。所以训练前的预处理不建议用 ImageNet 那套直接 resize 到 224 就完事需要额外做两步一是统一图像的最长边与最短边比例二是做颜色增强而不是几何增强——旋转、翻转对皮肤病诊断没有物理意义但色相微调、亮度抖动能提升泛化。位数深度也要检查。部分皮肤镜图像是 16 位 PNG如果直接 PIL 打开默认转 8 位高光区域的信息会整体丢一层。训练前用脚本扫一遍图像模式RGB、L、RGBA和位深再决定是否统一转成 RGB 8 位。类别不均衡在皮肤病数据里几乎必然存在。23 类里良性痣、脂溢性角化这类常见病的样本数可能是罕见皮肤淋巴瘤的几十倍。如果直接按原始分布训模型会在验证集上表现出一种“看起来很准”的假象整体准确率 92%但对罕见类召回率是 0因为模型学会了把一切不确定的图都分到多数类。处理这个问题的顺序是先用原始分布跑一个基线再看验证集每一类的 recall而不是直接上手就用 WeightedRandomSampler 强行均衡。3. 把训练集、验证集组织成 PyTorch 能吃的目录结构从原始文件夹到 DataLoader3.1 按 ImageFolder 约定重组目录软链优先复制次之拿到数据集后目录结构往往不是标准 ImageFolder 格式。常见的情况是 train/ 和 val/ 目录下直接堆了几万张 jpg类别名写在 CSV 里偶尔是每个类别一个文件夹但类别名带中文或空格。第一步我建议统一成 ImageFolder 约定结构如下skin23/ ├── train/ │ ├── acne/ │ ├── eczema/ │ └── ... └── val/ ├── acne/ └── ...因为 ImageFolder 会按子目录字母序自动分配类别索引标签自动对齐不用手写映射表。如果原始目录是“一个大目录 CSV 标签”写个脚本按类别建目录并整理文件。处理大量文件时用软链而不是复制避免硬盘被两份数据占满。Linux 下ln -s软链每个文件的时间成本远低于cp而且推理阶段不占用双倍空间唯一的风险是原始目录被移动后软链全部失效所以软链路径要用绝对路径。# 以 CSV 标签文件 labels.csv 为例 # 假设 CSV 结构filename,class_name # 把图片从 flat/ 目录按类别软链到 train/ 对应子目录 mkdir -p skin23/train skin23/val python - EOF import csv from pathlib import Path data_root Path(flat_images) label_file Path(labels.csv) out_root Path(skin23) # 读取 CSV建立 文件名 - 类别 的映射 with open(label_file, encodingutf-8-sig) as f: rows list(csv.DictReader(f)) # 这里按常见的 train/val 比例切分如果你使用的是数据集自带的 # train/val 划分说明则按照说明里的清单来决定每个文件进 train 还是 val for i, row in enumerate(rows): fname row[filename] cls row[class_name] # 8:2 切分固定随机种子保证结果可复现 split train if i % 10 8 else val src data_root / fname dst_dir out_root / split / cls dst_dir.mkdir(parentsTrue, exist_okTrue) # 用绝对路径建软链避免相对路径失效 dst dst_dir / fname if not dst.exists(): dst.symlink_to(src.resolve()) EOF上面这个脚本的关键参数是encodingutf-8-sig它专门处理 Excel 导出的 CSV 带 BOM 头的问题——不去掉 BOM第一个类别名会被读成 “\ufeffacne”后续字典查找全部错位。另一个参数是固定的取模规则 8:2如果你不需要重新划分直接按数据集作者给的 train.txt/val.txt 文件清单来筛文件而不是再切一次。3.2 类别映射与载入别写死索引动态生成 label_mapImageFolder 会自动按字母序生成 class_to_idx这个映射会在训练和推理阶段反复使用。但这里有一个很隐蔽的坑ImageFolder 的索引与 CSV 里的类别顺序不一定一致。比如 CSV 里 acne 排在第 0 位但 ImageFolder 按字母序可能把“basal_cell_carcinoma”排到了第 0 位。如果训练脚本里写死了某个索引推理时就会张冠李戴。我的做法是从 ImageFolder 里反推出映射并保存成 JSON后续所有脚本都读这份映射不再用手写的字典。这样也方便之后加载模型做推理时输出索引直接查类别名。# make_label_map.py import json from pathlib import Path from torchvision.datasets import ImageFolder root Path(skin23) train_set ImageFolder(root / train) label_map train_set.class_to_idx print(类别数与索引:, label_map) # 保存映射训练和推理共用同一份 with open(label_map.json, w, encodingutf-8) as f: json.dump(label_map, f, indent2, ensure_asciiFalse) # 反向映射index - class_name inv_map {v: k for k, v in label_map.items()} with open(index_to_label.json, w, encodingutf-8) as f: json.dump(inv_map, f, indent2, ensure_asciiFalse)这里关键的一点是程序必须先做classes list(label_map.keys())让验证集的类别顺序与训练集严格一致。如果你在验证集上单独再调一次ImageFolder验证集的class_to_idx虽然和训练集按同一套字母序规则生成理论上一致但只要某个类别在验证集里一张图都没有索引序号就会整体错位。所以验证集和训练集应该共享同一个 label_map。3.3 自定义 Dataset 与样本均衡策略从路径列表出发而不是从目录出发当数据集已经按目录整理好后直接使用ImageFolder就能跑但如果你需要做图像级的数据清洗去掉全黑图、模糊图或者要做更强的数据增强管线从路径列表出发自定义 Dataset 更灵活。类似 YOLOv8 训练自己的数据集时要先准备 yaml 和 labelsUNet 训练自己的数据集时要先做 mask 配对这个 23 类皮肤分类数据集的载入也需要把“文件名列表”与“标签列表”配对。一个稳健的 Dataset 实现至少要返回图像和标签两个字段并且支持索引采样。下面这个实现考虑了三点一是用transform参数把预处理逻辑外置二是用__len__和__getitem__的标准接口方便后续接入任何训练框架三是返回文件名便于排查某个 loss 突变的批次到底来自哪张图。# dataset.py import cv2 import numpy as np from torch.utils.data import Dataset from pathlib import Path class Skin23Dataset(Dataset): def __init__(self, img_dir, label_map, transformNone, grayscale_prob0.0): self.img_dir Path(img_dir) self.label_map label_map self.transform transform self.grayscale_prob grayscale_prob # 偶发灰度增强的概率 self.samples [] # 动态遍历子目录构造 (路径, 标签索引) 样本对 for class_name, class_idx in self.label_map.items(): class_dir self.img_dir / class_name if not class_dir.exists(): continue for img_path in sorted(class_dir.glob(*.jpg)): self.samples.append((str(img_path), class_idx)) # 兼容 png、jpeg 后缀按需补充 for ext in (*.png, *.jpeg, *.bmp): for img_path in class_dir.glob(ext): self.samples.append((str(img_path), class_idx)) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, label self.samples[idx] # 用 cv2 读图是为了能拿到原图位深信息PIL 有时会吞掉16位图 img cv2.imread(img_path, cv2.IMREAD_COLOR) if img is None: # 空图直接返回同分布的新样本避免 batch 中出现 None return self.__getitem__((idx 1) % len(self.samples)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 小概率转灰度再转回三通道模拟灰度皮肤镜图像 if self.grayscale_prob 0 and np.random.random() self.grayscale_prob: gray cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) img cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB) if self.transform: img self.transform(img) return img, label这个 Dataset 有两点值得说明一是用glob按扩展名依次找全图像避免目录里混了.jpeg和.jpg两种后缀导致漏图二是img is None时的处理不是抛异常而是递归取下一个样本这样训练过程中单个坏图不会导致整个 epoch 崩溃。灰度增强的概率一般设 0.1 或 0.2它对皮肤镜图像有用对标准临床照片可能反而降低区分度需要根据验证集表现来调节。4. 用 ResNet-50 微调 23 类皮肤病分类器训练脚本与参数设定思路4.1 为什么从 ImageNet 预训练权重起步而不是从零训练23 类皮肤病数据集每类的样本量通常在几百到几千之间从零训练一个深度分类网络大概率会在验证集上出现过拟合。常见做法是加载 ImageNet 预训练权重把最后一层替换成 23 输出。这背后是一个迁移学习的常识ImageNet 预训练模型的前几层学到的是边缘、纹理、颜色块等基础特征这些特征在皮肤图像上同样有效需要重新学习的只是高层语义组合。但医学图像有一个特殊点需要警惕ImageNet 预训练模型对颜色分布并不是中性的。ImageNet 里大量图片是自然场景绿色和蓝色占比高皮肤病图像以红、棕、肤色为主。所以微调骨干网络时学习率不宜过小完全冻结 backbone 的做法通常效果不好。我一般用三层差异化学习率backbone 前几层用很小的学习率因为底层特征通用backbone 后几层用中等学习率因为颜色纹理特征开始偏医学域分类头用较大学习率因为是随机初始化的需要更快收敛。如果显存有限也可以先冻结全部 backbone只训练分类头跑几个 epoch 看验证集能否达到可用基线再解冻后几层做二次微调。4.2 训练脚本主体每个参数都值得解释下面这个脚本直接对应“拿到数据集后从零到验证集出指标”的最小闭环。脚本包含数据增强、混合精度训练、验证集评估和模型保存可以直接复制后根据显存大小调整 batch_size。# train_skin23.py import json import torch import torch.nn as nn from torch.utils.data import DataLoader, WeightedRandomSampler from torchvision import models, transforms from dataset import Skin23Dataset # ---------- 配置区 ---------- BATCH_SIZE 32 # RTX 3090 可跑 6416G 以下显存建议 16 或 32 EPOCHS 40 LR 3e-4 # 分类头学习率backbone 在其基础上乘 0.1 WEIGHT_DECAY 1e-4 NUM_WORKERS 4 DEVICE cuda if torch.cuda.is_available() else cpu IMG_SIZE 640 # 比 224 更适合皮肤病灶保留了更多纹理细节 # ---------- 加载标签映射 ---------- with open(label_map.json, r, encodingutf-8) as f: label_map json.load(f) num_classes len(label_map) print(f类别数量: {num_classes}) # ---------- 数据增强与归一化 ---------- train_transform transforms.Compose([ transforms.ToPILImage(), transforms.Resize((IMG_SIZE, IMG_SIZE)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, hue0.05), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.ToPILImage(), transforms.Resize((IMG_SIZE, IMG_SIZE)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # ---------- 数据集与采样器 ---------- train_set Skin23Dataset(skin23/train, label_map, transformtrain_transform) val_set Skin23Dataset(skin23/val, label_map, transformval_transform) # 按类别样本数的倒数计算采样权重用 WeightedRandomSampler 缓解不均衡 labels [s[1] for s in train_set.samples] class_counts torch.bincount(torch.tensor(labels), minlengthnum_classes) weights 1.0 / class_counts.float() sample_weights weights[torch.tensor(labels)] sampler WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights), replacementTrue) train_loader DataLoader(train_set, batch_sizeBATCH_SIZE, samplersampler, num_workersNUM_WORKERS, pin_memoryTrue) val_loader DataLoader(val_set, batch_sizeBATCH_SIZE, shuffleFalse, num_workersNUM_WORKERS, pin_memoryTrue) # ---------- 模型初始化 ---------- model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) model.to(DEVICE) # 用 AdamW 而不是 Adam配合 WEIGHT_DECAY 效果更稳 optimizer torch.optim.AdamW([ {params: [p for n, p in model.named_parameters() if fc not in n], lr: LR * 0.1}, {params: model.fc.parameters(), lr: LR} ], weight_decayWEIGHT_DECAY) criterion nn.CrossEntropyLoss() scaler torch.cuda.amp.GradScaler() # ---------- 训练循环 ---------- best_acc 0.0 for epoch in range(EPOCHS): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(DEVICE), labels.to(DEVICE) optimizer.zero_grad() with torch.autocast(device_typecuda, dtypetorch.float16): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() running_loss loss.item() * images.size(0) # 每个 epoch 结束跑一次验证集 model.eval() correct, total 0, 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(DEVICE), labels.to(DEVICE) with torch.autocast(device_typecuda, dtypetorch.float16): outputs model(images) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) acc correct / total print(fEpoch [{epoch1}/{EPOCHS}] Loss: {running_loss/len(train_set):.4f} Val Acc: {acc:.4f}) if acc best_acc: best_acc acc torch.save({model_state: model.state_dict(), label_map: label_map, img_size: IMG_SIZE}, best_skin23.pth) print(f模型已保存, best_acc{best_acc:.4f})几个参数值得展开说。IMG_SIZE用 640 而不是 224是因为皮肤病灶的纹理细节鳞屑、毛细血管扩张、色素网在 224 下会被压缩到几乎不可辨认验证集准确率通常有 1 到 3 个百分点的差距。代价是显存占用翻倍RTX 3060 以下建议先用 480 或 384 试探。ColorJitter里hue0.05是刻意限制的皮肤病诊断高度依赖颜色色相偏移过大会把“红斑”变成“紫斑”反而教会模型错误关联。WeightedRandomSampler是按类别频率倒数重采样能显著提升罕见类召回但要付出多数类欠采样的代价使用后观察多数类是否有明显掉点。4.3 加载保存的最佳模型做推理预测脚本要可回溯训练结束后加载 best 模型做推理的脚本同样重要。最关键的一点是推理脚本里label_map的顺序必须与训练时完全一致。我通常把 label_map、img_size、归一化参数都写进 checkpoint 文件而不是训练完后单独保存 JSON。因为 JSON 文件可能被误改而 checkpoint 是二进制快照基本不会被手滑破坏。# inference.py import json import torch import cv2 from torchvision import transforms # 加载 checkpoint ckpt torch.load(best_skin23.pth, map_locationcpu) label_map ckpt[label_map] img_size ckpt[img_size] inv_map {v: k for k, v in label_map.items()} model models.resnet50() model.fc nn.Linear(model.fc.in_features, len(label_map)) model.load_state_dict(ckpt[model_state]) model.eval() transform transforms.Compose([ transforms.ToPILImage(), transforms.Resize((img_size, img_size)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def predict(img_path): img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) tensor transform(img).unsqueeze(0) with torch.no_grad(): output model(tensor) probs torch.softmax(output, dim1).squeeze(0) conf, idx torch.max(probs, 0) return inv_map[idx.item()], conf.item()这里的model.fc nn.Linear(model.fc.in_features, len(label_map))必须与训练脚本里完全一致少一层或多一层都会在 load_state_dict 时报错。如果训练时换过预训练权重推理时也要用同一套归一化参数。临床上推理时还会遇到一类特殊情况输入图像不是皮肤病灶而是背景或文档扫描件模型会强行输出一个类别这时需要额外加一个置信度阈值判断低于阈值就拒绝输出。5. 避免常见错误训练集、验证集在皮肤病分类中的高频翻车记录5.1 训练 loss 持续下降验证集 acc 却纹丝不动现象每个 epoch 训练 loss 都在降从 2.1 降到 0.8但验证集准确率一直停在 60% 左右。原因两种可能性最大。一是数据增强过强或过弱增强太强训练集分布被破坏模型学不到稳定的关键特征增强太弱模型过拟合训练集特有背景。二是训练集与验证集之间存在系统性的图像风格差异比如训练集是皮肤镜图像验证集混入了手机拍摄的临床照片这种域偏移不会因为模型变强而消失。解决先把数据增强全部关掉仅保留 Resize 和 Normalize 训 5 个 epoch如果验证集 acc 能随训练 acc 一起上升说明增强参数过于激进逐步把 ColorJitter 和旋转加回去每加一项跑一次验证。如果关掉增强后验证集依然不动那就需要检查验证集图像来源把手机图和皮肤镜图分开评估。5.2 验证集所有样本都被分到同一个多数类现象验证集 acc 显示 85% 以上但打开混淆矩阵发现 23 行里 21 行的预测结果都指向“脂溢性角化”或样本量最大的那一类。原因数据集中类别样本量极度不均衡最大类样本量可能是最小类的 50 倍以上。交叉熵损失对多数类的梯度贡献大模型倾向于把不确定样本全部推给多数类。验证集 acc 虚高是因为验证集本身也是同样的不均衡分布。解决先不做任何重采样只用原始分布跑出一个基线确认多数类占比是多少然后用混淆矩阵按每个类单独算 recall如果最小类的 recall 为 0需要配合 WeightedRandomSampler 重采样或改损失函数为 Focal Loss。Focal Loss 的 gamma 参数从 1.5 起调gamma 太大比如 3 以上会把训练推向另一极端多数类 acc 暴跌整体 acc 反而下降。5.3 换了一台机器后验证集指标暴跌 5% 以上现象同一份代码、同一个 checkpoint在自己的机器上跑验证集 acc 88%换到同事的机器上变成 82%代码完全没变。原因最常见的变量是图像解码库版本不同。OpenCV 的cv2.imread在部分版本里默认把非彩色图强制转成三通道而 Pillow 不会这样做另外torchvision的 Resize 在 0.15 版本后默认插值方式改为双线性但如果代码里没指定interpolation不同版本之间结果会有细微差异。还有一个容易被忽视的变量是机器 CPU 指令集导致浮点结果微小不同通常影响在 0.1% 内不构成 5% 的暴跌。解决在训练环境里把 torch、torchvision、opencv-python 的版本固定写入 requirements.txt并在验证脚本开头打印三个库的版本号。推理时统一用cv2.resize(..., interpolationcv2.INTER_CUBIC)替代 torchvision 的 Resize因为它不受版本影响。5.4 验证集 loss 低于训练集 loss怀疑人生现象训练集 loss 一直高于验证集 loss两者差距稳定在 0.2 左右。原因背后的机制通常是 Dropout 和 BatchNorm 在训练与评估两阶段行为不同。训练时 Dropout 随机屏蔽神经元BatchNorm 按 mini-batch 统计归一化验证时 Dropout 关闭BatchNorm 用全局统计量。如果训练集图像增强引入了较大噪声训练集 loss 偏高很正常验证集 loss 低于训练集并不代表模型没在学而是验证时模型更“确定”。另一种可能是数据划分存在问题验证集里混入了训练集图像这类情况通常伴随验证集 acc 异常高需要回到第 2.2 节做图片级去重检查。解决不必纠结 loss 的绝对值高低重点看验证集 acc 和 loss 的走势以及验证集 loss 是否在某个 epoch 后回升过拟合信号。如果在第 15 个 epoch 后验证集 loss 开始上升说明模型开始记忆训练集噪声此时早停即可。5.5 训练中途 loss 变成 NaN现象前几个 epoch 正常某一步之后 loss 直接变成 nan整个训练崩溃。原因最常见的是学习率过大导致梯度爆炸混合精度训练配合 FP16 时这一风险更大。少部分是图像数据空值导致 loss 计算异常比如某张图本身损坏读取后为全零像素模型输出极端值。解决把 AdamW 的eps参数从默认 1e-8 调到 1e-6很多情况下能直接规避 FP16 下的 nan。同时在训练循环里加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0)把所有参数的梯度范数限制在 5 以内。如果加了裁剪仍然 nan把 DataLoader 的num_workers设为 0 跑一次排除多进程加载时图像解码出错导致的脏数据。6. 用验证集做质量闭环混淆矩阵、Top-2 容错和临床可用性判断训练完成不是终点验证集上跑出的“整体 acc 88%”这个数字在医学图像场景里几乎没有临床参考价值。真正有用的评估方式是逐类计算 recall 和 precision并画出 23x23 混淆矩阵。混淆矩阵能直接告诉你哪些病被模型搞混了混的是同类形态湿疹 vs 接触性皮炎还是不同部位脚底 vs 头皮这决定了你能否通过合并类别来改善指标。# eval_confusion.py import json import torch import numpy as np from sklearn.metrics import confusion_matrix, classification_report from torch.utils.data import DataLoader from dataset import Skin23Dataset from torchvision import models, transforms ckpt torch.load(best_skin23.pth, map_locationcpu) label_map ckpt[label_map] inv_map {v: k for k, v in label_map.items()} # 与训练完全相同的验证 transform val_transform transforms.Compose([ transforms.ToPILImage(), transforms.Resize((ckpt[img_size], ckpt[img_size])), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_set Skin23Dataset(skin23/val, label_map, transformval_transform) val_loader DataLoader(val_set, batch_size32, shuffleFalse) model models.resnet50() model.fc nn.Linear(model.fc.in_features, len(label_map)) model.load_state_dict(ckpt[model_state]) model.eval() all_preds, all_labels [], [] top2_correct 0 with torch.no_grad(): for images, labels in val_loader: outputs model(images) probs torch.softmax(outputs, dim1) # top-1 预测 _, preds torch.topk(probs, 2, dim1) all_preds.extend(preds[:, 0].tolist()) all_labels.extend(labels.tolist()) # top-2 命中真实标签落在前两个预测里 top2_correct (preds labels.unsqueeze(1)).any(dim1).sum().item() print(fTop-1 Acc: {(np.array(all_preds) np.array(all_labels)).mean():.4f}) print(fTop-2 Acc: {top2_correct / len(val_set):.4f}) # 输出每个类别的精确率、召回率、F1 report classification_report(all_labels, all_preds, target_names[inv_map[i] for i in range(len(label_map))], digits3, zero_division0) print(report)Top-2 指标在皮肤病场景里很有意义。皮肤科医生看一张图时通常会给两个鉴别诊断比如“脂溢性角化或日光性角化”然后通过皮肤镜或活检确认。如果模型 Top-2 Acc 能到 95% 以上即使 Top-1 只有 70%这个模型在临床辅助分诊场景仍然有实用价值——可以把 Top-1 概率低、Top-2 命中的样本标为“疑似待复查”而不是直接给出一个错误确断。我的习惯是每次训练结束都跑一遍这份评估把混淆矩阵和 classification_report 存成文件跟 checkpoint 放在一起。之后如果有人问我“这个模型能不能上临床”我不会甩 acc 数字而是打开混淆矩阵指出哪几类高置信度错判——比如模型把 30% 的基底细胞癌判成了良性痣这种错误在临床上不可容忍模型的定位就只能是“预筛工具”而不是“诊断工具”。明确边界之后再决定要不要继续增加训练数据、调损失函数还是改网络结构希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
河南专业舞台灯光音响公司:报告厅、会议室音响与会议系统一体化设计与施工 1. 引言
在现代政企单位、学校与各类场馆中,报告厅与会议室早已不只是"开会的地方",更是承载汇报、培训、远程视频会议、文艺演出等多重功能的核心空间。一套稳定、清晰、易用的音响与会议系统,直接决定了每一次活动的效果与体验。… · 2026/9/23 10:36:57
红鱼儿实战避坑指南:从零搭建全栈项目不踩雷 红鱼儿实战避坑指南:从零搭建全栈项目不踩雷 代码复制下来直接跑就报错?别急着怀疑人生,90%的初学者都卡在环境配置和依赖冲突上。这份红鱼儿项目实战避坑指南,就是帮你把那些藏在角落里的“暗坑”一个个填平。 很多兄弟在 CSDN 或… · 2026/9/23 10:36:57
豪威(OmniVision)OX08D10图像传感器CMOS现货型号大全 一、豪威(OMNIVISION)图像传感器简介据豪威(OMNIVISION)图像传感器现货代理商 广盛宫科技介绍,豪威集团-上海韦尔半导体股份有限公司是全球排名前列的中国半导体设计公司。豪威集团的核心业务是CMOS图像传感器… · 2026/9/23 10:36:57
搞懂日语新闻抓取底层逻辑:3个最佳实践让你避开90%的坑 搞懂日语新闻抓取底层逻辑:3个最佳实践让你避开90%的坑 官方文档动辄几百页,读完脑子还是空的?别急,这很正常。 很多人想抓取日语新闻数据,打开官方API文档或者爬虫库文档,看到密密麻麻的参数说明,直接劝退。… · 2026/9/23 11:19:08
低复杂度无边带信息SLM:基于循环移位与盲检测的OFDM PAPR抑制方案 简介:这份PDF资料聚焦通信与网络中的OFDM系统峰均功率比(PAPR)抑制问题,面向通信工程、无线通信等领域的研究人员和学生。传统SLM算法虽能有效降低PAPR,但存在计算复杂度高且需额外传输边带信息两大缺陷,影… · 2026/9/23 11:19:07
本地部署星辰Xing4.0-29B:MoE架构下的AI表格与文档助手实战 开源大模型这段时间是真的热闹,各个团队轮番放新东西,但真能让人踏踏实实跑在本地、干实际工作的,其实没那么多。我拿到中国电信星辰Xing4.0-29B这个开源版本之后,第一时间就在自己的机器上部署了一轮,重点测了两个高频… · 2026/9/23 11:19:01
Phoenix 前端性能优化:将 await 延迟到真正需要的分支,消除无谓阻塞 Phoenix 前端性能优化:将 await 延迟到真正需要的分支,消除无谓阻塞 【免费下载链接】phoenix AI Observability & Evaluation 项目地址: https://gitcode.com/gh_mirrors/phoenix13/phoenix
本指南源自 Vercel React Best Practices 规则集中… · 2026/9/23 11:19:01
番茄叶子缺陷分类数据集:7类3000张已划分训练测试资源 简介:这份番茄叶子缺陷图像分类数据集面向从事图像分类、农业病害识别与深度学习实践的开发者及学生,提供约3000张已标注的番茄叶片图像,覆盖细菌斑点、早疫病、健康、Septoria_spot等7个类别,可直接作为分类网络输入,… · 2026/9/23 11:18:55
Atlas 300V 24G推理卡部署YOLO:从模型转换到AscendCL实践 接到这个标题的时候我愣了一下,因为“atlas”这个名字太容易让人联想到地图册或者希腊神话里的擎天巨神了。可实际一查,圈里人最近聊的“atlas”,大概率是围绕华为昇腾的Atlas系列AI加速卡,尤其是“atlas 300V 24G”这块卡和“atl… · 2026/9/23 11:18:54
3招搞定手机怎么下载微信面试难题实战项目解析 3招搞定手机怎么下载微信面试难题实战项目解析 面试被问“手机怎么下载微信”背后的原理,90%的人答不上来。别笑,这看似弱智的问题,实则是考察你对移动应用分发机制、安全校验及网络协议理解的试金石。我带过不少校招新人,他们背了八股文,却连一个A… · 2026/9/23 0:00:03
你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 面试被问“高并发下如何保证消息不丢失”,你张口就是“用Redis”,结果面试官追问“如果Redis宕机了怎么办”,你瞬间卡壳。这种场景太常见了,很多新手在背八股文时,只记住了技术名词… · 2026/9/23 0:00:29