精华内容
下载资源
问答
  • 卷积层

    2020-12-11 16:28:48
    与普通神经网络相比较,卷积神经网络不同的地方在于 其包含了一个由卷积层和池化层构成的特征提取器。通常 把卷积神经网络中的卷积层叫作 C 层(特征提取层),卷 积层的输入来源于输入层或者池化层,在卷积层中,每个...

    与普通神经网络相比较,卷积神经网络不同的地方在于 其包含了一个由卷积层和池化层构成的特征提取器。通常 把卷积神经网络中的卷积层叫作 C 层(特征提取层),卷 积层的输入来源于输入层或者池化层,在卷积层中,每个神 经元与上一层的输入(特征图)中的神经元仅仅只是部分连 接的。

    ​ 通常对卷积神经网络的任何一个卷积层来说,其里面 均包含若干个由一些矩形排列的神经元组成的特征平面,拥 有同一特征平面的神经元权值共享,而卷积核就是这里所说 的共享的权值。一般用随机小数矩阵的形式初始化卷积核,卷积核在网络的训练过程中将学习得到合理的权值。相比 普通神经网络,权值共享带来的直接好处是减少了网络中各 层之间的连接,从而使得权值数量变得更少,大大减小了计 算量,同时又降低了过拟合的风险。

    ​ 卷积层中的每一个特征 图都有一个与之相对应的卷积核,并且与卷积核的大小相 同,卷积层的每一个特征图是通过不同的卷积核在前一层输 入的特征图上作卷积,然后将对应元素累加之后再加一个偏 置,最后通过激活函数所获得到得。假设现在第 l 层的卷积 层,那么该卷积层中第 j 个特征图的计算公式如式(1)所示。

    在这里插入图片描述

    式中:xl j表示当前第 l 层的第 j 个输出,Mj表示先从第 l-1 层 的特征图里,选择若干个组成第 l 输入特征图的集合,xl-1 i 表 示第 l-1 层的第 i 个特征图,kl ij表示第 l 层的卷积核,bl j 表示 第 l 层的第 j 个偏置,f 表示激活函数。

    展开全文
  • 本文来自于云社区,本文章主要介绍了转置卷积层和正卷积层的关系和区别是什么呢,转置卷积层实现过程又是什么样的呢,希望读后会给您带来帮助。在CNN提出之前,我们所提到的人工神经网络应该多数情况下都是前馈神经...
  • Caffe源代码之卷积层和反卷积层

    千次阅读 2018-08-13 10:47:01
    Caffe源代码之卷积层和反卷积层 标签[空格] : Caffe源代码 Caffe源代码之卷积层和反卷积层 举例 Conv_layer头文件 Conv_layer源文件 BaseConvolutionLayer头文件 BaseConvolutionLayer源文件 im2col col2im ...

    Caffe源代码之卷积层和反卷积层

    标签[空格] : Caffe源代码

    卷积层是深度学习核心中的核心,反卷积网络则是广泛应用于图像分割领域。在Caffe中,卷积和反卷积是颠倒的,卷积层的前向传播与反卷积层的反向传播原理是一样的;反卷积层的反向传播和卷积层的前向传播是一样的。下面,我们还是一个例子说明,卷积层前向传播和反向传播的原理。

    举例

    有这样一个卷积层:
    输入: 32×512×14×14

    输出:32×1024×7×7

    卷积层相关的参数:
    kernel = 3
    stride = 2
    padding = 1
    dilition = 1
    group = 2

    这样,可以计算出网络权重的大小为:weight 1024×256×3×3

    下面大概介绍一下在Caffe中,前向传播中,计算的一个步骤:

    (1)给权重分配内存空间(1024,256,3,3),显而易见对于group = 2 的一个卷积来说,每一组的卷积其权重大小为(512,256,3,3)

    (2)通过im2col变换,将输入的四维数据变成方便与卷积计算的二维矩阵,这样输入数据由(512,14,14) 变成(51233,77),怎样变换呢?按照卷积的操作以(14,14)为例,卷积核在特征图上每一层滑动,就会覆盖33的一个区域,转换一列(33,1),因此对于一个特征图(14,14)来说,将会产生(33,77)的一个二维矩阵!对于每一个特征图,在下面进行堆积,最终产生(51233,77)

    (3)此外,对于(1024,256,3,3)权重来说,在C++也可以理解成是一个(1024,25633)的一个二维矩阵,当然如果考虑group的因素则进一步可以拆分为2个(512,25633)的矩阵

    (4)最终将上面转换得来group个输入二维矩阵和其对应的权值二维矩阵相乘,也就是(512,25633)2563377相乘,注意这样的乘法有group个

    以上就是Caffe中,卷积层前向进行的运算!

    通过源码,我们可以了解到输入图像怎么经过im2col转换成方便计算的矩阵的、反向传播过程中怎么经过col2im计算输入图像的梯度的包括权值的梯度、Caffe如何处理dilition的!

    Conv_layer头文件

    #ifndef CAFFE_CONV_LAYER_HPP_
    #define CAFFE_CONV_LAYER_HPP_
    
    #include <vector>
    
    #include "caffe/blob.hpp"
    #include "caffe/layer.hpp"
    #include "caffe/proto/caffe.pb.h"
    
    #include "caffe/layers/base_conv_layer.hpp"
    
    namespace caffe {
    //ConvolutionLayer是BaseConvolutionLayer继承类,关于BaseConvolutionLayer后面再说
    template <typename Dtype>
    class ConvolutionLayer : public BaseConvolutionLayer<Dtype> {
     public:
        explicit ConvolutionLayer(const LayerParameter& param)
          : BaseConvolutionLayer<Dtype>(param) {}
    
      virtual inline const char* type() const { return "Convolution"; }
    
     protected:
      virtual void Forward_cpu(const vector<Blob<Dtype>*>& bottom,
          const vector<Blob<Dtype>*>& top);
      virtual void Forward_gpu(const vector<Blob<Dtype>*>& bottom,
          const vector<Blob<Dtype>*>& top);
      virtual void Backward_cpu(const vector<Blob<Dtype>*>& top,
          const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom);
      virtual void Backward_gpu(const vector<Blob<Dtype>*>& top,
          const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom);
      virtual inline bool reverse_dimensions() { return false; }
      //计算输出特征图的大小
      virtual void compute_output_shape();
    };
    
    }  // namespace caffe
    
    #endif  // CAFFE_CONV_LAYER_HPP_

    在头文件中,没有有实质性的东西,只是有一个compute_output_shape函数的声明,接下来我们看看其源文件!

    Conv_layer源文件

    #include <vector>
    
    #include "caffe/layers/conv_layer.hpp"
    
    namespace caffe {
    
    template <typename Dtype>
    void ConvolutionLayer<Dtype>::compute_output_shape() {
    //获取卷积核大小的数据(只是卷积核大小,并非权重,后面一样)
      const int* kernel_shape_data = this->kernel_shape_.cpu_data();
      //获取stride
      const int* stride_data = this->stride_.cpu_data();
      //padding
      const int* pad_data = this->pad_.cpu_data();
      //dilation
      const int* dilation_data = this->dilation_.cpu_data();
      //dilation
      this->output_shape_.clear();
      //大循环,对于输入特征图 32 * 512 * 14 * 14,后面两个维度,属于特征图的空间维度
      for (int i = 0; i < this->num_spatial_axes_; ++i) {
        // i + 1 to skip channel axis
    
        const int input_dim = this->input_shape(i + 1);
        //扩展的卷积核大小,由于有dilation的存在,相当于将卷积核扩大到
        // kernel = dilation * (kernel - 1) + 1
        const int kernel_extent = dilation_data[i] * (kernel_shape_data[i] - 1) + 1;
        //输出特征图大小 = (input_size + 2 * pad - kernel)/ stride + 1
        //向下取整
        const int output_dim = (input_dim + 2 * pad_data[i] - kernel_extent)
            / stride_data[i] + 1;
        this->output_shape_.push_back(output_dim);
      }
    }
    
    template <typename Dtype>
    void ConvolutionLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
          const vector<Blob<Dtype>*>& top) {
          //获取权重指针
      const Dtype* weight = this->blobs_[0]->cpu_data();
      for (int i = 0; i < bottom.size(); ++i) {//大循环,对于所有输入卷积
        const Dtype* bottom_data = bottom[i]->cpu_data();
        Dtype* top_data = top[i]->mutable_cpu_data();
        for (int n = 0; n < this->num_; ++n) {//第一维度,32不参与卷积
        //forward_cpu_gemm不在Conv_Layer中定义
        //那就在BaseConvLayer中定义
        //参数: 输入, 权重, 输出
          this->forward_cpu_gemm(bottom_data + n * this->bottom_dim_, weight,
              top_data + n * this->top_dim_);
          if (this->bias_term_) {
          //以及计算偏置值
            const Dtype* bias = this->blobs_[1]->cpu_data();
            this->forward_cpu_bias(top_data + n * this->top_dim_, bias);
          }
        }
      }
    }
    
    //反向传播
    //在卷基层中,反向传播,需要对bottom求梯度,也有对weight求梯度
    template <typename Dtype>
    void ConvolutionLayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,
          const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom) {
          //获取权重指针,不可修改
      const Dtype* weight = this->blobs_[0]->cpu_data();
      //获取权重指针,可修改
      Dtype* weight_diff = this->blobs_[0]->mutable_cpu_diff();
      for (int i = 0; i < top.size(); ++i) {//大循环,对每一个输出求,都要反向传播
        const Dtype* top_diff = top[i]->cpu_diff();//输出的梯度
        const Dtype* bottom_data = bottom[i]->cpu_data();//输入
        Dtype* bottom_diff = bottom[i]->mutable_cpu_diff();//输入的梯度,可修改
        // Bias gradient, if necessary.首先对偏置值求取梯度
        if (this->bias_term_ && this->param_propagate_down_[1]) {
          Dtype* bias_diff = this->blobs_[1]->mutable_cpu_diff();
          for (int n = 0; n < this->num_; ++n) {//第一维度32
            this->backward_cpu_bias(bias_diff, top_diff + n * this->top_dim_);
          }
        }
        //param_propagate_down_表示权重是否求梯度
        //propagate_down表示bottom是否求梯度
        if (this->param_propagate_down_[0] || propagate_down[i]) {
        //权重求梯度
          for (int n = 0; n < this->num_; ++n) {
            // gradient w.r.t. weight. Note that we will accumulate diffs.
            if (this->param_propagate_down_[0]) {
            //权重求梯度
              this->weight_cpu_gemm(bottom_data + n * this->bottom_dim_,
                  top_diff + n * this->top_dim_, weight_diff);
            }
            // gradient w.r.t. bottom data, if necessary.
            if (propagate_down[i]) {
            //bottom求梯度
              this->backward_cpu_gemm(top_diff + n * this->top_dim_, weight,
                  bottom_diff + n * this->bottom_dim_);
            }
          }
        }
      }
    }
    
    #ifdef CPU_ONLY
    STUB_GPU(ConvolutionLayer);
    #endif
    
    INSTANTIATE_CLASS(ConvolutionLayer);
    
    }  // namespace caffe

    conv_layer在实质上并没有说明,卷积层是怎么做的,只是有这样的一个流程!
    说一些与卷积实现无关的题外话,卷积层的反向传播可以看出,我们在训练网络的过程中,bottom和weight是否反向传播是相互独立的,在某些应用中,我们可以固定某一层权值不进行求梯度,但梯度依然也可以向后传播!
    通过这样的设置,我们能够更加灵活的fine-tuning!

    在conv_layer类的前向和反向运算中,分别调用forward_cpu_gemm, weight_cpu_gemm, backward_cpu_gemm显然是在BaseConvolutionLayer类中

    BaseConvolutionLayer头文件

    #ifndef CAFFE_BASE_CONVOLUTION_LAYER_HPP_
    #define CAFFE_BASE_CONVOLUTION_LAYER_HPP_
    
    #include <vector>
    
    #include "caffe/blob.hpp"
    #include "caffe/layer.hpp"
    #include "caffe/proto/caffe.pb.h"
    #include "caffe/util/im2col.hpp"
    
    namespace caffe {
    
    template <typename Dtype>
    class BaseConvolutionLayer : public Layer<Dtype> {
     public:
      explicit BaseConvolutionLayer(const LayerParameter& param)
          : Layer<Dtype>(param) {}
      virtual void LayerSetUp(const vector<Blob<Dtype>*>& bottom,
          const vector<Blob<Dtype>*>& top);
      virtual void Reshape(const vector<Blob<Dtype>*>& bottom,
          const vector<Blob<Dtype>*>& top);
    
      virtual inline int MinBottomBlobs() const { return 1; }
      virtual inline int MinTopBlobs() const { return 1; }
      virtual inline bool EqualNumBottomTopBlobs() const { return true; }
    
     protected:
     //以下这些函数在conv_layer中被调用
      void forward_cpu_gemm(const Dtype* input, const Dtype* weights,
          Dtype* output, bool skip_im2col = false);
      void forward_cpu_bias(Dtype* output, const Dtype* bias);
      void backward_cpu_gemm(const Dtype* input, const Dtype* weights,
          Dtype* output);
      void weight_cpu_gemm(const Dtype* input, const Dtype* output, Dtype*
          weights);
      void backward_cpu_bias(Dtype* bias, const Dtype* input);
    
    #ifndef CPU_ONLY
      void forward_gpu_gemm(const Dtype* col_input, const Dtype* weights,
          Dtype* output, bool skip_im2col = false);
      void forward_gpu_bias(Dtype* output, const Dtype* bias);
      void backward_gpu_gemm(const Dtype* input, const Dtype* weights,
          Dtype* col_output);
      void weight_gpu_gemm(const Dtype* col_input, const Dtype* output, Dtype*
          weights);
      void backward_gpu_bias(Dtype* bias, const Dtype* input);
    #endif
    
      /// @brief The spatial dimensions of the input.
      //获取输入特征图的大小
      //channel_axis_指定哪一个维度是通道的维度,后面的都是特征图的大小
      inline int input_shape(int i) {
        return (*bottom_shape_)[channel_axis_ + i];
      }
    //reverse_dimensions函数是否翻转输入输出
    //在卷积层中,为false
    //在反卷积层中,为true
    //这也进一步说明,卷积反卷积前后向传播完全相反
      virtual bool reverse_dimensions() = 0;
      // Compute height_out_ and width_out_ from other parameters.
      virtual void compute_output_shape() = 0;
    //以下参数,可从其命名知道其含义,更多的含义在源文件中注释
      /// @brief The spatial dimensions of a filter kernel.
      Blob<int> kernel_shape_;
      /// @brief The spatial dimensions of the stride.
      Blob<int> stride_;
      /// @brief The spatial dimensions of the padding.
      Blob<int> pad_;
      /// @brief The spatial dimensions of the dilation.
      Blob<int> dilation_;
      /// @brief The spatial dimensions of the convolution input.
      Blob<int> conv_input_shape_;
      /// @brief The spatial dimensions of the col_buffer.
      vector<int> col_buffer_shape_; //im2col的后矩阵的大小
      /// @brief The spatial dimensions of the output.
      vector<int> output_shape_;
      const vector<int>* bottom_shape_;
    
      int num_spatial_axes_;
      int bottom_dim_;
      int top_dim_;
    
      int channel_axis_;
      int num_;
      int channels_;
      int group_;
      int out_spatial_dim_;
      int weight_offset_;
      int num_output_;
      bool bias_term_;
      bool is_1x1_;
      bool force_nd_im2col_;
    
     private:
     //im2col以及col2im
      inline void conv_im2col_cpu(const Dtype* data, Dtype* col_buff) {
        if (!force_nd_im2col_ && num_spatial_axes_ == 2) {//如果特征图是二维的情况
          im2col_cpu(data, conv_in_channels_,
              conv_input_shape_.cpu_data()[1], conv_input_shape_.cpu_data()[2],
              kernel_shape_.cpu_data()[0], kernel_shape_.cpu_data()[1],
              pad_.cpu_data()[0], pad_.cpu_data()[1],
              stride_.cpu_data()[0], stride_.cpu_data()[1],
              dilation_.cpu_data()[0], dilation_.cpu_data()[1], col_buff);
        } else {//如果特征图是多维的情况
          im2col_nd_cpu(data, num_spatial_axes_, conv_input_shape_.cpu_data(),
              col_buffer_shape_.data(), kernel_shape_.cpu_data(),
              pad_.cpu_data(), stride_.cpu_data(), dilation_.cpu_data(), col_buff);
        }
      }
      inline void conv_col2im_cpu(const Dtype* col_buff, Dtype* data) {
        if (!force_nd_im2col_ && num_spatial_axes_ == 2) {
          col2im_cpu(col_buff, conv_in_channels_,
              conv_input_shape_.cpu_data()[1], conv_input_shape_.cpu_data()[2],
              kernel_shape_.cpu_data()[0], kernel_shape_.cpu_data()[1],
              pad_.cpu_data()[0], pad_.cpu_data()[1],
              stride_.cpu_data()[0], stride_.cpu_data()[1],
              dilation_.cpu_data()[0], dilation_.cpu_data()[1], data);
        } else {
          col2im_nd_cpu(col_buff, num_spatial_axes_, conv_input_shape_.cpu_data(),
              col_buffer_shape_.data(), kernel_shape_.cpu_data(),
              pad_.cpu_data(), stride_.cpu_data(), dilation_.cpu_data(), data);
        }
      }
    #ifndef CPU_ONLY
      inline void conv_im2col_gpu(const Dtype* data, Dtype* col_buff) {
        if (!force_nd_im2col_ && num_spatial_axes_ == 2) {
          im2col_gpu(data, conv_in_channels_,
              conv_input_shape_.cpu_data()[1], conv_input_shape_.cpu_data()[2],
              kernel_shape_.cpu_data()[0], kernel_shape_.cpu_data()[1],
              pad_.cpu_data()[0], pad_.cpu_data()[1],
              stride_.cpu_data()[0], stride_.cpu_data()[1],
              dilation_.cpu_data()[0], dilation_.cpu_data()[1], col_buff);
        } else {
          im2col_nd_gpu(data, num_spatial_axes_, num_kernels_im2col_,
              conv_input_shape_.gpu_data(), col_buffer_.gpu_shape(),
              kernel_shape_.gpu_data(), pad_.gpu_data(),
              stride_.gpu_data(), dilation_.gpu_data(), col_buff);
        }
      }
      inline void conv_col2im_gpu(const Dtype* col_buff, Dtype* data) {
        if (!force_nd_im2col_ && num_spatial_axes_ == 2) {
          col2im_gpu(col_buff, conv_in_channels_,
              conv_input_shape_.cpu_data()[1], conv_input_shape_.cpu_data()[2],
              kernel_shape_.cpu_data()[0], kernel_shape_.cpu_data()[1],
              pad_.cpu_data()[0], pad_.cpu_data()[1],
              stride_.cpu_data()[0], stride_.cpu_data()[1],
              dilation_.cpu_data()[0], dilation_.cpu_data()[1], data);
        } else {
          col2im_nd_gpu(col_buff, num_spatial_axes_, num_kernels_col2im_,
              conv_input_shape_.gpu_data(), col_buffer_.gpu_shape(),
              kernel_shape_.gpu_data(), pad_.gpu_data(), stride_.gpu_data(),
              dilation_.gpu_data(), data);
        }
      }
    #endif
    
      int num_kernels_im2col_;
      int num_kernels_col2im_;
      int conv_out_channels_;
      int conv_in_channels_;
      int conv_out_spatial_dim_;
      int kernel_dim_;
      int col_offset_;
      int output_offset_;
    
      Blob<Dtype> col_buffer_;
      Blob<Dtype> bias_multiplier_;
    };
    
    }  // namespace caffe
    
    #endif  // CAFFE_BASE_CONVOLUTION_LAYER_HPP_
    

    BaseConvolutionLayer源文件

    #include <algorithm>
    #include <vector>
    
    #include "caffe/filler.hpp"
    #include "caffe/layers/base_conv_layer.hpp"
    #include "caffe/util/im2col.hpp"
    #include "caffe/util/math_functions.hpp"
    
    namespace caffe {
    
    template <typename Dtype>
    void BaseConvolutionLayer<Dtype>::LayerSetUp(const vector<Blob<Dtype>*>& bottom,
          const vector<Blob<Dtype>*>& top) {
      // Configure the kernel size, padding, stride, and inputs.
      ConvolutionParameter conv_param = this->layer_param_.convolution_param();
      force_nd_im2col_ = conv_param.force_nd_im2col();
      //channel_axis_通道位于的维度上,1
      channel_axis_ = bottom[0]->CanonicalAxisIndex(conv_param.axis());
      //通道之后的维度都认为是特征图的空间维度,2
      const int first_spatial_axis = channel_axis_ + 1;
      //输入的数据的维度,4
      const int num_axes = bottom[0]->num_axes();
      //特征图的空间有多少维度,2
      num_spatial_axes_ = num_axes - first_spatial_axis;
      CHECK_GE(num_spatial_axes_, 0);
      //卷积核的维度至少是以为的
      vector<int> spatial_dim_blob_shape(1, std::max(num_spatial_axes_, 1));
      // Setup filter kernel dimensions (kernel_shape_).
      //kernel_shape_卷积核形状信息,卷积核可以是多维的,也就是可以实现三维卷积
      kernel_shape_.Reshape(spatial_dim_blob_shape);
      int* kernel_shape_data = kernel_shape_.mutable_cpu_data();
      //如果在prototxt中定义了has_kernel_w,则强制为2维卷积
      //后面的几个if语句都是为了处理,二维卷积和多维卷积在prototxt定义的一些冲突
      //后面stride_ padding等都和kernel的处理方式一样
      //这样做的目的是,可以兼容多维卷积
      if (conv_param.has_kernel_h() || conv_param.has_kernel_w()) {
        CHECK_EQ(num_spatial_axes_, 2)
            << "kernel_h & kernel_w can only be used for 2D convolution.";
        CHECK_EQ(0, conv_param.kernel_size_size())
            << "Either kernel_size or kernel_h/w should be specified; not both.";
        kernel_shape_data[0] = conv_param.kernel_h();
        kernel_shape_data[1] = conv_param.kernel_w();
      } else {
        const int num_kernel_dims = conv_param.kernel_size_size();
        CHECK(num_kernel_dims == 1 || num_kernel_dims == num_spatial_axes_)
            << "kernel_size must be specified once, or once per spatial dimension "
            << "(kernel_size specified " << num_kernel_dims << " times; "
            << num_spatial_axes_ << " spatial dims).";
            //循环,获取每一卷积维度上卷积核的大小
          for (int i = 0; i < num_spatial_axes_; ++i) {
            kernel_shape_data[i] =
                conv_param.kernel_size((num_kernel_dims == 1) ? 0 : i);
          }
      }
      for (int i = 0; i < num_spatial_axes_; ++i) {
        CHECK_GT(kernel_shape_data[i], 0) << "Filter dimensions must be nonzero.";
      }
      // Setup stride dimensions (stride_).
      stride_.Reshape(spatial_dim_blob_shape);
      int* stride_data = stride_.mutable_cpu_data();
      if (conv_param.has_stride_h() || conv_param.has_stride_w()) {
        CHECK_EQ(num_spatial_axes_, 2)
            << "stride_h & stride_w can only be used for 2D convolution.";
        CHECK_EQ(0, conv_param.stride_size())
            << "Either stride or stride_h/w should be specified; not both.";
        stride_data[0] = conv_param.stride_h();
        stride_data[1] = conv_param.stride_w();
      } else {
        const int num_stride_dims = conv_param.stride_size();
        CHECK(num_stride_dims == 0 || num_stride_dims == 1 ||
              num_stride_dims == num_spatial_axes_)
            << "stride must be specified once, or once per spatial dimension "
            << "(stride specified " << num_stride_dims << " times; "
            << num_spatial_axes_ << " spatial dims).";
        const int kDefaultStride = 1;
        for (int i = 0; i < num_spatial_axes_; ++i) {
          stride_data[i] = (num_stride_dims == 0) ? kDefaultStride :
              conv_param.stride((num_stride_dims == 1) ? 0 : i);
          CHECK_GT(stride_data[i], 0) << "Stride dimensions must be nonzero.";
        }
      }
      // Setup pad dimensions (pad_).
      pad_.Reshape(spatial_dim_blob_shape);
      int* pad_data = pad_.mutable_cpu_data();
      if (conv_param.has_pad_h() || conv_param.has_pad_w()) {
        CHECK_EQ(num_spatial_axes_, 2)
            << "pad_h & pad_w can only be used for 2D convolution.";
        CHECK_EQ(0, conv_param.pad_size())
            << "Either pad or pad_h/w should be specified; not both.";
        pad_data[0] = conv_param.pad_h();
        pad_data[1] = conv_param.pad_w();
      } else {
        const int num_pad_dims = conv_param.pad_size();
        CHECK(num_pad_dims == 0 || num_pad_dims == 1 ||
              num_pad_dims == num_spatial_axes_)
            << "pad must be specified once, or once per spatial dimension "
            << "(pad specified " << num_pad_dims << " times; "
            << num_spatial_axes_ << " spatial dims).";
        const int kDefaultPad = 0;
        for (int i = 0; i < num_spatial_axes_; ++i) {
          pad_data[i] = (num_pad_dims == 0) ? kDefaultPad :
              conv_param.pad((num_pad_dims == 1) ? 0 : i);
        }
      }
      // Setup dilation dimensions (dilation_).
      dilation_.Reshape(spatial_dim_blob_shape);
      int* dilation_data = dilation_.mutable_cpu_data();
      const int num_dilation_dims = conv_param.dilation_size();
      CHECK(num_dilation_dims == 0 || num_dilation_dims == 1 ||
            num_dilation_dims == num_spatial_axes_)
          << "dilation must be specified once, or once per spatial dimension "
          << "(dilation specified " << num_dilation_dims << " times; "
          << num_spatial_axes_ << " spatial dims).";
      const int kDefaultDilation = 1;
      for (int i = 0; i < num_spatial_axes_; ++i) {
        dilation_data[i] = (num_dilation_dims == 0) ? kDefaultDilation :
                           conv_param.dilation((num_dilation_dims == 1) ? 0 : i);
      }
      // Special case: im2col is the identity for 1x1 convolution with stride 1
      // and no padding, so flag for skipping the buffer and transformation.
      //确定是否为1 * 1的卷积
      is_1x1_ = true;
      for (int i = 0; i < num_spatial_axes_; ++i) {
        is_1x1_ &=
            kernel_shape_data[i] == 1 && stride_data[i] == 1 && pad_data[i] == 0;
        if (!is_1x1_) { break; }
      }
      // Configure output channels and groups.
      //输入通道数 512
      channels_ = bottom[0]->shape(channel_axis_);
      //输出通道数 1024
      num_output_ = this->layer_param_.convolution_param().num_output();
      CHECK_GT(num_output_, 0);
      // group_ = 2
      group_ = this->layer_param_.convolution_param().group();
     //分组卷积当然需要被group_整除
      CHECK_EQ(channels_ % group_, 0);
      CHECK_EQ(num_output_ % group_, 0)
          << "Number of output should be multiples of group.";
      if (reverse_dimensions()) {//如果输入和输出反向的话,则
        conv_out_channels_ = channels_; // 512
        conv_in_channels_ = num_output_;// 1024
      } else {
        conv_out_channels_ = num_output_;// 1024
        conv_in_channels_ = channels_;//512
      }
      // Handle the parameters: weights and biases.
      // - blobs_[0] holds the filter weights
      // - blobs_[1] holds the biases (optional)
      //在理论上,权值应该是一个 1024 * 256 * 3 * 3的一个四维的矩阵
      vector<int> weight_shape(2);
      weight_shape[0] = conv_out_channels_;//第一维度大小为 输出通道数 1024
      weight_shape[1] = conv_in_channels_ / group_; //第二维度为 输入通道/group_ = 256
      for (int i = 0; i < num_spatial_axes_; ++i) {
        weight_shape.push_back(kernel_shape_data[i]);//卷积核,这本例中为3 * 3
      }
      //在Caffe中,每一层的Blob_表示该层一些参量(一般都是可学习的)
      //在卷积层(反卷积层)中,Blob_[0]为权值参量,Blob_[1]为偏置值
      bias_term_ = this->layer_param_.convolution_param().bias_term();
      vector<int> bias_shape(bias_term_, num_output_);
      if (this->blobs_.size() > 0) {
        CHECK_EQ(1 + bias_term_, this->blobs_.size())
            << "Incorrect number of weight blobs.";
        if (weight_shape != this->blobs_[0]->shape()) {
          Blob<Dtype> weight_shaped_blob(weight_shape);
          LOG(FATAL) << "Incorrect weight shape: expected shape "
              << weight_shaped_blob.shape_string() << "; instead, shape was "
              << this->blobs_[0]->shape_string();
        }
        if (bias_term_ && bias_shape != this->blobs_[1]->shape()) {
          Blob<Dtype> bias_shaped_blob(bias_shape);
          LOG(FATAL) << "Incorrect bias shape: expected shape "
              << bias_shaped_blob.shape_string() << "; instead, shape was "
              << this->blobs_[1]->shape_string();
        }
        LOG(INFO) << "Skipping parameter initialization";
      } else {
        if (bias_term_) {
          this->blobs_.resize(2);
        } else {
          this->blobs_.resize(1);
        }
        // Initialize and fill the weights:
        // output channels x input channels per-group x kernel height x kernel width
        //为权值分配内存
        this->blobs_[0].reset(new Blob<Dtype>(weight_shape));
        //权值初始化
        shared_ptr<Filler<Dtype> > weight_filler(GetFiller<Dtype>(
            this->layer_param_.convolution_param().weight_filler()));
        weight_filler->Fill(this->blobs_[0].get());
        // If necessary, initialize and fill the biases.
        if (bias_term_) {
          this->blobs_[1].reset(new Blob<Dtype>(bias_shape));
          shared_ptr<Filler<Dtype> > bias_filler(GetFiller<Dtype>(
              this->layer_param_.convolution_param().bias_filler()));
          bias_filler->Fill(this->blobs_[1].get());
        }
      }
      //kernel_dim为对于一个输出特征图来说,其权值指针的偏移量
      //256 * 3 * 3
      kernel_dim_ = this->blobs_[0]->count(1);
      //weight_offset_则是对于每一个group_权值指针偏移
      // 512 * 256 * 3 * 3
      weight_offset_ = conv_out_channels_ * kernel_dim_ / group_;
      // Propagate gradients to the parameters (as directed by backward pass).
      this->param_propagate_down_.resize(this->blobs_.size(), true);
    }
    
    template <typename Dtype>
    void BaseConvolutionLayer<Dtype>::Reshape(const vector<Blob<Dtype>*>& bottom,
          const vector<Blob<Dtype>*>& top) {
      const int first_spatial_axis = channel_axis_ + 1;
      CHECK_EQ(bottom[0]->num_axes(), first_spatial_axis + num_spatial_axes_)
          << "bottom num_axes may not change.";
      num_ = bottom[0]->count(0, channel_axis_);
      CHECK_EQ(bottom[0]->shape(channel_axis_), channels_)
          << "Input size incompatible with convolution kernel.";
      // TODO: generalize to handle inputs of different shapes.
      for (int bottom_id = 1; bottom_id < bottom.size(); ++bottom_id) {
        CHECK(bottom[0]->shape() == bottom[bottom_id]->shape())
            << "shape mismatch - bottom[0]: " << bottom[0]->shape_string()
            << " vs. bottom[" << bottom_id << "]: "
            << bottom[bottom_id]->shape_string();
      }
      // Shape the tops.
      bottom_shape_ = &bottom[0]->shape();
      //计算输出特征图维度的大小
      compute_output_shape();
      vector<int> top_shape(bottom[0]->shape().begin(),
          bottom[0]->shape().begin() + channel_axis_);
      top_shape.push_back(num_output_);
      for (int i = 0; i < num_spatial_axes_; ++i) {
        top_shape.push_back(output_shape_[i]);//并返回到top_shape
      }
      //为输出分配内存空间
      for (int top_id = 0; top_id < top.size(); ++top_id) {
        top[top_id]->Reshape(top_shape);
      }
      //conv_out_spatial_dim_为输出特征图的空间大小 7 * 7 = 49
      if (reverse_dimensions()) {
        conv_out_spatial_dim_ = bottom[0]->count(first_spatial_axis);
      } else {
        conv_out_spatial_dim_ = top[0]->count(first_spatial_axis);
      }
      //在每一个group中,col_buf的偏移量为256 * 3 * 3 * 7 * 7
      col_offset_ = kernel_dim_ * conv_out_spatial_dim_;
      //每一个group中,输出的偏移量:512 * 7 * 7
      output_offset_ = conv_out_channels_ * conv_out_spatial_dim_ / group_;
      // Setup input dimensions (conv_input_shape_).
      vector<int> bottom_dim_blob_shape(1, num_spatial_axes_ + 1);
      conv_input_shape_.Reshape(bottom_dim_blob_shape);
      int* conv_input_shape_data = conv_input_shape_.mutable_cpu_data();
      //conv_input_shape_data输入特征图的形状(包含通道那个维度):512 * 14 * 14
      for (int i = 0; i < num_spatial_axes_ + 1; ++i) {
        if (reverse_dimensions()) {
          conv_input_shape_data[i] = top[0]->shape(channel_axis_ + i);
        } else {
          conv_input_shape_data[i] = bottom[0]->shape(channel_axis_ + i);
        }
      }
      // The im2col result buffer will only hold one image at a time to avoid
      // overly large memory usage. In the special case of 1x1 convolution
      // it goes lazily unused to save memory.
      //给col_buffer分配内存空间
      col_buffer_shape_.clear();
      //第一维度的大小为:512 * 3 * 3
      //也就是col_buffer二维数据矩阵的行数
      col_buffer_shape_.push_back(kernel_dim_ * group_);
      //后面两个维度,分别为输出特征图的空间尺寸 7 * 7
      //因此,col_buffer经过im2col之后
      //所得矩阵大小为(512 * 3 * 3, 7 * 7)
      for (int i = 0; i < num_spatial_axes_; ++i) {
        if (reverse_dimensions()) {
          col_buffer_shape_.push_back(input_shape(i + 1));
        } else {
          col_buffer_shape_.push_back(output_shape_[i]);
        }
      }
      col_buffer_.Reshape(col_buffer_shape_);
      //bottom_dim_和top_dim_一次卷积输入数据块和输出数据块的大小(也就是num_ = 1的时候)
      //bottom_dim_ : 512 * 14 * 14
      bottom_dim_ = bottom[0]->count(channel_axis_);
      //top_dim_ : 1024 * 7 * 7
      top_dim_ = top[0]->count(channel_axis_);
      //num_kernels_im2col_: 1024 * 7 * 7
      num_kernels_im2col_ = conv_in_channels_ * conv_out_spatial_dim_;
      //num_kernels_col2im_ : 512 * 14 * 14
      num_kernels_col2im_ = reverse_dimensions() ? top_dim_ : bottom_dim_;
      // Set up the all ones "bias multiplier" for adding biases by BLAS
      out_spatial_dim_ = top[0]->count(first_spatial_axis);
      if (bias_term_) {
        vector<int> bias_multiplier_shape(1, out_spatial_dim_);
        bias_multiplier_.Reshape(bias_multiplier_shape);
        caffe_set(bias_multiplier_.count(), Dtype(1),
            bias_multiplier_.mutable_cpu_data());
      }
    }
    //forward_cpu_gemm在卷积层中是前向传播
    //在反卷积中,为反向传播
    template <typename Dtype>
    void BaseConvolutionLayer<Dtype>::forward_cpu_gemm(const Dtype* input,
        const Dtype* weights, Dtype* output, bool skip_im2col) {
      const Dtype* col_buff = input;
      if (!is_1x1_) {
        if (!skip_im2col) {
        //将输入的四维矩阵,通过im2col转换成二维矩阵
        //在所举例子中
        //512, 14, 14 --> 512 * 3 * 3, 7 * 7
          conv_im2col_cpu(input, col_buffer_.mutable_cpu_data());
        }
        //获取转换后的指针
        col_buff = col_buffer_.cpu_data();
      }
      for (int g = 0; g < group_; ++g) {//分组卷积(矩阵相乘)
      //矩阵乘法
      //output = weight * col_buff
      //根据caffe_cpu_gemm函数,来看看其相乘的合法性
      //三个矩阵大小通过offset * g来理解,则矩阵大小为*_offset(*: weight, col, output)
      //根据Reshape函数:
      //weigh_offset: 512 * 256 * 3 * 3
      //col_offset: 256 * 3 * 3 * 7 * 7
      //output_offset: 512 * 7 * 7
      //矩阵的所占空间确定了,再来解析矩阵的行和列
      //weight: (conv_out_channels_ /group_, kernel_dim_) = (512, 256 * 3 * 3)
      //col: (kernel_dim_, conv_out_spatial_dim_) = (256 * 3 * 3,  7 * 7)
      //output: (conv_out_channels_ /group_, conv_out_spatial_dim_) = (512, 7 * 7)
        caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans, conv_out_channels_ /
            group_, conv_out_spatial_dim_, kernel_dim_,
            (Dtype)1., weights + weight_offset_ * g, col_buff + col_offset_ * g,
            (Dtype)0., output + output_offset_ * g);
      }
    }
    
    template <typename Dtype>
    void BaseConvolutionLayer<Dtype>::forward_cpu_bias(Dtype* output,
        const Dtype* bias) {
      caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans, num_output_,
          out_spatial_dim_, 1, (Dtype)1., bias, bias_multiplier_.cpu_data(),
          (Dtype)1., output);
    }
    //backward_cpu_gemm在卷积层中,为反向传播
    //在反卷积层中,为前向传播
    template <typename Dtype>
    void BaseConvolutionLayer<Dtype>::backward_cpu_gemm(const Dtype* output,
        const Dtype* weights, Dtype* input) {
      Dtype* col_buff = col_buffer_.mutable_cpu_data();
      if (is_1x1_) {
        col_buff = input;
      }
      for (int g = 0; g < group_; ++g) {//每一个group单独卷积
      //和forward_cpu_gemm一样的,我们来对该矩阵进行分析
      //col_buff = weight' * output('表示转置)
      //weigh_offset: 512 * 256 * 3 * 3
      //col_offset: 256 * 3 * 3 * 7 * 7
      //output_offset: 512 * 7 * 7
      //剩下的就和和forward_cpu_gemm一样的一样的
      //之所以,backward_cpu_gemm和forward_cpu_gemm都能够实现反向和前向传播
      //是因为,输入指针的问题
      //以backward_cpu_gemm举例,若输入的指针分别是:top_diff,weight_data, bottom_diff
      //经过后面conv_col2im_cpu的转换,其本质是对bottom求梯度
      //若输入的指针分别是:bottom_data, weight_data, top_data,其本质是前向传播
      //当然,偏移量和矩阵大小在Reshape中的reverse_dimensions函数,是否调换输入输出
      //在卷积层中,reverse_dimensions函数返回false
      //在反卷积层中,reverse_dimensions函数返回true
      //在有些文献中,Deconvolution也称作convolution_transpose
        caffe_cpu_gemm<Dtype>(CblasTrans, CblasNoTrans, kernel_dim_,
            conv_out_spatial_dim_, conv_out_channels_ / group_,
            (Dtype)1., weights + weight_offset_ * g, output + output_offset_ * g,
            (Dtype)0., col_buff + col_offset_ * g);
      }
      //将col_buf返回为四维的矩阵
      if (!is_1x1_) {
        conv_col2im_cpu(col_buff, input);
      }
    }
    //对权重求梯度
    //同样的input和output指针
    //卷积运算来说,input: bottom, output: top_diff
    //反卷积运算来说,input: top, output: bottom_diff
    template <typename Dtype>
    void BaseConvolutionLayer<Dtype>::weight_cpu_gemm(const Dtype* input,
        const Dtype* output, Dtype* weights) {
      const Dtype* col_buff = input;
      //和卷积一样的,首先进行im2col
      //将输入由(512, 14, 14)变成(512 * 3 * 3, 7 * 7) 
      if (!is_1x1_) {
        conv_im2col_cpu(input, col_buffer_.mutable_cpu_data());
        col_buff = col_buffer_.cpu_data();
      }
      for (int g = 0; g < group_; ++g) {//分组卷积
      //同样的
      //weight_offset: 512 * 256 * 3 * 3
      //col_offset_: 256 * 3 * 3 *  7 * 7
      //output_offset: 512 * 7 * 7
      //weight_diff = output * col_buf'
      //weight_diff : (512, 256 * 3 * 3)
      //output: (512, 7 * 7)
      //input:(256 * 3 * 3, 7 * 7)
        caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasTrans, conv_out_channels_ / group_,
            kernel_dim_, conv_out_spatial_dim_,
            (Dtype)1., output + output_offset_ * g, col_buff + col_offset_ * g,
            (Dtype)1., weights + weight_offset_ * g);
      }
    }
    //后面都是GPU以及偏置求导的相关
    template <typename Dtype>
    void BaseConvolutionLayer<Dtype>::backward_cpu_bias(Dtype* bias,
        const Dtype* input) {
      caffe_cpu_gemv<Dtype>(CblasNoTrans, num_output_, out_spatial_dim_, 1.,
          input, bias_multiplier_.cpu_data(), 1., bias);
    }
    
    #ifndef CPU_ONLY
    
    template <typename Dtype>
    void BaseConvolutionLayer<Dtype>::forward_gpu_gemm(const Dtype* input,
        const Dtype* weights, Dtype* output, bool skip_im2col) {
      const Dtype* col_buff = input;
      if (!is_1x1_) {
        if (!skip_im2col) {
          conv_im2col_gpu(input, col_buffer_.mutable_gpu_data());
        }
        col_buff = col_buffer_.gpu_data();
      }
      for (int g = 0; g < group_; ++g) {
        caffe_gpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans, conv_out_channels_ /
            group_, conv_out_spatial_dim_, kernel_dim_,
            (Dtype)1., weights + weight_offset_ * g, col_buff + col_offset_ * g,
            (Dtype)0., output + output_offset_ * g);
      }
    }
    
    template <typename Dtype>
    void BaseConvolutionLayer<Dtype>::forward_gpu_bias(Dtype* output,
        const Dtype* bias) {
      caffe_gpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans, num_output_,
          out_spatial_dim_, 1, (Dtype)1., bias, bias_multiplier_.gpu_data(),
          (Dtype)1., output);
    }
    
    template <typename Dtype>
    void BaseConvolutionLayer<Dtype>::backward_gpu_gemm(const Dtype* output,
        const Dtype* weights, Dtype* input) {
      Dtype* col_buff = col_buffer_.mutable_gpu_data();
      if (is_1x1_) {
        col_buff = input;
      }
      for (int g = 0; g < group_; ++g) {
        caffe_gpu_gemm<Dtype>(CblasTrans, CblasNoTrans, kernel_dim_,
            conv_out_spatial_dim_, conv_out_channels_ / group_,
            (Dtype)1., weights + weight_offset_ * g, output + output_offset_ * g,
            (Dtype)0., col_buff + col_offset_ * g);
      }
      if (!is_1x1_) {
        conv_col2im_gpu(col_buff, input);
      }
    }
    
    template <typename Dtype>
    void BaseConvolutionLayer<Dtype>::weight_gpu_gemm(const Dtype* input,
        const Dtype* output, Dtype* weights) {
      const Dtype* col_buff = input;
      if (!is_1x1_) {
        conv_im2col_gpu(input, col_buffer_.mutable_gpu_data());
        col_buff = col_buffer_.gpu_data();
      }
      for (int g = 0; g < group_; ++g) {
        caffe_gpu_gemm<Dtype>(CblasNoTrans, CblasTrans, conv_out_channels_ / group_,
            kernel_dim_, conv_out_spatial_dim_,
            (Dtype)1., output + output_offset_ * g, col_buff + col_offset_ * g,
            (Dtype)1., weights + weight_offset_ * g);
      }
    }
    
    template <typename Dtype>
    void BaseConvolutionLayer<Dtype>::backward_gpu_bias(Dtype* bias,
        const Dtype* input) {
      caffe_gpu_gemv<Dtype>(CblasNoTrans, num_output_, out_spatial_dim_, 1.,
          input, bias_multiplier_.gpu_data(), 1., bias);
    }
    
    #endif  // !CPU_ONLY
    
    INSTANTIATE_CLASS(BaseConvolutionLayer);
    
    }  // namespace caffe

    im2col col2im

    之前说过了im2col是将三维的输入(channel, height, width)特征图通过卷积的对应关系转换成二维矩阵方便矩阵运算。
    1. 在卷积层来说,前向传播首先通过im2col转换成col_buf,然后再和权重矩阵相乘,输出结果。其反向传播,则是权值weight_data和top_diff矩阵相乘,转换成col_buf_diff,然后通过col2im进一步转换成bottom_diff;
    2. 对于反卷积层来说,由于在反卷积层Reverse_dim函数返回的是True,因此,其权值的矩阵设定和col_buf矩阵的设定都是按照输入输出对调的情况,因此,在前向传播,其本质是卷积层的反向传播,首先通过weight_data和bottom_data相乘得到col_buf的值,然后通过col2im转换成top_data。在反向传播的过程,其本质是卷积层前向传播,通过im2col将top_diff转换为col_buff,然后权值矩阵weight_data和col_buf矩阵相乘得到bottom_diff。、
    以上看出其实im2col、col2im其实就是根据卷积的关系从图像到矩阵,从矩阵到图像的过程。其源码如下:

    //data_im:输入图像,三维数据(channel, height, width)
    //kernel、pad、stride、dilation卷积的基本信息
    //再来说说,data_col的形状,根据对应关系以及上文描述的矩阵相乘所满足的信息
    //data_col:(channel * kernel_h * kernel_w, output_w * output_h)
    template <typename Dtype>
    void im2col_cpu(const Dtype* data_im, const int channels,
        const int height, const int width, const int kernel_h, const int kernel_w,
        const int pad_h, const int pad_w,
        const int stride_h, const int stride_w,
        const int dilation_h, const int dilation_w,
        Dtype* data_col) {
    //计算输出特征图的长和宽
      const int output_h = (height + 2 * pad_h -
        (dilation_h * (kernel_h - 1) + 1)) / stride_h + 1;
      const int output_w = (width + 2 * pad_w -
        (dilation_w * (kernel_w - 1) + 1)) / stride_w + 1;
      const int channel_size = height * width;
    //第一级循环,channel,同时data_im做数据偏移,则以下循环对每一个通道
      for (int channel = channels; channel--; data_im += channel_size) {
        for (int kernel_row = 0; kernel_row < kernel_h; kernel_row++) {//第二级循环,kernel_h
          for (int kernel_col = 0; kernel_col < kernel_w; kernel_col++) {//第三级循环,kernel_w
          //以上三级循环,为col_buf行数
          //显然以下两级循环,求col_buf的每一行
          //每一行的每一个元素代表一张特征图需要卷积点
            int input_row = -pad_h + kernel_row * dilation_h;//卷积点 相对于卷积核的行位置
            for (int output_rows = output_h; output_rows; output_rows--) {
              if (!is_a_ge_zero_and_a_lt_b(input_row, height)) {//如果当前行坐标大于height,则后面的补零
                for (int output_cols = output_w; output_cols; output_cols--) {
                  *(data_col++) = 0;
                }
              } else {
                int input_col = -pad_w + kernel_col * dilation_w;//卷积点,相对于卷积核列位置
                for (int output_col = output_w; output_col; output_col--) {
                  if (is_a_ge_zero_and_a_lt_b(input_col, width)) {
                    *(data_col++) = data_im[input_row * width + input_col];//获取卷积点相对于图像的坐标点
                  } else {
                    *(data_col++) = 0;
                  }
                  input_col += stride_w;//进行偏移
                }
              }
              input_row += stride_h;
            }
          }
        }
      }
    }
    展开全文
  • 卷积层与池化层

    万次阅读 多人点赞 2018-08-04 16:08:16
    卷积神经网络(Convolutional Neural Layer, CNN),除了全连接层以外(有时候也不含全连接层,因为出现了Global average pooling),还包含了卷积层和池化层。卷积层用来提取特征,而池化层可以减少参数数量。 ...

    构建了最简单的网络之后,是时候再加上卷积和池化了。这篇,虽然我还没开始构思,但我知道,一定是很长的文章。

    卷积神经网络(Convolutional Neural Layer, CNN),除了全连接层以外(有时候也不含全连接层,因为出现了Global average pooling),还包含了卷积层和池化层。卷积层用来提取特征,而池化层可以减少参数数量。

    卷积层

    先谈一下卷积层的工作原理。

    我们是使用卷积核来提取特征的,卷积核可以说是一个矩阵。假如我们设置一个卷积核为3*3的矩阵,而我们图片为一个分辨率5*5的图片。那么卷积核的任务就如下所示:

    从左上角开始,卷积核就对应着数据的3*3的矩阵范围,然后相乘再相加得出一个值。按照这种顺序,每隔一个像素就操作一次,我们就可以得出9个值。这九个值形成的矩阵被我们称作激活映射(Activation map)。这就是我们的卷积层工作原理。也可以参考下面一个gif:
    其中,卷积核为

    101010101

    其实我们平时举例的卷积核已经被翻转180度一次了,主要是因为计算过程的原因。详细不用了解,但原理都一样。

    但其实我们输入的图像一般为三维,即含有R、G、B三个通道。但其实经过一个卷积核之后,三维会变成一维。它在一整个屏幕滑动的时候,其实会把三个通道的值都累加起来,最终只是输出一个一维矩阵。而多个卷积核(一个卷积层的卷积核数目是自己确定的)滑动之后形成的Activation Map堆叠起来,再经过一个激活函数就是一个卷积层的输出了。

    卷积层还有另外两个很重要的参数:步长和padding。

    所谓的步长就是控制卷积核移动的距离。在上面的例子看到,卷积核都是隔着一个像素进行映射的,那么我们也可以让它隔着两个、三个,而这个距离被我们称作步长。

    而padding就是我们对数据做的操作。一般有两种,一种是不进行操作,一种是补0使得卷积后的激活映射尺寸不变。上面我们可以看到5*5*3的数据被3*3的卷积核卷积后的映射图,形状为3*3,即形状与一开始的数据不同。有时候为了规避这个变化,我们使用“补0”的方法——即在数据的外层补上0。

    下面是示意图:

    步长2
    步长为2 (图片来自于机器之心
    补0
    补0的变化 (图片来自于机器之心

    了解卷积发展史的人都应该知道,卷积神经网络应用最开始出现是LeCun(名字真的很像中国人)在识别手写数字创建的LeNet-5。

    LeNet-5

    后面爆发是因为AlexNet在ImageNet比赛中拔得头筹,硬生生把误差变成去年的一半。从此卷积网络就成了AI的大热点,一大堆论文和网络不断地发挥它的潜能,而它的黑盒性也不断被人解释。

    能否对卷积神经网络工作原理做一个直观的解释? - Owl of Minerva的回答 - 知乎里面通过我们对图像进行平滑的操作进而解释了卷积核如何读取特征的。

    我们需要先明确一点,实验告诉我们人类视觉是先对图像边缘开始敏感的。在我的理解中,它就是说我们对现有事物的印象是我们先通过提取边界的特征,然后逐渐的完善再进行组装而成的。而我们的卷积层很好的做到了这一点。

    这是两个不同的卷积核滑动整个图像后出来的效果,可以看出,经过卷积之后图像的边界变得更加直观。我们也可以来看下VGG-16网络第一层卷积提取到的特征:

    VGG-16

    由此来看,我们也知道为什么我们不能只要一个卷积核。在我的理解下,假使我们只有一个卷积核,那我们或许只能提取到一个边界。但假如我们有许多的卷积核检测不同的边界,不同的边界又构成不同的物体,这就是我们怎么从视觉图像检测物体的凭据了。所以,深度学习的“深”不仅仅是代表网络,也代表我们能检测的物体的深度。即越深,提取的特征也就越多。

    Google提出了一个项目叫Deepdream,里面通过梯度上升、反卷积形象的告诉我们一个网络究竟想要识别什么。之前权重更新我们讲过梯度下降,而梯度上升便是计算卷积核对输入的噪声的梯度,然后沿着上升的方向调整我们的输入。详细的以后再讲,但得出的图像能够使得这个卷积核被激活,也就是说得到一个较好的值。所以这个图像也就是我们卷积核所认为的最规范的图像(有点吓人):

    Deepdream
    其实这鹅看着还不错,有点像孔雀。

    池化层 (pooling layer)

    前面说到池化层是降低参数,而降低参数的方法当然也只有删除参数了。

    一般我们有最大池化和平均池化,而最大池化就我认识来说是相对多的。需要注意的是,池化层一般放在卷积层后面。所以池化层池化的是卷积层的输出!

    扫描的顺序跟卷积一样,都是从左上角开始然后根据你设置的步长逐步扫描全局。有些人会很好奇最大池化的时候你怎么知道哪个是最大值,emmm,其实我也考虑过这个问题。CS2131n里面我记得是说会提前记录最大值保存在一个矩阵中,然后根据那个矩阵来提取最大值。

    至于要深入到计算过程与否,应该是没有必要的。所以我也没去查证过程。而且给的都是示例图,其实具体的计算过程应该也是不同的,但效果我们可以知道就好了。

    至于为什么选择最大池化,应该是为了提取最明显的特征,所以选用的最大池化。平均池化呢,就是顾及每一个像素,所以选择将所有的像素值都相加然后再平均。

    池化层也有padding的选项。但都是跟卷积层一样的,在外围补0,然后再池化。

    代码解析
    import tensorflow as tf
    import numpy as np
    from tensorflow.examples.tutorials.mnist import input_data
    
    mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
    summary_dir = './summary'
    #批次
    batch_size = 100
    
    n_batch = mnist.train.num_examples // batch_size
    
    x = tf.placeholder(tf.float32, [None, 784], name='input')
    y = tf.placeholder(tf.float32, [None, 10], name='label')
    
    def net(input_tensor):
        conv_weights = tf.get_variable('weight', [3, 3, 1, 32],
                                        initializer=tf.truncated_normal_initializer(stddev=0.1))
        conv_biases = tf.get_variable('biase', [32], initializer=tf.constant_initializer(0.0))
    
        conv = tf.nn.conv2d(input_tensor, conv_weights, strides=[1, 1, 1, 1], padding='SAME')
        relu = tf.nn.relu(tf.nn.bias_add(conv, conv_biases))
    
        pool = tf.nn.max_pool(relu, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME')
    
        pool_shape = pool.get_shape().as_list()
        nodes = pool_shape[1] * pool_shape[2] * pool_shape[3]
        pool_reshaped = tf.reshape(pool, [-1, nodes])
    
        W = tf.Variable(tf.zeros([nodes, 10]), name='weight')
        b = tf.Variable(tf.zeros([10]), name='bias')
        fc = tf.nn.softmax(tf.matmul(pool_reshaped, W) + b)
    
        return fc
    
    reshaped = tf.reshape(x, (-1, 28, 28, 1))
    prediction = net(reshaped)
    loss_ = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=tf.argmax(y, 1), logits=prediction, name='loss')
    loss = tf.reduce_mean(loss_)
    
    train_step = tf.train.GradientDescentOptimizer(0.2).minimize(loss)
    
    correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(prediction, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32), name='accuracy')
    
    init = tf.global_variables_initializer()
    
    with tf.Session() as sess:
        sess.run(init)
        for epoch in range(31):
            for batch in range(n_batch):
                batch_xs, batch_ys = mnist.train.next_batch(batch_size)
                sess.run(train_step, feed_dict={x: batch_xs, y: batch_ys})
            acc = sess.run(accuracy, feed_dict={x: mnist.test.images, y: mnist.test.labels})
            print('Iter' + str(epoch) + ",Testing Accuracy" + str(acc))

    这相对于我第一个只用全连接的网络只多了一个net函数,还有因为卷积层的关系进来的数据x需要改变形状。只讲这两部分:

    reshaped = tf.reshape(x, (-1, 28, 28, 1))
    prediction = net(reshaped)

    由于我们feedict上面是,feed_dict={x: mnist.test.images, y: mnist.test.labels},而这样子调用tensorflow的句子我们得到的x固定的形状。因此我们应用tf.reshape(x_need_reshaped,object_shape)来得到需要的形状。

    其中的1表示拉平,不能用None,是固定的。

    conv_weights = tf.get_variable('weight', [3, 3, 1, 32],
                                        initializer=tf.truncated_normal_initializer(stddev=0.1))
    conv_biases = tf.get_variable('biase', [32], initializer=tf.constant_initializer(0.0))
    
    conv = tf.nn.conv2d(input_tensor, conv_weights, strides=[1, 1, 1, 1], padding='SAME')
    relu = tf.nn.relu(tf.nn.bias_add(conv, conv_biases))
    
    pool = tf.nn.max_pool(relu, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME')

    大部分都是应用内置的函数,来初始化weight(就是卷积核)和biases(偏置项)。偏置项我们没有提到,但其实就是多了一个参数来调控,因此我们讲卷积层的时候也没怎么讲。按照代码就是出来Activation Map之后再分别加上bias。池化也是用到了最大池化。

    注意一下relu。它也是一个激活函数,作用可以说跟之前讲的softmax一样,不过它在卷积层用的比较多,而且也是公认的比较好的激活函数。它的变体有很多。有兴趣大家可以自己去查阅资料。以后才会写有关这方面的文章。

        pool_shape = pool.get_shape().as_list()
        nodes = pool_shape[1] * pool_shape[2] * pool_shape[3]
        pool_reshaped = tf.reshape(pool, [-1, nodes])
    
        W = tf.Variable(tf.zeros([nodes, 10]), name='weight')

    池化层的输出我们并不知道它是如何的形状(当然,你也可以动手算)。因此就算把池化层拉成一维的矩阵,我们也不知道W需要如何的形状。因此,我们查看pool(即池化层的输出)的形状,我暗地里print了一下为[None, 14, 14, 32],因此pool拉平后,就是[None, 14*14*32, 10]。为了接下来进行全连接层的计算,我们的W的形状也应该为[14*14*32, 10]。这段代码的原理就是如此。

    准确率也一样取后15次:

    结果

    emmm, 不用跟之前比了,明显比以前好很多了。下一章决定总结一下,优化的方法好了。

    参考
    https://mlnotebook.github.io/post/CNN1/(可惜是全英)
    能否对卷积神经网络工作原理做一个直观的解释? - Owl of Minerva的回答 - 知乎
    CS231n

    展开全文
  • 导语:转置卷积层(Transpose Convolution Layer)又称反卷积层或分数卷积层,在最近提出的卷积神经网络中越来越常见了,特别是在对抗生成神经网络(GAN)中,生成器网络中上采样部分就出现了转置卷积层,用于恢复...

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~

    本文由forrestlin发表于云+社区专栏

    导语:转置卷积层(Transpose Convolution Layer)又称反卷积层或分数卷积层,在最近提出的卷积神经网络中越来越常见了,特别是在对抗生成神经网络(GAN)中,生成器网络中上采样部分就出现了转置卷积层,用于恢复减少的维数。那么,转置卷积层和正卷积层的关系和区别是什么呢,转置卷积层实现过程又是什么样的呢,笔者根据最近的预研项目总结出本文。

    1. 卷积层和全连接层

    在CNN提出之前,我们所提到的人工神经网络应该多数情况下都是前馈神经网络,两者区别主要在于CNN使用了卷积层,而前馈神经网络用的都是全连接层,而这两个layer的区别又在于全连接层认为上一层的所有节点下一层都是需要的,通过与权重矩阵相乘层层传递,而卷积层则认为上一层的有些节点下一层其实是不需要的,所以提出了卷积核矩阵的概念,如果卷积核的大小是nm,那么意味着该卷积核认为上一层节点每次映射到下一层节点都只有nm个节点是有意义的,具体的映射方式下一节会讲到。到这里,有些初学者会认为全连接层也可以做到,只要让权重矩阵某些权重赋值为0就可以实现了,例如假设在计算当前层第2个节点时认为上一层的第1个节点我不需要,那么设置w01=0就可以了。其实没错,卷积层是可以看做全连接层的一种特例,卷积核矩阵是可以展开为一个稀疏的包含很多0的全连接层的权重矩阵,下图就是一个由44图片经过33卷积核生成一个大小为2*2output时,卷积核所展开的全连接层的权重矩阵。

    img卷积核对应的全连接层权重矩阵

    可以看到,上面的矩阵大小为416,比卷积核33大了不少,因此使用卷积层而不用全连接层第一个原因就是可以极大的减少参数的个数,第二个原因就是卷积核关注的是某几个相邻的节点之间的关系,学习了图片的局部特征,可以说是带有目的性的学习,例如33的卷积核学习的就是相互距离为2的节点之间的关系。这与全连接层无区别的对待所有节点进行学习有极大的差别,这样一来就解决了前馈神经网络不能学习位移不变性的缺点。举个栗子,当我们在前馈神经网络中学习一个44的图片中是否有横折图案时,使用下图中4个训练数据进行训练,那么最终只会对5,6,9,a这四个节点的权重有所调节,然后如果出现如下图最后一张图片作为测试时,就会导致网络无法识别,而由于卷积核在不同节点间权重是共享的,所以就自然而然克服了这个问题。

    img卷积克服平移不变性

    2. 卷积层的运算过程

    2.1 最简单的卷积

    卷积层的运算其实就是将多个卷积核作用于输入上,如下图所示,是最简单的一个卷积核所做的运算,no padding,no stride,底下蓝色方块看做是输入的图片,阴影部分就是33的卷积核(一般卷积核是个正方形,且边长为奇数),卷积核扫过时便与输入相乘再相加,最终得到22的输出,对应青色区域。

    imgno padding, no stride的卷积

    通常一层卷积层会包含多个卷积核,代表着卷积层的输出深度,例如下图就是我们经常在论文中看到的深度网络的架构,其中第一层就是卷积层+最大池化层,先不管最大池化层,至少我们可以明确卷积核的大小是55,卷积核个数是16,该层输出的size是1818。

    img论文常见的卷积层

    2.2 带padding的卷积

    从最简单的卷积动图中我们可以看到,经过卷积操作,输出会比输入要小,但是有时候我们希望输出的size要与输入保持一致,而padding就是为了这个而引入的,而这种为了让输入和输出size保持一样的padding,我们会称之为"same padding",可参考下面的动图,卷积核大小是3*3,padding是1,padding实际的表现就是在输入的四周补0,padding是多少就补多少层,且上限是卷积核大小-1,正如下图中虚线区域,一般来说,论文中是不会给出padding的大小,需要我们自己推导,推导公式可见下文。

    imgpadding=1的卷积

    根据padding大小不同,我们可以分为三种padding:

    • same padding: 为了让输出和输入的size一样而补上的padding,例如33的核,same padding = 1,55的核,same padding = 2。
    • full padding: padding = kernel size - 1
    • valid padding: padding = 0

    2.3 stride大于1的卷积

    stride就是步长,表示卷积核两次卷积操作的距离,默认是1,上述讲的两个例子步长都是1,而下面两个动图展示的是stride为2的情况,分别是无padding和有padding的情况。通常stride大于1时我们称为等距下采样,因为这样输出肯定会丢失信息,size比输入的小。

    imgno padding, stride=2的卷积

    imgpadding=1, stride=2的卷积

    2.4 卷积核输入输出size与卷积核的关系

    上文中我们提到padding通常需要我们自己算出来,那么我们该怎么算呢,其实就是根据输入输出size和卷积核大小的关系算出来的,上面提到的几种卷积,其实就是卷积操作的三个参数,核大小(F)、padding(P)和stride(S),如果细心的读者在看动图时就会发现输出size是可以根据输入size和那三个参数计算出来的,公式如下,这里只给出宽度的计算,高度也是一样的。

    W2=(W1−F+2P)÷S+1

    这里我们注意到上面的公式是有除法的,所以就会存在除不尽的情况,这时候我们需要向下取整,这种情况我们称为odd卷积,其过程可参考下面动图。

    imgodd卷积

    3. 转置卷积层

    讲完卷积层后,我们来看CNN中另一个进行卷积操作的层次转置卷积层,有时我们也会称做反卷积层,因为他的过程就是正常卷积的逆向,但是也只是size上的逆向,内容上不一定,所以有些人会拒绝将两者混为一谈。转置卷积层最大的用途就是上采样了,刚刚我们说到在正常卷积中stride大于1时我们进行的是等距下采样,会让输出的size比输入小,而转置卷积层我们就会用stride小于1的卷积进行上采样,使输出的size变大,所以转置卷积层还有个别称就是分数卷积层。上采样最常见的场景可以说就是GAN中的生成器网络,如下图所示,虽然论文作者使用的是conv,但由于它的步长为1/2,所以代表的就是转置卷积层。

    img转置卷积例子

    为了理解转置卷积层,我们需要明白什么叫做正常卷积的逆向,这通常也是新手难以理解的地方,下面笔者通过两个图来更好的解释,第一个图是正常卷积的过程,第二个图就是其对应的转置卷积,在第一个图中,大的正方形中数字1只参与小正方形中数字1的计算,那么在转置卷积中,大正方形的1也只能由小正方形的1生成,这就是逆向的过程。

    imgno padding, no stride的卷积

    img转置卷积.png

    和讲述正常卷积的过程一样,笔者下面也会一一给出相对应的转置卷积。

    3.1 no padding no stride的卷积对应的转置卷积

    上面用作解释转置卷积的逆向过程时用到的图其实就是最简单(no padding, no stride)卷积以及其对应的转置卷积,这里给出它的动图。

    imgno padding, no stride的卷积转置

    3.2 带padding的卷积的转置卷积

    在正卷积中如果是有padding,那么在转置卷积中不一定会有padding,其计算公式下文会给出,这里先给出2.2对应的转置卷积动图。

    imgpadding为1的卷积转置

    3.3 stride大于1的卷积的转置卷积

    在本节一开始就讲到,stride大于1的卷积是下采样,那么其对应的转置卷积便是stride小于1的上采样,但是不管是在pyTorch还是TensorFlow中,convTranspose函数的参数都是整数,不可能将stride设置为小于1的浮点数,那么我们会依然给convTranspose函数传入正卷积的stride,而convTranspose是怎么做的呢,可见下面的动图,它是2.3中无padding卷积对应的转置卷积,我们先不看转置卷积中的转置padding,也就是动图中外部的虚线区域,然后会发现每两个蓝色块之间都插入了白色块,也就是0,这样一来,卷积核每移动一步不就相当于是只移动了1/2步嘛,所以我们可以得出每两个蓝色块之间需要插入stride -1个0。

    imgstride为2的卷积转置

    3.4 正卷积和转置卷积的换算关系

    3.4.1 转置卷积的padding

    从上面3个例子的转置卷积中我们可以发现,如果用正卷积实现转置卷积时,卷积核的大小是保持不变的,而stride是为正卷积stride的倒数(只是我们插入0来模拟分数移动),最后,转置卷积的padding要怎么算呢,虽然如果我们调用pyTorch或TensorFlow时不需要管,传入正卷积的padding即可,但是理解convTranspose是怎么做的也有助于我们理解转置卷积。说了这么多,其实在我们为了让转置卷积保证是正卷积的逆向时,我们就不得不补充转置padding,我们用PT表示,其计算公式为:PT=F−P−1,其中F为正卷积的核大小,P为正卷积的padding。

    3.4.2 转置卷积的输出size

    这个其实很好算,因为我们都说转置卷积的逆向,所以我们只需在2.4给出公式中转换下求出W1即可,公式如下:

    W1=(W2−1)×S−2P+F

    其中S是正卷积的stride,P是正卷积的padding,F是正卷积的核边长。

    3.4.3 odd卷积的转置卷积

    这个可以说是转置卷积中最难理解的一种情况,在2.4中我们提到在除以stride时可能会除不尽要向下取整,那么我们在求W1时就会有不确定性,举个栗子,还是第3节一开始给出的图,我们是希望将W/4的图放大到W/2的程度,这是一个转置卷积的过程,我们先算一遍正卷积,从W/2下采样到W/4,k代表核边长为3,s是stride为1/2的倒数,即2,padding根据2.4的公式推导为1,所以正卷积的计算公式是:(W2−3+2)÷2+1=W4+12,然后向下取整就是W4,和图上显示的是一样,但是如果我们通过3.4.2的公式反过来计算,就是(W4−1)×2−2+3=W2−1,这就是odd转置卷积的不确定性,我们再回头看2.4给出的动图,会发现右边和下边的填充区域我们并没有进行卷积运算,因为向下取整而忽略了,所以我们在转置卷积时需要将这部分加回来,因此,在PyTorch中convTranspose函数还有一个参数output_padding就是负责处理这个的,TensorFlow应该也有相应的参数,笔者不太熟悉,下面就是PyTorch对该参数的描述,和我们遇到的情形一模一样。

    imgPyTorch中转置卷积的output_padding参数

    至于output_padding的值,应该为(W1−F+2P)%S,在上面提到的例子中就应该是1。

    4. 总结

    本文先是介绍了卷积神经网络和传统的前馈神经网络的联系和区别,然后再通过不同参数的卷积过程阐述卷积运算,最后再介绍刚入门深度学习时晦涩难懂的转置卷积,给出不同参数下正卷积所对应的转置卷积,最后总结出在卷积运算中所用到的公式。希望笔者上述的分析和解释能对刚入门CNN的同学有所帮助,而且笔者是从事iOS开发的,对于CNN和深度学习也是刚刚入门,希望各位AI大牛们不吝指教。

    5. 参考文档

    相关阅读
    【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识

    此文已由作者授权腾讯云+社区发布,更多原文请点击

    搜索关注公众号「云加社区」,第一时间获取技术干货,关注后回复1024 送你一份技术课程大礼包!

    海量技术实践经验,尽在云加社区

    转载于:https://www.cnblogs.com/qcloud1001/p/10000106.html

    展开全文
  • 本文关注以下四个问题: 卷积层的作用? 池化层的作用? 卷积层的卷积核的大小选取? 池化层的参数设定?引出的另外两个问题: 全链接层的作用? 1*1的卷积核的作用? 卷积神经网络为什么效果这么好?卷积层的作用?...
  • TF之DD:实现输出Inception模型内的某个卷积层或者所有卷积层的形状 目录 输出结果 整体思路架构 实现(部分)代码 输出结果 整体思路架构 实现(部分)代码 #TF之DD:实现输出Inception模型内的...
  • TensorFlow卷积层函数(卷积层+池化层)

    千次阅读 2018-05-10 22:34:12
    TensorFlow中卷积神经网络常用的有卷积层和池化层,下面对常用函数的常用参数进行总结:主要是三个函数:import tensorflow as tf tf.nn.conv2d() tf.nn.max_pool() tf.nn.avg_pool()1.tf.nn.conv2d 一般常用的...
  • 欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~导语:转置卷积层(Transpose Convolution Layer)又称反卷积层或分数卷积层,在最近提出的卷积神经网络中越来越常见了,特别是在对抗生成神经网络(GAN)中,...
  • 卷积层、反卷积层和相关层三个层之间有很大的相似之处,现在好好分析一下。 1.卷积层 下图较大网格表示一幅图片,有颜色填充的网格表示一个卷积核,卷积核的大小为33。假设我们做步长为1的卷积操作,表示卷积核...
  • 卷积层

    2021-04-11 16:58:59
    创建卷积层类时,只需要指定卷积核数量参数filters,卷积核大小kernel_size,步长strides,填充padding等。创建了4个3*3大小的卷积核的卷积层,步长为1 ,padding设置为‘SAME’ import tensorflow as tf import ...
  • 二维卷积层

    2020-09-09 15:10:56
    《动手学深度学习pytorch》部分学习笔记,仅用作自己...虽然卷积层得名于卷积(convolution)运算,但我们通常在卷积层中使⽤更加直观的互相关(cross-correlation)运算。在二维卷积层中,一个二维输入数组和⼀个二维
  • TensorFlow 卷积层

    2019-09-28 09:00:23
    TensorFlow 卷积层 让我们看下如何在 TensorFlow 里面实现 CNN。 TensorFlow 提供了tf.nn.conv2d()和tf.nn.bias_add()函数来创建你自己的卷积层。 1 # Output depth 2 k_output = 64 ...
  • 本章主要内容:二维卷积层,填充和步幅,多输入通道和多输出通道,卷积层与全连接层的对比,卷积层的简洁实现,池化。
  • 卷积层与反卷积层是CNN中最常用的两个操作层,网上有很多关于他们的介绍,但是看过后都觉得不是那么易懂,深度学习的大佬太多,但是缺少针对初学者的理解教程和文章,先挖个坑,有空会写一下我对卷积层和反卷积层的...
  • tensorflow代码(Tensorflow官方文档)中: w_conv1=weight_variable([5,5,1,32]),一直不明白这个32是怎么来的,表示的是什么?...就用上面的例子,在第一个卷积层就有55x55x96=290,400个神经元,每个有11x11

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 10,563
精华内容 4,225
关键字:

卷积层