空间金字塔池化SPP(Spatial Pyramid Pooling)

前言

何凯明大神于2014年在《Spatial Pyramid Pooling in Deep Convolutional Networks for Visual Recognition》,这篇paper主要的创新点在于提出了空间金字塔池化。这个方法对于目前很多任务非常实用,尤其是现在的神经网络算法叠加的情况下(即前一个神经网络输出的结果是下一个神经网络的输入),很多输出的图片尺寸或者说bbox大小都是不固定的。下面就简单讲讲这篇文章里面的SPP算法的应用和解决的问题。

① 空间金字塔池化简介

在一般的结构确定的CNN中,通常需要输入固定大小的图片(比如224x224,128x96)来进行训练和测试。因此,对于大小不一的图片,需要经过裁剪,或者缩放等一系列操作,将其变为统一的尺寸。但是这样往往会降低识别检测的精度,于是paper提出了“空间金字塔池化”方法,这个算法的优势在于——使得我们构建的网络,可以输入任意大小的图片,而不需要提前经过裁剪缩放等预处理操作。不仅如此,这个算法用了以后,精度也会有所提高。

空间金字塔池化,又称为“SPP-Net”,记住这个名字,因为在以后的外文文献中,你会经常遇到,特别是物体检测方面的paper。空间金字塔是一个很久之前提出的算法了,空间金字塔在SIFT等经典算法都留下了身影。

我们知道,在对图片进行卷积操作的时候,卷积核的大小是不会发生变化的,反向调节的权重只有卷积核中的各个位置的数值。但是,为什么一般性的CNN要求输入的图片大小是固定尺寸的呢?其实图片尺寸的变化主要是对于全连接层(FC)有影响,因为全连接层后输出的特征维度是跟图片大小紧密相关的。

所以,凯明大神们提出一种新的思路,即在卷积层(包括convolution,Pooling等)和全连接层(FC)之间,加入一种新的网络层——空间金子塔池化层,从而让全连接层得到的特征数目固定。即解决了输入图片尺寸不一的问题。

② 为什么要引入空间金字塔池化?

首先说一下为什么要有这个层:我们处理图片的大小不一,都有自己不同的像素值,但是同一批数据,如果非要进过一定的裁剪把他处理成为相同大小的图像,例如,我们可以先把图片的四个角裁剪下来,在加上一个中心区域的图片,这是五个变形的图片,然后再把图片水平翻转之后依然是相同的操作会得到五个图片,总合计是10个大小相同的图片,这是一种方法,当然还有其他的方法,例如在overFeat那篇论文中也提到一种方法,等等。这些裁剪修补技术(cropping/wraping technique)都会达到不错的结果,但是依然会存在一些问题,例如,有些区域裁剪的时候都会有重复,无形之中加大了该区域的权重。所以,这篇paper就提出了金字塔池化来解决输入图片大小不一的情况。
这里写图片描述

由上图可以看出,经过裁剪或者修补扩大的图像,虽然尺寸变为一样了,但是因为这样的技术使得图片中的某些特征被放大,而导致出现识别问题。

而通过空间金字塔池化,我们可以在不损失图片位置信息,且不对原图片本身作大量特征变化的情况下进行训练,这样最大限度的保留了原始的图片信息,因此在实际应用中,确实提高了一些任务(如object detection等)的精度。

③ 什么是空间金字塔池化?

输入层:一张任意大小的图片,假设其大小为(w,h)。

输出层:21个神经元。

也就是我们输入一张任意大小的特征图的时候,我们希望提取出21个特征。空间金字塔特征提取的过程如下:

这里写图片描述
图片尺度划分

如上图所示,当我们输入一张图片的时候,我们利用不同大小的刻度,对一张图片进行了划分。上面示意图中,利用了三种不同大小的刻度,对输入的特征图进行了划分,最后总共可以得到16+4+1=21个块(bin),我们即将从这21个块中,每个块提取出一个特征,这样刚好就是我们要提取的21维特征向量。这里w和h分别为特征图的宽和高,假设都为16,即经过卷积操作得到输入到SPP的特征图都是16x16的。

现在从左到右来看:

蓝色的图1——我们把一张完整的图片,分成了16个块,也就是每个块的大小就是(w/4,h/4);

绿色的图2,划分了4个块,每个块的大小就是(w/2,h/2);

黑色的图3,把整张图片作为了一个块,也就是块的大小为(w,h)

空间金字塔最大池化的过程,其实就是从这21个图片块中,分别计算每个块的最大值(局部max-pooling)。通过SPP,我们就把一张任意大小的图片转换成了一个固定大小的21维特征(当然你可以设计其它维数的输出,增加金字塔的层数,或者改变划分网格的大小)。上面的三种不同刻度的划分,每一种刻度我们称之为:金字塔的一层,每一个图片块大小我们称之为:windows size了。如果你希望,金字塔的某一层输出n*n个特征,那么你就要用windows size大小为:(w/n,h/n)进行池化了。

当我们有很多层网络的时候,当网络输入的是一张任意大小的图片,这个时候我们可以一直进行卷积、池化,直到网络的倒数几层的时候,也就是我们即将与全连接层连接的时候,就要使用金字塔池化,使得任意大小的特征图都能够转换成固定大小的特征向量,这就是空间金字塔池化的奥义(多尺度特征提取出固定大小的特征向量)。

④ 空间金字塔池化caffe源码

转载自hjimce——深度学习(十九)基于空间金字塔池化的卷积神经网络物体检测

理论学的再多,终归要实践,实践是检验理论的唯一标准,caffe中有关于空间金字塔池化的源码,我这边就直接把它贴出来,以供学习使用,源码来自https://github.com/BVLC/caffe

//1、输入参数pyramid_level:表示金字塔的第几层。我们将对这一层,进行划分为2^n个图片块。金字塔从第0层开始算起,0层就是一整张图片
//第1层就是把图片划分为2*2个块,第2层把图片划分为4*4个块,以此类推……,也就是说我们块的大小就是[w/(2^n),h/(2^n)]
//2、参数bottom_w、bottom_h是我们要输入这一层网络的特征图的大小
//3、参数spp_param是设置我们要进行池化的方法,比如最大池化、均值池化、概率池化……
LayerParameter SPPLayer<Dtype>::GetPoolingParam(const int pyramid_level,
      const int bottom_h, const int bottom_w, const SPPParameter spp_param)
{
  LayerParameter pooling_param;
  int num_bins = pow(2, pyramid_level);//计算可以划分多少个刻度,最后我们图片块的个数就是num_bins*num_bins
   //计算垂直方向上可以划分多少个刻度,不足的用pad补齐。然后我们最后每个图片块的大小就是(kernel_w,kernel_h)
  int kernel_h = ceil(bottom_h / static_cast<double>(num_bins));//向上取整。采用pad补齐,pad的像素都是0
  int remainder_h = kernel_h * num_bins - bottom_h;
  int pad_h = (remainder_h + 1) / 2;//上下两边分摊pad
//计算水平方向的刻度大小,不足的用pad补齐
  int kernel_w = ceil(bottom_w / static_cast<double>(num_bins));
  int remainder_w = kernel_w * num_bins - bottom_w;
  int pad_w = (remainder_w + 1) / 2;


  pooling_param.mutable_pooling_param()->set_pad_h(pad_h);
  pooling_param.mutable_pooling_param()->set_pad_w(pad_w);
  pooling_param.mutable_pooling_param()->set_kernel_h(kernel_h);
  pooling_param.mutable_pooling_param()->set_kernel_w(kernel_w);
  pooling_param.mutable_pooling_param()->set_stride_h(kernel_h);
  pooling_param.mutable_pooling_param()->set_stride_w(kernel_w);

  switch (spp_param.pool()) {
  case SPPParameter_PoolMethod_MAX://窗口最大池化
    pooling_param.mutable_pooling_param()->set_pool(
        PoolingParameter_PoolMethod_MAX);
    break;
  case SPPParameter_PoolMethod_AVE://平均池化
    pooling_param.mutable_pooling_param()->set_pool(
        PoolingParameter_PoolMethod_AVE);
    break;
  case SPPParameter_PoolMethod_STOCHASTIC://随机概率池化
    pooling_param.mutable_pooling_param()->set_pool(
        PoolingParameter_PoolMethod_STOCHASTIC);
    break;
  default:
    LOG(FATAL) << "Unknown pooling method.";
  }

  return pooling_param;
}

template <typename Dtype>
//这个函数是为了获取我们本层网络的输入特征图、输出相关参数,然后设置相关变量,比如输入特征图的图片的大小、个数
void SPPLayer<Dtype>::LayerSetUp(const vector<Blob<Dtype>*>& bottom,
      const vector<Blob<Dtype>*>& top) {
  SPPParameter spp_param = this->layer_param_.spp_param();

  num_ = bottom[0]->num();//batch size 大小
  channels_ = bottom[0]->channels();//特征图个数
  bottom_h_ = bottom[0]->height();//特征图宽高
  bottom_w_ = bottom[0]->width();
  reshaped_first_time_ = false;
  CHECK_GT(bottom_h_, 0) << "Input dimensions cannot be zero.";
  CHECK_GT(bottom_w_, 0) << "Input dimensions cannot be zero.";

  pyramid_height_ = spp_param.pyramid_height();//金子塔有多少层
  split_top_vec_.clear();//清空相关数据
  pooling_bottom_vecs_.clear();
  pooling_layers_.clear();
  pooling_top_vecs_.clear();
  pooling_outputs_.clear();
  flatten_layers_.clear();
  flatten_top_vecs_.clear();
  flatten_outputs_.clear();
  concat_bottom_vec_.clear();
  //如果金字塔只有一层,那么我们其实是对一整张图片进行pooling,也就是文献所提到的:global pooling
  if (pyramid_height_ == 1) {
    // pooling layer setup
    LayerParameter pooling_param = GetPoolingParam(0, bottom_h_, bottom_w_,spp_param);
    pooling_layers_.push_back(shared_ptr<PoolingLayer<Dtype> > (new PoolingLayer<Dtype>(pooling_param)));
    pooling_layers_[0]->SetUp(bottom, top);
    return;
  }
  //这个将用于保存金子塔每一层
  for (int i = 0; i < pyramid_height_; i++) {
    split_top_vec_.push_back(new Blob<Dtype>());
  }

  // split layer setup
  LayerParameter split_param;
  split_layer_.reset(new SplitLayer<Dtype>(split_param));
  split_layer_->SetUp(bottom, split_top_vec_);

  for (int i = 0; i < pyramid_height_; i++) {
    // pooling layer input holders setup
    pooling_bottom_vecs_.push_back(new vector<Blob<Dtype>*>);
    pooling_bottom_vecs_[i]->push_back(split_top_vec_[i]);


    pooling_outputs_.push_back(new Blob<Dtype>());
    pooling_top_vecs_.push_back(new vector<Blob<Dtype>*>);
    pooling_top_vecs_[i]->push_back(pooling_outputs_[i]);

    // 获取金字塔每一层相关参数
    LayerParameter pooling_param = GetPoolingParam(i, bottom_h_, bottom_w_, spp_param);

    pooling_layers_.push_back(shared_ptr<PoolingLayer<Dtype> > (new PoolingLayer<Dtype>(pooling_param)));
    pooling_layers_[i]->SetUp(*pooling_bottom_vecs_[i], *pooling_top_vecs_[i]);

    //每一层金字塔输出向量
    flatten_outputs_.push_back(new Blob<Dtype>());
    flatten_top_vecs_.push_back(new vector<Blob<Dtype>*>);
    flatten_top_vecs_[i]->push_back(flatten_outputs_[i]);

    // flatten layer setup
    LayerParameter flatten_param;
    flatten_layers_.push_back(new FlattenLayer<Dtype>(flatten_param));
    flatten_layers_[i]->SetUp(*pooling_top_vecs_[i], *flatten_top_vecs_[i]);

    // concat layer input holders setup
    concat_bottom_vec_.push_back(flatten_outputs_[i]);
  }

  // 把所有金字塔层的输出,串联成一个特征向量
  LayerParameter concat_param;
  concat_layer_.reset(new ConcatLayer<Dtype>(concat_param));
  concat_layer_->SetUp(concat_bottom_vec_, top);
}

函数GetPoolingParam是我们需要细读的函数,里面设置了金子塔每一层窗口大小的计算,其它的函数就不贴了,对caffe底层实现感兴趣的,可以自己慢慢细读。

参考资料

[1] caffe github
[2] hjimce——深度学习(十九)基于空间金字塔池化的卷积神经网络物体检测
[3] SPP空间金字塔池化(Spatial Pyramid Pooling)

猜你喜欢

转载自blog.csdn.net/g11d111/article/details/80789538