前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >我如何用飞桨复现了ICCV 2019 ACNet模型?

我如何用飞桨复现了ICCV 2019 ACNet模型?

作者头像
用户1386409
发布2020-07-10 15:34:08
6890
发布2020-07-10 15:34:08
举报
文章被收录于专栏:PaddlePaddlePaddlePaddle
【飞桨开发者说】尚方信,某一线互联网公司计算机视觉算法开发者

ACNet源于ICCV 2019的一篇文章:

https://arxiv.org/abs/1908.03930v3

其核心思想是通过非对称卷积块增强CNN的核骨架。即使用非对称的卷积核组,替换目前CNN架构中常用的3x3 / 5x5 / 7x7方形卷积核,以支持网络对某些非对称的图像特征实现更有效的特征提取。这样的非对称卷积核组,可以即插即用地与任何卷积神经网络架构结合。

模型结构

此外,这样的非对称卷积核,其卷积结果不受图像水平/垂直翻转的影响。

非对称卷积核

进一步地,所有的非对称卷积核可以合并为一个方形卷积核,因此在部署上线的模型中,多个非对称卷积分支并不会带来更多的运算量。

关于论文的更多介绍,可以阅读论文或查看这篇博客:

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

近日,我们基于飞桨(PaddlePaddle)复现了ACNet,并被飞桨官方模型库收录。下面和大家分享下模型复现的技术细节。

搭建ACNet

ACNet模型结构定义请查看:

https://github.com/PaddlePaddle/models/blob/develop/PaddleCV/image_classification/models/resnet_acnet.py

我们基于飞桨官方实现的ResNet模型结构,实现了ResNet结构+非对称卷积核。

具体来说,是将ResNet中每一个【Conv+BN+Relu】模块,替换为【非对称卷积核组 + BN组 + Relu + Add】的组结构。

不对称卷积核组结构

首先考虑训练模式的模型结构,代码如下:

代码语言:javascript
复制
square_conv = fluid.layers.conv2d(
            input=input,
            num_filters=num_filters,
            filter_size=filter_size,
            stride=stride,
            padding=padding,
            groups=groups,
            act=None,
            param_attr=ParamAttr(name=name + "_acsquare_weights"),
            bias_attr=None,
            name=name + '.acsquare.conv2d.output.1')

飞桨的语法和TensorFlow比较类似,如上的代码片段,即定义了一个卷积层。

具体参数含义如下:

  • Input:传入待卷积处理的张量对象
  • num_filters:卷积核数量(输出的卷积结果的通道数)
  • filter_size:卷积核形状
  • stride:卷积步长
  • padding:卷积时边缘填充像素个数
  • groups:分组卷积的组数量
  • act:激活函数,由于后面还要接BN,这里给None代表不激活
  • param_attr:卷积核参数对象的属性,这里指定对象张量的名称
  • bias_attr:偏置参数对象属性,给None代表不需要bias
  • name:当前卷积层作为一个整体,在运算图中的对象名称

squareconv代表方形对称的卷积核,同理,改变filtersizepaddingstride参数,可以定义水平/垂直两个方向的非对称卷积核(1x3, 3x1等)。

代码语言:javascript
复制
# 垂直方向,vertical
ver_conv = fluid.layers.conv2d(
            input=input,
            num_filters=num_filters,
            filter_size=(filter_size, 1),
            stride=stride,
            padding=(padding, 0),
            groups=groups,
            act=None,
            param_attr=ParamAttr(name=name + "_acver_weights"),
            bias_attr=False,
            name=name + '.acver.conv2d.output.1')

# 水平方向,horizontal
hor_conv = fluid.layers.conv2d(
            input=input,
            num_filters=num_filters,
            filter_size=(1, filter_size),
            stride=stride,
            padding=(0, padding),
            groups=groups,
            act=None,
            param_attr=ParamAttr(name=name + "_achor_weights"),
            bias_attr=False,
            name=name + '.achor.conv2d.output.1')

接着可以定义BN和Relu结构,调用batch_norm API定义批归一化层,对方形卷积核的输出进行批归一化。

代码语言:javascript
复制
square_bn = fluid.layers.batch_norm(
            input=square_conv,
            act=None,
            name=bn_name + '.acsquare.output.1',
            param_attr=ParamAttr(name=bn_name + '_acsquare_scale'),
            bias_attr=ParamAttr(bn_name + '_acsquare_offset'),
            moving_mean_name=bn_name + '_acsquare_mean',
            moving_variance_name=bn_name + '_acsquare_variance', )

同理,可以定义不对称卷积核输出结果的批归一化层。

代码语言:javascript
复制
# 垂直方向批归一化
ver_bn = fluid.layers.batch_norm(
                input=ver_conv,
                act=None,
                name=bn_name + '.acver.output.1',
                param_attr=ParamAttr(name=bn_name + '_acver_scale'),
                bias_attr=ParamAttr(bn_name + '_acver_offset'),
                moving_mean_name=bn_name + '_acver_mean',
                moving_variance_name=bn_name + '_acver_variance', )

# 水平方向批归一化
hor_bn = fluid.layers.batch_norm(
                input=hor_conv,
                act=None,
                name=bn_name + '.achor.output.1',
                param_attr=ParamAttr(name=bn_name + '_achor_scale'),
                bias_attr=ParamAttr(bn_name + '_achor_offset'),
                moving_mean_name=bn_name + '_achor_mean',
                moving_variance_name=bn_name + '_achor_variance', )

最后,将三个批归一化结果张量相加,并进行ReLU激活。

代码语言:javascript
复制
output = fluid.layers.elementwise_add(
                x=square_bn, y=ver_bn + hor_bn, act="relu")
return output

如上,就完成了ResNet核心的非对称卷积 + BN + ReLU模块conv_bn_layer_ac()的定义。

由于ResNet中存在1x1卷积,不存在相应的不对称卷积核结构,因此1x1 conv + BN + ReLU结构被保持原状,未被修改为ACNet结构。

代码语言:javascript
复制
def conv_bn_layer(self, **kwargs):
    """
    conv_bn_layer
    """
    if kwargs['filter_size'] == 1:
        # 原始卷积BN
        return self.conv_bn_layer_ori(**kwargs)
    else:
        # 不对称卷积BN
        return self.conv_bn_layer_ac(**kwargs)

部署模型(权值聚合)

权值聚合方案

上文中描述的ACNet结构(训练模式,下同)将每一个卷积拆分成了3个卷积的卷积组,显然,这样的结构成倍增加了运算量。

事实上,我们可以对多个卷积+BN的分支进行聚合处理,产生一个方形卷积核,其卷积输出与原先多个卷积+BN的输出一致(部署模式,下同),这样的聚合结果,可以从数学上证明,是与训练模式的运算过程等价的。

因此,模型部署工作需要分为两步,即定义部署模式的模型运算图结构和权值聚合。

部署形式的模型结构定义

训练模式和部署模式的ACNet均包含方形卷积核,但训练模式的方形卷积核不包含偏置项。

在部署模式下,根据推导,多个卷积分支的BN层bias偏置项将被融合为一,并作为方形卷积核的偏置项被纳入运算过程中。除此以外,训练模式下所有的BN层和不对称卷积层,在部署模式的运算图中均不被定义。(以上讨论均不涉及1x1卷积)

因此,模型运算图定义可被修改如下。当模型为部署模式时,模型类对象的deploy属性为True,将影响所有self.deploy相关的逻辑。

代码语言:javascript
复制
square_conv = fluid.layers.conv2d(
            input=input,
            num_filters=num_filters,
            filter_size=filter_size,
            stride=stride,
            padding=padding,
            groups=groups,
            act=act if self.deploy else None,
            param_attr=ParamAttr(name=name + "_acsquare_weights"),
            bias_attr=ParamAttr(name=name + "_acsquare_bias") if self.deploy else None,
            name=name + '.acsquare.conv2d.output.1')

if self.deploy:
    return square_conv
else:
    ver_conv = ...
    hor_conv = ...
    square_bn = ...
    ver_bn = ...
    hor_bn = ...

    return fluid.layers.elementwise_add(
                x=square_bn, y=ver_bn + hor_bn, act=act)

权值聚合

根据飞桨官方文档介绍,我们可以使用Numpy操作飞桨模型权值,即将运算图中特定权值张量读取为Numpy数组,或将Numpy数组赋值给运算图中特定权值张量。

按照如上公式和图5,我们需要读取每一个卷积模块的卷积核权值、BN层scale,offset,running_mean和running_var,每一个卷积分支包含5个参数,我们的运算图中共3个卷积分支(方形、垂直、水平),因此共需要读取15个参数。

可以通过如下方式获取批归一化层的参数。

代码语言:javascript
复制
def get_ac_tensor(name):
    gamma = fluid.global_scope().find_var(name + '_scale').get_tensor()
    beta = fluid.global_scope().find_var(name + '_offset').get_tensor()
    mean = fluid.global_scope().find_var(name + '_mean').get_tensor()
    var = fluid.global_scope().find_var(name + '_variance').get_tensor()
    return gamma, beta, mean, var

我们使用张量名称来获取具体的张量对象,需要与模型结构定义时的张量名称相对应。接着,我们获取一个卷积模块的15个参数。

代码语言:javascript
复制
def get_kernel_bn_tensors(name):
    if "conv1" in name:
        bn_name = "bn_" + name
    else:
        bn_name = "bn" + name[3:]
    # 获取方形卷积核
    ac_square = fluid.global_scope().find_var(name +
                                              "_acsquare_weights").get_tensor()
    # 获取垂直卷积核
    ac_ver = fluid.global_scope().find_var(name + "_acver_weights").get_tensor()
    # 获取水平卷积核
    ac_hor = fluid.global_scope().find_var(name + "_achor_weights").get_tensor()

    # 方形卷积结果归一化参数
    ac_square_bn_gamma, ac_square_bn_beta, ac_square_bn_mean, ac_square_bn_var = \
            get_ac_tensor(bn_name + '_acsquare')
    # 垂直卷积结果归一化参数
    ac_ver_bn_gamma, ac_ver_bn_beta, ac_ver_bn_mean, ac_ver_bn_var = \
            get_ac_tensor(bn_name + '_acver')
    # 水平卷积结果归一化参数
    ac_hor_bn_gamma, ac_hor_bn_beta, ac_hor_bn_mean, ac_hor_bn_var = \
            get_ac_tensor(bn_name + '_achor')

    kernels = [np.array(ac_square), np.array(ac_ver), np.array(ac_hor)]
    gammas = [
        np.array(ac_square_bn_gamma), np.array(ac_ver_bn_gamma),
        np.array(ac_hor_bn_gamma)
    ]
    betas = [
        np.array(ac_square_bn_beta), np.array(ac_ver_bn_beta),
        np.array(ac_hor_bn_beta)
    ]
    means = [
        np.array(ac_square_bn_mean), np.array(ac_ver_bn_mean),
        np.array(ac_hor_bn_mean)
    ]
    var = [
        np.array(ac_square_bn_var), np.array(ac_ver_bn_var),
        np.array(ac_hor_bn_var)
    ]

    return {"kernels": kernels, "bn": (gammas, betas, means, var)}

按照公式,基于Numpy进行权值聚合运算。

代码语言:javascript
复制
def kernel_fusion(kernels, gammas, betas, means, var):
    """fuse conv + BN"""
    kernel_size_h, kernel_size_w = kernels[0].shape[2:]

    square = (gammas[0] / (var[0] + 1e-5)
              **0.5).reshape(-1, 1, 1, 1) * kernels[0]
    ver = (gammas[1] / (var[1] + 1e-5)**0.5).reshape(-1, 1, 1, 1) * kernels[1]
    hor = (gammas[2] / (var[2] + 1e-5)**0.5).reshape(-1, 1, 1, 1) * kernels[2]

    b = 0
    for i in range(3):
        b += -((means[i] * gammas[i]) / (var[i] + 1e-5)**0.5) + betas[i]  # eq.7
    square[:, :, :, kernel_size_w // 2:kernel_size_w // 2 + 1] += ver
    square[:, :, kernel_size_h // 2:kernel_size_h // 2 + 1, :] += hor

    return square, b

完整权值聚合运算代码可参考链接:

https://github.com/PaddlePaddle/models/blob/develop/PaddleCV/image_classification/utils/acnet/weights_aggregator.py

复现效果

我们的复现方案基于ResNet50在ImageNet上进行了验证,可以观察到精度所有提升,且聚合后的运算量与原版ResNet50结构接近。

如在使用过程中有问题,可加入飞桨官方QQ群交流:1108045677

如果您想详细了解更多飞轮的相关内容,请参见以下文档。

官网地址:

https://www.paddlepaddle.org.cn

飞轮开源框架项目地址:

的GitHub:

https://github.com/PaddlePaddle/Paddle

Gitee:

https://gitee.com/paddlepaddle/Paddle

END

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-07-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 PaddlePaddle 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ACNet模型结构定义请查看:
  • https://github.com/PaddlePaddle/models/blob/develop/PaddleCV/image_classification/models/resnet_acnet.py
  • 部署形式的模型结构定义
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档