激活函数发展的新里程——EvoNorms

激活函数发展的新里程——EvoNorms


  之所以把这个函数称为激活函数发展的新里程,我的理由就是给人们提供了一种新视角,甚至说打破了之前的固有思维。神经网络为了更加容易训练,提出了Normalization,这种思路取得的成功,使得现在的神经网络越来越离不开归一化。之前写过一篇关于 Normalization的文章,是站在框架的角度去写的,有兴趣可以阅读一下,也便于对本文理解深刻。
  随着网络的发展,归一化通常和激活函数都是在一起出现的,都伴随着卷积而出现。所以也就难免很多人把归一化和激活放在一起讲,但是那么多归一化和那么多激活,并不是所有的效果都好,也基本上就产生了,大家所常见的几种范式,比如BN-ReLU,GN-ReLU,BN-Swish等。既然联系已经这么紧密了,能否通过一个操作,一次搞定呢。或者对于财大气粗的谷歌,这种传统的计算范式就真的好么,人家可以利用各种组合,把组合出来的这么多结果搜索找出最好的。
  搜索的过程也具有很好的借鉴意义,感兴趣的同学建议阅读原文。本文直接讨论这个组合算法的特点。
  回顾以前的归一化方法,BN是基于批统计量来计算样本的均值和方差,而像是LayerNorm,则就独立于批统计量,同时还有GroupNorm,则是弱化了批统计量的概念。而在实现中BN因为有批统计量的需求,所以需要使用 指数滑动均值来计算统计量,并保存。
  谷歌在算法中也搜索了两类计算,一种是需要使用批统计量的,这一类算法被称为EvoNorm-B系列。一种是独立于批统计量的,这一类算法称为EvoNorms-S系列。而两个系列中评分最高,总体效果最好的,则分别就是EvoNorm-B0和EvoNorm-S0。这就是本文的主角。

EvoNorm-B0

  需要使用批统计量,则基本上就是对标BN的,但是这个算法还有激活函数的功能,一个BN不够打,再加一个ReLU。因为搜索的时候,是基于计算图替换的,下面给出EvoNorm-B0的计算图。这个计算图保证了输入和输出的维度不变,因为归一化层+激活层的特点也是不改变输入维度,只是进行函数映射。
EvoNorm-B0的计算图
  图中节点所表示的意思,从图中轻易就能获取,初始节点包括输入,输出,以及两个沿通道维度的可训练节点,分别被被初始化为0和1,称为 v 0 v_0 v 1 v_1 。未使用的节点表示这个过程中未采用的节点,但是在搜索过程中,使用了遗传算法的变体,所以这些节点可以用于突变。
  计算图不够直观,这里看看表达式,这个表达式一个就干了正则化和激活两件事。
在这里插入图片描述
  这个时候,再来看看BN-ReLU的组合表达式,其实发现功能类似,参与运算的元素(计算图中的节点)也是类似的。
在这里插入图片描述
  反观EvoNorm-B0,很大的一个特点就是不单单使用一种方差,而是使用两种方差的混合作为分母,前者就是BatchNorm中使用的方差,后者则是InstanceNorm使用的方差,显然这个计算更加复杂了。但是效果好啊,你问为什么要这要搞,搜索出来的啊。但是也有解释,那就是前者方差可以在batch中捕获数据的全局信息,而后者则很好捕获每张图像的局部信息。然后在分母上取了最大,这样提供了非线性,连激活函数都省了。
  实验结果表明,在ImageNet上,在目标检测和分割任务中,甚至在生成任务中,EvoNorm-B0都是相对于BN-ReLU更好的。

pytorch实现

class EvoNormB0_2d(nn.Module):
    def __init__(self, num_features, nonlinearity=True, affine=True, momentum=0.9, eps=1e-5, training=True):
        super(EvoNormB0_2d, self).__init__()
        self.nonlinearity = nonlinearity
        self.training = training
        self.momentum = momentum
        self.eps = eps
        self.num_features = num_features
        self.affine = affine

        if self.affine:
            self.gamma = nn.Parameter(torch.ones(1, self.num_features, 1, 1))
            self.beta = nn.Parameter(torch.zeros(1, self.num_features, 1, 1))
            if self.nonlinearity:
                self.v = nn.Parameter(torch.ones(1, self.num_features, 1 ,1))
        else:
            self.register_parameter('gamma', None)
            self.register_parameter('beta', None)
            self.register_buffer('v', None)

        self.register_buffer('running_var', torch.ones(1, self.num_features, 1, 1))
        self.reset_parameters()

    def reset_parameters(self):
        self.running_var.fill_(1)

    def instance_std(self, x):
        var = torch.var(x, dim=(2,3), keepdim=True).expand_as(x)
        if torch.isnan(var).any():
            var = torch.zeros(var.shape)
        return torch.sqrt(var + self.eps)

    def forward(self, x):
        if x.dim() != 4:
            raise ValueError('expected 4D input (got {}D input)'.format(x.dim()))

        if self.training:
            var = torch.var(x, (0, 2, 3), keepdim=True)
            self.running_var.mul_(self.momentum)
            self.running_var.add_((1-self.momentum) * var)

        else:
            var = self.running_var

        if self.nonlinearity:
            den = torch.max((var+self.eps).sqrt(), self.v * x + self.instance_std(x))
            return x/den * self.gamma + self.beta

        else:
            return x * self.gamma + self.beta

EvoNorms-S

  搜索过程中使用的有些计算是独立于批统计量的,所组成的节点称之位EvoNorms-S系列,其中综合效果最好的是EvoNorms-S0。这一系列主要是对标GN-ReLU这种不需要使用指数滑动均值来计算批统计量的算法,计算图如下。这些内容也就不赘述了,有需要可以看Normalization的文章。
 EvoNorms-S0计算图
  图中所示的含义和上面提到的是一个意思。直接看S0的表达式,下面表达式中 σ ( x ) \sigma(x) 代表sigmoid函数,所以分子部分其实就是一个很类似于swish函数的表达式,而分母部分,恰好就是GroupNorm的表达式。下面同样看看GN-Swish的表达式(省略了 γ , β \gamma,\beta )。

EvoNorms-S0表达式
GN-Swish的表达式
  其实两者的表达式还是有差别的,而且一个巨大的差别就是EvoNorms-S0中的x没有经过减均值除方差这个标准化,而GN里是有这个操作的。同样再看看GN-ReLU的表达式。
GN-ReLU的表达式
  最终的实验结果也是做了很多实验室,然后EvoNorms-S0全都要比GN-ReLU,和GN-Swish要好。

pytorch实现

class EvoNormS0_2d(nn.Module):
    __constants__ = ['num_features', 'eps', 'nonlinearity']

    def __init__(self, num_features, nonlinearity=True, eps=1e-5, groups=32):
        super(EvoNormS0_2d, self).__init__()

        self.num_features = num_features
        self.eps = eps
        self.nonlinearity = nonlinearity
        self.groups = groups

        self.gamma = nn.Parameter(torch.ones(1, num_features, 1, 1))
        self.beta = nn.Parameter(torch.zeros(1, num_features, 1, 1))
        if self.nonlinearity:
            self.v = nn.Parameter(torch.ones(1, num_features, 1, 1))

    def group_std(self, x):
        N, C, H, W = x.shape
        x = torch.reshape(x, (N, self.groups, C//self.groups, H, W))
        std = torch.std(x, (2, 3, 4), keepdim=True).expand_as(x)
        return torch.reshape(std + self.eps, (N, C, H, W))

    def forward(self, x):
        if x.dim() != 4:
            raise ValueError('expected 4D input (got {}D input)'.format(x.dim()))
        if self.nonlinearity:
            num = x*torch.sigmoid(self.v * x)
            return num / self.group_std(x) * self.gamma + self.beta
        else:
            return x * self.gamma + self.beta

  从激活函数和归一化的角度,这篇文章大致就是这些内容了,但是从实验性来讲,这种神经网络子结构的搜索算法,以及谷歌操作流程具有很多可借鉴的地方。这些内容建议大家自行阅读。

系列文章:

神经网络中的激活函数总述
sigmoid激活函数
tanh激活函数
ReLU系列激活函数
maxout激活函数
Swish激活函数
激活函数发展的新里程——EvoNorms

猜你喜欢

转载自blog.csdn.net/m0_38065572/article/details/106231577
今日推荐