1.前言
距离上次论文阅读过去蛮久的了,在完成老板的要求后,终于有空上班的时候苟一苟了,下面就记录一下自己阅读yolov3的一些心得,方便以后回顾
本文目录:
- yolo v3的网络结构
- yolo v3的先验框anchor
- yolo v3的前向传播和损失函数
2.Yolo v3的网络结构
在刚开始阅读yolo v3的时候总感觉缺了点东西,后来才知道,作者并没有像v1中那样,清晰的给出网络的结构图。于是乎,找呀找,找到了这张宝图,图的作者还是比较用心的,基本比较严谨,可以拿来作为参考。
原来yolo v2中的darknet-19(特征提取网络)在yolo v3中变成了darknet-53(上图中红色虚线部分),网络越深,表达效果肯定也会得到提升,不过,这也要归功于ResNet的残差组件。有关ResNet的部分,可以参考ResNet介绍。
DBL:上图中的左下角部分,卷积+BatchNormal+Lecky_relu,基本是v3最小的组件,除了最后一个卷积,其余的后面都会跟着BN、Lecky_relu。
Resn:n代表数字,有res1,res2, … ,res8等等,表示这个res_block里含有多少个res_unit。这是yolo_v3的大组件,yolo_v3开始借鉴了ResNet的残差结构,使用这种结构可以让网络结构更深(从v2的darknet-19上升到v3的darknet-53,前者没有残差结构)。对于res_block的解释,可以在图1的右下角直观看到,其基本组件也是DBL。
concat与add:concat是张量的拼接,将darknet-53中间层与后面某一层经过上采样得到的张量进行拼接,会造成维度的增加。而对于残差组件中的add操作,只会将对应位置进行相加,不会造成维度的变化。
从上图可以看到,这个特征提取网络最后有三个输出,分别对应三个分支。这三个输出特征图的宽高分别为(w/8,h/8),(w/16,h/16),(w/32,h/32)。
说到这,想起来前段时间看Faster RCNN。作者发现对于小物体检测效果不佳,于是后面进行了一个小的升级,将自己的VGG卷积积换成了FPN(特征金字塔网络),在我之前的文章中,也说到过这,可以参考文章前半部分。
不过,需要注意的是,yolo v3对特征进行融合的时候,还是与FPN有些许不同的,这里对两个特征尺度提取的张量进行融合,没有用add,而是用的concat,这两个是不一样的,虽然效果相近。
对yolo v3的网络结构大概说到这,下面看看yolo v3的先验框anchor。
2.YOLO V3 的Anchor
YOLO V3继承了YOLO V2中的锚(anchor,我们理解为基础框/先验框,后面需要以这些框为基础进行bounding box微调),但是又不太一样。下面引用知乎大佬对这部分的一些理解:
anchor box最初是由Faster RCNN引入的。anchor box(论文中也称为bounding box prior,后面均使用anchor box)其实就是从训练集的所有ground truth box中统计(使用k-means)出来的在训练集中最经常出现的几个box形状和尺寸。比如,在某个训练集中最常出现的box形状有扁长的、瘦高的和宽高比例差不多的正方形这三种形状。我们可以预先将这些统计上的先验(或来自人类的)经验加入到模型中,这样模型在学习的时候,瞎找的可能性会变小,有助于模型快速收敛了
在YOLO V2中,设置了5个宽高比例的anchor(通过聚类获得),每个cell负责的anchor的数量为5,而在YOLO V3中,共设置9个宽高不同的anchor(同样是通过聚类获取得到),每个cell的anchor的数量为9/3=3个,因为YOLO V3有3个feature_map,不同feature_map的size和感受野是不一样的,较小size的feature_map具有较大的感受野,所以负责检测较大的物体,同理,较大size的feature_map感受野较小,负责检测较小的物体。
在yolo v1中,每个cell中的三个bbox(bounding box)只能用来预测同一个物体,但是到v3,作者取消了这一限制,同一个cell中不同的bbox可以用来预测不同的物体。了解了这个之后,并且获得了量化后的anchor,我们要怎么在实际模型中加入anchor的先验知识呢?
可能有人会说,可以让第一个bbox预测与第一个anchor相似的box,第二个bbox预测与第二个anchor相似的box,以此类推......那么问题来了,我们如何绑定每个gt(ground truth)对应的anchor,也就是说,我们要用bbox来预测哪一个ground truth,答案是IOU(交并比),也就是说与gt IOU最大的anchor对应的bbox来预测该gt。
接下来,还有一个问题,就是说我如何让bbox的形状保持与anchor类似,也就是说bbox在训练时即使会发生变化,但是仍然会保持与anchor类似的长宽比。yolo的做法是,不让bbox直接预测实际box的长和宽,而是与anchor的长和宽绑定,公式如下:
这样不管bbox预测的
对于box中心点坐标
其中:
3.YOLO V3的前向传播和损失函数
3.1.YOLO V3的前向传播
如图一所示,yolo v3模型接受输入为416*416*3的图片,返回三个不同size的特征图y1(13*13*255)、y2(26*26*255)和y3(52*52*255),这里的255跟实际数据集的类别数有关。COCO数据集一共有80类,每个特征图的grid cell负责三个先验框anchor,所以有:
255 = 3 * [80(类别估计概率分布) + 1(置信度) + 4(预测框)]
对于训练初期,网络输出的y1、y2和y3会与实际框的位置信息、类别有较大差异,所以会产生非常大的损失,然后根据损失反向传递不断优化模型参数,直到模型收敛就好了.......
3.2.YOLO V3的损失函数
值得一提的是,作者在训练的时候并不是将
假设真实框的坐标为
由于sigmoid的反函数比较难计算,所以直接计算对应的sigmoid函数的值:
这样,我们就可以根据训练的输出
所以接下来大致分为两部分,第一部分介绍对真值的操作过程,第二部分介绍得到要回归的真值之后如何与神经网络的输出构建loss
首先看对真值的操作过程。何为对真值的操作?上面已经说过,我们需要将gt转换成与网络输出相同的格式然后进行loss计算,下面结合图二,看一下整体流程:
输入是target,每一行是一个box的信息[image,class,x,y,w,h],分别代表属于batch中的哪张图片、box的类别以及框的坐标。然后针对不同的特征图尺寸进行单独处理,下面以最小尺寸为例,其他类似:
- 首先选取n个box的长宽真值wh_gt(n*2),分别与anchor计算IOU,保留大于iou阈值的框(m个),其他的框说明不适合在当前尺寸特征图下进行检测。
以下针对剩余的m个框
2. 提取框位置的真值xy_gt,并与向下取整后的值做差(之前说过,框位置坐标是相对于grid cell方格左上角顶点的偏移,而对于xy_gt向下取整正好对应着左上角的坐标),得到
3. 提取框尺寸真值wh_gt,并与对应的anchor比较,计算尺寸偏差
4. 提取框的类别cls。
5. 记录剩余m个框对应anchor的id和对应图片的id,以及位置取整后的值(对应grid cell左上角的坐标,表示用哪个格子进行预测)indicies。
在获得gt真值预处理后的结果indicies、
- 按照ouput的大小构建tconf,表示框的真实置信度。
2. 按照真值的indices将对应的某张图片中的某张格子的某个anchor的置信度置1。因为框要由这个图片的这个格子的这个anchor预测,所以这个图片中的这个格子的这个预测的置信度的真值应该为1。
3. 将ouput中的值与真值比较构建位置的loss(lxy),尺寸的loss(lwh),类别的loss(lcls),置信度的loss(lconf)。
4. 然后加权得到总的Loss。
具体的计算如下:
- 参数
:在训练中,如果某个grid cell的bbox没有负责预测某个对象,那我们就不应该训练该bbox的条件类别概率和坐标参数,因为使用这些参数的前提是在明确清楚该bbox负责预测某个gt box(后面说明怎么决定是否负责),即不应该根据条件类别概率和中心坐标输出误差调整相应的weights,那如何不进行这部分训练呢,当然是不让他们对loss做出贡献,也就没有它们什么事情了,这个时候就需要参数了,当该bbox负责预测某个gt box时,,否则。
- 参数
的取值是由grid cell的bbox有没有负责预测某个对象决定的。如果负责,那么,否则。如何确定有没有负责预测某个对象呢?前文已经说过,与gt IOU最大的anchor对应的bbox来预测该gt。
- 参数
,其他情况。
本篇大致说了一些yolo v3的框架,最后附上上文说的gt真值的预处理(build_targets)以及loss的计算(compute_loss),留着慢慢啃吧。。。。。。
def build_targets(p, targets, model): #p:prediction
# Build targets for compute_loss(), input targets(image,class,x,y,w,h)
nt = targets.shape[0] #一个batch中target的总数
tcls, tbox, indices, anch = [], [], [], []
gain = torch.ones(6, device=targets.device) # normalized to gridspace gain
off = torch.tensor([[1, 0], [0, 1], [-1, 0], [0, -1]], device=targets.device).float() # overlap offsets
style = None
multi_gpu = type(model) in (nn.parallel.DataParallel, nn.parallel.DistributedDataParallel)
for i, j in enumerate(model.yolo_layers):
anchors = model.module.module_list[j].anchor_vec if multi_gpu else model.module_list[j].anchor_vec
gain[2:] = torch.tensor(p[i].shape)[[3, 2, 3, 2]] # xyxy gain
na = anchors.shape[0] # number of anchors
at = torch.arange(na).view(na, 1).repeat(1, nt) # anchor tensor, same as .repeat_interleave(nt)
#type = (na,nt)
# Match targets to anchors
a, t, offsets = [], targets * gain, 0
if nt:
# r = t[None, :, 4:6] / anchors[:, None] # wh ratio
# j = torch.max(r, 1. / r).max(2)[0] < model.hyp['anchor_t'] # compare
j = wh_iou(anchors, t[:, 4:6]) > model.hyp['iou_t'] # iou(3,n) = wh_iou(anchors(3,2), gwh(n,2))
a, t = at[j], t.repeat(na, 1, 1)[j] # filter
# overlaps
gxy = t[:, 2:4] # grid xy
z = torch.zeros_like(gxy)
if style == 'rect2':
g = 0.2 # offset
j, k = ((gxy % 1. < g) & (gxy > 1.)).T
a, t = torch.cat((a, a[j], a[k]), 0), torch.cat((t, t[j], t[k]), 0)
offsets = torch.cat((z, z[j] + off[0], z[k] + off[1]), 0) * g
elif style == 'rect4':
g = 0.5 # offset
j, k = ((gxy % 1. < g) & (gxy > 1.)).T
l, m = ((gxy % 1. > (1 - g)) & (gxy < (gain[[2, 3]] - 1.))).T
a, t = torch.cat((a, a[j], a[k], a[l], a[m]), 0), torch.cat((t, t[j], t[k], t[l], t[m]), 0)
offsets = torch.cat((z, z[j] + off[0], z[k] + off[1], z[l] + off[2], z[m] + off[3]), 0) * g
# Define
b, c = t[:, :2].long().T # image, class
gxy = t[:, 2:4] # grid xy
gwh = t[:, 4:6] # grid wh
gij = (gxy - offsets).long()
gi, gj = gij.T # grid xy indices
# Append
indices.append((b, a, gj, gi)) # image, anchor, grid indices
tbox.append(torch.cat((gxy - gij, gwh), 1)) # box
anch.append(anchors[a]) # anchors
tcls.append(c) # class
if c.shape[0]: # if any targets
assert c.max() < model.nc, 'Model accepts %g classes labeled from 0-%g, however you labelled a class %g. '
'See https://github.com/ultralytics/yolov3/wiki/Train-Custom-Data' % (
model.nc, model.nc - 1, c.max())
return tcls, tbox, indices, anch
def compute_loss(p, targets, model): # predictions, targets, model
ft = torch.cuda.FloatTensor if p[0].is_cuda else torch.Tensor
lcls, lbox, lobj = ft([0]), ft([0]), ft([0])
tcls, tbox, indices, anchors = build_targets(p, targets, model) # targets
h = model.hyp # hyperparameters
red = 'mean' # Loss reduction (sum or mean)
# Define criteria
BCEcls = nn.BCEWithLogitsLoss(pos_weight=ft([h['cls_pw']]), reduction=red)
BCEobj = nn.BCEWithLogitsLoss(pos_weight=ft([h['obj_pw']]), reduction=red)
# class label smoothing https://arxiv.org/pdf/1902.04103.pdf eqn 3
cp, cn = smooth_BCE(eps=0.0)
# focal loss
g = h['fl_gamma'] # focal loss gamma
if g > 0:
BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)
# per output
nt = 0 # targets
for i, pi in enumerate(p): # layer index, layer predictions
b, a, gj, gi = indices[i] # image, anchor, gridy, gridx
tobj = torch.zeros_like(pi[..., 0]) # target obj
nb = b.shape[0] # number of targets
if nb:
nt += nb # cumulative targets
ps = pi[b, a, gj, gi] # prediction subset corresponding to targets
# GIoU
pxy = ps[:, :2].sigmoid()
pwh = ps[:, 2:4].exp().clamp(max=1E3) * anchors[i]
pbox = torch.cat((pxy, pwh), 1) # predicted box
giou = bbox_iou(pbox.t(), tbox[i], x1y1x2y2=False, GIoU=True) # giou(prediction, target)
lbox += (1.0 - giou).sum() if red == 'sum' else (1.0 - giou).mean() # giou loss
# Obj
tobj[b, a, gj, gi] = (1.0 - model.gr) + model.gr * giou.detach().clamp(0).type(tobj.dtype) # giou ratio
# Class
if model.nc > 1: # cls loss (only if multiple classes)
t = torch.full_like(ps[:, 5:], cn) # targets
t[range(nb), tcls[i]] = cp
lcls += BCEcls(ps[:, 5:], t) # BCE
# Append targets to text file
# with open('targets.txt', 'a') as file:
# [file.write('%11.5g ' * 4 % tuple(x) + 'n') for x in torch.cat((txy[i], twh[i]), 1)]
lobj += BCEobj(pi[..., 4], tobj) # obj loss
lbox *= h['giou']
lobj *= h['obj']
lcls *= h['cls']
if red == 'sum':
bs = tobj.shape[0] # batch size
g = 3.0 # loss gain
lobj *= g / bs
if nt:
lcls *= g / nt / model.nc
lbox *= g / nt
loss = lbox + lobj + lcls
return loss, torch.cat((lbox, lobj, lcls, loss)).detach()