基于Objects as Points的尝试(二)

作者:xggiou

https://zhuanlan.zhihu.com/p/76378871

本文已由作者授权,未经允许,不得二次转载

前言

这次仍旧待业在家,有时间做自己想做的事情。因为个人喜欢简单高效的东西,所以上一次自己DIY了一把,将shufflenetv2以及yolov3作为backbone,然后接上centernet_detect_head进行了一把尝试。当时在人脸检测上效果还可以接受,但是在voc的多目标检测上基本上算是拉闸。详细的可见:基于Objects as Points检测算法的小小尝试

在这之后想多学点东西,参加了一个分割比赛,中途也没抽出时间去尝试解决为何多目标检测不work的问题。直到大概十来天前忙完,为了解决出现的这个问题,所以接着之前的工作进行了一些尝试性实验,发现了一些端倪,不知道对不对,希望对大家能有所帮助。这里记录下来供参考交流。

以下的实验结论都只是针对本实验项目,欢迎各位批评指正。

项目地址:

https://github.com/xggIoU/centernet_tensorflow_wilderface_voc

正文

实验环境

1.anaconda3、pycharm-community、python3.6、numpy1.14.3
2.tensorflow1.13.1、slim
3.cuda10.0、cudnn7.6.0
4.opencv-python4.1
5.GTX1080ti*1

数据集

仍旧保持上次的数据集:

  • wilderface人脸检测数据集,包含12876张训练集;
  • pascal voc2012目标检测数据集,包含17125张训练集,分20个类别。

由于我只有单卡,为了节约时间快速验证想法,所以训练集与之前一样仍旧没有任何data augmentation!!!

回顾

之前的尝试实验中,对于shufflenet,我采用的是很常见的deconv+add特征融合方法,用于检测人脸,出于显存考虑,所以没用concate操作;对于yolov3,我直接将检测头接到了downsampling_rate=8的特征图上用于voc的检测。网络简易图示如下:(注:这次网络有点小改动,一是把offset预测分支删除,二是将第一个maxpool改成了stride=2的3x3卷积代替)

图1 shufflenetv2_centernet

图2 yolov3_centernet

以前的实验结果: (1)shufflenetv2_centernet+face,收敛,效果还行。

(2)yolov3_centernet+voc,我的实验中难以收敛。

尝试一

从上面的结果分析,很容易想到的是shufflenetv2_centernet检测人脸有效果,那么就用这个网络去检测voc的20个类别。具体过程此处省略M个字。

结果: 仍旧难以收敛,网络结构应该不是主因。

尝试二

后来我想着的是,一个项目无非就是数据+模型+算法。数据是公开数据集,大家都在用,应该没有问题;算法是基于centernet的,官方实现得挺好的,应该问题不大;最后剩下的是模型,这里可以理解为网络结构。对于网络结构,除开shufflenet主干,问题应该出在特征融合阶段。由于先前做了图像分割的比赛,了解过很多分割模型,分割任务本身的属性决定它存在特征融合以及上采用过程,这恰好是centernet需要的。基于此,我尝试了很多种方式,具体过程此处省略N个字。

结果:效果仍旧不好,损失值在0.9~1.4之间不再下降,看来网络结构确定不是主因。

尝试三

对网络结构进行诸多改变失败后,我想了一下,centernet是检测中心关键点,在human pose estimation中对人体关键点检测,常常采用l2_loss。于是仍旧基于shufflenet,将focal loss更换l2_loss进行实验。具体过程此处省略X个字。

结果:有那么点儿效果,但是仍旧不能很好的收敛,还是不可行。说明loss本身没问题,不然之前在face_detection上也不可能凑效。

尝试四

在前面的尝试完之后,已经过去差不多十天了。我重新梳理了下思路,之前说数据虽然没问题,但是有一个过程是处理数据,构建数据标签,我怀疑是在这个环节出了问题。最终我发现从官方的copy过来的heatmap构造存在一些不合理的地方,导致不能很好的work。结论只针对我的实验,毕竟官方代码我没跑过,官方具体怎么做的我也不完全清楚。

官方代码中是由以下两个函数create heatmap的:

def gaussian_radius(det_size, min_overlap=0.7):
    width,height  = det_size
        a1 = 1
        b1 = (height + width)
        c1 = width * height * (1 - min_overlap) / (1 + min_overlap)
        sq1 = np.sqrt(b1 ** 2 - 4 * a1 * c1)
        r1 = (b1 + sq1) / 2

        a2 = 4
        b2 = 2 * (height + width)
        c2 = (1 - min_overlap) * width * height
        sq2 = np.sqrt(b2 ** 2 - 4 * a2 * c2)
        r2 = (b2 + sq2) / 2

        a3 = 4 * min_overlap
        b3 = -2 * min_overlap * (height + width)
        c3 = (min_overlap - 1) * width * height
        sq3 = np.sqrt(b3 ** 2 - 4 * a3 * c3)
        r3 = (b3 + sq3) / 2
        return min(r1, r2, r3)
def draw_msra_gaussian(heatmap, center, sigma):
    tmp_size = sigma * 3
    mu_x = int(center[0])
    mu_y = int(center[1])
    h, w = heatmap.shape[0], heatmap.shape[1]
    ul = [int(mu_x - tmp_size), int(mu_y - tmp_size)]
    br = [int(mu_x + tmp_size + 1), int(mu_y + tmp_size + 1)]
    if ul[0] >= w or ul[1] >= h or br[0] < 0 or br[1] < 0:
        return heatmap
    size = 2 * tmp_size + 1
    x = np.arange(0, size, 1, np.float32)
    y = x[:, np.newaxis]
    x0 = y0 = size // 2
    g = np.exp(- ((x - x0) ** 2 + (y - y0) ** 2) / (2 * sigma ** 2))
    g_x = max(0, -ul[0]), min(br[0], w) - ul[0]
    g_y = max(0, -ul[1]), min(br[1], h) - ul[1]
    img_x = max(0, ul[0]), min(br[0], w)
    img_y = max(0, ul[1]), min(br[1], h)
    heatmap[img_y[0]:img_y[1], img_x[0]:img_x[1]] = np.maximum(
        heatmap[img_y[0]:img_y[1], img_x[0]:img_x[1]],
        g[g_y[0]:g_y[1], g_x[0]:g_x[1]])
    return heatmap

函数的具体实现细节这里不讨论,只需要知道它是先根据目标尺寸算一个自适应高斯核半径,然后再全图构造heatmap。问题就出在:

  • (a)对一个目标,gaussian_radius是相等的。
  • (b)计算的坐标范围是整个feature map。

我觉得应该这样计算:

  • (a)对于一个目标而言,gaussian_radius应该随着目标的宽w和高h的趋势变化。
  • (b)计算的坐标范围应该限于目标box内。

为了说明清楚,我将这两种方式的区别可视化如下:

图3 原图目标以及bbox_gt

图4.1 heatmap_original

图4.2 heatmap_modified

图5.1 heatmap_original_box

图5.2 heatmap_modified_box

图4.1与4.2是对应官方代码构造的heatmap以及我自己改的heatmap;图5.1与5.2是目标bbox对应到featuremap上的位置。它们有以下不同:

*(1)在图5.1中目标的bbox外存在非0值,而图5.2中bbox外都是0;

*(2)图5.1中heatmap不随着目标形状变化而变化,对于不同外形的目标都是一个圆形heatmap,而图5.2中的构造方式就不存在这种问题;

我个人认为:这种差异带来的问题是,前一种构造方式会导致正负样本划分模糊,在计算loss时加权不准确,会迷惑网络的学习。我们可以根据loss的计算来理解,在计算中心点损失时,采用的focal loss如下:

def focal_loss(pred, gt):
  pos_inds = tf.cast(tf.equal(gt,1.0),dtype=tf.float32)
  neg_inds = 1.0-pos_inds
  # neg_inds=tf.cast(tf.greater(gt,0.0),dtype=tf.float32)-pos_inds
  neg_weights = tf.pow(1.0 - gt, 4.0)
  loss = 0.0
  pred=tf.clip_by_value(pred, 1e-6, 1.0 - 1e-6)
  pos_loss = tf.log(pred) * tf.pow(1.0 - pred, 2.0) * pos_inds
  neg_loss = tf.log(1.0 - pred) * tf.pow(pred, 2.0) * neg_weights * neg_inds

  num_pos  = tf.reduce_sum(pos_inds)
  pos_loss = tf.reduce_sum(pos_loss)
  neg_loss = tf.reduce_sum(neg_loss)

  if num_pos == 0.0:
    loss = loss - neg_loss
  else:
    loss = loss - (pos_loss + neg_loss) / num_pos
  return loss

可以看出,除了中心点外,其他都为负样本。但是负样本损失是有权重的,离中心点越近,负样本损失权重越低,反之越高。那么对于bbox外的点,本就是负样本,理论上反映到heatmap上,bbox外的值应该为0,由此可见图5.2的heatmap才是更合理的。否则采用图5.1的heatmap计算时,bbox外的样本损失权重理论上应该为(1-0=1)的,但实际是小于1的值,大概意思就是说本来严格的负样本变得有一定的权重偏向于正样本(大概这么个意思),这与实际不符合。由此可以推断出,起初用于人脸检测为什么能work,我觉得是因为人脸的bbox基本都是接近方形的,这两种create方法出来的heatmap差异不大,反之在voc上目标的形状不规则,所以导致网络学习不work。(由于和官方的各方面配置都不一样,至于官方的实现为什么能work我也不是很明白,这里不做讨论,总之我这样处理,以前训练不收敛的现在能很好的收敛。)

我们将图5.1和图5.2的权重值放大20倍,会更加直观明了,如下所示:

图6.1 heatmap_original_weights x20

图6.2heatmap_modified_weights x20

在foveabox检测算法中,作者采用的是目标中心区域一定的比例为正样本,而这个区域始终在bbox内,意思就是bbox外为严格负样本,而且中心区域也是跟随宽高wh的比例变化的,作者也在论文中提到这种方式对目标的伸缩具有一定的鲁棒性。

在fcos中,落在bbox内的点才是ground truth点,再乘上center-ness分支,这就与我们修改后的heatmap构造非常相似了。

结果

我使用修改过后的heatmap构造方法进行标签制作,将图1所示的shufflenetv2_centernet和图2所示的yolov3_centernet采用voc重新进行了训练。初始学习率统一采用0.00025,采用指数衰减学习率,每2200步衰减0.85,对于shufflenetv2,batch_size=16,epoch=28时开始达到收敛状态;对于yolov3,batch_size=8,epoch=18时开始达到收敛状态。tensorboard loss曲线如下:

图7 shufflenetv2_centernet+voc total_loss收敛曲线

图8 yolov3_centernet+voc total_loss收敛曲线

可视化如下:

图9 shufflenetv2_centernet检测结果

图10 yolov3_centernet检测结果

可以看出检测效果不是很好,尤其是目标挨得比较近时,无论中心点还是尺寸预测都拉稀。而且这里只贴了检测出目标的图供大家看下。在test数据上误检和漏检还很严重!!!

推理速度(从读图开始计时到解析出bbox为止):

运行环境:
    软件环境: python3.6
    硬件环境: gpu:gtx1080ti*1,cpu:intel i7-8700k
model_name              avg_time(ms)    input_size   model_size(.pb)
shufflenetv2_voc        17.4            512x512      24.9MB
yolo3_centernet_voc     25.53           512x512      227.7MB

总结

(1)我不能保证这个方法对所有人所有任务有效,需要结合自己的实验作结论,仅供参考。

(2)模型结构还需要完善,毕竟我功力比大佬们还差不少。

(3)想在val或者test集上取得较好的效果还更多的工作。对于误检和漏检可能是没做data augmentation,毕竟在训练集上能收敛。

(4)其实我觉得可以将centernet的检测方式应用于FPN结构,应该可行。

(5)我的cv之路仍需更多的努力,需要多动手,多实践,坚持到底,才能进步。

最后,对于第(2)点,我在实验中也感受到了更好的特征融合方式,效果会更好,起码收敛速度会更快。比如我参考了旷视的ExFuse分割算法中的semantic_embed_block,将它融入到shuffulenet中去训练voc,起码收敛速度提升了。简易图如下:(仅供参考)

图11 semantic_embed_block

图12 网络结构

这里是根据我自己的理解设计的,跟ExFuse中的连接方式不完全一样的。反正有效就行了。欢迎大家批评指正!

原文发布于微信公众号 - CVer(CVerNews)

原文发表时间:2019-08-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券