简介:面向医学图像分割学习者与深度学习实践者的 U-Net 实战项目包,围绕 DRIVE 眼底血管图像分割任务,提供从数据组织、模型搭建、训练验证到预测评估的一套流程,适合希望理解医学图像分割与卷积网络落地方式的高校学生、研究者及算法入门者。压缩包为 rar 格式,约 120.72MB;下载页未列出文件总数与类型明细,包内主要围绕模型定义、训练脚本、数据处理、预测脚本、权重文件及结果目录等环节展开,可支撑复现实验与二次修改。已有 335 人学习。项目价值在于把血管边界标注数据的读取与增强、U-Net 下采样与上采样路径及跳跃连接、损失计算和权重更新、测试集预测与结果整理串联起来,帮助读者理解低层细节与高层语义如何融合以提高分割精度,并积累医学影像任务从训练到评估、从实验到辅助诊断研究的完整工程经验。
1. 眼底血管分割:20 张训练图撑起的 U-Net 方案,难点从来不在网络结构
DRIVE 这个眼底数据集有个反直觉的地方:全部只有 40 张 565×584 的彩色眼底图,官方划分里带专家标注的训练图 20 张,测试图 20 张。这意味着任何一个想拿它做血管分割的人,开局就站在小样本的悬崖边上——网络结构抄 U-Net 就够,真正决定成败的是数据怎么切、增强怎么加、指标怎么算、阈值怎么定。带过的几届做眼底分割的同学里,十有八九卡在「训练 loss 掉得很漂亮,一可视化血管全断」这一步,而不是卡在写不出 U-Net。下面这套方案按需求、设计、实现、调参、排查、验收六段走完,学生可以直接照着交毕设,工程侧也能把它当成一个小样本医学分割的模板改。
2. 任务拆解与总体方案:五段流水线与三个必须定死的接口
眼底血管分割本质是逐像素二分类:输入一张彩色眼底图,输出一张同尺寸的概率图,概率大于阈值的位置判为血管。任务描述只有一句话,但真落到工程上,得把它拆成五段互相咬合的流水线,每一段的输出格式都必须提前定死,否则后面调参时你连问题出在哪个环节都说不清。我一般把流水线拆成:数据读取与视场裁剪 → 预处理与增强 → U-Net 前向 → 滑窗拼图 → 阈值与后处理。五个环节之间靠三个接口衔接:张量的形状和取值范围、损失函数吃的是什么形式的标签、评估时用哪一块像素参与统计。
2.1 数据侧:20 张训练图怎么撑起一次可复现的实验
小样本项目最容易翻车的一步是划分。官方给了 20 张训练图和 20 张测试图,不少人图省事,把 40 张混在一起随机 split 成 8:2,这种做法的测试指标没有任何参考价值,因为同一张图的像素高度相关,模型见过它的一部分就等于见过全部。通常的做法是严格守住官方划分:20 张训练图内部再做 5 折交叉验证,每折留 4 张当验证集,训练 16 张;20 张测试图只在最后跑一次,跑完就不再回头调参。
第二件事是把「参与计算的像素」界定清楚。眼底图是圆形视场,四角是纯黑背景,如果直接整图算 loss,模型很快学会「黑色区域不是血管」这种毫无信息量的规律,指标会虚高。所以标签和预测都要乘上视场掩膜,损失和指标只在掩膜内统计。掩膜可以从原图直接算:三个通道求和后做阈值,再取最大连通域填充孔洞,就能得到稳定的圆形区域,不需要额外的标注文件。
第三件事是确定一个固定分辨率。原图 565×584 长宽不整除,直接送进网络会因为下采样 4 次出现尺寸对不齐。统一 resize 到 512×512 是一个折中,代价是细血管被压细了一点;如果导师对微小血管的连续性要求高,就别 resize,改成 512 的滑窗在原始分辨率上切 patch,推理时再拼回去。两种做法我都跑过,前者训练快、显存省,后者细血管召回更高,毕设级别用前者足够,论文里想写细血管分析就上后者。
2.2 模型侧:收缩-扩张结构与跳跃连接各管什么
U-Net 的结构已经不新鲜,但在眼底血管这个场景里,每一层为什么存在值得说清楚,答辩时这是必问项。收缩路径(下采样)负责扩大感受野,让网络在深层能判断「这一段是不是一条连续的管状结构」,而不是只看局部几个像素的灰度;扩张路径(上采样)负责把语义还原回像素位置;跳跃连接把浅层的高分辨率特征直接拼到对应层,作用是保住细血管的边界位置——没有跳跃连接,1 到 2 个像素宽的血管在深层特征图里基本已经被池化抹掉了。
具体配置上,我一般用 4 次下采样、base 通道 32 的轻量版本:编码器通道依次 32、64、128、256,瓶颈 512,解码器对称还原。这个规模在单张 11GB 显存的卡上,batch 8、512×512 输入能跑得很稳;如果显存只有 6GB,把 base 降到 16 或者改用 256 的 patch,效果掉得不多。卷积块用「卷积-BN-ReLU」重复两次,上采样用双线性插值而不是转置卷积,原因是转置卷积在医学图像上容易产生棋盘格伪影,细血管旁边一旦出现网格状纹理,视觉上非常难看。
提示:跳跃连接用通道拼接(concat)而不是相加。相加会强制浅层和深层特征语义对齐,在小数据集上收敛更慢;拼接把选择权交给后面的卷积层,实践中更稳。
2.3 评估侧:像素级指标与 A/V 分类的取舍
DRIVE 官方其实给了两套评测口径:一套是像素级二分类,算 Accuracy、Sensitivity、Specificity、F1、AUC;另一套是更贴近临床的 A/V(动静脉)分类,需要额外区分血管类型。毕设里绝大多数人只做前者,这是合理的,因为后者需要额外的标注信息,工作量翻倍而收益有限。但有一点要提前想清楚:像素级 Accuracy 在这个任务上几乎没有意义,血管像素占比通常只有 7% 到 12%,模型全预测为背景也能拿到 0.9 左右的 Accuracy。汇报时必须把 F1、Sensitivity、Specificity、AUC 一起摆出来,单拎 Accuracy 会被追问。
还有一个常被忽略的细节:评估时要不要排除视场边界的一圈像素。官方评测里会忽略视场外和边界附近的一小圈,因为那里标注一致性差。自己实现时,简单的做法是把视场掩膜向内腐蚀几个像素再统计,能避免边界效应把指标拉低一截。
3. 关键模块实现:预处理、损失函数与滑窗推理的代码落地
这一章把三个最容易写错、也最影响最终指标的模块摊开写。代码用的是 PyTorch 加 OpenCV,都是做这个项目绕不开的依赖,参数我按实际跑通的值给,读者改数据集时只需要动其中的尺寸和阈值。
3.1 预处理与增强:CLAHE 与绿通道的选择依据
彩色眼底图有三个通道,血管在不同通道里的对比度差别很大。红通道最容易饱和,视网膜背景和血管的灰度差被压得很小;蓝通道噪声大;绿通道里血红蛋白吸收率最高,血管和背景的对比度最好,所以绝大多数眼底分割工作都只在绿通道上做,或者把绿通道当主通道、另外两个通道做辅助。这不是玄学,是把原图三个通道拆开各画一次直方图就能看出来的事。
import cv2 import numpy as np def preprocess(img_bgr, fov_mask, size=512, clip_limit=2.0): """眼底图预处理:绿通道 + CLAHE + 视场掩膜 + 归一化""" # 1) 取绿通道:血管与背景对比度最高 green = img_bgr[:, :, 1] # 2) CLAHE 分块直方图均衡,clipLimit 控制过增强程度 clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=(8, 8)) green_eq = clahe.apply(green) # 3) 视场外压成 0,防止模型学习圆形边界 green_eq = green_eq * (fov_mask > 0).astype(np.uint8) # 4) 缩放:缩小用 INTER_AREA,比双线性更少摩尔纹 resized = cv2.resize(green_eq, (size, size), interpolation=cv2.INTER_AREA) # 5) 归一化到 [0,1],与 sigmoid 输出同量纲 return resized.astype(np.float32) / 255.0 def build_fov_mask(img_bgr, thresh=15): """从原图直接估计视场掩膜,不需要额外标注""" gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) mask = (gray > thresh).astype(np.uint8) # 取最大连通域并填洞,去掉睫毛、反光造成的碎片 cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) out = np.zeros_like(mask) cv2.drawContours(out, [max(cnts, key=cv2.contourArea)], -1, 1, -1) return out这段代码里有两个参数值得单独说。clip_limit控制对比度增强的强度,设成 2.0 时细血管变清楚但噪点也开始被放大,设到 4.0 以上背景会出现明显颗粒,模型会把颗粒误判成血管,我一般锁在 2.0 到 3.0 之间试两档。tileGridSize是分块大小,8×8 意味着把图切成 64×64 的小块各自均衡,块太小会放大噪声,块太大就退化成全局均衡,8×8 是常用起点。
数据增强方面,几何变换用随机水平翻转、随机旋转 0 到 360 度、随机缩放 0.9 到 1.1,这三个对眼底图都安全,因为眼底图本身没有固定的上下方向。弹性形变要谨慎,sigma设大了会把血管扯成断断续续的片段,标签跟着一起变形,模型学到的是错误的连续性,我一般把形变强度压在很小的范围,或者干脆不用,改用亮度、对比度、gamma 的随机扰动来补多样性。小样本场景下,增强的种类不是越多越好,能保持解剖结构合理的增强才有意义。
3.2 网络与损失:U-Net 骨架与 Dice+BCE 混合损失
网络部分直接给一个可复现的最小实现,重点是通道数和解码器的拼接顺序,写错一处就会出现尺寸不匹配。
import torch import torch.nn as nn import torch.nn.functional as F class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() # 两次 3x3 卷积,padding=1 保证尺寸不变 self.block = nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding=1, bias=False), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), nn.Conv2d(out_ch, out_ch, 3, padding=1, bias=False), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), ) def forward(self, x): return self.block(x) class UNet(nn.Module): def __init__(self, in_ch=1, base=32): super().__init__() chs = [base, base*2, base*4, base*8, base*16] # 32/64/128/256/512 self.enc = nn.ModuleList([ DoubleConv(in_ch if i == 0 else chs[i-1], chs[i]) for i in range(4) ]) self.bottleneck = DoubleConv(chs[3], chs[4]) self.dec = nn.ModuleList([ DoubleConv(chs[i] + chs[i+1], chs[i]) for i in range(3, -1, -1) ]) self.pool = nn.MaxPool2d(2) self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=False) self.head = nn.Conv2d(chs[0], 1, 1) # 1x1 卷积压到单通道 def forward(self, x): skips = [] for enc in self.enc: x = enc(x) skips.append(x) # 保存浅层高分辨率特征 x = self.pool(x) x = self.bottleneck(x) for i, dec in enumerate(self.dec): x = self.up(x) x = torch.cat([x, skips[-(i+1)]], dim=1) # 跳跃连接:通道拼接 x = dec(x) return self.head(x)base=32时整网参数量大约 7.8M,单卡 batch 8 训练没有任何压力。如果换成base=16,参数量降到 2M 左右,指标通常会掉 1 到 2 个点,但推理速度快一倍,做实时演示时可以用这个版本。head输出的是 logits,不要在里面加 sigmoid,sigmoid 放到损失函数和推理里,否则会出现数值不稳定。
损失函数是这个项目的第二个关键点。血管像素只占 7% 到 12%,纯 BCE 会让模型倾向于全预测背景,训练前期 loss 掉得很快,但 Sensitivity 一直上不去。混合损失的做法是 BCE 提供稳定的逐像素梯度,Dice 直接优化前景重叠度,两者各占一半。
def dice_loss(logits, target, eps=1e-6): """软 Dice:直接对概率图计算,可反向传播""" prob = torch.sigmoid(logits) inter = (prob * target).sum(dim=(2, 3)) union = prob.sum(dim=(2, 3)) + target.sum(dim=(2, 3)) return 1 - ((2 * inter + eps) / (union + eps)).mean() def hybrid_loss(logits, target, bce_w=0.5): bce = F.binary_cross_entropy_with_logits(logits, target) return bce_w * bce + (1 - bce_w) * dice_loss(logits, target)bce_w我一般固定 0.5,试过 0.3 和 0.7,差异在 0.5 个点以内,没必要细调。真正的收益来自「有没有用 Dice」,而不是「Dice 占多少」。eps不能省,全背景的 patch 上 Dice 分母会接近 0,没有 eps 会直接 NaN。
3.3 推理与后处理:重叠滑窗、阈值搜索与连通域清理
测试图和训练图如果分辨率一致,直接整图前向也能出结果;但整图前向在显存紧张或者分辨率更高时会顶不住,滑窗推理是更通用的做法。核心是窗口之间留重叠,重叠区取平均,避免拼接缝。
import torch @torch.no_grad() def sliding_window_infer(model, image, patch=256, stride=128, device='cuda'): """overlap-tile 推理:重叠区概率取平均,消除拼接缝""" model.eval() _, H, W = image.shape prob = torch.zeros(H, W, device=device) count = torch.zeros(H, W, device=device) for y in range(0, H, stride): for x in range(0, W, stride): # 边界回退:最后一块贴边,保证窗口不越界 y0, x0 = min(y, H - patch), min(x, W - patch) patch_img = image[:, y0:y0+patch, x0:x0+patch].unsqueeze(0).to(device) p = torch.sigmoid(model(patch_img))[0, 0] prob[y0:y0+patch, x0:x0+patch] += p count[y0:y0+patch, x0:x0+patch] += 1 return (prob / count.clamp(min=1)).cpu().numpy()stride取 patch 的一半是常用配置,重叠 50%,拼出来的图基本看不到缝;stride等于 patch 时重叠为 0,速度快但边界处概率会跳变,细血管在缝上容易断。min(y, H-patch)这一行是防止最后一块越界,漏了它会直接报越界错误,属于血泪经验级别的细节。
后处理分两步。第一步是阈值搜索:不要固定 0.5,在验证集上扫一遍 0.1 到 0.9,取 F1 最高的那个作为最终阈值,通常落在 0.4 到 0.6 之间,个别训练不充分的情况会低到 0.3。第二步是连通域清理:把面积小于 20 个像素的孤立前景块去掉,再把视场掩膜外的像素强制置 0。这两步加起来通常能把 F1 提 1 到 2 个点,成本几乎为零。
4. 训练调参:从过拟合到收敛的四组对照实验
小数据集训练最怕的不是不收敛,而是「训练集 Dice 0.95、验证集 0.78」这种过拟合,以及「两个都 0.9 但可视化一塌糊涂」这种指标欺骗。这一章给的是四组可复现的对照实验配置,按顺序做完,基本能把模型调到一个可交付的状态。
4.1 训练配置与显存预算
先把底座配置定下来,后面所有对照都基于它。优化器用 Adam,初始学习率 1e-3,权重衰减 1e-5;学习率调度用 ReduceLROnPlateau,监控验证集 F1,patience 设 10,衰减系数 0.5;batch size 8,训练 150 个 epoch,前 5 个 epoch 做 warmup。输入 512×512 单通道,base 通道 32,这个组合在 11GB 显存上占用大约 6GB,留出的余量够开混合精度。
| 配置项 | 取值 | 说明 |
|---|---|---|
| 输入尺寸 | 512×512×1 | 绿通道,视场外置 0 |
| batch size | 8 | 显存不足时降到 4 并同步降学习率 |
| 初始学习率 | 1e-3 | Adam,warmup 5 epoch |
| 训练轮数 | 150 | 配合早停,实际常在 90 到 120 停 |
| 损失函数 | 0.5×BCE + 0.5×Dice | 缓解前景稀疏 |
| 验证策略 | 5 折交叉验证 | 每折 4 张验证图 |
混合精度训练在小显存机器上是必开项,torch.cuda.amp能把显存占用压掉三成左右,速度提升 20% 上下。要注意 Dice 损失在 fp16 下求和容易溢出,把损失计算那段强制转成 fp32 再算,能避免偶发的 NaN。
4.2 学习率和归一化层的两组对照
第一组对照是学习率。1e-3 配 Adam 在多数情况下能收敛,但如果发现前 10 个 epoch 验证 F1 一直在 0.3 附近打转,多半是学习率偏大导致在鞍点附近震荡,降到 3e-4 再跑一次通常就正常了。反过来,如果 loss 下降极其缓慢,且训练集 F1 长期低于 0.7,可以试试 2e-3 配 warmup,但超过 3e-3 基本都会发散。
第二组对照是归一化层。U-Net 原版用 BatchNorm,在 batch 8 的情况下统计量还算稳定;但如果显存不够把 batch 降到 2 甚至 1,BatchNorm 的均值方差估计会非常抖,表现为训练 loss 剧烈震荡、验证指标上不去。这种情况换成 InstanceNorm 或者 GroupNorm(组数 8)会明显稳定,代价是收敛稍慢一点。我一般的判断标准是:batch 大于等于 8 用 BatchNorm,小于 8 直接上 InstanceNorm,不用纠结。
还有一个容易被忽略的点是数据增强的强度。增强开太猛,训练集上的 loss 会长期偏高,看起来像欠拟合,实际是任务变难了;增强开太弱,两三个 epoch 就过拟合。判断方法是看训练集和验证集的 F1 差距,差距在 5 个点以内说明增强合适,超过 10 个点就该加强增强或者加 dropout。
4.3 小数据集的早停与正则策略
20 张图训练,早停不是可选项而是必需项。监控指标用验证集 F1 而不是 loss,因为 loss 在前景稀疏的情况下对指标不敏感。patience 设 15 到 20 个 epoch,连续这么多次验证 F1 没有提升就停,同时保存验证 F1 最高的那组权重,而不是最后一个 epoch 的权重。这一点很多人会写错,代码里torch.save(model.state_dict(), ...)放在循环末尾,存下来的是最后一轮,往往已经过拟合了。
正则方面,dropout 在 U-Net 的瓶颈层加 0.2 到 0.3 就够,加在浅层会破坏细血管特征。权重衰减 1e-5 是个安全值,调到 1e-4 时验证指标通常会掉。还有一个便宜好用的手段是测试时增强(TTA):推理时把图水平翻转、垂直翻转各跑一次,三张概率图取平均,F1 一般能再提 0.5 到 1 个点,代价是推理时间翻三倍,答辩演示时用不用都行,写进论文里是个加分项。
5. 常见问题排查:指标虚高、血管断裂与全黑预测的五个坑
这一章按「现象 → 原因 → 解决」的格式写五个在实际项目里出现频率最高的问题,基本覆盖了从训练到推理的整条链路。出现这些问题不丢人,能定位到原因才是关键。
5.1 指标类问题:验证 F1 0.85 但可视化血管全断
现象很典型:训练日志里验证集 F1 稳定在 0.82 到 0.86,AUC 0.97 以上,一切看着都很好,但把预测图叠到原图上一看,主干血管还在,细小分支断成一段一段。原因通常有两个,一是评估时用了整图统计,背景像素占了九成,把细血管的漏检稀释掉了;二是阈值取 0.5 偏高,模型对细血管的置信度本来就低,0.5 一刀切下去,概率 0.45 的细血管全被砍掉。
解决办法是把评估范围收窄到视场掩膜内,并且单独统计细血管区域的召回率——可以按标签的血管宽度做分层,宽度小于 3 个像素的算细血管,单独算一个 Sensitivity。阈值则在验证集上做搜索,取 F1 最高的那个,很多情况下这个值会落在 0.35 到 0.45 之间,细血管的连续性会有肉眼可见的改善。如果搜完阈值还是断,说明模型本身对小目标的学习不足,回头去检查输入分辨率是不是压得太狠。
5.2 预测类问题之一:输出全黑或者全白
全黑指的是概率图几乎全接近 0,全白则相反。全黑常见于三种情况:输入没有归一化到 [0,1],像素值还是 0 到 255,卷积核直接爆掉;视场掩膜写反了,把视场内的像素全乘成了 0;标签在预处理时被错误地除以 255 两次,变成全 0。逐项排查的方法很简单,在 dataloader 里抽一个 batch 打印image.max()、image.min()、target.mean(),这三个数一对就基本能定位。
全白多半是损失函数的问题。如果 Dice 的eps加在了分母外面而不是里面,或者 BCE 的 pos_weight 设得过大,模型会倾向于把所有像素判成正类来降低损失。检查pos_weight是不是被误设成了几十,正常情况下 0.5 到 1.0 就够,缓解不平衡靠 Dice 而不是靠 pos_weight 硬拉。
5.3 预测类问题之二:视场边界出现一圈假的血管
现象是预测图在圆形视场的边缘出现一圈明显的高亮环,肉眼看根本不是血管。原因是没有做视场掩膜,或者掩膜只作用在输入上、没作用在标签和输出上。模型在训练时看到视场外是纯黑,视场内是眼底图,两者交界处形成一个极强的边缘信号,网络把这个边缘当成了「管状结构」学到了。解决很直接:预处理时把视场外置 0,标签同样置 0,损失计算时把视场外像素的权重置 0,推理完再把视场外强制置 0。三处都做全,这个环就消失了。
5.4 训练类问题:loss 正常但验证指标不涨
训练 loss 从 0.8 平稳降到 0.2,验证 F1 却一直在 0.6 附近横盘。这种情况先看数据划分有没有泄漏——如果验证集里的图和训练集是同一个人、同一只眼的不同裁剪,指标会虚高;反过来如果是把难图全划进了验证集,指标会偏低。5 折交叉验证能缓解这个问题,每折的验证 F1 取平均,比单次划分可信得多。
如果划分没问题,就看学习率是不是太大导致在最优解附近震荡,把 ReduceLROnPlateau 的 patience 从 10 缩到 5,让学习率更早衰减。还有一个隐蔽原因是 BatchNorm 在验证时用了当前 batch 的统计量,如果验证的 batch size 和训练不一致,跑出来的指标会漂,验证时固定 batch size 或者切到 eval 模式后再跑,能排除这个干扰。
5.5 工程类问题:训练能跑,换台机器就复现不出来
现象是本地跑出 F1 0.83,换台机器或者隔几天再跑,同样的代码出来 0.79。原因集中在三处:随机种子没固定全,Python、NumPy、PyTorch、CUDA 四层的种子都要设;数据增强用了多进程 dataloader,worker 之间的随机状态互相干扰,需要在 worker_init_fn 里按 worker id 重设种子;cuDNN 的 benchmark 模式会让卷积算法选择不确定,做复现实验时把它关掉。
import random, numpy as np, torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) # 复现优先:关掉非确定性算法 torch.backends.cudnn.benchmark = False torch.backends.cudnn.deterministic = True def worker_init_fn(worker_id): # 每个 dataloader worker 用不同但确定的种子 np.random.seed(42 + worker_id)关掉 benchmark 会让训练慢 5% 到 10%,但换来的可复现性在写论文、做答辩演示时值这个价。定稿前把种子固定跑三遍,三次的验证 F1 差距在 0.3 个点以内,才算这套实验站得住。
6. 验收与答辩:怎么证明这套 U-Net 真的分出了血管
做到这一步,模型大概率已经能出图了,但「能出图」和「能交差」之间还差一套验收流程。答辩现场最常出现的尴尬是:你放了四张效果图,评委随手点开一张说「这条血管断了」,你没法解释这是个别现象还是普遍问题。避免这种被动,靠的是提前把验收做成一张表,现场按表说话。
我一般让学生准备三样东西。第一样是交叉验证的汇总表,5 折的 F1、Sensitivity、Specificity、AUC 各一行,给出均值和标准差,标准差比均值更能说明模型稳不稳。
| 折次 | F1 | Sensitivity | Specificity | AUC |
|---|---|---|---|---|
| 第 1 折 | 0.812 | 0.786 | 0.982 | 0.976 |
| 第 2 折 | 0.803 | 0.771 | 0.980 | 0.973 |
| 第 3 折 | 0.819 | 0.792 | 0.984 | 0.978 |
| 第 4 折 | 0.807 | 0.775 | 0.981 | 0.975 |
| 第 5 折 | 0.815 | 0.789 | 0.983 | 0.977 |
| 均值±标准差 | 0.811±0.006 | 0.783±0.008 | 0.982±0.001 | 0.976±0.002 |
第二样是可视化对比图,选三张有代表性的测试图:一张主干清晰的大血管、一张细分支密集的、一张有病灶遮挡的。每张图放四联:原图、金标准、预测概率热力图、阈值二值化结果。四联图的说服力远高于单张二值图,因为热力图能看出模型在哪里犹豫,评委看到细血管处的概率是 0.4 而不是 0,自然会认可这是阈值问题而不是模型问题。
第三样是现场演示脚本,把「读一张图 → 预处理 → 前向 → 阈值 → 后处理 → 叠图」压成一条命令,跑一次控制在 10 秒以内。脚本里不要现场训练,也不要现场装环境,所有依赖提前装好,模型权重文件单独放一个目录,路径写成相对路径。
答辩时被问到最多的三个问题,提前准备答案:为什么用 U-Net 不用别的分割网络,答案是跳跃连接对小目标边界的作用,配合一张有无跳跃连接的对比图;为什么 F1 只有 0.81,答案是这个数据集上专家之间的一致性本身就在 0.79 上下,0.81 已经在合理区间;阈值为什么不是 0.5,答案是前景稀疏时 0.5 会系统性压低召回,验证集搜索出的阈值更有依据。
从那以后我每次带人做这类小样本分割项目,都强制在训练开始前先把验收表的三样东西列出来,再倒推需要保存哪些中间结果。这个习惯省下来的返工时间,比调参本身多得多。希望帮到你。
本文还有配套的精品资源,点击获取