【学习笔记4】Convolutional Pose Mashines在FashionAI中的应用【第二弹】——我的深度学习首秀(天池FashionAI关键点挑战赛复赛篇)

版权声明:欢迎指正存在的错误,欢迎分享最新的学习资源,转载请注明原出处 https://blog.csdn.net/qq_28659831/article/details/80545668

又持续炼了一个月左右的丹,今天复赛B阶段结束了,老衲总算熬到头了。和一群“仙人”竞赛,既压力山大,又动力满满。压力山大是前排“大仙”令人惊叹的NE值以及Top20的诱人奖励,毕竟自己曾经那么接近Top20;动力满满是自己一直是抱着学习的态度把这次比赛当做一个实践项目在做,能得到锻炼是我们一直做下去的动力,有差距,说明还有提升空间,这个差距激励着我坚持着做了下去。最终我们以复赛B榜NE=4.45%的成绩排名41/2322,较之初赛的NE=7.49%有了很大的提升,也算完成了初赛定下的保50的目标,排行榜上有名字还是好看一些的嘛(原谅我的虚荣心)。写本文的目的旨在和大家分享一些复赛阶段调CPM的一些细节,并开源我已证明有效的实验代码。此外,也和大家分享一下自己的感受,自勉并共勉吧。

我的实验代码github地址:https://github.com/shaoniangu/Realize_Convolutional_Pose_Machines_On_FashionAI

使用的CPM的tensorflow源码地址:https://github.com/timctho/convolutional-pose-machines-tensorflow

初赛阶段其实就已经发现了CPM存在的一些问题,在我的上一篇博文中也已提及,之所以没有放弃它一方面因为我们把初赛的多模型替换成单模型后性能有了质的提升,对CPM又重拾希望;另一方面是我们团队大多抱着学习的态度去做这次比赛,CPM的单模型Baseline大约在NE=6%左右,我们就想挑战一下baseline,就想看看在不使用检测方法提供先验constrain、不使用多checkpoint的ensemble、不与其他模型ensemble、只在原模型基础上加入一些tricks的情况下,作为姿态估计经典模型的CPM究竟能达到什么样的效果。于是乎,我们便做了各种实验,下文便说一些已经实现并证明有效的trick。

Trick 1——单模型和Weighted Loss

初赛阶段我们想着已有的类别标签是一个非常强的先验条件,一个网络模型只预测单独一个类别的衣服的关键点,网络不需要去考虑分类问题,任务更加简单,因此训了五个模型去分别预测五种衣服的关键点,最后初赛的NE=7.49%,效果不尽人意。复赛阶段开始我们就决定抛弃所谓的类别标签的先验条件,配合weighted loss使用单模型去完成五种类别的衣服的关键点定位任务。全身所有点的加入使得网络可以学习到其他点的分布、以及点与点之间的关联性,对于CPM这类“脑补”能力极强的网络作用更为明显,所以说全身所有点的加入给网络增加了更多的global信息,使得网络不再关注局部的某一种类别的衣服,从实验结果来看,把裤脚预测成袖口等这类错误预测案例大大减少。

实现细节上就是feed进网络的ground truth的heatmap的channel数是24,也就是五种类别的衣服的所有关键点数,对于不存在的点,gt_heatmap所在的channel全为0。weighted loss即计算loss的时候,每个channel的loss乘上一个系数,存在点系数为1,不存在点为0,使得存在的点参与loss计算与梯度反传,不存在的点loss为0,不产生梯度且不参与梯度反传。实现代码如下:

# 计算每个stage的loss, weighted l2 loss
        for stage in range(self.stages):
            with tf.variable_scope('stage' + str(stage + 1) + '_loss'):
                self.stage_loss_batch = [0] * self.batch_size_np
                for batch in range(self.batch_size_np):
                    self.stage_loss_batch_hmindex = [0] * self.num_joints
                    for hmindex in range(self.num_joints):
                        self.stage_loss_batch_hmindex[hmindex] = tf.nn.l2_loss(self.stage_heatmap[stage][batch,:,:,hmindex] -
                                                      self.gt_heatmap[batch,:,:,hmindex]) * self.train_weights[batch][hmindex]
                    self.stage_loss_batch[batch] = tf.reduce_sum(self.stage_loss_batch_hmindex)
                self.stage_loss[stage] = tf.reduce_sum(self.stage_loss_batch) / self.batch_size

最终,通过train和warm_up_train两个数据集训练的单模型,在复赛A阶段test_a上的NE=5.86%,较之多模型有了很大的提升,正是这个提升也让我们重拾了对CPM的信心,也就有了后面的实验。

Trick 2——FPN(特征金字塔)

正如上一篇博文所说,CPM是没有Hourglass那样的多尺度特性的,初赛阶段我们加入了尺度的增广,通过数据预处理的方式使网络学习多尺度特性,但是这种做法治标不治本,我们需要极大的增加数据量,因此增加了训练时间。还有一种主流的做法就是加入FPN。FPN的思想现在非常流行,很多领域都会用到,例如语义分割开山之作FCN、旷世今年的CVPR姿态估计大作CPN的RefineNet里,都用到了多尺度特征融合的思想。多尺度特征融合就是将不同尺度上的特征融合起来,形成更加全面的特征。我们知道,网络的初始阶段,图片细节信息丰富,网络关注细节信息,像是衣服的花纹、衣服上的纽扣等等;随着网络pooling层的进行,feature map的scale在缩小,这也导致了图片细节特征的流失,这时候的网络关注global一些的信息,例如衣服的轮廓等,而我们要做的是关键点定位,只有网络后端的衣服轮廓的特征会因为细节特征的流失影响精度,因此利用特征金字塔思想,将不同pooling层后的不同scale的feature map叠加起来,为后端的feature map加入细节信息,使得特征图同时具备细节和局部的信息,实现细节就是小scale的feature map上采样和上一级大尺度的feature map相加,新的feature map再上采样和更大尺度的feature map相加,以此类推。实现代码如下:

# FPN block
            C5 = sub_pool4  # channel = 256, size = 32
            C4 = sub_pool3  # channel = 256, size = 64
            C3 = sub_pool2  # channel = 128, size = 128

            C4_conv1 = C4

            # 1x1 conv change channels
            C3_conv1 = tf.layers.conv2d(inputs=C3,
                                        filters=256,
                                        kernel_size=[1, 1],
                                        strides=[1, 1],
                                        padding='same',
                                        kernel_initializer=tf.contrib.layers.xavier_initializer(),
                                        name='C3_conv1')    # channel = 256, size = 256
            P5 = C5     # channel = 256,size = 64

            # P5 2x
            W_P5 = self.create_variables(name="W_P5", shape=[3, 3, 256, 256])
            b_P5 = self.create_variables(name="b_P5", shape=[256])
            P5_2x = self.conv2d_transpose_strided(P5, W_P5, b_P5, tf.shape(C4_conv1))     # channel = 256, size = 64

            # P5 4x
            W_P5_4 = self.create_variables(name="W_P5_4", shape=[3, 3, 256, 256])
            b_P5_4 = self.create_variables(name="b_P5_4", shape=[256])
            P5_4x = self.conv2d_transpose_strided(P5_2x, W_P5_4, b_P5_4, tf.shape(C3_conv1))  # channel = 256, size = 128

            P4 = tf.add(C4_conv1, P5_2x, name="P4")      # channel = 256, size = 64
            # P4_2x
            W_P4 = self.create_variables(name="W_P4", shape=[3, 3, 256, 256])
            b_P4 = self.create_variables(name="b_P4", shape=[256])
            P4_2x = self.conv2d_transpose_strided(P4, W_P4, b_P4, tf.shape(C3_conv1))     # channel = 256, size = 128

            # P3
            P3 = tf.add(C3_conv1, P4_2x, name="P3")

            P_cat = tf.concat([P3, P4_2x, P5_4x], axis=3)       # channel = 256, size = 128

最后的P_cat便是多尺度特征融合的特征图。实验证明,通过train和warm_up_train两个数据集训练的加入FPN的单模型,在复赛A阶段test_a上的NE=5.47%,效果还是很明显的。

Trick 3——On-line Hard Key point Mining

在线难点挖掘这个trick也有很多网络都用到了,例如旷厂的CPN。确实对于一些案例有很多点很困难,比如说小姐姐在奇怪的场景中穿一样奇怪的衣服再摆出奇怪的pose给预测增大了很多难度。判断难点的依据无非是loss的大小。要知道我们在算loss的时候是所有channel的L2 loss相加,也就是所有点的loss都加到一起,这些点的loss有大有小,因此我们在算梯度反传的时候是将这个加和的loss同等大小平均反传给每一个点。换句话说,我们希望大的loss的点有大的梯度,小的loss的点有小的梯度,而加和平均这一操作使得所有点不管loss大小,梯度都是一样的,因此大loss点的梯度不够,不足以抵消偏差,所以loss大的点预测上依然存在问题。因此和CPN一样,我们考虑加入在线难点挖掘。当初加这个trick的时候还没有关注到旷厂的CPN,最后写出来发现和CPN的实现几乎一个意思,很是欣慰。

在线难点挖掘现在主流有hard和soft的两种方法,hard方法就是对loss进行排序,取loss的前几个相加取平均,这样就拉大了loss的平均值,反传的梯度也就加大了;soft方法就是利用softmax等对loss进行一个权值加权,大的loss权值大,小的loss权值小,这样回传的梯度也是大的loss梯度大,小的loss梯度小。我们和CPN一样采用的是hard方法,对loss进行排序,取前一半计算loss参与梯度计算和回传。实现代码如下:

# 计算每个stage的loss, weighted l2 loss, online hard keypoint mining
        for stage in range(self.stages):
            with tf.variable_scope('stage' + str(stage + 1) + '_loss'):
                self.stage_loss_batch = [0] * self.batch_size_np
                for batch in range(self.batch_size_np):
                    self.stage_loss_batch_hmindex = [0] * self.num_joints
                    for hmindex in range(self.num_joints):
                        self.stage_loss_batch_hmindex[hmindex] = tf.nn.l2_loss(self.stage_heatmap[stage][batch, :, :, hmindex] -
                                                      self.gt_heatmap[batch, :, :, hmindex]) * self.loss_weights[batch][hmindex]
                    '''
                        find index of top half loss
                    '''
                    # calculate the num of joints that is trained of each batch
                    num_visible_and_invisible = tf.reduce_sum(self.loss_weights[batch])
                    num_loss = num_invisible_and_visible
                    top_k = num_loss / 2
                    # consider top half of the joints are hard joints
                    max_loss_index_tensor = tf.nn.top_k(self.stage_loss_batch_hmindex, tf.cast(top_k, dtype=tf.int32))[1]
                    max_stage_loss_batch_hmindex = tf.gather(self.stage_loss_batch_hmindex, max_loss_index_tensor)
                    self.stage_loss_batch[batch] = tf.reduce_sum(max_stage_loss_batch_hmindex) / top_k * num_loss

                self.stage_loss[stage] = tf.reduce_sum(self.stage_loss_batch) / self.batch_size

实验证明,通过train和warm_up_train两个数据集训练的单模型,在复赛A阶段test_a上提升了0.1%,效果有,但是作用小,小到我都不敢肯定到底是难点挖掘带来的提升还是fine tune带来的提升。这一点,我也会继续在别的模型上验证,soft方法我也会继续尝试。

Trick 4——Test with augmentation

这一点其实也是惭愧,这个trick其实是就为了提升结果而采取的投机取巧的方法,就是在test的时候也给输入图片加上旋转和尺度上的增广,我是做了9次增广的,加上原图,相当于一张图片在不同旋转和尺度上预测10次,再把预测的heatmap相加,再提坐标。说实在的,其实这就是一个只用一套模型参数的ensemble方法,有点违背我前面提到的不用ensemble、只挑战baseline的初衷,但这种ensemble又确实没有和别的模型融合,也没有多套参数融合,也不算很大地违背吧。在这里提到这个trick也就是想说明这是一个很有效的比赛技巧,但是实际应用肯定不行,因为它降低了test的速度,是以时间为代价来换取精度的。比赛中也有很多其他队伍用了这个小技巧,令我惊讶的是,他们的提升竟然比我小这么多。他们采用这个方法提升大概在0.2%~0.3%左右,我的竟然提升了有0.6%。其实这也从另外一个角度反映了CPM并没有很好地解决FashionAI这个数据集的问题,模型并没有很好的拟合所有情景,一张图片需要通过加入增广来尽可能多地模拟所有情景再ensemble才能得到更高的精度。怎么说呢,欲哭无泪吧。实现起来其实也就多一个数据增广环节,代码如下:

# 读关键点信息
name = valid_set[valid_iter]
# 读图片
img = open_img(name)

# get dress type to determine the extract index
name_split = name.split('/')
dress_type = name_split[1]
if dress_type == 'blouse':
    index = Valid_FLAGS.blouse_index
elif dress_type == 'dress':
    index = Valid_FLAGS.dress_index
elif dress_type == 'outwear':
    index = Valid_FLAGS.outwear_index
elif dress_type == 'skirt':
    index = Valid_FLAGS.skirt_index
else:
    index = Valid_FLAGS.trousers_index

valid_img[0] = img
center_map = make_gaussian(height=512, width=512, sigma=150, center=None)
center_map = np.asarray(center_map)
center_map = center_map.reshape(512, 512, 1)
valid_centermap[0] = center_map

compress_ratio = np.ones((10), dtype=np.float32)
r_angle = np.zeros((10), dtype=np.float32)
cnt = 1
# test augmentation
while cnt < aug_num:
    # img2 = color_augment(img)
    img2, compress_ratio[cnt] = size_augment(img)
    img2, r_angle[cnt] = rotate_augment(img2)
    valid_img[cnt] = img2
    valid_centermap[cnt] = center_map
    cnt += 1

说到在test上加增广,我还用了一个小技巧。我发现一些衣服在图片中面积占比特别小的话,很多点的预测就不是很准,而且heatmap上的高斯点全都叠加到一起去了,在提标签的时候又会进一步造成误差。为了解决这个问题,我根据预测的标签结果计算衣服站图片的比例,如果图片比例小于7%,我就在test增广的时候增大尺度放大区间,让图片有机会放大地更大参与预测。这一个trick又获得了0.02%的微弱提升。

其它Tricks

  • 减小ground truth的heatmap上的高斯点的sigma的值,这个值越小表明,辐射坡度越陡,根据标签坐标打上的高斯点越小。适当减小高斯点的大小可以有效地解决前文提到的衣服占比小时预测的高斯点叠加的问题,进一步提高提标签的精度。

  • 在条件允许的情况下,图片越大越好,大图可以提供更多的信息,但是也增大了计算量。在单卡的情况下用大图可能只能用很小的batch size了。

  • 加入更多的数据集。数据集多了,提供的情景模式多了,当然会提高网络性能。最后我们加入初赛的测试集训练,基本上是一个数据集0.15个点的提升。

  • 这是代码执行效率上的问题:少用for循环,能用内嵌函数的就用函数解决,我是吃了很多时间上的亏了,因为代码执行慢确实浪费了不少时间。

  • 其他理论有效但仍在实验的tricks。

炼丹感悟

  • 第一条感悟就是没卡真心不能活,比赛后期一个人用五张卡依然觉得不够用。深度学习真是门吃硬件资源的学问。

  • 第二条感悟就是熬人,做深度学习真的是要有耐心,尤其越往后做越煎熬,基本上一个trick的实验就算fine tune也得等个一天吧,也许等个一天效果还不一定好,而且效果不好肯定是占大多数的。我不觉得这是徒劳,因为实验的过程中我一方面锻炼了代码能力,另一方面也通过实验感受trick在cpm中的作用。

  • 第三条感悟就是收获颇丰。虽然最后没有进入Top20拿到奖励,但是通过这两个月的学习,我得到了很大的锻炼。第一我也算了解了绝大部分姿态估计、关键点检测中的主流做法,有很多关键trick都通过代码实现在cpm上感受过,理论功底得到了很大程度的提升;另外一方面代码能力也得到了很大的锻炼,比赛后期几乎都是罗老师提方法,我们代码实现,我很享受这个锻炼过程,此外通过彻彻底底地调通一个模型对我今后实现别的模型都具有参考意义。自己科研的任务中写代码也轻松了很多,和别人的科研交流中也开始可以为别人提出建设性意见。总之,很感谢这个过程给我带来的锻炼。

  • 第四条感悟就是任重而道不远。自己一直都想学更多东西,不仅是传统机器学习,还有深度学习,深入学习以后才发现面是在太广,全部掌握完全不可能。我自己又是一个对自己要求特别高的人,别人知道的理论我不知道,我就想去学,别人的代码写的我看都看不懂,我就很难受。自己还有很多要提高的地方,明年这个时候估计就在实习了吧,所以说任重而道不远,要学的还有很多,但是留给自己的时间只有一年了。

  • 最后就是我们确实受CPM模型限制了,确实也只能达到这样的效果了,可能和前排大仙只差一个检测模型的差距,这谁又知道呢~~~后面还有一些没做完的非常fancy的实验,我会接着继续做下去,也会加入检测,也会尝试cpn,hg这样更先进的模型继续探索这一方向,只是不再这么赶这么熬人了。

就写这么多了,本文中存在的问题,欢迎大家指正。欢迎大家和我交流,欢迎这次比赛的大仙们看到了给我提提意见,你们是真的牛,我是服气的。


-------------------------------------------

Youzhi Gu, master student

Foresight Control Center
College of Control Science & Engineering
Zhejiang University
Email: [email protected] ,[email protected]


猜你喜欢

转载自blog.csdn.net/qq_28659831/article/details/80545668
今日推荐