爱笑的小姐姐 · 2021年01月28日

基于CenterFace的模型优化记录

文章转载于:GiantPandaCV
作者:jmc

【GiantPandaCV导语】CenterFace移动端模型优化实验记录

一、序

CenterFace是基于CenterNet的一种AnchorFree人脸检测模型。在widerface上性能虽然没有超过SOTA(Retinaface),但是胜于推理速度较快(不需要NMS),模型结构简单,便于移植部署。

二、背景

模型主要是使用在移动端App中,需要满足:

  1. 上传图片后返回带有人脸检测框的结果图片。
  2. 打开摄像头实时进行检测并拍照,返回检测后的图片。
  3. 需要在室内、室外、白天,晚上场景下均可使用,受众群体密集度比较高,需要支持戴口罩检测,Recall和precision均要求比较高(大于90%)。
  4. 需要支持大小人脸检测。样例如下,难点在于支持小人脸检测以及小模型优化。

image.png

三、CenterFace

本节先简单介绍一下CenterFace模型

  • 模型结构

    CenterFace模型构造比较简单,基础backbone+FPN+head即完成网络构建。1. backbone模型上采用了mobilenetv2作为backbone,mobilenetv3因为多分支以及SE模块,手机端移植不是很友好,实机测试,iphonex上mbv3相比mbv2要慢1ms左右,如果是低端机,这个差距会更大。2. FPN采用的是传统的top-down结构,没有使用PAN。3. Head采用4个conv+bn结构分别输出locationmapscalemapoffsetmappointsmap。整体结构图如下:

image.png

  • 损失函数

    CenterFace的损失函数和CenterNet一样,由FocalLoss形式Location分类损失和L1回归损失构成

image.png

对于location损失,只有每个bbox的中心点为正样本,其余点均为负样本,公式中,。对于offset损失,由于featuremap进行下采样的时候,计算中心点会由于取整产生偏移,需要用l1损失计算这个偏差。对于scale损失,这个是对bbox的w和h进行回归,取log便于计算。对于points损失,计算的是人脸5个关键点到中心点之间的距离的损失,做了normalize处理。最后的损失,就是各个损失的加权之和。

  • 标签生成

  1. locationmap, centerface中最重要的target就是bbox中心点gaussianmap生成,代码,效果图如下:
def _gaussian_radiusv1(self, height, width, min_overlap=0.7):  
        """from cornernet"""  
        a1 = 1  
        b1 = (height + width)  
        c1 = width * height * (1 - min_overlap) / (1 + min_overlap)  
        sq1 = torch.sqrt(b1 ** 2 - 4 * a1 * c1)  
        r1 = (b1 + sq1) / 2  
      
        a2 = 4  
        b2 = 2 * (height + width)  
        c2 = (1 - min_overlap) * width * height  
        sq2 = torch.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 = torch.sqrt(b3 ** 2 - 4 * a3 * c3)  
        r3 = (b3 + sq3) / 2  
        return min(r1, r2, r3)  
      
    def _gaussian2D(self, shape, sigma=1):  
        m, n = [(ss - 1.) / 2. for ss in shape]  
        # y, x = np.ogrid[-m:m+1,-n:n+1]  
        y = torch.arange(-m, m+1, dtype=torch.int).view(-1, 1)  
        x = torch.arange(-n, n+1, dtype=torch.int).view(1, -1)  
      
        h = torch.exp(-(x * x + y * y) / (2 * sigma * sigma))  
        h[h < 1e-5 * h.max()] = 0  
        return h  
      
    def _draw_umich_gaussian(self, heatmap, center, radius, k=1):  
        diameter = 2 * radius + 1  
        # get the gaussian heatmap  
        gaussian = self._gaussian2D((diameter, diameter), sigma=diameter / 6)  
        x, y = int(center[0]), int(center[1])  
        height, width = heatmap.shape[0:2]  
      
        left, right = min(x, radius), min(width - x, radius + 1)  
        top, bottom = min(y, radius), min(height - y, radius + 1)  
      
        masked_heatmap = heatmap[y - top:y + bottom, x - left:x + right]  
        masked_gaussian = gaussian[radius - top:radius +  
                                bottom, radius - left:radius + right]  
        if min(masked_gaussian.shape) > 0 and min(masked_heatmap.shape) > 0:  # TODO debug  
            torch.max(masked_heatmap, masked_gaussian * k, out=masked_heatmap)  
      
        return heatmap  

image.png

  1. offsetmap, 每个bbox中心点上x方向和y方向的偏移结果,输出是一个(BX2XHXW)的map。
  2. scalemap, 每个bbox中心点上w和h的log结果,输出是一个(BX2XHXW)的map。
  3. pointsmap, 每个bbox的中心点到5个关键点x和y方向的距离,输出是一个(BX10XHXW)的map。
  • 模型推理

    CenterFace没有使用NMS作为后处理,而是采用的maxpooling作为代替,代码如下:

loc = loc.unsqueeze(0)    # heatmap  
    hm_max = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)(loc)  
    loc[loc != hm_max] = 0  

四、优化路线

  1. 数据:由于场景任务中存在部分带有口罩的情况,所以采集了2000张百度的带有口罩的数据,混合widerface的train和val的数据来进行训练,测试数据使用业务提供的数据,保持一致。
  2. 优化:流程图如下:

image.png

  • baseline模型为mobilenetv2+fpn模型,测试数据上ap为95%,初始图片训练大小为800x800,测试大小为416x416,FLOPs为1G。
  • 通常来讲,train的size大于测试的时候,效果表现不好。所以调整图片的trainsize,分别为800,640,512,416,最终在416x416测试的情况下,训练size为416的时候效果最好。模型固定为416训练416测试
  • centerface的FPN的最后一层直接进行输出,这里把多层layer进行了concat,ap提升了一个点,但是FLOPs增加,收益不大。
  • 由于centerface没有anchorbbox,bbox回归和中心点损失没有实质的关联,所以bbox的表现不是很好,添加了一个scale\_dis分支,输出(BX4XHXW)的一个featuremap,分别表示的是中心点到上下左右的距离,计算IOU损失,效果提升不明显,还带来了4个通道的featuremap的冗余计算,直接弃用了(有兴趣的同学可以参考FCOS的说明)。
  • 由于FPN最后的输出,经过一个卷积后,要输出多个head,会带来多个卷积计算,所以考虑优化为一个卷积输出,输出的层为(BX(1+2+2+10)xHXW),计算的时候分别对应channel计算损失,减少了10M的FLOPs,推理速度有所提升,同时提升了将近1个点的ap。

image.png

  • 由于业务只要求输出框,对于关键点没有需求,所以构建结构图的时候,把关键点的channel砍掉,可以减少20M的FLOPs,精度保持不变。

image.png

  • 使用mobilenetv2x0.25+fpn作为backbone,使用上面的操作,FLOPs降低为0.2G,ap基本保持不变。
  • 修改训练size为400,推理size也为400,修改FPN的channel从24降低到16,FLOPs降低到了0.131G,精度保持95.2%相比baseline有轻微的提升。
  • 最后对模型进行剪枝,采用SlimmingBN方法,对mobilenetv2中的InvertResdiual模块中的升维层进行剪枝。流程如下:
  • 训练的时候,对bn的gamma进行l1正则,设置为1e-4代码如下:
def updataBN(self):  
        """add a l1 panety for bn channel"""  
        for m in self.model.modules():  
            if isinstance(m, nn.BatchNorm2d):  
                m.weight.grad.data.add_(self.s * torch.sign(m.weight.data))  
  • 剪枝的时候,根据bn总数设置阈值,对weights从小到大进行排序,按比例定位阈值,根据阈值对bn的weights置0,重新计算测试集ap,测试发现卡0.3的时候,精度保持不变,0.4的时候精度下降1个点的ap,需要进行finetune。
  • 保存模型,根据剪枝出来的Config重构模型结构,复制保留下来的权重到对应修改的层上,再次进行推理,ap保持不变即可,需要finetune的话,load权重后进行finetune,这里finetune个人建议是pretrain,finetune的话,可能会由于剪枝导致过检和漏检的情况在训练结束后还存在。
        # only prune the invertedresidual conv and bn  
        if isinstance(m0, InvertedResidual):  
            if len(m0.conv) > 5:  
                for i in range(len(m0.conv)):  
                    if i == 1:  
                     if isinstance(m0.conv[i], nn.BatchNorm2d):  
      
                            idx1 = np.squeeze(np.argwhere(np.asarray(mask.cpu().numpy())))  
                            # first batchnormalize (hidden)  
                            m1.conv[i].weight.data = m0.conv[i].weight.data[idx1].clone()  
                            m1.conv[i].bias.data = m0.conv[i].bias.data[idx1].clone()  
                            m1.conv[i].running_mean = m0.conv[i].running_mean[idx1].clone()  
                            m1.conv[i].running_var = m0.conv[i].running_var[idx1].clone()  
                            # first conv (hidden, inp, 1, 1)  
                            if isinstance(m0.conv[i-1], nn.Conv2d):  
                                w = m0.conv[i-1].weight.data[idx1, :, :, :].clone()  
                             m1.conv[i-1].weight.data = w.clone()  
      
                            # second conv  (hidden, 1, 1, 1)  
                            if isinstance(m0.conv[i+2], nn.Conv2d):  
                                w = m0.conv[i+2].weight.data[idx1, :, :, :].clone()  
                             m1.conv[i+2].weight.data = w.clone()  
      
                            # second bn (hidden)  
                            if isinstance(m0.conv[i+3], nn.BatchNorm2d):  
                                m1.conv[i+3].weight.data = m0.conv[i+3].weight.data[idx1].clone()  
                                m1.conv[i+3].bias.data = m0.conv[i+3].bias.data[idx1].clone()  
                                m1.conv[i+3].running_mean = m0.conv[i+3].running_mean[idx1].clone()  
                             m1.conv[i+3].running_var = m0.conv[i+3].running_var[idx1].clone()  
      
                            # third conv (oup, hidden, 1, 1)  
                            if isinstance(m0.conv[i+5], nn.Conv2d):  
                                w = m0.conv[i+5].weight.data[:, idx1, :, :].clone()  
                             m1.conv[i+5].weight.data = w.clone()  
      
                            # third bn (oup)  
                            if isinstance(m0.conv[i+6], nn.BatchNorm2d):  
                                m1.conv[i+6].weight.data = m0.conv[i+6].weight.data.clone()  
                                m1.conv[i+6].bias.data = m0.conv[i+6].bias.data.clone()  
                                m1.conv[i+6].running_mean = m0.conv[i+6].running_mean.clone()  
                             m1.conv[i+6].running_var = m0.conv[i+6].running_var.clone()  
      
                            layer_id_in_cfg += 1  
                            if layer_id_in_cfg < len(cfg_mask):  
                             mask = cfg_mask[layer_id_in_cfg]  
  • 最后输出的模型为0.116G的FLOPs,相比baseline降低了10倍FLOPs,参数量400k左右,测试集上ap95.2%相比baseline高了0.2%个点。iphonex上测试10ms左右,低端机可以满足10FPS以上的输出,精度,速度均满足要求。

五、简单的思考

  1. 为什么gaussian map是一个3X3的map,而不是全局的map或者说是一个点?
  • 可能是因为cornernet是计算角点的,因为存在偏差所以需要一个小点的map来做约束,centernet直接继承了这个idea,论文中也是直接引用没有过多的思考。
  • 从损失构建上来看,如果增大gaussianmap,对应的增加负样本,会影响中心点的约束。相当于中心点是points anchor,其他的都是非anchor。
  • 从gaussianmap生成来看,对应的3x3可以cover出现offset过大的情况,可以约束范围,毕竟没有bboxanchor。
  1. 如果把bbox全部填满?
  • bbox填满,那么就是每个点都是正样本,框中不存在负样本,这样中心点的价值就不存在了,论文的延伸也就是FCOS。
  1. loss之间的联系?
  • 由于anchorbase的方法是存在海量的positive和negative anchors,回归的好坏,一是会受到anchor的影响,二是会受到classification的影响,所以生成的bbox比较稳定。centernet的各个loss实际上是独立的,只是建立在中心点的基础上,所以导致框会不稳定,尤其是在实时中抖动比较大,OneNet就这个问题给出了解决方案。

预测结果

400x400 移动端上进行测试的结果如下

image.png

结束语

本人才疏学浅,以上都是自己在做项目中的一些方法和实验,以及一些粗浅的思考,并不一定完全正确,只是个人的理解,欢迎大家指正,留言评论。

参考文献

*SlimmingBN(https://openaccess.thecvf.com...\_ICCV\_2017/papers/Liu\_Learning\_Efficient\_Convolutional\_ICCV\_2017\_paper.pdf)

- The End -

推荐阅读

更多嵌入式AI技术相关内容请关注嵌入式AI专栏。
推荐阅读
关注数
18838
内容数
1372
嵌入式端AI,包括AI算法在推理框架Tengine,MNN,NCNN,PaddlePaddle及相关芯片上的实现。欢迎加入微信交流群,微信号:aijishu20(备注:嵌入式)
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息