深度学习之物体检测算法yolov3

一 yolov3网络结构

首先回顾下激活函数Relu

Relu函数将输入矩阵x内所有负值都设置为0,其余值不变

在Relu的基础上,引入LeakyRelu

对比Relu,LeakyRelu将负值以一个较小斜率保留下来了

再了解上采样(upsampling)的概念

简单的说就是把图片进行放大了,图片放大一般采用内插值方法,即在原有图像像素点之间采用合适的算法插入新的元素。

对应Pytorch函数,

torch.nn.Upsample(size, # 指定输出tensor大小

scale_factor, # 输出放大的倍数

mode, # 上采样算法,‘nearest’ 最近邻点插值法,‘linear’, ‘bilinear’,’trilinear’ 线性,双线性,三线性,插值法

align_corners # 如果为True表示输出tensor和原有tensor边角是对齐相等的)

使用方式,

import torch.nn as nn

m = nn.Upsample(scaler_factor=2, model=‘bilinear’, align_corners=True)

m(input)

yolov3完整网络结构图如下,结构图来源网上

从上图可以看出,

yolo系列的模型将(卷积+BN+LeakyRelu激活函数)一起作为单元组件CBL

class Conv(nn.Module):

   def __int__(self, c1, c2, k=1, s=1):

      super(Conv, self).__int__()

      self.conv = nn.Conv2d(in_channels=c1, out_channels=c2, kernel_size=k, stride=s)

      self.bn = nn.BatchNorm2d(num_features=c2)

   self.act = nn.LeakyRelu(0.1)

   def forward(self, x):

      return self.act(self.bn(self.conv(x)))

yolov3借鉴Resnet的残差结构,由(2个单元组件+残差)构成基础组件ResUnit

class Bottleneck(nn.Module):

   def __int__(self, c1, c2, shortcut=True, e=0.5):

      super(Bottleneck, self).__int__()

   c_ = int(c2 * e)

      self.cv1 = Conv(c1, c_, 1, 1)

      self.cv2 = Conv(c_, c2, 3, 1)

   self.shortcut = shortcut and c1==c2

   def forward(self, x):

      return x + self.cv2(self.cv1(x)) if self.shortcut else self.cv2(self.cv1(x))

注意,基础组件ResUnit的输入和输出tensor的大小不变,只是内部两个卷积计算的channels个数变动了下

重要组件ResX包含一个(CBL单元组件+X个基础组件ResUnit)

其中CBL单元组件中的卷积窗口移动步长stride为(2, 2), 因此每经过一个ResX组件,feature map大小缩小一倍,输出tensor的channels也会扩大一倍

X个ResUnit首尾串联在一起

再回到上图,

输入图片大小为608*608,chanles为3

经过第一个CBL,tensor大小为[608, 608, 32], 通过一次卷积实现channels从3到32

经过Res1,tensor大小变为[304, 304, 64]

经过Res2,tensor大小变为[152, 152, 128]

经过Res8,tensor大小变为[76, 76, 256]

到这里为止,

图像的特征图大小已经由最初的608*608缩小到76*76,因此也叫8倍下采样,作为模型第三个输出y3的一部分

再经过一个Res8,tensor大小为[38, 38, 512]

图像特征图大小继续缩小一倍,也叫16倍下采样,作为模型第二个输出y2的一部分

经过最后一个Res4,tensor大小为[19, 19, 1024]

图像特征图进行了32倍下采样,是模型第一个输出y1的一部分

可以看出的是,

yolov3模型网络不存在全连接层,图像特征图的大小缩小由设置卷积参数stride实现,需要对齐大小时,采用上采样Upsample实现

Concat操作作用在channels维度上,将宽和高一致的图像拼接起来

模型输出y1,y2,y3的channels维度都是255,都是由前一个卷积Conv(如图上)将输出channels设置为255实现的

前面介绍了yolov3的网络结构

那么,yolov3是如何实现准确检测不同大小的物体边框呢?

二 Yolov3物体检测原理

首先对训练数据集采用聚类算法,将物体大小聚类到K个框内,这些框称为anchor

yolov3采用COCO数据集,能够检测80种不同物体,物体大小聚类到K=9个anchors

以3个为一组,分别是,

[10,13, 16,30, 33,23]

[30,61, 62,45, 59,119]

[116,90, 156,198, 373,326]

这三组anchors分别分配给yolov3的三层输出y3,y2,y1

即输出tensor的特征图(feature map)越小,每个网格单元(grid cell)的感受野越大,越能对应大物体的检测

以y1为例,19*19为特征图大小,在三层输出中最小,对应anchors为[116,90, 156,198, 373,326],用来检测大物体

y2用来检测中等大小物体

y1用来检测小物体

输出中的255可以拆分为3*(4+1+80),

3表示分配给该层输出的3个anchor,每个网格单元都对应3个anchor

4表示相对所在网格单元左上角坐标的偏移,包含(x, y, w, h)

如何将(x, y, w, h)映射到真正的预测框(px, py, pw, ph),并和预测图片的标签数据计算损失函数,将在下一节详细给出

1表示当前预测框正好包含预测目标的置信度

80表示COCO数据集80个物体的置信度

yolov3 模型拥有三层输出,分别对应大中小三种物体大小的目标检测,将实际目标的中心坐标和目标的宽和高映射到每层输出的网格上,使得yolov3的输出具备预测图片上各种大小,各个位置的目标物体

三 Yolov3 损失函数计算

首先给出训练数据的标签数据格式,每一行表示图片上的一个目标,

class x_center, y_center, width, height

这里x_center,y_center为目标实际中心坐标除以宽/高,取值范围为0~1

width和height计算方式同上,取值范围也是0~1

为了描述方便,假定训练的Batch_size为1,即每次从训练集中取一张图片输入模型进行预测,且每张图片都只有一个检测目标,并在此过程中完成一次损失函数的计算

因此问题变为,给定一张图片的标签数据t,和这张图片的预测结果p,计算一次损失函数的值L

L由三部分组成,

lbox 计算p中预测框和t中标签物体框的差异

lobj 计算p中预测框置信度和根据p中预测框与t中物体框计算的实际置信度的差异

lcls 计算p中class与t中class的差异

L = lbox + lobj + lcls

针对模型的三层输出,损失函数值的计算是独立的

为了方便描述,以第一层y1为例,

预测结果为p[0],格式为(anchors, grid, grid, xywh+conf+classes)

标签t的格式为(class, x, y, w, h)

ng代表网格19*19的大小,用元组来表示,为(19,19)

anchor_vec表示anchor适配到当前网格上的大小,就是将原有anchor缩小32倍后的值,为[3.6,2.8], [4.875,6.18], [11.6,10.1]

原有anchor为[116,90, 156,198, 373,326]

将标签数据适配到当前网格的大小

class = t[0]

gxy = t[1:3] * ng[0]

gwh = t[3:5] * ng[1]

计算标签数据中心点所在网格左上角坐标

long()函数返回取整

gi, gj = gxy.long()

计算标签数据中心点相对网格左上角坐标的偏移

floor()函数返回数字的下舍整数

gxy -= gxy.floor()

得到标签数据的网格偏移坐标和映射到网格的宽高

tbox = (gxy, gwh)

计算gwh和anchor_vec的最大IoU(两个面积的交集/并集)

得到IoU最大值对应的anchor下标

iou = wh_iou(anchor_vec, gwh)

a = np.argmax(iou)

根据正样本所在位置索引到预测结果值

ps = p[0][a, gi, gj]

计算ps的预测框并与tbox计算GIoU, Ac为两个框的最小闭包面积

clamp有将数据夹紧到某个区间的功能,这里是为了防止溢出

pxy = torch.sigmoid(ps[0:2])

pwh = torch.exp(ps[2:4]).clamp(max=1E3) * anchor_vec[a]

pbox = (pxy, pwh)

giou = bbox_iou(pbox, tbox, GIoU=True)

计算损失函数值的第一部分lbox

lbox += (1.0 - giou)

计算损失函数的第二部分lobj

pytorch中使用detarch表示返回一个新的变量,从当前计算图中分离,不计算梯度

这一部分正负样本都参与了计算

采用的BCEWithLogitsLoss损失函数

tobj[a, gi, gj] = giou.detach().type(tobj.dtype)

lobj += BCEobj(p[0][..., 4], tobj)

计算损失函数的第三部分lcls

设置正样本class处置信度为1

也采用的BCEWithLogitsLoss损失函数

t = torch.zeros_like(ps[5:])

t[class] = 1.0

lcls = BCEcls(ps[5:], t)

到这里为止,在前面假设的训练Batch_size为1,且图片预测物体个数为1的简单情形下,损失函数值计算的全部过程已经介绍了一遍

实际情形下,Batch_size的大小不会是1,上面很多变量都需要增加一个维度images,这里就不累述了