Operators in MXNet-Pooling

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_20965753/article/details/71641418

       本篇文章将对mxnet的Pooling操作进行详细说明, 源码见src/operator/pooling-inl.h. 现将源码pooling-inl.h.及注释贴上. 源码的注释都是笔者自己写的, 有分析不对的地方网各位读者加以指正.只把层的参数部分, 前向传播和反向传播部分贴上.

/*!
 * Copyright (c) 2015 by Contributors
 * \file pooling-inl.h
 * \brief
 * \author
*/

#ifndef MXNET_OPERATOR_POOLING_INL_H_
#define MXNET_OPERATOR_POOLING_INL_H_
/*
#ifndef是"if not defined"的简写, 先测试要定义的宏变量是否被宏定义过. 定义mxnet的plloing操作宏. 
*/

#include <dmlc/logging.h> // mxnet的日志头文件. 在dmlc-core/include/dmlc下,.
#include <dmlc/parameter.h> // mxnet的参数头文件, 在dmlc-core/include/dmlc下, 定义参数的. 
#include <mxnet/operator.h> // 在include/mxnet下, 定义操作基类(operator), 操作属性类, 方法等. 对OP或Prop的函数进行声明.
#include <algorithm> // c++算法库. 
#include <map> // 关联式容器, 元素的值与某个特定的键相关联, 而并非通过元素在数组中的位置类获取.
#include <vector> // c++向量容器. 
#include <string> // c++字符串 
#include <utility> // utility头文件定义重载的关系运算符, 简化关系运算符的写入, 还定义了pair类型,
// pair类型是一种模板类型, 可以存储一对值.
#include "./operator_common.h" // src/operator下, mxnet的层一些常用的属性.

namespace mxnet {
namespace op {

namespace pool_enum { // 定义pooling操作的输出, 输出等时, 命名空间定义为pool_enum. 
enum PoolingOpInputs {kData}; // pooling操作的输入数据kData, 为0. 
enum PoolingOpOutputs {kOut}; // pooling操作的输出kOut, 为0. 
enum PoolingOpType {kMaxPooling, kAvgPooling, kSumPooling}; // pooling操作的类型, mxnet的pooling操作使用三种类型:
/*
kMaxPooling: 最大池化, 为0.
kAvgPooling: 平均池化, 为1.
kSumPooling: 求和池化, 为2. 
*/ 
enum PoolingOpPadConventionType {kValid, kFull}; // 因为pooling操作其实也是一种卷积操作, 因此设定卷积操作的类型.
/*卷积操作类型, 这和MATLAB的卷积函数conv和conv2是类似的, conv做向量间的卷积, 即一维卷积; conv2做二维卷积, 即矩阵卷积. 
w = conv(A, B,'shape')回卷积的一部分. 这一部分由shape参数指定:
full 返回全部卷积值(缺省). kFull,  为1.
same 返回卷积的中心部分, 与A有相同的大小. 
valid 仅返回卷积中的那些被计算而没有补零的部分, 不补零.. kValid, 为0. 

卷积的计算步骤:
(1)卷积核绕自己的核心元素顺时针旋转180度, 是顺时针旋转180, 不是做转置! 
(2)移动卷积核的中心元素,使它位于输入图像待处理像素的正上方. 根据shape的不同会选择不同的卷积方式.
(3)在旋转后的卷积核中,将输入图像的像素值作为权重相乘. 
(4)第三步各结果的和做为该输入像素对应的输出像素.

A=[1 2 3;4 5 6;7 8 9];
B=[1 2;3 4];

conv2(A, B, 'full'), 是做全卷积, 首先对B进行180度顺时针旋转, 然后对A进行补0操作. 补零的时候在A的外围进行补零. 
补零的行数 = 2 *(Nh - 1), 补零的列数 = 2 * (Nw - 1). 卷积核B大小为Nh * Nw. 
操作时的A为      操作时B为 . 然后再对应元素相乘在相加即是卷积后的结果. 
0 0 0 0 0        4 3       
0 1 2 3 0        2 1 
0 4 5 6 0
0 7 8 9 0 
0 0 0 0 0

conv2(A, B, 'valid'), 返回卷积计算中没有补零部分的计算结果, 即是CNN中的卷积操作, 让卷积核对齐A即可. 不补零. 

conv2(A,B, 'same'), 返回和A同样大小的卷积后的结果, 利用full操作可以得到same操作的结果. 不在左上 

*/ 
}  // namespace pool_enum

/*
卷积输出的特征图的大小, 输出特征图尺寸 = [(输入特征图尺寸 + 2 * pad - 卷积核尺寸)/步长](向下取整) + 1. 
在卷积层的操作时, 还有膨胀卷积的时, 而pooling操作就是一个普通的普通的卷积. 没有膨胀系数. 

一般池化由于每一池化窗口都是不重复的, 所以stride = size(kernel). 另外还有重叠池化, 即相邻的池化窗口间是有重叠的.还有金字塔
池化SPP.  
*/

struct PoolingParam : public dmlc::Parameter<PoolingParam> { // pooling操作的参数类PoolingParam. 包括参数的描述以及初值等. 
  TShape kernel; // 池化窗口大小(卷积核), TShape: 一个shape类, 该类可以表示一个Tensor的形状. 利用TShape来表示Tensor的大小.  
  TShape stride; // 池化的移动步长.  
  TShape pad; // 原始数据的高度或宽度方向上补上0值的圈数. 
  int pool_type; // pooling的类型, 是int型的变量pool_type. 即0, 1, 2: kMaxPooling, kAvgPooling, kSumPooling. 
  int pooling_convention; // 做pooling操作的时候卷积的类型, 0, 1: kValid, kFull.
  bool global_pool; // bool类型的变量global_pool, 是否做全局池化. 
  DMLC_DECLARE_PARAMETER(PoolingParam) { // set_default函数设置参数的默认值; describe函数对参数进行描述. 
    DMLC_DECLARE_FIELD(global_pool).set_default(false) // global_pool默认值是false. 
    .describe("Ignore kernel size, do global pooling based on current input feature map. "
              "This is useful for input with different shape"); // 不管池化窗口的大小, 根据输入特征图做全局pooling, 这对输入
              // 特征图的大小不一样的情况下式有用的. global_pool先理解为全卷积.

    DMLC_DECLARE_FIELD(kernel)
    .enforce_nonzero()
    /*
    inline FieldEntry<mxnet::TShape> &enforce_nonzero() {
       this->enforce_nonzero_ = true;
       return this->self();
    }
    bool enforce_nonzero_;
    强制设为不为0. 
    */
    .describe("pooling kernel size: (y, x) or (d, y, x)"); // pooling卷积核(池化窗口的大小). 卷积也分二维卷积核三维卷积, 和
    // convolution是类似的. 根据输入特征图的第三个维度来定. 

    DMLC_DECLARE_FIELD(pool_type)
    .add_enum("max", pool_enum::kMaxPooling)
    .add_enum("avg", pool_enum::kAvgPooling)
    .add_enum("sum", pool_enum::kSumPooling)
    .describe("Pooling type to be applied."); // 使用kMaxPooling等参数时, 用pool_enum命名空间限定其区域即可. 
    /*
    利用函数add_enum(const std::string &key, int value)来为pool_type添加参数. 添加一对键值key和value. 指定pooling的类型. 
    */

    DMLC_DECLARE_FIELD(pooling_convention).set_default(pool_enum::kValid)
    .add_enum("full", pool_enum::kFull)
    .add_enum("valid", pool_enum::kValid)
    .describe("Pooling convention to be applied."
              "kValid is default setting of Mxnet and rounds down the output pooling size."
              "kFull is compatible with Caffe and rounds up the output pooling size.");
    /*
    利用函数add_enum(const std::string &key, int value)来为pooling_convention添加参数. 指定pooling操作中卷积的类型, 默认是
    pool_enum::kValid, 即将卷积核对齐特征图后, 在对应位置元素相乘再相加, 不补零. 
    */

    int stride_shape[] = {1, 1}; // 定义一个一维数组stride_shape, 初值为[1, 1]. 然后再设置stride时将数组再转化为TShape类型. 
    DMLC_DECLARE_FIELD(stride).set_default(TShape(stride_shape, stride_shape + 2))
    .enforce_nonzero() // 强制不为0. 
    .describe("stride: for pooling (y, x) or (d, y, x)");
    /*
    pooling-inl.h中新版的mxnet对stride的初值进行了改变, 从原来的:
    int stride_shape[] = {1, 1}; 
    DMLC_DECLARE_FIELD(stride).set_default(TShape(stride_shape, stride_shape + 2))
    改为:
    DMLC_DECLARE_FIELD(stride).set_default(TShape())

    在c++中数组名是一个常量指针, 它指向数组的开头, 数组名加2表示把指针向下移两个单位. 如同一个指针加一个常数, 
    *(数组名+i)才等于数组名(i). 

    TShape定义: http://mxnet.io/doxygen/classmxnet_1_1TShape.html. TShape(stride_shape, stride_shape + 2)用法具体见TShape的
    构造函数:
    template<typename RandomAccessIterator >
    mxnet::TShape::TShape(RandomAccessIterator begin, RandomAccessIterator end)
    构造Tuple, Tuple是一个动态大小的数据结构, 可以存储少量元素类型相同的数据. RandomAccessIterator是一个typename.    

    池化窗口(卷积核)移动步长, 设置默认值是TShape(stride_shape, stride_shape + 2), 即是一个Tuple. 对于TShape的数据, 可以正常
    输出.

    TShape stride = TShape(); 
    std::cout<<"stride: "<<stride<<std::endl; // stride是(), 即为空的. 
    int stride_shape[] = {1, 1};
    TShape a = TShape(stride_shape, stride_shape + 2);
    std::cout<<"a: "<<a<<std::endl; // a是(1, 1).  
    */

    int pad_shape[] = {0, 0};
    DMLC_DECLARE_FIELD(pad).set_default(TShape(pad_shape, pad_shape + 2))
    .describe("pad for pooling: (y, x) or (d, y, x)"); // pad是补零的圈数, 也有二维和三维的区别, 看输入特征图是二维的还是三
    // 维的. 
    /*
    pooling-inl.h中新版的mxnet对pad的初值进行了改变, 从原来的:
    int pad_shape[] = {1, 1}; 
    DMLC_DECLARE_FIELD(stride).set_default(TShape(pad_shape, pad_shape + 2))
    改为:
    DMLC_DECLARE_FIELD(pad).set_default(TShape())
    */
  }
};

/*
* [pool](#pool): do pooling on image
* [unpool](#unpool): get gradient of pooling result
* [crop](#crop): crop the original image to a smaller size

使用sublime的全局查找功能, 来寻找pool函数和unpool函数的定义. 在本机上用sublime打开pooling-inl.h, 然后再使用sublime的全局查找
功能即可查询到函数的定义. 

以后寻找函数的定义, 可以参考 mxnet 官方文档以及使用sublime的全局查找功能实现. 
*/

template<typename xpu, typename Reducer, typename DType> // 模板类定义时, 一般都有xpu(cpu or gpu)和Dtype(float). pooling操作
// 这加了一个Reducer, 池化类型. 
class PoolingOp : public Operator {
 public:
  explicit PoolingOp(PoolingParam p) {
    this->param_ = p; // explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的.
    // p是PoolingParam卷积层参数类的对象, 将p赋值给param_. 这和单纯的赋值不一样, param_就是p, 可以看做是指向p的指针.
    // 利用param_来访问PoolingParam类的成员, 如kerne, stride等参数. 
    // PoolingParam param_;
  }

  virtual void Forward(const OpContext &ctx,
                       const std::vector<TBlob> &in_data,
                       const std::vector<OpReqType> &req,
                       const std::vector<TBlob> &out_data,
                       const std::vector<TBlob> &aux_args) {
    /*前向操作, 虚函数. 函数的实现在类中定义. 不需要返回值. 本层为第 l 层. 
    in_data: 本层输入data. pooling操作层是没有权重和偏置的, 根据以往的池化层的操作, max池化就是取池化窗口对应的特征图中的元
    素最大值; avg就是取元素的平均值; sum就是求和. 所以并不需要权重wmat和偏置bias. 
    req: 数据操作模式. 
    out_data: 本层输出, out. 
    */
    using namespace mshadow;
    using namespace mshadow::expr;

    CHECK_EQ(in_data.size(), 1);
    CHECK_EQ(out_data.size(), 1);
    /*
    判断输入和输出的TBlob个数, 容器大小应该是1. 即pooling操作的in_data只有kData, out_data只有kOut. 所以容器大小为1, 若不为1,
    断言.  
    */

    Stream<xpu> *s = ctx.get_stream<xpu>();
    if (param_.kernel.ndim() == 3) {
      LOG(FATAL) << "3D kernel not implemented";
    } // 3维的卷积也是没有实现的. 就像caffe的数据结构blob一样, blob是四维的结构, 单个数据是二维的, 因此做不了三维的. 

    Tensor<xpu, 4, DType> data = in_data[pool_enum::kData].get<xpu, 4, DType>(s);
    Tensor<xpu, 4, DType> out = out_data[pool_enum::kOut].get<xpu, 4, DType>(s);
    /*引用kData个kOut需要指定命名空间, 指定其定义域. 
    将本层(第l层)的输入数据in_data[kData], out_data[pool_enum::kOut拉成4维的张量. 这使用get函数:
    mshadow::Tensor<Device, dim, DType> mxnet::TBlob::get(mshadow::Stream<Device> *stream = NULL)const. 4维的张量, 这里就和
    blob比较类似了, 即number N x channel K x height H x width W, (N, K, H, W). Blob memory is row-major in layout. 
    */

    mshadow::Shape<2> out_shape = Shape2(out.shape_[2], out.shape_[3]);
    /*
    定义一个2为的shape out_shape, Shape2定义如下:
    MSHADOW_XINLINE Shape<2> Shape2(index_t s0, index_t s1) {
        Shape<2> s;
        s[0] = s0; s[1] = s1;
        return s;
    } 
    这里对out_shape赋值, 采用的是out.shape_[2], out.shape_[3], 即本层(第l层)输入数据的高度H和宽度W. out_shape即本层输出数据的
    大小. 
    out.shape_[0]: number N 
    out.shape_[1]: channel K 
    out.shape_[2]: height H 
    out.shape_[3]: width W 
    */

    if (param_.pool_type == pool_enum::kMaxPooling || param_.pool_type == pool_enum::kSumPooling) { // param_.pool_type访问
    // 池化的类型. 最大池化和求和池化: 
      Assign(out,
             req[pool_enum::kOut],
             pool<Reducer>(pad(data, param_.pad[0], param_.pad[1]),
                           out_shape,
                           param_.global_pool ? data.shape_[2] : param_.kernel[0],
                           param_.global_pool ? data.shape_[3] : param_.kernel[1],
                           param_.global_pool ? 1 : param_.stride[0],
                           param_.global_pool ? 1 : param_.stride[1]));
    /*
    赋值操作. pooling输入是data, 输出是out, 给out赋值.
    Assign操作定义在include/mxnet/operator_util.h下, 是定义的一个宏函数. 根据需求将exp的值赋给out. 
    这不是C++的字符串赋值函数assign. 
    #define ASSIGN_DISPATCH(out, req, exp)  \
    {                                     \
      switch (req) {                      \
        case kNullOp:                     \
          break;                          \
        case kWriteTo:                    \
        case kWriteInplace:               \
          (out) = (exp);                  \
          break;                          \
        case kAddTo:                      \
          (out) += (exp);                 \
          break;                          \
        default:                          \
          LOG(FATAL) << "not reached";    \
      }                                   \
    } 

    pool_enum::kOut就是获取一种枚举类型(找到OpReqType类型的那个索引), 那么req[pool_enum::kOut]即OpReqType中的kWriteInplace或
    kAddTo, 然后通过exp给out赋值.

    Assign的赋值操作中exp(计算结果)为 pool<Reducer>(...). 在mshadow/doc/README.md下有 pool 和 unpool 的一些介绍:
    pool<Reducer>(Expr<xpu, dim> img, [Shape<2> pshape,] int ksize_y, int ksize_x, int kstride) 给定池化窗口的大小和滑动步长
    做池化操作. Reducer是一个输入参数, operation can be max or sum. 

    pool函数的具体定义在mshadow/mshadow/extension/spatial_pool.h下, unpool定义在mshadow/mshadow/extension/spatial_unpool.h下.
    241行使用的pool函数, 其第二个参数是out_shape, 类型是Shape<2>, 因此241行使用的pool函数定义如下: 定义在 mshadow::expr 命名
    空间下, 因此要using namespace mshadow::expr; 

    template<typename Reducer, typename SrcExp, typename DType, int etype>
    pool(const Exp<SrcExp, DType, etype> &src, Shape<2> pshape,
     index_t ksize_y, index_t ksize_x, index_t kstride_y, index_t kstride_x)
    src是源图像; pshape是经pooling后输出特征图的shape, 类型是Shape<2>; ksize_y核在y方向的大小(高); ksize_x核宽度; 
    kstride_y步长高度; kstride_x步长宽度. 返回池化后的结果. 但是241行使用pool操作的时候, src是利用pad函数定义的:

    pad也是定义下mshadow::expr命名空间下, 因此要using namespace mshadow::expr; 定义见mshadow/mshadow/extension/pad.h. 由于241
    行使用的pad有三个参数, 因此:
    template<typename SrcExp, typename DType, int etype>
    pad(const Exp<SrcExp, DType, etype> &src, index_t pad_y, index_t pad_x)
    对一张图片进行补零操作, 在图片的四周补零. src原图像; pad_y padding size in y, 即在y方向上补零的行数; pad_x 
    padding size in x, 在x方向上补零的列数. 返回补零的结果, 即返回补完零之后的矩阵. 

    pad(data, param_.pad[0], param_.pad[1])即对Tensor<xpu, 4, DType>的data进行补零操作. 返回的是补完零后的矩阵.
    out_shape是输出特征图的高度和宽度, 类型是Shape<2>.
    param_.global_pool ? data.shape_[2] : param_.kernel[0], 是否做全局pool(global_pool是否为真, global_pool默认为假), 因此
    ksize_y是param_.kernel[0], 即核的高度; global_pool 为真, ksize_y为data.shape_[2], 即是样本矩阵的高度.  
    param_.global_pool ? data.shape_[3] : param_.kernel[1], global_pool默认为假, 因此ksize_x为param_.kernel[1], 即和的宽度.
    global_pool 为真, ksize_x为data.shape_[3], 即是样本矩阵的宽度. 
    param_.global_pool ? 1 : param_.stride[0], global_pool默认为假, 因此kstride_y为param_.stride[0], 滑动步长的高度; 
    global_pool 为真, 此kstride_y为1. 
    param_.global_pool ? 1 : param_.stride[1], global_pool默认为假, 因此kstride_x为param_.stride[1], 滑动步长的宽度; 
    global_pool 为真, 此kstride_x为1.   

    池化的类型. 最大池化和求和池化: 
    */

    } else if (param_.pool_type == pool_enum::kAvgPooling) { // 池化类型: 平均池化. 
      Assign(out,
             req[pool_enum::kOut],
             scalar<DType>(1.0f / (param_.global_pool ?
                      data.shape_[2] * data.shape_[3] :
                      param_.kernel[0] * param_.kernel[1])) *  // C++中 \ 是为了书写方便时使用的, 换行. 有 \ 会出错. 
             pool<Reducer>(pad(data, param_.pad[0], param_.pad[1]),
                           out_shape,
                           param_.global_pool ? data.shape_[2] : param_.kernel[0],
                           param_.global_pool ? data.shape_[3] : param_.kernel[1],
                           param_.global_pool ? 1 : param_.stride[0],
                           param_.global_pool ? 1 : param_.stride[1]));
    /*
    Assign赋值操作, 将exp的值通过数据操作模式req传递给输出out. 做平均池化和最大池化, 求和池化的exp不同, 平均池化对pool(....)
    的结果利用scalar进行伸缩了. 

    scalar<DType>见mshadow/mshadow/expression.h:
    template<typename DType>
    inline ScalarExp<DType> scalar(DType s) 建立一个标量(scalar)表达式. DType是data的类型, 如float; s是一个value. 这里s是一
    个表达式:
    1.0f / (param_.global_pool ? data.shape_[2] * data.shape_[3] : param_.kernel[0] * param_.kernel[1])
    global_pool默认为假, 因此(..)为param_.kernel[0] * param_.kernel[1], 即核的尺寸乘积; global_pool 为真, (..)为
    data.shape_[2] * data.shape_[3], 即样本矩阵的尺寸. 然后再执行 1.0f / (...) 就是s值.   

    Reducer是一个输入参数, 这里是avg. 
    */
    }
  }

  virtual void Backward(const OpContext &ctx,
                        const std::vector<TBlob> &out_grad,
                        const std::vector<TBlob> &in_data,
                        const std::vector<TBlob> &out_data,
                        const std::vector<OpReqType> &req,
                        const std::vector<TBlob> &in_grad,
                        const std::vector<TBlob> &aux_args) {
    /*池化层(第l层)没有权重和偏置, 因此要计算的是损失J关在池化层(第l层)的残差.
    !!!!!!!!!!!!!!!!梯度可以看做是损失J关于层参数的导数, 残差可以看做是损失J关于层输入的导数!!!!!!!!!!!!!!!!!!!!!!!!!!!! 

    in_grad输出残差参数, 向量容器, 每个元素的类型是TBlob. 本层(第l层)的.
    out_grad输入残差参数, 向量容器, 每个元素的类型是TBlob. 上一层(第l + 1层)的残差, 计算本层的残差. 
    in_data输入参数, 向量容器, 每个元素的类型是TBlob. 本层(第l层)的输入.  
    out_data输出参数, 向量容器, 每个元素的类型是TBlob. 本层(第l层)的输出.  
    req: 数据操作模式, 向量数组. 元素类型是OpReqType.
    */

    using namespace mshadow;
    using namespace mshadow::expr;

    CHECK_EQ(out_grad.size(), 1); // 上一层(第l + 1层)反向传播中只有损失的残差sigma^(l + 1). 若不然, 断言. 
    CHECK_EQ(in_data.size(), 1); // 本层(第l层)的输入只有数据. 
    CHECK_EQ(out_data.size(), 1); // 本层(第l + 1层)的输出只有数据. 
    CHECK_EQ(req.size(), 1); // 数据操作模式只有一种.  
    CHECK_EQ(in_grad.size(), 1); // 本层(第l层)反向传播中只有残差. 

    // TODO(bing): remove pad (0,0)
    if (param_.kernel.ndim() == 3) {
      LOG(FATAL) << "3D kernel not implemented";
    } // 核的维数只能是2维的. 
    Stream<xpu> *s = ctx.get_stream<xpu>();

    // pool_enum::kOut为0; pool_enum::kData为0. 
    Tensor<xpu, 4, DType> grad = out_grad[pool_enum::kOut].get<xpu, 4, DType>(s);
    // 将第l + 1层的残差out_grad[0]利用get函数拉成四维的张量. 即残差和数据是一样的, 是4维的. 
    Tensor<xpu, 4, DType> data = in_data[pool_enum::kData].get<xpu, 4, DType>(s);
    // 将第l层的输入in_data[0]利用get函数拉成4维的张量. 
    Tensor<xpu, 4, DType> output_data = out_data[pool_enum::kOut].get<xpu, 4, DType>(s);
    // 将第l层的输出利用get函数拉成4维的张量. 
    Tensor<xpu, 4, DType> input_grad = in_grad[pool_enum::kData].get<xpu, 4, DType>(s);
    // 定义本层(第l层)的残差是4维的张量. 

    mshadow::Shape<2> in_shape = Shape2(data.shape_[2], data.shape_[3]); // 定义in_shape, 类型是Shape<2>. 是第l层的输入的样本
    // 矩阵大大小. data.shape_[2]为高度, data.shape_[1]为宽度. 反向传播时, 要将本层的残差reshape成和本层的输入一样大小. 即:
    // 本层的 残差 和 本层的输入 的shape是一致的. 

    if (param_.pool_type == pool_enum::kMaxPooling || param_.pool_type == pool_enum::kSumPooling) { // 最大池化和求和池化. 
      Assign(input_grad, req[pool_enum::kData],
             crop(unpool<Reducer>(pad(data, param_.pad[0], param_.pad[1]),
                                  pad(output_data, 0, 0),
                                  pad(grad, 0, 0),
                                  param_.global_pool ? in_shape[0] : param_.kernel[0],
                                  param_.global_pool ? in_shape[1] : param_.kernel[1],
                                  param_.global_pool ? 1 : param_.stride[0],
                                  param_.global_pool ? 1 : param_.stride[1]),
                  in_shape,
                  param_.pad[0],
                  param_.pad[1]));
    /*
    Assign, 赋值操作. 通过req数据操作模式(req[0]), 将exp的值赋给input_grad, 即本层的残差. 其中exp是:
    crop(*, in_shape, param_.pad[0], param_.pad[1]). * 是unpool<Reducer>(...).

    crop函数定义见: mshadow/mshadow/extension/crop.h. 定义在mshadow::expr命名空间下, 因此要using namespace mshadow::expr;
    crop函数也有两种定义, 由于383行的crop有四个输入参数, 因此crop定义如下:
    template<typename SrcExp, typename DType, int etype>
    crop(const Exp<SrcExp, DType, etype> &src, Shape<2> oshape, index_t start_height, index_t start_width){....} 
    src是源图像; 
    oshape是裁剪后的数据的大小, in_shape; 
    start_height裁剪起始高度值; 
    start_width裁剪起始宽度值; 
    返回裁剪后的数据
    383行使用 crop(*, in_shape, param_.pad[0], param_.pad[1]). * 是对上一层的残差进行上采样得到的结果; in_shape即oshape, 是本
    输入的大小; 裁剪的起始位置是(param_.pad[0], param_.pad[1]). 

    * 是对上一层的残差进行上采样得到的结果. 是通过使用 unpool 函数对上一层的残差进行上采样得到, 具体过程如下:
    1> 最大池化:
    由于一般情况下是一般池化, 即 stride = kerne. 例如, kernel = (2, 2). 在反向传播时, 只有那个最大值对下一层有贡献,
    所以将残差传递到该最大值的位置, 区域内其他2*2-1=3个位置置零. 即

    1       2       反向传播        0 0 0 0
                   ---------->      0 1 2 0            
    3       4                       0 3 4 0
      残差                          0 0 0 0

    2>平均池化:
    如, kernel = (2, 2). 我们需要把残差平均分成2*2=4份, 传递到前边小区域的4个单元即可. 即:

    1       2       反向传播        1/4 1/4 1/2 1/2 
                   ---------->      1/4 1/4 1/2 1/2            
    3       4                       1/3 1/3 1   1  
      残差                          1/3 1/3 1   1 

    unpool函数定义如下:
    template<typename Reducer, typename SrcExp, typename DType, int etype>
    unpool(const Exp<SrcExp, DType, etype> &data_src,
       const Exp<SrcExp, DType, etype> &data_pooled,
       const Exp<SrcExp, DType, etype> &grad_pooled,
       index_t ksize_y, index_t ksize_x, index_t kstride_y, index_t kstride_x). 对四维数据进行上采样, 获得池化层的参禅.
    data_src为本层(第l层)的输入, 即池化层的输入, 利用pad(...)函数补零的数据;
    data_pooled是本层(第l层)的输出, 即池化操作后的特征图, 利用pad(...)函数补零的数据; 
    grad_pooled为上一层(第l + 1层)的残差, 利用pad(...)函数补零的数据;
    ksize_y核的高度, global_pool默认为假, 因此ksize_y是param_.kernel[0], 即核的高度; global_pool 为真, ksize_y为data.shape_[2],
    即是样本矩阵的高度; 
    ksize_x核的宽度, global_pool默认为假, 因此ksize_x为param_.kernel[1], 即和的宽度. global_pool 为真, ksize_x为data.shape_[3],
    即是样本矩阵的宽度. 
    kstride_y为y方向上的滑动步长, global_pool默认为假, 因此kstride_y为param_.stride[0], 滑动步长的高度; global_pool 为真, 
    此kstride_y为1. 
    kstride_x为x方向上的滑动步长, global_pool默认为假, 因此kstride_x为param_.stride[1], 滑动步长的宽度; global_pool 为真, 
    此kstride_x为1.  
    */

    } else if (param_.pool_type == pool_enum::kAvgPooling) { // 平均池化. 
      Assign(input_grad, req[pool_enum::kData],
             scalar<DType>(1.0f / (param_.global_pool ?
                      data.shape_[2] * data.shape_[3] :
                      param_.kernel[0] * param_.kernel[1])) * \
             crop(unpool<Reducer>(pad(data, param_.pad[0], param_.pad[1]),
                                  pad(output_data, 0, 0),
                                  pad(grad, 0, 0),
                                  param_.global_pool ? in_shape[0] : param_.kernel[0],
                                  param_.global_pool ? in_shape[1] : param_.kernel[1],
                                  param_.global_pool ? 1 : param_.stride[0],
                                  param_.global_pool ? 1 : param_.stride[1]),
                  in_shape,
                  param_.pad[0],
                  param_.pad[1]));
    /*
    这和前向的处理是一样的, 将crop(...)的结果伸缩, 乘上一个标量表达式scalar<DType>(DType s).

    1.0f / (param_.global_pool ? data.shape_[2] * data.shape_[3] : param_.kernel[0] * param_.kernel[1])
    global_pool默认为假, 因此(..)为param_.kernel[0] * param_.kernel[1], 即核的尺寸乘积; global_pool 为真, (..)为
    data.shape_[2] * data.shape_[3], 即样本矩阵的尺寸. 然后再执行 1.0f / (...) 就是s值. 
    */  
    }
  }

 private:
  PoolingParam param_;
};  // class PoolingOp

猜你喜欢

转载自blog.csdn.net/qq_20965753/article/details/71641418