自然语言处理知识图谱

一文搞懂池化层!Pooling详解(网络下采样篇)

2022-07-11  本文已影响0人  晓柒NLP与药物设计

池化(Pooling)是卷积神经网络中的一个重要的概念,它实际上是一种形式的降采样。有多种不同形式的非线性池化函数,池化层会不断地减小数据的空间大小,因此参数的数量和计算量也会下降,这在一定程度上也控制了过拟合。通常来说,CNN的卷积层之间都会周期性地插入池化层

一. 池化的目的及作用

池化层大大降低了网络模型参数和计算成本,也在一定程度上降低了网络过拟合的风险。概括来说,池化层主要有以下五点作用:

对于池化操作,大量常用的是Max_Pooling和Average_Pooling,但实际上卷积神经网络的池化方法还有很多,下文将对业界目前所出现的一些池化方法进行归纳总结:

二. 池化函数分类详解

1. Max Pooling(最大池化)

最大池化(Max Pooling)是将输入的矩阵划分为若干个矩形区域,对每个子区域输出最大值,其定义如下:
y_{k i j}=\max _{(p, q) \in \mathcal{R}_{i j}} x_{k p q} \tag{1}
其中,y_{k i j}表示与第k个特征图有关的在矩形区域\mathcal{R}_{i j}的最大池化输出值,x_{k p q}表示矩形区域\mathcal{R}_{i j}中位于(p,q)处的元素

对于最大池化操作,只选择每个矩形区域中的最大值进入下一层,而其他元素将不会进入下一层。所以最大池化提取特征图中响应最强烈的部分进入下一层,这种方式摒弃了网络中大量的冗余信息,使得网络更容易被优化。同时这种操作方式也常常丢失了一些特征图中的细节信息,所以最大池化更多保留些图像的纹理信息

# Torch 实现
torch.nn.MaxPool1d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)
2. Average Pooling(平均池化)

平均池化(Average Pooling)是将输入的图像划分为若干个矩形区域,对每个子区域输出所有元素的平均值,其定义如下:
y_{k i j}=\frac{1}{\left|\mathcal{R}_{i j}\right|} \sum_{(p, q) \in \mathcal{R}_{i j}} x_{k p q} \tag{2}
其中,y_{k i j}表示与第k个特征图有关的在矩形区域\mathcal{R}_{i j}的平均池化输出值,x_{k p q}表示矩形区域\mathcal{R}_{i j}中位于(p,q)处的元素,|\mathcal{R}_{i j}|表示矩形区域\mathcal{R}_{i j}中元素个数

平均池化取每个矩形区域中的平均值,可以提取特征图中所有特征的信息进入下一层,而不像最大池化只保留值最大的特征,所以平均池化可以更多保留些图像的背景信息

torch.nn.AvgPool1d(kernel_size, stride=None, padding=0, ceil_mode=False, count_include_pad=True)
3. Global Average Pooling(全局平均池化)

在卷积神经网络训练初期,卷积层通过池化层后一般要接多个全连接层进行降维,最后再Softmax分类,这种做法使得全连接层参数很多,降低了网络训练速度,且容易出现过拟合的情况。在这种背景下,M Lin等人提出使用全局平均池化Global Average Pooling来取代最后的全连接层。用很小的计算代价实现了降维,更重要的是GAP极大减少了网络参数(CNN网络中全连接层占据了很大的参数)。

全局平均池化是一种特殊的平均池化,只不过它不划分若干矩形区域,而是将整个特征图中所有的元素取平均输出到下一层。其定义如下:
y_{k}=\frac{1}{\left|\mathcal{R}\right|} \sum_{(p, q) \in \mathcal{R}} x_{k p q} \tag{3}
其中,y_k表示与第k个特征图的全局平均池化输出值,x_{k p q}表示第k个特征图区域\mathcal{R}中位于(p,q)处的元素,|\mathcal{R}|表示第k个特征图全部元素的个数

作为全连接层的替代操作,GAP对整个网络在结构上做正则化防止过拟合,直接剔除了全连接层中黑箱的特征,直接赋予了每个channel实际的类别意义。除此之外,使用GAP代替全连接层,可以实现任意图像大小的输入,而GAP对整个特征图求平均值,也可以用来提取全局上下文信息,全局信息作为指导进一步增强网络性能

class GlobalAvgPool1d(nn.Module):
    def __init__(self):
        super(GlobalAvgPool1d,self).__init__()
    def forward(self, x):
        return nn.AvgPool1d(x,kernel_size=x.shape[2])
4. Mix Pooling(混合池化)

在模型训练期间随机采用了最大池化和平均池化方法,并在一定程度上有助于防止网络过拟合现象,其定义如下:
y_{k i j}=\lambda \cdot \max _{(p, q) \in \mathcal{R}_{i j}} x_{k p q}+(1-\lambda) \cdot \frac{1}{\left|\mathcal{R}_{i j}\right|} \sum_{(p, q) \in \mathcal{R}_{i j}} x_{k p q} \tag{4}
其中\lambda是0或1的随机值,表示选择使用最大池化或平均池化,换句话说,混合池化以随机方式改变了池调节的规则,这将在一定程度上解决最大池和平均池所遇到的问题

混合池化优于传统的最大池化和平均池化方法,并可以解决过拟合问题来提高分类精度。此外该方法所需要的计算开销可忽略不计,而无需任何超参数进行调整,可被广泛运用于CNN

将AvgPool1d与MaxPool1d加权求和即可
5. Stochastic Pooling(随机池化)

随机池化是Zeiler等人于ICLR2013提出的一种池化操作,随机池化的计算过程如下:

随机池化只需对特征图中的元素按照其概率值大小随机选择,即元素值大的被选中的概率也大,而不像max-pooling那样,永远只取那个最大值元素,这使得随机池化具有更强的泛化能力

class StochasticPool2DLayer(nn.Module):
    def __init__(self, pool_size=2, maxpool=True, training=False, grid_size=None, **kwargs):
        super(StochasticPool2DLayer, self).__init__(**kwargs)
        self.pool_size = pool_size
        self.maxpool_flag = maxpool
        self.training = training
        if grid_size:
            self.grid_size = grid_size
        else:
            self.grid_size = pool_size
        self.Maxpool = torch.nn.MaxPool2d(kernel_size=self.pool_size, stride=1)
        self.Avgpool = torch.nn.AvgPool2d(kernel_size=self.pool_size, stride=self.pool_size, padding=self.pool_size//2,)
        self.padding = nn.ConstantPad2d((0,1,0,1),0)
    def forward(self, x, training=False, **kwargs):
        if self.maxpool_flag:
            x = self.Maxpool(x)
            x = self.padding(x)
        if not self.training:
            x = self.Avgpool(x)
            return x # [:, :, ::self.pool_size, ::self.pool_size]       
        else:
            w, h = x.data.shape[2:]
            n_w, n_h = w//self.grid_size, h//self.grid_size
            n_sample_per_grid = self.grid_size//self.pool_size
            idx_w = []
            idx_h = []
            if w>2 and h>2:
                for i in range(n_w):
                    offset = self.grid_size * i
                    if i < n_w - 1:
                        this_n = self.grid_size
                    else:
                        this_n = x.data.shape[2] - offset
                    
                    this_idx, _ = torch.sort(torch.randperm(this_n)[:n_sample_per_grid])
                    idx_w.append(offset + this_idx)
                for i in range(n_h):
                    offset = self.grid_size * i
                    if i < n_h - 1:
                        this_n = self.grid_size
                    else:
                        this_n = x.data.shape[3] - offset
                    this_idx, _ = torch.sort(torch.randperm(this_n)[:n_sample_per_grid])
                    idx_h.append(offset + this_idx)
                idx_w = torch.cat(idx_w, dim=0)
                idx_h = torch.cat(idx_h, dim=0)
            else:
                idx_w = torch.LongTensor([0])
                idx_h = torch.LongTensor([0])
            output = x[:, :, idx_w.cuda()][:, :, :, idx_h.cuda()]
            return output
6. Power Average Pooling(幂平均池化)

幂平均池化是基于平均池化和最大池化的结合,利用一个学习参数p来确定这两种方法的相对重要性;当p=1时,使用局部求和,当p \rightarrow \infty时,使用最大池化,其定义如下:
\tilde{\mathbf{a}}=\sqrt[p]{\sum_{i \in \mathbf{R}} \mathbf{a}_{i}^{p}} \tag{5}
其中\mathbf{R}表示待池化区域中的像素值集

torch.nn.LPPool1d(norm_type, kernel_size, stride=None, ceil_mode=False)
7. Detail-Preserving Pooling(DPP池化)

为了降低隐藏层的规模或数量,大多数CNN都会采用池化方式来减少参数数量,来改善某些失真的不变性并增加感受野的大小。由于池化本质上是一个有损的过程,所以每个这样的层都必须保留对网络可判别性最重要的部分进行激活。但普通的池化操作只是在特征图区域内进行简单的平均或最大池化来进行下采样过程,这对网络的精度有比较大的影响。基于以上几点,Faraz Saeedan等人提出一种自适应的池化方法-DPP池化,该池化可以放大空间变化并保留重要的图像结构细节,且其内部的参数可通过反向传播加以学习。DPP池化主要受Detail-Preserving Image Downscaling的启发。

O[p]=\frac{1}{k_{p}} \sum_{q \in \Omega_{p}} I[q] \cdot\|I[q]-\tilde{I}[p]\|^{\lambda} \tag{6}

\tilde{I}=I_{D} * \frac{1}{16}\left[\begin{array}{lll} 1 & 2 & 1 \end{array}\right]^{T}\left[\begin{array}{lll} 1 & 2 & 1 \end{array}\right] \tag{7}

DPP池化允许缩减规模以专注于重要的结构细节,可学习的参数控制着细节的保存量,此外,由于细节保存和规范化相互补充,DPP可以与随机合并方法结合使用,以进一步提高准确率

class DetailPooling(nn.Module):
    def __init__(self, tensor_size, asymmetric=False, lite=True,
                 *args, **kwargs):
        super(DetailPooling, self).__init__()
        self._lambda = nn.Parameter(torch.Tensor(1))
        self._lambda.data.mul_(0).add_(.6)
        self._alpha = nn.Parameter(torch.Tensor(1))
        self._alpha.data.mul_(0).add_(.1)
        self.asymmetric = asymmetric
        self.lite = lite
        if self.lite:
            self.weight = torch.FloatTensor([[[[1, 2, 1]]]])
            self.weight = self.weight.expand((tensor_size[1], 1, 1, 3))
        else:
            self.weight = nn.Parameter(torch.rand(*(tensor_size[1], 1, 3, 3)))
            self.weight = nn.init.xavier_normal_(self.weight, gain=0.01)
        self.tensor_size = tensor_size[:2] + \
            F.avg_pool2d(torch.rand(1, 1, tensor_size[2],
                                    tensor_size[3]), (2, 2)).size()[2:]
    def forward(self, tensor):
        self._alpha.data.pow_(2).pow_(.5)
        self._lambda.data.pow_(2).pow_(.5)
        padded_tensor = F.pad(tensor, (1, 1, 1, 1), mode="replicate")
        if self.lite:
            if tensor.is_cuda and not self.weight.is_cuda:
                self.weight = self.weight.cuda()
            equation2 = F.conv2d(F.conv2d(padded_tensor, self.weight, groups=tensor.size(1)), self.weight.transpose(2, 3), groups=tensor.size(1)).div(16)
        else:
            equation2 = F.conv2d(padded_tensor, self.weight, groups=tensor.size(1))
        eps = 1e-6
        if self.asymmetric:
            equation56 = equation2.mul(-1).add(tensor).clamp(0).pow(2)
            equation56 = equation56.add(eps**2).pow(2).pow(self._lambda)
        else:
            equation56 = equation2.mul(-1).add(tensor).pow(2).add(eps**2)
            equation56 = equation56.pow(2).pow(self._lambda)
        equation4 = equation56.add(self._alpha)
        equation7 = equation4.div(F.avg_pool2d(F.pad(equation4, (0, 1, 0, 1), mode="replicate"), (2, 2), (1, 1)).add(1e-8))
        equation8 = F.avg_pool2d(tensor.mul(equation7), (2, 2))
        return equation8
8. Local Importance Pooling(局部重要性池化)

CNN通常使用空间下采样层来缩小特征图,以实现更大的接受场和更少的内存消耗,但对于某些任务而言,这些层可能由于不合适的池化策略而丢失一些重要细节,最终损失模型精度。为此,作者从局部重要性的角度提出了局部重要性池化,通过基于输入学习自适应重要性权重,LIP可以在下采样过程中自动增加特征判别功能

池化操作可归纳为如下公式:
O_{x^{\prime}, y^{\prime}}=\frac{\sum_{(\Delta x, \Delta y) \in \Omega} F(I)_{x+\Delta x, y+\Delta y} I_{x+\Delta x, y+\Delta y}}{\sum_{(\Delta x, \Delta y) \in \Omega} F(I)_{x+\Delta x, y+\Delta y}} \tag{12}
其中F的大小和特征I一致,代表每个点的重要性。Local Aggregation and Normalization框架如下图所示:

图中分别对应了平均池化,最大池化和步长为2的卷积。首先最大池化对应的最大值不一定是最具区分力的特征,并且在梯度更新中也难以更新到最具区分力的特征,除非最大值被抑制掉。而步长为2的卷积问题主要在于固定的采样位置。因此,合适的池化操作应该包含两点:

  1. 下采样的位置要尽可能非固定间隔
  2. 重要性的函数F需通过学习获得

LIP首先在原特征图上学习一个类似于注意力的特征图,然后再和原特征图进行加权求均值,公式可表述如下:
O_{x^{\prime}, y^{\prime}}=\frac{\sum_{(\Delta x, \Delta y) \in \Omega} I_{x+\Delta x, y+\Delta y} \exp (\mathcal{G}(I))_{x+\Delta x, y+\Delta y}}{\sum_{(\Delta x, \Delta y) \in \Omega} \exp (\mathcal{G}(I))_{x+\Delta x, y+\Delta y}} \tag{13}
Local Importance Pooling可以学习自适应和可判别性的特征图以汇总下采样特征,同时丢弃无信息特征。这种池化机制能极大保留物体大部分细节,对于一些细节信息异常丰富的任务至关重要

def lip2d(x, logit, kernel size=3, stride=2, padding=1):
    weight = torch.exp(logit)
    return F.avg pool2d(x∗weight , kernel size, stride, padding)/F.avg pool2d(
                        weight, kernel size, stride, padding)
9. Soft Pooling(软池化)

现有的一些池化方法大都基于最大池化和平均池化的不同组合,而软池化****是基于softmax加权的方法来保留输入的基本属性,同时放大更大强度的特征激活。与maxpooling不同,softpool是可微的,所以网络在反向传播过程中为每个输入获得一个梯度,这有利于提高训练效果。

SoftPool的计算流程如下:

  1. 特征图透过滑动视窗来框选局部数值
  2. 框选的局部数值会先经过指数计算,计算出的值为对应的特征数值的权重
  3. 将各自的特征数值与其相对应的权重相乘
  4. 最后进行加总

这样的方式让整体的局部数值都有所贡献,重要的特征占有较高的权重。比Max pooling、Average pooling能够保留更多信息

SoftPool的数学定义如下:

作为一种新颖地池化方法,SoftPool可以在保持池化层功能的同时尽可能减少池化过程中带来的信息损失,更好地保留信息特征并因此改善CNN中的分类性能。大量的实验结果表明该算法的性能优于原始的Avg池化与Max池化。随着神经网络的设计变得越来越困难,而通过NAS等方法也几乎不能大幅度提升算法的性能,为了打破这个瓶颈,从基础的网络层优化入手,不失为一种可靠有效的精度提升手段

class SOFTPOOL1d(Function):
    def forward(ctx, input, kernel=2, stride=None):
        no_batch = False
        if len(input.size()) == 2:
            no_batch = True
            input.unsqueeze_(0)
        B, C, D = input.size()
        kernel = _single(kernel)
        if stride is None:
            stride = kernel
        else:
            stride = _single(stride)
        oD = (D-kernel[0]) // stride[0] + 1
        output = input.new_zeros((B, C, oD))
        softpool_cuda.forward_1d(input.contiguous(), kernel, stride, output)
        ctx.save_for_backward(input)
        ctx.kernel = kernel
        ctx.stride = stride
        if no_batch:
            return output.squeeze_(0)
        return output
    def backward(ctx, grad_output):
        grad_input = torch.zeros_like(ctx.saved_tensors[0])
        saved = [grad_output.contiguous()] + list(ctx.saved_tensors) + [ctx.kernel, ctx.stride] + [grad_input]
        softpool_cuda.backward_1d(*saved)
        saved[-1][torch.isnan(saved[-1])] = 0
        return saved[-1], None, None
上一篇 下一篇

猜你喜欢

热点阅读