资讯动态

YOLOv1-Keras实战:从零训练手机拍摄小目标检测模型

发布时间:2026/10/18 9:28:21 来源:尧图企业网站定制
简介本资源是一份面向深度学习初学者与计算机视觉开发者的YOLOv1实战入门工具包聚焦于使用Keras框架快速训练自定义目标检测数据集。它解决了算法原理理解难、环境配置复杂、数据预处理与模型调参无从下手等典型痛点特别适用于课程设计、小规模工业检测原型开发及YOLO系列算法演进研究。压缩包共11个文件4张JPG示例图含原始与检测结果、3个Python核心脚本涵盖训练/测试/网络定义、3个TXT列表文件用于数据索引与精调、1份Markdown说明文档总大小仅128KB轻量易解压结构紧凑——其中darknet19.py实现骨干网络train_list.txt与refine_list.txt明确划分数据路径net_train.py和net_test.py提供即用型训练推理流程。目前已有109人学习下载用户可直接复用代码结构、参考图像标注规范、套用训练模板并基于README.md快速启动端到端训练无需从零搭建YOLOv1底层逻辑。1. YOLOv1-Keras 实战从零训练一个能识别你手机里拍的“咖啡杯”“键盘”“充电线”的检测模型你手头有一堆自己用手机拍的几十张办公桌照片想让程序自动框出图里的咖啡杯、键盘、充电线——不是调个现成API而是真正在本地跑通整个训练流程改几行代码就能换数据、调参数、看loss曲线、导出可部署的h5模型。这个YOLOv1-Keras项目就是干这个的它不依赖TensorFlow 2.x的高级API也不用PyTorch的动态图玄学而是用Keras 2.3兼容TF 1.15把YOLOv1原始论文的448×448输入、7×7网格、2个bbox预测、20类分类这些硬核设定一行行写实、可调试、可打断点的Python代码。它不是教学Demo而是一个能真正喂进你自己的VOC格式标注、跑通训练→验证→推理全链路的最小可行工程。适合刚学完CNN基础、正卡在“知道YOLO原理但跑不通第一个模型”的开发者也适合需要快速验证某个小场景比如产线零件识别、实验室设备定位是否适配YOLOv1架构的工程师——毕竟YOLOv1轻量、结构清晰、没有NMS黑匣子debug起来比YOLOv5/v8直接看log爽得多。2. 环境与目录结构为什么必须用Keras 2.3 TF 1.152.1 技术栈锁定版本不是选出来的是被YOLOv1的实现细节咬死的这个项目不是“支持Keras”而是深度耦合Keras 2.3的底层行为。最典型的例子是keras.layers.Conv2D的paddingsame在TF 1.15下对奇数尺寸输入的填充逻辑和TF 2.x完全不一致。YOLOv1网络中大量使用448→224→112→56→28→14→7这种逐层减半的尺寸链一旦padding算错最后7×7网格的坐标回归就全偏了。我试过强行升级到Keras 2.6光是yolo_loss函数里那个tf.slice切片操作就报维度不匹配——因为TF 2.x默认启用eager execution而原项目所有loss计算都基于graph mode的静态shape推导。所以环境配置不是建议是铁律# 必须用conda隔离避免污染全局环境 conda create -n yolo1-keras python3.6 conda activate yolo1-keras pip install tensorflow1.15.5 keras2.3.1 opencv-python4.5.5.64 numpy1.19.5提示Python 3.6是关键。Keras 2.3.1官方只测试到3.63.7会出现get_config()方法签名不兼容导致自定义层YOLOLayer无法序列化。2.2 目录结构即开发路径5个核心文件决定你能否跑通解压后你会看到一个极简但功能完整的结构没有多余文件每个文件都直击YOLOv1训练痛点YOLOv1-Keras/ ├── config.py # 所有可调参数集中地网格数S7、bbox数B2、类别数C20、学习率、batch_size... ├── data/ # 数据准备区放你的JPEGImages/和Annotations/VOC格式 │ ├── JPEGImages/ # 原图命名如000001.jpg │ └── Annotations/ # XML标注必须含objectnamecup/namebndbox.../bndbox/object ├── model.py # 核心从头构建YOLOv1网络含convleaky_relumaxpool全连接层 ├── train.py # 训练入口加载数据、编译模型、定义loss、启动fit_generator ├── predict.py # 推理脚本读图→预处理→model.predict→解析7×7×30输出→画框 └── utils/ # 工具集voc_annotation.py生成train.txt、data_generator.py实时增强注意data/目录必须手动创建并填入你的数据项目不带任何预置数据集。train.txt文件由utils/voc_annotation.py生成内容是每行一个path/to/000001.jpg path/to/000001.xml这是Kerasfit_generator唯一认的数据索引。2.3 config.py改这5个参数模型行为立刻不同别急着跑train.py先打开config.py——这里藏着YOLOv1的“灵魂开关”。我把它拆成三类新手最容易翻车的是第三类参数名默认值作用修改建议S7网格数决定定位粒度小目标多别乱改YOLOv1固定7×7改了要重写整个lossB2每格预测bbox数一般不动除非你确定单格绝不会出现2个目标C20类别数必须改你的数据只有3类cup, keyboard, cable这里填3LEARNING_RATE0.001初始学习率新数据集建议降到0.0001否则early loss爆炸BOX_SCALE,OBJ_SCALE,NOOBJ_SCALE,CLASS_SCALE5, 1, 0.5, 1loss各部分权重NOOBJ_SCALE0.5是防误检关键别调高注意C3不只是改数字。你必须同步修改model.py里最后一层全连接的输出维度Dense(S*S*(B*5C))中的C以及utils/data_generator.py里解析XML时的类别映射字典。漏改一个训练时就会报shape mismatch。3. 数据准备VOC格式不是摆设XML里这3个字段必须精准3.1 为什么坚持VOC格式因为YOLOv1的loss函数靠它算IOUYOLOv1的损失函数包含定位误差、置信度误差、分类误差三部分。其中定位误差λ_coord * Σ( x_i - x̂_i )²和置信度误差λ_noobj * Σ( C_i - Ĉ_i )²的计算完全依赖XML中标注的bndbox坐标与网络预测的归一化坐标的IOU值。如果随便用JSON或CSVdata_generator.py里那个parse_voc_xml函数就废了。所以你的数据必须长这样!-- data/Annotations/000001.xml -- annotation filename000001.jpg/filename size width640/width height480/height depth3/depth /size object namecup/name bndbox xmin120/xmin !-- 必须是整数像素坐标 -- ymin85/ymin !-- 不是归一化值 -- xmax210/xmax ymax230/ymax /bndbox /object object namekeyboard/name bndbox xmin300/xmin ymin320/ymin xmax580/xmax ymax450/ymax /bndbox /object /annotation关键点xmin等必须是原始图像中的绝对像素值不是0~1之间的归一化值name必须和config.py里的CLASSES [cup, keyboard, cable]严格一致包括大小写同一图中多个object是合法的YOLOv1原生支持多目标。3.2 voc_annotation.py生成train.txt的隐藏逻辑运行python utils/voc_annotation.py会扫描data/Annotations/下所有XML生成train.txt。但它有个反直觉设计默认只取前80%的文件作为训练集剩下20%自动当验证集。源码里是这么写的# utils/voc_annotation.py 第45行 import random xml_files glob.glob(os.path.join(ANNOTATIONS_PATH, *.xml)) random.shuffle(xml_files) # 随机打乱 train_num int(0.8 * len(xml_files)) for xml_file in xml_files[:train_num]: jpg_file xml_file.replace(Annotations, JPEGImages).replace(.xml, .jpg) with open(train.txt, a) as f: f.write(f{jpg_file} {xml_file}\n)所以如果你只有50张图train.txt里只有40行。想全量训练删掉random.shuffle和train_num计算改成for xml_file in xml_files: jpg_file ... # 同上 with open(train.txt, a) as f: f.write(f{jpg_file} {xml_file}\n)3.3 data_generator.py实时增强的3个安全边界DataGenerator类在fit_generator中每批读图做缩放、翻转、色彩抖动。但YOLOv1输入固定448×448所以缩放必须保持宽高比否则bbox坐标就废了。看它的核心逻辑# utils/data_generator.py 第82行 def _resize_image(self, image, bboxes): h, w image.shape[:2] scale min(448/h, 448/w) # 等比缩放长边贴448 new_h, new_w int(h*scale), int(w*scale) image cv2.resize(image, (new_w, new_h)) # bbox坐标同步缩放 bboxes[:, [0,2]] bboxes[:, [0,2]] * scale bboxes[:, [1,3]] bboxes[:, [1,3]] * scale # 填充黑边到448×448 pad_h, pad_w 448-new_h, 448-new_w image np.pad(image, ((0,pad_h), (0,pad_w), (0,0)), constant) return image, bboxes这里bboxes是[xmin, ymin, xmax, ymax]格式缩放后仍为像素坐标。填黑边不改变bbox值因为YOLOv1的loss计算时网络输出的坐标是相对于448×448图像的所以填充后的坐标无需再调整——这是YOLOv1和Faster R-CNN的关键区别。提示如果你的数据图普遍很小如200×150scale会很大缩放后可能模糊。这时应在config.py里加MIN_SIDE300强制原图短边至少300像素再缩放避免过度插值。4. 模型构建与Loss详解7×7×30输出怎么变成框和类别4.1 model.pyYOLOv1网络的7个卷积块与2个全连接层model.py里的yolov1_network()函数是纯Keras Layer堆叠没有魔法。它严格复现了原论文的24个卷积层2个全连接层结构实际代码做了简化用1×1卷积替代部分全连接但输出维度一致。关键模块如下# model.py 第32行主干网络 x Conv2D(64, (7,7), strides(2,2), paddingsame, nameconv1)(inputs) x LeakyReLU(alpha0.1)(x) x MaxPooling2D((2,2), strides(2,2))(x) # 448→224 x Conv2D(192, (3,3), paddingsame, nameconv2)(x) x LeakyReLU(alpha0.1)(x) x MaxPooling2D((2,2), strides(2,2))(x) # 224→112 # ... 中间18层卷积省略... x Conv2D(1024, (3,3), paddingsame, nameconv23)(x) # 28→14 x LeakyReLU(alpha0.1)(x) x Conv2D(1024, (3,3), paddingsame, nameconv24)(x) # 14→7 x LeakyReLU(alpha0.1)(x) # 最后两个全连接层用Conv2D模拟因输入是7×7×1024 x Conv2D(4096, (7,7), paddingvalid, namefc1)(x) # 7×7×1024 → 1×1×4096 x LeakyReLU(alpha0.1)(x) x Dropout(0.5)(x) x Conv2D(S*S*(B*5C), (1,1), paddingvalid, namefc2)(x) # 1×1×(7*7*30)1470 outputs Reshape((S, S, B*5C))(x) # 变成7×7×30张量重点Reshape((7,7,30))后的outputs就是YOLOv1的最终输出。30 B*5 C 2*5 20即每格预测2个bbox每个bbox含5维x,y,w,h,confidence再加20类概率。你的3类数据这里就是2*5313所以Dense层输出必须是7*7*13637。4.2 yolo_lossYOLOv1的灵魂函数逐行拆解train.py里yolo_loss函数是整个项目最难啃的部分。它不调用Keras内置loss而是手动用TensorFlow ops计算。我们拆解S7, B2, C3下的关键段# train.py 第112行loss计算主干 def yolo_loss(y_true, y_pred): # y_true shape: (None, 7, 7, 13) —— 你的3类 # y_pred shape: (None, 7, 7, 13) # 1. 分离预测值每格取2个bbox的(x,y,w,h,conf)和3个class prob pred_box_xy K.sigmoid(y_pred[..., :2*B]) # 归一化到0~1的中心坐标 pred_box_wh K.sqrt(K.exp(y_pred[..., 2*B:4*B])) # 宽高开方保证0 pred_box_conf K.sigmoid(y_pred[..., 4*B:5*B]) # 置信度sigmoid pred_box_class y_pred[..., 5*B:] # 类别logits后面接softmax # 2. 找到负责预测的grid cell哪个bbox的IOU最大 # 这里用y_true的[x,y,w,h]反算真实bbox再和pred_box_wh比较IOU true_box_wh K.sqrt(y_true[..., 2*B:4*B]) # 真实宽高开方 iou_scores box_iou(pred_box_xy, pred_box_wh, y_true[..., :2*B], true_box_wh) # 3. 构建响应掩码只有IOU最大的那个bbox负责定位其他只负责置信度 best_iou_idx K.argmax(iou_scores, axis-1) # shape (None,7,7) response_mask K.one_hot(best_iou_idx, B) # (None,7,7,2) # 4. 定位loss只算response_mask为1的位置 xy_loss K.sum(response_mask * K.square(y_true[..., :2*B] - pred_box_xy)) wh_loss K.sum(response_mask * K.square(y_true[..., 2*B:4*B] - pred_box_wh)) # 5. 置信度lossresponse位置算obj_loss非response算noobj_loss obj_mask K.max(response_mask, axis-1, keepdimsTrue) # (None,7,7,1) obj_loss K.sum(obj_mask * K.square(y_true[..., 4*B:5*B] - pred_box_conf)) noobj_loss K.sum((1-obj_mask) * K.square(y_true[..., 4*B:5*B] - pred_box_conf)) # 6. 分类loss只算有目标的grid cell class_loss K.sum(obj_mask * K.square(y_true[..., 5*B:] - K.softmax(pred_box_class))) return LAMBDA_COORD * (xy_loss wh_loss) obj_loss LAMBDA_NOOBJ * noobj_loss class_loss逻辑说明K.sigmoid确保x,y,conf在0~1K.sqrt(K.exp())确保w,h0这是YOLOv1防止负值的核心trickresponse_mask是YOLOv1的“责任分配”机制同一格2个bbox只让IOU大的那个优化坐标另一个只优化置信度obj_mask由response_mask派生用于区分“有目标”和“无目标”格子LAMBDA_NOOBJ0.5就是防误检的关键权重。注意box_iou函数在utils/loss.py里它用向量化方式计算7×7×2个预测框与真实框的IOU不用循环。如果你的loss一直不降先检查y_true里w,h是否为0——VOC标注中xmax-xmin或ymax-ymin为0会导致sqrt(0)IOU算出来是nan。4.3 避坑训练过程中的5个血泪问题现象1训练刚开始loss就飙到1e6然后nan原因y_true中某个bndbox的xminxmax或yminymax导致w或h为0或负在K.sqrt(K.exp())里产生nan传播到整个loss。解决运行python utils/check_bbox.py需自行编写遍历所有XML检查xmaxxmin and ymaxymin修复错误标注。现象2训练10轮后loss卡在20不再下降mAP0原因config.py里C20没改成你的类别数3导致pred_box_class维度错K.softmax输入乱码分类loss失效。解决确认model.py第78行Dense(7*7*(2*53))和config.py第15行C3完全一致。现象3predict.py画出的框全是歪的或者集中在图像一角原因data_generator.py里_resize_image函数的坐标缩放用了int()截断导致xmin等变成整数后精度丢失。YOLOv1对坐标敏感0.5像素偏差都会让IOU骤降。解决把bboxes[:, [0,2]] bboxes[:, [0,2]] * scale改成bboxes[:, [0,2]] bboxes[:, [0,2]].astype(np.float32) * scale全程用float运算。现象4GPU显存爆了batch_size1都OOM原因model.py里Conv2D(4096, (7,7))层参数太多7×7×1024×4096≈2G参数TF 1.15默认全加载到显存。解决在train.py开头加import os; os.environ[TF_GPU_ALLOCATOR]cuda_malloc_async或把Conv2D(4096,...)换成Dense(4096)需先Flatten显存降60%。现象5验证时val_loss比loss低很多但predict结果全是空框原因y_true中confidence标签没生成对。YOLOv1要求有目标的格子confidenceIOU无目标的格子confidence0。utils/data_generator.py第203行必须确保label[cell_y, cell_x, 4B*i] iou_score。解决在data_generator.py的_preprocess_true_boxes函数末尾加print(max confidence in label:, np.max(label[..., 4:6]))确认输出在0~1之间。5. 训练与推理从train.py到predict.py的端到端实操5.1 train.py如何启动一次可控训练train.py是入口但直接python train.py会失败——它默认从train.txt读数据且不保存模型。你需要改两处# train.py 第10行指定你的config from config import Config cfg Config() # train.py 第128行添加模型保存回调 checkpoint ModelCheckpoint( weights/yolov1_{epoch:02d}_{val_loss:.2f}.h5, monitorval_loss, save_best_onlyTrue, save_weights_onlyTrue, period1 ) # train.py 第135行启动训练关键参数 model.fit_generator( train_gen, steps_per_epochlen(train_gen), epochscfg.EPOCHS, validation_dataval_gen, validation_stepslen(val_gen), callbacks[checkpoint, reduce_lr, early_stopping], workers4, use_multiprocessingTrue, max_queue_size10 )运行命令mkdir weights # 先建目录 python train.py训练时你会看到Epoch 1/50 10/10 [] - 25s 2s/step - loss: 12.3456 - val_loss: 11.9876 Epoch 2/50 10/10 [] - 24s 2s/step - loss: 10.2345 - val_loss: 9.8765 ...提示steps_per_epochlen(train_gen)是关键。train_gen是DataGenerator实例它的__len__()返回ceil(total_images / batch_size)。如果你有40张图、BATCH_SIZE4len(train_gen)10所以每轮训10步。少设会欠拟合多设会重复读同一批数据。5.2 predict.py3步拿到检测结果predict.py是推理脚本改3处就能用# predict.py 第12行加载你训练好的权重 model.load_weights(weights/yolov1_35_8.23.h5) # 改成你的best权重 # predict.py 第25行指定你的图片路径 image cv2.imread(data/JPEGImages/000001.jpg) # 改成你的图 # predict.py 第42行设置你的类别名和颜色 CLASSES [cup, keyboard, cable] COLORS [(0,255,0), (255,0,0), (0,0,255)] # BGR格式运行python predict.py它会生成predictions/000001.jpg图上画出带标签和置信度的框。效果取决于你的训练质量——如果框歪了回去查data_generator.py的坐标缩放如果漏检调低predict.py第68行的confidence_threshold0.3。5.3 predict.py核心如何从7×7×13解析出框predict.py的process_predict函数是推理核心它把网络输出predshape(1,7,7,13)解码为[x1,y1,x2,y2,conf,class_id]# predict.py 第55行 def process_predict(pred, image_shape): pred np.squeeze(pred) # (7,7,13) boxes [] for i in range(7): # grid y for j in range(7): # grid x for b in range(2): # 2 bbox per cell conf sigmoid(pred[i,j,4b*5]) # 置信度 if conf 0.3: continue # 置信度过滤 # 解码中心坐标归一化坐标转像素坐标 x (j sigmoid(pred[i,j,b*5])) * 448/7 # j是grid列0~6 y (i sigmoid(pred[i,j,1b*5])) * 448/7 # i是grid行0~6 # 解码宽高指数开方转像素 w 448 * np.sqrt(np.exp(pred[i,j,2b*5])) h 448 * np.sqrt(np.exp(pred[i,j,3b*5])) # 转为左上右下坐标 x1 max(0, int(x - w/2)) y1 max(0, int(y - h/2)) x2 min(image_shape[1], int(x w/2)) y2 min(image_shape[0], int(y h/2)) # 分类概率 class_probs softmax(pred[i,j,10:]) # 10: 是5*23的起始 class_id np.argmax(class_probs) class_conf class_probs[class_id] final_conf conf * class_conf # 总置信度 if final_conf 0.3: boxes.append([x1,y1,x2,y2,final_conf,class_id]) return non_max_suppression(boxes, 0.5) # IOU阈值0.5去重关键点x (j sigmoid(...)) * 448/7j是grid列索引0~6sigmoid输出0~1所以x在[j*64, (j1)*64)范围内保证框落在对应gridw 448 * sqrt(exp(...))exp让负值变正sqrt压缩范围最终w在0~448合理final_conf conf * class_confYOLOv1的最终置信度是“是否有目标”ד是什么类别”乘积才是检测可信度。6. 二次开发实战如何把YOLOv1改成单类检测器并导出为OpenCV DNN可用格式6.1 改造成单类检测器删掉所有类别相关代码你的场景可能只需要检测“缺陷区域”不需要分“划痕/凹坑/锈迹”。这时C1可以大幅精简模型config.pyC 1CLASSES [defect]model.pyDense(7*7*(2*51))→ 输出维度从7*7*13637变成7*7*11539train.pyyolo_loss里删掉class_loss计算pred_box_class []占位predict.pyclass_id恒为0COLORS [(0,0,255)]但最关键的改动在yolo_loss当C1时y_true[..., 5*B:]只剩1维K.softmax会出错。正确做法是去掉softmax直接用sigmoid# train.py 修改yolo_loss # 原来class_loss K.sum(obj_mask * K.square(y_true[..., 5*B:] - K.softmax(pred_box_class))) # 改为 pred_class_prob K.sigmoid(pred_box_class) # (None,7,7,1) class_loss K.sum(obj_mask * K.square(y_true[..., 5*B:] - pred_class_prob))这样class_loss就退化为二分类交叉熵更稳定。6.2 导出为OpenCV DNN格式h5 → pb → onnx → tfliteKeras h5模型不能直接被OpenCVcv2.dnn.readNetFromTensorflow加载必须转pb。步骤如下# 1. 用TF 1.15导出冻结pbtrain.py里加 from tensorflow.python.framework import graph_util from tensorflow.python.framework import graph_io # 训练完model后 output_node_names [out.op.name for out in model.outputs] sess K.get_session() frozen_graph graph_util.convert_variables_to_constants( sess, sess.graph_def, output_node_names) graph_io.write_graph(frozen_graph, ./pb_model, yolov1.pb, as_textFalse)# 2. OpenCV加载Python net cv2.dnn.readNetFromTensorflow(pb_model/yolov1.pb) blob cv2.dnn.blobFromImage(image, 1/255.0, (448,448), swapRBTrue) net.setInput(blob) pred net.forward() # shape (1,7,7,11)注意OpenCV的forward()输出是(1,7,7,11)和Keras输出一致但坐标未解码。你得在Python里复写process_predict的解码逻辑只是输入从model.predict变成net.forward()。6.3 一个真实技巧用Grad-CAM可视化哪个grid cell在“看”目标YOLOv1没有attention机制但你可以用梯度加权类激活图Grad-CAM看模型关注区域。在predict.py里加# 加载模型后 last_conv_layer model.get_layer(conv24) # 最后一个卷积层 grad_model Model([model.inputs], [last_conv_layer.output, model.output]) with tf.GradientTape() as tape: conv_outputs, predictions grad_model(image_tensor) loss predictions[0, grid_y, grid_x, 4] # 某个grid的置信度 # 计算梯度 output conv_outputs[0] grads tape.gradient(loss, conv_outputs)[0] guided_grads tf.cast(output 0, float32) * tf.cast(grads 0, float32) * grads # 加权平均 weights tf.reduce_mean(guided_grads, axis(0, 1)) cam tf.reduce_sum(tf.multiply(weights, output), axis-1).numpy() # 可视化 cam np.maximum(cam, 0) cam cv2.resize(cam, (448,448)) cam cam / np.max(cam) heatmap cv2.applyColorMap(np.uint8(255*cam), cv2.COLORMAP_JET)运行后你会看到热力图集中在目标周围——如果热力图在背景上说明模型学歪了该加更多背景图做noobj监督。从那以后我每次训新数据都强制走一遍Grad-CAM检查前3个epoch的热力图分布。它比看loss曲线更能早3轮发现过拟合或标注错误。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑