ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Unet+Resnet细胞核分割实战:从训练到多分类扩展

Unet+Resnet细胞核分割实战:从训练到多分类扩展 简介面向医学影像与深度学习分割入门者这份实战项目以 Unet 为框架、Resnet 为backbone完成子宫颈细胞核二分类分割压缩包将数据集、训练代码与已训练权重打包经测试可直接运行适合快速上手多尺度训练与多类别扩展。包体共804个文件以jpg/png图像数据为主另有py训练推理脚本、txt类别映射、xml标注、pth权重及readme说明整体113.33MB已有228人学习下载。价值点在于train脚本默认开启多尺度训练自动将输入随机缩放到设定尺寸的0.5-1.5倍utils中的compute_gray函数把mask灰度值写入txt并自动设定Unet输出通道方便扩展到多类别分割学习率采用cosine衰减run_results内提供训练/测试loss与iou曲线日志可查看各类别iou、recall、precision及全局像素准确率。实测仅训练50个epochs全局像素准确度达0.89miou为0.72若增加训练轮数仍有提升空间推理时只需把图片放入inference目录并运行predict脚本配合README新手也能顺利跑通。1. 把 UnetResnet 跑通自己的分割数据这份细胞核分割项目能直接改着用做过分割任务的人都知道Unet 是骨架backbone 用 Resnet 还是 VGG直接决定模型上限和收敛速度。这份子宫颈细胞核分割项目属于少有的「代码、数据、权重三件套齐全」的实战资源——下载下来解压就能训练训练脚本里已经把多尺度、多类别、学习率衰减这些训练 trick 都封好了。我拆完之后比较惊喜的一点是项目只训了 50 个 epochs全局像素准确度到了 0.89miou 到 0.72如果加大训练轮数还有上升空间。也就是说拿来做毕设、课程设计或者入门 Unet 分割工程它是一个能跑通且有明确结果预期的起点。适合两类人一类是想看 UnetResnet 完整训练流程的初学者另一类是准备做医学图像细胞分割但还没搭好基础代码的开发者。2. 从目录到训练脚本先搞清楚这套代码怎么组织2.1 项目文件的构成与职责划分解压之后你能看到图片文件是以 数字_数字_宽_高.jpg 形式命名的这是医学图像常见的裁剪切片命名方式图像来自原始病理涂片切出来的小图。整个项目的核心不是这些图片而是 train 脚本、predict 脚本、utils 工具目录和 run_results 输出目录。train 脚本负责完整训练流程predict 脚本负责推理utils 里的 compute_gray 函数是关键枢纽——它负责解析 mask 灰度值并自动配置 Unet 的输出类别数。run_results 目录存放训练日志、损失曲线、iou 曲线和最好的权重文件。这里面有一套设计思路值得学把数据预处理、类别配置和网络结构解耦由 mask 驱动模型输出这样从二分类切到多分类时不需要改网络结构代码。2.2 训练流程的入口与关键逻辑训练时直接运行 train.py常见做法是在项目根目录执行python train.py脚本会自动读取数据目录中的原图和 mask不需要你手动指定训练集、验证集的划分比例也没有额外配置文件。代码会自动把数据按设定尺寸随机缩放到 0.5-1.5 倍之间实现多尺度训练。这样做的好处很直接同一张图在训练过程中多次出现但每次尺寸不同、感受野对应关系不停变化相当于免费做了数据增强让模型对细胞核大小差异更鲁棒。注意几个参数含义设定尺寸是你预先指定的输入分辨率代码在此基础上做随机缩放0.5-1.5 这个倍率区间其实覆盖了缩放为一半到一点五倍的全部中间值。这样的多尺度策略特别适合细胞核分割场景因为染片厚度、成像设备差异会导致核的大小在不同图里差别很大固定分辨率训练很容易过拟合到特定尺度。2.3 mask 灰度值如何自动决定输出 channel这里重点讲 utils 中 compute_gray 函数的工作原理。它遍历 mask 图像提取所有出现的灰度值写入 txt 文本。与此同时它根据这些灰度值的数量自动为 Unet 网络设置输出 channel 数。比如二分类场景下mask 灰度值只有 0 和 255那么输出 channel 为 2。如果做多分类比如想要分出核、细胞质和背景mask 里有 0、128、255 三个灰度值输出 channel 自动变为 3。这就是这套项目支持多类别分割的机制。# 这段逻辑等价于项目 utils 中的核心思路 def compute_gray(mask_dir): gray_values set() # 扫描所有 mask 图片 for mask_name in os.listdir(mask_dir): mask cv2.imread(os.path.join(mask_dir, mask_name), cv2.IMREAD_GRAYSCALE) # 统计这张 mask 中出现了哪些灰度值 gray_values.update(np.unique(mask).tolist()) # 将灰度值写入 txt 文件 with open(gray_values.txt, w) as f: f.write(,.join([str(v) for v in sorted(gray_values)])) # 输出 channel 灰度值类别数 return len(gray_values)这段代码的逻辑是先用 set 收集所有 mask 中出现的灰度值去重之后写入文本文件返回类别数量。注意每个 mask 里未标注的区域灰度值为 0在二分类时背景即类别 0所以灰度值集合长度为 2对应输出 channel 为 2。类别顺序与灰度值大小的一般对应关系是灰度值越小越靠前。在多分类扩展时要注意mask 中每个类别必须用单一且固定的灰度值不能出现两个类用同一个灰度值的情况。2.4 训练过程中的学习率调度与指标记录学习率采用的是 cosine 衰减策略。训练初期学习率较大、收敛速度快随着 epoch 推进学习率平滑下降在后期更精细地逼近最优解。对于医学小数据集来说这个策略能够有效避免震荡。每轮训练结束后代码会计算训练集和测试集的损失和 iou用 matplotlib 绘制曲线图并保存到 run_results。同时保存训练日志日志内容包括每个类别的 iou、recall、precision以及全局像素点的准确率。这些指标全部以可读文本形式记录训练完成后你打开 run_results 目录里的日志文件就能看到。关于权重文件的使用训练过程中会保存最好的权重也就是验证集 iou 最高的那个模型存放在 run_results 中。推理时 predict 脚本会自动加载最好的权重文件不需要手动指定路径。这一点对新手来说很友好整个流程做到了零参数配置运行。3. 训练与推理实操从数据准备到结果验证的完整路径3.1 数据目录的组织方式与格式要求这套代码能直接跑通但它对数据组织有隐约的约定。核心要求是原图和 mask 文件名保持一一对应且 mask 是单通道灰度图尺寸与原图一致。文件名的前缀数字串是病理涂片的样本编号宽高表示裁剪尺寸同一编号下不同宽高的图来自同一张切片的多个视野。实操中我一般把数据整理成下面这种结构dataset/ images/ # 原图jpg 格式 masks/ # 标签图png 或 bmp 格式单通道灰度原图一般为 RGB 三通道彩色图模型输入时会做归一化。mask 必须是单通道灰度图背景灰度值为 0前景目标灰度值为 255二分类场景。这套项目里的 mask 是单通道灰度标签网络在推理时会输出每个像素属于各个类别的概率取最大概率对应的类别作为预测结果。3.2 predict 脚本的推理流程推理是整个项目里最简单的一步。把待推理图像放在 inference 目录下也就是放一张或多张需要预测的图片然后在项目根目录执行python predict.py脚本会自动遍历 inference 目录中的所有图片加载 run_results 中的最好权重执行推理并把预测结果写到指定的输出目录。这里有一个使用习惯值得养成在放图片进去之前先确认图片尺寸和训练设定尺寸不要差太远。虽然 Unet 是全卷积网络能处理任意尺寸输入但如果输入尺寸和训练尺寸差一个数量级容易出现分割碎片化的问题。预测输出的图像中像素值对应模型预测的类别编号背景保持黑色前景为白色。如果你用的是多分类权重则每个类别显示为对应灰度值。3.3 训练前后的验证闭环怎么打训练完成后不要急着看曲线就完事。我会先看全局像素准确率和 miou 之间的关系再去看单独类别的 recall 和 precision。对于细胞核分割来说recall 低意味着漏检了核precision 低则说明把杂质当成核了。校验预测效果有一个比较直接的方法把某张测试图的原图和 mask 放在一起对比观察再看 predict 脚本输出的预测掩膜。如果整体轮廓对得上边缘有小锯齿是正常的如果出现大面积误判先回看数据前处理或类别灰度值是否填对。# 用 Python 快速检查一张 mask 的灰度值 import cv2 import numpy as np mask cv2.imread(test_mask.png, cv2.IMREAD_GRAYSCALE) unique_vals np.unique(mask) print(mask 灰度值类别:, unique_vals)这段代码用来判断 mask 是否符合项目的白名单机制——如果打印出 0、255 之外的值比如 254 或 1说明标注软件在做压缩或抗锯齿处理需要修正 mask 中的灰度值。常见做法是先阈值化再保存确保二分类 mask 严格只有两个灰度值。3.4 训练日志中每个指标的含义与解读方法训练日志通常包含每轮的训练损失、验证损失、各类别 iou、全局准确率等。重点看这三个指标全局像素准确率为预测正确的像素数占全部像素数的比例这个指标容易虚高因为背景像素占比大哪怕只预测背景也能得到不错的准确率。miou 是各类别 iou 的平均该指标更均衡。单独类别的 recall、precision 用来发现类别不均衡问题如果某一个类别 recall 极低基本可以判定是该类样本量太少。日志中还有每个类别的 iou 数值观察这个数值从第 1 epoch 到第 50 epoch 的上升幅度如果训练到后期每个类别的 iou 还在明显上升说明加长 epoch 确实能进一步涨点这与摘要结论是吻合的。4. 避坑与排查细胞核分割中常见的项目级问题4.1 predict 之后得到全黑图像现象推理完输出的预测结果全黑看不到任何分割目标。原因最常见是加载的权重文件与当前推理类别数不匹配。比如权重是二分类的但 inference 目录里放的图是灰度图或做了不恰当的归一化导致所有像素落到背景类。其次是输入图像的通道顺序问题如果图像被误读成单通道灰度图模型仍然能跑但特征全部偏移。解决先确认权重文件来自 run_results 中的最好权重确认训练时 mask 的灰度值配置。对于输入图以 RGB 彩色读入并归一化到 0-1 区间不要用灰度图直接送进网络。如果模型支持单通道输出某些库的下采样会把通道数压缩可以检查输入 tensor 是否经统一预处理。4.2 多尺度训练导致显存溢出现象训练中途报 CUDA out of memory并不是一开始就溢出而是某个随机缩放后的较大尺寸样本崩溃。原因多尺度训练把输入随机缩放到设定尺寸的 0.5-1.5 倍意味着 batch 中每张图的尺寸不同最大尺寸是设定值的 1.5 倍。如果设定尺寸是 512最大图的边长达 768特征图相应变大显存占用增长是非线性的。解决把训练批次减小一半或者把设定尺寸调小比如从 512 改成 384。还有一个技巧是在代码里做尺寸裁剪保证最长边落在某个范围。对于只有一块 8G 显存的显卡设定尺寸 384 配 batch size 4 是比较稳妥的组合。4.3 mask 出现了非标准灰度值现象训练时类别数莫名变多比如二分类项目突然输出 3 个或更多 channel。原因mask 中有像素值不是标准的 0 和 255而是如 128、254、1 等中间值。这通常来自标注工具导出时的抗锯齿、JPEG 压缩伪影或画图工具修改痕迹。解决训练前让 compute_gray 机制先跑一遍并观察输出的类别数量。如果类别数多于预期对 mask 做阈值处理强制二值化或者检查标注导出设置换用 PNG 无损格式保存。从那以后我每次准备新数据集时都会在计算灰度值后额外打印一份各类像素数量分布防患于未然。4.4 训练损失下降但 miou 不涨现象训练损失在持续下降但验证集 miou 到 20 个 epoch 后就不动了甚至偶尔往下走。原因典型的多类别不均衡和数据增强过度。多尺度缩放虽然增加了泛化能力但某些尺寸下细胞核的数量极多小目标被严重压缩模型学到的是大核特征小核的 iou 提不上去。另一种可能是类别权重没配损失函数里背景类和前景类对梯度的贡献等同前景类存在小幅欠拟合。解决先给损失函数中的前景类别加权重常用做法是权重设为背景类的两倍或者使用 focal loss 替代标准交叉熵。再观察每个类别的 iou如果小核的 recall 过低可以把缩放倍率改成 0.8-1.2 区间缩窄尺度变化范围。4.5 loss 曲线恢复正常但预测结果呈网格状伪影现象预测输出的分割图上有明显的块状或网格效应边缘呈锯齿状。原因数据在预处理阶段可能做了随机 resize推理时原图直接输入模型看到了和训练时略有差异的尺度导致感受野覆盖不一致。Unet 结构经过多层池化后特征图的感受野位置偏移在小尺寸输入上表现尤其明显。解决推理前把输入图 resize 到训练时的标准设定尺寸而不是直接原图尺寸输入必要时也采用多尺度推理取平均。这是 Unet 系列的老毛病——训练多尺度但推理单尺度效果会打折扣。5. 从二分类切到多分类灰度值映射与输出层改造的完整方法5.1 为什么要从二分类扩到多分类摘要中指出该项目支持多类别分割只要类别数量在计算灰度值时自适应即可。在实际病理应用中往往不只是分割细胞核还需要同时分出细胞质、鳞状上皮细胞、中性粒细胞等结构。这类多分类标签的 mask 图里每个类别固定一个灰度值——比如背景为 0细胞核为 64细胞质为 128鳞状上皮为 192——网络输出通道直接等于灰度值类别数。从二分类扩展的好处是不需要重写任何训练代码只需要替换数据集的 mask 文件并重新执行训练脚本。utils 中的 compute_gray 函数会自动识别新出现灰度值的数量并调整输出通道这种设计解决了很多教程中手动改网络输出层的痛点。5.2 多类别 mask 制作时的规则与规范制作多分类 mask 时有三条硬性规定需要遵守。第一每个类别必须使用固定且唯一的灰度值同一类所有 mask 中该值不变更不要出现两个类别共用灰度值的情况。第二mask 统一保存为 PNG 格式PNG 是无损压缩不会像 JPEG 那样改变像素值。第三灰度值之间要留间隔常用方案是 0、85、170、255 四个值均匀分布避免后续做颜色映射时混淆。很多标注工具导出 mask 时会自动把标签映射成 RGB 彩色保存而代码读的是单通道灰度图遇到这种情况一定要在导出时选择 label 模式而不是 RGB 模式。如果拿到的是 RGB 标签图需要手动转换import cv2 import numpy as np # 读取 RGB 标签图 img cv2.imread(label.png) # BGR 通道 # 将 RGB 标签转换为灰度索引 gray np.zeros((img.shape[0], img.shape[1]), dtypenp.uint8) # 例如背景(0,0,0) - 0核(255,0,0) - 1质(0,255,0) - 2 gray[np.all(img [0, 0, 0], axis-1)] 0 gray[np.all(img [255, 0, 0], axis-1)] 1 gray[np.all(img [0, 255, 0], axis-1)] 2 cv2.imwrite(mask_gray.png, gray)这段代码用像素级 RGB 比较完成颜色到类别索引的映射。注意 OpenCV 读图默认是 BGR 顺序所以比较时要按 BGR 来写。类别 0、1、2 分别对应灰度值代码中不设置缩放因子的话保持原欧氏距离运行后 mask_gray.png 就是能直接被项目读取的训练标签。5.3 多类别分割中的调参与参数设定的差异点多分类相比二分类有几个地方要改。第一是数据划分策略如果某类样本特别少建议固定随机种子并采用分层采样确保训练集、验证集每个类别都有。第二是训练轮数需要适当增加多分类收敛速度会慢于二分类因为输出空间变大、类别间的边界更难学。第三是学习率可以调低一些比如初始学习率减半防止类别间梯度竞争导致震荡。另一个比较重要的是评价指标的选择。二分类里 miou 是一个直观指标多分类里全局准确率会被背景主导。阅读日志时更值得关注的是每个类别的 recall特别是前景小类别的 recall。如果某个类 recall 明显低于其他类优先检查该类的样本量和质量其次考虑在数据增强中对该类做针对性处理。5.4 推理脚本在多分类下的使用训练完多分类模型之后predict 脚本仍然不需要改参数它会根据权重文件中的输出通道自动调整。输出掩膜中是每个类别的灰度值如果你要在视觉上展示不同类别一般会在展示脚本里给每个灰度值映射一个颜色。# 将预测掩膜渲染为彩色图像 import numpy as np import cv2 # 定义类别颜色映射 color_map { 0: (0, 0, 0), # 背景 黑色 1: (0, 0, 255), # 细胞核 红色 2: (0, 255, 0), # 细胞质 绿色 3: (255, 0, 0), # 其他 蓝色 } pred cv2.imread(predicted_mask.png, cv2.IMREAD_GRAYSCALE) height, width pred.shape render np.zeros((height, width, 3), dtypenp.uint8) for gray_val, color in color_map.items(): render[pred gray_val] color cv2.imwrite(visualized_result.png, render)这里用最原始的逐像素方式做颜色映射。渲染后的图像适合论文插图或做定性对比实验能直观看到每个类别的输出区域。6. 权重选择策略与生命周期管理训练完成后要做的四件事训练完成并拿到权重文件后直接进入推理阶段不算结束。以这个项目的 run_results 目录为例里面往往有最优权重和最后一次权重两者要区分使用。最优权重是验证集 miou 最大时保存的模型适合推理以及后续微调最后一次权重则用于对比观察模型在训练集和验证集上的差距判断是否过拟合。第一件事是记录训练日志末尾的指标汇总数据包括全局像素准确率、miou、各个类别的 recall 和 precision把它们整理到实验记录表里。同一份数据换一次超参数就再记录一次积累十组以上后你能清楚地看到多尺度缩放区间和学习率衰减策略对最终分割效果的影响趋势。第二件事是用随机抽取的测试图做一次预测并统计该图上每个类别的 recall 和 precision。这些指标应该和其他测试图保持同一水平。如果某张图异常低大概率是这张图的染色或成像风格与训练数据差异显著医学图像里这种例子很常见。我会把这类特殊图片单独归入 hard set后续做针对性增强或微调从那里开始模型有一个持续迭代的闭环。第三件事是保存训练时的配置信息。包括输入尺寸、缩放区间、学习率初值、batch size、epoch 数和数据目录结构说明。这些信息写入一个简单的 config.txt 放在 run_results 下方便后续重新加载权重时对比。因为权重文件只包含模型参数不包含超参数没有配置文件的话隔两周再回来做实验会很混乱。第四件事是决定是否做权重压缩或转换。PyTorch 格式的权重直接用于研究最方便但如果你要在工程环境部署比如用 OpenCV DNN 模块推理你需要把权重转换到 ONNX 格式。python torch2onnx.py --weights run_results/best_model.pth --output model.onnx --input-size 384 384转换成功之后可以用 ONNX Runtime 直接推理。注意 ONNX 转换时需要指定输入尺寸如果训练用了多尺度转换时选择一个居中尺寸转换完成后最好用一张真实测试图对比原权重和 ONNX 模型的输出差异差异小于 1% 像素就算合格。如果推理节点使用 float16 量化还需额外检查精度损失细胞核边缘检测对量化误差比较敏感。做完这四件事一份训练好的权重文件就有了相对完整的生命周期管理。后续不管是继续训、部署还是发给其他人复现都有据可查。这个项目当前的 50 个 epoch 结果已经不错但从 miou 曲线图来看如果继续训练到 150 个 epoch全局像素准确率和 miou 大概率还能再上一层。多尺度训练配合 Resnet backbone 的初始化特征提取能力属于性价比很高的基础配置方案尤其在细胞核这类相对规则的目标上稳定性值得信任。希望这份拆解笔记对你有用。我从这个项目里学到最有价值的一点就是类别数由 mask 自动推导的设计把数据、标签和网络结构三者之间的耦合降到了很低换数据集、换分类数时都不用再改动核心代码整套流程也因此有了工程层面的可复制性——希望你把它用在自己手头的数据上时也能有这个感觉。本文还有配套的精品资源点击获取
返回列表