资讯详情

资讯详情

Keras-YOLOv3息肉检测实战:从数据标注到推理全流程

简介这份资源是面向医疗影像分析与深度学习入门者的息肉目标检测实战项目基于Python与Keras-YOLOv3实现适合具备一定神经网络基础、希望将目标检测落地到医学图像场景的开发者。压缩包共41个文件约149KB以25个Python脚本为核心辅以10个txt标签与锚点配置、2个cfg网络结构文件、2个md说明文档及字体等辅助资源覆盖数据预处理、模型构建、训练、评估到推理部署的完整链路。项目围绕YOLOv3多尺度预测与Darknet架构展开包含数据标注转换、损失函数定义、mAP评估、锚点聚类及可视化等模块目录按数据、模型、工具函数分层组织便于按需查阅与二次开发。目前已有378人学习可作为医学图像目标检测的练手模板帮助读者理解从数据准备到模型微调的全流程并在此基础上迁移到其他检测任务。1. 息肉检测为什么值得用 Keras-YOLOv3 跑一遍肠镜画面里息肉往往只占几百个像素边缘还和肠壁黏膜糊在一起医生盯着屏幕找漏检率一直下不来。把目标检测模型塞进这套流程核心诉求就一个在近实时帧率下把可疑区域框出来给医生当第二双眼睛。python keras yolov3 这个组合是很多人入门医学图像检测的第一站——Keras 把网络搭建和训练循环压到几十行YOLOv3 本身又是单阶段检测里结构清晰、改起来不心疼的经典骨架。这个标题对应的就是一套能跑通「数据标注→训练→推理→看框」全链路的工程包适合有 python 基础、想拿真实医学图像练手的工程师也适合做内镜辅助诊断方向、需要快速验证想法的人。它不解决临床级精度问题但能让你在一周内看到模型在自己数据上出框这个反馈速度才是它真正的价值。2. 把 Keras-YOLOv3 拆开看骨干、先验框与损失怎么配合2.1 Darknet-53 骨干在 Keras 里长什么样YOLOv3 的骨干是 Darknet-5353 层卷积堆叠靠残差连接把梯度稳住。在 Keras 里复现时常见做法是用Conv2D BatchNormalization LeakyReLU组成一个conv_block再按[1,2,8,8,4]的重复次数堆残差块。这里有个容易忽略的点Darknet 的 LeakyReLU 斜率是 0.1不是 Keras 默认的 0.3改错这个值前期 loss 下降会明显变慢。from tensorflow.keras.layers import Conv2D, BatchNormalization, LeakyReLU, Add def conv_block(x, filters, kernel_size, strides1): # Darknet 标准卷积单元卷积 BN LeakyReLU(0.1) x Conv2D(filters, kernel_size, stridesstrides, paddingsame, use_biasFalse)(x) x BatchNormalization()(x) x LeakyReLU(alpha0.1)(x) # 斜率必须是 0.1 return x def residual_block(x, filters): # 残差块1x1 降维 3x3 升维再与输入相加 shortcut x x conv_block(x, filters // 2, 1) x conv_block(x, filters, 3) return Add()([shortcut, x])逻辑说明conv_block把卷积、归一化、激活绑成一个可复用单元use_biasFalse是因为 BN 自带偏置再加一层是浪费。residual_block里先 1x1 再 3x3这是 Darknet 的标准瓶颈结构能压参数量。参数上filters按[32,64,128,256,512,1024]逐级翻倍strides2只在下采样时用。如果你把alpha写成默认值训练初期 loss 曲线会抖得厉害这是血泪经验。2.2 三个尺度的特征图与先验框分配YOLOv3 在三个尺度上出预测13×13、26×26、52×52。小特征图管大目标大特征图管小目标。息肉在画面里通常偏小所以 52×52 这一路的召回最关键。每个尺度上每个网格预测 3 个先验框COCO 预训练用的是 9 个聚类框但息肉尺寸分布和 COCO 差得远直接套用会明显偏。特征图尺度感受野适合目标先验框宽,高示例13×13大大息肉/近景(116,90) (156,198) (373,326)26×26中中等息肉(30,61) (62,45) (59,119)52×52小小息肉/远景(10,13) (16,30) (33,23)先验框不是随便填的我一般会用自己的标注框跑一遍 k-means把 9 个聚类中心按面积排序后分配到三个尺度。如果懒得聚类至少把 52×52 那一路的框改小否则小息肉根本匹配不上正样本训练时正样本数少得可怜模型学不到东西。2.3 损失函数里三个部分的权重YOLOv3 的 loss 由三块组成框回归、置信度、分类。框回归用 CIOU 或原始 MSE置信度和分类用二值交叉熵。Keras 实现时常见做法是把三路输出拉平后统一算 loss再按lambda_coord、lambda_noobj加权。import tensorflow as tf def yolo_loss(y_true, y_pred, lambda_coord5.0, lambda_noobj0.5): # y_true/y_pred 形状: (batch, grid, grid, anchors, 5num_classes) obj_mask y_true[..., 4:5] # 有目标的位置 noobj_mask 1.0 - obj_mask # 背景位置 # 框回归损失只算有目标的位置 xy_loss tf.reduce_sum(obj_mask * tf.square(y_true[..., :2] - y_pred[..., :2])) wh_loss tf.reduce_sum(obj_mask * tf.square( tf.sqrt(y_true[..., 2:4] 1e-6) - tf.sqrt(y_pred[..., 2:4] 1e-6))) # 置信度损失有目标 背景分开加权 obj_loss tf.reduce_sum(obj_mask * tf.square(y_true[..., 4:5] - y_pred[..., 4:5])) noobj_loss tf.reduce_sum(lambda_noobj * noobj_mask * tf.square(y_true[..., 4:5] - y_pred[..., 4:5])) # 分类损失 cls_loss tf.reduce_sum(obj_mask * tf.square(y_true[..., 5:] - y_pred[..., 5:])) return lambda_coord * (xy_loss wh_loss) obj_loss noobj_loss cls_loss逻辑说明obj_mask把有目标的位置挑出来背景位置只算置信度损失且乘lambda_noobj压低权重防止背景样本淹没正样本。wh_loss里对宽高开根号是为了让大框和小框的误差尺度接近。参数上lambda_coord5.0是原论文的推荐值lambda_noobj0.5可以按你数据里正负样本比例微调——息肉数据正样本极少这个值可以再降到 0.3 左右。注意1e-6是防止开根号出现 NaN别省。3. 从标注到训练息肉数据跑通的最小闭环3.1 标注格式与目录结构息肉数据一般来自肠镜视频抽帧标注用 LabelImg 画框导出 YOLO 格式的 txt每行class_id cx cy w h坐标都归一化到 0~1。目录我习惯这样放dataset/ images/ train/ val/ labels/ train/ val/ classes.txt # 一行一个类名息肉就写 polyp train.txt # 每行一个图片绝对路径 val.txttrain.txt和val.txt里只写图片路径代码根据路径替换images为labels再找同名 txt。这个约定能省掉大量路径拼接的麻烦后面换数据集也不用改代码。3.2 用 Keras 的 Sequence 写数据生成器息肉数据量通常不大几百到几千张一次性读进内存也行但为了后续能加增强用Sequence更稳。核心是把图片 resize 到 416×416同时把标注框按同样比例缩放。import cv2, numpy as np from tensorflow.keras.utils import Sequence class PolypGenerator(Sequence): def __init__(self, img_paths, input_shape(416,416), batch_size8): self.img_paths img_paths self.input_shape input_shape self.batch_size batch_size def __len__(self): return len(self.img_paths) // self.batch_size def __getitem__(self, idx): batch self.img_paths[idx*self.batch_size:(idx1)*self.batch_size] images, labels [], [] for p in batch: img cv2.imread(p) h, w img.shape[:2] img cv2.resize(img, self.input_shape) / 255.0 images.append(img) # 读取同名 label 文件按缩放比例调整框 label_path p.replace(images, labels).replace(.jpg, .txt) boxes [] if os.path.exists(label_path): for line in open(label_path): c, cx, cy, bw, bh map(float, line.split()) boxes.append([c, cx, cy, bw, bh]) labels.append(boxes) return np.array(images), labels # labels 后续在 loss 里编码逻辑说明__getitem__里读图、归一化、读标注返回原始框编码成网格格式的活交给 loss 前的处理函数这样生成器职责单一。参数上input_shape用 416×416 是 YOLOv3 的经典尺寸显存不够就降到 320×320但小息肉会更难检。batch_size8在 8G 显存上比较稳爆显存就减半。注意cv2.imread读进来是 BGR如果你用预训练权重要确认权重训练时的通道顺序不一致就cv2.cvtColor转一下。3.3 加载预训练权重与冻结策略从零训 Darknet-53 在几千张图上基本没戏常见做法是加载 COCO 预训练权重冻结骨干前几十层只训检测头。Keras 里可以按层名匹配加载或者用model.load_weights(..., by_nameTrue)。# 假设 model 已经搭好权重文件是 darknet53 的 h5 model.load_weights(yolov3_weights.h5, by_nameTrue, skip_mismatchTrue) # 冻结前 100 层只训后面的检测头 for layer in model.layers[:100]: layer.trainable False model.compile(optimizertf.keras.optimizers.Adam(1e-4), lossyolo_loss)逻辑说明by_nameTrue按层名匹配skip_mismatchTrue跳过形状不匹配的层比如分类数从 80 改成 1 的那几层。冻结前 100 层是经验值Darknet-53 前 100 层基本是底层纹理特征通用性强冻住能防过拟合。1e-4的学习率适合微调训 20 个 epoch 后可以解冻全部层降到1e-5再训几轮。如果 loss 一直不降先检查权重有没有真的加载进去——打印几个层的权值范数看看别问我怎么知道的。4. 推理与评估框出来了怎么判断它靠不靠谱4.1 非极大值抑制的阈值怎么定模型输出一堆框要去重。NMS 的iou_threshold直接决定留下多少框。息肉检测里同一个息肉被重复框住很常见阈值太低会误删相邻息肉太高会留一堆重叠框。def nms(boxes, scores, iou_threshold0.45, score_threshold0.3): # boxes: (N,4) xywh, scores: (N,) keep [] idx scores.argsort()[::-1] while len(idx) 0: i idx[0] keep.append(i) if len(idx) 1: break # 计算当前框与剩余框的 IoU ious compute_iou(boxes[i], boxes[idx[1:]]) idx idx[1:][ious iou_threshold] return [k for k in keep if scores[k] score_threshold]逻辑说明按分数降序每次取最高分框删掉和它 IoU 超阈值的框。参数上iou_threshold0.45是通用起点息肉密集时调到 0.5 以上避免把相邻息肉删掉。score_threshold0.3是显示阈值评估时可以降到 0.05 看召回。注意 NMS 是在每个类别内做的多类时别混在一起算。4.2 mAP 计算与验证集抽查评估不能只看 loss要看 mAP。息肉通常只有一个类mAP0.5 就够用。计算时把验证集所有图的预测框和真实框按类别收集逐张算 precision-recall 曲线再积分。指标含义息肉场景参考值mAP0.5IoU0.5 时的平均精度0.6~0.8 算可用Recall召回率优先保证漏检代价高Precision精确率0.5 以上可接受我一般会额外抽 20 张验证图把预测框画出来肉眼过一遍。mAP 高但框歪的情况太常见了尤其是小息肉框可能偏到黏膜褶皱上。这一步不能省指标是黑匣子眼睛才是后悔药。4.3 推理脚本与单张图测试训练完导出模型写个推理脚本输入单张图或视频帧输出带框的图。def detect_image(model, img_path, input_shape(416,416), score_th0.3): img cv2.imread(img_path) h, w img.shape[:2] inp cv2.resize(img, input_shape) / 255.0 inp np.expand_dims(inp, 0) preds model.predict(inp) # 三路输出 boxes decode_predictions(preds, input_shape, (h, w)) boxes nms(boxes[boxes], boxes[scores], score_thresholdscore_th) for b in boxes: cv2.rectangle(img, (b[0],b[1]), (b[2],b[3]), (0,255,0), 2) cv2.imwrite(result.jpg, img)逻辑说明decode_predictions把网格偏移、先验框、sigmoid 后的值还原成原图坐标这一步最容易写错建议单独写单元测试拿一个已知框反推验证。score_th0.3是显示阈值实际部署时可以调低到 0.2 提高召回让医生自己判断。注意 resize 回原图时坐标要乘回缩放比例别直接用 416 的坐标画框。5. 避坑与排查息肉训练里最容易翻车的五件事5.1 现象loss 降到某个值就不动了框全是乱的原因先验框和你的数据尺寸不匹配正样本匹配不上模型只能学背景。解决用 k-means 重新聚类标注框或者至少把 52×52 那路的先验框改小到 (8,8)(12,16)(20,30) 这类。改完重新训loss 会明显往下走。5.2 现象验证集 mAP 还行但实际图片上框偏移严重原因坐标解码时忘了乘回原图缩放比例或者先验框的宽高顺序写反了。解决写一个解码测试构造一个已知网格和先验框手动算一遍期望坐标和代码输出对比。宽高顺序在 YOLO 里是 (w,h)别和 (h,w) 搞混。5.3 现象训练时显存爆了batch_size 降到 1 还是爆原因输入尺寸 416×416 下Darknet-53 的中间特征图占显存很大加上三路输出8G 卡确实吃紧。解决把输入降到 320×320或者用混合精度tf.keras.mixed_precision.set_global_policy(mixed_float16)。再不行就冻结更多层减少反向传播的显存占用。5.4 现象小息肉几乎检不出来大息肉还行原因小目标在 52×52 特征图上只有几个像素经过多次下采样后信息丢失严重。解决除了调小先验框可以在 52×52 那路加一个额外的上采样融合把更浅层的特征引过来。或者简单点把输入尺寸提到 608×608代价是显存和速度。5.5 现象模型把反光、气泡也框成息肉原因训练集里负样本太少模型没见过这些干扰。解决收集一批不含息肉的肠镜帧作为背景图加入训练让模型学会区分。另外可以在 loss 里提高lambda_noobj加大对背景误检的惩罚。数据层面的问题调参只能缓解加数据才是根治。6. 把 Keras-YOLOv3 推到能用的边缘几个我常做的微调训练跑通只是起点要让息肉检测在实际画面里稳一点我一般会做三件事。第一件是换 CIOU loss 替代原始 MSE 框回归CIOU 把重叠面积、中心距离、长宽比都考虑进去小框的回归会稳不少代码上把wh_loss那段换成 CIOU 计算即可收敛速度通常快 3~5 个 epoch。第二件是加 Mosaic 增强把四张图拼成一张让模型见到更多尺度的息肉组合这个增强对小目标尤其友好但要注意拼完的框别超出边界越界框要裁掉。第三件是推理时用 TTA水平翻转一次两次预测做 NMS 融合召回能提一两个点代价是推理时间翻倍看你的帧率要求。验证模型有没有真的学到东西我有个笨办法把验证集里检出的框和真实框的 IoU 分布画出来如果大部分落在 0.5 以下说明框的位置学得不好回去查解码和先验框如果 IoU 高但漏检多说明置信度阈值或正样本分配有问题。这个分布图比 mAP 一个数字信息量大得多。最后说个习惯每次改完先验框或 loss 权重我都会先拿 50 张图训 5 个 epoch看 loss 有没有正常下降再决定要不要全量跑。全量训一次几小时用 50 张图 10 分钟就能排除大部分低级错误。息肉检测这个方向数据质量比模型结构重要得多标注框画准了Keras-YOLOv3 这种经典结构完全够用。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →