我们有关YOLOv1的解读已经完成了。
Tobehonest,看完YOLOV1的源代码和这篇关于YOLOV2基于YOLOV1的改进[1]
阅读YOLOV2的代码就方便多了,以下我们照着上面那篇博客,关于YOLOV2在YOLOV1的基础上做的一些改进,进行重点解读。
YOLOV2基于YOLOV1的改进总结如下:
使用了BatchNormlization,建议读一下我上一篇文章,学习如何在代码中使用BN。训练上的不同,卷积基(darknet-19)的训练先用*图片训练,再使用*的图片训练。使用anchor,但是和FasterRCNN不太一样,后面我们会进行详细的对比。Anchor的提取,使用聚类算法找到可能最适合物体大小的5个宽高组合(5个效果就挺好的)passthrough层检测细粒度特征,网络层的一些改进接下来,我们照着论文[2]和代码来进行解说。2.基于代码的解读2.1网络结构和损失函数解读我们刚刚说了YOLOV2在YOLOV1上做的一些改进,下面结合代码进行说明,代码相比于FasterRCNN,甚至YOLOV1都更简单。
我们先搭建YOLOV2的网络结构。
defbuild_networks(self,inputs):net=self.conv_layer(inputs,[3,3,3,32],name=0_conv)net=self.pooling_layer(net,name=1_pool)net=self.conv_layer(net,[3,3,32,64],name=2_conv)net=self.pooling_layer(net,name=3_pool)net=self.conv_layer(net,[3,3,64,],name=4_conv)net=self.conv_layer(net,[1,1,,64],name=5_conv)net=self.conv_layer(net,[3,3,64,],name=6_conv)net=self.pooling_layer(net,name=7_pool)net=self.conv_layer(net,[3,3,,],name=8_conv)net=self.conv_layer(net,[1,1,,],name=9_conv)net=self.conv_layer(net,[3,3,,],name=10_conv)net=self.pooling_layer(net,name=11_pool)net=self.conv_layer(net,[3,3,,],name=12_conv)net=self.conv_layer(net,[1,1,,],name=13_conv)net=self.conv_layer(net,[3,3,,],name=14_conv)net=self.conv_layer(net,[1,1,,],name=15_conv)net16=self.conv_layer(net,[3,3,,],name=16_conv)net=self.pooling_layer(net16,name=17_pool)net=self.conv_layer(net,[3,3,,],name=18_conv)net=self.conv_layer(net,[1,1,,],name=19_conv)net=self.conv_layer(net,[3,3,,],name=20_conv)net=self.conv_layer(net,[1,1,,],name=21_conv)net=self.conv_layer(net,[3,3,,],name=22_conv)net=self.conv_layer(net,[3,3,,],name=23_conv)net24=self.conv_layer(net,[3,3,,],name=24_conv)#这里输出为(h/32,w/32,)net=self.conv_layer(net16,[1,1,,64],name=26_conv)net=self.reorg(net)#输出shape为(h/32,w/32,64*4)net=tf.concat([net,net24],3)#拼接,shape为(bz,h/32,w/32,+)net=self.conv_layer(net,[3,3,int(net.get_shape()[3]),],name=29_conv)#转(bz,h/32,w/32,)net=self.conv_layer(net,[1,1,,self.box_per_cell*(self.num_class+5)],batch_norm=False,name=30_conv)#转(bz,h/32,w/32,5*(num_classes+5))returnnet
其中的self.conv_layer层是自定义的,加入了BatchNormlization,如下:
defconv_layer(self,inputs,shape,batch_norm=True,name=0_conv):weight=tf.Variable(tf.truncated_normal(shape,stddev=0.1),name=weight)biases=tf.Variable(tf.constant(0.1,shape=[shape[3]]),name=biases)conv=tf.nn.conv2d(inputs,weight,strides=[1,1,1,1],padding=SAME,name=name)ifbatch_norm:depth=shape[3]scale=tf.Variable(tf.ones([depth,],dtype=float32),name=scale)shift=tf.Variable(tf.zeros([depth,],dtype=float32),name=shift)mean=tf.Variable(tf.ones([depth,],dtype=float32),name=rolling_mean)variance=tf.Variable(tf.ones([depth,],dtype=float32),name=rolling_variance)conv_bn=tf.nn.batch_normalization(conv,mean,variance,shift,scale,1e-05)conv=tf.add(conv_bn,biases)conv=tf.maximum(self.alpha*conv,conv)else:conv=tf.add(conv,biases)returnconv
对应了YOLOV2较YOLOV1做的第一个改进,使用了BatchNormlization。
搭建完网络模型后,如何设置损失函数就成了最重要的了。和YOLOV1的损失函数一样,输入的两个值为
(1)模型前向传播的结果predict(2)人为定义的,从图片中获取的labelpredict的维度,我们结合上面的网络,可以推出predict的shape为[batchsize,13,13,5,5+num_classes],然后我们需要在predict中提取相关位置对应的信息。
代码如下:
predict=tf.reshape(predict,[self.batch_size,self.cell_size,self.cell_size,self.box_per_cell,self.num_class+5])box_coordinate=tf.reshape(predict[:,:,:,:,:4],[self.batch_size,self.cell_size,self.cell_size,self.box_per_cell,4])box_confidence=tf.reshape(predict[:,:,:,:,4],[self.batch_size,self.cell_size,self.cell_size,self.box_per_cell,1])box_classes=tf.reshape(predict[:,:,:,:,5:],[self.batch_size,self.cell_size,self.cell_size,self.box_per_cell,self.num_class])
我们可以从上面代码中了解到我们需要提取的信息有:
(1)预测框的个数为(batchsize,13,13,5)(2)坐标信息(x,y,w,h),总共(batchsize,13,13,5)个坐标信息,所以shape为(batchsize,13,13,5,4)(3)置信度信息,shape为(batchsize,13,13,5,1)个,每个预测框对应一个置信度(4)每个框中的分类信息,shape为(batchsize,13,13,5,num_classes)我们提取了predict的信息后,需要对信息进行处理。
这里我们根据代码和论文进行详细的说明,代码如下
boxes1=tf.stack([(1.0/(1.0+tf.exp(-1.0*box_coordinate[:,:,:,:,0]))+self.offset)/self.cell_size,(1.0/(1.0+tf.exp(-1.0*box_coordinate[:,:,:,:,1]))+tf.transpose(self.offset,(0,2,1,3)))/self.cell_size,tf.sqrt(tf.exp(box_coordinate[:,:,:,:,2])*np.reshape(self.anchor[:5],[1,1,1,5])/self.cell_size),tf.sqrt(tf.exp(box_coordinate[:,:,:,:,3])*np.reshape(self.anchor[5:],[1,1,1,5])/self.cell_size)])box_coor_trans=tf.transpose(boxes1,(1,2,3,4,0))box_confidence=1.0/(1.0+tf.exp(-1.0*box_confidence))box_classes=tf.nn.softmax(box_classes)
这段代码先对predict数据中的坐标信息(x,y,w,h)进行了处理。
我们先解读:
(1.0/(1.0+tf.exp(-1.0*box_coordinate[:,:,:,:,0]))+self.offset)/self.cell_size
这里的box_coordinate[:,:,:,:,0]是对上述predict坐标信息中提取x,即中心坐标的横坐标x。这里将提取的x进入到sigmoid函数中,使其范围为(0,1)。再加入移动值self.offset,这里的self.offset在YOLOV1解读中已经说的挺清楚了,这段代码的意思,对应论文中的
其中
就是box_coordinate[:,:,:,:,0],σ函数就是sigmoid函数,
就是self.offset,而得到的
就是框的中心横坐标x在特征图13*13(每个格子宽高为1)中大小,最后的最后,它除了个self.cell_size(13),这意思就是归一化呀,就相当于整个特征图的宽高为1,那么这个框的横坐标占全图的比例。
第二行对框中心纵坐标y的处理和横坐标x一样。
1.0/(1.0+tf.exp(-1.0*box_coordinate[:,:,:,:,1]))+tf.transpose(self.offset,(0,2,1,3)))/self.cell_size
对应公式中的
得到的是归一化的框中心纵坐标y的坐标。
2.2YOLOV2中的Anchor(插入话题)在介绍对坐标信息的宽高处理前,我们插入YOLOV2中的anchor操作,因为我们看到了在宽、高处理的时候使用了anchor,所以这么就提前说下
(1)问:这些anchor是怎么获得的?
答:使用IOU为度量的K均值聚类获得的!获得的anchor是一系列宽、高的组合。
我们根据上面的图看出,随着聚类中心数的增加,avgIOU增大,但是我们实际中使用k=5就够了,即我们需要五组宽和五组高的组合。
代码中的config.py文件中已经给出了:
ANCHOR=[0.,1.,3.,7.,9.,0.,2.,5.,3.,9.]
(2)问:YOLOV2的anchor和FasterRCNN中的Anchor有什么不同呢?
答:在FasterRCNN中,我们用了9组固定宽高比的anchor,他们以特征图每个格点(每个cell)中心为中心,按照9个固定宽高比生成候选框,后面通过候选框和真实框的差距进行候选框的回归修正。而在YOLOV2中,我们用了5组固定宽高比的anchor,他们不是以特征图每个格点(每个cell)中心为中心,而是以上面提到的(box_coordinate[:,:,:,:,0],box_coordinate[:,:,:,:,1])为中心,生成5个中心不同,宽高也不同的box。然后根据落在格点是否为reponse格点和格点内的IOU最大框来进行学习中心坐标和宽高大小。
2.3代码继续解读对anchor进行了个简单的说明后,现在我们看下对网络输出的坐标信息中的宽、高进行处理的步骤:
tf.sqrt(tf.exp(box_coordinate[:,:,:,:,2])*np.reshape(self.anchor[:5],[1,1,1,5])/self.cell_size),tf.sqrt(tf.exp(box_coordinate[:,:,:,:,3])*np.reshape(self.anchor[5:],[1,1,1,5])/self.cell_size)])
这里对应论文中的
其中tw,th是网络输出的坐标信息中的宽、高(未处理)。
和
是anchor的宽和高,这里的
对应anchor的前五个数,
对应anchor的后五个数。后面又用了self.cell_size做了个归一化。
直到这里,所有的box的中心和宽高都做了归一化。
YOLOV2的label和YOLOV1的label处理基本上是一样的(多了个self.box_per_cell维度)。如下代码:
response=tf.reshape(label[:,:,:,:,0],[self.batch_size,self.cell_size,self.cell_size,self.box_per_cell])boxes=tf.reshape(label[:,:,:,:,1:5],[self.batch_size,self.cell_size,self.cell_size,self.box_per_cell,4])classes=tf.reshape(label[:,:,:,:,5:],[self.batch_size,self.cell_size,self.cell_size,self.box_per_cell,self.num_class])
接着我们按照YOLOV1的步骤,来计算下预测框和真实框之间的IOU,然后找到所有格子点(共13*13个格子点,其中每个格子中最大的IOU的索引)。代码如下:
iou=self.calc_iou(box_coor_trans,boxes)best_box=tf.to_float(tf.equal(iou,tf.reduce_max(iou,axis=-1,keep_dims=True)))confs=tf.expand_dims(best_box*response,axis=4)
这里的confs的意思是:格子点中最大IOU索引的集合。Shape为(batchsize,13,13,5,1)。
根据confs,我们构建conid、cooid和proid,这些会按所对应位置是0还是1计算损失。
conid=self.noobject_scale*(1.0-confs)+self.object_scale*confscooid=self.coordinate_scale*confsproid=self.class_scale*confs
最后我们计算各类损失:
coo_loss=cooid*tf.square(box_coor_trans-boxes)con_loss=conid*tf.square(box_confidence-confs)pro_loss=proid*tf.square(box_classes-classes)
最后合成总损失:
loss=tf.concat([coo_loss,con_loss,pro_loss],axis=4)loss=tf.reduce_mean(tf.reduce_sum(loss,axis=[1,2,3,4]),name=loss)3.小结
老样子,说完网络结构和loss函数,这个模型就算搞完了。关于train和test,和YOLOV1或者是FasterRCNN都挺像的,就不赘述!论文后面还有关于YOLO模型的一些知识点,后面我们抽空再说。
参考资料[1]关于YOLOV2基于YOLOV1的改进: