4.4.yolo系列

学习目标

  • 知道yolo网络架构,理解其输入输出
  • 知道yolo模型的训练样本构建的方法
  • 理解yolo模型的损失函数
  • 知道yoloV2模型的改进方法
  • 知道yoloV3的多尺度检测方法
  • 知道yoloV3模型的网络结构及网络输出
  • 了解yoloV3模型先验框设计的方法
  • 知道yoloV3模型为什么适用于多标签的目标分类
  • 了解yoloV4模型

YOLO系列算法是一类典型的one-stage目标检测算法,其利用anchor box将分类与目标定位的回归问题结合起来,从而做到了高效、灵活和泛化性能好,所以在工业界也十分受欢迎,接下来我们介绍YOLO 系列算法。

1.yolo算法

Yolo算法采用一个单独的CNN模型实现end-to-end的目标检测,核心思想就是利用整张图作为网络的输入,直接在输出层回归 bounding box(边界框) 的位置及其所属的类别,整个系统如下图所示:

首先将输入图片resize到448x448,然后送入CNN网络,最后处理网络预测结果得到检测的目标。相比R-CNN算法,其是一个统一的框架,其速度更快。

1.1 Yolo算法思想

在介绍Yolo算法之前,我们回忆下RCNN模型,RCNN模型提出了候选区(Region Proposals)的方法,先从图片中搜索出一些可能存在对象的候选区(Selective Search),大概2000个左右,然后对每个候选区进行对象识别,但处理速度较慢。

Yolo意思是You Only Look Once,它并没有真正的去掉候选区域,而是创造性的将候选区和目标分类合二为一,看一眼图片就能知道有哪些对象以及它们的位置。

Yolo模型采用预定义预测区域的方法来完成目标检测,具体而言是将原始图像划分为 7x7=49 个网格(grid),每个网格允许预测出2个边框(bounding box,包含某个对象的矩形框),总共 49x2=98 个bounding box。我们将其理解为98个预测区,很粗略的覆盖了图片的整个区域,就在这98个预测区中进行目标检测。

只要得到这98个区域的目标分类和回归结果,再进行NMS就可以得到最终的目标检测结果。那具体要怎样实现呢?

1.2 Yolo的网络结构

YOLO的结构非常简单,就是单纯的卷积、池化最后加了两层全连接,从网络结构上看,与前面介绍的CNN分类网络没有本质的区别,最大的差异是输出层用线性函数做激活函数,因为需要预测bounding box的位置(数值型),而不仅仅是对象的概率。所以粗略来说,YOLO的整个结构就是输入图片经过神经网络的变换得到一个输出的张量,如下图所示:

网络结构比较简单,重点是我们要理解网络输入与输出之间的关系。

1.2.1 网络输入

网络的输入是原始图像,唯一的要求是缩放到448x448的大小。主要是因为Yolo的网络中,卷积层最后接了两个全连接层,全连接层是要求固定大小的向量作为输入,所以Yolo的输入图像的大小固定为448x448。

1.2.2 网络输出

网络的输出就是一个7x7x30 的张量(tensor)。那这个输出结果我们要怎么理解那?

1.7X7网格

根据YOLO的设计,输入图像被划分为 7x7 的网格(grid),输出张量中的 7x7 就对应着输入图像的 7x7 网格。或者我们把 7x7x30 的张量看作 7x7=49个30维的向量,也就是输入图像中的每个网格对应输出一个30维的向量。如下图所示,比如输入图像左上角的网格对应到输出张量中左上角的向量。

2.30维向量

30维的向量包含:2个bbox的位置和置信度以及该网格属于20个类别的概率

  • 2个bounding box的位置 每个bounding box需要4个数值来表示其位置,(Center_x,Center_y,width,height),即(bounding box的中心点的x坐标,y坐标,bounding box的宽度,高度),2个bounding box共需要8个数值来表示其位置。
  • 2个bounding box的置信度 bounding box的置信度 = 该bounding box内存在对象的概率 * 该bounding box与该对象实际bounding box的IOU,用公式表示就是:

Pr(Object)是bounding box内存在对象的概率

  • 20个对象分类的概率

Yolo支持识别20种不同的对象(人、鸟、猫、汽车、椅子等),所以这里有20个值表示该网格位置存在任一种对象的概率.

1.3Yolo模型的训练

在进行模型训练时,我们需要构造训练样本和设计损失函数,才能利用梯度下降对网络进行训练。

1.3.1训练样本的构建

将一幅图片输入到yolo模型中,对应的输出是一个7x7x30张量,构建标签label时对于原图像中的每一个网格grid都需要构建一个30维的向量。对照下图我们来构建目标向量:

  • 20个对象分类的概率

对于输入图像中的每个对象,先找到其中心点。比如上图中自行车,其中心点在黄色圆点位置,中心点落在黄色网格内,所以这个黄色网格对应的30维向量中,自行车的概率是1,其它对象的概率是0。所有其它48个网格的30维向量中,该自行车的概率都是0。这就是所谓的"中心点所在的网格对预测该对象负责"。狗和汽车的分类概率也是同样的方法填写

  • 2个bounding box的位置

训练样本的bbox位置应该填写对象真实的位置bbox,但一个对象对应了2个bounding box,该填哪一个呢?需要根据网络输出的bbox与对象实际bbox的IOU来选择,所以要在训练过程中动态决定到底填哪一个bbox。

  • 2个bounding box的置信度

预测置信度的公式为:

IOUtruthpredIOUpredtruth利用网络输出的2个bounding box与对象真实bounding box计算出来。然后看这2个bounding box的IOU,哪个比较大,就由哪个bounding box来负责预测该对象是否存在,即该bounding box的Pr(Object)=1,同时对象真实bounding box的位置也就填入该bounding box。另一个不负责预测的bounding box的Pr(Object)=0。

上图中自行车所在的grid对应的结果如下图所示:

1.3.2 损失函数

损失就是网络实际输出值与样本标签值之间的偏差:

yolo给出的损失函数:

注:其中1obji1iobj表示目标是否出现在网格单元i中,1objij1ijobj表示单元格i中的第j个边界框预测器负责该预测,YOLO设置 λcoord=5λcoord=5 来调高位置误差的权重, λnoobj=0.5λnoobj=0.5 即调低不存在对象的bounding box的置信度误差的权重。

1.3.3 模型训练

Yolo先使用ImageNet数据集对前20层卷积网络进行预训练,然后使用完整的网络,在PASCAL VOC数据集上进行对象识别和定位的训练。

Yolo的最后一层采用线性激活函数,其它层都是Leaky ReLU。训练中采用了drop out和数据增强(data augmentation)来防止过拟合.

1.4 模型预测

将图片resize成448x448的大小,送入到yolo网络中,输出一个 7x7x30 的张量(tensor)来表示图片中所有网格包含的对象(概率)以及该对象可能的2个位置(bounding box)和可信程度(置信度)。在采用NMS(Non-maximal suppression,非极大值抑制)算法选出最有可能是目标的结果。

1.5 yolo总结

优点

  • 速度非常快,处理速度可以达到45fps,其快速版本(网络较小)甚至可以达到155fps。
  • 训练和预测可以端到端的进行,非常简便。

缺点

  • 准确率会打折扣
  • 对于小目标和靠的很近的目标检测效果并不好

2.yoloV2

YOLOv2相对v1版本,在继续保持处理速度的基础上,从预测更准确(Better),速度更快(Faster),识别对象更多(Stronger)这三个方面进行了改进。其中识别更多对象也就是扩展到能够检测9000种不同对象,称之为YOLO9000。 下面我们看下yoloV2的都做了哪些改进?

2.1 预测更准确(better)

2.1.1 batch normalization

批标准化有助于解决反向传播过程中的梯度消失和梯度爆炸问题,降低对一些超参数的敏感性,并且每个batch分别进行归一化的时候,起到了一定的正则化效果,从而能够获得更好的收敛速度和收敛效果。在yoloV2中卷积后全部加入Batch Normalization,网络会提升2%的mAP。

2.1.2 使用高分辨率图像微调分类模型

YOLO v1使用ImageNet的图像分类样本采用 224x224 作为输入,来训练CNN卷积层。然后在训练对象检测时,检测用的图像样本采用更高分辨率的 448x448 的图像作为输入。但这样切换对模型性能有一定影响。

YOLOV2在采用 224x224 图像进行分类模型预训练后,再采用 448x448 的高分辨率样本对分类模型进行微调(10个epoch),使网络特征逐渐适应 448x448 的分辨率。然后再使用 448x448 的检测样本进行训练,缓解了分辨率突然切换造成的影响。

使用该技巧后网络的mAP提升了约4%。

2.1.3 采用Anchor Boxes

YOLO1并没有采用先验框,并且每个grid只预测两个bounding box,整个图像98个。YOLO2如果每个grid采用5个先验框,总共有13x13x5=845个先验框。通过引入anchor boxes,使得预测的box数量更多(13x13xn)。

2.2.4 聚类提取anchor尺度

Faster-rcnn选择的anchor比例都是手动指定的,但是不一定完全适合数据集。YOLO2尝试统计出更符合样本中对象尺寸的先验框,这样就可以减少网络微调先验框到实际位置的难度。YOLO2的做法是对训练集中标注的边框进行聚类分析,以寻找尽可能匹配样本的边框尺寸。

YoloV2选择了聚类的五种尺寸最为anchor box。

2.1.5 边框位置的预测

Yolov2中将边框的结果约束在特定的网格中:

其中,

bx,by,bw,bhbx,by,bw,bh是预测边框的中心和宽高。 Pr(object)∗IOU(b,object)Pr(object)∗IOU(b,object)是预测边框的置信度,YOLO1是直接预测置信度的值,这里对预测参数toto进行σ变换后作为置信度的值。 cx,cycx,cy是当前网格左上角到图像左上角的距离,要先将网格大小归一化,即令一个网格的宽=1,高=1。 pw,phpw,ph是先验框的宽和高。 σ是sigmoid函数。 tx,ty,tw,th,totx,ty,tw,th,to是要学习的参数,分别用于预测边框的中心和宽高,以及置信度。

如下图所示:

由于σ函数将 tx,tytx,ty约束在(0,1)范围内,预测边框的蓝色中心点被约束在蓝色背景的网格内。约束边框位置使得模型更容易学习,且预测更为稳定。

假设网络预测值为:

anchor框为:

则目标在特征图中的位置:

在原图像中的位置:

2.1.6 细粒度特征融合

图像中对象会有大有小,输入图像经过多层网络提取特征,最后输出的特征图中,较小的对象可能特征已经不明显甚至被忽略掉了。为了更好的检测出一些比较小的对象,最后输出的特征图需要保留一些更细节的信息。

YOLO2引入一种称为passthrough层的方法在特征图中保留一些细节信息。具体来说,就是在最后一个pooling之前,特征图的大小是26x26x512,将其1拆4,直接传递(passthrough)到pooling后(并且又经过一组卷积)的特征图,两者叠加到一起作为输出的特征图。

具体的拆分方法如下所示:

2.1.7 多尺度训练

YOLO2中没有全连接层,可以输入任何尺寸的图像。因为整个网络下采样倍数是32,采用了{320,352,…,608}等10种输入图像的尺寸,这些尺寸的输入图像对应输出的特征图宽和高是{10,11,…19}。训练时每10个batch就随机更换一种尺寸,使网络能够适应各种大小的对象检测。

2.2 速度更快(Faster)

yoloV2提出了Darknet-19(有19个卷积层和5个MaxPooling层)网络结构作为特征提取网络。DarkNet-19比VGG-16小一些,精度不弱于VGG-16,但浮点运算量减少到约⅕,以保证更快的运算速度。

yoloV2的网络中只有卷积+pooling,从416x416x3 变换到 13x13x5x25。增加了batch normalization,增加了一个passthrough层,去掉了全连接层,以及采用了5个先验框,网络的输出如下图所示:

2.3 识别对象更多

VOC数据集可以检测20种对象,但实际上对象的种类非常多,只是缺少相应的用于对象检测的训练样本。YOLO2尝试利用ImageNet非常大量的分类样本,联合COCO的对象检测数据集一起训练,使得YOLO2即使没有学过很多对象的检测样本,也能检测出这些对象。

3.yoloV3

yoloV3以V1,V2为基础进行的改进,主要有:利用多尺度特征进行目标检测;先验框更丰富;调整了网络结构;对象分类使用logistic代替了softmax,更适用于多标签分类任务。

3.1算法简介

YOLOv3是YOLO (You Only Look Once)系列目标检测算法中的第三版,相比之前的算法,尤其是针对小目标,精度有显著提升。

yoloV3的流程如下图所示,对于每一幅输入图像,YOLOv3会预测三个不同尺度的输出,目的是检测出不同大小的目标。

3.2多尺度检测

通常一幅图像包含各种不同的物体,并且有大有小。比较理想的是一次就可以将所有大小的物体同时检测出来。因此,网络必须具备能够“看到”不同大小的物体的能力。因为网络越深,特征图就会越小,所以网络越深小的物体也就越难检测出来。

在实际的feature map中,随着网络深度的加深,浅层的feature map中主要包含低级的信息(物体边缘,颜色,初级位置信息等),深层的feature map中包含高等信息(例如物体的语义信息:狗,猫,汽车等等)。因此在不同级别的feature map对应不同的scale,所以我们可以在不同级别的特征图中进行目标检测。如下图展示了多种scale变换的经典方法。

(a) 这种方法首先建立图像金字塔,不同尺度的金字塔图像被输入到对应的网络当中,用于不同scale物体的检测。但这样做的结果就是每个级别的金字塔都需要进行一次处理,速度很慢。

(b) 检测只在最后一层feature map阶段进行,这个结构无法检测不同大小的物体

© 对不同深度的feature map分别进行目标检测。SSD中采用的便是这样的结构。这样小的物体会在浅层的feature map中被检测出来,而大的物体会在深层的feature map被检测出来,从而达到对应不同scale的物体的目的,缺点是每一个feature map获得的信息仅来源于之前的层,之后的层的特征信息无法获取并加以利用。

(d) 与©很接近,但不同的是,当前层的feature map会对未来层的feature map进行上采样,并加以利用。因为有了这样一个结构,当前的feature map就可以获得“未来”层的信息,这样的话低阶特征与高阶特征就有机融合起来了,提升检测精度。在YOLOv3中,就是采用这种方式来实现目标多尺度的变换的。

3.3网络模型结构

在基本的图像特征提取方面,YOLO3采用了Darknet-53的网络结构(含有53个卷积层),它借鉴了残差网络ResNet的做法,在层之间设置了shortcut,来解决深层网络梯度的问题,shortcut如下图所示:包含两个卷积层和一个shortcut connections。

yoloV3的模型结构如下所示:整个v3结构里面,没有池化层和全连接层,网络的下采样是通过设置卷积的stride为2来达到的,每当通过这个卷积层之后图像的尺寸就会减小到一半。

下面我们看下网络结构:

  • 基本组件:蓝色方框内部分

1、CBL:Yolov3网络结构中的最小组件,由Conv+Bn+Leaky_relu激活函数三者组成。 2、Res unit:借鉴Resnet网络中的残差结构,让网络可以构建的更深。 3、ResX:由一个CBL和X个残差组件构成,是Yolov3中的大组件。每个Res模块前面的CBL都起到下采样的作用,因此经过5次Res模块后,得到的特征图是608->304->152->76->38->19大小。

  • 其他基础操作:

1、Concat:张量拼接,会扩充两个张量的维度,例如26×26×256和26×26×512两个张量拼接,结果是26×26×768。

2、Add:张量相加,张量直接相加,不会扩充维度,例如104×104×128和104×104×128相加,结果还是104×104×128。

  • Backbone中卷积层的数量:

每个ResX中包含1+2×X个卷积层,因此整个主干网络Backbone中一共包含1+(1+2×1)+(1+2×2)+(1+2×8)+(1+2×8)+(1+2×4)=52,再加上一个FC全连接层,即可以组成一个Darknet53分类网络。不过在目标检测Yolov3中,去掉FC层,仍然把Yolov3的主干网络叫做Darknet53结构。

3.4先验框

yoloV3采用K-means聚类得到先验框的尺寸,为每种尺度设定3种先验框,总共聚类出9种尺寸的先验框。

在COCO数据集这9个先验框是:(10x13),(16x30),(33x23),(30x61),(62x45),(59x119),(116x90),(156x198),(373x326)。在最小的(13x13)特征图上(有最大的感受野)应用较大的先验框(116x90),(156x198),(373x326),适合检测较大的对象。中等的(26x26)特征图上(中等感受野)应用中等的先验框(30x61),(62x45),(59x119),适合检测中等大小的对象。较大的(52x52)特征图上(较小的感受野)应用,其中较小的先验框(10x13),(16x30),(33x23),适合检测较小的对象。

直观上感受9种先验框的尺寸,下图中蓝色框为聚类得到的先验框。黄色框式ground truth,红框是对象中心点所在的网格。

3.5 logistic回归

预测对象类别时不使用softmax,而是被替换为一个1x1的卷积层+logistic激活函数的结构。使用softmax层的时候其实已经假设每个输出仅对应某一个单个的class,但是在某些class存在重叠情况(例如woman和person)的数据集中,使用softmax就不能使网络对数据进行很好的预测。

3.6 yoloV3模型的输入与输出

YoloV3的输入输出形式如下图所示:

输入416×416×3的图像,通过darknet网络得到三种不同尺度的预测结果,每个尺度都对应N个通道,包含着预测的信息;

每个网格每个尺寸的anchors的预测结果。

YOLOv3共有13×13×3 + 26×26×3 + 52×52×3个预测 。每个预测对应85维,分别是4(坐标值)、1(置信度分数)、80(coco类别概率)。

4.yoloV4[了解]

YOLO之父在2020年初宣布退出CV界,YOLOv4 的作者并不是YOLO系列 的原作者。YOLO V4是YOLO系列一个重大的更新,其在COCO数据集上的平均精度(AP)和帧率精度(FPS)分别提高了10% 和12%,并得到了Joseph Redmon的官方认可,被认为是当前最强的实时对象检测模型之一。

yoloV4总结了大部分检测技巧,然后经过筛选,排列组合,挨个实验(ablation study)哪些方法有效,总体来说,Yolov4并没有创造新的改进,而是使用了大量的目标检测的技巧。在这里我们主要给大家看下它的网络架构:

Yolov4的结构图和Yolov3是相似的,不过使用各种新的算法思想对各个子结构都进行了改进。 先整理下Yolov4的结构组件

  • 基本组件:
  • CBM:Yolov4网络结构中的最小组件,由Conv+Bn+Mish激活函数三者组成。
  • CBL:由Conv+Bn+Leaky_relu激活函数三者组成。
  • Res unit:借鉴Resnet网络中的残差结构,让网络可以构建的更深。
  • CSPX:由三个卷积层和X个Res unint模块Concate组成。
  • SPP:采用1×1,5×5,9×9,13×13的最大池化的方式,进行多尺度融合。
  • 其他基础操作:
  • Concat:张量拼接,维度会扩充,和Yolov3中的解释一样,对应于cfg文件中的route操作。
  • Add:张量相加,不会扩充维度,对应于cfg文件中的shortcut操作。
  • Backbone中卷积层的数量: 每个CSPX中包含3+2×X个卷积层,因此整个主干网络Backbone中一共包含2+(3+2×1)+2+(3+2×2)+2+(3+2×8)+2+(3+2×8)+2+(3+2×4)+1=72。

注意:

网络的输入大小不是固定的,在yoloV3中输入默认是416×416,在yoloV4中默认是608×608,在实际项目中也可以根据需要修改,比如320×320,一般是32的倍数。 输入图像的大小和最后的三个特征图的大小也是对应的,比如416×416的输入,最后的三个特征图大小是13×13,26×26,52×52, 如果是608×608,最后的三个特征图大小则是19×19,38×38,76×76。


总结

  • 知道yolo网络架构,理解其输入输出

YOLO的整个结构就是输入图片经过神经网络的变换得到一个输出的张量

  • 知道yolo模型的训练样本构建的方法

对于原图像中的每一个网格grid都需要构建一个30维的向量:分类,置信度,回归的目标值

  • 理解yolo模型的损失函数

损失函数分为3部分:分类损失,回归损失,置信度损失

  • 知道yoloV2模型的改进方法

使用了BN层,高分辨率训练,采用Anchorbox,聚类得到anchorbox的尺寸,改进边界框预测的方法,特征融合,多尺度训练,网络模型使用darknet19,利用imagenet数据集识别更多的目标

  • yoloV3的多尺度检测方法

在YOLOv3中采用FPN结构来提高对应多尺度目标检测的精度,当前的feature map利用“未来”层的信息,将低阶特征与高阶特征进行融合,提升检测精度。

  • yoloV3模型的网络结构

以darknet-53为基础,借鉴resnet的思想,在网络中加入了残差模块,利于解决深层次网络的梯度问题

整个v3结构里面,没有池化层和全连接层,只有卷积层

网络的下采样是通过设置卷积的stride为2来达到的

  • yoloV3模型先验框设计的方法

采用K-means聚类得到先验框的尺寸,为每种尺度设定3种先验框,总共聚类出9种尺寸的先验框。

  • yoloV3模型为什么适用于多标签的目标分类

预测对象类别时不使用softmax,而是使用logistic的输出进行预测

  • yoloV3模型的输入输出

对于416×416×3的输入图像,在每个尺度的特征图的每个网格设置3个先验框,总共有 13×13×3 + 26×26×3 + 52×52×3 = 10647 个预测。每一个预测是一个(4+1+80)=85维向量,这个85维向量包含边框坐标(4个数值),边框置信度(1个数值),对象类别的概率(对于COCO数据集,有80种对象)。

4.5 YoloV3 案例

学习目标

  • 熟悉TFRecord文件的使用方法
  • 知道YoloV3模型结构及构建方法
  • 知道数据处理方法
  • 能够利用yoloV3模型进行训练和预测

1.TFrecord文件

该案例中我们依然使用VOC数据集来进行目标检测,不同的是我们要利用tfrecord文件来存储和读取数据,首先来看一下tfrecord文件的相关内容。

为什么要使用tfrecord文件?

  • TFRecord是Google官方推荐使用的数据格式化存储工具,为TensorFlow量身打造的。
  • TFRecord规范了数据的读写方式,数据读取和处理的效率都会得到显著的提高。

1.1 什么是TFrecord文件

TFRecord 是Google官方推荐的一种数据格式,是Google专门为TensorFlow设计的一种数据格式,利用这种方式存储数据可以使其与网络架构更适配。TFRecord是一种二进制文件,其能更好的利用内存,与csv,hdf5文件是类似的。

TFRecord的文件的内容如下图所示:

TFRecord内部包含多个tf.train.Example,一般来说对应一个图像数据,在一个Example消息体中包含了一系列的tf.train.feature属性,而 每一个feature是一个key-value的键值对,其中,key 是string类型,而value 的取值有三种:

  • tf.train.bytes_list: 可以存储string 和byte两种数据类型。图像数据使用这种方式存储即可。
  • tf.train.float_list: 可以存储float(float32)与double(float64) 两种数据类型 。
  • tf.train.int64_list: 可以存储:bool, enum, int32, uint32, int64, uint64 。

TFRecord 并非是TensorFlow唯一支持的数据格式,也可以使用CSV或文本等其他格式,但是对于TensorFlow来说,TFRecord 是最友好的,最方便的,而且tensorflow也提供了丰富的API帮助我们轻松的创建和获取TFRecord文件。

1.2 将数据转换为TFRecord文件

对于中大数据集来说,Google官方推荐先将数据集转化为TFRecord数据, 这样可加快在数据读取, 预处理中的速度。接下来我们就将VOC数据集转换为Records格式,在这里首先读取标注XML文件,并找到对应的图像数据,最后将数据写入TFRecords文件中。

1.2.1 读取标注信息

VOC数据集的标注信息存储在xml文件中,在VOC2007的数据中主要获取fIlename,width,height,和object(图像中的目标)下的name(目标名称)和bndbox(框的位置)。具体大家可以看下在FasterRCNN中的介绍,代码如下所示:

import xml.dom.minidom as xdom
# VOC数据集中的类别信息
voc_classes = {'none': 0,'aeroplane': 1,'bicycle': 2,'bird': 3,'boat': 4, 'bottle': 5,'bus': 6,'car': 7, 'cat': 8, 'chair': 9, 'cow': 10, 'diningtable': 11, 'dog': 12,'horse': 13, 'motorbike': 14,'person': 15, 'pottedplant': 16,'sheep': 17,'sofa': 18,'train': 19,'tvmonitor': 20,
}
# 读取XML文件中的信息
def Prase_Singel_xml(xml_path):DOMTree = xdom.parse(xml_path)RootNode = DOMTree.documentElement#获取XML文件对应的图像image_name = RootNode.getElementsByTagName("filename")[0].childNodes[0].data#获取图像宽和高size = RootNode.getElementsByTagName("size")image_height = int(size[0].getElementsByTagName("height")[0].childNodes[0].data)image_width = int(size[0].getElementsByTagName("width")[0].childNodes[0].data)#获取图像中目标对象all_obj = RootNode.getElementsByTagName("object")bndbox_lable_dic = []# 遍历所有的对象for one_obj in all_obj:# 获取目标的标注信息obj_name = one_obj.getElementsByTagName("name")[0].childNodes[0].data# 获取对应的label值obj_label = voc_classes[obj_name]# 获取bboxbndbox = one_obj.getElementsByTagName("bndbox")# 获取目标的左上右下的位置xmin = int(bndbox[0].getElementsByTagName("xmin")[0].childNodes[0].data)ymin = int(bndbox[0].getElementsByTagName("ymin")[0].childNodes[0].data)xmax = int(bndbox[0].getElementsByTagName("xmax")[0].childNodes[0].data)ymax = int(bndbox[0].getElementsByTagName("ymax")[0].childNodes[0].data)# 将目标框和类别组合在一起bndbox_lable_dic.append([xmin, ymin, xmax, ymax, obj_label])# 返回相应的信息return image_name, image_width, image_height, bndbox_lable_dic

接下来我们读取一个XML文件看下效果:

# 展示效果
print(Prase_Singel_xml('VOCdevkit/VOC2007/Annotations/000007.xml'))

结果如下所示:

('000007.jpg', 500, 333, [[141, 50, 500, 330, 7]])

从中可以看出,对应的图像是000007.jpg,图像的宽高是500, 333,图像中只包含一个目标,位置是141, 50, 500, 330,类别是7 car.

1.2.2 将数据写入TFRecord文件中

在将数据写入时,我们可以使用tf.io.TFRecordWriter来完成,主要步骤是:

1、使用tf.io.TFRecordWriter打开TFRecords文件

2、使用tf.train.Int64List,tf.train.BytesList或tf.train.FloatList对数据进行类型转换

3、将类型转换后的数据传入tf.train.Feature创建的特征中

4、将特征传入tf.train.Example创建的example中

5、使用example.SerializeToString()将example序列化为字符串

6、使用writer.write将序列化后的example写入TFRecords文件

7、最后使用writer.close()关闭文件

import tensorflow as tf
import glob
import os
# 指明xml文件,tfrecord文件和图像的位置
def write_to_tfrecord(all_xml_path, tfrecord_path, voc_img_path):# 1、使用tf.io.TFRecordWriter打开TFRecords文件writer = tf.io.TFRecordWriter(tfrecord_path)# 遍历所有的XML文件for i, single_xml_path in enumerate(all_xml_path):# 读取xml文件中的内容image_name, image_width, image_height, bndbox_lable_dic = Prase_Singel_xml(single_xml_path)# 获取图像的路径sigle_img_path = os.path.join(voc_img_path, image_name)# 读取图像image_data = open(sigle_img_path, 'rb').read()xmin = []ymin = []xmax = []ymax = []obj_label = []# 遍历box和label信息,并记录下来for j in range(len(bndbox_lable_dic)):xmin.append(bndbox_lable_dic[j][0])ymin.append(bndbox_lable_dic[j][1])xmax.append(bndbox_lable_dic[j][2])ymax.append(bndbox_lable_dic[j][3])obj_label.append(bndbox_lable_dic[j][4])# 创建特征:图像,size,box和label# 2、使用tf.train.Int64List,tf.train.BytesList或tf.train.FloatList对数据进行类型转换# 3、将类型转换后的数据传入tf.train.Feature创建的特征中feature = {'image': tf.train.Feature(bytes_list=tf.train.BytesList(value=[image_data])),'width': tf.train.Feature(float_list=tf.train.FloatList(value=[image_width])),'height': tf.train.Feature(float_list=tf.train.FloatList(value=[image_height])),'xmin': tf.train.Feature(float_list=tf.train.FloatList(value=xmin)),'ymin': tf.train.Feature(float_list=tf.train.FloatList(value=ymin)),'xmax': tf.train.Feature(float_list=tf.train.FloatList(value=xmax)),'ymax': tf.train.Feature(float_list=tf.train.FloatList(value=ymax)),'label': tf.train.Feature(int64_list=tf.train.Int64List(value=obj_label))}# 4、将特征传入tf.train.Example创建的example中example = tf.train.Example(features=tf.train.Features(feature=feature))# 将example写入到tfrecord文件中# 5、使用example.SerializeToString()将example序列化为字符串# 6、使用writer.write将序列化后的example写入TFRecords文件writer.write(example.SerializeToString())# 最后使用writer.close()关闭文件writer.close()print('第{}张图片写入完毕'.format(i))

接下来调用上述方法将VOC数据写入到TFRecord文件中:

# 获取所有的xml文件
all_xml_path = glob.glob('VOCdevkit/VOC2007/Annotations/*.xml')
# 指定tfrecords文件的路径
tfrecord_path = 'voc_2007.tfrecords'
# 指定图像所在的路径
voc_img_path = 'VOCdevkit/VOC2007/JPEGImages'
# 将信息写入到tfrecord文件中
write_to_tfrecord(all_xml_path, tfrecord_path, voc_img_path)

结果如下所示:

1.3 读取TFRecord文件

VOC数据集已经被写入到TFRecord文件中了,那我们就要从TFrecord文件中将数据读取出来。只需要简单的使用 tf.data.TFRecordDataset 就能够轻松的读取数据。

  • 使用tf.data.TFRecordDataset来获取TFRecord文件中的数据
  • 定义特征的描述方法,与写入时是对应的
  • 使用tf.io.parse_single_example将一个example转换为原始数据
  • 使用功能map方法对所有数据进行处理获取最终的结果(map方法稍后介绍)
import tensorflow as tf
import os
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
# 获取tfreocrd中的所有数据
raw_datasets = tf.data.TFRecordDataset('voc_2007.tfrecords')
# 定义特征的描述方法:图像,box和label,注意:要和写入时是一一对应的
feature_description = {'image': tf.io.FixedLenFeature([], tf.string),'width': tf.io.FixedLenFeature([], tf.float32),'height': tf.io.FixedLenFeature([], tf.float32),'xmin': tf.io.VarLenFeature(tf.float32),'ymin': tf.io.VarLenFeature(tf.float32),'xmax': tf.io.VarLenFeature(tf.float32),'ymax': tf.io.VarLenFeature(tf.float32),'label': tf.io.VarLenFeature(tf.int64),
}# 将tfrecord中的数据转换为原始图像和标注信息(只能对一个数据进行处理)
def parse_example(example_string):# 将tfreocord文件中的一个example映射回原始数据feature_dict = tf.io.parse_single_example(example_string, feature_description)# 获取图像数据image_data = tf.io.decode_jpeg(feature_dict['image'])# 获取boxboxes = tf.stack([tf.sparse.to_dense(feature_dict['xmin']),tf.sparse.to_dense(feature_dict['ymin']),tf.sparse.to_dense(feature_dict['xmax']),tf.sparse.to_dense(feature_dict['ymax'])], axis=1)# 获取标注信息boxes_category = tf.sparse.to_dense(feature_dict['label'])# 返回结果return image_data, feature_dict['width'], feature_dict['height'], boxes, boxes_category
# 利用map方法调用parse_example方法对所有数据进行处理,得到最终的经过
raw_datasets = raw_datasets.map(parse_example)

我们将从TFRecord文件中读取的数据展示出来:

# 将VOC_class字典的key和value进行翻转
new_voc_class = {v:k for k,v in voc_classes.items()}
# 将tfrecord中的图像进行展示
plt.figure(figsize=(15, 10))
# 初始化:第几个图像
i = 0
# 从raw_datasets中选取3个样本,获取图像,大小,框的标注信息和类别信息
for image, width, height, boxes, boxes_category in raw_datasets.take(3):# 进行绘图plt.subplot(1, 3, i+1)# 绘制图像plt.imshow(image)# 获取坐标区域ax = plt.gca()# 遍历所有的框for j in range(boxes.shape[0]):# 绘制框rect = Rectangle((boxes[j, 0], boxes[j, 1]), boxes[j, 2] -boxes[j, 0], boxes[j, 3]-boxes[j, 1], color='r', fill=False)# 将框显示在图像上ax.add_patch(rect)# 显示标注信息# 获取标注信息的idlabel_id = boxes_category[j]# 获取标准信息label = new_voc_class.get(label_id.numpy())# 将标注信息添加在图像上ax.text(boxes[j, 0], boxes[j, 1] + 8, label,color='w', size=11, backgroundcolor="none")# 下一个结果i += 1
# 显示图像
plt.show()

结果为:

1.4 数据处理的Pipeline

使用数据处理的tf.data.Dataset模块中pipline机制,可实现CPU多线程处理输入的数据,如读取图片和图片的一些的预处理,这样GPU可以专注于训练过程,而CPU去准备数据。

Dataset支持一类特殊的操作:Transformation。一个Dataset通过Transformation变成一个新的Dataset。通常我们可以通过Transformation完成数据变换,打乱,组成batch,生成epoch等一系列操作。常用的Transformation有:map、batch、shuffle和repeat。解析tfrecord文件得到的数据都可以使用这些方法,例如我们前面使用的:

# 利用map方法调用parse_example方法对所有数据进行处理,得到最终的经过
raw_datasets = raw_datasets.map(parse_example)

下面我们分别介绍:

1.4.1 map

使用 tf.data.Dataset.map,我们可以很方便地对数据集中的各个元素进行预处理。因为输入元素之间时独立的,所以可以在多个 CPU 核心上并行地进行预处理。map 变换提供了一个 num_parallel_calls参数去指定并行的级别。

dataset = dataset.map(map_func=parse_fn, num_parallel_calls=FLAGS.num_parallel_calls)

1.4.2 repeat

repeat的功能就是将整个序列重复多次,主要用来处理机器学习中的epoch,假设原先的数据是一个epoch,使用repeat(5)就可以将之变成5个epoch的数据。

1.4.3 prefetch

tf.data.Dataset.prefetch 提供解耦了 数据产生的时间 和 数据消耗的时间。具体来说,在数据被请求前,就从 dataset 中预加载一些数据,从而进一步提高性能。prefech(n) 一般作为最后一个 transformation,其中 n 为 batch_size。 prefetch 的使用方法如下:

# 最后一个变换
dataset = dataset.prefetch(buffer_size=FLAGS.prefetch_buffer_size)
return dataset

另外还可使用bacth方法组成批次数据送入网络中,也可使用shuffle方法对数据打乱。

1.5. 数据处理

yoloV3模型的输入图像的大小是32的倍数,所以我们需要对图像进行处理。在这里我们将图像的尺度调整为416x416的大小,为了保持长宽比,我将四周为0的像素以灰度值128进行填充。

def preprocess(image, bbox, input_shape=(416, 416)):# 增加batch维image = tf.expand_dims(image, axis=0)# 获取图像的高宽[height, width]img_shape = image.shape[1:3]# 将图像进行调整,插值方法是双三次插值,保留长宽比resize_image = tf.image.resize(image, input_shape, method=tf.image.ResizeMethod.BICUBIC, preserve_aspect_ratio=True)# 获取图像的宽高[height,width]resize_shape = resize_image.shape[1:3]  # 图像上方的填充大小top_pad = (input_shape[0] - resize_shape[0]) // 2# 图像下方的填充大小bottom_pad = input_shape[0] - resize_shape[0] - top_pad# 图像左方的填充大小left_pad = (input_shape[1] - resize_shape[1]) // 2# 图像右方的填充大小right_pad = input_shape[1] - resize_shape[1] - left_pad# 将图像周围填充128resize_image = tf.pad(resize_image, [[0, 0], [top_pad, bottom_pad], [left_pad, right_pad], [0, 0]], constant_values=128)# 类型转化image_data = tf.cast(resize_image, tf.float32) / 255.# 对标注框进行调整:进行尺度和平移调整# 尺度变换bbox = bbox * tf.convert_to_tensor([resize_shape[1], resize_shape[0], resize_shape[1], resize_shape[0]], dtype=tf.float32)# 除以原图像大小bbox = bbox / tf.convert_to_tensor([img_shape[1], img_shape[0], img_shape[1], img_shape[0]], dtype=tf.float32)# 平移,获取最终的结果bbox = bbox + tf.convert_to_tensor([left_pad, top_pad, left_pad, top_pad], dtype=tf.float32)# 返回return image_data, bbox

经过图像处理的送入到网络中的图像结果为:

# 将VOC_class字典的key和value进行翻转
new_voc_class = {v:k for k,v in voc_classes.items()}
# 将tfrecord中的图像进行展示
plt.figure(figsize=(15, 10))
i=0
# 从raw_datasets中选取3个样本,获取图像,大小,框的标注信息和类别信息
for image, width, height, boxes, boxes_category in raw_datasets.take(3):# 图像处理image, boxes = preprocess(image, boxes)# 进行绘图plt.subplot(1, 3, i+1)# 绘制图像plt.imshow(image[0])# 获取坐标区域ax = plt.gca()# 遍历所有的框for j in range(boxes.shape[0]):# 绘制框rect = Rectangle((boxes[j, 0], boxes[j, 1]), boxes[j, 2] -boxes[j, 0], boxes[j, 3]-boxes[j, 1], color='r', fill=False)# 将框显示在图像上ax.add_patch(rect)# 显示标注信息# 获取标注信息的idlabel_id = boxes_category[j]# 获取标准信息label = new_voc_class.get(label_id.numpy())# 将标注信息添加在图像上ax.text(boxes[j, 0], boxes[j, 1] + 8, label,color='w', size=11, backgroundcolor="none")# 下一个结果i += 1
# 显示图像
plt.show()

效果如下图所示:

2.模型构建

yoloV3的模型结构如下所示:整个v3结构里面,没有池化层和全连接层,网络的下采样是通过设置卷积的stride为2来达到的,每当通过这个卷积层之后图像的尺寸就会减小到一半。

2.1 基本组件

基本组件指蓝色方框内部分:

2.1.1 CBL

Yolov3网络结构中的最小组件,由Conv+Bn+Leaky_relu激活函数三者组成,

源码实现如下:

def ConvBlock(input_shape, filters, kernel_size, strides=(1, 1), padding=None):# padding根据步长的大小进行修改padding = 'valid' if strides == (2, 2) else 'same'# 输入inputs = tf.keras.Input(shape=input_shape)# 卷积层:加入L2正则化的卷积层conv = tf.keras.layers.Conv2D(filters, kernel_size=kernel_size, strides=strides,padding=padding, kernel_regularizer=tf.keras.regularizers.l2(l=5e-4))(inputs)# BN 层bn = tf.keras.layers.BatchNormalization()(conv)# 激活函数relu = tf.keras.layers.LeakyReLU(alpha=0.1)(bn)# 模型构建return tf.keras.Model(inputs=inputs, outputs=relu)

2.1.2 ResX

残差组件借鉴Resnet网络中的残差结构,让网络可以构建的更深,ResX由一个CBL和X个残差组件构成,是Yolov3中的大组件。每个Res模块前面的CBL都起到下采样的作用。

def ResBlock(input_shape, filters, blocks):# 指定输入inputs = tf.keras.Input(shape=input_shape)# 对输入进行padpad = tf.keras.layers.ZeroPadding2D(padding=((1, 0), (1, 0)))(inputs)# 卷积步长为2results = ConvBlock(pad.shape[1:], filters=filters,kernel_size=(3, 3), strides=(2, 2))(pad)# 构建残差单元for i in range(blocks):# 卷积results_conv = ConvBlock(results.shape[1:], filters=filters // 2, kernel_size=(1, 1))(results)# 卷积results_conv = ConvBlock(results_conv.shape[1:], filters=filters, kernel_size=(3, 3))(results_conv)# 融和results = tf.keras.layers.Add()([results_conv, results])# 返回模型return tf.keras.Model(inputs=inputs, outputs=results)

2.2 BackBone

BackBone是DarkNet53构成,用来进行特征提取,主要是ResX模块。

def Body(input_shape):# 模型输入inputs = tf.keras.Input(shape=input_shape)# 卷积结果(batch, 416, 416, 32)cb = ConvBlock(inputs.shape[1:], filters=32, kernel_size=(3, 3))(inputs)  # 残差模块 (batch, 208, 208, 64)rb1 = ResBlock(cb.shape[1:], filters=64, blocks=1)(cb)  # (batch, 104, 104, 128)rb2 = ResBlock(rb1.shape[1:], filters=128, blocks=2)(rb1)  # (batch, 52, 52, 256)rb3 = ResBlock(rb2.shape[1:], filters=256, blocks=8)(rb2)  # (batch, 26, 26, 512)rb4 = ResBlock(rb3.shape[1:], filters=512, blocks=8)(rb3)  # (batch, 13, 13, 1024)rb5 = ResBlock(rb4.shape[1:], filters=1024, blocks=4)(rb4)  return tf.keras.Model(inputs=inputs, outputs=(rb5, rb4, rb3))

2.3 输出部分

输出是3个尺度输出的CBL串联结构:

def Output(input_shape, input_filters, output_filters):# 输入数据inputs = tf.keras.Input(shape=input_shape)# 输出连续的六个模块cb1 = ConvBlock(inputs.shape[1:], filters=input_filters, kernel_size=(1, 1))(inputs)cb2 = ConvBlock(cb1.shape[1:], filters=input_filters * 2, kernel_size=(3, 3))(cb1)cb3 = ConvBlock(cb2.shape[1:], filters=input_filters,kernel_size=(1, 1))(cb2)cb4 = ConvBlock(cb3.shape[1:], filters=input_filters * 2, kernel_size=(3, 3))(cb3)cb5 = ConvBlock(cb4.shape[1:], filters=input_filters,kernel_size=(1, 1))(cb4)cb6 = ConvBlock(cb5.shape[1:], filters=input_filters * 2, kernel_size=(3, 3))(cb5)# 最后的第七个卷积块cb7 = ConvBlock(cb6.shape[1:], filters=output_filters, kernel_size=(1, 1))(cb6)return tf.keras.Model(inputs=inputs, outputs=(cb5, cb7))

2.4 V3模型构建

将模型的backbone输出的特征图进行融合后送入到output模块,构建整个yoloV3模型。

def YOLOv3(input_shape, class_num=80):# anchor数目anchor_num = 3# 输入数据inputs = tf.keras.Input(shape=input_shape)# 获取backbone输出的3个特征图large, middle, small = Body(inputs.shape[1:])(inputs)# 较大目标的检测x1, y1 = Output(large.shape[1:], 512, anchor_num * (class_num + 5))(large)# reshape成最终的数据结果y1 = tf.keras.layers.Reshape((input_shape[0] // 32, input_shape[1] // 32, 3, 5 + class_num))(y1)# 中等目标的检测cb1 = ConvBlock(x1.shape[1:], filters=256, kernel_size=(1, 1))(x1)# 上采样us1 = tf.keras.layers.UpSampling2D(2)(cb1)# 拼接cat1 = tf.keras.layers.Concatenate()([us1, middle])# 计算输出结果x2, y2 = Output(cat1.shape[1:], 256, anchor_num * (class_num + 5))(cat1)# reshape成最终的数据结果y2 = tf.keras.layers.Reshape((input_shape[0] // 16, input_shape[1] // 16, 3, 5 + class_num))(y2)# 较小目标检测cb2 = ConvBlock(x2.shape[1:], filters=128, kernel_size=(1, 1))(x2)# 上采样us2 = tf.keras.layers.UpSampling2D(2)(cb2)# 拼接cat2 = tf.keras.layers.Concatenate()([us2, small])# 计算输出结果x3, y3 = Output(cat2.shape[1:], 128, anchor_num * (class_num + 5))(cat2)# reshape成最终的数据结果y3 = tf.keras.layers.Reshape((input_shape[0] // 8, input_shape[1] // 8, 3, 5 + class_num))(y3)# 返回结果return tf.keras.Model(inputs=inputs, outputs=(y1, y2, y3))

2.5 输出结果处理

网络的输出结果是:

坐标是对anchor的修正,将其转换中心点坐标和宽高的形式,在预测过程和计算损失函数时使用。

V3网络输出的结果为$ t_x,t_y,t_w,t_h$ 与边框表示 bx,by,bw,bhbx,by,bw,bh之间的关系是:

cx,cycx,cy是当前网格左上角到图像左上角的距离, pw,phpw,ph是先验框的宽和高。根据上述关系对网络的输出进行修正。

另外对于分类的输出结果应送入到Sigmoid激活函数中进行处理。

在这里我们使用了一个常见的方法,它的作用是把任意的表达式function作为一个“Layer”对象:

keras.layers.Lambda(function, output_shape=None, mask=None, arguments=None)

参数:

  • function:需要封装的函数。
  • output_shape: 预期的函数输出尺寸。
  • arguments: 可选的需要传递给函数的关键字参数

转换过程如下:

# 将网络的输出结果转换为bbox的坐标及宽高
def OutputParser(input_shape, img_shape, anchors):# feats/input_shape的意义:[batch,height,width,anchor_num,(1(delta x) + 1(delta y) + 1(width scale) + 1(height scale) + 1(object mask) + class_num(class probability))]feats = tf.keras.Input(input_shape)# 获取网格grid的左上角x,y坐标,对应着cx,cy# 获取行y的坐标# 1.使用tf.shape获取feats的高# 2.使用tf.cast进行类型转换,转换为float32类型# 3.使用tf.range创建数字序列# 4.使用tf.reshape进行形状转换为(height,1,1,1)# 5.使用tf.tile对上述结果按照列数x进行平铺# 6.使用tf.keras.layers.Lambda转换成层grid_y = tf.keras.layers.Lambda(lambda x: tf.tile(tf.reshape(tf.range(tf.cast(tf.shape(x)[1], dtype=tf.float32), dtype=tf.float32), (-1, 1, 1, 1)), (1, tf.shape(x)[2], 1, 1)))(feats)# 获取列x的坐标# 1.使用tf.shape获取feats的宽# 2.使用tf.cast进行类型转换,转换为float32类型# 3.使用tf.range创建数字序列# 4.使用tf.reshape进行形状转换为(1,width,1,1)# 5.使用tf.tile对上述结果按照行数y进行平铺# 6.使用tf.keras.layers.Lambda转换成层grid_x = tf.keras.layers.Lambda(lambda x: tf.tile(tf.reshape(tf.range(tf.cast(tf.shape(x)[2], dtype=tf.float32), dtype=tf.float32), (1, -1, 1, 1)), (tf.shape(x)[1], 1, 1, 1)))(feats)# 构建grid的网格表示# grid.shape = (grid h, grid w, 1, 2)grid = tf.keras.layers.Concatenate(axis=-1)([grid_x, grid_y])# 获取每一个检测结果中心点坐标:将预测结果转换为中心点坐标# box_xy = (delta x, delta y) + (priorbox upper left x,priorbox upper left y) / (feature map.width, feature map.height)# box_xy.shape = (batch, grid h, grid w, anchor_num, 2)box_xy = tf.keras.layers.Lambda(lambda x: (tf.math.sigmoid(x[0][..., 0:2]) + x[1]) / tf.cast([tf.shape(x[1])[1], tf.shape(x[1])[0]], dtype=tf.float32))([feats, grid])# box_wh.shape = (batch, grid h, grid w, anchor_num, 2)# 获取检测结果的宽高# box_wh = (width scale, height scale) * (anchor width, anchor height) / (image.width, image.height)box_wh = tf.keras.layers.Lambda(lambda x, y, z: tf.math.exp(x[..., 2:4]) * y / tf.cast([z[1], z[0]], dtype=tf.float32), arguments={'y': anchors, 'z': img_shape})(feats)# 获取某一个anchor中包含目标的概率box_confidence = tf.keras.layers.Lambda(lambda x: tf.math.sigmoid(x[..., 4]))(feats)# 获取某一个anchor属于某一个类别的概率box_class_probs = tf.keras.layers.Lambda(lambda x: tf.math.sigmoid(x[..., 5:]))(feats)# 返回输出结果return tf.keras.Model(inputs=feats, outputs=(box_xy, box_wh, box_confidence, box_class_probs))

3.模型训练

3.1损失函数的计算

YoloV3的损失函数分为三部分:

  • box的损失:

只有负责检测的gridcell中的anchor才会计入损失,对x,y,w,h分别求均方误差

  • 置信度的损失

置信度的损失是二分类的交叉熵损失函数,所有的box都计入损失计算

  • 分类的损失:

分类的损失是二分类的交叉熵损失,只有负责检测目标的才计算损失

def Loss(img_shape, class_num=80):# anchor的尺度:分别检测小,中,大的目标anchors = {2: [[10, 13], [16, 30], [33, 23]], 1: [[30, 61], [62, 45], [59, 119]], 0: [[116, 90], [156, 198], [373, 326]]}# 构建计算损失函数的数组input_shapes = [(img_shape[0] // 32, img_shape[1] // 32, 3, 5 + class_num),(img_shape[0] // 16, img_shape[1] // 16, 3, 5 + class_num),(img_shape[0] // 8, img_shape[1] // 8, 3, 5 + class_num)]# 网络的输出值inputs = [tf.keras.Input(input_shape) for input_shape in input_shapes]# 目标值labels = [tf.keras.Input(input_shape) for input_shape in input_shapes]losses = list()# 遍历三个尺度的输出for l in range(3):# 获取当前尺度的形状input_shape_of_this_layer = input_shapes[l]# 获取当前尺度的anchoranchors_of_this_layer = anchors[l]# 获取网络输出input_of_this_layer = inputs[l]# 获取对应的目标值label_of_this_layer = labels[l]# YOLOV3模型输出的结果:中心点坐标,宽高,置信度pred_xy, pred_wh, pred_box_confidence, pred_class = OutputParser(input_shape_of_this_layer, img_shape, anchors_of_this_layer)(input_of_this_layer)# 预测框pred_box = tf.keras.layers.Concatenate()([pred_xy, pred_wh])# 真实值true_box = tf.keras.layers.Lambda(lambda x: x[..., 0:4])(label_of_this_layer)true_box_confidence = tf.keras.layers.Lambda(lambda x: x[..., 4])(label_of_this_layer)true_class = tf.keras.layers.Lambda(lambda x: x[..., 5:])(label_of_this_layer)# 获取box的置信度object_mask = tf.keras.layers.Lambda(lambda x: tf.cast(x, dtype=tf.bool))(true_box_confidence)# 计算MSE损失:只有正样本参与损失计算pos_loss = tf.keras.layers.Lambda(lambda x:tf.math.reduce_sum(tf.keras.losses.MSE(tf.boolean_mask(x[0], x[2]),tf.boolean_mask(x[1], x[2]))))([true_box, pred_box, object_mask])# 置信度的损失:交叉熵损失confidence_loss = tf.keras.layers.Lambda(lambda x:# 正样本的损失tf.keras.losses.BinaryCrossentropy(from_logits=False)(tf.boolean_mask(x[0], x[2]),tf.boolean_mask(x[1], x[2])) +# 负样本的损失100 * tf.keras.losses.BinaryCrossentropy(from_logits=False)(tf.boolean_mask(x[0], tf.math.logical_not(x[2])),tf.boolean_mask(x[1], tf.math.logical_not(x[2]))))([true_box_confidence, pred_box_confidence, object_mask])# 分类损失:只有正样本计算损失class_loss = tf.keras.layers.Lambda(lambda x:tf.keras.losses.BinaryCrossentropy(from_logits=False)(tf.boolean_mask(x[0], x[2]),tf.boolean_mask(x[1], x[2])))([true_class, pred_class, object_mask])# 损失结果loss = tf.keras.layers.Lambda(lambda x: tf.math.add_n(x))([pos_loss, confidence_loss, class_loss])losses.append(loss)# 计算损失值loss = tf.keras.layers.Lambda(lambda x: tf.math.add_n(x))(losses)return tf.keras.Model(inputs=(*inputs, *labels), outputs=loss)

3.2 正负样本的设定

在上述的loss计算中,负责进行目标预测的anchor就是正样本,而不负责进行目标预测的就是负样本,也就是背景,那在这里我们是如何设置正负样本的呢?如下图所示:

  • 正样本:首先计算目标中心点落在哪个grid上,然后计算这个grid对应的3个先验框(anchor)和目标真实位置的IOU值,取IOU值最大的先验框和目标匹配。那么该anchor 就负责预测这个目标,那这个anchor就作为正样本,将其置信度设为1,其他的目标值根据标注信息设置。
  • 负样本:所有不是正样本的anchor都是负样本,将其置信度设为0,参与损失计算,其它的值不参与损失计算,默认为0。

在实现的时候,为了提高计算速度做了优化,在计算是否为正样本时,我们认为anchor和目标的中心点是相同的,直接利用anchor和目标box的宽高计算交并比,确定正样本。实现如下:

  • 定义anchor:
YOLOv3_anchors = np.array([[10, 13], [16, 30], [33, 23], [30, 61], [62, 45], [59, 119], [116, 90], [156, 198], [373, 326]], dtype=np.int32)
  • 定义方法计算anchor对应的目标值,确定正负样本:
def bbox_to_tensor(bbox, label, input_shape=(416, 416), anchors=YOLOv3_anchors, num_classes=80):# bbox:真实值坐标表示为(xmin,ymin,xmax,ymax),是相对坐标# label: 每个bbox的类别# anchors = (9,2)# 返回:anchor对应的真实值,即正负样本的标记结果
  • 获取尺度个数和box的绝对坐标
# 获取有几个尺度的输出,每个尺度对应3个anchor:3num_layers = anchors.shape[0] // 3# anchor对应的特征图掩码:第一个特征图对应第6,7,8个anchor...anchor_mask = tf.cond(tf.equal(num_layers, 3), lambda: tf.constant([[6, 7, 8], [3, 4, 5], [0, 1, 2]]), lambda: tf.constant([[3, 4, 5], [1, 2, 3]]))# bbox的相对中心点坐标true_boxes_xy = (bbox[..., 0:2] + bbox[..., 2:4]) / 2.# bbox的相对宽高true_boxes_wh = tf.math.abs(bbox[..., 2:4] - bbox[..., 0:2])# bbox的结果:将中心点坐标和宽高拼接在一起true_boxes = tf.concat([true_boxes_xy, true_boxes_wh], axis=-1)# bbox的绝对坐标和绝对宽高boxes_xy = true_boxes[..., 0:2] * input_shapeboxes_wh = true_boxes[..., 2:4] * input_shape
  • 创建一个与网络输出大小相同的全零数组,用来设置真实值
# 生成与yoloV3输出结果相同大小的全0数组:y_true.shape[layer] = (height, width, anchor num, 5 + class num)y_true = tuple((np.zeros(shape=(input_shape[0] // {0: 32, 1: 16, 2: 8}[l], input_shape[1] // {0: 32, 1: 16, 2: 8}[l], tf.shape(anchor_mask[l, ...])[0], 5 + num_classes), dtype=np.float32) for l in range(num_layers)))
  • 计算anchor的位置信息
# 扩展一个维度,用来存放anchor的索引anchors = tf.expand_dims(tf.convert_to_tensor(anchors, dtype=tf.float32), 0)# 用于计算交并比# 以anchor中心为原点,计算右下角坐标anchor_maxes = anchors / 2.  # 以anchor中心为原点,计算左上角坐标anchor_mins = -anchor_maxes
  • 对目标进行筛选,只有宽度大于0的认为是真正的目标
# 创建一个mask,指明目标是否存在,宽度大于0的认为是真实的目标valid_mask = tf.greater(boxes_wh[..., 0], 0)# 获取真实的目标的宽高wh = tf.boolean_mask(boxes_wh, valid_mask)# 获取真实目标的box:valid_true_boxes.shape = (valid box num, 4)valid_true_boxes = tf.boolean_mask(boxes, valid_mask)# 获取真实目标的标签值:valid_label.shape = (valid box num)valid_label = tf.boolean_mask(label, valid_mask)
  • 获取与目标交并最大的anchor,那这些anchor即为正样本
# 当图像中存在目标时,计算与目标交并比最大的anchor作为正样本,并设置标记结果if wh.shape[0] > 0:# 扩展一个维度,用来存放对应的anchor:wh.shape = (valid box num, 1, 2)wh = tf.expand_dims(wh, -2)  # 以box的中心点为原点:计算右下角坐标:max of width, height, box_maxes.shape = (valid box num, 1, 2)box_maxes = wh / 2# 以box的中心点为原点:计算左上角坐标:min of width, height, box_mins.shape = (valid box num, 1, 2)box_mins = -box_maxes# 计算box与anchor交的左上角坐标:intersect_mins.shape = (valid box num, anchor num(9), 2)intersect_mins = tf.math.maximum(box_mins, anchor_mins)# 计算box与anchor交的右下角坐标:intersect_maxes.shape = (valid box num, anchor num(9), 2)intersect_maxes = tf.math.minimum(box_maxes, anchor_maxes)# 计算交集的宽高:intersect_wh.shape = (valid box num, anchor num(9), 2)intersect_wh = tf.math.maximum(intersect_maxes - intersect_mins, 0.)# 计算交集的面积:intersect_area.shape = (valid box num, anchor num(9))intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1]# 计算box的面积:box_area.shape = (valid box_num, 1)box_area = wh[..., 0] * wh[..., 1]# 计算anchor的面积:anchor_area.shape = (1, anchor num(9))anchor_area = anchors[..., 0] * anchors[..., 1]# 计算交并比:iou.shape = (valid box num, anchor num(9))iou = intersect_area / (box_area + anchor_area - intersect_area)# 计算与box交并比最大的anchor,将其作为正样本:best_anchor.shape = (valid box num)best_anchor = tf.math.argmax(iou, axis=-1, output_type=tf.int32)
  • 遍历匹配成功的anchor(正样本),设置目标值
# 遍历与box匹配成功的anchorfor t in range(tf.shape(best_anchor)[0]):# 获取第t个anchorn = best_anchor[t]# 获取anchor的位置pos = tf.where(tf.equal(anchor_mask, n))# 获取尺度值:0,1,2l = pos[0][0]# 获取对应的anchor索引k = pos[0][1]# 获取anchor对应的grid cell的列数,限制在0到最大值之间i = int(tf.clip_by_value(valid_true_boxes[t, 1] * y_true[l].shape[0], clip_value_min=0, clip_value_max=y_true[l].shape[0] - 1))# 获取anchor对应的grid cell的行数,限制在0到最大值之间 j = int(tf.clip_by_value(valid_true_boxes[t, 0] * y_true[l].shape[1], clip_value_min=0, clip_value_max=y_true[l].shape[1] - 1))# 获取anchor的类别c = valid_label[t]  # box的位置:(x,y,width,height)y_true[l][i, j, k, 0:4] = valid_true_boxes[t, 0:4]# 匹配上的都包含目标,置信度设为1y_true[l][i, j, k, 4] = 1  # 类别信息y_true[l][i, j, k, 5 + c] = 1
  • 返回结果
# 返回3个尺度对应的真实值return (tf.convert_to_tensor(y_true[0]), tf.convert_to_tensor(y_true[1]), tf.convert_to_tensor(y_true[2]))

3.3 模型训练

接下来我们利用已搭建好的网络和数据进行模型训练:

3.3.1 获取数据集

  • 首先从TFRecord文件中获取数据,并进行数据处理,得到对应的目标值,这些通过map方法来实现

1.定义方法进行数据处理和获取目标值

def map_function_impl(image, bbox, label):# 图像尺度调整image, bbox = preprocess(image, bbox, random=True)# 获取对应的目标值label1, label2, label3 = bbox_to_tensor(bbox, label)# 返回结果return image, label1, label2, label3

2.使用py_function来提高性能

def map_function(image, width, height, boxes, boxes_category):# 对数据进行处理,获取图像及目标值:提升性能image, label1, label2, label3 = tf.py_function(map_function_impl, inp=[image, boxes, boxes_category], Tout=[tf.float32, tf.float32, tf.float32, tf.float32])# 对图像和目标值进行尺度调整image = tf.reshape(image, (416, 416, 3))label1 = tf.reshape(label1, (13, 13, 3, 85))label2 = tf.reshape(label2, (26, 26, 3, 85))label3 = tf.reshape(label3, (52, 52, 3, 85))# 返回结果return image, (label1, label2, label3)

3.使用map方法对从TFRcords中读取的数据进行处理

# 从TFRecord文件中获取数据,并进行处理
batch_size=10
trainset = raw_datasets.map(map_function).shuffle(batch_size).batch(batch_size).prefetch(tf.data.experimental.AUTOTUNE)

3.3.2 模型训练

  • 模型初始化:
yolov3 = YOLOv3((416, 416, 3,), 20)
yolov3_loss = Loss((416,416,3), 20)
  • 定义优化方法:
# 定义优化方法
optimizer = tf.keras.optimizers.Adam(1e-4)
  • 接下来进行网络训练,这里使用:

1.定义tf.GradientTape的作用域,计算损失值

2.使用 tape.gradient(ys, xs)自动计算梯度

3.使用 optimizer.apply_gradients(grads_and_vars)自动更新模型参数

完成网络训练,并保存训练结果

# 遍历图像和目标值,进行更新
for images, labels in trainset:# 定义作用域with tf.GradientTape() as tape:# 将图像送入网络中outputs = yolov3(images)# 计算损失函数loss = yolov3_loss([*outputs, *labels])# 计算梯度grads = tape.gradient(loss, yolov3.trainable_variables)try:# 进行梯度检查grads_check = [tf.debugging.check_numerics(grad, 'the grad is not correct! cancel gradient apply!') for grad in grads]with tf.control_dependencies(grads_check):# 梯度更新optimizer.apply_gradients(zip(grads, yolov3.trainable_variables))except BaseException as e:print(e.message)
# 保存模型训练结果
yolov3.save('yolov3.h5')

4.模型预测

我们使用训练好的模型进行预测,在这里我们通过yoloV3模型进行预测,预测之后转换为绝对坐标后,获取多个尺度的预测结果拼接在一起,使用NMS进行检测框的筛选。

首先定义预测类:

# 定义预测类
class Predictor(object):

指明anchor的大小:

# anchorbox的大小anchors = {2: [[10, 13], [16, 30], [33, 23]], 1: [[30, 61], [62, 45], [59, 119]], 0: [[116, 90], [156, 198], [373, 326]]}

4.1 初始化

进行模型初始化

# 初始化def __init__(self, input_shape=(416, 416, 3), class_num=80, yolov3=None):# 输入大小self.input_shape = input_shape# 模型初始化self.yolov3 = tf.keras.models.load_model('yolov3.h5', compile = False)# 将结果转换为坐标值self.parsers = [OutputParser(tuple(self.yolov3.outputs[l].shape[1:]), self.input_shape, self.anchors[l]) for l in range(3)]

4.2 预测方法实现

在这里加入NMS方法:

4.2.1 获取网络的预测结果

    def predict(self, image, conf_thres=0.5, nms_thres=0.5):# conf_thres:置信度的阈值,NMS中交并比的阈值# 增加一维batchimages = tf.expand_dims(image, axis=0)# 图像变形resize_images = tf.image.resize(images, self.input_shape[:2], method=tf.image.ResizeMethod.BICUBIC, preserve_aspect_ratio=True)# 图像变形后的大小resize_shape = resize_images.shape[1:3]# 图像在上下左右填充的大小top_pad = (self.input_shape[0] - resize_shape[0]) // 2bottom_pad = self.input_shape[0] - resize_shape[0] - top_padleft_pad = (self.input_shape[1] - resize_shape[1]) // 2right_pad = self.input_shape[1] - resize_shape[1] - left_pad# 填充为128resize_images = tf.pad(resize_images, [[0, 0], [top_pad, bottom_pad], [left_pad, right_pad], [0, 0]], constant_values=128)# 标准差deviation = tf.constant([left_pad / self.input_shape[1],top_pad / self.input_shape[0], 0, 0], dtype=tf.float32)# 尺度的变换scale = tf.constant([self.input_shape[1] /resize_shape[1], self.input_shape[0] / resize_shape[0],self.input_shape[1] /resize_shape[1], self.input_shape[0] / resize_shape[0]], dtype=tf.float32)# 类型转换images_data = tf.cast(resize_images, tf.float32) / 255.# 输出结果outputs = self.yolov3(images_data)

4.2.2 结果组合

  • 遍历每个尺度的结果,进行拼接
        # 目标值whole_targets = tf.zeros((0, 6), dtype=tf.float32)# 遍历每一个尺度for i in range(3):# 获取预测的位置、置信度和分类结果pred_xy, pred_wh, pred_box_confidence, pred_class = self.parsers[i](outputs[i])# 获取目标框的位置pred_box = tf.keras.layers.Concatenate(axis=-1)([pred_xy, pred_wh])#目标框的置信度大于阈值的部分:target_mask.shape = (h, w, anchor num)target_mask = tf.greater(pred_box_confidence, conf_thres)# 获取大于阈值的部分的置信度:pred_box_confidence = (pred target num, 1)pred_box_confidence = tf.boolean_mask(pred_box_confidence, target_mask)# 在最后增加一维pred_box_confidence = tf.expand_dims(pred_box_confidence, axis=-1)# 获取对应的目标框检测结果 pred_box.shape = (pred target num, 4)pred_box = tf.boolean_mask(pred_box, target_mask)# 归一化处理pred_box = (pred_box - deviation) * scale * \[image.shape[1], image.shape[0], image.shape[1], image.shape[0]]# 分类结果:pred_class.shape = (pred target num, 1)pred_class = tf.boolean_mask(pred_class, target_mask)# 获取每个类别最大的索引pred_class = tf.math.argmax(pred_class, axis=-1)# 类型转换pred_class = tf.cast(tf.expand_dims(pred_class, axis=-1), dtype=tf.float32)# 将预测结果拼接在一起 targets,sgaoe = (pred target num, 6)targets = tf.keras.layers.Concatenate(axis=-1)([pred_box, pred_box_confidence, pred_class])# 将多个尺度的结果拼接在一起whole_targets = tf.keras.layers.Concatenate(axis=0)([whole_targets, targets])

4.2.3 NMS

  • 进行NMS得到最终的预测结果
# 进行NMS,排序以置信度排序,从大到小排序descend_idx = tf.argsort(whole_targets[..., 4], direction='DESCENDING')i = 0# 遍历while i < descend_idx.shape[0]:# 获取索引值idx = descend_idx[i]# 左上角坐标cur_upper_left = whole_targets[idx,0:2] - whole_targets[idx, 2:4] / 2# 右下角坐标cur_down_right = cur_upper_left + whole_targets[idx, 2:4]# 宽高wh = whole_targets[idx, 2:4]# 获取面积area = wh[..., 0] * wh[..., 1]# 下一个检测框的索引following_idx = descend_idx[i+1:]# 下一个检测框following_targets = tf.gather(whole_targets, following_idx)# 下一个检测框的左上角坐标following_upper_left = following_targets[...,0:2] - following_targets[..., 2:4] / 2# 下一个检测框的右下角坐标following_down_right = following_upper_left + following_targets[..., 2:4]# 下一个检测框的宽高following_wh = following_targets[..., 2:4]# 下一个检测框的面积following_area = following_wh[..., 0] * following_wh[..., 1]# 计算交并比# 计算交的左上角坐标max_upper_left = tf.math.maximum(cur_upper_left, following_upper_left)# 计算交的右下角坐标min_down_right = tf.math.minimum(cur_down_right, following_down_right)# 交的宽高intersect_wh = min_down_right - max_upper_left# 将宽高大于0,保持不变,小于0的置为0intersect_wh = tf.where(tf.math.greater(intersect_wh, 0), intersect_wh, tf.zeros_like(intersect_wh))# 计算交的面积intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1]# 计算交并比overlap = intersect_area / (area + following_area - intersect_area)# 获取小于NMS阈值的保留,其他的舍弃indices = tf.where(tf.less(overlap, nms_thres))# 进行切片,保留结果following_idx = tf.gather_nd(following_idx, indices)# 将其添加到descend中即可descend_idx = tf.concat([descend_idx[:i + 1], following_idx], axis=0)i += 1# 获取最终的结果whole_targets = tf.gather(whole_targets, descend_idx)# 左上角坐标upper_left = (whole_targets[..., 0:2] - whole_targets[..., 2:4] / 2)# 右下角坐标down_right = (upper_left + whole_targets[..., 2:4])# 获取检测结果boundings = tf.keras.layers.Concatenate(axis=-1)([upper_left, down_right, whole_targets[..., 4:]])return boundings

4.3 预测结果

模型的预测效果:

import cv2
import numpy as np
import matplotlib.pyplot as plt
# 图像读取
img = cv2.imread("image.jpg")
# 实例化
predictor = Predictor()
# 获取结果
boundings = predictor.predict(img)
# 显示图像
plt.imshow(img[:, :, ::-1])
# 获取坐标区域
ax = plt.gca()
# 加载模型:模型训练是在COCO数据集中进行的,
# coco数据集中的类别信息
classes = ['person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog','horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe','backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard','tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush']
for bounding in boundings:# 绘制框rect = Rectangle((bounding[0].numpy(), bounding[1].numpy()), bounding[2].numpy() - bounding[0].numpy(), bounding[3].numpy()-bounding[1].numpy(), color='r', fill=False)# 将框显示在图像上ax.add_patch(rect)# 显示类别信息# 获取类别信息的idlabel_id = bounding[5].numpy().astype('int32')# 获取类别label = classes[label_id]# 将标注信息添加在图像上ax.text(bounding[0].numpy(), bounding[1].numpy() + 8,label, color='w', size=11, backgroundcolor="none")# 下一个结果# 显示图像
plt.show()

预测结果如下图所示:


总结

  • 熟悉TFRecord文件的使用方法

TFRecord是Google官方推荐使用的数据格式化存储工具,为TensorFlow量身打造的。TFRecord内部包含多个tf.train.Example,一般来说对应一个图像数据,在一个Example消息体中包含了一系列的tf.train.feature属性,而 每一个feature是一个key-value的键值对。

  • 知道YoloV3模型结构及构建方法

基本组件的构建,backbone,output, yoloV3, 输出值的转换

  • 知道数据处理方法

知道对图像进行resize,保持宽高比,进行pad的方法

  • 能够利用yoloV3模型进行训练和预测

, ‘umbrella’, ‘handbag’, ‘tie’, ‘suitcase’, ‘frisbee’,
‘skis’, ‘snowboard’, ‘sports ball’, ‘kite’, ‘baseball bat’, ‘baseball glove’,
‘skateboard’, ‘surfboard’,‘tennis racket’, ‘bottle’, ‘wine glass’, ‘cup’, ‘fork’,
‘knife’, ‘spoon’, ‘bowl’, ‘banana’, ‘apple’, ‘sandwich’, ‘orange’, ‘broccoli’,
‘carrot’, ‘hot dog’, ‘pizza’, ‘donut’, ‘cake’, ‘chair’, ‘couch’, ‘potted plant’,
‘bed’, ‘dining table’, ‘toilet’, ‘tv’, ‘laptop’, ‘mouse’, ‘remote’, ‘keyboard’,
‘cell phone’, ‘microwave’, ‘oven’, ‘toaster’, ‘sink’, ‘refrigerator’, ‘book’,
‘clock’, ‘vase’, ‘scissors’, ‘teddy bear’, ‘hair drier’, ‘toothbrush’]
for bounding in boundings:
# 绘制框
rect = Rectangle((bounding[0].numpy(), bounding[1].numpy()), bounding[2].numpy(
) - bounding[0].numpy(), bounding[3].numpy()-bounding[1].numpy(), color=‘r’, fill=False)
# 将框显示在图像上
ax.add_patch(rect)
# 显示类别信息
# 获取类别信息的id
label_id = bounding[5].numpy().astype(‘int32’)
# 获取类别
label = classes[label_id]
# 将标注信息添加在图像上
ax.text(bounding[0].numpy(), bounding[1].numpy() + 8,
label, color=‘w’, size=11, backgroundcolor=“none”)
# 下一个结果

显示图像

plt.show()


预测结果如下图所示:[外链图片转存中...(img-4wDe0yzV-1652848417340)]------**总结**- 熟悉TFRecord文件的使用方法TFRecord是Google官方推荐使用的数据格式化存储工具,为TensorFlow量身打造的。TFRecord内部包含多个tf.train.Example,一般来说对应一个图像数据,在一个Example消息体中包含了一系列的tf.train.feature属性,而 每一个feature是一个key-value的键值对。- 知道YoloV3模型结构及构建方法基本组件的构建,backbone,output, yoloV3, 输出值的转换- 知道数据处理方法知道对图像进行resize,保持宽高比,进行pad的方法- 能够利用yoloV3模型进行训练和预测知道损失函数,正负样本设置,进行训练,并预测的过程。

人工智能教程第四课 yolo入门和案例相关推荐

  1. 微信公众平台开发教程(四) 实例入门:机器人(附源码)

    微信公众平台开发教程(四) 实例入门:机器人(附源码) 上一篇文章,写了基本框架,可能很多人会觉得晕头转向,这里提供一个简单的例子来予以说明,希望能帮你解开谜团. 一.功能介绍 通过微信公众平台实现在 ...

  2. 火山PC抓取快递物流查询接口教程第四课

    本源码转载自利快云https://www.lkuaiy.com/ 火山PC抓取快递物流查询接口教程第四课 一.需要调用的模块 视窗基本类 MFC界面基本类 MFC界面扩展类库1 火山模块 二.火山项目 ...

  3. 【零基础深度学习教程第四课:卷积神经网络 (上)】

    [零基础深度学习教程第四课:卷积神经网络 (上)] 一.边缘检测与卷积运算 1.1 案例分析 1.2 卷积运算 1.2.1 概述 1.2.2 具体说明 1.3 原理解读 二.卷积运算参数 2.1 几个 ...

  4. NeHe OpenGL教程 第四课:旋转

    转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...

  5. Bootstrap 教程第四课:制作一组功能图标按钮

    上一节内容,我们讲到在Bootstrap中,如何使用文字图标制作带有图标的按钮( Bootstrap教程第三课:制作有图标的按钮),这节课,我们将在上一节的基础上,进行一组功能图标按钮的制作. 我们以 ...

  6. MT4/MQL4入门到精通EA教程第四课-MQL语言常用函数(四)-K线取值常用函数

    MQL中有一组"函数",他们长得跟其他函数不一样,是不一样的函数,就是下面这几个功能强大的"函数" Open[].Close[].High[].Low[].Ti ...

  7. 易语言新手入门教程第四课 - 简单模仿QQ登录窗口

    简单模仿QQ登录窗口 1.编辑框 2.如果() 3.载入() 4.销毁() 作业: 看完教程,然后自己用易语言写一个一样的程序 易语言如果命令的说明: 调用格式: 〈无返回值〉 如果 (逻辑型 条件) ...

  8. C#教程第四课:循环控制语句

    本节课将介绍如何使用C#控制语句中的循环语句,本课目的如下: 1.学会"while"循环的用法. 2.学会"do" 循环的用法. 3.学会"for&q ...

  9. status c语言_STM32 嵌入式C语言教程--第四课C语言中的存储空间与位域

    1.存储空间--堆和栈的区别 一个由C/C++编译的程序占用的内存分为以下几个部分 1.栈区(stack)- 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等.其操作方式类似于数据结构中的栈. ...

最新文章

  1. TableLayout(表格布局)
  2. python与excel做数据可视化-我在工作中是怎么玩数据的—数据可视化系列教程—Python篇...
  3. java发送焦点做移键值_xiaoguozi's Blog
  4. spring boot redis分布式锁
  5. node.js 微信小程序 部署服务器_微信小程序云开发如何上手
  6. python实验五答案_python程序设计 实验指导答案
  7. 数字图像处理 关于matlab的图像处理操作
  8. Chrome安装Octotree插件
  9. 操作系统进程调度实验
  10. 范德堡计算机科学硕士,美国范德堡大学计算机科学专业怎么样?
  11. 处理Nginx返回octet-stream数据流的配置
  12. XSS修炼之独孤九剑
  13. RASP技术进阶系列(一):与WAF的“相爱相杀”
  14. excel综合应用(一):信息查询
  15. 后门触发器之频域角度——Rethinking the Backdoor Attacks’ Triggers A Frequency Perspective
  16. 5.入律古风、排律与柏梁体
  17. CacheCloud管理平台
  18. 冈萨雷斯数字图像处理——彩色图像增强3实例
  19. 去IOE运动-正在路上
  20. vue 调用语音播放

热门文章

  1. 大话设计模式读书笔记之单例模式
  2. 论文阅读:SUPER: A Novel Lane Detection System
  3. AutoJS4.1.0实战教程---京东领京豆
  4. EOJ 3674.唐纳德先生与 .DOC
  5. 孙子兵法 军形第四(翻译)
  6. ip话机 mitel_华为IP话机
  7. 案例:用逻辑回归制作评分卡
  8. 面试官-你真的懂computed原理?(源码解读)
  9. 电子台账之自定义财务报表模板
  10. 55. Jump Game 解题记录