简介:一份用于淋巴细胞目标检测的医学图像数据集,包含训练集580张、验证集290张、测试集282张,共1152张病理HE染色图像,标注由医学专家校验,采用YOLO格式的边界框与类别标签。资源面向医疗AI开发者、病理辅助诊断系统研究者及医学教育场景,可训练YOLOv5/v7/v8等算法,支持细胞计数、密度分析等下游任务。包内含2000个文件,以jpg图像和txt标注文件为主,另有yaml配置和docx说明文档,压缩后大小67.68MB,便于直接用于模型训练与推理。针对淋巴细胞簇等密集分布场景进行了专门适配,有助于提升模型在真实病理图像中的泛化能力,可直接用于癌症病理分析、免疫状态评估等临床前研究。目前已有60人学习下载,适合需要快速获取规范标注数据开展医学目标检测实验的读者。
1. 拿到淋巴细胞目标检测数据集.zip,先别急着训练
拿到一个带“数据集”三个字的 zip,并不等于你手里已经有了一份能直接喂给训练脚本的输入——这是我在医学图像方向碰过几回壁之后最真实的体会。“淋巴细胞目标检测数据集.zip”这类压缩包,常见于病理切片或细胞涂片场景:要检测的淋巴细胞直径只有几微米到十几微米,落到图像里往往只是几十个像素的小目标,还经常和炎症细胞、坏死碎片挤在一起,背景很脏。这个 zip 解决的问题很直接:把一批已标注的淋巴细胞图像集中交付,省掉你从零找数据、手工标注的漫长周期。它适合正在做细胞检测、肿瘤微环境分析,以及一切想用 YOLO 系模型在显微图像上做目标检测的从业者。但别急着把 zip 拖进训练代码,先按下面的流程把这份家底盘明白。
2. 解压与盘库:先确认数据格式,再谈训练淋巴细胞模型
2.1 解压这一步就藏着坑:中文路径、伪加密和损坏包
在 Linux 服务器上拿到 zip,最常见的处理方式自然是 unzip。但直接 unzip 在遇到中文文件名时大概率会乱码,因为 Windows 下压缩时默认用 GBK 编码文件名,而 Linux 的 unzip 默认按 UTF-8 去解。表现是解压出来一堆乱码目录名,后续 Python 的 os.listdir 能读到,但路径一旦拼接或打印就出问题。
我一般分三步走。第一步先用 unzip -l 看压缩包列表,确认顶层目录名和文件数量,而不是盲目解压:
unzip -l 淋巴细胞目标检测数据集.zip | head -50 unzip -l 淋巴细胞目标检测数据集.zip | tail -5head 看开头目录结构,tail 看末尾统计的总文件数。如果发现文件名乱码,用 -O 参数指定原始编码:
unzip -O CP936 淋巴细胞目标检测数据集.zip -d ./lympho_data-CP936 是让 unzip 用 GBK 解码文件名再转成 UTF-8 落地。如果没有 -O 选项(部分精简版 unzip 不支持),改用 7z 也能解,7z 对中文编码的容错更好:
7z x 淋巴细胞目标检测数据集.zip -o./lympho_data提示:7z 解压后建议用 ls -la 检查目录权限。如果数据从 U 盘或 Windows 分区拷过来,常见问题是脚本只有读权限,后面 YOLO 训练写缓存文件时会报 Permission denied。
还有一个容易被忽略的点是 zip“伪加密”。有的 zip 文件设置了伪加密标志位,unzip 会要求输入密码,但实际数据并没有加密,7z 或者 Python 的 zipfile 模块仍然可以正常读取。这不是让你去破解密码,密码保护的数据务必走正规授权途径确认密码;伪加密只是在排查“解压不了”时值得知道的现象。
解压之前还要做完整性检查,这一步很多人跳过,跳过的代价是训练到一半报“图片损坏”或者某个 batch 读不出来。unzip -t 能快速校验每个文件的 CRC:
unzip -t 淋巴细胞目标检测数据集.zip如果输出里有 bad CRC 或者 mismatch 字样,说明这个包在传输过程中损坏过,优先找交付方重新打包。别信“就一张图坏了,删掉继续训练”这种侥幸,医学数据一张坏图背后往往意味着整个包的不完整交付。
2.2 目录结构阅读:三类典型布局与标注格式的大致判断
解压完成之后,先摸清目录长什么样。常见的目标检测数据集打包方式大概有三种:
- images/ + annotations/:图像是一批,标注是另一批。annotations 里可能是 XML、JSON 或 txt。
- JPEGImages/ + Annotations/ + ImageSets/:VOC 时代留下的经典布局,ImageSets/Main 里会有 train.txt、val.txt。
- images/ + labels/ + classes.txt:已经处理好的 YOLO 格式,labels 里每个 txt 对应一张同名的图像。
不管原始数据来自哪一款目标检测常用标注工具,导出格式无非下面几种。判断标注格式主要看文件扩展名,再打开首行内容确认:
| 标注格式 | 典型文件位置 | 坐标形式 | 读取方式 |
|---|---|---|---|
| VOC XML | annotations/*.xml | xmin/ymin/xmax/ymax,绝对像素 | xml.etree.ElementTree |
| COCO JSON | annotations/instances_*.json | bbox 为 x,y,w,h,绝对像素 | json + pycocotools |
| YOLO TXT | labels/*.txt | class, x_center, y_center, w, h,归一化到 0-1 | 纯文本 |
| LabelMe JSON | 每张图一个 .json | polygon 点集 | json,做检测需先转矩形框 |
VOC XML 的根节点是 annotation,object 里有 bndbox,坐标是绝对的像素值;COCO 则是一个大 JSON,images 和 annotations 通过 image_id 关联;YOLO txt 每行五个数字,第一个是类别 ID,后四个是归一化坐标。LabelMe 的 JSON 在分割任务里常见,但如果这个 zip 的目标检测数据集是用它标注的,那就意味着你要先从多边形里算出外接矩形,多一层转换工作。
2.3 先写脚本盘库存:图像数、标注数、类别平衡
不做任何格式转换之前,先写一个小的 Python 脚本把所有关键数字统计出来。这一步能帮你判断这个数据集是否值得投入时间清洗,也能在后续训练时作为 baseline 参照。
import os import xml.etree.ElementTree as ET from collections import Counter DATA_ROOT = "./lympho_data" IMG_DIR = os.path.join(DATA_ROOT, "images") ANN_DIR = os.path.join(DATA_ROOT, "annotations") img_exts = (".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff") imgs = [f for f in os.listdir(IMG_DIR) if f.lower().endswith(img_exts)] anns = [f for f in os.listdir(ANN_DIR) if f.lower().endswith(".xml")] class_counter = Counter() no_ann = [] for img in imgs: xml_name = os.path.splitext(img)[0] + ".xml" xml_path = os.path.join(ANN_DIR, xml_name) if not os.path.exists(xml_path): no_ann.append(img) continue tree = ET.parse(xml_path) for obj in tree.findall("object"): cls = obj.findtext("name", "unknown").strip() class_counter[cls] += 1 print(f"图像总数: {len(imgs)}") print(f"XML 标注总数: {len(anns)}") print(f"缺失标注的图像: {len(no_ann)} {no_ann[:5]}") print(f"类别分布: {dict(class_counter)}")逻辑说明:这里以“同名 XML 是否存在”作为标注完整性的判断依据,并统计每个类别的目标数量。import 里用了 ElementTree 解析 VOC XML,如果你的包是 COCO JSON 或 YOLO txt,把解析段替换成 json.load 或直接读文本即可。
参数说明:IMG_DIR 和 ANN_DIR 要跟你的实际目录一致;img_exts 覆盖了医学图像常见的 .tif/.tiff,很多病理切片导出图是 TIFF 格式,这一点和自然图像数据集不太一样。class_counter 帮你一眼看出类别是否平衡,如果某类目标只有几十个,后面训练就要考虑给这类加权重或做复制粘贴增强。
这一轮盘库之后,你手里应该有三张表:图像总数、标注总数、类别分布,以及一份缺标注的坏图名单。接下来才轮到格式转换。
3. 把 VOC 标注转成 YOLO txt:转换脚本与四个边界坑
3.1 为什么要统一成 YOLO txt
不论原始包是 VOC XML 还是 COCO JSON,我最终都会统一转成 YOLO txt 再喂训练。原因有三:第一,YOLO 系列的训练脚本默认读 txt,目录只要 images 和 labels 两套,配一个 data.yaml 就能跑,省掉大量胶水代码;第二,txt 是归一化坐标,和图像分辨率解耦,训练时无论缩放到 640 还是 1024,标注都不需要跟着换算;第三,txt 的排查成本最低,任何文本编辑器都能打开看,格式错一眼能发现。
3.2 VOC XML 转 YOLO txt:完整脚本
这是一个我在多个细胞检测数据上验证过的转换脚本,直接复制改路径就能用。
import os import xml.etree.ElementTree as ET ANN_DIR = "./lympho_data/annotations" # VOC XML 所在目录 LABEL_DIR = "./lympho_data/labels" # 输出的 YOLO txt 目录 os.makedirs(LABEL_DIR, exist_ok=True) # 类别映射表:必须和后面 data.yaml 里的 names 顺序完全一致 CLASS_MAP = { "lymphocyte": 0, "plasma_cell": 1, "neutrophil": 2, } def convert_xml(xml_path, label_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") img_w = int(size.findtext("width")) img_h = int(size.findtext("height")) lines = [] for obj in root.findall("object"): name = obj.findtext("name", "").strip() if name not in CLASS_MAP: continue # VOC 里 difficult 和 truncated 的目标,医学数据里一般忽略掉 if obj.findtext("difficult", "0") == "1": continue bndbox = obj.find("bndbox") xmin = float(bndbox.findtext("xmin")) ymin = float(bndbox.findtext("ymin")) xmax = float(bndbox.findtext("xmax")) ymax = float(bndbox.findtext("ymax")) # 过滤掉宽高小于 1 像素的退化框,通常是标注误操作产生的 if xmax - xmin < 1 or ymax - ymin < 1: continue # YOLO 格式:归一化的中心点坐标 + 归一化宽高 x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 浮点坐标裁到 [0, 1] 区间,防止标注越界引发的训练报错 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) w = min(max(w, 0.0), 1.0) h = min(max(h, 0.0), 1.0) cls_id = CLASS_MAP[name] lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") if lines: with open(label_path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) for xml_name in os.listdir(ANN_DIR): if not xml_name.lower().endswith(".xml"): continue stem = os.path.splitext(xml_name)[0] convert_xml( os.path.join(ANN_DIR, xml_name), os.path.join(LABEL_DIR, stem + ".txt"), )逻辑说明:脚本的核心是把绝对像素坐标换算成归一化坐标——中心点就是 (xmin+xmax)/2 和 (ymin+ymax)/2 各自除以宽高,宽高则是 (xmax-xmin) 和 (ymax-ymin) 各自除以宽高。每个目标输出 5 个数字:类别 ID、中心 x、中心 y、宽、高。文本里的换行符用 \n,不要用 Windows 的 \r\n,Linux 和容器里读起来更干净。
参数说明:这里有三个边界坑,每一个我都踩过,值得单独说。
第一,CLASS_MAP 的顺序决定了训练时的类别 ID,后续 data.yaml 里的 names 必须和它一一对应。顺序错了模型不会报错,但预测结果张冠李戴,验证集上看起来 mAP 不错,实际部署全错。
第二,difficult 标记在 VOC 里表示“难以辨认的目标”,医学数据里经常出现在边缘不清的淋巴细胞上,常规检测训练建议直接跳过,否则模型会被不确定的标注干扰,训练损失来回震荡。
第三,clip 操作只管住越界到 1.0 的浮点误差,如果原标注本身框就在图像外,这种数据要回到标注工具里修,不能靠 clip 糊弄过去,因为框的中心已经偏移了。
3.3 转换后的校验:反投影画框,眼见为实
转换完不要直接开训练,先把 txt 反投影回图像上画框,用眼睛确认一遍。我吃过这个亏:有一版脚本把 ymin 和 ymax 的除法写反了,训练了两天 loss 一直在降,最后可视化才发现框全部上下颠倒。坐标转换的 bug 不会让训练崩掉,只是让模型学一个错误的映射。
import cv2 IMG_DIR = "./lympho_data/images" LABEL_DIR = "./lympho_data/labels" VIS_DIR = "./check_vis" os.makedirs(VIS_DIR, exist_ok=True) for txt_name in os.listdir(LABEL_DIR): stem = os.path.splitext(txt_name)[0] img_path = os.path.join(IMG_DIR, stem + ".jpg") img = cv2.imread(img_path) if img is None: print(f"[warn] 读不到图像: {img_path}") continue h, w = img.shape[:2] with open(os.path.join(LABEL_DIR, txt_name), "r") as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cls_id, xc, yc, bw, bh = map(float, parts) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, str(int(cls_id)), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) out_path = os.path.join(VIS_DIR, stem + "_check.jpg") cv2.imwrite(out_path, img)逻辑说明:从 txt 读归一化坐标,反向乘回图像宽高,换算成像素框的左上角和右下角,然后用 OpenCV 画红色矩形和类别 ID,输出到 check_vis 目录。
参数说明:cls_id 显示为 int 类型,方便你对类别名。每张图抽查右下角是否有越界框——如果矩形画到了图像边缘外部,说明原始标注就有问题,这类图单独记录下来,不要混进训练集。挑 5 到 10 张肉眼检查,重点看细胞密集区域:框是否贴合细胞膜边缘、是否出现一个细胞被两个框套住、整张图的框是否整体偏移。这一步是把数据质量问题暴露在训练之前的最后机会,也是整个目标检测数据集处理流程里最值得花时间的步骤。
4. 划分数据集与 YOLOv8 训练:参数怎么设才不翻车
4.1 划分数据:按图像源分组,避免同源泄露
数据划分最忌讳的是随手 random.shuffle。淋巴细胞图像经常来自同一个病人的多张切片,或者同一张切片的不同视野截图,如果这些高度相似的图一部分进了训练集一部分进了验证集,验证指标会虚高到离谱,换到新病人数据上直接崩掉。正确做法是先看文件名前缀或者目录名是否包含病例 ID,有的话按病例 ID 分组,再把组划分成训练、验证、测试。
import os import random import shutil random.seed(42) IMG_DIR = "./lympho_data/images" LABEL_DIR = "./lympho_data/labels" SPLIT_DIR = "./lympho_data/split" TRAIN_RATIO, VAL_RATIO = 0.8, 0.1 os.makedirs(f"{SPLIT_DIR}/images/train", exist_ok=True) os.makedirs(f"{SPLIT_DIR}/images/val", exist_ok=True) os.makedirs(f"{SPLIT_DIR}/images/test", exist_ok=True) os.makedirs(f"{SPLIT_DIR}/labels/train", exist_ok=True) os.makedirs(f"{SPLIT_DIR}/labels/val", exist_ok=True) os.makedirs(f"{SPLIT_DIR}/labels/test", exist_ok=True) # 按文件名下划线前缀分组,假设前缀是病例 ID,如 case03_slide01_001.jpg groups = {} for img_name in os.listdir(IMG_DIR): stem = os.path.splitext(img_name)[0] group_key = stem.split("_")[0] groups.setdefault(group_key, []).append(stem) group_keys = list(groups.keys()) random.shuffle(group_keys) n_train = int(len(group_keys) * TRAIN_RATIO) n_val = int(len(group_keys) * VAL_RATIO) for i, key in enumerate(group_keys): split = "train" if i < n_train else "val" if i < n_train + n_val else "test" for stem in groups[key]: src_img = os.path.join(IMG_DIR, stem + ".jpg") src_lbl = os.path.join(LABEL_DIR, stem + ".txt") if os.path.exists(src_img): shutil.copy(src_img, f"{SPLIT_DIR}/images/{split}/{stem}.jpg") if os.path.exists(src_lbl): shutil.copy(src_lbl, f"{SPLIT_DIR}/labels/{split}/{stem}.txt") print(f"group 总数: {len(group_keys)}") print(f"train groups: {n_train}, val groups: {n_val}, test groups: {len(group_keys) - n_train - n_val}")逻辑说明:先按文件名前缀聚合病例,再在“病例组”级别随机打乱和划分,而不是在单张图像级别打乱。这样同一病例的图像绝不会同时出现在训练和验证里,对医学检测任务,这是评估可信度的底线。
参数说明:TRAIN_RATIO 取 0.8、VAL_RATIO 取 0.1,剩下 0.1 做测试。样本量小的时候可以改成 0.7/0.15/0.15,但测试集无论如何要留一部分全程不参与训练过程,只在最后评估用一次。seed 固定为 42,是为了复现实验时划分结果完全一致。如果你的数据集文件名是纯数字编号,看不出分组关系,那就需要回到原始采集记录里找切片编号,这个功夫不能省。
4.2 data.yaml 与训练超参数:先从 yolov8n 跑 baseline
YOLOv8 训练自己的数据集,写对 data.yaml 是所有后续动作的入口:
path: /data/lympho_data/split train: images/train val: images/val test: images/test names: 0: lymphocyte 1: plasma_cell 2: neutrophil参数说明:path 必须是绝对路径,YOLO 会把 train/val 里的相对路径拼到它后面。names 的索引顺序必须和转换脚本的 CLASS_MAP 一致,这是整条链路里最容易出错的地方,我在第 3 章和第 5 章都会提到。test 这一项在训练时不参与,val 才是验证集。
训练我一般从轻量模型开始跑,命令如下:
yolo detect train \ model=yolov8n.pt \ data=/data/lympho_data/split/data.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20逻辑说明:yolov8n.pt 是预训练权重,加载它能显著加速收敛,在标注数量不富裕的医学数据上尤其重要。模型不会从头学起,而是在 ImageNet 和 COCO 的通用特征基础上微调,淋巴细胞的细胞核纹理、边缘信息属于底层视觉特征,预训练权重能帮上忙。
参数说明:这里每一个参数都值得讲清楚。
- imgsz:输入分辨率。淋巴细胞的典型框可能只有 30×30 像素,640 是下限;显存允许就上 960 甚至 1024。imgsz 的改动对目标检测的小目标场景影响比换模型结构还大,后面第 6 章专门说。
- batch:显存不够就减半,但 batch 太小(低于 8)时 BatchNorm 统计不稳定,loss 会抖得厉害。这时优先考虑梯度累积,而不是硬扛一个小 batch。
- lr0:预训练模型微调用 0.01 起步是安全的。如果从头训练(不加载 .pt),要用 0.001,否则前几个 epoch 很容易数值爆炸。
- patience:医学数据集小,训练后期 val 指标经常震荡,patience 调大到 30 或 40 更稳,否则容易在模型还没收敛时就停掉。
- 数据增强:YOLOv8 默认开启 mosaic=1.0,对小目标检测有帮助,但 mosaic 会拼接四张图,标注框在拼接处会被裁剪。如果训练后期 loss 反复横跳,把 mosaic 降到 0.5 或关闭再试。
4.3 训练过程中看什么:别只盯 mAP
训练命令跑起来之后,runs/detect/train 目录会生成一系列图表。我最常盯的几张是:
- confusion_matrix.png:看淋巴细胞是否被系统性地误判成别的类别,这比单个 mAP 数字信息量大得多。
- PR_curve.png:如果曲线在 recall 接近 1 的位置明显下坠,说明模型对遮挡和密集细胞场景处理不好,这是淋巴细胞数据集的典型痛点。
- val 阶段的 box_loss 和 cls_loss:两个损失如果在最后 20 个 epoch 还在持续下降,说明 patience 可能不够,模型还没收敛就早停了。
训练完成后,用最佳权重在 test 集上跑一次验证,观察指标相对 val 是否明显下跌:
yolo detect val \ model=runs/detect/train/weights/best.pt \ data=/data/lympho_data/split/data.yaml \ split=test输出里重点看每一类的 mAP50 和 recall,而不是只看平均。哪一类 recall 低,就去难例里找原因。
5. 淋巴细胞数据集避坑实录:5 个高频问题与排查路径
5.1 图像路径或文件名带中文,训练一启动就报 FileNotFoundError
现象:数据在本地 Windows 上跑得好好的,传到 Linux 服务器后,yolo detect train 刚启动就报 FileNotFoundError,提示找不到某张训练图,但 ls 明明能看到文件。
原因:Windows 压缩时使用 GBK 编码的中文目录名,Linux 端默认 UTF-8 解码后目录名变成乱码;YOLO 在读取 train.txt 时按乱码路径去找文件,自然找不到。或者反过来,解压时文件名是好的,但脚本里用了硬编码的中文路径,而服务器 locale 不是 UTF-8。
解决:解压时统一用 unzip -O CP936 转成 UTF-8(就是第 2.1 节的命令);用脚本把所有文件名批量重命名为小写字母和数字组合,训练路径永远只用 ASCII。这一步在医学项目交付时尤其该做,很多合作方的导出数据文件名都很随意,别把风险留到训练阶段。
5.2 训练 loss 正常下降,但画出来的框全部偏移
现象:训练了 50 个 epoch,loss 曲线看起来完全正常,打开验证集的可视化结果却发现每个框都偏在细胞一侧,或者整张图所有框同时偏移一个固定距离。
原因:这是坐标换算错位的典型表现。最常见的是 VOC 的 xmin/ymin 被直接当成中心点用,或者除以图像尺寸时把宽和高搞反。YOLO 的归一化坐标即便错位,loss 依然能通过调整其他参数拟合一部分,所以不会出现 loss 不下降的明显报错,这也是这类 bug 极具迷惑性的原因。
解决:回到第 3.3 节的反投影脚本,把每个 txt 的框画回原图人工检查;重点看 y 方向是否出现系统性偏差,因为高宽比不一致的图最容易暴露这个问题。修复转换脚本后,重新走一遍转换和反投影,再开始训练。这个教训我记了很多年:任何坐标转换代码,哪怕只改了一行,都要重新可视化一批图再放心训练。
5.3 类别 ID 从 1 开始或不连续,训练不报错但结果全错
现象:训练正常结束,weights 也正常保存,但验证集上模型预测的类别和实际标注对不上,混淆矩阵对角线全是稀碎的数值。
原因:标注工具导出的类别名经常是手动填的字符,比如 "1-lymphocyte"、"2-lymphocyte",或者原始包里的 classes.txt 本身就是 1-based。转换脚本如果不做严格映射,只把类别字符串 int() 一下,就会得到从 1 开始的 ID。YOLO 的类别 ID 必须从 0 开始且连续,否则模型输出层的类别索引和 names 之间会出现整体偏移。
解决:在转换脚本里统一维护 CLASS_MAP 字典,把原始类别名显式映射到从 0 开始的 ID,杜绝任何“自动分配类别号”的逻辑;转换完成后扫描所有 txt 文件,统计出现过的类别 ID 集合,确认它是 {0, 1, 2} 而不是 {1, 2, 3}。这一步一分钟就能跑完,但能省掉一整天排错时间。
5.4 验证指标虚高 10 个点,新数据上打回原形
现象:val 的 mAP50 高得让人惊喜,但把训练好的模型拿到新的病理切片上跑,效果明显差一截。
原因:数据划分时在单张图像层面做了随机打乱,同一病例或同一视野的多个截图同时进入了训练集和验证集。模型相当于见过验证集图像的“兄弟版本”,指标自然虚高。医学图像和自然图像的这点差别非常关键:自然图像的类间差异大,同源泄露的影响有限;显微图像里同一个病人的细胞形态高度相似,泄露带来的虚高会非常明显。
解决:按第 4.1 节的方法,以病例组为单位划分数据集。划分之后还要做一个动作:从训练集第 1 张和验证集第 1 张的文件夹路径上确认,两份数据确实来自不同的病例组,而不是同一组的两个子目录。这种错误在多人协作交付的数据集里尤其容易发生。
5.5 目标检测模型微调崩了怎么办:第一个 epoch loss 直接 NaN
现象:使用 yolov8n.pt 微调,第一个 epoch 的 loss 就输出 nan,之后一直 nan,终端没有其他报错。
原因:最常碰见的有三种:一是数据里有空标签 txt,模型在某个 batch 上找不到正样本,损失函数对空预测求导出了问题;二是学习率相对于当前 batch 尺寸过大,数值溢出;三是图像里存在全黑或全白的异常通道,输入张量的数值范围异常。
解决:先写个脚本统计 labels 目录里每个 txt 的目标数量和图像数组的数值范围(min/max 是否全 0),把空标签文件和可疑的全黑图移出训练目录;再把 lr0 降到 0.001 重试。如果还不行,用最小数据集(比如只留 10 张图)跑 1 个 epoch,逐步加回来,定位是数据的问题还是超参的问题。另外,老显卡驱动下 AMP 混合精度偶尔会产出 NaN,命令里加 amp=False 强制单精度重跑一次,成本很低。
6. 小目标检测场景的增量手段:从输入分辨率到切图推理
淋巴细胞在常规 640 分辨率下经常只有 20 到 40 像素,属于标准的小目标检测问题。我拿到一个新数据集时,第一件事不是调模型,而是统计标注框的像素尺寸分布:把第 3 章反投影脚本里的 w 和 h 乘回图像分辨率,画一张框宽高的分布直方图。如果 70% 以上的框小于 32×32,那后续所有优化都围绕“小目标”展开。
第一个手段是提高输入分辨率。imgsz 从 640 提到 960 或 1024,相当于直接放大目标在输入张量里的占比。代价是显存和训练速度:batch 可能要减半,如果显存只有 8G 左右基本跑不动 1024 的 batch 16,这时可以把 mosaic 降一点、关掉部分增强来换训练稳定性。另一个手段是推理侧切图,也就是 SAHI 的思路:把一张大图切成若干有重叠的窗口,每个窗口独立推理,再把结果映射回原图坐标做融合。切图窗口大小一般取训练用 imgsz 的 1.5 到 2 倍,重叠率 20% 到 30%。它对病理大图尤其好用,一张 2000×2000 的切片直接推理,小目标看不清;切图之后每个细胞都变成了“大目标”,模型的检测能力立刻被释放。不过切图也有成本:一个大图边长翻倍,推理次数翻四倍,批量跑上百张切片时耗时会显著拉长,要在实际交付前先算清楚性价比。
评估指标上,我习惯额外看两类数字:一是 recall@50 在每一类上的表现,尤其是淋巴细胞这一类,因为漏检一个细胞比错检一个细胞对后续统计的影响更大;二是按目标尺寸分层的 AP,比如 ap_small,看优化前后小目标 AP 的变化,这比整体 mAP 的变化更有说服力。如果优化后 ap_small 涨了 2 个点而整体 mAP 波动不大,说明方向是对的。
我自己的习惯是任何优化都只动一个变量:先跑一个不做任何增强处理的 baseline,把 val 和 test 的数字记下来;然后每次只改一个参数,训练完跟 baseline 比,绝不一次调三个参数。这样虽然慢,但最后写交付报告时能说清楚“每个分数是怎么涨起来的”。这个习惯是从第一次调参翻车后学来的——那次我同时改了 imgsz、mosaic 和优化器,结果指标涨了,却说不清是谁的功劳。希望帮到你。
本文还有配套的精品资源,点击获取