佛山网站快速优化排名,android开发环境,上海城隍庙好玩吗,网站备案幕布多少钱精读论文
前言
从这篇开始#xff0c;我们将进入YOLO的学习。YOLO是目前比较流行的目标检测算法#xff0c;速度快且结构简单#xff0c;其他的目标检测算法如RCNN系列#xff0c;以后有时间的话再介绍。
本文主要介绍的是YOLOV1#xff0c;这是由以Joseph Redmon为首的…精读论文
前言
从这篇开始我们将进入YOLO的学习。YOLO是目前比较流行的目标检测算法速度快且结构简单其他的目标检测算法如RCNN系列以后有时间的话再介绍。
本文主要介绍的是YOLOV1这是由以Joseph Redmon为首的大佬们于2015年提出的一种新的目标检测算法。它与之前的目标检测算法如R-CNN等不同之处在于R-CNN等目标检测算法是两阶段算法 步骤为先在图片上生成候选框然后利用分类器对这些候选框进行逐一的判断而YOLOv1是一阶段算法是端到端的算法它把目标检测问题看作回归问题将图片输入单一的神经网络然后就输出得到了图片的物体边界框即boundingbox以及分类概率等信息。下面我们就开始学习吧。 下面是一些学习资料
论文链接[1506.02640] You Only Look Once: Unified, Real-Time Object Detection (arxiv.org)
项目地址 YOLO: Real-Time Object Detection (pjreddie.com)
Github源码地址mirrors / alexeyab / darknet · GitCode 目录
Abstract—摘要
一、Introduction—前言
二、Unified Detection—统一检测
2.1 Network Design—网络设计
2.2 Training—训练
2.3 Inference—推论
2.4 Limitations of YOLO—YOLO的局限性
三、Comparison to Other Detection Systems—与其他目标检测算法的比较
四、Experiments—实验
4.1 Comparison to Other RealTime Systems—与其他实时系统的比较 4.2 VOC 2007 Error Analysis—VOC 2007误差分析 4.3 Combining Fast R-CNN and YOLO—Fast R-CNN与YOLO的结合
4.4 VOC 2012 Results—VOC 2012结果
4.5 Generalizability: Person Detection in Artwork—泛化性图像中的人物检测
五、Real-Time Detection In The Wild—自然环境下的实时检测
六、Conclusion—结论 Abstract—摘要
翻译
我们提出的YOLO是一种新的目标检测方法。以前的目标检测方法通过重新利用分类器来执行检测。与先前的方案不同我们将目标检测看作回归问题从空间上定位边界框bounding box*并*预测该框的类别概率。我们使用单个神经网络**在一次评估中直接从完整图像上预测边界框和类别概率。由于整个检测流程仅用一个网络所以可以直接对检测性能进行端到端的优化。
我们的统一架构速度极快。我们的基本YOLO模型以45 fps帧/秒的速度实时处理图像。该网络的一个较小版本——Fast YOLO以155 fps这样惊人的速度运行同时仍然达到其他实时检测器的两倍。与最先进的state-of-the-artSOTA检测系统相比YOLO虽然产生了较多的定位误差但它几乎不会发生把背景预测为目标这样的假阳性False Positive的错误。最后YOLO能学习到泛化性很强的目标表征。当从自然图像学到的模型用于其它领域如艺术画作时它的表现都优于包括DPM和R-CNN在内的其它检测方法。
精读
之前的方法RCNN系列
1通过region proposal产生大量的可能包含待检测物体的potential bounding box
2再用分类器去判断每个bounding box里是否包含有物体以及物体所属类别的probability或者 confidence
3最后回归预测
YOLO的简介
本文将检测变为一个regression problem回归问题YOLO 从输入的图像仅仅经过一个神经网络直接得到一些bounding box以及每个bounding box所属类别的概率。
因为整个的检测过程仅仅有一个网络所以它可以直接进行end-to-end的优化。 end-to-end 端到端指的是输入原始数据输出的是最后结果原来输入端不是直接的原始数据而是在原始数据中提取的特征。通过缩减人工预处理和后续处理尽可能使模型从原始输入到最终输出给模型更多可以根据数据自动调节的空间增加模型的整体契合度。在CV中具体表现是神经网络的输入为原始图片神经网络的输出为可以直接控制机器的控制指令。 一、Introduction—前言
翻译
人们只需瞄一眼图像立即知道图像中的物体是什么它们在哪里以及它们如何相互作用。人类的视觉系统是快速和准确的使得我们在无意中就能够执行复杂的任务如驾驶。快速且准确的目标检测算法可以让计算机在没有专门传感器的情况下驾驶汽车使辅助设备能够向人类用户传达实时的场景信息并解锁通用、响应性的机器人系统的潜能。
目前的检测系统通过重用分类器来执行检测。为了检测目标这些系统为该目标提供一个分类器在测试图像的不同的位置和不同的尺度上对其进行评估。像deformable parts modelsDPM可变形部分模型这样的系统使用滑动窗口方法其分类器在整个图像上均匀间隔的位置上运行[10]。
最近的方法如R-CNN使用region proposal区域候选策略首先在图像中生成潜在的边界框bounding box然后在这些框上运行分类器。在分类之后执行用于细化边界框的后处理消除重复的检测并根据场景中的其它目标为边界框重新打分[13]。这些复杂的流程是很慢很难优化的因为每个独立的部分都必须单独进行训练。
**我们将目标检测看作是一个单一的回归问题直接从图像像素得到边界框坐标和类别概率。**使用我们的系统——You Only Look OnceYOLO便能得到图像上的物体是什么和物体的具体位置。
YOLO非常简单见图1它仅用单个卷积网络就能同时预测多个边界框和它们的类别概率。YOLO在整个图像上训练并能直接优化检测性能。与传统的目标检测方法相比这种统一的模型下面所列的一些优点
第一YOLO速度非常快。由于我们将检测视为回归问题所以我们不需要复杂的流程。测试时我们在一张新图像上简单的运行我们的神经网络来预测检测结果。在Titan X GPU上不做批处理的情况下YOLO的基础版本以每秒45帧的速度运行而快速版本运行速度超过150fps。这意味着我们可以在不到25毫秒的延迟内实时处理流媒体视频。此外YOLO实现了其它实时系统两倍以上的平均精度。关于我们的系统在网络摄像头上实时运行的演示请参阅我们的项目网页YOLO: Real-Time Object Detection。
第二YOLO是在整个图像上进行推断的。与基于滑动窗口和候选框的技术不同YOLO在训练期间和测试时都会顾及到整个图像所以它隐式地包含了关于类的上下文信息以及它们的外观。Fast R-CNN是一种很好的检测方法[14]但由于它看不到更大的上下文会将背景块误检为目标。与Fast R-CNN相比YOLO的背景误检数量少了一半。
第三YOLO能学习到****目标的泛化表征generalizable representations of objects。把在自然图像上进行训练的模型用在艺术图像进行测试时YOLO大幅优于DPM和R-CNN等顶级的检测方法。由于YOLO具有高度泛化能力因此在应用于新领域或碰到意外的输入时不太可能出故障。
YOLO在精度上仍然落后于目前最先进的检测系统。虽然它可以快速识别图像中的目标但它在定位某些物体尤其是小的物体上精度不高。
我们在实验中会进一步探讨精度时间的权衡。我们所有的训练和测试代码都是开源的而且各种预训练模型也都可以下载。 精读
之前的研究
DPM 系统为检测对象使用分类器并在测试图像的不同位置和尺度对其进行评估
**R-CNN**SS方法提取候选框CNN分类回归。
YOLO处理步骤 (1)将输入图像的大小调整为448×448分割得到7*7网格
(2)通过CNN提取特征和预测
(3)利用非极大值抑制NMS进行筛选 YOLO的定义
YOLO将目标检测重新定义为单个回归问题从图像像素直接到边界框坐标和类概率。YOLO可以在一个图像来预测哪些对象是存在的它们在哪里
如 Figure 1将图像输入单独的一个 CNN 网络就会预测出 bounding box以及这些 bounding box 所属类别的概率。
YOLO 用一整幅图像来训练同时可以直接优化性能检测。
性能检测对比 YOLO的优点
1**YOLO的速度非常快。**能够达到实时的要求。在 Titan X 的 GPU 上 能够达到 45 帧每秒。
2**YOLO在做预测时使用的是全局图像。**与FastR-CNN相比YOLO产生的背景错误数量不到一半。
3**YOLO 学到物体更泛化的特征表示。**因此当应用于新域或意外输入时不太可能崩溃。 二、Unified Detection—统一检测
网格单元
翻译
我们将目标检测的独立部分(the separate components )整合到单个神经网络中。我们的网络使用整个图像的特征来预测每个边界框。它还可以同时预测一张图像中的所有类别的所有边界框。这意味着我们的网络对整张图像和图像中的所有目标进行全局推理(reason globally)。YOLO设计可实现端到端训练和实时的速度同时保持较高的平均精度。
我们的系统将输入图像分成 S×S 的网格。如果目标的中心落入某个网格单元(grid cell)中那么该网格单元就负责检测该目标。
每个网格单元都会预测 B个 边界框和这些框的置信度分数confidence scores。这些置信度分数反映了该模型对那个框内是否包含目标的置信度以及它对自己的预测的准确度的估量。在形式上我们将置信度定义为 confidencePr(Object)∗IOUpred truth 。如果该单元格中不存在目标即Pr(Object)0则置信度分数应为 0 。否则即Pr(Object)1我们希望置信度分数等于预测框predict box与真实标签框ground truth之间联合部分的交集IOU。
每个网格单元还预测了C类的条件概率Pr(Classi|Object)。这些概率是以包含目标的网格单元为条件的。我们只预测每个网格单元的一组类别概率而不考虑框B的数量。
在测试时我们将条件类概率和单个框的置信度预测相乘[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-411RQXtW-1700230508536)(https://latex.csdn.net/eq?Pr%28Class_%7Bi%7D%7CObject%29Pr%28Object%29IOU%5E%7Btruth%7D_%7Bpred%7D%3DPr%28Class_%7Bi%7D%29*IOU%5E%7Btruth%7D_%7Bpred%7D)]
这给我们提供了每个框的特定类别的置信度分数。这些分数既是对该类出现在框里的概率的编码也是对预测的框与目标的匹配程度的编码。 精读
思想
YOLO将目标检测问题作为回归问题。会将输入图像分成S×S的网格如果一个物体的中心点落入到一个cell中那么该cell就要负责预测该物体一个格子只能预测一个物体会生成两个预测框。
对于每个grid cell
1预测B个边界框每个框都有一个置信度分数confidence score这些框大小尺寸等等都随便只有一个要求就是生成框的中心点必须在grid cell里。
2每个边界框包含5个元素(x,y,w,h) ● xy 是指bounding box的预测框的中心坐标相较于该bounding box归属的grid cell左上角的偏移量在0-1之间。 在上图中绿色虚线框代表grid cell绿点表示该grid cell的左上角坐标为00红色和蓝色框代表该grid cell包含的两个bounding box红点和蓝点表示这两个bounding box的中心坐标。有一点很重要bounding box的中心坐标一定在该grid cell内部因此红点和蓝点的坐标可以归一化在0-1之间。在上图中红点的坐标为0.50.5即xy0.5蓝点的坐标为0.90.9即xy0.9。 ● wh 是指该bounding box的宽和高但也归一化到了0-1之间表示相较于原始图像的宽和高即448个像素。比如该bounding box预测的框宽是44.8个像素高也是44.8个像素则w0.1h0.1。 红框的x0.8y0.5w0.1h0.2。 3不管框 B 的数量是多少只负责预测一个目标。
4预测 C 个条件概率类别物体属于每一种类别的可能性 **综上S×S 个网格每个网格要预测 B个bounding box 中间上图还要预测 C 个类中间下图。**将两图合并网络输出就是一个 S × S × (5×BC)。S x S个网格每个网格都有B个预测框每个框又有5个参数再加上每个网格都有C个预测类 Q1为什么每个网格有固定的B个bounding box即B2 在训练的时候会在线地计算每个predictor预测的bounding box和ground truth的IOU计算出来的IOU大的那个predictor就会负责预测这个物体另外一个则不预测。这么做有什么好处我的理解是这样做的话实际上有两个predictor来一起进行预测然后网络会在线选择预测得好的那个predictor也就是IOU大来进行预测。 Q2每个网格预测的两个bounding box是怎么得到的 YOLO中两个bounding box是人为选定的(2个不同 长宽比的box在训练开始时作为超参数输入bounding box的信息随着训练次数增加loss降低bounding box越来越准确。Faster RCNN也是人为选定的9个 不同长宽比和scaleYOLOv2是统计分析ground true box的特点得到的5个。 预测特征组成
最终的预测特征由边框的位置、边框的置信度得分以及类别概率组成这三者的含义如下
边框位置 对每一个边框需要预测其中心坐标及宽、高这4个量 两个边框共计8个预测值边界框宽度w和高度h用图像宽度和高度归一化。因此 x,y,w,h 都在0和1之间。置信度得分(box confidence score) c 框包含一个目标的可能性以及边界框的准确程度。类似于Faster RCNN 中是前景还是背景。由于有两个边框因此会存在两个置信度预测值。类别概率 由于PASCAL VOC数据集一共有20个物体类别因此这里预测的是边框属于哪一个类别。
注意 一个cell预测的两个边界框共用一个类别预测 在训练时会选取与标签IoU更大的一个边框负责回归该真实物体框在测试时会选取置信度更高的一个边框另一个会被舍弃因此7×749个gird cell最多只能预测49个物体。 因为每一个 grid cell只能有一个分类也就是他只能预测一个物体这也是导致YOLO对小目标物体性能比较差的原因。 如果所给图片极其密集导致 grid cell里可能有多个物体但是YOLO模型只能预测出来一个那这样就会忽略在本grid cell内的其他物体。 2.1 Network Design—网络设计
翻译
我们将此模型作为卷积神经网络来实现并在Pascal VOC检测数据集[9]上进行评估。网络的初始卷积层从图像中提取特征而全连接层负责预测输出概率和坐标。
我们的网络架构受图像分类模型GoogLeNet的启发[34]。我们的网络有24个卷积层后面是2个全连接层。我们只使用1×1降维层后面是3×3卷积层这与Lin等人[22]类似而不是GoogLeNet使用的Inception模块。
我们还训练了快速版本的YOLO旨在推动快速目标检测的界限。快速YOLO使用具有较少卷积层9层而不是24层的神经网络在这些层中使用较少的卷积核。除了网络规模之外基本版YOLO和快速YOLO的所有训练和测试参数都是相同的。
我们网络的最终输出是7×7×30的预测张量。 精读
网络结构
YOLO网络结构借鉴了 GoogLeNet 经典神经网络论文超详细解读三——GoogLeNet InceptionV1学习笔记翻译精读代码复现。输入图像的尺寸为448×448经过24个卷积层2个全连接的层FC最后在reshape操作输出的特征图大小为7×7×30。 Q7×7×30怎么来的 张量剖面图 图片来源YOLO v1详细解读_yolov1详解_迪菲赫尔曼的博客-CSDN博客 7×7 一共划分成7×7的网格。30 30包含了两个预测框的参数和Pascal VOC的类别参数每个预测框有5个参数x,y,w,h,confidence。另外Pascal VOC里面还有20个类别所以最后的30实际上是由5x220组成的也就是说这一个30维的向量就是一个gird cell的信息。7×7×30 总共是7 × 7个gird cell一共就是7 × 7 ×2 × 5 20 7 × 7 × 30 tensor 1470 outputs正好对应论文。 网络详解
1YOLO主要是建立一个CNN网络生成预测7×7×1024 的张量 。
2然后使用两个全连接层执行线性回归以进行7×7×2 边界框预测。将具有高置信度得分大于0.25的结果作为最终预测。
3在3×3的卷积后通常会接一个通道数更低1×1的卷积这种方式既降低了计算量同时也提升了模型的非线性能力。
4除了最后一层使用了线性激活函数外其余层的激活函数为 Leaky ReLU 。
5在训练中使用了 Dropout 与数据增强的方法来防止过拟合。
6对于最后一个卷积层它输出一个形状为 (7, 7, 1024) 的张量。 然后张量展开。使用2个全连接层作为一种线性回归的形式它输出1470个参数然后reshape为 (7, 7, 30) 。 2.2 Training—训练
翻译
我们在ImageNet的1000类竞赛数据集[30]上预训练我们的卷积层。对于预训练我们使用图3中的前20个卷积层接着是平均池化层和全连接层。我们对这个网络进行了大约一周的训练并且在ImageNet 2012验证集上获得了单一裁剪图像88%的top-5准确率与Caffe模型池中的GoogLeNet模型相当。我们使用Darknet框架进行所有的训练和推断[26]。
然后我们转换模型来执行检测训练。Ren等人表明预训练网络中增加卷积层和连接层可以提高性能[29]。按照他们的方法我们添加了四个卷积层和两个全连接层这些层的权重都用随机值初始化。检测通常需要细粒度的视觉信息因此我们将网络的输入分辨率从224×224改为448×448。
模型的最后一层预测类概率和边界框坐标。我们通过图像宽度和高度来规范边界框的宽度和高度使它们落在0和1之间。我们将边界框x和y坐标参数化为特定网格单元位置的偏移量所以它们的值被限定在在0和1之间。
模型的最后一层使用线性激活函数而所有其它的层使用下面的Leaky-ReLU
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2xEDccXI-1700230508539)(https://latex.csdn.net/eq?%5Cphi%20%28x%29%3D%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%20x%2C%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20if%20%5C%3A%20x%3E0%20%26%20%26%20%5C%5C0.1x%2C%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20otherwise%26%20%26%20%5Cend%7Bmatrix%7D%5Cright.)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B4veVrdH-1700230508540)(https://latex.csdn.net/eq?%5Cphi%20%28x%29%3D%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%20x%2C%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20if%20%5C%3A%20x%3E0%20%26%20%26%20%5C%5C0.1x%2C%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20otherwise%26%20%26%20%5Cend%7Bmatrix%7D%5Cright.)]
我们对模型输出的平方和误差(sum-squared error)进行优化。我们选择使用平方和误差是因为它易于优化但是它并不完全符合最大化平均精度average precision的目标。它给分类误差与定位误差的权重是一样的这点可能并不理想。另外每个图像都有很多网格单元并没有包含任何目标这将这些单元格的“置信度”分数推向零通常压制了包含目标的单元格的梯度。这可能导致模型不稳定从而导致训练在早期就发散(diverge)。
为了弥补平方和误差的缺陷我们增加了边界框坐标预测的损失并减少了不包含目标的框的置信度预测的损失。 我们使用两个参数λ c o o r d λ_{coord}λcoord和λ n o o b j λ_{noobj}λnoobj来实现这一点。 我们设定λ c o o r d 5 λ_{coord} 5λcoord5 和 λ n o o b j 0.5 λ_{noobj}0.5λnoobj0.5。
平方和误差对大框和小框的误差权衡是一样的而我们的错误指标(error metric)应该要体现出大框的小偏差的重要性不如小框的小偏差的重要性。为了部分解决这个问题我们直接预测边界框宽度和高度的平方根而不是宽度和高度。
YOLO为每个网格单元预测多个边界框。在训练时每个目标我们只需要一个边界框预测器来负责。若某预测器的预测值与目标的实际值的IOU值最高则这个预测器被指定为“负责”预测该目标。这导致边界框预测器的专业化。每个预测器可以更好地预测特定大小方向角或目标的类别从而改善整体召回率(recall)。
在训练期间我们优化以下多部分损失函数 注意如果目标存在于该网格单元中前面讨论的条件类别概率则损失函数仅惩罚(penalizes)分类错误。如果预测器“负责”实际边界框即该网格单元中具有最高IOU的预测器则它也仅惩罚边界框坐标错误。
我们用Pascal VOC 2007和2012的训练集和验证数据集进行了大约 135个epoch 的网络训练。因为我们仅在Pascal VOC 2012上进行测试所以我们的训练集里包含了Pascal VOC 2007的测试数据。在整个训练过程中我们使用batch size64momentum0.9decay0.0005。
我们的学习率learning rate计划如下在第一个epoch中我们将学习率从1 0 − 3 10^{-3}10−3慢慢地提高到 1 0 − 2 10^{-2}10−2。如果从大的学习率开始训练我们的模型通常会由于不稳定的梯度而发散(diverge)。我们继续以 1 0 − 2 10^{-2}10−2 进行75个周期的训练然后以 1 0 − 3 10^{-3}10−3 进行30个周期的训练最后以 1 0 − 4 10^{-4}10−4 进行30个周期的训练。
为避免过拟合我们使用了Dropout和大量的数据增强。 在第一个连接层之后的dropout层的丢弃率设置为0.5以防止层之间的相互适应[18]。 对于数据增强(data augmentation)我们引入高达20的原始图像大小的随机缩放和平移(random scaling and translations )。我们还在 HSV 色彩空间中以高达 1.5 的因子随机调整图像的曝光度和饱和度。 精读
预训练分类网络
在 ImageNet 1000数据集上预训练一个分类网络这个网络使用Figure3中的前20个卷积层然后是一个平均池化层和一个全连接层。此时网络输入是224×224。 Q主干结构的输入要求必须是448x448的固定尺寸为什么在预训练阶段可以输入224x224的图像呢 主要原因是加入了平均池化层这样不论输入尺寸是多少在和最后的全连接层连接时都可以保证相同的神经元数目。 训练检测网络
经过上一步的预训练就已经把主干网络的前20个卷积层给训练好了前20层的参数已经学到了图片的特征。接下来的步骤本质就是迁移学习在训练好的前20层卷积层后加上4层卷积层和2层全连接层然后在目标检测的任务上进行迁移学习。
在整个网络242的训练过程中除最后一层采用ReLU函数外其他层均采用leaky ReLU激活函数。leaky ReLU相对于ReLU函数可以解决在输入为负值时的零梯度问题。YOLOv1中采用的leaky ReLU函数的表达式为
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6j8khv2N-1700230508541)(https://latex.csdn.net/eq?%5Cphi%20%28x%29%3D%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%20x%2C%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20if%20%5C%3A%20x%3E0%20%26%20%26%20%5C%5C0.1x%2C%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20%5C%3A%20otherwise%26%20%26%20%5Cend%7Bmatrix%7D%5Cright.)] NMS非极大值抑制
**概念**NMS算法主要解决的是一个目标被多次检测的问题意义主要在于在一个区域里交叠的很多框选一个最优的。
YOLO中具体操作 1对于上述的98列数据先看某一个类别也就是只看98列的这一行所有数据先拿出最大值概率的那个框剩下的每一个都与它做比较如果两者的IoU大于某个阈值则认为这俩框重复识别了同一个物体就将其中低概率的重置成0。
2最大的那个框和其他的框比完之后再从剩下的框找最大的继续和其他的比依次类推对所有类别进行操作。 注意这里不能直接选择最大的因为有可能图中有多个该类别的物体所以IoU如果小于某个阈值则会被保留。
3最后得到一个稀疏矩阵因为里面有很多地方都被重置成0拿出来不是0的地方拿出来概率和类别就得到最后的目标检测结果了。
注意 NMS只发生在预测阶段训练阶段是不能用NMS的因为在训练阶段不管这个框是否用于预测物体的他都和损失函数相关不能随便重置成0。 损失函数
损失函数包括
localization loss - 坐标损失
confidence loss - 置信度损失
classification loss - 分类损失 损失函数详解
1坐标损失 第一行 负责检测物体的框中心点x, y定位误差。第二行 负责检测物体的框的高宽w,h)定位误差这个根号的作用就是为了修正对大小框一视同仁的缺点削弱大框的误差。 Q为啥加根号 在上图中大框和小框的bounding box和ground truth都是差了一点但对于实际预测来讲大框大目标差的这一点也许没啥事儿而小框小目标差的这一点可能就会导致bounding box的方框和目标差了很远。而如果还是使用第一项那样直接算平方和误差就相当于把大框和小框一视同仁了这样显然不合理。而如果使用开根号处理就会一定程度上改善这一问题 。 这样一来同样是差一点小框产生的误差会更大即对小框惩罚的更严重。 2置信度损失 第一行 负责检测物体的那个框的置信度误差。第二行 不负责检测物体的那个框的置信度误差。
3分类损失 负责检测物体的grid cell分类的误差。 特殊符号的含义 2.3 Inference—推论
翻译
就像在训练中一样预测测试图像的检测只需要一次网络评估。在Pascal VOC上每张图像上网络预测 98 个边界框和每个框的类别概率。YOLO在测试时非常快因为它只需要一次网络评估(network evaluation)这与基于分类器的方法不同。
网格设计强化了边界框预测中的空间多样性。通常一个目标落在哪一个网格单元中是很明显的而网络只能为每个目标预测一个边界框。然而一些大的目标或接近多个网格单元的边界的目标能被多个网格单元定位。非极大值抑制(Non-maximal suppression,NMS)可以用来修正这些多重检测。非最大抑制对于YOLO的性能的影响不像对于R-CNN或DPM那样重要但也能增加2−3%的mAP。 精读
1预测测试图像的检测只需要一个网络评估。
2测试时间快
3当图像中的物体较大或者处于 grid cells 边界的物体可能在多个 cells 中被定位出来。
4利用NMS去除重复检测的物体使mAP提高但和RCNN等相比不算大。 2.4 Limitations of YOLO—YOLO的局限性
翻译
由于每个格网单元只能预测两个框并且只能有一个类因此YOLO对边界框预测施加了很强的空间约束。这个空间约束限制了我们的模型可以预测的邻近目标的数量。我们的模型难以预测群组中出现的小物体比如鸟群。
由于我们的模型学习是从数据中预测边界框因此它很难泛化到新的、不常见的长宽比或配置的目标。我们的模型也使用相对较粗糙的特征来预测边界框因为输入图像在我们的架构中历经了多个下采样层(downsampling layers)。
最后我们的训练基于一个逼近检测性能的损失函数这个损失函数无差别地处理小边界框与大边界框的误差。大边界框的小误差通常是无关要紧的但小边界框的小误差对IOU的影响要大得多。我们的主要错误来自于不正确的定位。 精读
1对于图片中一些群体性小目标检测效果比较差。因为yolov1网络到后面感受野较大小目标的特征无法再后面7×7的grid中体现针对这一点yolov2已作了一定的修改加入前层感受野较小的特征进行融合。
2原始图片只划分为7x7的网格当两个物体靠的很近时挨在一起且中点都落在同一个格子上的情况效果比较差。因为yolov1的模型决定了一个grid只能预测出一个物体所以就会丢失目标针对这一点yolov2引入了anchor的概念一个grid有多少个anchor理论上就可以预测多少个目标。
3每个网格只对应两个bounding box当物体的长宽比不常见(也就是训练数据集覆盖不到时)效果较差。
4最终每个网格只对应一个类别容易出现漏检(物体没有被识别到)。 三、Comparison to Other Detection Systems—与其他目标检测算法的比较
翻译
目标检测是计算机视觉中的核心问题。检测流程通常是首先从输入图像上提取一组鲁棒特征Haar [25]SIFT [23]HOG [4]卷积特征[6]。然后分类器[36,21,13,10]或定位器[1,32]被用来识别特征空间中的目标。这些分类器或定位器或在整个图像上或在图像中的一些子区域上以滑动窗口的方式运行[35,15,39]。我们将YOLO检测系统与几种顶级检测框架进行比较突出了关键的相似性和差异性。
Deformable parts models。可变形部分模型DPM使用滑动窗口方法进行目标检测[10]。DPM使用不相交的流程来提取静态特征对区域进行分类预测高评分区域的边界框等。我们的系统用单个卷积神经网络替换所有这些不同的部分。网络同时进行特征提取边界框预测非极大值抑制和上下文推理。网络的特征feature是在**在线(in-line)**训练出来的而不是静态因此可以根据特定的检测任务进行优化。我们的统一架构比DPM更快更准确。
R-CNN。R-CNN及其变体(variants)使用区域候选而不是滑动窗口来查找图像中的目标。选择性搜索[35]生成潜在的边界框(Selective Search generates potential bounding boxes)卷积网络提取特征SVM对框进行评分线性模型调整边界框非最大抑制消除重复检测(eliminates duplicate detections)。 这个复杂流水线的每个阶段都必须独立地进行精确调整(precisely tuned independently)所得到的系统非常缓慢在测试时间每个图像需要超过40秒[14]。
YOLO与R-CNN有一些相似之处。每个网格单元提出潜在的边界框并使用卷积特征对这些框进行评分。然而我们的系统对网格单元的候选框施加空间限制这有助于缓解对同一目标的多次检测的问题。 我们的系统还生成了更少的边界框每张图像只有98个而选择性搜索则有约2000个。最后我们的系统将这些单独的组件(individual components)组合成一个单一的、共同优化的模型。
其它快速检测器。 Fast R-CNN 和 Faster R-CNN 通过共享计算和使用神经网络替代选择性搜索[14][28]来提出候选区域来加速 R-CNN 框架。虽然它们提供了比 R-CNN 更快的速度和更高的准确度但仍然不能达到实时性能。
许多研究工作集中在加快DPM流程上[31] [38] [5]。它们加速HOG计算使用级联(cascades)并将计算推动到多个GPU上。但是实际上只有30Hz的DPM [31]可以实时运行。
YOLO并没有试图优化大型检测流程的单个组件相反而是完全抛弃(throws out…entirely)了大型检测流程并通过设计来提高速度。
像人脸或行人等单个类别的检测器可以高度优化因为他们只需处理较少的多样性[37]。YOLO是一种通用的检测器它可以同时(simultaneously)检测多个目标。
Deep MultiBox。与R-CNN不同Szegedy等人 训练一个卷积神经网络来预测感兴趣的区域(regions of interest,ROI)[8]而不是使用选择性搜索。 MultiBox还可以通过用单个类别预测替换置信度预测来执行单个目标检测。 但是MultiBox无法执行一般的目标检测并且仍然只是较大检测流水线中的一部分需要进一步的图像补丁分类。 YOLO和MultiBox都使用卷积网络来预测图像中的边界框但YOLO是一个完整的检测系统。
OverFeat。Sermanet等人训练了一个卷积神经网络来执行定位并使该定位器进行检测[32]。OverFeat高效地执行滑动窗口检测但它仍然是一个不相交的系统(disjoint system)。OverFeat优化了定位功能而不是检测性能。像DPM一样定位器在进行预测时只能看到局部信息。OverFeat无法推断全局上下文因此需要大量的后处理来产生连贯的检测。
MultiGrasp。我们的系统在设计上类似于Redmon等[27]的抓取检测。 我们的网格边界框预测方法基于MultiGrasp系统进行回归分析。 然而抓取检测比物体检测要简单得多。 MultiGrasp只需要为包含一个目标的图像预测一个可抓取区域。 它不必估计目标的大小位置或边界或预测它的类别只需找到适合抓取的区域就可以了。 而YOLO则是预测图像中多个类的多个目标的边界框和类概率。 精读
DPM
用传统的HOG特征方法也用的是传统的支持向量机SVM分类器然后人工造一个模板再用滑动窗口方法不断的暴力搜索整个待识别图去套那个模板。这个方法比较大的问题就是在于设计模板计算量巨大而且是个静态的没办法匹配很多变化的东西鲁棒性差。
R-CNN
第一阶段每个图片使用选择性搜索SS方法提取2000个候选框。第二阶段将每个候选框送入CNN网络进行分类(使用的SVM)。
YOLO对比他们俩都很强YOLO和R-CNN也有相似的地方比如也是提取候选框YOLO的候选框就是上面说过的那98个 bounding boxes也是用到了NMS非极大值抑制也用到了CNN提取特征。
Other Fast Detectors
Fast和Faster R-CNN 这俩模型都是基于R-CNN的改版速度和精度都提升了很多但是也没办法做到实时监测也就是说FPS到不了30作者在这里并没有谈准确度的问题实际上YOLO的准确度在这里是不占优势的甚至于比他们低。
Deep MultiBox
训练卷积神经网络来预测感兴趣区域而不是使用选择性搜索。多盒也可以用单个类预测替换置信预测来执行单个目标检测。YOLO和MultiBox都使用卷积网络来预测图像中的边界框但YOLO是一个完整的检测系统。
OverFeat
OverFeat有效地执行滑动窗口检测优化了定位而不是检测性能。与DPM一样定位器在进行预测时只看到本地信息。OverFeat不能推理全局环境。
MultiGrasp
YOLO在设计上与Redmon等人的抓取检测工作相似。边界盒预测的网格方法是基于多重抓取系统的回归到抓取。
总之作者就是给前人的工作都数落一遍凸显自己模型的厉害学到了 四、Experiments—实验
4.1 Comparison to Other RealTime Systems—与其他实时系统的比较
翻译
目标检测方面的许多研究工作都集中在使标准的检测流程更快[5][38][31][14][17][28]。然而只有Sadeghi等人实际上产生了一个实时运行的检测系统每秒30帧或更好[31]。我们将YOLO与DPM的GPU实现进行了比较其在30Hz或100Hz下运行。虽然其它的算法没有达到实时性的标准我们也比较了它们的mAP和速度的关系从而探讨目标检测系统中精度和性能之间的权衡。
Fast YOLO是PASCAL上最快的目标检测方法据我们所知它是现有的最快的目标检测器。具有52.7%的mAP实时检测的精度是以前的方法的两倍以上。普通版YOLO将mAP推到63.4%的同时保持了实时性能。
我们还使用VGG-16训练YOLO。 这个模型比普通版YOLO更精确但也更慢。 它的作用是与依赖于VGG-16的其他检测系统进行比较但由于它比实时更慢所以本文的其他部分将重点放在我们更快的模型上。
最快的DPM可以在不牺牲太多mAP的情况下有效加速DPM但仍然会将实时性能降低2倍[38]。与神经网络方法相比DPM的检测精度相对较低这也是限制它的原因。
减去R的R-CNN用静态侯选边界框取代选择性搜索[20]。虽然速度比R-CNN更快但它仍然无法实时并且由于该方法无法找到好的边界框准确性受到了严重影响。
Fast R-CNN加快了R-CNN的分类阶段但它仍然依赖于选择性搜索每个图像需要大约2秒才能生成边界候选框。因此它虽然具有较高的mAP但的速度是0.5 fps仍然远未达到实时。
最近的Faster R-CNN用神经网络替代了选择性搜索来候选边界框类似于Szegedy等人[8]的方法。在我们的测试中他们最准确的模型达到了 7fps而较小的、不太准确的模型以18 fps运行。 Faster R-CNN的VGG-16版本比YOLO高出10mAP但比YOLO慢了6倍。 Zeiler-Fergus 版本的Faster R-CNN只比YOLO慢2.5倍但也不如YOLO准确。 精读
Table 1 在Pascal VOC 2007 上与其他检测方法的对比
**结论**实时目标检测FPS30YOLO最准Fast YOLO最快。 4.2 VOC 2007 Error Analysis—VOC 2007误差分析
翻译
为了进一步研究YOLO和最先进的检测器之间的差异我们详细分析了VOC 2007的分类(breakdown)结果。我们将YOLO与Fast R-CNN进行比较因为Fast R-CNN是PASCAL上性能最高的检测器之一并且它的检测代码是可公开得到的。
我们使用Hoiem等人的方法和工具[19]对于测试的每个类别我们查看该类别的前N个预测。每个预测都或是正确的或是根据错误的类型进行分类
Correct: correct class and IOU0.5Localization: correct class, 0.1IOU0.5Similar: class is similar, IOU0.1Other: class is wrong, IOU0.1Background: IOU0.1 for any object所有目标的IOU都0.1
YOLO难以正确地定位目标因此定位错误比YOLO的所有其他错误总和都要多。Fast R-CNN定位错误更少但把背景误认成目标的错误比较多。它的最高检测结果中有13.6是不包含任何目标的误报(false positive背景)。 Fast R-CNN把背景误认成目标的概率比YOLO高出3倍。 精读
本文使用HoeMm等人的方法和工具。对于测试时间的每个类别查看该类别的N个预测。每个预测要么是正确的要么是基于错误类型进行分类的 参数含义
**Correct**正确分类且预测框与ground truth的IOU大于0.5既预测对了类别预测框的位置和大小也很合适。**Localization**正确分类但预测框与ground truth的IOU大于0.1小于0.5即虽然预测对了类别但预测框的位置不是那么的严丝合缝不过也可以接受。Similar 预测了相近的类别且预测框与ground truth的IOU大于0.1。即预测的类别虽不正确但相近预测框的位置还可以接受。**Other**预测类别错误预测框与ground truth的IOU大于0.1。即预测的类别不正确但预测框还勉强把目标给框住了。**Background**预测框与ground truth的IOU小于0.1即该预测框的位置为背景没有目标。
Figure 4 显示了所有20个类中每种错误类型的平均细分情况
**结论**YOLO定位错误率高于Fast R-CNNFast R-CNN背景预测错误率高于YOLO 4.3 Combining Fast R-CNN and YOLO—Fast R-CNN与YOLO的结合
翻译
YOLO误认背景为目标的情况比Fast R-CNN少得多。 通过使用YOLO消除Fast R-CNN的背景检测我们获得了显著的性能提升。 对于R-CNN预测的每个边界框我们检查YOLO是否预测了一个相似的框。 如果确实如此那么我们会根据YOLO预测的概率和两个框之间的重叠情况提高预测值。
最好的Fast R-CNN模型在VOC 2007测试集中达到了 71.8 的mAP。 当与YOLO合并时其mAP增加了 3.2 至 75.0。 我们还尝试将顶级Fast R-CNN模型与其他几个版本的Fast R-CNN结合起来。 这写的结合的平均增长率在 0.3 至 0.6 之间。
结合YOLO后获得的性能提高不仅仅是模型集成的副产品因为结合不同版本的Fast R-CNN几乎没有什么益处。 相反正是因为YOLO在测试时出现了各种各样的错误所以它在提高Fast R-CNN的性能方面非常有效。
不幸的是这种组合不会从YOLO的速度中受益因为我们分别运行每个模型然后合并结果。 但是由于YOLO速度如此之快与Fast R-CNN相比它不会增加任何显著的计算时间。
精读
Table2 模型组合在VOC 2007上的实验结果对比
**结论**因为YOLO在测试时犯了各种错误所以它在提高快速R-CNN的性能方面非常有效。但是这种组合并不受益于YOLO的速度由于YOLO很快和Fast R-CNN相比它不增加任何有意义的计算时间。 4.4 VOC 2012 Results—VOC 2012结果
翻译
在VOC 2012测试集中YOLO的mAp得分是57.9。这比现有最先进的技术水平低更接近使用VGG-16的原始的R-CNN见表3。与其最接近的竞争对手相比我们的系统很难处理小物体上(struggles with small objects)。在瓶子、羊、电视/监视器等类别上YOLO得分比R-CNN和Feature Edit低8-10。然而在其他类别如猫和火车YOLO取得了更好的表现。
我们的Fast R-CNN YOLO模型组合是性能最高的检测方法之一。 Fast R-CNN与YOLO的组合提高了2.3在公共排行榜上提升了5个位置。
精读
Table 3 在VOC2012上mAP排序
**结论**Fast R-CNN从与YOLO的组合中得到2.3%的改进在公共排行榜上提升了5个百分点。 4.5 Generalizability: Person Detection in Artwork—泛化性图像中的人物检测
翻译
用于目标检测的学术数据集的训练和测试数据是服从同一分布的。但在现实世界的应用中很难预测所有可能的用例他的测试数据可能与系统已经看到的不同[3]。我们将YOLO与其他检测系统在毕加索(Picasso)数据集[12]和人物艺术(People-Art)数据集[3]上进行了比较这两个数据集用于测试艺术品上的人物检测。
作为参考(for reference)我们提供了VOC 2007的人形检测的AP其中所有模型仅在VOC 2007数据上训练。在Picasso数据集上测试的模型在是在VOC 2012上训练而People-Art数据集上的模型则在VOC 2010上训练。
R-CNN在VOC 2007上有很高的AP值。然而当应用于艺术图像时R-CNN显着下降。R-CNN使用选择性搜索来调整自然图像的候选边界框。R-CNN在分类器阶段只能看到小区域而且需要有很好的候选框。
DPM在应用于艺术图像时可以很好地保持其AP。之前的研究认为DPM表现良好因为它具有强大的物体形状和布局空间模型。虽然DPM不会像R-CNN那样退化但它的AP本来就很低。
YOLO在VOC 2007上表现出色其应用于艺术图像时其AP降低程度低于其他方法。与DPM一样YOLO模拟目标的大小和形状以及目标之间的关系和目标通常出现的位置之间的关系。艺术图像和自然图像在像素级别上有很大不同但它们在物体的大小和形状方面相似因此YOLO仍然可以预测好的边界框和检测结果。
精读
Figure 5 通用性Picasso 数据集和 People-Art数据集
**结论**YOLO都具有很好的检测结果 五、Real-Time Detection In The Wild—自然环境下的实时检测
翻译
YOLO是一款快速精确的物体检测器非常适合计算机视觉应用。 我们将YOLO连接到网络摄像头并验证它是否保持实时性能包括从摄像头获取图像并显示检测结果的时间。
由此产生的系统是互动的和参与的。 虽然YOLO单独处理图像但当连接到网络摄像头时它的功能类似于跟踪系统可在目标移动并在外观上发生变化时检测目标。 系统演示和源代码可在我们的项目网站上找到YOLO: Real-Time Object Detection。
精读 **结论**将YOLO连接到一个网络摄像头上并验证它是否保持了实时性能包括从摄像头中获取图像和显示检测结果的时间。结果证明效果很好如上图所示除了第二行第二个将人误判为飞机以外别的没问题。 六、Conclusion—结论
翻译
我们介绍YOLO——一种用于物体检测的统一模型。 我们的模型构造简单可以直接在完整图像上训练。 与基于分类器的方法不同YOLO是通过与检测性能直接对应的损失函数进行训练的并且整个模型是一起训练的。
快速YOLO是文献中最快的通用目标检测器YOLO推动实时对象检测的最新技术。 YOLO还能很好地推广到新领域使其成为快速鲁棒性强的应用的理想选择。
精读
到底什么是YOLO
YOLO眼里目标检测是一个回归问题一次性喂入图片然后给出bbox和分类概率简单来说只看一次就知道图中物体的类别和位置
YOLO过程总结
训练阶段:
首先将一张图像分成 S × S个 gird cell然后将它一股脑送入CNN生成S × S × (B × 5 C个结果最后根据结果求Loss并反向传播梯度下降。
预测、验证阶段
首先将一张图像分成 S × S网格(gird cell)然后将它一股脑送入CNN生成S × S × (B × 5 C个结果最后用NMS选出合适的预选框。 复现代码
复现YOLO v1 PyTorch
Paper: [1506.02640] You Only Look Once: Unified, Real-Time Object Detection (arxiv.org)
Github: EclipseR33/yolo_v1_pytorch (github.com)
数据集
VOC2007The PASCAL Visual Object Classes Challenge 2007 (VOC2007)
VOC2012The PASCAL Visual Object Classes Challenge 2012 (VOC2012)
PASCAL VOC 07/12的目录结构都是一致的因此只需要针对VOC07编写代码再扩展即可。VOC2007目录下有5个文件夹。我们需要其中的’Annotations’存有标注信息,‘ImageSets’存有train、val、test各类文件名, ‘JPEGImages’存有图像。VOC中的图像都是.jpg文件ImageSets中的文件都是.txt文件Annotations中的注释都是.xml文件。
//这是一个xml注释的示例我们需要其中的object信息
annotationfolderVOC2007/folderfilename000001.jpg/filenamesourcedatabaseThe VOC2007 Database/databaseannotationPASCAL VOC2007/annotationimageflickr/imageflickrid341012865/flickrid/sourceownerflickridFried Camels/flickridnameJinky the Fruit Bat/name/ownersizewidth353/widthheight500/heightdepth3/depth/sizesegmented0/segmentedobjectnamedog/nameposeLeft/posetruncated1/truncateddifficult0/difficultbndboxxmin48/xminymin240/yminxmax195/xmaxymax371/ymax/bndbox/objectobjectnameperson/name // name中包含的就是class信息poseLeft/posetruncated1/truncateddifficult0/difficultbndboxxmin8/xminymin12/yminxmax352/xmaxymax498/ymax/bndbox/object
/annotationfind_classes.py
首先我们需要获得VOC数据集中的所有class信息并为其编号将该信息存储到json文件中
# 路径: ./dataset/find_classes.py
import xml.etree.ElementTree as ET
from tqdm import tqdm
import json
import osdef xml2dict(xml):使用递归读取xml文件若c指向的元素是nameperson/name那么c.tag是namec.text则是person# data初始化时就已经将所有子元素的tag定义为keydata {c.tag: None for c in xml}for c in xml:# add函数用于将tag与text添加到data中def add(data, tag, text):if data[tag] is None:# data中该tag为空则直接添加textdata[tag] textelif isinstance(data[tag], list):# data中该tag为不为空且已经创建了list则append(text)data[tag].append(text)else:# data中该tag不为空但是没有创建list需要先创建listdata[tag] [data[tag], text]return dataif len(c) 0:# len(c)表示c的子元素个数若为0则表示c是叶元素没有子元素data add(data, c.tag, c.text)else:data add(data, c.tag, xml2dict(c))return datajson_path ./classes.json # json保存到的地址root rF:\AI\Dataset\VOC2012\VOCdevkit\VOC2012 # 数据集rootVOC2007与VOC2012的class信息一致
# 获取所有xml注释地址
annotation_root os.path.join(root, Annotations)
annotation_list os.listdir(annotation_root)
annotation_list [os.path.join(annotation_root, a) for a in annotation_list]s set()
for annotation in tqdm(annotation_list):xml ET.parse(os.path.join(annotation)).getroot()data xml2dict(xml)[object]if isinstance(data, list):# 有多个objectfor d in data:s.add(d[name])else:# 仅有一个objects.add(data[name])s list(s)
s.sort()# 以class名称为key可以便于xml2label的转换
data {value: i for i, value in enumerate(s)}
json_str json.dumps(data)with open(json_path, w) as f:f.write(json_str)运行./dataset/find_classes.py之后我们在指定目录下得到一个json文件
{aeroplane: 0, bicycle: 1, bird: 2, boat: 3, bottle: 4, bus: 5, car: 6, cat: 7, chair: 8, cow: 9, diningtable: 10, dog: 11, horse: 12, motorbike: 13, person: 14, pottedplant: 15, sheep: 16, sofa: 17, train: 18, tvmonitor: 19}接下来我们要开始写data.py文件主要流程
1.通过ImageSets中train.txt, val.txt, test.txt文件的指引寻找数据集对应的所有图像名与其地址。
2.编写getitem读取xml中的信息并转化为label形式读取图像并将两者都转为tensor
这里设置的dataset传出的label都是xmin, ymin, xmax, ymax, class的VOC形式并且是直接的坐标数值并没有使用百分比表示。
data.py
# 路径: ./dataset/data.py
from dataset.transform import * # 导入我们重写的transform类from torch.utils.data import Dataset
import xml.etree.ElementTree as ET
from PIL import Image
import numpy as np
import json
import osdef get_file_name(root, layout_txt):with open(os.path.join(root, layout_txt)) as layout_txt:.read() 读取文件中的数据会得到一个str字符串.split(\n) 以\n回车符为分界将str字符串分割成list[:-1] 去除最后一个空字符串文件末尾有\n分割后会有空字符串所以要去除file_name layout_txt.read().split(\n)[:-1]return file_namedef xml2dict(xml):# 这里的xml2dict与上一个文件的xml2dict一致data {c.tag: None for c in xml}for c in xml:def add(data, tag, text):if data[tag] is None:data[tag] textelif isinstance(data[tag], list):data[tag].append(text)else:data[tag] [data[tag], text]return dataif len(c) 0:data add(data, c.tag, c.text)else:data add(data, c.tag, xml2dict(c))return dataclass VOC0712Dataset(Dataset):def __init__(self, root, class_path, transforms, mode, data_rangeNone, get_infoFalse):# label: xmin, ymin, xmax, ymax, class# 从json文件中获得class的信息with open(class_path, r) as f:json_str f.read()self.classes json.loads(json_str)如果是train模式那么root的输入将为一个list长为2分别为2007、2012两年的数据集根目录 main中的root0712是一个示例。将两个root与train、val两种分割组合成四个layout_txt路径这四个路径指向VOC07/12的所有可用训练数据。如果是test模式那么只有VOC2007的test分割可用。这里也转换为list形式就可以同一两种模式的代码。layout_txt Noneif mode train:root [root[0], root[0], root[1], root[1]]layout_txt [rImageSets\Main\train.txt, rImageSets\Main\val.txt,rImageSets\Main\train.txt, rImageSets\Main\val.txt]elif mode test:if not isinstance(root, list):root [root]layout_txt [rImageSets\Main\test.txt]assert layout_txt is not None, Unknown modeself.transforms transformsself.get_info get_info # get_info表示在getitem时是否需要获得图像的名称以及图像大小信息 bool# 由于有多root所以image_list与annotation_list均存储了图像与xml文件的绝对路径self.image_list []self.annotation_list []for r, txt in zip(root, layout_txt):self.image_list [os.path.join(r, JPEGImages, t .jpg) for t in get_file_name(r, txt)]self.annotation_list [os.path.join(r, Annotations, t .xml) for t in get_file_name(r, txt)]# data_range是一个二元tuple分别表示数据集需要取哪一段区间训练时若使用全部的数据则无需传入data_range默认None的取值是会选择所有的数据的if data_range is not None:self.image_list self.image_list[data_range[0]: data_range[1]]self.annotation_list self.annotation_list[data_range[0]: data_range[1]]def __len__(self):# 返回数据集长度return len(self.annotation_list)def __getitem__(self, idx):image Image.open(self.image_list[idx])image_size image.sizelabel self.label_process(self.annotation_list[idx])if self.transforms is not None:# 由于目标检测中image的变换如随机裁剪与Resize都会导致label的变化所以需要重写transform添加部分的label处理代码image, label self.transforms(image, label)if self.get_info:return image, label, os.path.basename(self.image_list[idx]).split(.)[0], image_sizeelse:return image, labeldef label_process(self, annotation):xml ET.parse(os.path.join(annotation)).getroot()data xml2dict(xml)[object]# 根据data的两种形式将其读取到label中并将label转为numpy形式if isinstance(data, list):label [[float(d[bndbox][xmin]), float(d[bndbox][ymin]),float(d[bndbox][xmax]), float(d[bndbox][ymax]),self.classes[d[name]]]for d in data]else:label [[float(data[bndbox][xmin]), float(data[bndbox][ymin]),float(data[bndbox][xmax]), float(data[bndbox][ymax]),self.classes[data[name]]]]label np.array(label)return labelif __name__ __main__:from dataset.draw_bbox import drawroot0712 [rF:\AI\Dataset\VOC2007\VOCdevkit\VOC2007, rF:\AI\Dataset\VOC2012\VOCdevkit\VOC2012]transforms Compose([ToTensor(),RandomHorizontalFlip(0.5),Resize(448)])ds VOC0712Dataset(root0712, classes.json, transforms, train, get_infoTrue)print(len(ds))for i, (image, label, image_name, image_size) in enumerate(ds):if i 1000:continueelif i 1010:breakelse:print(label.dtype)print(tuple(image.size()[1:]))draw(image, label, ds.classes)print(VOC2007Dataset)transform.py
# 路径: ./dataset/transform.py
import torch
import torchvision
import randomclass Compose:def __init__(self, transforms):self.transforms transformsdef __call__(self, image, label):for t in self.transforms:image, label t(image, label)return image, labelclass ToTensor:def __init__(self):self.totensor torchvision.transforms.ToTensor()def __call__(self, image, label):image self.totensor(image)label torch.tensor(label)return image, labelclass RandomHorizontalFlip:def __init__(self, p0.5):self.p pdef __call__(self, image, label)::param label: xmin, ymin, xmax, ymax如果图片被水平翻转,那么label的xmin与xmax会互换变成 xmax, ymin, xmin, ymax由于YOLO的输出是(center_x, center_y, w, h) ,因此label的xmin与xmax换位不会影响损失计算与训练但是需要注意w,h计算时使用absif random.random() self.p:height, width image.shape[-2:]image image.flip(-1) # 水平翻转bbox label[:, :4]bbox[:, [0, 2]] width - bbox[:, [0, 2]]label[:, :4] bboxreturn image, labelclass Resize:def __init__(self, image_size, keep_ratioTrue)::param image_size: intkeep_ratio True 保留宽高比keep_ratio False 填充成正方形self.image_size image_sizeself.keep_ratio keep_ratiodef __call__(self, image, label)::param in_image: tensor [3, h, w]:param label: xmin, ymin, xmax, ymax:return:# 将所有图片左上角对齐构成448*448tensor的Transformh, w tuple(image.size()[1:])label[:, [0, 2]] label[:, [0, 2]] / wlabel[:, [1, 3]] label[:, [1, 3]] / hif self.keep_ratio:r_h min(self.image_size / h, self.image_size / w)r_w r_helse:r_h self.image_size / hr_w self.image_size / wh, w int(r_h * h), int(r_w * w)h, w min(h, self.image_size), min(w, self.image_size)label[:, [0, 2]] label[:, [0, 2]] * wlabel[:, [1, 3]] label[:, [1, 3]] * hT torchvision.transforms.Resize([h, w])Padding torch.nn.ZeroPad2d((0, self.image_size - w, 0, self.image_size - h))image Padding(T(image))assert list(image.size()) [3, self.image_size, self.image_size]return image, labeldraw_bbox.py
# 路径: ./dataset/draw_bbox.py
import torchvision.transforms as F
import numpy as np
from PIL import ImageDraw, ImageFont
import matplotlib.pyplot as pltcolors [Pink, Crimson, Magenta, Indigo, BlueViolet,Blue, GhostWhite, LightSteelBlue, Brown, SkyBlue,Tomato, SpringGreen, Green, Yellow, Olive,Gold, Wheat, Orange, Gray, Red]def draw(image, bbox, classes, show_confFalse, conf_th0.0)::param image: tensor:param bbox: tensor xmin, ymin, xmax, ymaxkeys list(classes.keys())values list(classes.values())# 设置字体包括大小font ImageFont.truetype(arial.ttf, 10)transform F.ToPILImage()image transform(image)draw_image ImageDraw.Draw(image)bbox np.array(bbox.cpu())for b in bbox:print(b)if show_conf and b[-2] conf_th:continuedraw_image.rectangle(list(b[:4]), outlinecolors[int(b[-1])], width3)if show_conf:draw_image.text(list(b[:2] 5), keys[values.index(int(b[-1]))] {:.2f}.format(b[-2]),fillcolors[int(b[-1])], fontfont)else:draw_image.text(list(b[:2] 5), keys[values.index(int(b[-1]))],fillcolors[int(b[-1])], fontfont)plt.figure()plt.imshow(image)plt.show()模型
darknet.py
根据论文的描述首先构建Darknet的Backbone部分
下图是Backbone的基本结构请添加图片描述,源站可能有防盗链机制,建议将图片保存下来直接上传 请添加图片描述 下文指出了darknet全部使用LeakyReLU并且参数为0.1除了最后的全连接层不需要激活函数。 # 路径: ./model/darknet.py
import torch.nn as nndef conv(in_ch, out_ch, k_size3, stride1, padding1):return nn.Sequential(nn.Conv2d(in_ch, out_ch, k_size, stride, padding, biasFalse),nn.LeakyReLU(0.1))def make_layer(param):layers []if not isinstance(param[0], list):param [param]for p in param:layers.append(conv(*p))return nn.Sequential(*layers)class Block(nn.Module):def __init__(self, param, use_poolTrue):super(Block, self).__init__()self.conv make_layer(param)self.pool nn.MaxPool2d(2)self.use_pool use_pooldef forward(self, x):x self.conv(x)if self.use_pool:x self.pool(x)return xclass DarkNet(nn.Module):def __init__(self):super(DarkNet, self).__init__()self.conv1 Block([[3, 64, 7, 2, 3]])self.conv2 Block([[64, 192, 3, 1, 1]])self.conv3 Block([[192, 128, 1, 1, 0],[128, 256, 3, 1, 1],[256, 256, 1, 1, 0],[256, 512, 3, 1, 1]])self.conv4 Block([[512, 256, 1, 1, 0],[256, 512, 3, 1, 1],[512, 256, 1, 1, 0],[256, 512, 3, 1, 1],[512, 256, 1, 1, 0],[256, 512, 3, 1, 1],[512, 256, 1, 1, 0],[256, 512, 3, 1, 1],[512, 512, 1, 1, 0],[512, 1024, 3, 1, 1]])self.conv5 Block([[1024, 512, 1, 1, 0],[512, 1024, 3, 1, 1],[1024, 512, 1, 1, 0],[512, 1024, 3, 1, 1],[1024, 1024, 3, 1, 1],[1024, 1024, 3, 2, 1]], False)self.conv6 Block([[1024, 1024, 3, 1, 1],[1024, 1024, 3, 1, 1]], False)for m in self.modules():if isinstance(m, nn.Conv2d):nn.init.kaiming_normal_(m.weight, modefan_out, nonlinearityleaky_relu)def forward(self, x):x self.conv1(x)x self.conv2(x)x self.conv3(x)x self.conv4(x)x self.conv5(x)x self.conv6(x)return xif __name__ __main__:import torchx torch.randn([1, 3, 448, 448])net DarkNet()print(net)out net(x)print(out.size())resnet.py 作者还说明了Darknet需要预训练由于在ImageNet上进行预训练耗时过长我选择使用修改过的Resnet50作为Backbone并使用Pytorch官方的预训练参数。
# 路径: ./model/resnet.py
import torch
from torchvision.models.resnet import ResNet, Bottleneck
通过继承Pytorch的ResNet代码重写其中的_forward_impl来去除最后的avgpool与fc层
此外我将Resnet50原有的layer4省略并额外增加了两个maxpool层使得Resnet输出的特征图与Darknet一致
均为[1024, 7, 7]class ResNet_(ResNet):def __init__(self, block, layers):super(ResNet_, self).__init__(blockblock, layerslayers)def _forward_impl(self, x):x self.conv1(x)x self.bn1(x)x self.relu(x)x self.maxpool(x)x self.layer1(x)x self.layer2(x)x self.maxpool(x)x self.layer3(x)x self.maxpool(x)return xdef forward(self, x):return self._forward_impl(x)def _resnet(block, layers, pretrained):model ResNet_(block, layers)if pretrained is not None:state_dict torch.load(pretrained)model.load_state_dict(state_dict)return modeldef resnet_1024ch(pretrainedNone) - ResNet:resnet _resnet(Bottleneck, [3, 4, 6, 3], pretrained)return resnetif __name__ __main__:x torch.randn([1, 3, 448, 448])net resnet_1024ch(resnet50-19c8e357.pth)print(net)y net(x)print(y.size())yolo.py
接下来复现yolo v1的目标检测核心部分。 Yolo v1的目标检测思路是将一张图像分割成S × S个grid cell每个grid cell都会预测B个bbox同时每个grid cell都会预测一个类别作为其预测出的B个bbox的共同类别。如果一个目标的中心坐标位于某个grid cell中那么这个grid cell就负责预测这个目标。一个bbox由xywhconf五个参数表示分别是中心X坐标中心Y坐标这里的x、y是相对于grid cell的x、y宽高object置信度。其中这五个参数都是百分比的表示形式x、y需要除以grid cell的宽、高w、h则需要除以整张图片的宽、高对于object conf如果没有对应bbox则label为0如果有对应bbox则label为预测出的bbox与对应bbox的IoU交并比。而且在测试时最终confobject conf * conditional class probabilities。最终结果是xywh都在0到1之间。负责预测类别的tensor长度应为class个数。模型结构上最后一个全连接层的大小则是S × S × ( B * 5 C )。 最后预测bbox的全连接层之间有Dropout层rate0.5
损失函数上YOLO v1以均方损失为为主在部分参数的损失计算上使用了一些技巧。如w、h计算中就先开根号使得小目标的损失更为显著加入λcoord5和λnoobj0.5以增加xy的损失值(0到1中开根号会让值变大)降低框出背景bbox的损失以适应该类bbox较多的状况。复现过程中xywhc的五个损失值都直接计算、class的损失值由CorssEntropyLoss函数直接计算。文中也定义了预测的bbox与label中的bbox匹配方法——IoU匹配。以IoU较大的一对bbox确定对应关系而且这个过程取走一对之后不能重复取。只有对应的bbox才能计算x y w h conf各类损失。如果一个预测bbox没有对应的label bbox那么就认为其标记到了背景只计算conf。class的损失是有负责label bbox的grid cell需要计算的。因此其计算次数只是S2次。
这里需注意计算损失时如果没有使用pytorch中官方定义的损失函数那么需要先通过sigmoid函数将模型输出限制到0~1之间才能进行直接的损失计算。
yolo.py的代码了yolo模型、yolo损失计算、yolo后处理三个部分。对前两个模块yolo.py都有相应的测试代码而后处理部分需要在test.py中测试其不会被yolo.py内部调用。
# 路径: ./model/yolo.py
import numpy as npfrom model.darknet import DarkNet
from model.resnet import resnet_1024chimport torch
import torch.nn as nn
import torchvision# yolo模型
class yolo(nn.Module):def __init__(self, s, cell_out_ch, backbone_name, pretrainNone):return: [s, s, cell_out_ch]super(yolo, self).__init__()self.s sself.backbone Noneself.conv Noneif backbone_name darknet:self.backbone DarkNet()elif backbone_name resnet:self.backbone resnet_1024ch(pretrainedpretrain)self.backbone_name backbone_nameassert self.backbone is not None, Wrong backbone nameself.fc nn.Sequential(nn.Linear(1024 * s * s, 4096),nn.LeakyReLU(0.1),nn.Dropout(0.5),nn.Linear(4096, s * s * cell_out_ch))def forward(self, x):batch_size x.size(0)x self.backbone(x)x torch.flatten(x, 1)x self.fc(x)x x.view(batch_size, self.s ** 2, -1)return x# yolo损失计算
class yolo_loss:def __init__(self, device, s, b, image_size, num_classes):self.device deviceself.s sself.b bself.image_size image_sizeself.num_classes num_classesself.batch_size 0def __call__(self, input, target)::param input: (yolo net output)tensor[s, s, b*5 n_class] bbox: b * (c_x, c_y, w, h, obj_conf), class1_p, class2_p.. %:param target: (dataset) tensor[n_bbox] bbox: x_min, ymin, xmax, ymax, class:return: loss tensorgrid type: [[bbox, ..], [], ..] - bbox_in_grid: c_x(%), c_y(%), w(%), h(%), class(int)target to grid typeif s 7 - grid idx: 1 - 49由于没有使用PyTorch的损失函数所以需要先分离不同的batch分别计算损失self.batch_size input.size(0)# label预处理target [self.label_direct2grid(target[i]) for i in range(self.batch_size)]# IoU 匹配predictor和label# 以Predictor为基准每个Predictor都有且仅有一个需要负责的Target前提是Predictor所在Grid Cell有Target中心位于此# x, y, w, h, cmatch []conf []for i in range(self.batch_size):m, c self.match_pred_target(input[i], target[i])match.append(m)conf.append(c)loss torch.zeros([self.batch_size], dtypetorch.float, deviceself.device)xy_loss torch.zeros_like(loss)wh_loss torch.zeros_like(loss)conf_loss torch.zeros_like(loss)class_loss torch.zeros_like(loss)for i in range(self.batch_size):loss[i], xy_loss[i], wh_loss[i], conf_loss[i], class_loss[i] \self.compute_loss(input[i], target[i], match[i], conf[i])return torch.mean(loss), torch.mean(xy_loss), torch.mean(wh_loss), torch.mean(conf_loss), torch.mean(class_loss)def label_direct2grid(self, label)::param label: dataset type: xmin, ymin, xmax, ymax, class:return: label: grid type, if the grid doesnt have object - put None将label转换为c_x, c_y, w, h, conf再根据不同的grid cell分类并转换成百分比形式若一个grid cell中没有label则都用None代替output [None for _ in range(self.s ** 2)]size self.image_size // self.s # h, wn_bbox label.size(0)label_c torch.zeros_like(label)label_c[:, 0] (label[:, 0] label[:, 2]) / 2label_c[:, 1] (label[:, 1] label[:, 3]) / 2label_c[:, 2] abs(label[:, 0] - label[:, 2])label_c[:, 3] abs(label[:, 1] - label[:, 3])label_c[:, 4] label[:, 4]idx_x [int(label_c[i][0]) // size for i in range(n_bbox)]idx_y [int(label_c[i][1]) // size for i in range(n_bbox)]label_c[:, 0] torch.div(torch.fmod(label_c[:, 0], size), size)label_c[:, 1] torch.div(torch.fmod(label_c[:, 1], size), size)label_c[:, 2] torch.div(label_c[:, 2], self.image_size)label_c[:, 3] torch.div(label_c[:, 3], self.image_size)for i in range(n_bbox):idx idx_y[i] * self.s idx_x[i]if output[idx] is None:output[idx] torch.unsqueeze(label_c[i], dim0)else:output[idx] torch.cat([output[idx], torch.unsqueeze(label_c[i], dim0)], dim0)return outputdef match_pred_target(self, input, target):match []conf []with torch.no_grad():input_bbox input[:, :self.b * 5].reshape(-1, self.b, 5)ious [match_get_iou(input_bbox[i], target[i], self.s, i)for i in range(self.s ** 2)]for iou in ious:if iou is None:match.append(None)conf.append(None)else:keep np.ones([len(iou[0])], dtypebool)m []c []for i in range(self.b):if np.any(keep) False:breakidx np.argmax(iou[i][keep])np_max np.max(iou[i][keep])m.append(np.argwhere(iou[i] np_max).tolist()[0][0])c.append(np.max(iou[i][keep]))keep[idx] 0match.append(m)conf.append(c)return match, confdef compute_loss(self, input, target, match, conf):# 计算损失ce_loss nn.CrossEntropyLoss()input_bbox input[:, :self.b * 5].reshape(-1, self.b, 5)input_class input[:, self.b * 5:].reshape(-1, self.num_classes)input_bbox torch.sigmoid(input_bbox)loss torch.zeros([self.s ** 2], dtypetorch.float, deviceself.device)xy_loss torch.zeros_like(loss)wh_loss torch.zeros_like(loss)conf_loss torch.zeros_like(loss)class_loss torch.zeros_like(loss)# 不同grid cell分别计算再求和for i in range(self.s ** 2):# 0 xy_loss, 1 wh_loss, 2 conf_loss, 3 class_lossl torch.zeros([4], dtypetorch.float, deviceself.device)# Negif target[i] is None:# λ_noobj 0.5obj_conf_target torch.zeros([self.b], dtypetorch.float, deviceself.device)l[2] torch.sum(torch.mul(0.5, torch.pow(input_bbox[i, :, 4] - obj_conf_target, 2)))else:# λ_coord 5l[0] torch.mul(5, torch.sum(torch.pow(input_bbox[i, :, 0] - target[i][match[i], 0], 2) torch.pow(input_bbox[i, :, 1] - target[i][match[i], 1], 2)))l[1] torch.mul(5, torch.sum(torch.pow(torch.sqrt(input_bbox[i, :, 2]) -torch.sqrt(target[i][match[i], 2]), 2) torch.pow(torch.sqrt(input_bbox[i, :, 3]) -torch.sqrt(target[i][match[i], 3]), 2)))obj_conf_target torch.tensor(conf[i], dtypetorch.float, deviceself.device)l[2] torch.sum(torch.pow(input_bbox[i, :, 4] - obj_conf_target, 2))l[3] ce_loss(input_class[i].unsqueeze(dim0).repeat(target[i].size(0), 1),target[i][:, 4].long())loss[i] torch.sum(l)xy_loss[i] torch.sum(l[0])wh_loss[i] torch.sum(l[1])conf_loss[i] torch.sum(l[2])class_loss[i] torch.sum(l[3])return torch.sum(loss), torch.sum(xy_loss), torch.sum(wh_loss), torch.sum(conf_loss), torch.sum(class_loss)def cxcywh2xyxy(bbox)::param bbox: [bbox, bbox, ..] tensor c_x(%), c_y(%), w(%), h(%), cbbox[:, 0] bbox[:, 0] - bbox[:, 2] / 2bbox[:, 1] bbox[:, 1] - bbox[:, 3] / 2bbox[:, 2] bbox[:, 0] bbox[:, 2]bbox[:, 3] bbox[:, 1] bbox[:, 3]return bboxdef match_get_iou(bbox1, bbox2, s, idx)::param bbox1: [bbox, bbox, ..] tensor c_x(%), c_y(%), w(%), h(%), c:return:if bbox1 is None or bbox2 is None:return Nonebbox1 np.array(bbox1.cpu())bbox2 np.array(bbox2.cpu())# c_x, c_y转换为对整张图片的百分比bbox1[:, 0] bbox1[:, 0] / sbbox1[:, 1] bbox1[:, 1] / sbbox2[:, 0] bbox2[:, 0] / sbbox2[:, 1] bbox2[:, 1] / s# c_x, c_y加上grid cell左上角左边变成完整坐标grid_pos [(j / s, i / s) for i in range(s) for j in range(s)]bbox1[:, 0] bbox1[:, 0] grid_pos[idx][0]bbox1[:, 1] bbox1[:, 1] grid_pos[idx][1]bbox2[:, 0] bbox2[:, 0] grid_pos[idx][0]bbox2[:, 1] bbox2[:, 1] grid_pos[idx][1]bbox1 cxcywh2xyxy(bbox1)bbox2 cxcywh2xyxy(bbox2)# %return get_iou(bbox1, bbox2)def get_iou(bbox1, bbox2)::param bbox1: [bbox, bbox, ..] tensor xmin ymin xmax ymax:param bbox2::return: area:s1 abs(bbox1[:, 2] - bbox1[:, 0]) * abs(bbox1[:, 3] - bbox1[:, 1])s2 abs(bbox2[:, 2] - bbox2[:, 0]) * abs(bbox2[:, 3] - bbox2[:, 1])ious []for i in range(bbox1.shape[0]):xmin np.maximum(bbox1[i, 0], bbox2[:, 0])ymin np.maximum(bbox1[i, 1], bbox2[:, 1])xmax np.minimum(bbox1[i, 2], bbox2[:, 2])ymax np.minimum(bbox1[i, 3], bbox2[:, 3])in_w np.maximum(xmax - xmin, 0)in_h np.maximum(ymax - ymin, 0)in_s in_w * in_hiou in_s / (s1[i] s2 - in_s)ious.append(iou)ious np.array(ious)return iousdef nms(bbox, conf_th, iou_th):bbox np.array(bbox.cpu())bbox[:, 4] bbox[:, 4] * bbox[:, 5]bbox bbox[bbox[:, 4] conf_th]order np.argsort(-bbox[:, 4])keep []while order.size 0:i order[0]keep.append(i)iou get_iou(np.array([bbox[i]]), bbox[order[1:]])[0]inds np.where(iou iou_th)[0]order order[inds 1]return bbox[keep]# yolo后处理
def output_process(output, image_size, s, b, conf_th, iou_th):输入是包含batch的模型输出:return output: list[], bbox: xmin, ymin, xmax, ymax, obj_conf, classes_conf, classesbatch_size output.size(0)size image_size // soutput torch.sigmoid(output)# Get Class# 将class conf依次添加到bbox中classes_conf, classes torch.max(output[:, :, b * 5:], dim2)classes classes.unsqueeze(dim2).repeat(1, 1, 2).unsqueeze(dim3)classes_conf classes_conf.unsqueeze(dim2).repeat(1, 1, 2).unsqueeze(dim3)bbox output[:, :, :b * 5].reshape(batch_size, -1, b, 5)bbox torch.cat([bbox, classes_conf, classes], dim3)# To Direct# 百分比形式转直接表示bbox[:, :, :, [0, 1]] bbox[:, :, :, [0, 1]] * sizebbox[:, :, :, [2, 3]] bbox[:, :, :, [2, 3]] * image_size# 添加grid cell坐标grid_pos [(j * image_size // s, i * image_size // s) for i in range(s) for j in range(s)]def to_direct(bbox):for i in range(s ** 2):bbox[i, :, 0] bbox[i, :, 0] grid_pos[i][0]bbox[i, :, 1] bbox[i, :, 1] grid_pos[i][1]return bboxbbox_direct torch.stack([to_direct(b) for b in bbox])bbox_direct bbox_direct.reshape(batch_size, -1, 7)# cxcywh to xyxybbox_direct[:, :, 0] bbox_direct[:, :, 0] - bbox_direct[:, :, 2] / 2bbox_direct[:, :, 1] bbox_direct[:, :, 1] - bbox_direct[:, :, 3] / 2bbox_direct[:, :, 2] bbox_direct[:, :, 0] bbox_direct[:, :, 2]bbox_direct[:, :, 3] bbox_direct[:, :, 1] bbox_direct[:, :, 3]bbox_direct[:, :, 0] torch.maximum(bbox_direct[:, :, 0], torch.zeros(1))bbox_direct[:, :, 1] torch.maximum(bbox_direct[:, :, 1], torch.zeros(1))bbox_direct[:, :, 2] torch.minimum(bbox_direct[:, :, 2], torch.tensor([image_size]))bbox_direct[:, :, 3] torch.minimum(bbox_direct[:, :, 3], torch.tensor([image_size]))# 整合不同batch中的bboxbbox [torch.tensor(nms(b, conf_th, iou_th)) for b in bbox_direct]bbox torch.stack(bbox)return bboxif __name__ __main__:import torch# Test yolox torch.randn([1, 3, 448, 448])# B * 5 n_classesnet yolo(7, 2 * 5 20, resnet, pretrainNone)# net yolo(7, 2 * 5 20, darknet, pretrainNone)print(net)out net(x)print(out)print(out.size())# Test yolo_loss# 测试时假设 s2, class2s 2b 2image_size 448 # h, winput torch.tensor([[[0.45, 0.24, 0.22, 0.3, 0.35, 0.54, 0.66, 0.7, 0.8, 0.8, 0.17, 0.9],[0.37, 0.25, 0.5, 0.3, 0.36, 0.14, 0.27, 0.26, 0.33, 0.36, 0.13, 0.9],[0.12, 0.8, 0.26, 0.74, 0.8, 0.13, 0.83, 0.6, 0.75, 0.87, 0.75, 0.24],[0.1, 0.27, 0.24, 0.37, 0.34, 0.15, 0.26, 0.27, 0.37, 0.34, 0.16, 0.93]]])target [torch.tensor([[200, 200, 353, 300, 1],[220, 230, 353, 300, 1],[15, 330, 200, 400, 0],[100, 50, 198, 223, 1],[30, 60, 150, 240, 1]], dtypetorch.float)]criterion yolo_loss(cpu, 2, 2, image_size, 2)loss criterion(input, target)print(loss)scheduler.py 论文作者也给出了他的训练参数。我们首先复现学习率的调整方式。原文分成三段学习率每一段都有不同的保持epochs长度。此外我额外增加了热身训练阶段。
# 路径: ./scheduler.py
from torch.optim.lr_scheduler import _LRSchedulerclass Scheduler(_LRScheduler):def __init__(self, optimizer, step_warm_ep, lr_start, step_1_lr, step_1_ep,step_2_lr, step_2_ep, step_3_lr, step_3_ep, last_epoch-1):self.optimizer optimizerself.lr_start lr_startself.step_warm_ep step_warm_epself.step_1_lr step_1_lrself.step_1_ep step_1_epself.step_2_lr step_2_lrself.step_2_ep step_2_epself.step_3_lr step_3_lrself.step_3_ep step_3_epself.last_epoch last_epochsuper(Scheduler, self).__init__(optimizer, last_epoch)def get_lr(self):if self.last_epoch 0:return [self.lr_start for _ in self.optimizer.param_groups]lr self._compute_lr_from_epoch()return [lr for _ in self.optimizer.param_groups]def _get_closed_form_lr(self):return self.base_lrsdef _compute_lr_from_epoch(self):if self.last_epoch self.step_warm_ep:lr ((self.step_1_lr - self.lr_start)/self.step_warm_ep) * self.last_epoch self.lr_startelif self.last_epoch self.step_warm_ep self.step_1_ep:lr self.step_1_lrelif self.last_epoch self.step_warm_ep self.step_1_ep self.step_2_ep:lr self.step_2_lrelif self.last_epoch self.step_warm_ep self.step_1_ep self.step_2_ep self.step_3_ep:lr self.step_3_lrelse:lr self.step_3_lrreturn lrif __name__ __main__:import torch.nn as nnimport torch.optim as optimimport numpy as npimport matplotlib.pyplot as pltimport warningswarnings.filterwarnings(ignore)batch_size 16epoch 135scheduler_params {lr_start: 1e-3,step_warm_ep: 10,step_1_lr: 1e-2,step_1_ep: 75,step_2_lr: 1e-3,step_2_ep: 30,step_3_lr: 1e-4,step_3_ep: 20}model nn.Sequential(nn.Linear(1, 10),nn.Linear(10, 1))optimizer optim.SGD(model.parameters(), lrscheduler_params[lr_start])scheduler Scheduler(optimizer, **scheduler_params)lrs []for _ in range(epoch):lrs.append(optimizer.param_groups[0][lr])scheduler.step()print(lrs)lrs np.array(lrs)# 使用plt可视化学习率plt.figure()plt.plot(lrs)plt.show()train.py
训练参数主要参考了论文原文。由于batch size较小学习率都等比减小。优化器选择了SGD并带有momentum与weight_decay。代码中有冻结Backbone的选项。 root0712 存储了VOC07/12两年数据集的根目录 model_root 存储模型路径 backbone resnet’则代表使用修改后的resnet;darknet’则代表使用原文中的darknet pretrain None则不使用预训练模型;或直接输入预训练权重地址 with_amp 混合精度选项 transforms 需要使用代码中定义的transforms而不是PyTorch直接给出的transforms start_epoch 中断训练重启的开始epoch epoch 总epoch数 freeze_backbone_till 若为-1则不冻结backbone其他数则会冻结backbone直到该epoch后解冻 # 路径: ./train.py
from dataset.data import VOC0712Dataset
from dataset.transform import *
from model.yolo import yolo, yolo_loss
from scheduler import Schedulerimport torch
from torch.utils.data import DataLoader
from torch import optim
from torch.cuda.amp import autocast, GradScalerfrom tqdm import tqdm
import pandas as pd
import json
import osimport warningsclass CFG:device cuda:0 if torch.cuda.is_available() else cpuroot0712 [rF:\AI\Dataset\VOC2007\VOCdevkit\VOC2007, rF:\AI\Dataset\VOC2012\VOCdevkit\VOC2012]class_path r./dataset/classes.jsonmodel_root r./log/ex7# 若model_path是一个指向权重文件的str路径那么会将模型传入指定模型权重model_path None# 这里没有编写创建文件夹的代码直接运行需要手动将文件夹创建好backbone resnetpretrain model/resnet50-19c8e357.pth# 混合精度可选若为False则采用常规精度with_amp TrueS 7B 2image_size 448transforms Compose([ToTensor(),RandomHorizontalFlip(0.5),Resize(448, keep_ratioFalse)])start_epoch 0epoch 135batch_size 16num_workers 2# freeze_backbone_till -1 则不冻结freeze_backbone_till 30scheduler_params {lr_start: 1e-3 / 4,step_warm_ep: 10,step_1_lr: 1e-2 / 4,step_1_ep: 75,step_2_lr: 1e-3 / 4,step_2_ep: 40,step_3_lr: 1e-4 / 4,step_3_ep: 10}momentum 0.9weight_decay 0.0005def collate_fn(batch):return tuple(zip(*batch))class AverageMeter:def __init__(self):self.reset()def reset(self):self.val 0self.avg 0self.sum 0self.count 0def update(self, val, n1):self.val valself.sum val * nself.count nself.avg self.sum / self.countdef train():device torch.device(CFG.device)print(Train:\nDevice:{}.format(device))with open(CFG.class_path, r) as f:json_str f.read()classes json.loads(json_str)CFG.num_classes len(classes)train_ds VOC0712Dataset(CFG.root0712, CFG.class_path, CFG.transforms, train)test_ds VOC0712Dataset(CFG.root0712, CFG.class_path, CFG.transforms, test)train_dl DataLoader(train_ds, batch_sizeCFG.batch_size, shuffleTrue,num_workersCFG.num_workers, collate_fncollate_fn)test_dl DataLoader(test_ds, batch_sizeCFG.batch_size, shuffleFalse,num_workersCFG.num_workers, collate_fncollate_fn)yolo_net yolo(sCFG.S, cell_out_chCFG.B * 5 CFG.num_classes, backbone_nameCFG.backbone, pretrainCFG.pretrain)yolo_net.to(device)if CFG.model_path is not None:yolo_net.load_state_dict(torch.load(CFG.model_path))if CFG.freeze_backbone_till ! -1:print(Freeze Backbone)for param in yolo_net.backbone.parameters():param.requires_grad_(False)param [p for p in yolo_net.parameters() if p.requires_grad]optimizer optim.SGD(param, lrCFG.scheduler_params[lr_start],momentumCFG.momentum, weight_decayCFG.weight_decay)criterion yolo_loss(CFG.device, CFG.S, CFG.B, CFG.image_size, len(train_ds.classes))scheduler Scheduler(optimizer, **CFG.scheduler_params)scaler GradScaler()for _ in range(CFG.start_epoch):scheduler.step()best_train_loss 1e9train_losses []test_losses []lrs []for epoch in range(CFG.start_epoch, CFG.epoch):if CFG.freeze_backbone_till ! -1 and epoch CFG.freeze_backbone_till:print(Unfreeze Backbone)for param in yolo_net.backbone.parameters():param.requires_grad_(True)CFG.freeze_backbone_till -1# Trainyolo_net.train()loss_score AverageMeter()dl tqdm(train_dl, totallen(train_dl))for images, labels in dl:batch_size len(labels)images torch.stack(images)images images.to(device)labels [label.to(device) for label in labels]optimizer.zero_grad()if CFG.with_amp:with autocast():outputs yolo_net(images)loss, xy_loss, wh_loss, conf_loss, class_loss criterion(outputs, labels)scaler.scale(loss).backward()scaler.step(optimizer)scaler.update()else:outputs yolo_net(images)loss, xy_loss, wh_loss, conf_loss, class_loss criterion(outputs, labels)loss.backward()optimizer.step()loss_score.update(loss.detach().item(), batch_size)dl.set_postfix(ModeTrain, AvgLossloss_score.avg, Lossloss.detach().item(),Epochepoch, LRoptimizer.param_groups[0][lr])lrs.append(optimizer.param_groups[0][lr])scheduler.step()train_losses.append(loss_score.avg)print(Train Loss: {:.4f}.format(loss_score.avg))if best_train_loss loss_score.avg:print(Save yolo_net to {}.format(os.path.join(CFG.model_root, yolo.pth)))torch.save(yolo_net.state_dict(), os.path.join(CFG.model_root, yolo.pth))best_train_loss loss_score.avgloss_score.reset()with torch.no_grad():# Testyolo_net.eval()dl tqdm(test_dl, totallen(test_dl))for images, labels in dl:batch_size len(labels)images torch.stack(images)images images.to(device)labels [label.to(device) for label in labels]outputs yolo_net(images)loss, xy_loss, wh_loss, conf_loss, class_loss criterion(outputs, labels)loss_score.update(loss.detach().item(), batch_size)dl.set_postfix(ModeTest, AvgLossloss_score.avg, Lossloss.detach().item(), Epochepoch)test_losses.append(loss_score.avg)print(Test Loss: {:.4f}.format(loss_score.avg))df pd.DataFrame({Train Loss: train_losses, Test Loss: test_losses, LR: lrs})df.to_csv(os.path.join(CFG.model_root, result.csv), indexTrue)if __name__ __main__:warnings.filterwarnings(ignore)train()voc_eval.py
测试时计算mAP的代码
# 路径: ./voc_eval.py
Adapted from:longcw faster_rcnn_pytorch: https://github.com/longcw/faster_rcnn_pytorchrbgirshick py-faster-rcnn https://github.com/rbgirshick/py-faster-rcnnLicensed under The MIT License [see LICENSE for details]这个文件的代码有改动我删除了缓存的代码因此也不需要传入缓存文件夹
import xml.etree.ElementTree as ET
import numpy as np
import osdef voc_ap(rec, prec, use_07_metricFalse): ap voc_ap(rec, prec, [use_07_metric])Compute VOC AP given precision and recall.If use_07_metric is true, uses theVOC 07 11 point method (default:False).if use_07_metric:# 11 point metricap 0.for t in np.arange(0., 1.1, 0.1):if np.sum(rec t) 0:p 0else:p np.max(prec[rec t])ap ap p / 11.else:# correct AP calculation# first append sentinel values at the endmrec np.concatenate(([0.], rec, [1.]))mpre np.concatenate(([0.], prec, [0.]))# compute the precision envelopefor i in range(mpre.size - 1, 0, -1):mpre[i - 1] np.maximum(mpre[i - 1], mpre[i])# to calculate area under PR curve, look for points# where X axis (recall) changes valuei np.where(mrec[1:] ! mrec[:-1])[0]# and sum (\Delta recall) * precap np.sum((mrec[i 1] - mrec[i]) * mpre[i 1])return apdef parse_rec(filename): Parse a PASCAL VOC xml file tree ET.parse(filename)objects []for obj in tree.findall(object):obj_struct {}obj_struct[name] obj.find(name).textobj_struct[pose] obj.find(pose).textobj_struct[truncated] int(obj.find(truncated).text)obj_struct[difficult] int(obj.find(difficult).text)bbox obj.find(bndbox)obj_struct[bbox] [int(bbox.find(xmin).text),int(bbox.find(ymin).text),int(bbox.find(xmax).text),int(bbox.find(ymax).text)]objects.append(obj_struct)return objectsdef voc_eval(detpath,annopath,imagesetfile,classname,ovthresh0.5,use_07_metricFalse):rec, prec, ap voc_eval(detpath,annopath,imagesetfile,classname,[ovthresh],[use_07_metric])Top level function that does the PASCAL VOC evaluation.detpath: Path to detectionsdetpath.format(classname) should produce the detection results file.annopath: Path to annotationsannopath.format(imagename) should be the xml annotations file.imagesetfile: Text file containing the list of images, one image per line.classname: Category name (duh)cachedir: Directory for caching the annotations[ovthresh]: Overlap threshold (default 0.5)[use_07_metric]: Whether to use VOC07s 11 point AP computation(default False)# assumes detections are in detpath.format(classname)# assumes annotations are in annopath.format(imagename)# assumes imagesetfile is a text file with each line an image name# cachedir caches the annotations in a pickle file# read list of imageswith open(imagesetfile, r) as f:lines f.readlines()imagenames [x.strip() for x in lines]# load annotsrecs {}for i, imagename in enumerate(imagenames):recs[imagename] parse_rec(annopath.format(imagename))# if i % 100 0:# print(Reading annotation for {:d}/{:d}.format(# i 1, len(imagenames)))# extract gt objects for this classclass_recs {}npos 0for imagename in imagenames:R [obj for obj in recs[imagename] if obj[name] classname]bbox np.array([x[bbox] for x in R])difficult np.array([x[difficult] for x in R]).astype(np.bool)det [False] * len(R)npos npos sum(~difficult)class_recs[imagename] {bbox: bbox,difficult: difficult,det: det}# read detsdetfile detpath.format(classname)with open(detfile, r) as f:lines f.readlines()splitlines [x.strip().split( ) for x in lines]image_ids [x[0] for x in splitlines]confidence np.array([float(x[1]) for x in splitlines])BB np.array([[float(z) for z in x[2:]] for x in splitlines])# sort by confidencesorted_ind np.argsort(-confidence)sorted_scores np.sort(-confidence)BB BB[sorted_ind, :]image_ids [image_ids[x] for x in sorted_ind]# go down dets and mark TPs and FPsnd len(image_ids)tp np.zeros(nd)fp np.zeros(nd)for d in range(nd):R class_recs[image_ids[d]]bb BB[d, :].astype(float)ovmax -np.infBBGT R[bbox].astype(float)if BBGT.size 0:# compute overlaps# intersectionixmin np.maximum(BBGT[:, 0], bb[0])iymin np.maximum(BBGT[:, 1], bb[1])ixmax np.minimum(BBGT[:, 2], bb[2])iymax np.minimum(BBGT[:, 3], bb[3])iw np.maximum(ixmax - ixmin 1., 0.)ih np.maximum(iymax - iymin 1., 0.)inters iw * ih# unionuni ((bb[2] - bb[0] 1.) * (bb[3] - bb[1] 1.) (BBGT[:, 2] - BBGT[:, 0] 1.) *(BBGT[:, 3] - BBGT[:, 1] 1.) - inters)overlaps inters / uniovmax np.max(overlaps)jmax np.argmax(overlaps)if ovmax ovthresh:if not R[difficult][jmax]:if not R[det][jmax]:tp[d] 1.R[det][jmax] 1else:fp[d] 1.else:fp[d] 1.# compute precision recallfp np.cumsum(fp)tp np.cumsum(tp)rec tp / float(npos)# avoid divide by zero in case the first detection matches a difficult# ground truthprec tp / np.maximum(tp fp, np.finfo(np.float64).eps)ap voc_ap(rec, prec, use_07_metric)return rec, prec, aptest.py
# 路径: ./test.py
from dataset.data import VOC0712Dataset, Compose, ToTensor, Resize
from dataset.draw_bbox import draw
from model.yolo import yolo, output_process
from voc_eval import voc_evalimport torchfrom tqdm import tqdm
import jsonclass CFG:device cuda:0 if torch.cuda.is_available() else cpuroot rF:\AI\Dataset\VOC2007\VOCdevkit\VOC2007class_path rdataset/classes.jsonmodel_path rlog/ex7/yolo.pth# 这里也要手动新建det文件夹用于保存每个class的目标情况detpath rdet\{}.txtannopath rF:\AI\Dataset\VOC2007\VOCdevkit\VOC2007\Annotations\{}.xmlimagesetfile rF:\AI\Dataset\VOC2007\VOCdevkit\VOC2007\ImageSets\Main\test.txtclassname Nonetest_range Noneshow_image Falseget_ap Truebackbone resnetS 7B 2image_size 448get_info Truetransforms Compose([ToTensor(),Resize(448, keep_ratioFalse)])num_classes 0conf_th 0.2iou_th 0.5def test():device torch.device(CFG.device)print(Test:\nDevice:{}.format(device))dataset VOC0712Dataset(CFG.root, CFG.class_path, CFG.transforms, test,data_rangeCFG.test_range, get_infoCFG.get_info)with open(CFG.class_path, r) as f:json_str f.read()classes json.loads(json_str)CFG.classname list(classes.keys())CFG.num_classes len(CFG.classname)yolo_net yolo(sCFG.S, cell_out_chCFG.B * 5 CFG.num_classes, backbone_nameCFG.backbone)yolo_net.to(device)yolo_net.load_state_dict(torch.load(CFG.model_path))bboxes []with torch.no_grad():for image, label, image_name, input_size in tqdm(dataset):image image.unsqueeze(dim0)image image.to(device)output yolo_net(image)output output_process(output.cpu(), CFG.image_size, CFG.S, CFG.B, CFG.conf_th, CFG.iou_th)if CFG.show_image:draw(image.squeeze(dim0), output.squeeze(dim0), classes, show_confTrue)draw(image.squeeze(dim0), label, classes, show_confTrue)# 还原output[:, :, [0, 2]] output[:, :, [0, 2]] * input_size[0] / CFG.image_sizeoutput[:, :, [1, 3]] output[:, :, [1, 3]] * input_size[1] / CFG.image_sizeoutput output.squeeze(dim0).numpy().tolist()if len(output) 0:pred [[image_name, output[i][-3] * output[i][-2]] output[i][:4] [int(output[i][-1])]for i in range(len(output))]bboxes preddet_list [[] for _ in range(CFG.num_classes)]for b in bboxes:det_list[b[-1]].append(b[:-1])if CFG.get_ap:map 0for idx in range(CFG.num_classes):file_path CFG.detpath.format(CFG.classname[idx])txt \n.join([ .join([str(i) for i in item]) for item in det_list[idx]])with open(file_path, w) as f:f.write(txt)rec, prec, ap voc_eval(CFG.detpath, CFG.annopath, CFG.imagesetfile, CFG.classname[idx])print(rec)print(prec)map apprint(ap)map / CFG.num_classesprint(mAP, map)if __name__ __main__:test()