当前位置: 首页 > news >正文

目标检测—YOLO系列(二 ) 全面解读论文与复现代码YOLOv1 PyTorch

精读论文

前言

从这篇开始,我们将进入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-art,SOTA)检测系统相比,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 models(DPM,可变形部分模型)这样的系统使用滑动窗口方法,其分类器在整个图像上均匀间隔的位置上运行[10]。

最近的方法,如R-CNN使用region proposal(区域候选)策略,首先在图像中生成潜在的边界框(bounding box),然后在这些框上运行分类器。在分类之后,执行用于细化边界框的后处理,消除重复的检测,并根据场景中的其它目标为边界框重新打分[13]。这些复杂的流程是很慢,很难优化的,因为每个独立的部分都必须单独进行训练

**我们将目标检测看作是一个单一的回归问题,直接从图像像素得到边界框坐标和类别概率。**使用我们的系统——You Only Look Once(YOLO),便能得到图像上的物体是什么和物体的具体位置。

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处理步骤:

img

(1)将输入图像的大小调整为448×448,分割得到7*7网格;

(2)通过CNN提取特征和预测;

(3)利用非极大值抑制(NMS)进行筛选


YOLO的定义:

YOLO将目标检测重新定义为单个回归问题从图像像素直接到边界框坐标和类概率。YOLO可以在一个图像来预测:哪些对象是存在的?它们在哪里?

如 Figure 1:将图像输入单独的一个 CNN 网络,就会预测出 bounding box,以及这些 bounding box 所属类别的概率。

YOLO 用一整幅图像来训练,同时可以直接优化性能检测。

性能检测对比:

img


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)。这些置信度分数反映了该模型对那个框内是否包含目标的置信度,以及它对自己的预测的准确度的估量。在形式上,我们将置信度定义为 confidence=Pr(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)

img

● x,y: 是指bounding box的预测框的中心坐标相较于该bounding box归属的grid cell左上角的偏移量,在0-1之间。

img

在上图中,绿色虚线框代表grid cell,绿点表示该grid cell的左上角坐标,为(0,0);红色和蓝色框代表该grid cell包含的两个bounding box,红点和蓝点表示这两个bounding box的中心坐标。有一点很重要,bounding box的中心坐标一定在该grid cell内部,因此,红点和蓝点的坐标可以归一化在0-1之间。在上图中,红点的坐标为(0.5,0.5),即x=y=0.5,蓝点的坐标为(0.9,0.9),即x=y=0.9。

● w,h: 是指该bounding box的宽和高,但也归一化到了0-1之间,表示相较于原始图像的宽和高(即448个像素)。比如该bounding box预测的框宽是44.8个像素,高也是44.8个像素,则w=0.1,h=0.1。

img

红框的x=0.8,y=0.5,w=0.1,h=0.2。

(3)不管框 B 的数量是多少,只负责预测一个目标

(4)预测 C 个条件概率类别(物体属于每一种类别的可能性)

img

**综上,S×S 个网格,每个网格要预测 B个bounding box (中间上图),还要预测 C 个类(中间下图)。**将两图合并,网络输出就是一个 S × S × (5×B+C)。(S x S个网格,每个网格都有B个预测框,每个框又有5个参数,再加上每个网格都有C个预测类)

Q1:为什么每个网格有固定的B个bounding box?(即B=2)

在训练的时候会在线地计算每个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个 不同长宽比和scale),YOLOv2是统计分析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×7=49个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。

img


Q:7×7×30怎么来的?

张量剖面图

img

(图片来源:YOLO v1详细解读_yolov1详解_迪菲赫尔曼的博客-CSDN博客)

  • 7×7: 一共划分成7×7的网格。
  • 30: 30包含了两个预测框的参数和Pascal VOC的类别参数:每个预测框有5个参数:x,y,w,h,confidence。另外,Pascal VOC里面还有20个类别;所以最后的30实际上是由5x2+20组成的,也就是说这一个30维的向量就是一个gird cell的信息。
  • 7×7×30: 总共是7 × 7个gird cell一共就是7 × 7 ×(2 × 5+ 20)= 7 × 7 × 30 tensor = 1470 outputs,正好对应论文。

网络详解

(1)YOLO主要是建立一个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λcoord=5 和 λ n o o b j = 0.5 λ_{noobj}=0.5λnoobj=0.5。

平方和误差对大框和小框的误差权衡是一样的,而我们的错误指标(error metric)应该要体现出,大框的小偏差的重要性不如小框的小偏差的重要性。为了部分解决这个问题,我们直接预测边界框宽度和高度的平方根,而不是宽度和高度。

YOLO为每个网格单元预测多个边界框。在训练时,每个目标我们只需要一个边界框预测器来负责。若某预测器的预测值与目标的实际值的IOU值最高,则这个预测器被指定为“负责”预测该目标。这导致边界框预测器的专业化。每个预测器可以更好地预测特定大小,方向角,或目标的类别,从而改善整体召回率(recall)。

在训练期间,我们优化以下多部分损失函数:

img

注意,如果目标存在于该网格单元中(前面讨论的条件类别概率),则损失函数仅惩罚(penalizes)分类错误。如果预测器“负责”实际边界框(即该网格单元中具有最高IOU的预测器),则它也仅惩罚边界框坐标错误。

我们用Pascal VOC 2007和2012的训练集和验证数据集进行了大约 135个epoch 的网络训练。因为我们仅在Pascal VOC 2012上进行测试,所以我们的训练集里包含了Pascal VOC 2007的测试数据。在整个训练过程中,我们使用:batch size=64,momentum=0.9,decay=0.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的图像呢?

主要原因是加入了平均池化层,这样不论输入尺寸是多少,在和最后的全连接层连接时都可以保证相同的神经元数目。

img


训练检测网络

经过上一步的预训练,就已经把主干网络的前20个卷积层给训练好了,前20层的参数已经学到了图片的特征。接下来的步骤本质就是迁移学习,在训练好的前20层卷积层后加上4层卷积层和2层全连接层,然后在目标检测的任务上进行迁移学习。

在整个网络(24+2)的训练过程中,除最后一层采用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中具体操作

img

(1)对于上述的98列数据,先看某一个类别,也就是只看98列的这一行所有数据,先拿出最大值概率的那个框,剩下的每一个都与它做比较,如果两者的IoU大于某个阈值,则认为这俩框重复识别了同一个物体,就将其中低概率的重置成0。

(2)最大的那个框和其他的框比完之后,再从剩下的框找最大的,继续和其他的比,依次类推对所有类别进行操作。 注意,这里不能直接选择最大的,因为有可能图中有多个该类别的物体,所以IoU如果小于某个阈值,则会被保留。

(3)最后得到一个稀疏矩阵,因为里面有很多地方都被重置成0,拿出来不是0的地方拿出来概率和类别,就得到最后的目标检测结果了。

注意: NMS只发生在预测阶段,训练阶段是不能用NMS的,因为在训练阶段不管这个框是否用于预测物体的,他都和损失函数相关,不能随便重置成0。


损失函数

损失函数包括:

localization loss -> 坐标损失

confidence loss -> 置信度损失

classification loss -> 分类损失

img

损失函数详解:

(1)坐标损失

img

  • 第一行: 负责检测物体的框中心点(x, y)定位误差。
  • 第二行: 负责检测物体的框的高宽(w,h)定位误差,这个根号的作用就是为了修正对大小框一视同仁的缺点,削弱大框的误差。

Q:为啥加根号?

img

在上图中,大框和小框的bounding box和ground truth都是差了一点,但对于实际预测来讲,大框(大目标)差的这一点也许没啥事儿,而小框(小目标)差的这一点可能就会导致bounding box的方框和目标差了很远。而如果还是使用第一项那样直接算平方和误差,就相当于把大框和小框一视同仁了,这样显然不合理。而如果使用开根号处理,就会一定程度上改善这一问题 。

img

这样一来,同样是差一点,小框产生的误差会更大,即对小框惩罚的更严重。

(2)置信度损失

img

  • 第一行: 负责检测物体的那个框的置信度误差。
  • 第二行: 不负责检测物体的那个框的置信度误差。

(3)分类损失

img 负责检测物体的grid cell分类的误差。


特殊符号的含义:

img


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 上与其他检测方法的对比img

**结论:**实时目标检测(FPS>30),YOLO最准,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 IOU>0.5
  • Localization: correct class, 0.1<IOU<0.5
  • Similar: class is similar, IOU>0.1
  • Other: class is wrong, IOU>0.1
  • Background: IOU<0.1 for any object(所有目标的IOU都<0.1)

YOLO难以正确地定位目标,因此定位错误比YOLO的所有其他错误总和都要多。Fast R-CNN定位错误更少,但把背景误认成目标的错误比较多。它的最高检测结果中有13.6%是不包含任何目标的误报(false positive,背景)。 Fast R-CNN把背景误认成目标的概率比YOLO高出3倍。


精读

本文使用HoeMm等人的方法和工具。对于测试时间的每个类别,查看该类别的N个预测。每个预测要么是正确的,要么是基于错误类型进行分类的:

img

参数含义:

  • **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个类中每种错误类型的平均细分情况img

**结论:**YOLO定位错误率高于Fast R-CNN;Fast 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上的实验结果对比img

**结论:**因为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排序img

**结论:**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数据集)img

**结论:**YOLO都具有很好的检测结果


五、Real-Time Detection In The Wild—自然环境下的实时检测

翻译

YOLO是一款快速,精确的物体检测器,非常适合计算机视觉应用。 我们将YOLO连接到网络摄像头,并验证它是否保持实时性能,包括从摄像头获取图像并显示检测结果的时间。

由此产生的系统是互动的和参与的。 虽然YOLO单独处理图像,但当连接到网络摄像头时,它的功能类似于跟踪系统,可在目标移动并在外观上发生变化时检测目标。 系统演示和源代码可在我们的项目网站上找到:YOLO: Real-Time Object Detection。

精读

img

**结论:**将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)

数据集

VOC2007:The PASCAL Visual Object Classes Challenge 2007 (VOC2007)

VOC2012:The 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>信息
<annotation><folder>VOC2007</folder><filename>000001.jpg</filename><source><database>The VOC2007 Database</database><annotation>PASCAL VOC2007</annotation><image>flickr</image><flickrid>341012865</flickrid></source><owner><flickrid>Fried Camels</flickrid><name>Jinky the Fruit Bat</name></owner><size><width>353</width><height>500</height><depth>3</depth></size><segmented>0</segmented><object><name>dog</name><pose>Left</pose><truncated>1</truncated><difficult>0</difficult><bndbox><xmin>48</xmin><ymin>240</ymin><xmax>195</xmax><ymax>371</ymax></bndbox></object><object><name>person</name>		// name中包含的就是class信息<pose>Left</pose><truncated>1</truncated><difficult>0</difficult><bndbox><xmin>8</xmin><ymin>12</ymin><xmax>352</xmax><ymax>498</ymax></bndbox></object>
</annotation>
find_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指向的元素是<name>person</name>,那么c.tag是name,c.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 = r'F:\AI\Dataset\VOC2012\VOCdevkit\VOC2012'	# 数据集root(VOC2007与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_range=None, get_info=False):# 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 = [r'ImageSets\Main\train.txt', r'ImageSets\Main\val.txt',r'ImageSets\Main\train.txt', r'ImageSets\Main\val.txt']elif mode == 'test':if not isinstance(root, list):root = [root]layout_txt = [r'ImageSets\Main\test.txt']assert layout_txt is not None, 'Unknown mode'self.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 = [r'F:\AI\Dataset\VOC2007\VOCdevkit\VOC2007', r'F:\AI\Dataset\VOC2012\VOCdevkit\VOC2012']transforms = Compose([ToTensor(),RandomHorizontalFlip(0.5),Resize(448)])ds = VOC0712Dataset(root0712, 'classes.json', transforms, 'train', get_info=True)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, p=0.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计算时使用abs"""if 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_ratio=True):""":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, label
draw_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_conf=False, conf_th=0.0):""":param image: tensor:param bbox: tensor xmin, ymin, xmax, ymax"""keys = 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]), outline=colors[int(b[-1])], width=3)if show_conf:draw_image.text(list(b[:2] + 5), keys[values.index(int(b[-1]))] + ' {:.2f}'.format(b[-2]),fill=colors[int(b[-1])], font=font)else:draw_image.text(list(b[:2] + 5), keys[values.index(int(b[-1]))],fill=colors[int(b[-1])], font=font)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_size=3, stride=1, padding=1):return nn.Sequential(nn.Conv2d(in_ch, out_ch, k_size, stride, padding, bias=False),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_pool=True):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, mode='fan_out', nonlinearity='leaky_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__(block=block, layers=layers)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(pretrained=None) -> 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由x,y,w,h,conf五个参数表示,分别是中心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交并比。而且在测试时最终conf=object conf * conditional class probabilities。最终结果是xywh都在0到1之间。负责预测类别的tensor长度应为class个数。模型结构上最后一个全连接层的大小则是S × S × ( B * 5 + C )。
在这里插入图片描述
​ 最后预测bbox的全连接层之间有Dropout层,rate=0.5
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

损失函数上,YOLO v1以均方损失为为主,在部分参数的损失计算上使用了一些技巧。如w、h计算中就先开根号使得小目标的损失更为显著,加入λcoord=5和λnoobj=0.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, pretrain=None):"""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(pretrained=pretrain)self.backbone_name = backbone_nameassert self.backbone is not None, 'Wrong backbone name'self.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], dtype=torch.float, device=self.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 doesn't 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], dim=0)else:output[idx] = torch.cat([output[idx], torch.unsqueeze(label_c[i], dim=0)], dim=0)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])], dtype=bool)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], dtype=torch.float, device=self.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], dtype=torch.float, device=self.device)# Negif target[i] is None:# λ_noobj = 0.5obj_conf_target = torch.zeros([self.b], dtype=torch.float, device=self.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], dtype=torch.float, device=self.device)l[2] = torch.sum(torch.pow(input_bbox[i, :, 4] - obj_conf_target, 2))l[3] = ce_loss(input_class[i].unsqueeze(dim=0).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(%), c"""bbox[:, 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, classes"""batch_size = output.size(0)size = image_size // soutput = torch.sigmoid(output)# Get Class# 将class conf依次添加到bbox中classes_conf, classes = torch.max(output[:, :, b * 5:], dim=2)classes = classes.unsqueeze(dim=2).repeat(1, 1, 2).unsqueeze(dim=3)classes_conf = classes_conf.unsqueeze(dim=2).repeat(1, 1, 2).unsqueeze(dim=3)bbox = output[:, :, :b * 5].reshape(batch_size, -1, b, 5)bbox = torch.cat([bbox, classes_conf, classes], dim=3)# 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', pretrain=None)# net = yolo(7, 2 * 5 + 20, 'darknet', pretrain=None)print(net)out = net(x)print(out)print(out.size())# Test yolo_loss# 测试时假设 s=2, class=2s = 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]], dtype=torch.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(), lr=scheduler_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 'cpu'root0712 = [r'F:\AI\Dataset\VOC2007\VOCdevkit\VOC2007', r'F:\AI\Dataset\VOC2012\VOCdevkit\VOC2012']class_path = r'./dataset/classes.json'model_root = r'./log/ex7'# 若model_path是一个指向权重文件的str路径,那么会将模型传入指定模型权重model_path = None# 这里没有编写创建文件夹的代码,直接运行需要手动将文件夹创建好backbone = 'resnet'pretrain = 'model/resnet50-19c8e357.pth'# 混合精度可选,若为False则采用常规精度with_amp = TrueS = 7B = 2image_size = 448transforms = Compose([ToTensor(),RandomHorizontalFlip(0.5),Resize(448, keep_ratio=False)])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, n=1):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_size=CFG.batch_size, shuffle=True,num_workers=CFG.num_workers, collate_fn=collate_fn)test_dl = DataLoader(test_ds, batch_size=CFG.batch_size, shuffle=False,num_workers=CFG.num_workers, collate_fn=collate_fn)yolo_net = yolo(s=CFG.S, cell_out_ch=CFG.B * 5 + CFG.num_classes, backbone_name=CFG.backbone, pretrain=CFG.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, lr=CFG.scheduler_params['lr_start'],momentum=CFG.momentum, weight_decay=CFG.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 = 1e+9train_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, total=len(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(Mode='Train', AvgLoss=loss_score.avg, Loss=loss.detach().item(),Epoch=epoch, LR=optimizer.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, total=len(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(Mode='Test', AvgLoss=loss_score.avg, Loss=loss.detach().item(), Epoch=epoch)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'), index=True)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_pytorch@rbgirshick 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_metric=False):""" 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,ovthresh=0.5,use_07_metric=False):"""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 VOC07's 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, ap

test.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 'cpu'root = r'F:\AI\Dataset\VOC2007\VOCdevkit\VOC2007'class_path = r'dataset/classes.json'model_path = r'log/ex7/yolo.pth'# 这里也要手动新建det文件夹,用于保存每个class的目标情况detpath = r'det\{}.txt'annopath = r'F:\AI\Dataset\VOC2007\VOCdevkit\VOC2007\Annotations\{}.xml'imagesetfile = r'F:\AI\Dataset\VOC2007\VOCdevkit\VOC2007\ImageSets\Main\test.txt'classname = Nonetest_range = Noneshow_image = Falseget_ap = Truebackbone = 'resnet'S = 7B = 2image_size = 448get_info = Truetransforms = Compose([ToTensor(),Resize(448, keep_ratio=False)])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_range=CFG.test_range, get_info=CFG.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(s=CFG.S, cell_out_ch=CFG.B * 5 + CFG.num_classes, backbone_name=CFG.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(dim=0)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(dim=0), output.squeeze(dim=0), classes, show_conf=True)draw(image.squeeze(dim=0), label, classes, show_conf=True)# 还原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(dim=0).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()

相关文章:

目标检测—YOLO系列(二 ) 全面解读论文与复现代码YOLOv1 PyTorch

精读论文 前言 从这篇开始&#xff0c;我们将进入YOLO的学习。YOLO是目前比较流行的目标检测算法&#xff0c;速度快且结构简单&#xff0c;其他的目标检测算法如RCNN系列&#xff0c;以后有时间的话再介绍。 本文主要介绍的是YOLOV1&#xff0c;这是由以Joseph Redmon为首的…...

Redis维护缓存的方案选择

Redis中间件常常被用作缓存&#xff0c;而当使用了缓存的时候&#xff0c;缓存中数据的维护&#xff0c;往往是需要重点关注的&#xff0c;尤其是重点考虑的是数据一致性问题。以下是维护数据库缓存的一些常用方案。 1、先删除缓存&#xff0c;再更新数据库 导致数据不一致的…...

LeetCode236. Lowest Common Ancestor of a Binary Tree

文章目录 一、题目二、题解 一、题目 Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree. According to the definition of LCA on Wikipedia: “The lowest common ancestor is defined between two nodes p and q as the lowest…...

基于Gin+Gorm框架搭建MVC模式的Go语言企业级后端系统

文/朱季谦 环境准备&#xff1a;安装Gin与Gorm 本文搭建准备环境&#xff1a;GinGormMySql。 Gin是Go语言的一套WEB框架&#xff0c;在学习一种陌生语言的陌生框架&#xff0c;最好的方式&#xff0c;就是用我们熟悉的思维去学。作为一名后端Java开发&#xff0c;在最初入门…...

【开源】基于Vue和SpringBoot的固始鹅块销售系统

项目编号&#xff1a; S 060 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S060&#xff0c;文末获取源码。} 项目编号&#xff1a;S060&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 鹅块类型模块2.3 固…...

Windows11怎样投屏到电视上?

电视屏幕通常比电脑显示器更大&#xff0c;能够提供更逼真的图像和更震撼的音效&#xff0c;因此不少人也喜欢将电脑屏幕投屏到电视上&#xff0c;缓解一下低头看电脑屏幕的烦恼。 Windows11如何将屏幕投射到安卓电视&#xff1f; 你需要在电脑和电视分贝安装AirDroid Cast的电…...

ubuntu中用docker部署jenkins,并和码云实现自动化部署

1.部署jenkins docker network create jenkins docker run --name jenkins-docker --rm --detach \--privileged --network jenkins --network-alias docker \--env DOCKER_TLS_CERTDIR/certs \--volume jenkins-docker-certs:/certs/client \--volume jenkins-data:/var/jen…...

for,while,do-while,死循环,嵌套循环,跳转关键字,随机数

1.for循环 public class ForDemo1 {public static void main(String[] args) {for (int i 0; i < 5; i) {System.out.println("HelloWorld");}System.out.println("--------------------------------------------");for (int i 1; i <10 ; i) {Sy…...

【六袆 - MySQL】SQL优化;Explain SQL执行计划分析;

Explain SQL执行计划分析 概念:English Unit案例分析1.分析的SQL2.执行计划分析 【如图】MySQL执行计划参数以及它们的影响或意义:概念: MySQL执行计划(Execution Plan)是数据库系统根据查询语句生成的一种执行策略,用于指导数据库引擎执行查询操作。 English Unit This…...

【AI视野·今日NLP 自然语言处理论文速览 第六十二期】Wed, 25 Oct 2023

AI视野今日CS.NLP 自然语言处理论文速览 Wed, 25 Oct 2023 (showing first 100 of 112 entries) Totally 100 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computation and Language Papers MuSR: Testing the Limits of Chain-of-thought with Multistep Soft R…...

各种符号地址,可以直接复制粘贴使用

字符符号 - 文本数字工具 | 偷懒工具 toolight.cn...

C语言测试题:用冒泡法对输入的10个字符由小到大排序 ,要求数组做为函数参数。

编写一个函数&#xff1a; 用冒泡法对输入的10个字符由小到大排序 &#xff0c;要求数组做为函数参数。 冒泡排序是一种简单的排序算法&#xff0c;它会多次遍历要排序的数列&#xff0c; 每次遍历时&#xff0c;依次比较相邻的两个元素&#xff0c;如果它们的顺序不符合要求…...

uni-app开发微信小程序 vue3写法添加pinia

说明 使用uni-app开发&#xff0c;选择vue3语法&#xff0c;开发工具是HBliuderX。虽然内置有vuex&#xff0c;但是个人还是喜欢用Pinia&#xff0c;所以就添加进去了。 Pinia官网连接 添加步骤 第一步&#xff1a; 在项目根目录下执行命令&#xff1a; npm install pinia …...

centos三台主机配置互信ssh登录

1. 修改hosts信息 1.1三台主机上分别修改hosts文件 vi /etc/hosts1.2 三台主机分别填入如下内容&#xff0c;ip地址需要检查正确 192.168.126.223 node1 192.168.126.224 node2 192.168.126.225 node32. 秘钥生成和分发 2.1 在三台主机上分别生成秘钥 命令输入后&#xff…...

验证码案例 —— Kaptcha 插件介绍 后端生成验证码,前端展示并进行session验证(带完整前后端源码)

&#x1f9f8;欢迎来到dream_ready的博客&#xff0c;&#x1f4dc;相信你对这篇博客也感兴趣o (ˉ▽ˉ&#xff1b;) &#x1f4dc;表白墙/留言墙 —— 中级SpringBoot项目&#xff0c;MyBatis技术栈MySQL数据库开发&#xff0c;练手项目前后端开发(带完整源码) 全方位全步骤手…...

js/jQuery 的一些常用操作(js/jQuery获取表单元素值 以及 清空元素值的各种实现方式)——附测试例子,拿来即能实现效果

js/jQuery 的一些常用操作&#xff08;js/jQuery获取表单元素值 以及 清空元素值的各种实现方式&#xff09;——附测试例子&#xff0c;拿来即能实现效果 1. 前言2. 获取表单元素的值2.1 简单获取元素中的值2.1.1 根据 id 简单取值2.2.2 根据name 简单取值2.1.3 获取单选按钮的…...

h5(react ts 适配)

一、新建项目并放在码云托管 1、新建项目&#xff1a;react ts h5 考虑到这些 用 create-react-app 脚手架来搭建项目。 首先&#xff0c;确保你已经安装了 Node.js。如果没有安装&#xff0c;请先从官方网站 https://nodejs.org/ 下载并安装 Node.js。打开命令行工具&#x…...

计算机视觉:驾驶员疲劳检测

目录 前言 关键点讲解 代码详解 结果展示 改进方向&#xff08;打哈欠检测疲劳方法&#xff09; 改进方向&#xff08;点头检测疲劳&#xff09; GUI界面设计展示 前言 上次博客我们讲到了如何定位人脸&#xff0c;并且在人脸上进行关键点定位。其中包括5点定位和68点定…...

Vue向pdf文件中添加二维码

&#x1f680; 场景一&#xff1a;利用vue向pdf文件中写入二维码图片或其他图片 &#x1f680; 场景二&#xff1a;向pdf中添加水印 思路&#xff1a; 1、先通过url链接生成二维码&#xff0c;二维码存在于dom中 2、使用html2canvas库将二维码的dom转为一个canvas对象 3、根据c…...

idea一键打包docker镜像并推送远程harbor仓库的方法(包含spotify和fabric8两种方法)--全网唯一正确,秒杀99%水文

我看了很多关于idea一键打包docker镜像并推送harbor仓库的文章&#xff0c;不论国内国外的&#xff0c;基本上99%都是瞎写的&#xff0c; 这些人不清楚打包插件原理&#xff0c;然后就是复制粘贴一大篇&#xff0c;写了一堆垃圾&#xff0c;然后别人拿来也不能用。 然后这篇文…...

程序设计:C++11原子 写优先的读写锁(源码详解二:操作跟踪)

本文承接程序设计&#xff1a;C11原子 写优先的读写锁&#xff08;源码详解&#xff09;-CSDN博客 上文已经列出了完整代码&#xff0c;完整代码里面增加了操作跟踪&#xff0c;这里就讲解一下这部分是如何实现的。 操作跟踪有两个层面&#xff1a;进程层面和线程层面。 由于这…...

Django视图层解析

Django视图&#xff08;View&#xff09;是Django Web框架中负责处理HTTP请求和返回HTTP响应的组件。视图是一段Python代码&#xff0c;接收HTTP请求作为输入&#xff0c;处理请求并返回HTTP响应作为输出。Django视图的主要目的是实现Web应用程序的业务逻辑&#xff0c;将模型和…...

JAVA使用RXTXcomm进行串口通信(一)

首先下载相应的jar文件 压缩包包括:RXTXcomm.jar(64位环境)、win32com.dll和javax.comm.properties。 下载地址:https://www.aliyundrive.com/s/JSeSQsAyYeZ 点击链接保存&#xff0c;或者复制本段内容&#xff0c;打开「阿里云盘」APP &#xff0c;无需下载极速在线查看&#…...

Vue+ElementUI技巧分享:自定义表单项label的文字提示

文章目录 概要在表单项label后添加文字提示1. 使用 Slot 自定义 Label2. 添加问号图标与提示信息 slot的作用详解1. 基本用法2. 具名插槽 显示多行文字提示的方法1. 问题背景2. 实现多行内容显示3. 样式优化 结语 概要 在Vue和ElementUI的丰富组件库中&#xff0c;定制化表单是…...

【QML】警告Name is declared more than once

1. 问题&#xff1a; qml函数中的不同块中定义同名变量&#xff0c;报警&#xff1a;Name is declared more than once 举例&#xff1a; function test(a){if(a "1"){var re 1;console.log(re);}else{var re 2; //这里会报警&#xff1a;Name is declared mor…...

【自用总结】正项级数审敛法的总结

注&#xff1a;收敛半径的求法就是lim n->∞ |an1/an| ρ&#xff0c;而ρ1/R&#xff0c;最基本的不能忘。 比较判别法&#xff1a;从某项起&#xff0c;该级数后面的项均小于等于另一级数&#xff0c;则敛散性可进行一定的比较 可以看到&#xff0c;比较判别法实际上比较…...

ARMv8平台上安装QT开发环境

安装Qt Creator sudo -iapt-get update apt-get upgrade apt list --installed | grep -v oldstable | cut -d/ -f1 | xargs apt-mark unholdapt-get install gcc g clang make-guile build-essential qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtcreator qt5* 配置…...

基于人工电场算法优化概率神经网络PNN的分类预测 - 附代码

基于人工电场算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于人工电场算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于人工电场优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神…...

在服务器导出kafka topic数据

使用Kafka自带的工具&#xff1a;Kafka提供了一个命令行工具kafka-console-consumer&#xff0c;可以用来消费指定Topic的数据并将其打印到控制台。 1.打印到控制台 命令如下&#xff1a; kafka-console-consumer.sh --bootstrap-server $kafkaHost --topic $topicName --from-…...

农户建档管理系统的设计与实现-计算机毕业设计源码20835

摘 要 随着互联网趋势的到来&#xff0c;各行各业都在考虑利用互联网将自己推广出去&#xff0c;最好方式就是建立自己的互联网系统&#xff0c;并对其进行维护和管理。在现实运用中&#xff0c;应用软件的工作规则和开发步骤&#xff0c;采用Java技术建设农户建档管理系统。 本…...

uniapp的Vue2,Vue3配置跨域(proxy代理)

vue2 找到manifest.json文件&#xff0c;通过源码视图的方式打开文件&#xff1a;在文件中添加一下代码即可完成代理&#xff1a; "h5": {"devServer": {"disableHostCheck": true, //禁止访问本地host文件"port": 8000, //修改项目…...

处理BOP数据集,将其和COCO数据集结合

处理BOP数据集&#xff0c;将其和COCO数据集结合 BOP 取消映射关系&#xff0c;并自增80 取消文件名的images前缀 import os import json from tqdm import tqdm import argparseparser argparse.ArgumentParser() parser.add_argument(--json_path, defaultH:/Dataset/COCO…...

跟李沐学AI-深度学习课程05线性代数

线性代数 &#x1f3f7;sec_linear-algebra 在介绍完如何存储和操作数据后&#xff0c;接下来将简要地回顾一下部分基本线性代数内容。 这些内容有助于读者了解和实现本书中介绍的大多数模型。 本节将介绍线性代数中的基本数学对象、算术和运算&#xff0c;并用数学符号和相应…...

电子病历编辑器源码(Springboot+原生HTML)

一、系统简介 本系统主要面向医院医生、护士&#xff0c;提供对住院病人的电子病历书写、保存、修改、打印等功能。本系统基于云端SaaS服务方式&#xff0c;通过浏览器方式访问和使用系统功能&#xff0c;提供电子病历在线制作、管理和使用的一体化电子病历解决方案&#xff0c…...

Qt的日志输出

在Qt中&#xff0c;一般习惯使用qDebug信息进行输出和打印调试信息到console或者文件中&#xff0c;在qDebug中&#xff0c;也有一些小技巧&#xff0c;可以帮助我们更好的使用qDebug打印日志记录&#xff0c;本文分享了qDebug使用的一些小技巧。 1. 打印出文件名、行号、调用函…...

基于热交换算法优化概率神经网络PNN的分类预测 - 附代码

基于热交换算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于热交换算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于热交换优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神经网络…...

main.js 中的 render函数

按照之前的单组件文件中的写法&#xff0c;我们的写法应该是这样的 import App from ./App.vuenew Vue({el: #app,templete: <App></App>,components: {App}, }) 1、定义el根节点。2、注册App组件。3、渲染 templete 模板 但是在脚手架工程中&#xff0c;他是这…...

Pandas 将DataFrame中单元格内的列表拆分成单独的行

使用 explode 函数 import pandas as pddata {month: [1, 2],week: [[i for i in range(2)], [i for i in range(3)]]} df pd.DataFrame(data) print(df)df df.explode(week) print(df)...

PDF转化为图片

Java 类 PDF2Image 在包 com.oncloudsoft.zbznhc.common.util.pdf 中是用来将 PDF 文件转换为图像的。它使用了 Apache PDFBox 库来处理 PDF 文档并生成图像。下面是类中每个部分的详细解释&#xff1a; 类和方法说明 类 PDF2Image: 使用了 Lombok 库的 Slf4j 注解&#xff0c…...

【Java】智慧工地管理系统源码(SaaS模式)

智慧工地是聚焦工程施工现场&#xff0c;紧紧围绕人、机、料、法、环等关键要素&#xff0c;综合运用物联网、云计算、大数据、移动计算和智能设备等软硬件信息技术&#xff0c;与施工生产过程相融合。 一、什么是智慧工地 智慧工地是指利用移动互联、物联网、智能算法、地理信…...

torch.nn.functional.log_softmax 函数解析

该函数将输出向量转化为概率分布&#xff0c;作用和softmax一致。 相比softmax&#xff0c;对较小的概率分布处理能力更好。 一、定义 softmax 计算公式&#xff1a; log_softmax 计算公式&#xff1a; 可见仅仅是将 softmax 最外层套上 log 函数。 二、使用场景 log_soft…...

jQuery、vue、小程序、uni-app中的本地存储数据和接受数据是什么?

在这四个工具/框架中&#xff0c;Uni-app和微信小程序比较类似&#xff0c;因为它们都是为了实现跨平台开发而设计的。 jQuery 是一个快速、小巧且特性丰富的 JavaScript 库。它提供了各种操作和处理 HTML DOM、事件、动画&#xff0c;以及提供各种工具函数的功能。然而&#…...

黑马React18: 基础Part 1

黑马React: 基础1 Date: November 15, 2023 Sum: React介绍、JSX、事件绑定、组件、useState、B站评论 React介绍 概念: React由Meta公司研发&#xff0c;是一个用于 构建Web和原生交互界面的库 优势: 1-组件化的开发方式 2-优秀的性能 3-丰富的生态 4-跨平台开发 开发环境搭…...

windows Oracle Database 19c 卸载教程

目录 打开任务管理器 停止数据库服务 Universal Installer 卸载Oracle数据库程序 使用Oracle Installer卸载 删除注册表项 重新启动系统 打开任务管理器 ctrlShiftEsc可以快速打开任务管理器&#xff0c;找到oracle所有服务然后停止。 停止数据库服务 在开始卸载之前&a…...

动态规划解决leetcode上的两道回文问题(针对思路)

本期主讲的是使用动态规划去解决两道回文问题&#xff0c;分别是 647. 回文子串 - 力扣&#xff08;LeetCode&#xff09; 516. 最长回文子序列 - 力扣&#xff08;LeetCode&#xff09; 而不是leetcode5.最长回文子串&#xff0c;虽然这道题也是回文问题&#xff0c;也可以…...

使用人工智能自动测试 Flutter 应用程序

移动应用程序开发的增长速度比以往任何时候都快。几乎每个企业都需要移动应用程序来保持市场竞争力。由于像 React Native 这样的跨平台移动应用程序开发框架允许公司使用单一源代码和单一编程语言构建 iOS 和 Android 应用程序&#xff0c; Flutter是 Google 支持的另一个热门…...

四、程序员指南:数据平面开发套件

REORDER LIBRARY 重排序库提供了根据其序列号对mbuf进行重排序的机制。 16.1 操作 重排序库本质上是一个对mbuf进行重新排序的缓冲区。用户将乱序的mbuf插入重排序缓冲区&#xff0c;并从中提取顺序正确的mbuf。 在任何给定时刻&#xff0c;重排序缓冲区包含其序列号位于序列…...

Go 之 captcha 生成图像验证码

目前 chptcha 好像只可以生成纯数字的图像验证码&#xff0c;不过对于普通简单应用来说也足够了。captcha默认将store封装到内部&#xff0c;未提供对外操作的接口&#xff0c;因此使用自己显式生成的store&#xff0c;可以通过store自定义要生成的验证码。 package mainimpor…...

【Java从入门到大牛】多线程

&#x1f525; 本文由 程序喵正在路上 原创&#xff0c;CSDN首发&#xff01; &#x1f496; 系列专栏&#xff1a;Java从入门到大牛 &#x1f320; 首发时间&#xff1a;2023年11月18日 &#x1f98b; 欢迎关注&#x1f5b1;点赞&#x1f44d;收藏&#x1f31f;留言&#x1f4…...

UE5 C++报错:is not currently enabled for Live Coding

解决办法&#xff1a; 再次打开项目&#xff0c;以此法打开&#xff1a;...