yolo v1 tensorflow版分类训练与检测训练

在题主的上一篇博文中,对yolo v1的原理及训练过程进行了分析

yolo v1的学习与理解

在github上也有相应的yolo v1 tensorflow版的代码,代码的作者为hizhangp

yolo v1的tensorflow实现

在yolo v1原理分析的过程中,yolo的训练主要包含两个部分:

  1. 分类训练: 在ImageNet 1000-classcompetition dataset训练网络的前20层,外加一层average-pooling layer和一层fully connected layer

  2. 检测训练:完成分类网络的训练后,将训练好的模型去掉最后的 average-pooling layer和fully connected layer。然后加上原来的四层conv layer和fully connected layer。在psiacl voc2007和2012上进行检测网络的训练

在实际的移植过程中,大多直接使用经过分类训练好的模型fine-tune进行检测训练,也就是跳过分类训练这一步 用训练好的模型直接进行检测训练。接下来的内容是题主在具体代码的实验过程,在这个过程中我对yolo算法的细节有了更深一步的理解。在实验过程描述之前,首先描述一下该代码复现yolo v1时与论文描述的一些不同之处。

在网络结构方面,代码中对网络的描述是这样的:

    def build_network(self,
                      images,
                      num_outputs,
                      alpha,
                      keep_prob=0.5,
                      is_training=True,
                      scope='yolo'):
        with tf.variable_scope(scope):
            with slim.arg_scope(
                [slim.conv2d, slim.fully_connected],
                activation_fn=leaky_relu(alpha),
                weights_regularizer=slim.l2_regularizer(0.0005),
                weights_initializer=tf.truncated_normal_initializer(0.0, 0.01) 
            ):
                net = tf.pad(
                    images, np.array([[0, 0], [3, 3], [3, 3], [0, 0]]),
                    name='pad_1') #padding=3 Image size=454*454
                net = slim.conv2d(
                    net, 64, 7, 2, padding='VALID', scope='conv_2') 
                net = slim.max_pool2d(net, 2, padding='SAME', scope='pool_3')
                net = slim.conv2d(net, 192, 3, scope='conv_4') 		
                net = slim.max_pool2d(net, 2, padding='SAME', scope='pool_5')
                net = slim.conv2d(net, 128, 1, scope='conv_6')
                net = slim.conv2d(net, 256, 3, scope='conv_7')
                net = slim.conv2d(net, 256, 1, scope='conv_8')
                net = slim.conv2d(net, 512, 3, scope='conv_9')
                net = slim.max_pool2d(net, 2, padding='SAME', scope='pool_10') 
                net = slim.conv2d(net, 256, 1, scope='conv_11')
                net = slim.conv2d(net, 512, 3, scope='conv_12')
                net = slim.conv2d(net, 256, 1, scope='conv_13')
                net = slim.conv2d(net, 512, 3, scope='conv_14')
                net = slim.conv2d(net, 256, 1, scope='conv_15')
                net = slim.conv2d(net, 512, 3, scope='conv_16')
                net = slim.conv2d(net, 256, 1, scope='conv_17')
                net = slim.conv2d(net, 512, 3, scope='conv_18')
                net = slim.conv2d(net, 512, 1, scope='conv_19')
                net = slim.conv2d(net, 1024, 3, scope='conv_20')
                net = slim.max_pool2d(net, 2, padding='SAME', scope='pool_21')
                net = slim.conv2d(net, 512, 1, scope='conv_22')
                net = slim.conv2d(net, 1024, 3, scope='conv_23')
                net = slim.conv2d(net, 512, 1, scope='conv_24')
                net = slim.conv2d(net, 1024, 3, scope='conv_25')
                net = slim.conv2d(net, 1024, 3, scope='conv_26')
                net = tf.pad(
                    net, np.array([[0, 0], [1, 1], [1, 1], [0, 0]]),
                    name='pad_27')
                net = slim.conv2d(
                    net, 1024, 3, 2, padding='VALID', scope='conv_28')
                net = slim.conv2d(net, 1024, 3, scope='conv_29')
                net = slim.conv2d(net, 1024, 3, scope='conv_30')
                net = tf.transpose(net, [0, 3, 1, 2], name='trans_31')
                net = slim.flatten(net, scope='flat_32')
                net = slim.fully_connected(net, 512, scope='fc_33')
                net = slim.fully_connected(net, 4096, scope='fc_34')
                net = slim.dropout(
                    net, keep_prob=keep_prob, is_training=is_training,
                    scope='dropout_35')
                net = slim.fully_connected(
                    net, num_outputs, activation_fn=None, scope='fc_36')、
        return net

网络结构大体与yolo论文中的描述一致,不同的地方如下:

  1. 一开始就对图像进行了padding 处理,输入图片的size由448*448变成了454*454
  2. 添加了fc_33这一层,在yolo原论文中是flatten操作之后,接一个输出为4096维的fully_connected层,也就是fc_34层
  3. 在flatten操作之前进行了一个transpose操作

针对第一个问题,题主猜想可能是为了有效的提取特征。454经过pool后达到227,因此就有了几何中心。或者yolo的原作者在darknet里的实现也是这样的?(题主没有看过yolo作者写的darknet版的yolo源码,所以这些只是猜想)

针对第二个问题,题主在之后的实验里发现。这一层的能够减小网络的参数量,因为在有这一层的情况下,输入的batch_size能达到45张图片,而去掉这层后batch_size=45时会出现内存溢出的情况(题主使用的是两块1080GPU进行训练)。后来分析在有这fc_33这一层的情况下,全连接层的参数量为flatten得到的参数量*512+512*4096也就是7*7*1024*512+512*4096。而去掉这层后,参数量变为7*7*1024*4096。7*7*1024*4096>>7*7*1024*512+512*4096所以经过分析,这层的作用是减小全连接层的参数量。

针对第三个问题,题主在去掉这个操作后发现对最终的结果并没有很明显的影响,所以暂时并不清楚这个transpose的作用。

Optimizer的选择:

在代码中optimizer选择使用的是gradientdescent optimizer,learning_rate设置为固定值0.0001

self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.learning_rate)

而yolo原论文里的选择是使用momentum optimizer,momentum的参数设置为 0.9。题主在之后实验中发现momentum optimizer的性能要远优于gradientdescent。所以不是很清楚代码中为什么要使用gradientdescent optimizer而不是yolo 原论文所说的momentum optimizer。

Learnin_rate:

在yolo原论文中的Learning_rate是根据epoch变化而变化的,代码中设定的固定值0.0001,在具体实现过程中发现这个学习率的效果也能达到变化的设定。

接下来将描述在代码的实验过程:

1.不使用分类训练的模型fine-tune 直接从初始化开始训练检测模型

题主在运行 yolo v1的tensorflow 实现 代码时遇到的一个小问题,由于需要翻墙下载,网络不稳定的原因,该代码提供的分类训练好的参数模型一开始无法正常下载。当时的解决方案是在源代码上直接进行检测训练,也就是不用分类模型的参数fine-tune。最终的训练结果如下:

从训练结果可以看到,coord_loss得到很好的下降,而class_loss并没有下降的趋势。也就是说在网络学习过程中网络学会了如何分辨出物体并框出物体的位置,但对于物体的类别是什么却无法很好的分辨处理。

通过训练好的参数进行检测,发现也无法达到检测的效果。其中一张图片的检测效果如下:

图的左边输出的是每一个bounding box的置信度,可以看到置信度都约等于0。在上一篇博文里有说到,在测试时的置信度是将conditional class probabilities与每个 bounding box的 confidence相乘。与左图对应的,右图中yolo也无法检测出物体,并不能达到很好的检测。

2.在yolo网络的检测训练过程中,去掉coord_loss项,只对class_loss项和object_loss、noobject_loss项进行训练

由于class_loss无法很好的下降,并无法得到分类训练好的权重。题主尝试在检测训练过程中只针对class_loss进行训练。但训练结果也是差强人意,并不能达到很好的效果。

所以总结一下,从以上的训练过程可以看出,yolo训练步骤中将分类训练与检测训练分开是有必要的。就像人的认知过程一样,是要先学会目标分类,才能完成目标检测。在题主的上一篇博文里也有提到,水果分类的问题:

首先要将一个物体放在眼前或者聚焦在某个物体上,先识别出这个物体的特征,能够认出这个物体。首先,要能够认出一个个水果的种类,比如先认出什么是苹果,什么是香蕉

在能够认出什么是苹果,什么是香蕉后,才能在果篮中找出香蕉和苹果,以及它们的位置。

3.通过分类训练好的模型fine-tune 进行检测训练

后来通过朋友的帮助,成功下载了分类训练好的模型,也就是YOLO_small.ckpt。这里提供百度网盘的链接,有需要的可以下载:

YOLO_small.ckpt下载链接

密码是wh16

使用该模型参数进行fine-tune后的训练结果如下图:

可以看到每一项误差都得到了很好的下降,训练效果比之前不使用分类模型的效果要好很多。

同样的测试结果也有了很好的提升,同样的一张图片的测试结果:

可以看到网络能检测出目标并框出目标位置,图左的置信度的概率也有相应的输出。但从检测结果也可以看出,yolo对于同一个grid中的多个物体不能很好的区分,无法同时分辨出自行车和人。这也是yolo v1的弊端。在之后的改进版本yolo v2中对这个问题有很好的改进。

4.从头开始复现yolo v1的训练过程

通过pretrained的模型参数进行fine-tune确实能达到不错的效果,但如何从头开始打造自己的模型。也就是说自己从头开始完成yolo训练过程中的分类训练与检测训练。由于ImageNet数据集达到了1000G,并且从yolo的论文中的描述,达到在imageNet 2012上Top 5的训练精度达到88%需要一周左右的时间。按照yolo v1正常的方式进行训练过程的复现的话将会花费大量的时间,所以题主首先想到的是能不能通过其他小型的分类网络训练或者减小训练集的图片数量进行训练,然后将训练好的模型移植到YOLO的模型中。题主首先进行的是使用CIFAR10数据集来进行分类网络训练,分类网络的结构为:

 # conv1
  with tf.variable_scope('cifar10'):
            with slim.arg_scope( 
                [slim.conv2d, slim.fully_connected],
                activation_fn=leaky_relu(0.01),
                weights_regularizer=slim.l2_regularizer(0.0005),
                weights_initializer=tf.truncated_normal_initializer(0.0, 0.01)
            ):
                net = slim.conv2d(images, 64, 7,2,padding='VALID',scope='conv1')
                net = slim.conv2d(net, 192, 3, scope='conv2')
                net = slim.conv2d(net, 128, 3, scope='conv3')
                net = slim.max_pool2d(net, 3, padding='SAME', scope='pool_1')
                net = slim.conv2d(net, 256, 3, scope='conv4')
										
 # local3
  with tf.variable_scope('local3') as scope:
    # Move everything into depth so we can perform a single matrix multiply.
    reshape = tf.reshape(net, [FLAGS.batch_size, -1])
    dim = reshape.get_shape()[1].value
    weights = _variable_with_weight_decay('weights', shape=[dim, 384],
                                          stddev=0.04, wd=0.004)
    biases = _variable_on_cpu('biases', [384], tf.constant_initializer(0.1))
    local3 = tf.nn.relu(tf.matmul(reshape, weights) + biases, name=scope.name)
    _activation_summary(local3)

  # local4
  with tf.variable_scope('local4') as scope:
    weights = _variable_with_weight_decay('weights', shape=[384, 192],
                                          stddev=0.04, wd=0.004)
    biases = _variable_on_cpu('biases', [192], tf.constant_initializer(0.1))
    local4 = tf.nn.relu(tf.matmul(local3, weights) + biases, name=scope.name)
    _activation_summary(local4)

  # linear layer(WX + b),
  # We don't apply softmax here because
  # tf.nn.sparse_softmax_cross_entropy_with_logits accepts the unscaled logits
  # and performs the softmax internally for efficiency.
  with tf.variable_scope('softmax_linear') as scope:
    weights = _variable_with_weight_decay('weights', [192, NUM_CLASSES],
                                          stddev=1/192.0, wd=0.0)
    biases = _variable_on_cpu('biases', [NUM_CLASSES],
                              tf.constant_initializer(0.0))
    softmax_linear = tf.add(tf.matmul(local4, weights), biases, name=scope.name)
    _activation_summary(softmax_linear)

  return softmax_linear

网络的卷积层也就是conv1,conv2,conv3,conv4的结构与yolo前四层的网络结构类似,卷积层之间的通道数目也尽量匹配。

训练的loss为交叉熵形式,在cifiar10训练集上训练100k步后,在验证集上的精度达到88%。

完成训练后将cifiar网络的前四层卷积层加上yolo的后四层卷积层和两个全连接层,再到pascal voc2007数据集上进行检测模型的训练,最终的训练结果如下图所示:

可以看到,依旧无法对类别进行很好的训练,但坐标误差依旧有有效的下降。

总结与分析:导致将cifar10模型移植到yolo上进行分类训练效果不好的原因可能有两个

其一是网络的深度不够,分类模型的卷积层只有4层,相比于yolo的20层网络,网络的学习能力不够强。无法同时完成分类,检测,框出bounding box的任务。

第二个原因就是数据集的质量和类别,首先cifar10数据集的图片size都是32*32的,这就导致在移植过程中,网络学习的feature map的比例相差较大。cifar10数据集的class也只有10类,这就导致在本身在分类过程中就有没有学习检测训练中20类的类别,更加加大了学习的难度。

同时,从坐标误差依旧有很好的下降可以推断出,浅层网络还是能够识别出相应的图片特征。而检测训练,也就是框出物体的功能主要由后加的4层卷积层与2层全连接层来实现的。

打造自己的分类数据集进行YOLO分类网络的训练

首先说一下数据集的来源与大小,数据集来自PASCAL VOC2007中的部分图片和自己从网络上下载的一些图片。总共20类,与VOC2007中的类别数量和种类一致,数据集中大约有1400图片。同时对数据进行翻转处理增强数据集,训练集与验证集的比例大约为9:1(因为类别20类,平均到每一类上的图片已经很少了),相比于ImageNet的大小来说是非常小的一个数据集,但题主主要做的是复现YOLO的训练过程的实验,对于这个实验来说已经够用了。在实际的应用过程中可以根据需求,将不同的数据集用于分类训练。数据集的百度网盘链接,有需要的朋友可以下载:

数据集下载

密码是vjhx

分类训练的过程也按照YOLO训练的步骤进行,首先使用YOLO网络的前20层卷积层加1层的average pool和1层 fully connected层进行分类网络的训练,分类的模型的损失函数使用的是交叉熵形式。optimizer 使用rmsprop,总步数为30k,起始的learning rate为0.001,最终的learning rate为0.0001,learning_rate_decay=0.94。在训练30k步后,模型在验证集上的precision达到70%左右,recall达到90%。也就是说网络已能基本识别出voc2007 中20类物体的特征。

之后将分类训练好的模型fine-tune到yolo的检测模型上,也就是去掉分类模型的average pool层和fully connected层,再加上yolo网络本身的最后4层fully connected层和2层卷积层(原代码中多了fc33这一层),之前有提到过,这一层的主要作用是降低参数的数量。实验过程中发现,在有这一层的情况下,batch size的最大值能达到45,没有这一层的情况下只能达到20。所以fc33参数压缩的作用还是很明显的。还有一点是要提的,就是原代码中一开始使用的gradient descent optimizer,但在实际过程中发现class loss的下降非常的缓慢。(之前还尝试过用vgg_16进行fine-tune,一开始也用gradient descent optimizer,始终无法出现收敛的情况)在将optimizer 改为yolo论文里推荐的Adam optimizer后效果有很大的改善,optimizer的参数为0.9。learning rate设置为0.0001。最终的训练效果如下,终于出现了class loss下降的曲线:

对比之前使用ImageNet分类训练好的模型,二者loss最终的收敛结果相差不大。

使用该模型用于图像检测:

可以看到训练的结果能检测出图中的目标,也证明了实验步骤的正确性。

但可以看到检测的置信度及框的IOU值都不如使用ImageNet训练的结果,同时在检测数据集上测试发现模型的overfiting现象很严重。在之后的工作中可以通过进一步改进模型参数和分类数据集来提升模型的检测效果,减弱overfiting。

猜你喜欢

转载自blog.csdn.net/weixin_40446651/article/details/81199790
今日推荐