简介:皮肤癌目标检测数据集面向医学影像自动筛查,整合一千五百七十张高分辨率皮肤镜图像与完整标注,适合计算机视觉与医学图像处理方向的研究者、算法工程师及高校学生进行模型训练与评估。压缩包共两千个文件,包括一千五百七十个文本格式标注、四百二十八张皮肤病变图片、一个模型配置及一份说明文档,整体大小约六十八点一六兆字节,下载和本地实验都很方便。数据已按训练集一千二百五十六张、验证集三百一十四张划分,覆盖基底细胞癌、黑色素瘤、银屑病等九种病变类型,良恶性兼顾,贴近临床场景。直接导入YOLO框架即可训练,免去数据清洗与格式转换,显著降低医学数据获取成本,适用于科研探索、算法对比、毕业设计及教学演示。已有三百四十一人学习使用,适合需要标准化皮肤癌数据集的目标检测实践者。
1. 拿到皮肤癌目标检测数据集.zip,先别急着解压训练
医学图像目标检测这两年被 YOLO 系带得很热,许多人从公开分享或实验室网盘拿到一个皮肤癌目标检测数据集.zip 就开始解压训练,结果要么是 mAP 一直为 0,要么训练完发现模型只会预测良性那一类。这个压缩包里通常装的是皮肤镜图像加人工标注的病灶框,标注文件可能是 VOC 的 xml、COCO 的 json,也可能是 YOLO 的 txt。真正花时间的不是训练代码,而是把医疗标注整理成模型能吃的格式。这篇文章把从压缩包到可训练数据集的完整路径拆开讲,覆盖转换脚本、训练参数和五个高频踩坑点。适合算法工程师、医学图像方向的研究生,以及想自己动手做皮肤病变检测的入门者。
2. 解压前先核对三件事:伪加密、标注格式与图片完整性
皮肤科检测数据集的压缩包没有统一标准,不同来源的目录结构差别很大。我经手的这类数据里,有按 VOC 整理的,有导出成 COCO JSON 的,也有直接给 YOLO txt 的。建议先花十分钟做勘察,别把压缩包当成解压完就能训的成品,否则后面每一步都会跟着错。
2.1 用 zipfile 判断伪加密,别被压缩包坑在第一步
zip 文件不是只有“有密码”和“没密码”两种状态,还存在一种伪加密。成因是文件头里的 general purpose bit flag 第 0 位被置成了 1,让所有解压工具都认为文件加了密,要求输入密码,但文件本身并没有被真正的加密算法处理。网盘二次分享、某些打包脚本的 bug 都会造成这种现象,网上搜“zip 伪加密”能翻到大量讨论。
import zipfile path = '皮肤癌目标检测数据集.zip' with zipfile.ZipFile(path) as zf: for info in zf.infolist(): enc = bool(info.flag_bits & 0x1) comp = info.compress_type status = [] if enc: status.append('疑似加密/伪加密') if comp == 99: status.append('未知压缩算法') if status: print(f'[!] {info.filename} | flag_bits={info.flag_bits:#06x} | ' f'compress_type={comp} | {" / ".join(status)}')这段代码逐个遍历 zip 内文件条目。flag_bits 的第 0 位是加密标志,置位就打印出来;compress_type=99 表示用了非 deflate 的压缩算法,普通工具解不开。这两个检查放在解压前,能避免解开一半报 wrong password 的鬼打墙。伪加密的解法有两条路:一条是把 flag 位改成 0 再重新打包,另一条是换用能忽略加密标记的底层解压库。实际操作中,用 7-Zip 直接拖出文件有时也能绕过,因为它对数据段的读取不像 Windows 自带解压那么苛刻。但要注意,如果文件是真的加密过,这些办法都无效,只能找发布者要密码。
提示:开源数据集的授权条款差异很大,有的允许自由使用,有的只允许非商业研究。压缩包里通常有 README 或 license.txt,先读再动。
解压时建议放到不带中文、不带空格的纯英文路径下,YOLO 训练在 Windows 上遇到路径里的非 ASCII 字符会偶发编码问题。我一般解压到一个叫 dermdata 的文件夹,路径里不留空格。
2.2 十分钟摸清 VOC、COCO、YOLO 三种标注布局
解压后的目录先列一下再动手。皮肤癌数据集最常见三种布局,它们的特征差异很明显:
| 格式 | 典型文件 | 标注存储位置 | 关键特征 |
|---|---|---|---|
| VOC | .xml + .jpg | Annotations 目录 | xml 里有 size 标签和 bndbox 四坐标 |
| COCO | .json + .jpg | annotations 目录 | json 含 images、categories、annotations 三段 |
| YOLO | .txt + .jpg | labels 目录或与图片混放 | 每行 5 个数:类别ID、归一化中心点、宽高 |
光靠“看着像”容易翻车,有的数据集把 VOC 信息存成了 csv,有的 JSON 同时包含分类标签和分割掩码。用一个扫描脚本把扩展名分布打出来,马上就知道该走哪条转换路径。
import os from collections import Counter root = 'dermdata' ext_counter = Counter() for dirpath, dirnames, filenames in os.walk(root): for fname in filenames: ext = os.path.splitext(fname)[1].lower() ext_counter[ext] += 1 for ext, cnt in ext_counter.most_common(): print(f'{ext or "(无扩展名)"}: {cnt} 个文件')这个脚本在解压后的根目录跑一次,输出所有扩展名的数量统计。看到 .xml 和 .jpg 数量接近,大概率是 VOC;看到 .json 和 .jpg,则是 COCO 或分层标注。如果出现大量 .txt,可能已经有 YOLO 格式标注,能省不少事。如果跳出 .npy、.npz,要小心是分割掩码或特征文件,得先厘清用途再决定要不要用。
看完扩展名之后,随手抽查三到五张图片和对应标注,看框是否贴合病灶边缘。皮肤科医生画框的习惯和普通目标检测不太一样,有的画紧贴病灶的精确框,有的画包含病灶的宽松框。这种差异不影响转换,但会影响后面评估 mAP 时结果是否让你满意。
2.3 批量读图校验:灰度图、RGBA 和损坏文件提前暴露
标注格式确认后,先做图片完整性检查。医学图像在这块特别容易出问题,原因在于来源复杂:有的设备导出灰度 tiff 再转存 jpg,有的皮肤镜软件输出带 alpha 通道的 png,压缩包中途断流还会产生截断的半张图。下面这段批量校验脚本能一口气把所有异常图找出来。
import cv2 import glob errs = [] for img_path in (glob.glob('dermdata/**/*.jpg', recursive=True) + glob.glob('dermdata/**/*.png', recursive=True) + glob.glob('dermdata/**/*.jpeg', recursive=True)): img = cv2.imread(img_path, cv2.IMREAD_UNCHANGED) if img is None: errs.append((img_path, '损坏或不可读')) continue if img.ndim == 2: errs.append((img_path, f'灰度单通道 {img.shape}')) elif img.shape[2] == 4: errs.append((img_path, f'RGBA四通道 {img.shape}')) elif img.shape[2] != 3: errs.append((img_path, f'异常通道数 {img.shape}')) for p, reason in errs[:30]: print(p, reason) print('异常图片总数:', len(errs))这段代码用 OpenCV 匹配 jpg、png、jpeg 三类文件。IMREAD_UNCHANGED 是关键参数,它让 imread 保留原文件全部通道,而不是默认强制转成 BGR 三通道,能暴露灰度图、RGBA 图这类通道异常。这些文件不处理,训练中 dataloader 会在某一轮突然报读图错误,很容易被误判成显存溢出。处理办法也直接:灰度图复制成三通道,RGBA 只保留前三个通道重新保存。图片数量不多时脚本秒级跑完,结果保存成 txt 供清洗时对照。
到这里,压缩包里的内容摸清了,下一步就是把它转成 YOLO 能直接训练的格式,这是全程出错率最高的一段。
3. 标注统一成 YOLO 格式:VOC 转 TXT 脚本与四个边界坑
在皮肤癌数据集上,标注最常以 VOC 格式出现。转换本身不复杂,但皮肤病变标注有别的数据集少见的脏数据特征:越界框、类别缩写混用、同一张图多个目标。没有防御逻辑的转换脚本,往往在人不在的时候静默产出错误标签。
3.1 从 XML 到 TXT:坐标归一化不是唯一要做的事
VOC 的 xml 里,坐标是像素级的 xmin、ymin、xmax、ymax,YOLO 期望的是归一化中心点和宽高。公式很直接:
x_center_norm = (xmin + xmax) / 2 / img_width y_center_norm = (ymin + ymax) / 2 / img_height w_norm = (xmax - xmin) / img_width h_norm = (ymax - ymin) / img_height坑全在“如何安全地把 xml 文本变成数字”。XML 标签缺失是最常见的错误来源,有的文件缺 size,有的 object 里没有 name,还有的同一张图出现两个类别名不同的同名框。转换脚本要做逐字段检查并在异常时跳过,而不是让程序崩在跑了一半的时候。我见过有人图省事直接用字符串拆分解析 xml,遇到一个换行符差异就整体错位,得不偿失。
3.2 可复制的 VOC 转 YOLO 脚本与参数解释
import os import xml.etree.ElementTree as ET from glob import glob ANNO_DIR = 'dermdata/Annotations' OUT_DIR = 'dermdata/labels' os.makedirs(OUT_DIR, exist_ok=True) classes = ['melanoma', 'nevus', 'seborrheic_keratosis'] def normalize(size, box): dw = 1.0 / size[0] dh = 1.0 / size[1] cx = (box[0] + box[1]) / 2.0 * dw cy = (box[2] + box[3]) / 2.0 * dh w = (box[1] - box[0]) * dw h = (box[3] - box[2]) * dh return cx, cy, w, h for xml_path in glob(f'{ANNO_DIR}/*.xml'): try: root = ET.parse(xml_path).getroot() except ET.ParseError: print('跳过无法解析的XML:', xml_path) continue size_tag = root.find('size') if size_tag is None: print('缺size标签:', xml_path) continue img_w = int(size_tag.find('width').text) img_h = int(size_tag.find('height').text) lines = [] for obj in root.iter('object'): name_tag = obj.find('name') if name_tag is None or name_tag.text not in classes: continue cls_id = classes.index(name_tag.text) bnd = obj.find('bndbox') if bnd is None: continue x1 = float(bnd.find('xmin').text) y1 = float(bnd.find('ymin').text) x2 = float(bnd.find('xmax').text) y2 = float(bnd.find('ymax').text) x1 = max(0, min(x1, img_w - 1)) x2 = max(0, min(x2, img_w - 1)) y1 = max(0, min(y1, img_h - 1)) y2 = max(0, min(y2, img_h - 1)) if x2 - x1 < 1 or y2 - y1 < 1: continue cx, cy, w, h = normalize((img_w, img_h), (x1, x2, y1, y2)) lines.append(f'{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}') base = os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(OUT_DIR, f'{base}.txt'), 'w', encoding='utf-8') as f: f.write('\n'.join(lines))逻辑上三个点值得注意。第一,用 root.iter('object') 而不是 findall,因为部分数据集在 anno 标签里嵌套了物体分组,iter 能递归取到所有 object 节点。第二,对 xmax、ymax 做了 clamp,防止标注人员把框拖出图外导致归一化坐标大于 1。第三,丢弃宽或高小于 1 像素的框,这类框在皮肤科标注里通常是误操作生成。最后写出的 txt 每行 5 个值,精度保留 6 位小数。
参数说明:classes 列表的顺序不能随意改,训练时 data.yaml 里的 names 顺序必须与它完全一致,否则模型输出的类别 ID 和真实类别对不上。转换前先抽查几个 xml 的 size 值,如果发现 size 和实际图片分辨率不一致,比如图片被 resize 过但 xml 没同步,不要在脚本里找补,重新导出标注是更稳的做法。还有一个容易忽略的点:转换脚本跑完后要抽查生成的 txt,看坐标是否在 0 到 1 之间,如果出现负数或大于 1,基本是越界框没清理干净。
3.3 同类异名与类别映射:合并前先想清楚下游任务
皮肤病变的类别名从来就不干净。标黑色素瘤的有用 melanoma,有用 MM,有用 mel;benign 和 nevus 更是经常混着用。我在转换脚本前面放一个别名表,把同一类的不同写法归一化。
ALIAS = { 'melanoma': 'melanoma', 'mm': 'melanoma', 'mel': 'melanoma', 'benign': 'nevus', 'nevus': 'nevus', 'nevi': 'nevus', 'sk': 'seborrheic_keratosis', 'seborrheic_keratosis': 'seborrheic_keratosis', } def map_name(raw: str): return ALIAS.get(raw.strip().lower(), None)这段映射把 mm、mel 这类缩写归到 melanoma,把 benign 归到 nevus。但合并要谨慎:如果目标任务是临床辅助筛查,把全部良性病变合并成一个大类可以接受;如果目标是区分黑色素瘤和脂溢性角化病,那 benign 这个标签本身就模糊,一律并到 nevus 反而污染模型。我的习惯是:先统计全数据集类别名出现次数,再决定映射策略,映射表单独保存下来,不要只藏在转换脚本里。
3.4 验证转换结果:框宽高分布里藏着脏数据
转换完成后,逐个打开 txt 检查没有意义,正确做法是对全量标签做分布统计。这一步能发现前面脚本里没暴露的深层问题。
import glob import numpy as np widths, heights, labels = [], [], [] for label_path in glob.glob('dermdata/labels/*.txt'): with open(label_path) as f: for line in f: parts = line.split() if len(parts) != 5: print('非5列格式:', label_path, line.strip()) continue cls = int(parts[0]) w = float(parts[3]) h = float(parts[4]) widths.append(w) heights.append(h) labels.append(cls) widths = np.array(widths) heights = np.array(heights) print('框数量:', len(widths)) print('宽度分位:', np.percentile(widths, [1, 50, 99])) print('长宽比超4的框占比:', ((widths / np.maximum(heights, 1e-6)) > 4).mean())这段代码输出三个关键信息:数据集有多少个标注框、框宽在 1%、50%、99% 分位的数值、长宽比异常的框占比。皮肤镜图像里的病灶大多是近圆形或椭圆,如果长宽比大于 4 的框占比超过 10%,要么是标注时画了包含周围皮肤的大框,要么是转换时把 x 和 y 坐标对错了。宽度的中位数如果不到 0.05,说明病灶以小目标为主,训练时要重点考虑高分辨率输入。
顺便提一句 COCO 转 YOLO 的情况:COCO json 里的 bbox 字段是 [x, y, width, height],且 x、y 是左上角坐标,转换时用 x+width/2 得到中心点,不要直接拿来当中心。这个细节我在不止一个项目里看到有人写反。转换完的验证方式与 VOC 相同,也可以用上面的宽高分布脚本。
到这里,标注文件已经变成纯正的 YOLO txt。下一步是把它组织成能直接训练的数据集目录。
4. 用 YOLOv8 跑通皮肤癌检测:data.yaml 组织与关键参数
4.1 数据集目录布局:让 YOLO 直接认出来
YOLO 训练依赖一套固定的目录结构。常见做法是把转换结果整理成下面这个样子:
skin_dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/images 和 labels 必须同级,两个目录下分别用 train 和 val 子目录。每张图在 images/train 下的相对路径,要在 labels/train 下有同名 txt 对应。划分这一步用代码做,不要手动拖拽,因为还要处理按病人分组和类别均衡这些额外要求。
import os import random import shutil SEED = 42 random.seed(SEED) image_files = [p for p in os.listdir('dermdata/JPEGImages') if p.lower().endswith(('.jpg', '.png', '.jpeg'))] random.shuffle(image_files) val_count = max(1, int(len(image_files) * 0.2)) val_files = set(image_files[:val_count]) train_files = set(image_files[val_count:]) for subset, files in [('train', train_files), ('val', val_files)]: os.makedirs(f'skin_dataset/images/{subset}', exist_ok=True) os.makedirs(f'skin_dataset/labels/{subset}', exist_ok=True) for fname in files: base = os.path.splitext(fname)[0] shutil.copy(os.path.join('dermdata/JPEGImages', fname), os.path.join(f'skin_dataset/images/{subset}', fname)) lb_src = os.path.join('dermdata/labels', base + '.txt') if os.path.exists(lb_src): shutil.copy(lb_src, os.path.join(f'skin_dataset/labels/{subset}', base + '.txt'))这段脚本按 8:2 的比例随机划分并复制图片和标签。用 shutil.copy 不用 cut,保留原始数据方便重新划分。随机划分对皮肤癌数据其实不够,同一个病人的多张皮肤镜图像会泄漏到训练和验证两端,模型可能在“背病人”而不是学特征。更稳的做法是用数据集自带的病人 ID 做分组划分,按病人整体切分,后面细说。
数据量小的时候,比如总数不到一千张,验证集 20% 依然可以接受,但曲线波动会很大,最好配合 k 折交叉验证来选参数。
4.2 data.yaml:类别顺序必须和转换脚本一致
在 skin_dataset 目录下建一个 data.yaml:
path: /absolute/path/to/skin_dataset train: images/train val: images/val nc: 3 names: 0: melanoma 1: nevus 2: seborrheic_keratosispath 写绝对路径,相对路径在 Windows 和 Linux 上表现不一致,容易报 dataset not found。train 和 val 写的是相对于 path 的子目录。nc 必须和 names 条数一致,names 的顺序就是转换脚本里的 classes 顺序。如果训练后发现预测类别和真实类别对不上,先查这里,不要动模型。
4.3 训练命令与参数:imgsz、batch、epochs 怎么定
第一次训练不要追求精度,目标是确认数据链路没有断裂,用最小代价看指标是否合理。
yolo detect train \ model=yolov8n.pt \ data=skin_dataset/data.yaml \ epochs=50 \ imgsz=640 \ batch=16 \ device=0 \ patience=10这条命令用 yolov8n 这个最小的官方权重做起点,跑 50 个 epoch,输入尺寸 640,每批 16 张。参数怎么调:显存不够时优先减半 batch,而不是一味降低 imgsz。对皮肤癌这种小病灶密集的任务,imgsz 从 640 提到 1280 往往比调一堆增强参数更有效,因为病灶在原图上可能只有几十像素,缩到 640 后只剩几个像素,模型根本学不到。epochs 在确认数据正常后再加大,第一次跑 50 轮够用,太多只是浪费时间。
训练结束后先看 val 输出里的 mAP50-95,不要只看 mAP50。皮肤癌检测里病灶框通常不大,mAP50-95 对小目标定位更敏感。如果 mAP50-95 连 0.1 都不到,大概率是数据问题,继续加 epochs 没有意义。
4.4 小样本下的模型选择与预训练权重
皮肤癌目标检测数据集的规模通常在几百到两三千张图,属于典型小样本。模型越大越容易过拟合。我的习惯是只在 n 和 s 之间选基线,绝不用 l 或 x 直接跑长训练。预训练权重用官方 yolov8n.pt 是标准做法,模型在 ImageNet 和 COCO 上学到的基本纹理特征,对皮肤镜这种全局纹理差异不大的场景依然有价值。
判断过拟合不能只看训练 loss 和验证 loss 的差距,看 val 曲线更直观:验证 mAP 在某个 epoch 后一路向下而训练还在涨,说明开始背训练集了。此时把 epochs 调小或 rely patience 早停,同时收紧增强策略。对医学图像,不要随便上来就用随机擦除这类丢区域的增强,把病灶区域擦掉了,模型学到的是残缺样本,反而漏检。
5. 皮肤癌目标检测数据集避坑记录:五个现象、原因和解决
这一章是整篇里最值得反复看的部分。以下问题我都实际踩过,按现象、原因、解决的顺序写,方便你对照排查。
5.1 训练损失正常但验证 mAP 一直是 0
现象:训练日志里 box_loss 在下降,分类 loss 也在下降,但每个 epoch 结束后的验证 mAP 全部是 0,连 val/box_loss 都正常。
原因:最常见是验证集标签为空。划分数据集时脚本只复制了图片,没复制对应的 labels,或转换时漏掉了验证集图片的 xml,验证集标签目录里只有零星的几个文件。YOLO 对无标签验证集的处理方式是不报错,直接算出 0 mAP,非常容易误判成模型问题。
解决:训练前对比 skin_dataset/labels/val 下 txt 数量和 skin_dataset/images/val 下图片数量,两者差超过一两个就要回头查划分脚本。另一个原因是类别 ID 越界,比如 classes 只有三类,某张标签里却写了 class 3,验证时这个框会被忽略。用前面统计脚本打印全量标签的类别 ID 集合,确认最大 ID 等于 nc 减一。
5.2 类别不平衡让模型只会输出背景
现象:训练结束后所有测试图都预测为同一类,且置信度高。数据集里只有 5% 的图像带 melanoma 框,其余都是 nevus 或无框图像,模型很自然学会输出 nevus,因为这样得分最高。
原因:目标检测的分类损失按图像内目标数加权,背景像素远多于前景,正负样本天然严重失衡。皮肤病变数据里良性和恶性病例比例悬殊,恶性少得可怜,这种不平衡在统计指标上可能只表现为少数类召回率低,但实际预测时就是一边倒。
解决:先别急着加数据增强。把不含任何标注的图片单独分出来作为背景集,统计每个类别的框数量,再做复制采样或调整 mosiac 策略。类别少的类可以把它所在图片复制多份进 train 目录,注意同时复制标签。差距在 10 倍以内时,靠调低置信度阈值或调整分类损失权重也能缓解,但皮肤癌检测场景我一般先加强 mosaic,让模型在更多拼接上下文中看到小病灶。
5.3 病灶尺寸差异大,小病灶被漏检
现象:验证集里的大病灶都能检出,但直径小于图片宽度 10% 的小病灶几乎全漏。早期黑色素瘤在皮肤镜图上往往只是一个小暗点,原图 3000x2000,缩到 640 后这个点只剩 6-8 像素。
原因:YOLO 的下采样结构对深层特征图上的大目标更敏感,小目标经过多次 stride 后特征几乎耗尽。imgsz=640 时,最深特征层 stride 是 32,小目标只剩一两个特征点,检测器很难给它配到合适的锚框。
解决:优先提高 imgsz 到 1280,而不是换模型。显存够时 imgsz=1280 配合 yolov8s,对皮肤病灶这类目标小但图像分辨率高的场景立竿见影。显存不够时改用切片推理,把原图切成多个 patch 分别检测再融合结果,避免整体缩放造成的信息损失。我自己在 12G 显存的卡上跑过 1280,batch 压到 4 也能训练,只是慢一些。
5.4 标注坐标 W/H 颠倒,模型学了个寂寞
现象:训练能跑通,损失也在下降,但验证时预测框比真实框形状差很多,宽高比和病灶特征明显不符。
原因:VOC 转 YOLO 时把 xmax 和 ymin 做了混用,或者 COCO 的 width 和 height 字段读反。框宽高和中心点坐标的语义不同,这个错误不会让训练报错,所以特别隐蔽。肉眼不抽查根本发现不了。
解决:转换后按 3.4 小节做宽高分布统计,发现高度集中在 0.02 到 0.04 而宽度集中在 0.5 到 0.9 这类极端分布,立即检查公式。随手挑一张已知图片,把归一化坐标乘回原图宽高,用 OpenCV 画框,肉眼确认左右上下是否正确。这一步五分钟能完成,但能省下后面几天的无效调参。
5.5 提前用测试集调参,指标虚高还浑然不觉
现象:模型在 val 集上 mAP50 到 0.85,你很高兴,但换到真实的临床数据上效果明显下降,漏检一堆。
原因:整个训练过程中频繁用 val 集选择模型和超参,val 集的信息已经渗透进决策。皮肤癌数据集小、病灶分布集中,把 val 集来回看十几遍之后,模型实际上记忆的是 val 集的特征分布,而不是泛化规律。
解决:固定一个 test 集,只做最终评估,训练过程中一次都不许看。val 集可以反复调,但 test 集的结果一旦看过,这组指标就废了,需要重新采样。小数据上更严格的做法是外层划分 test、内层跑参数搜索的嵌套交叉验证,训练次数会翻倍,但对医学检测这种高误检成本的场景,指标诚实比指标好看重要得多。我后来习惯把 test 集单独放在一个加密压缩包里,只在最后评估时解压,物理上避免自己手滑。
6. 验证模型有没有学到“皮肤癌特征”:混淆矩阵、类激活图与增强
6.1 混淆矩阵与分类别召回
训练结束会生成 confusion_matrix.png,不要只看总 mAP,逐类看召回更有意义。皮肤癌检测里少数类往往被背景淹没,对 melanoma 这类高风险类别,把置信度阈值从默认 0.25 降到 0.1 再统计召回,能看清模型到底有没有学到特征。如果降低阈值后召回明显上升而误检可控,说明模型能力在,只是阈值保守;如果召回没变化,说明模型根本没学会这个类别。
6.2 用 Grad-CAM 看模型关注区域
我习惯用基于梯度的类激活图对几张典型样本做可视化,看模型到底在看哪里。对同一张黑色素瘤图片,如果高亮区域集中在病灶边缘或外围皮肤,而不是病灶内部的色素区域,说明模型更多依赖边界晕圈。皮肤镜诊断的关键依据是不对称、边界不规则、颜色分布,模型关注点是否落在这些区域,比 mAP 更能说明模型是否值得上线。这一步不需要复杂工具,YOLOv8 的检测头输出特征图配合简单的梯度回传就能算。
6.3 针对皮肤癌检测的三种增强技巧
水平翻转、小幅旋转、尺度抖动对皮肤镜图像是安全的,不建议做颜色剧烈扰动和随机擦除。皮肤镜的颜色是黑色素瘤判读的重要依据,Hue 和 Saturation 大范围扰动会破坏临床语义。我的做法是训练早期只用轻微 HSV 扰动,等模型在验证集上出现明显过拟合时,再加入更强一点的几何增强,同时配合早停。另外 mixup 在这个领域效果不稳定,我把它的开启概率调低到 0.1 以下。
做检测项目这么多年,我最大的教训是:转换脚本和训练参数只是起点,数据检查才是长期积累的资产。每个新数据集都要把它当成第一次见,重复做一遍完整性校验。希望这些经验能帮你在皮肤癌目标检测数据集上少走几段弯路,直接跑到能出指标的那一步。
本文还有配套的精品资源,点击获取