Operators in MXNet-Convolution

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

/*!
 * Copyright (c) 2015 by Contributors
 * \file convolution-inl.h
 * \brief
 * \author
*/
#ifndef MXNET_OPERATOR_CONVOLUTION_INL_H_
#define MXNET_OPERATOR_CONVOLUTION_INL_H_ // 宏 

#include <mxnet/io.h> // include/mxnet下, 声明了输入和输出数据结构和数据迭代器. 
#include <mxnet/base.h> // include/mxnet下, 声明mxnet的配置信息以及基本数据结构.
/*如是否支持OpenCV, CUDA, CUDNN, VS以及声明了mxnet命名空间下的变量的定义, 如cpu, gpu, index_t以及Save, Load等. 
*/ 
#include <mxnet/ndarray.h> // include/mxnet下, 
#include <mxnet/operator.h> // 在include/mxnet下, 定义操作基类(operator), 操作属性类, 方法等. 对OP或Prop的函数进行声明. 
#include <dmlc/logging.h> // mxnet的日志头文件. 在dmlc-core/include/dmlc下,
#include <dmlc/optional.h> // 在dmlc-core/include/dmlc下, 声明了class optional.  
#include <algorithm> // 标准C++算法库. 
#include <map> // C++ map容器头文件. 
#include <vector> // 向量容器. 
#include <string> // 字符串. 
#include <utility> // utility头文件定义重载的关系运算符, 简化关系运算符的写入, 还定义了pair类型,
// pair类型是一种模板类型, 可以存储一对值.
#include "./operator_common.h" // src/operator下, mxnet的层一些常用的属性.

#include<iostream>
#include <fstream>
#include <stdio.h>
#include <stdlib.h>

using namespace std;

namespace mxnet {
namespace op {

namespace conv {
enum ConvolutionOpInputs {kData, kWeight, kBias}; // 卷积层的输入索引, 包括前一层的输出数据data, 本层的权重wmat和偏置. 
enum ConvolutionOpOutputs {kOut}; // 卷积层的输出索引. 
enum ConvolutionOpResource {kTempSpace}; // 卷积层的资源配置, 设置一个临时空间, 这个空间可以是任意大小的. 
/*
有些操作需要额外的内存作为工作空间进行计算, 比如说cudnnConvolutionForward. 这种情况下, 
系统最好可以对这部分内存进行管理, 这样系统可以做一些优化, 比如说内存的重复利用.
struct ResourceRequest {
  enum Type {
    kRandom,  // get an mshadow::Random<xpu> object
    kTempSpace,  // request temporay space
  };
  Type type;
};
*/
enum ConvolutionOpCudnnTune {kOff, kLimited, kFastest}; // 利用GPU完成卷积操作时, 有三个索引. 没有使用, 因此convolution-inl.h
// 是cpu版的. 
}

/*
卷积层输出的特征图的大小, 输出特征图尺寸 = [(输入特征图尺寸 + 2 * pad - 卷积核尺寸)/步长](向下取整) + 1.

Matlab中的应用函数-conv2:二维卷积, 一维对应conv. 0.8版本的convolution-inl.h只能做2D卷积, 0.9版本的
convolution-inl.h可以做3D卷积, 而且将2D卷积核3D卷积结合在一起了.

卷积操作类型, 这和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操作的结果. 不在左上 
*/

struct ConvolutionParam : public dmlc::Parameter<ConvolutionParam> { // 卷积层的参数结构ConvolutionParam. 
  TShape kernel; // 卷积核, TShape: 一个shape类, 该类可以表示一个Tensor的形状. 利用TShape来表示Tensor的大小. 
  /*卷积核是二维的(h, w)或三维的(d, h, w). 即实现二维卷积或三维卷积, 对应到图像中可以是灰度图像(2维张量), RGB图像(3维张量).
  这和输入的数据是2维的还是3维的有关. (channel K不一定是图像的通道数了, 或者为前一层特征图的数量.) 
  TShape定义: http://mxnet.io/doxygen/classmxnet_1_1TShape.html*/  
  TShape stride; // 卷积核滑动步长, 二维的(h, w)或三维的(d, h, w).  
  TShape dilate;
  /*
  卷积核膨胀是将卷积核扩张到膨胀尺度约束的尺度中, 并将原卷积核没有占用的区域填充零. 例如:
  1 2 3      1 0 2 0 3
  4 5 6 ---->0 0 0 0 0      
  7 8 9      4 0 5 0 6
             0 0 0 0 0
             7 0 8 0 9
  卷积核由3*3膨胀到了5*5. 膨胀后的卷积核中填充了一些0. 膨胀系数与卷积核膨胀的关系, 首先回到卷积核膨胀公式:
  膨胀的卷积核尺寸 = 膨胀系数 * (原始卷积核尺寸 - 1) + 1.
  由于卷积的操作特性, 卷积核尺寸是奇数, 卷积核的膨胀系数刻画了卷积核高和宽方向的扩张倍数, 
  膨胀系数保证了膨胀的卷积核尺寸为奇数. 
  */
  TShape pad; // 原始数据的高度或宽度方向上补上0值的圈数, 二维的(h, w)或三维的(d, h, w).
  uint32_t num_filter; // 卷积核的个数. 经过卷积后一共有多少个特征图. 
  /*
  uint8_t,uint16_t,uint32_t: 使用typedef给类型起的别名.
  typedef unsigned char uint8_t; 1字节 
  typedef unsigned int uint16_t; 2字节 
  typedef unsigned long uint32_t; 4字节 
  typedef unsigned long long uint64_t; 8字节 
  */
  uint32_t num_group;
  /*
  将输入数据切割成num_group个partitions, 然后在每个partition上使用卷积操作, 再将卷积的结果连接起来. 有点并行的意思. 
  */
  uint64_t workspace; // 卷积操作最大的临时工作空间, 以MB计算. 
  bool no_bias; // 卷积层是否使用偏置, 默认使用偏置. 
  dmlc::optional<int> cudnn_tune;
  /*
  template<typename T>
  class dmlc::optional< T >
  C++17的可选类, 实例化对象cudnn_tune, cudnn_tune描述如下: 
  cudnn_tune决定是否通过测试性能来选择卷积算法, 由于要测试性能, 因此启动时间可能会变长, 但是操作的速度却会变快. 
  cudnn_tune有三个可选参数:
  off: 不调整
  limited_workspace: 在不超过工作区限制的情况下, 运行测试性能, 并选择最快的算法.
  fastest: 忽略工作区限制, 选择最快的算法. 
  */
  bool cudnn_off; // 该卷积层是否使用cudnn, 默认使用. 
  dmlc::optional<int> layout;
  /*
  layout与cudnn_tune类型一致. 设置输入, 输出和权重的layout(布局).  
  */ 
  DMLC_DECLARE_PARAMETER(ConvolutionParam) { // #define DMLC_DECLARE_PARAMETER(PType), 使用宏来描述卷积层的参数. 

    /*
    宏DMLC_DECLARE_PARAMETER, DMLC_DECLARE_FIELD的定义以及函数describe, set_default, set_range, add_enum均在
    #include <dmlc/parameter.h>下. 
    */

    DMLC_DECLARE_FIELD(kernel).describe("convolution kernel size: (h, w) or (d, h, w)");
    /*#define DMLC_DECLARE_FIELD(FieldName)使用宏#define DMLC_DECLARE_FIELD(FieldName)来对卷积层的参数进行描述, 利用函数
    describe(const std::string &description)对参数FieldName进行描述. 卷积核的大小, 二维卷积或三维卷积.*/
    DMLC_DECLARE_FIELD(stride).set_default(TShape())
    .describe("convolution stride: (h, w) or (d, h, w)");
    /*
    卷积核滑动步长, 二维滑动或三维滑动. 利用函数set_default(const DType &default_value)设置参数FieldName的默认值, 默认值是
    TShape(), 即mxnet::TShape::TShape( ), 调用TShape的默然构造函数, NONE; 利用函数describe(const std::string &description)
    对参数FieldName进行描述.  
    */

    /*
    TShape a = TShape();
    cout<<"TShape()[0]: "<<a[0]<<endl; 0
    cout<<"TShape()[1]: "<<a[1]<<endl; 0
    cout<<"TShape()[2]: "<<a[2]<<endl; 0

    即TShape的默然构造函数TShape()构造的 shape对象, 其值均是0. 

    但是stride的默认值是(1, 1), 不可能是(0, 0). dliate和pad的默认值均值1.
    conv1 = mx.symbol.Convolution(data=data, kernel=(5,5), stride=(2,2), num_filter=20), 指定stride=(2, 2).
    conv1 = mx.symbol.Convolution(data=data, kernel=(5,5), num_filter=20), 不指定stride=(1, 1). 
    */

    DMLC_DECLARE_FIELD(dilate).set_default(TShape())
    .describe("convolution dilate: (h, w) or (d, h, w)"); // 膨胀系数.  
    DMLC_DECLARE_FIELD(pad).set_default(TShape())
    .describe("pad for convolution: (h, w) or (d, h, w)"); // 始数据的高度或宽度方向上补上0值的圈数.
    DMLC_DECLARE_FIELD(num_filter).set_range(1, 100000)
    .describe("convolution filter(channel) number");
    /*
    卷积核的个数. 调用函数set_range(DType begin, DType end)设置num_filter的范围, 1-100000. 
    */
    DMLC_DECLARE_FIELD(num_group).set_default(1)
    .describe("Number of group partitions. Equivalent to slicing input into num_group\n    "
              "partitions, apply convolution on each, then concatenate the results"); // 将输入数据切割成num_group个partitions
    // 默然不切割输入. 
    DMLC_DECLARE_FIELD(workspace).set_default(1024).set_range(0, 8192)
    .describe("Maximum tmp workspace allowed for convolution (MB).");
    /*卷积操作最大的临时工作空间, 默认是1024M, 范围是0M - 8192M.*/
    DMLC_DECLARE_FIELD(no_bias).set_default(false)
    .describe("Whether to disable bias parameter."); // 卷积操作是否使用偏置. 
    DMLC_DECLARE_FIELD(cudnn_tune)
    .add_enum("off", conv::kOff)
    .add_enum("limited_workspace", conv::kLimited)
    .add_enum("fastest", conv::kFastest)
    .set_default(dmlc::optional<int>())
    .describe("Whether to pick convolution algo by running performance test.\n    "
              "Leads to higher startup time but may give faster speed. Options are:\n    "
              "\'off\': no tuning\n    "
              "\'limited_workspace\': run test and pick the fastest algorithm "
              "that doesn't exceed workspace limit.\n    "
              "\'fastest\': pick the fastest algorithm and ignore workspace limit.\n    "
              "If set to None (default), behavior is determined by environment\n    "
              "variable MXNET_CUDNN_AUTOTUNE_DEFAULT: 0 for off,\n    "
              "1 for limited workspace (default), 2 for fastest.");
    /*
    利用可选类cudnn_tune来选择卷积算法. 利用函数add_enum(const std::string &key, int value)来为cudnn_tune添加参数. 添加一对
    键值key和value, conv::kOff为0, conv::kLimited为1, conv::kFastest为2. 默认值是dmlc::optional<int>(), 即类optional的默认
    构造函数, 没有值为空.   
    */
    DMLC_DECLARE_FIELD(cudnn_off).set_default(false)
    .describe("Turn off cudnn for this layer."); // 是否使用cudnn, 默认是false. 即使用. 
    DMLC_DECLARE_FIELD(layout)
    .add_enum("NCHW", mshadow::kNCHW)
    .add_enum("NHWC", mshadow::kNHWC)
    .add_enum("NCDHW", mshadow::kNCDHW)
    .add_enum("NDHWC", mshadow::kNDHWC)
    .set_default(dmlc::optional<int>())
    .describe("Set layout for input, output and weight. Empty for\n    "
              "default layout: NCHW for 2d and NCDHW for 3d.");
    /*
    设置输入, 输出和权重的layout(布局), 定义本层输入data, 输出和权重的布局, 因为输入和输出均是4D的数据, 因此要设定在内存存储,
    是以行为主函数列为主. Blob memory is row-major in layout, so the last / rightmost dimension changes fastest. 
    默认2d使用NCHW, 3d使用NCDHW.   kNCHW = 0, kNHWC, kCHWN, kNCDHW = 1 << 5, kNDHWC,
    kCDHWN是枚举变量. 定义在mshadow/mshadow/base.h下.  
    */
  }
};

template<typename xpu, typename DType>
class ConvolutionOp : public Operator { // 卷积操作类, 前向操作和反向操作. 
 public:
  explicit ConvolutionOp(ConvolutionParam p) {
    // explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的. 参数是卷积参数p. 
    this->param_ = p;
    // p是ConvolutionParam卷积层参数类的对象, 将p赋值给param_. 这和单纯的赋值不一样, param_就是p, 可以看做是指向p的指针. 

    // convert MBytes first to Bytes and then to elements.
    param_.workspace = (param_.workspace << 20) / sizeof(DType);
    // param_.workspace赋值, 首先令param_.workspace为20, 再除以sizeof(DType). 例如sizeof(float). 
    CHECK(param_.layout.value() == mshadow::kNCHW ||
          param_.layout.value() == mshadow::kNCDHW)
      << "Only support NCHW and NCDHW layout";
    // 卷积层的layout只支持NCHW and NCDHW, 判断卷积层的layout.value()(layout的值, 返回一个int的数)是否是mshadow::kNCHW或
    // mshadow::kNCDHW. 如果不相等, 输出断言. 
  }

  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, 本层的权重wmat和偏置(卷积层可能有偏置, 可能没有).
    req: 数据操作模式. 
    out_data: 本层输出, out. 
    */
    using namespace mshadow;
    using namespace mshadow::expr;
    CHECK_EQ(req[conv::kOut], kWriteTo);
    /*
    判断卷积层前向传播的数据模式是不是kWriteTo, 如果不是, 断言. 一般情况下, 所有的out_data的类型应该是kWriteTo, 
    表示out_data代表的tensor是可以直接写入的原始的内存块 .  
    */
    size_t expected = param_.no_bias ? 2 : 3;
    /*
    size_t是标准C库中定义的, 应为unsigned int, 在64位系统中为 long unsigned int. 定义expected, param_.no_bias为真, 则expected
    为2, 否则为3. 即expected是用来定义前向传播中的参数的个数的, 是只有kData, KWeight, 还是kData, KWeight,, kBias. 
    */
    CHECK_EQ(in_data.size(), expected); // 检查输出in_data(向量数组)的大小是否和expected相等, 不相等, 断言. 
    CHECK_EQ(out_data.size(), 1); // 前向操作只有一个输出out, 因此前向操作中的out_data的大小应该是1.

    Stream<xpu> *s = ctx.get_stream<xpu>(); // XPU流. CPU或GPU. 
    if (param_.kernel.ndim() > 2) {
      LOG(FATAL) << "Volume convolution is not implmented in mshadow";
    }
    /*
    param_就代表了卷积层的参数实例, 利用param_可以调用卷积层的任意一个参数. kernel.ndim()即TShape对象的维数, mshadow只做了二维
    卷积. convolution-inl.h(0.9版本的)做了3D卷积, 基于NNVM. 

    log的级别:
    error是错误; fatal是致命; silence: 显示最简练的日志信息; verbose:显示最详细的日志信息.
    */
    Tensor<xpu, 4, DType> data = in_data[conv::kData].get<xpu, 4, DType>(s);
    /*
    将本层(第l层)的输入数据in_data[kData]拉成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. 

    这的channel K不一定是图像的通道数了, 或者为前一层特征图的数量.  
    */
    Shape<3> wmat_shape =
        Shape3(param_.num_group,
               param_.num_filter / param_.num_group,
               data.shape_[1] / param_.num_group * param_.kernel[0] * param_.kernel[1]);
    /*
    在https://raw.githubusercontent.com/jdeng/gomxnet/master/mxnet.cc可以找到Shape1, Shape2, Shape3, Shape4的定义:
    Shape3定义如下:
    MSHADOW_XINLINE Shape<3> Shape3(index_t s0, index_t s1, index_t s2) {
        Shape<3> s;
        s[0] = s0; s[1] = s1; s[2] = s2;
        return s;
    }

    定义wmat_shape, 为定义本层(第l层)的权重wmat做准备. 即卷积层权重矩阵的shape. 
    s0 = param_.num_group, 即num_group(输入数据切割成num_group个partitions), 默认为1;
    s1 = param_.num_filter / param_.num_group. num_filter为卷积核的个数, 在使用COnvolution时指定, num_filter的范围, 1-100000.
    s2 = data.shape_[1] / param_.num_group * param_.kernel[0] * param_.kernel[1];
    data.shape_[1]是data shape的第二个分量的值, 即channel K. 这里为卷积层前一层特征图的数量.  
    param_.kernel[0]即kernel[0], 只是使用param_来调用kernel. 在使用卷积层的时候: kernel = (5,5).  
    param_.kernel[1]即kernel[1]. 
    */
    Tensor<xpu, 3, DType> wmat =
        in_data[conv::kWeight].get_with_shape<xpu, 3, DType>(wmat_shape, s);
    /*
    将卷积层权重in_data[conv::kWeight]即in_data[1]拉成3维的张量. 这利用了get_with_shape. 定义如下:
    mshadow::Tensor<Device, dim, DType> mxnet::TBlob::get_with_shape(const mshadow::Shape<dim> & shape, 
    mshadow::Stream< Device > *stream = NULL)const, 其中Shape<dim> & shape 就是wmat_shape. 

    卷积层的权重wmat是3D的张量. 权重的大小是: wmat_shape. 一般的, 卷积层的卷积核(weight)的个数为:
    num_filter * (kernel[0] * kernel[1]). 由于mxnet做卷积时, 用到了 num_group 机制, 因此将wmat设置成3D的, 其中第一个维度是
    num_group的数目. 假设要将卷积层的输入数据data分成num_group个partitions, 那么每个partitions的卷积核个数就是:
    param_.num_filter / param_.num_group个.   
    */
    Tensor<xpu, 4, DType> out = out_data[conv::kOut].get<xpu, 4, DType>(s);
    /*定义本层(第l层)的输出数据out_data[kOut]即out_data[0], 为4维的张量.*/

#if defined(__CUDACC__)
    CHECK_EQ(s->blas_handle_ownership_, Stream<xpu>::OwnHandle)
        << "Must init CuBLAS handle in stream";
#endif // __CUDACC__ // 是否定义了__CUDA__宏. 
    const index_t nbatch = data.size(0);
    /*
    index_t的定义在mshadow/mshadow/base.h下.
    typedef unsigned index_t; 
    unsigned a; 与unsigned int a; 是同样的. 这里省略了int, 只能和unsigned int等价. 

    Tensor<xpu, 4, DType> data; data是4维的张量, 张量的大小是(N, C, H, W), 第一维是数据子集的个数, 可以看做是批训练中一批数据
    个数; C是图像的通道数, 即数据是二维的还是三维的数据(这的channel K不一定是图像的通道数了, 或者为前一层特征图的数量. ); 
    H是数据的高度; W是数据的宽度. 因此, data.size(0)即N, data.size(1)即C, data.size(2)即H, data.size(3)即W. 
    nbatch是索引类型(unsigned int)的变量, 代表了批量大小. 
    */

    Tensor<xpu, 1, DType> workspace =
        ctx.requested[conv::kTempSpace].get_space_typed<xpu, 1, DType>(
            Shape1(this->InitTemp(data.shape_, out.shape_)), s);
    /*
    get requested temp space. 获取所需的临时空间. 
    有些操作需要额外的内存作为工作空间进行计算, 比如说cudnnConvolutionForward. 这种情况下, 系统最好可以对这部分内存进行管理, 
    这样系统可以做一些优化, 比如说内存的重复利用. 因此BN有kTempSpace. 即BN的反向操作会申请一个临时的资源空间, 这个空间任意. 
    */            
    /*
    workspace是一维的张量(向量). 
    OpContext: 结构体, 定义在include/mxnet/operator.h中, 该结构体可以记录操作在前向和后向传播中的信息. ctx是结构体OpContext定
    义的对象, requested是OPContext结构体下的函数:
    // brief Resources requested by the operator
    std::vector<Resource> requested; // 用来返回卷积操作所需的资源. 
    ctx.requested返回的是一个向量容器, 我们需要的只是kTempSpace即卷积层的资源配置, 即一个临时的操作空间. 
    ctx.requested[conv::kTempSpace]就是一个Resource的对象, 再调用get_space_typed函数, 将这个卷积的操作空间拉成一个一维的张量.

    Resource结构体定义: http://mxnet.io/doxygen/structmxnet_1_1Resource.html. 定义了mxnet操作所需的资源.
    get_space_typed函数:
    mshadow::Tensor<xpu, ndim, DType> mxnet::Resource::get_space_typed (mshadow::Shape<ndim> shape, mshadow::Stream<xpu>* 
    stream )const 返回指定类型的Tensor. 
    其中, shape是返回的Tensor的Shape, stream是所需的流. 

    MSHADOW_XINLINE Shape<1> Shape1(index_t s0) {
        Shape<1> s; s[0] = s0;
        return s;
    } 

    这里第一个变量的定义用到了InitTemp函数, 该函数也是定义在类COnvolutionOP下:
    inline index_t InitTemp(const mshadow::Shape<4> &ishape, const mshadow::Shape<4> &oshape) {...} 
    data.shape_, out.shape_ 返回输入和输出的shape, 由于data和out均是4维的张量, 所以data.shape_, out.shape_均是4维的. 类型是
    Shape<4>, InitTemp函数返回值类型是index_t, 正和Shape1的参数类型一致. Shape1(this->InitTemp(data.shape_, out.shape_))返回
    的就是Tensor的shape. 
    */        

    for (index_t i = 0; i < nbatch; i += nstep_) {
      const index_t step = std::min(nstep_, nbatch - i);

      // cout<<"step: "<<step<<endl;  // 64, ji batch_size. 
      /*
      step是索引类型(unsigned int), index_t nstep_ 即nstep_是类ConvolutionOp下的一个数据成员, 在运行时会指定nsetp_的大小. 
      nstep_是batch_size. 因此for循环只执行一次, 即 i == 0.  
      step取nstep_ 和 nbatch_ - i 的最小值. 由于nstep_是batch_size, 因此step为batch_size. 
      */

      Tensor<xpu, 2, DType> temp_col = Tensor<xpu, 2, DType>(workspace.dptr_,
                                               Shape2(shape_colunit_[0],
                                                      shape_colunit_[1] * step), s);
      Tensor<xpu, 3, DType> temp_dst = Tensor<xpu, 3, DType>(
                                               workspace.dptr_ + temp_col.shape_.Size(),
                                               Shape3(shape_dstunit_[0],
                                                      shape_dstunit_[1],
                                                      shape_dstunit_[2] * step), s);
      /*
      lenet网执行时的特征图大小:
      28*28 --> 12*12(conv1) --> 6*6(pool1) --> 2*2(conv2) --> 1*1(pool2) -->... 
      */
      /*
      这三个量shape_colunit_, shape_dstunit_, nstep_. 
      是在运行时指定的, 是在ConvolutionOp类的私有函数InitTemp中指定的, 最新版本的convolution-inl.h中也有一个私有函数
      LayerSetUp, 用来指定一些值. 根据ishape(本层输入shape)和oshape(本层输出shape)即, data.shape_, out.shape_
      可以确定OP中用到的一些变量的值. 
      */

      // cout<<"forward_nstep_: "<<nstep_<<endl; // nstep_是batch_size.  
      /*cout<<"shape_colunit_[0]: "<<shape_colunit_[0]<<endl; 
      // conv1 = mx.symbol.Convolution(data=data, kernel=(5,5), stride=(2,2), num_filter=10)时, 为25. 
      // shape_colunit_[0]是: 卷积层前一层特征图个数 * kernel[0] * kernel[1]. ishape[1] * ksize_y * ksize_x.
      cout<<"shape_colunit_[1]: "<<shape_colunit_[1]<<endl;
      // 为144, 4. 是 卷积层输出特征图的高度 * 宽度 之积. oshape[2] * oshape[3].
      cout<<"shape_dstunit_[0]: "<<shape_dstunit_[0]<<endl;
      //  shape_dstunit_[0]是1. param_.num_group.  
      cout<<"shape_dstunit_[1]: "<<shape_dstunit_[1]<<endl;
      // shape_dstunit_[1]是卷积层卷积核的个数. 为10, 50. param_.num_filter / param_.num_group.
      cout<<"shape_dstunit_[2]: "<<shape_dstunit_[2]<<endl;
      // 是 卷积层输出特征图的高度 * 宽度 之积.  oshape[2] * oshape[3].*/ 

      /*
      这是利用了Tensor类的构造函数来创建xpu, ndim == 2, 3, DType下的对象temp_col和temp_dst. 需要Tensor类(结构体)的定义, 见:
      mshadow/mshadow/tensor.h 363行. 
      根据375和378的Tensor构造函数的形式, 确定375和378所使用的构造函数为:
      template<typename Device, int dimension, typename DType MSHADOW_DEFAULT_DTYPE>
      MSHADOW_XINLINE Tensor(DType *dptr, const Shape<dimension> &shape, Stream<Device> *stream) : dptr_(dptr), shape_(shape),
      stride_(shape[kSubdim]), stream_(stream) {}. 依据data pointer and shape, without stride来构造Tensor的对象. 
      Device会在make mxnet的指定, cpu或gpu. dimension会在定义卷积层操作的时候指定, 如2, 3. 
      DType *dptr: 即float* 型的指针dptr;
      Shape<dimension> &shape: 是一个shape, 可用Shape2或者Shape3定义(Shape2和Shape3定义如下). 即定义的Tensor对象的shape.  
      Stream<Device> *stream: 是Stream对象. 

      *dptr传入的实参一个是workspace.dptr_, 一个是workspace.dptr_ + temp_col.shape_.Size().
      workspace定义: Tensor<xpu, 1, DType> workspace, 即是1维Tensor的对象, 引用 struct Tensor<Device, 1, DType> 结构体下的
      成员: DType *dptr_; 514行. 是指向数据的指针. 
      temp_col.shape_.Size()是 temp_col.shape_各个分量的乘积, 即 shape_colunit_[0] * shape_colunit_[1] * step, 是一个数. 

      用了Tensor类的构造函数来创建 xpu, ndim == 2, 3, DType下的Tensor对象temp_col和temp_dst, 在创建的时候会指定
      temp_col和temp_dst的shape. temp_col和temp_dst是两个临时的Tensor, 利用tmp_col和tmp_dst来计算最终卷积层的输出Tensor
      Tensor<xpu, 4, DType> out.  

      Shape3定义如下:
      MSHADOW_XINLINE Shape<3> Shape3(index_t s0, index_t s1, index_t s2) {
        Shape<3> s;
        s[0] = s0; s[1] = s1; s[2] = s2;
        return s;
      }

      Shape2定义如下:
      MSHADOW_XINLINE Shape<2> Shape2(index_t s0, index_t s1) {
        Shape<2> s;
        s[0] = s0; s[1] = s1;
        return s;
      }

      // MSHADOW_XINLINE is used for inlining template code for both CUDA and CPU code. MSHADOW_XINLINE是一个宏. 
      #ifdef MSHADOW_XINLINE
          #error "MSHADOW_XINLINE must not be defined"
      #endif
      */

      if (param_.pad[0] == 0 && param_.pad[1] == 0) { // 0.8的卷积操作只能实现2D卷积, 因此kernel, stride, dilate, pad均是2D的.
        // param_指向结构体ConvolutionParam对象p的指针, 用来访问成员pad. 即pad[0], pad[1]. 
        // 做2D卷积时, 不对卷积核补零, temp_col是:

        /*// data是卷积层的输入数据! data是 64 * 1 * 28 * 28的. 手写数字体识别数据.                
        cout<<"data.shape_[0]: "<<data.shape_[0]<<endl; // 64 
        cout<<"data.shape_[1]: "<<data.shape_[1]<<endl; // 1, 通道数. channel K. 单通道(灰度图像), RGB图像(彩色图像). 
        cout<<"data.shape_[2]: "<<data.shape_[2]<<endl; // 28 
        cout<<"data.shape_[3]: "<<data.shape_[3]<<endl; // 28 */ 

        /*
        // 输出卷积层输入数据 data(4D tensor)的数据指针. 
        cout<<"data[0]: "<<data[0].dptr_<<endl; // data[0]: 0x7f98600008c0 
        cout<<"data[1]: "<<data[1].dptr_<<endl; // data[1]: 0x7f9860001500 
        cout<<"data[2]: "<<data[2].dptr_<<endl; // data[2]: 0x7f9860002140 
        cout<<"data[3]: "<<data[3].dptr_<<endl; // data[3]: 0x7f9860002d80 
        */

        /*
        // 写入数据, 将data数据保存到.txt中. 因为cout输出一个tensor会出错, 
        // 所以只操作单个值. 
        // std::ofstream fdata("/home/ly/MXNet/fdata.txt");
        for(int i = 0; i < data.shape_[0]; ++i){
                for(int j = 0; j < data.shape_[2]; ++j){
                        for(int k = 0; k < data.shape_[3]; ++k){ 
                                fdata << data[i][0][j][k] <<" "; 
                        }
                }
        }
        fdata<<flush;
        // fdata.close();
        // data的类型是mshadow::Tensor<mshadow::cpu, 4, float>&
        // data[0]的类型是mshadow::Tensor<mshadow::cpu, 3, float>
        // data[0][0]的类型是mshadow::Tensor<mshadow::cpu, 2, float>
        // data[0][0][0]的类型是mshadow::Tensor<mshadow::cpu, 1, float>
        // data[0][0][0][0]的类型是float&. 
        */

        temp_col = unpack_patch2col(data.Slice(i, i + step),
                                    param_.kernel[0],
                                    param_.kernel[1],
                                    param_.stride[0],
                                    param_.stride[1],
                                    param_.dilate[0],
                                    param_.dilate[1]);
        /*
        data.Slice(i, i + step), 用到了Slice函数, 切片函数, 将一个Tensor数据切片, 根据data.Slice(i, i + step)的调用格式, Slice
        应该是Tensor结构体的成员, slice函数和Slice函数是不一样的, Slice定义见:
        mshadow/mshadow/tensor.h 481行:
        MSHADOW_XINLINE Tensor<Device, dimension, DType> Slice(index_t begin, index_t end) const {...}. Slice是结构体Tensor下
        成员函数, 因此Tensor的对象data可以调用Slice函数.
        slice the tensor in highest dimension [begin,end)
        param begin: begin position of slice
        param end: end position of slice
        return tensor after slice. 返回切片后的Tensor. 
        */

        /*
        // cout<<"i: "<<i<<endl; // 0, i一直是0, 循环仅执行1次. 
        // 输入iamge后面就是卷积层. 
        auto a = data.Slice(i, i + step); // c++11类型推断.
        cout<<"a.shape_[0]: "<<a.shape_[0]<<endl; // step, 因为step是batch_size, 所以为64. 或者a.size(0). 
        cout<<"a.shape_[1]: "<<a.shape_[1]<<endl; // 1. 
        cout<<"a.shape_[2]: "<<a.shape_[2]<<endl; // 28.
        cout<<"a.shape_[3]: "<<a.shape_[3]<<endl; // 28.
        */

        /*
        由于Tensor型的变量不能直接用cout输出, 所以利用mshadow/mshadow/tensor.h 363行, Tensor结构体的成员来访问Tensor的内容:
        Tensor结构体的成员变量:
        pointer to the data 
        DType *dptr_; // 指向Tensor数据的指针 dptr_.  

        shape of the tensor
        Shape<dimension> shape_; // 利用 shape_ 来访问Tensor的shape. 

        storing the stride information in x dimension
        this is used to deal with pitch allocation in gpu or sse(align x dimension to 64bit) for efficiency
        index_t stride_;

        stream where the computation lies
        stream is a device dependency concept where each computation
        Stream<Device> *stream_; 
        */
        /*
        // 因此输出tensor时用dptr_试试.
        cout<<"a[0]: "<<a[0].dptr_<<endl; // a[0]: 0x7f98600008c0
        cout<<"a[1]: "<<a[1].dptr_<<endl; // a[1]: 0x7f9860001500 
        cout<<"a[2]: "<<a[2].dptr_<<endl; // a[2]: 0x7f9860002140 
        cout<<"a[3]: "<<a[3].dptr_<<endl; // a[3]: 0x7f9860002d80 
        */
        /*
        实验下, 由于step = batch_size, 输出tensor的dptr_. 一个用data, 一个用data.Slice(i, i + step). 比较差异.
        根据 data 和 data.Slice(i, i + step)的大小比较 + dptr_ 比较, 由于step是batch_size, 认为卷积层的输入数据没有发生改变. 

        data.Slice(i, i + step)是对data这个tensor进行切片, 取data的一部分(i, i + step). 
        */

        /*计算temp_col: 
        利用函数unpack_patch2col: 定义见mshadow/mshadow/extension/unpack_patch2col.h 91和104行. 输入参数不同, 根据445使用的
        unpack_patch2col, 函数定义为:
        template<typename SrcExp, typename DType, int etype>
        inline UnpackPatchToColXExp<SrcExp, DType, ExpInfo<SrcExp>::kDim> unpack_patch2col(
        const Exp<SrcExp, DType, etype> &img, index_t psize_y, index_t psize_x, index_t pstride_y_, index_t pstride_x_,
        index_t pdilate_y_, index_t pdilate_x_) {...}. 
        将图像块(patches of image)unpack(解压)成一个矩阵的一列, 在利用unpack_patch2col得到mat后, 可以实现卷积.

        img: source image, img可以是3D Tensor或者4D Tensor(多幅图像). 这里是4DTensor, batch_size * 1 * 28 * 28. 
        psize_y: 每个patch的高度. 这里是kernl[0], 即卷积核的高度. 
        psize_x: 每个patch的宽度. 这里是kernel[1], 即卷积核的宽度. 
        pstride_y_: 每个patch在y方向上的滑动步长. 这里是stride[0], 即卷积核在y方向上的滑动步长. 
        pstride_x_: 每个patch在x方向上的滑动步长.  这里是stride[1], 即卷积核在x方向上的滑动步长. 
        pdilate_y_: 每个patch在y方向上的膨胀系数. 这里是dilate[0], 即卷积核的膨胀系数, y方向. 
        pdilate_x_: 每个patch在x方向上的膨胀系数. 这里是dilate[1], 即卷积核的膨胀系数, x方向. 

        利用unpack_patch2col得的mat, 得到output: output = dot(weight, mat). output即是卷积层的输出特征图. 
        out_height = [(in_height - psize_y)] / pstride + 1,
        out_width  = [(in_width - psize_x)] / pstride + 1. 
        */

        /*
        cout<<"temp_col.shape_[0]: "<<temp_col.shape_[0]<<endl; // 25, param_.kernel[0] * param_.kernel[1] 
        // 这个大小是 shape_colunit_[0]是: 卷积层前一层特征图个数 * kernel[0] * kernel[1]. 
        cout<<"temp_col.shape_[1]: "<<temp_col.shape_[1]<<endl; // 9216, batch_size * [ out.size(2) * out.size(3) ], out是
        // 卷积层输出特征图.  这个大小是 shape_colunit_[1] * step, 是 step * 卷积层输出特征图的高度 * 宽度 之积.
        // 在定义张量temp_col时, 已将其大小定义好. 
        cout<<"temp_col.shape_.Size(): "<<temp_col.shape_.Size()<<endl; // 230400 = 25 * 9216.

        将图像块(patches of image) 28 * 28的patch, unpack(解压)成一个矩阵的一列, 
        利用unpack_patch2col得到mat后, 可以实现卷积.
        */


      } else {
        temp_col = unpack_patch2col(pad(data.Slice(i, i + step),
                                    param_.pad[0], param_.pad[1]),
                                    param_.kernel[0],
                                    param_.kernel[1],
                                    param_.stride[0],
                                    param_.stride[1],
                                    param_.dilate[0],
                                    param_.dilate[1]);
        /*
        如果pad[0]或pad[1]不是0, 先利用 pad函数 对 data.Slice(i, i + step) 进行补零操作, 然后再做unpack_patch2col操作.

        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方向上补零的列数. 返回补零的结果, 即返回补完零之后的矩阵.  
        */                            

      } 
      // temp_col 是做完 unpack_patch2col 后的tensor, 大小是 shape_colunit_[0] * [shape_colunit_[1] * step]. 

      const index_t gstride = temp_col.size(0) / param_.num_group; 
      // gstride类型是index_t, 即unsigned int型的. 其值为 temp_col.size(0) / param_.num_group.
      // 即 shape_colunit_[0](卷积层前一层特征图个数 * kernel[0] * kernel[1].) / num_group(默认为1, 
      // 将输入数据切割成num_group个partitions. num_group貌似只能是1). 

      for (uint32_t gid = 0; gid < param_.num_group; ++gid) { 
        /*
        使用typedef给类型起的别名. 1字节:uint8_t; 2字节: uint16_t; 4字节: uint32_t; 8字节: uint64_t.
        typedef unsigned char uint8_t;
        typedef unsigned int uint16_t; 
        typedef unsigned long uint32_t;
        typedef unsigned long long uint64_t;

        param_是ConvolutionParam结构体的对象, 调用成员num_group. 因为num_group默认为1, 因此 gid == 0. 即只做一次for循环.
        这个for循环做的就是: 将输入数据切割成num_group个partitions. 有点并行的意思. 
        */

        mshadow::Tensor<xpu, 2, DType> tmpc = temp_col.Slice(gstride * gid,
                                       gstride * (gid + 1));
        /*
        temp_col是做完unpack_patch2col之后的Tensor, 在pad默认为0的情况下, 其大小为: 
        shape_colunit_[0] * [shape_colunit_[1] * step]. 再次调用Slice, tensor切片函数:
        Slice函数, 调用格式: data.Slice(i, i + step)的调用格式. 

        gid的总数是 num_group - 1. 即gid起的作用就是, 将一个tensor(输入数据切割成num_group个partitions), gid就代表每个
        partitions的索引, 即第 gid 个partitions. 这里是利用Slice函数切割temp_col, 切割的范围是 gid-gid+1, 即将temp_col切割成
        1个partitions. 每个partitions(也是一个tensor)的大小是 gstride.  

        */
        /*
        // 输出tmpc的大小, 是一个2维的张量.
        cout<<"tmpc.shape_[0]: "<<tmpc.shape_[0]<<endl; // 25 
        cout<<"tmpc.shape_[1]: "<<tmpc.shape_[1]<<endl; // 9216 

        在默认 num_group == 1的情况下, gid == 0. gstride == temp_col.size(0). 
        因此, 为temp_col.Slice(0, gstride). 故, tmpc 和 temp_col是一样的. 这里因为没有做 输入数据切割成num_group个partitions
        这个操作, 因此 tmpc 和 temp_col是一样的. 
        */


        temp_dst[gid] = dot(wmat[gid], tmpc);
        /*
        temp_dst是3D的tensor. gid == 0, 因此是给 temp_dst[0]赋值, 根据 unpack_patch2col 函数的说明, 
        output: output = dot(weight, mat)即可以看做是卷积后的结果(离散卷积即, 卷积核元素和对应位置的元素相乘在相加, 即点乘dot
        . 不过dot现在是矩阵之间的点乘了). 
        因此, temp_dst就是做完卷积的结果. 

        1)num_group == 1的情况下, tmpc和tmp_col是一样大小的, 代表做完unpack_patch2col后的tensor. 即tmpc是卷积层的输入数据. 
        tmpc有两种情况, pad[0] == pad[1] == 0 或不全为0. 总之, 就是卷积层的输入数据. 

        2)wmat[0]就是卷积层全部的卷积核的权重值(由于权值共享, 因此一个特征图需要一个卷积核).
        wmat是卷积层的权重, 即卷积核的权重. wmat定义如下:
        Shape<3> wmat_shape =
        Shape3(param_.num_group,
               param_.num_filter / param_.num_group,
               data.shape_[1] / param_.num_group * param_.kernel[0] * param_.kernel[1]);
       Tensor<xpu, 3, DType> wmat;  

       wmat是3Dtensor, 因此wmat[0]就是2D的tensor. 因此, 默认num_group == 1, 因此wmat的第一维最大就是0. 故wmat[0]的维数是:
       { param_.num_filter / param_.num_group } * { data.shape_[1] / param_.num_group * param_.kernel[0] * param_.kernel[1] } 
       即: 卷积核的个数 * channel K * kernel[0] * kernel[1]. channel K == 1是做2D卷积. 因此, num_group == 1的情况下, 
       wmat[0]就是卷积层全部的卷积核的权重值(由于权值共享, 因此一个特征图需要一个卷积核). 
       批处理中, 对于一个样本来说, 卷积层的权重是相同的. 

       3)temp_dst[0] 即是对卷积层输入数据 tmpc 做完卷积(仅weight)后的 输出数据(批处理的).
       根据unpack_patch2col 函数的说明, dot(weight, tmpc)就是做完卷积后的结果. 
       temp_dst[0]就是做完卷积后的结果, 这里仅仅和卷积核发生了作用. 还没有涉及偏置.
       tem_dst定义如下:
       Tensor<xpu, 3, DType> temp_dst; 大小为: shape_dstunit_[0] * shape_dstunit_[1] * { shape_dstunit_[2] * step), s) };
       shape_dstunit_[0]是1, 即channel K; shape_dstunit_[1]是卷积层卷积核的个数; shape_dstunit_[2]是 积层输出特征图的
       高度 * 宽度 之积. 因此, temp_dst的第一维最大值为0. 
       即num_group == 1的情况下, temp_dst[0]是2D的tensor, 大小是:
       卷积核的个数(卷积层输出的特征图个数) * { 特征图大小 * batch_size }. 批处理中, 一个样本经过卷积层后的输出数据大小是:
       卷积核的个数(卷积层输出的特征图个数) * 特征图大小; 对于不同的样本, 这个大小相同, 只是值不同. 
       因此, temp_dst[0] 即是对卷积层输入数据 tmpc 做完卷积(仅weight)后的 输出数据(批处理的).   
        */
      }
      // 以上的有些参数的值的结果是在 num_group == 1和 step == batch_size的情况下, 推出来的. 如wmat的大小, wmat[gid]的大小, 
      //tmpc的大小, tmp_col的大小, tmp_dst的大小等等. 這下tensor在 num_group != 1時, 均要發生变化. 这里不再细研究了.   
      out.Slice(i, i + step) = swapaxis<1, 0>(reshape(temp_dst,
                                              mshadow::Shape4(param_.num_filter,
                                                  step,
                                                  out.size(2),
                                                  out.size(3))));
      /*
      num_group == 1的情况下, gid == 0. temp_dst[0] 即是对卷积层输入数据 tmpc 做完卷积(仅weight)后的输出数据(批处理的). 大小是:
      卷积核的个数(卷积层输出的特征图个数) * { 特征图大小 * batch_size }.

      // cout<<"i: "<<i<<endl; // 0, i一直是0, 循环仅执行1次. 
      // 输入iamge后面就是卷积层. 
      auto a = data.Slice(i, i + step); // c++11类型推断.
      cout<<"a.shape_[0]: "<<a.shape_[0]<<endl; // step, 因为step是batch_size, 所以为64. 或者a.size(0). 和index_t nstep_;相关. 
      cout<<"a.shape_[1]: "<<a.shape_[1]<<endl; // 1. 
      cout<<"a.shape_[2]: "<<a.shape_[2]<<endl; // 28.
      cout<<"a.shape_[3]: "<<a.shape_[3]<<endl; // 28.
      data是卷积层的输入数据, 还未做 unpack_patch2col操作. data.Slice(i, i + step)即将data切片成 [nbatcb/nstep_]份, 并行处理.

      如果卷积层的输入数据data别切成了 [nbatcb/nstep_]份, 做并行处理, 那么 tmp_col 也是 [nbatcb/nstep_]份, tmp_dst也是
      [nbatcb/nstep_]份. 因此, temp_dst[gid] = dot(wmat[gid], tmpc)(假设gid==0)就是做的是一份data的卷积后的结果. 
      out是卷积层的输入tensor, 是4D的. 如果 tmp_dst也是[nbatcb/nstep_]份的, 那么out也别切成了 [nbatcb/nstep_]份. 因此要一份份
      的赋值, 最终的输出是out. 
      即 out.Slice(i, i + step), 这是对卷积层的输出out(一份, 即大小为step)的赋值操作. 将[nbatcb/nstep_]份out赋值完毕才算是得到
      了卷积层的真正输出. 再将一份 temp_dst[0] 赋给一份的 out时, 要改变大小. 因为out是4D的, temp_dst[0]是2D的tensor.

      1)reshape函数, 重新定义输入的大小. 这个reshape不是python numpy中的reshape, reshape输入接收的第一个参数是tmp_dst, 类型是
      Tensor. 定义见: mshadow/mshadow/extension/reshape.h 48行. 在mshadow::exprc命名空间下. 
      template<typename SrcExp, typename DType, int etype, int dimdst>
      inline ReshapeExp<SrcExp, DType, dimdst, ExpInfo<SrcExp>::kDim> reshape(const Exp<SrcExp, DType, etype> &src, 
      Shape<dimdst> oshape) {...}. reshape a tensor to another shape.
      src: Tensor<Device, dimsrc>, dimsrc是src的维数, 如3.
      oshape: target shape. 是Shape<dimdst> oshape, dimdst是输出tensor的维数, 如4.
      return a expresion with type Tensor<Device, dimdst>. 
      因此reshape(temp_dst, *)就是重新temp_dst这个3D Tensor的shape, 但是数据的总个数是不变的. temp_dst[0]是2D的tensor, 大小是:
      卷积核的个数(卷积层输出的特征图个数) * { 特征图大小 * batch_size }. 因此, temp_dst的大小为:
      channel K * 卷积核的个数(卷积层输出的特征图个数) * { 特征图大小 * batch_size }.
      reshape函数输出的shape为: num_filter * step * out.size(2) * out.size(3). 即:
      卷积核个数 * step(batch_size) * 卷积层输出特征图高度 * 宽度(4D). 这样正好对上卷积层输出out tensor的维数.

      reshape(temp_dst, *)就返回 卷积核个数 * step(batch_size) * 卷积层输出特征图高度 * 宽度(4D)的一个4Dtensor.

      2)swapaxis函数, 函数定义见mshadow/mshadow/extension/swapaxis.h 52行. 在mshadow::exprc命名空间下. 
      template<int a1, int a2, typename SrcExp, typename DType, int etype>
      inline SwapAxisExp<SrcExp, DType, ExpInfo<SrcExp>::kDim, ExpInfo<SrcExp>::kDim - a1, a2> swapaxis(
      const Exp<SrcExp, DType, etype> &src) {...}. reshapes a tensor to another shape.
      src: Tensor<Device, dimsrc>, dimsrc是src的维数, 如4. 
      return a expresion with type Tensor<Device,dimdst>. dimdst是输出tensor的维数, 如4.
      模板参数a1: higher dimension to be swapped, assert a1 > a2. assert宏的原型定义在<assert.h>中, 
      其作用是如果它的条件返回错误, 则终止程序执行.
      模板参数a2: lower dimension to be swapped. 

      swapaxis<1, 0>(...)就是对 reshape(temp_dst, *)的返回结果再重新reshape一下(..). 1, 0即指定模板参数:
      int a1, int a2. 模板类, 模板函数的模板参数可以这样指定: swapaxis<1, 0>(...).  

      最后将reshape的结果(一份temp_dst)赋给一份 out. 一共[nbatcb/nstep_]份. 

      */

    }
    if (!param_.no_bias) { // no_bias默认为false, 这个if即使用bias. 卷积层默认不使用bias(偏置). 
      // add bias, broadcast bias to dim 1: channel
      Tensor<xpu, 1, DType> bias = in_data[conv::kBias].get<xpu, 1, DType>(s);
      // 利用get函数将in_data[2]拉成1维的张量, 即向量. 即卷积层的如果有bias, 其是向量. bias是一个1D的tensor. 
      // cout<<"bias.size(0): "<<bias.size(0)<<endl; // 输出卷积层bias的大小, 1D的tensor的大小. 为卷积层卷积核的个数.
      // 即一个卷积核对应一个bias, 也是共享的. 
      out += broadcast<1>(bias, out.shape_);
      /*
      broadcast见: mshadow/mshadow/extension/broadcast.h 69行:
      template<int dimcast, typename SrcExp, typename DType, int etype, int dimdst>
      inline Broadcast1DExp<SrcExp, DType, dimdst, dimdst - dimcast> broadcast(const expr::Exp<SrcExp, DType, etype> &src, 
      Shape<dimdst> shape) {..}. 
      src Tensor<Device,1>; shape: shape of output; 返回 a expresion with type Tensor<Device, dimdst>, dimdst为4, 
      返回的Tensor的维数为4, 和shape的个数是有关的.
      * input: Tensor<Device,1>: ishape[0]
      * output: Tensor<Device,dimdst> : oshape[dimcast] = ishape[0].
      模板参数tparam dimcast: target dimension where the 1D tensor will be broadcasted 
      将一个1维的 Tensor 扩充成 dimdst 维的 Tensor. 为了正确计算!! 

      out.shape_是卷积层输出out的shape, 是一个Shape<4>的变量, 大小为 卷积核数 * batch_size * 特征图高度 * 宽度.
      broadcast<1>(bias, out.shape_)就是将bias这个1D张量扩展成和卷积层输出 out, 具有一样大小的tensor. 方便做加法.

      即为out的每一个数据点都加上一个bias, 所以要将bias扩展成和out一样大小的tensor才可以做加法. bias的大小是卷积核的个数, 即
      一个卷积核对应一个bias. 也就是卷积层的一个输出特征图对应于一个bias. 
      */
    }
  }

  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层)有参数weight和bias(可能使用偏置, 可能不适应. 如果使用偏置, 每个卷积核配备一个), 
    因此要计算的是损失J关在BN层(第l层)的残差, weight的梯度和bias的梯度. 
    !!!!!!!!!!!!!!!!梯度可以看做是损失J关于层参数的导数, 残差可以看做是损失J关于层输入的导数!!!!!!!!!!!!!!!!!!!!!!!!!!!! 

    in_grad输出残差/梯度参数, 向量容器, 每个元素的类型是TBlob. 卷积层层(第l层)的.
    out_grad输入残差/梯度参数, 向量容器, 每个元素的类型是TBlob. 上一层(第l + 1层)的残差/梯度, 计算本层的残差/梯度. 
    利用上一层的残差来计算出损失关于本层输入的残差, 从而再计算出损失关于本层参数的梯度, 再进行sgd更新. 用到的一般均是上一层的
    残差.

    in_data输入参数, 向量容器, 每个元素的类型是TBlob. 本层(第l层)的输入.  
    out_data输出参数, 向量容器, 每个元素的类型是TBlob. 本层(第l层)的输出.  
    req: 数据操作模式, 向量数组. 元素类型是OpReqType.
    aux_args: 表示的是为了方便计算而需要的附加的 tensor. 附加的Tensor有两个: kMovingMean, kMovingVar. 以前看的操作均没使用
    aux_args来辅助计算.
    */

    using namespace mshadow;
    using namespace mshadow::expr; // 一些表达式所在的命名空间. mshadow::expr.  
    // TODO(bing): check the BLAS Handle, be careful
    if (param_.kernel.ndim() > 2) {
      LOG(FATAL) << "Volume convolution is not implmented in mshadow";
    } // 0.8版本的mxnet只能做2D卷积, 因此kernel的维数如果 > 2, 就输出一个log信息; 0.9版本的mxnet可以做3D卷积了.  
    CHECK_EQ(out_grad.size(), 1); // 卷积层的上一层(第l + 1层)传递给卷积层的只有残差, 因此out_grad容器的大小为1. 
    size_t expected = param_.no_bias == 0 ? 3 : 2; //  定义expected, 如果卷积层没有偏置就是2, 否则为3. 即用expected来控制卷积
    // 的in_data和in_grad容器的大小. 
    CHECK(in_data.size() == expected && in_grad.size() == expected); // 卷积层输入in_data和in_grad容器的大小为expected.
    // 如果卷积层有偏置, 那么in_data容器大小就是3, 包含输入kData, 卷积核权重kWeight, 偏置kBias.
    // in_grad容器的大小也是3, 包括损失关于卷积层输入的残差out, 损失关于卷积核权重的梯度, 损失关于偏置的梯度. 

    CHECK_EQ(req.size(), expected); // rep容器的大小是expected. 即, 对于:
    // 损失关于卷积层输入的残差out, 损失关于卷积核权重的梯度, 损失关于偏置的梯度, 采用不同的数据操作模式. 
    CHECK_EQ(in_data[conv::kWeight].CheckContiguous(), true);
    /*
    in_data[1].CheckContiguous(), 利用CheckContiguous函数, 定义见: include/mxnet/tensor_blob.h 136行:
    inline bool CheckContiguous(void) const {
        return shape_[shape_.ndim() - 1] == stride_;
    } CheckContiguous函数是TBlob类下的成员函数, 因此可以利用TBlob的对象调用. return whether the tensor's memory is continuous
    用来检查一个tensor的内存是不是连续的. 如是是返回true, 不是返回false.

    in_data[conv::kWeight]即in_data[1], 是TBlob的对象, 因此可以调用CheckContiguous函数. 用来检查in_data[1]这个tensor的内存
    是不是连续的. 
    */

    // get data
    Stream<xpu> *s = ctx.get_stream<xpu>();
    Tensor<xpu, 4, DType> data = in_data[conv::kData].get<xpu, 4, DType>(s);
    // 利用get函数将卷积层的输入in_data[0]拉成4D的tensor, data. 代表卷积层的输入. batch_size * channel K * 高度 * 宽度. 
    Shape<3> wmat_shape =
        Shape3(param_.num_group,
               param_.num_filter / param_.num_group,
               data.shape_[1] / param_.num_group * param_.kernel[0] * param_.kernel[1]);
    /*
    定义wmat_shape, 为定义本层(第l层)的权重wmat做准备. 即卷积层权重矩阵的shape. 
    s0 = param_.num_group, 即num_group(输入数据切割成num_group个partitions), 默认为1;
    s1 = param_.num_filter / param_.num_group. num_filter为卷积核的个数, 在使用COnvolution时指定, num_filter的范围, 1-100000.
    s2 = data.shape_[1] / param_.num_group * param_.kernel[0] * param_.kernel[1];
    data.shape_[1]是data shape的第二个分量的值, 即channel K. 这里为卷积层前一层特征图的数量.  
    param_.kernel[0]即kernel[0], 只是使用param_来调用kernel. 在使用卷积层的时候: kernel = (5,5).  
    param_.kernel[1]即kernel[1]. 
    */           

    Tensor<xpu, 3, DType> wmat =
        in_data[conv::kWeight].get_with_shape<xpu, 3, DType>(wmat_shape, s);
    /*
    将卷积层权重in_data[conv::kWeight]即in_data[1]拉成3维的张量. 这利用了get_with_shape. 定义如下:
    mshadow::Tensor<Device, dim, DType> mxnet::TBlob::get_with_shape(const mshadow::Shape<dim> & shape, 
    mshadow::Stream< Device > *stream = NULL)const, 其中Shape<dim> & shape 就是wmat_shape. 

    卷积层的权重wmat是3D的张量. 权重的大小是: wmat_shape. 一般的, 卷积层的卷积核(weight)的个数为:
    num_filter * (kernel[0] * kernel[1]). 由于mxnet做卷积时, 用到了 num_group 机制, 因此将wmat设置成3D的, 其中第一个维度是
    num_group的数目. 假设要将卷积层的输入数据data分成num_group个partitions, 那么每个partitions的卷积核个数就是:
    param_.num_filter / param_.num_group个.   
    */    

    Tensor<xpu, 4, DType> grad = out_grad[conv::kOut].get<xpu, 4, DType>(s);
    Tensor<xpu, 4, DType> gdata = in_grad[conv::kData].get<xpu, 4, DType>(s);
    Tensor<xpu, 3, DType> gwmat =
        in_grad[conv::kWeight].get_with_shape<xpu, 3, DType>(wmat_shape, s);
    /*
    利用get函数定义卷积层:
    上一层(第l + 1层)的残差out_grad[conv::kOut]即out_grad[0], 拉成4D张量.  

    定义损失关于卷积层输入的残差gdata, 将in_grad[conv::kData]即in_grad[0]拉成4D的张量. mxnet在进行层的计算时, 会事先分配好内存, 
    即in_grad是卷积层的残差/梯度的容器, 要事先分配好这些内存.

    定义损失关于卷积层权重weight的梯度gwmat, 利用get_with_shape将in_grad[conv::kWeight]即in_grad[1]拉成3D张量. 
    其中gwmat的shape和卷积层的卷积核的权重wmat的shape是一样的.  

    在使用函数get或get_with_shape时, 一并将模板参数传入. 
    */

#if defined(__CUDACC__)
    CHECK_EQ(s->blas_handle_ownership_, Stream<xpu>::OwnHandle)
        << "Must init CuBLAS handle in stream";
#endif // __CUDACC__宏. 
    const index_t nbatch = data.size(0); // nbatch即batch_size. data.size(0)即data的第一维的大小. 
    Tensor<xpu, 1, DType> workspace =
        ctx.requested[conv::kTempSpace].get_space_typed<xpu, 1, DType>(
            Shape1(this->InitTemp(data.shape_, grad.shape_)), s);
    /*
    与卷积层前向传播中的处理是一样的. 定义一个临时内存空间workspace, 是1D的tensor. 操作需要额外的内存作为工作空间进行计算.
    InitTemp输入参数, 一个是data.shape_, 一个是grad.shape_. 
    */

    for (index_t i = 0; i < nbatch; i += nstep_) {
      const index_t step = std::min(nstep_, nbatch - i);
      // 在实际运行过程中, nbatch == batch_size, nstep_ == batch_size. 因此step == batch_size. 这个和卷积层前向传播是一样的
      // 设置, 即在前向传播的过程中, 根据输入数据data, weight来计算输出out. 是一份一份的进行计算的(当nstep_ < nbatch时). 
      // 因此, 反向传播时, 在计算残差和梯度的时候也是一份一份的计算的. 也有点并行的意思.   
      Tensor<xpu, 2, DType> temp_col = Tensor<xpu, 2, DType>(workspace.dptr_,
                                               Shape2(shape_colunit_[0],
                                                      shape_colunit_[1] * step), s);
      Tensor<xpu, 3, DType> temp_dst = Tensor<xpu, 3, DType>(
                                               workspace.dptr_ + temp_col.shape_.Size(),
                                               Shape3(shape_dstunit_[0],
                                                      shape_dstunit_[1],
                                                      shape_dstunit_[2] * step), s);
      /*
      这三个量shape_colunit_, shape_dstunit_, nstep_. 
      是在运行时指定的, 是在ConvolutionOp类的私有函数InitTemp中指定的, 最新版本的convolution-inl.h中也有一个私有函数
      LayerSetUp, 用来指定一些值. 根据ishape(本层输入shape)和oshape(本层输出shape)即, data.shape_, out.shape_
      可以确定OP中用到的一些变量的值. 
      */

      // cout<<"forward_nstep_: "<<nstep_<<endl; // nstep_是batch_size.  
      /*cout<<"shape_colunit_[0]: "<<shape_colunit_[0]<<endl; 
      // conv1 = mx.symbol.Convolution(data=data, kernel=(5,5), stride=(2,2), num_filter=10)时, 为25. 
      // shape_colunit_[0]是: 卷积层前一层特征图个数 * kernel[0] * kernel[1]. ishape[1] * ksize_y * ksize_x.
      cout<<"shape_colunit_[1]: "<<shape_colunit_[1]<<endl;
      // 为144, 4. 是 卷积层输出特征图的高度 * 宽度 之积. oshape[2] * oshape[3].
      cout<<"shape_dstunit_[0]: "<<shape_dstunit_[0]<<endl;
      //  shape_dstunit_[0]是1. param_.num_group.  
      cout<<"shape_dstunit_[1]: "<<shape_dstunit_[1]<<endl;
      // shape_dstunit_[1]是卷积层卷积核的个数. 为10, 50. param_.num_filter / param_.num_group.
      cout<<"shape_dstunit_[2]: "<<shape_dstunit_[2]<<endl;
      // 是 卷积层输出特征图的高度 * 宽度 之积.  oshape[2] * oshape[3].*/ 
      /*
      和卷积层前行传播的变量一样, 利用Tensor结构体的构造函数: Tensor<xpu, 2/3, DType>(..)来创建Tensor对象temp_col和temp_dst.
      temp_col是2D的张量, temp_dst是3D的张量. 

      在num_group == 1的情况下: 
      temp_col的大小为: shape_colunit_[0] * (shape_colunit_[1] * step), 即: 
      { 卷积层前一层特征图个数 * kernel[0] * kernel[1] } * { 卷积层输出特征图的高度 * 宽度 * step }. 
      temp_dst大小为: shape_dstunit_[0] * shape_dstunit_[1] * (shape_dstunit_[2] * step), 即:
      { channel K } * { 卷积层卷积核的个数 } * { 卷积层输出特征图的高度 * 宽度 之积 }.
      */

      temp_dst = reshape(swapaxis<1, 0>(grad.Slice(i, i + step)), temp_dst.shape_);
      /*
      temp_dst是3D的tensor, shape上文已经定义了. grad是上一层(第l + 1层)的残差, 是4D的tensor.  
      利用上一层(第l + 1层)的残差定义temp_dst, 即temp_dst就代表上一层的残差. 只是在赋值的时候需要reshape.

      1)reshape函数. 定义见: mshadow/mshadow/extension/reshape.h 48行. 在mshadow::exprc命名空间下. 
      template<typename SrcExp, typename DType, int etype, int dimdst>
      inline ReshapeExp<SrcExp, DType, dimdst, ExpInfo<SrcExp>::kDim> reshape(const Exp<SrcExp, DType, etype> &src, 
      Shape<dimdst> oshape) {...}. reshape a tensor to another shape.
      src: Tensor<Device, dimsrc>, dimsrc是src的维数, 如4.
      oshape: target shape. 是Shape<dimdst> oshape, dimdst是输出tensor的维数, 如3.
      return a expresion with type Tensor<Device, dimdst>.  

      2)swapaxis函数. 函数定义见mshadow/mshadow/extension/swapaxis.h 52行. 在mshadow::exprc命名空间下. 
      template<int a1, int a2, typename SrcExp, typename DType, int etype>
      inline SwapAxisExp<SrcExp, DType, ExpInfo<SrcExp>::kDim, ExpInfo<SrcExp>::kDim - a1, a2> swapaxis(
      const Exp<SrcExp, DType, etype> &src) {...}. reshapes a tensor to another shape.
      src: Tensor<Device, dimsrc>, dimsrc是src的维数, 如4. 
      return a expresion with type Tensor<Device,dimdst>. dimdst是输出tensor的维数, 如4.
      模板参数a1: higher dimension to be swapped, assert a1 > a2. assert宏的原型定义在<assert.h>中, 
      其作用是如果它的条件返回错误, 则终止程序执行.
      模板参数a2: lower dimension to be swapped. 

      reshape的操作src相当于是: grad.Slice(i, i + step). 即对上一层的残差进行切片, 一份一份的来做. 一次for循环, 取一份(一共
      [nbatch/nstep_]份)上层的残差, 来计算卷积层的残差, 权重梯度, 偏置梯度.
      由于nbatch == nstep_, 因此 grad.Slice(i, i + step) 和 grad是相同的. i始终为0. 即拿上一层全部的残差grad来计算卷积层的
      损失关于卷积层输入的残差, 损失关于卷积层卷积核weight的梯度, 损失关于卷积核偏置的梯度. 
      */

      if (param_.pad[0] == 0 && param_.pad[1] == 0) { // pad[0] == pad[1] == 0, 即不对数据进行补零操作. 
        temp_col = unpack_patch2col(data.Slice(i, i + step),
                                     param_.kernel[0],
                                     param_.kernel[1],
                                     param_.stride[0],
                                     param_.stride[1],
                                     param_.dilate[0],
                                     param_.dilate[1]);
        // 定义temp_col, temp_col是2D的tensor. 还是利用unpack_patch2col来定义的. 
        // 由于i == 0, step == batch_size, 因此data和 data.Slice(i, i + step)是相同的. 
        // 将图像块(patches of image)unpack(解压)成一个矩阵的一列, 在利用unpack_patch2col得到mat后, 可以实现卷积前向和反向. 
      } else {
        temp_col = unpack_patch2col(pad(data.Slice(i, i + step), param_.pad[0], param_.pad[1]),
                                     param_.kernel[0],
                                     param_.kernel[1],
                                     param_.stride[0],
                                     param_.stride[1],
                                     param_.dilate[0],
                                     param_.dilate[1]);
        // pad[0]或pad[1]有一个不是0, 就先利用pad函数对data.Slice(i, i + step)进行补零操作, 然后再使用unpack_patch2col函数. 
      } // 这和前向操作的处理是一样的. data是卷积层的输入数据, 利用unpack_patch2col函数将数据数据进行unpack, 得到temp_col,
      // 再来实现卷积层的前向和反向操作.

      const index_t gstride = temp_col.size(0) / param_.num_group;
      // gstride类型是index_t, 即unsigned int型的. 其值为 temp_col.size(0) / param_.num_group.
      // 即 shape_colunit_[0](卷积层前一层特征图个数 * kernel[0] * kernel[1].) / num_group(默认为1, 
      // 将输入数据切割成num_group个partitions. num_group貌似只能是1).

      for (uint32_t gid = 0; gid < param_.num_group; ++gid) { // 与前向的操作是一样的. gid == 0, 开始循环. 一直到num_group.
        // 一个一个partitions的操作. num_group默认为1, 即不切割数据. 
        Tensor<xpu, 2, DType> tmpc = temp_col.Slice(gstride * gid, gstride * (gid + 1));
        /*
        定义tmpc, 和前向传播一样. 是一个临时的tensor, 如果num_group != 1, 那么就将temp_col切割. 
        在默认 num_group == 1的情况下, gid == 0. gstride == temp_col.size(0). 
        因此, 为temp_col.Slice(0, gstride). 故, tmpc 和 temp_col是一样的. 这里因为没有做 输入数据切割成num_group个partitions
        这个操作, 因此 tmpc 和 temp_col是一样的. 

        gid起的作用就是, 将一个tensor(输入数据切割成num_group个partitions), gid就代表每个
        partitions的索引, 即第 gid 个partitions. 这里是利用Slice函数切割temp_col, 切割的范围是 gid-gid+1, 即将temp_col切割成
        1个partitions. 每个partitions(也是一个tensor)的大小是 gstride.
        */

        if (i == 0) { // 因为 nbatch == nstep_, 因此 i == 0, 始终为0. 
          // cout<<"backward_i: "<<i<<endl; // 0 
          Tensor<xpu, 2, DType> tmp_gwmat = gwmat[gid]; // 可以看做是深拷贝. 
          /*
          在默认num_group == 1的情况下, gid是0. gwmat[gid]即gwmat[0], gid就是gwmat这个tensor第一维.
          gwmat是损失关于卷积层权重weight的梯度, 是3D的tensor, 其shape和wmat(卷积层的卷积核的权重)shape一样:
          { param_.num_group } * { param_.num_filter / param_.num_group } 
          * { data.shape_[1] / param_.num_group * param_.kernel[0] * param_.kernel[1] }.

          在默认num_group == 1的情况下, gwmat这个3D的tensor, 其第一维的最大值是0. gwmat[gid]是一个2D的tensor, 在0.8版本的mxnet
          中, 只能实现2D卷积, 因此特征图的channel K == 1. 故 tmp_gwmat这个2D tensor的大小就是:
          num_filter * { kernel[0] * kernel[1] }. 即正好是卷积层卷积核的参数的个数. 也是损失关于卷积层卷积核参数weight的梯度
          的个数.

          这里是定义tmp_gwmat, 即损失关于卷积层卷积核参数weight的梯度. 
          */

          Assign(tmp_gwmat, req[conv::kWeight], dot(temp_dst[gid], tmpc.T()));
          /*
          Assign赋值操作. 输出是tmp_gwmat, 即给tmp_gwmat赋值; 数据操作模式是req[conv::kWeight], 即卷积层权重的数据操作模式;
          exp是 dot(temp_dst[gid], tmpc.T()).
          根据 unpack_patch2col 函数的说明, output: output = dot(weight, mat)即可以看做是卷积后的结果
          (离散卷积即, 卷积核元素和对应位置的元素相乘在相加, 即点乘dot. 不过dot现在是矩阵之间的点乘了). 

          dot(temp_dst[gid], tmpc.T())即取了转置(即: A'*B 和 B'*A正好差了一个转置). 

          其中temp_dst[gid], 在默认num_group == 1的情况下, 其为temp_dst[0]. temp_dst是3D的tensor, 其值是上一层的残差(经过了
          reshape操作). temp_dst这个3D的tensor, 其第一维的大小是channel K. 在0.8版本的为1, 因此最大即temp_dst[0]. 
          temp_dst[0]是一个2D的tensor, 由于 i == 0, grad.Slice(i, i + step) 和 grad是相同的, 
          因此这个2D的tensor就是上一层全部的残差.

          tmpc: 在默认 num_group == 1的情况下, gid == 0. gstride == temp_col.size(0). 
          因此, 为temp_col.Slice(0, gstride). 故, tmpc 和 temp_col是一样的. 这里因为没有做 输入数据切割成num_group个partitions
          这个操作, 因此 tmpc 和 temp_col是一样的. tmpc即输卷积层的输入data经过unpack_patch2col操作后的结果, 即可以看做是卷积
          层的真正输入数据!

          tmp_gwmat就是损失关于卷积层卷积核的参数weight的梯度:
          其值为: 卷积层的输入 * 上一层的残差.    
          */

        } else { // 如果i != 0, 那么就累加即可. 每次累加的值是: dot(temp_dst[gid], tmpc.T()). 
          gwmat[gid] += dot(temp_dst[gid], tmpc.T());
        }
      }

      for (uint32_t gid = 0; gid < param_.num_group; ++gid) { // gid循环, gid == 0. 只做一次for循环. 
        Tensor<xpu, 2, DType> tmpc = temp_col.Slice(gstride * gid, gstride * (gid + 1));
        tmpc = dot(wmat[gid].T(), temp_dst[gid]);
        /*
        定义一个2D的tensor, tmpc. 定义的时候和上面的tmpc定义一样, 但是其值发生改变.
        tmpc这个2D的tensor的值为: dot(wmat[gid].T(), temp_dst[gid]). 即矩阵做点乘. tmpc在上面定义时, 是看做卷积层真正的输入. 

        num_group == 1的情况下, gid == 0. 
        1)wmat[0]就是卷积层全部的卷积核的权重值(由于权值共享, 因此一个特征图需要一个卷积核). 
        wmat是3Dtensor, 因此wmat[0]就是2D的tensor. 因此, 默认num_group == 1, 因此wmat的第一维最大就是0. num_group == 1的情况下, 
        wmat[0]就是 卷积层全部的卷积核的权重值(由于权值共享, 因此一个特征图需要一个卷积核). 
        批处理中, 对于一个样本来说, 卷积层的权重是相同的. 
        wmat[gid].T()取转置, T是Tensor结构体下的成员函数, 用来取一个tensor的转置.   

        2)其中temp_dst[gid], 在默认num_group == 1的情况下, 其为temp_dst[0]. temp_dst是3D的tensor, 其值是上一层的残差(经过了
        reshape操作). temp_dst这个3D的tensor, 其第一维的大小是channel K. 在0.8版本的为1, 因此最大即temp_dst[0]. 
        temp_dst[0]是一个2D的tensor, 由于 i == 0, grad.Slice(i, i + step) 和 grad是相同的, 
        因此这个2D的tensor就是上一层全部的残差. 

        tmpc就是: 卷积层卷积核的全部weight * 上一层的残差. 
        */
      }

      // 计算损失关于卷积层的输入的残差, gdata. 分两种情况, pad[0]==pad[1]==0和不全为0. 
      if (param_.pad[0] == 0 && param_.pad[1] == 0) {
        Assign(gdata.Slice(i, i + step), req[conv::kData],
               pack_col2patch(temp_col,
                              data.Slice(i, i + step).shape_,
                              param_.kernel[0],
                              param_.kernel[1],
                              param_.stride[0],
                              param_.stride[1],
                              param_.dilate[0],
                              param_.dilate[1]));
        /*
        计算损失关于卷积层的输入的残差, gdata. pad[0]==pad[1]==0的情况下.
        Assign赋值操作, 赋值的对象是: gdata.Slice(i, i + step), 这和卷积的前向操作是类似的. 如果卷积层的输入数据data别切成了
        [nbatcb/nstep_]份. 因此要一份份的赋值; 数据操作模式是req[conv::kData], 即卷积层输入数据的操作模式; exp为:
        pack_col2patch(...)的返回值. unpack_patch2col函数是将卷积层的输入data进行unpack, 以便进行卷积操作; 而pack_col2patch正
        好相反, 为了得到残差服务.

        pack_col2patch函数见: mshadow/mshadow/extension/pack_col2patch.h 72行和88行. 有不同的参数. 根据所选参数:
        template<typename SrcExp, typename DType, int dstdim, int etype>
        inline PackColToPatchXExp<SrcExp, DType, dstdim> pack_col2patch(const expr::Exp<SrcExp, DType, etype> &src,
        Shape<dstdim> imshape, index_t psize_y, index_t psize_x, index_t pstride_y, index_t pstride_x,
        index_t pdilate_y, index_t pdilate_x) {..}. pack_col2patch是反向操作, 可以用来做反卷积(deconvolution)(mxnet也将反卷积
        deconvolution做成了单独的一层). Deconvolution是将Convolution的方向反过来--前向变后向, 后向变前向. 
        返回pack 的图像. 可以这样来看, unpack_patch2col函数是将特征图变成tensor; pack_col2patch是将tensor变成特征图.  

        mat: source matrix; 源矩阵为 temp_col, 即对卷积层的输入data进行unpack_patch2col后的结果, 可以看做是卷积层真正的输入.  
        imshape: 目标img的shape; data.Slice(i, i + step).shape_, 即一份data的shape, 因为i == 0, step == batch_size, 所以就是
        data.shape_, 即卷积层输入data的shape. 
        psize_y: 每个patch的高度;
        psize_x: 每个patch的宽度;
        pstride_y: 每个patch在y方向上的滑动步长;
        pstride_x: 每个patch在x方向上的滑动步长;    
        pdilate_y_: 每个patch在y方向上的膨胀系数. 这里是dilate[0], 即卷积核的膨胀系数, y方向. 
        pdilate_x_: 每个patch在x方向上的膨胀系数. 这里是dilate[1], 即卷积核的膨胀系数, x方向. 

        !!!!!!!不知道在干什么!!!!!!! 
        */

        // cout<<"gdata[0][0].shape_[0]: "<<gdata[0][0].shape_[0]<<endl; // 第l - 1层特征图的大小. 
        // cout<<"gdata[0][0].shape_[1]: "<<gdata[0][0].shape_[1]<<endl;

      } else {
        Shape<4> pshape = data.Slice(i, i + step).shape_;
        pshape[2] += 2 * param_.pad[0];
        pshape[3] += 2 * param_.pad[1];
        Assign(gdata.Slice(i, i + step), req[conv::kData],
               crop(pack_col2patch(temp_col,
                                   pshape,
                                   param_.kernel[0],
                                   param_.kernel[1],
                                   param_.stride[0],
                                   param_.stride[1],
                                   param_.dilate[0],
                                   param_.dilate[1]),
                    gdata[i][0].shape_));
        /*
        pad[0]或pad[1]有一个不是0. 要先定义 imshape: 目标img的shape. 
        首先定义pshape, 其为: data.Slice(i, i + step).shape_. 由于pad[0]或pad[1]不为0, 因此对pshape[2]和pshape[3]进行加和:
        pshape[2] += 2 * param_.pad[0];
        pshape[3] += 2 * param_.pad[1]; // 即将pshape[2]加上 2 * pad[0]. 这样定义完的pshape就是 imshape: 目标img的shape.

        然后再次利用pack_col2patch函数, 操作temp_col, imshape: 目标img的shape为pshape(已经将pad信息包含在里面了).

        在对pack_col2patch(...)使用crop函数, 进行裁剪. 裁剪成和 gdata[i][0].shape_即gdata[0][0].shape_一样大小的tensor.

        gdata是4D的tensor, 因此gdata[0][0]就是2D的tensor, gdata[0][0].shape_就是损失关于卷积层输入的残差的shape. 
        是第l - 1层特征图的大小.  
        */
        // cout<<"gdata[0][0].shape_[0]: "<<gdata[0][0].shape_[0]<<endl; // 第l - 1层特征图的大小. 
        // cout<<"gdata[0][0].shape_[1]: "<<gdata[0][0].shape_[1]<<endl; 
      }
    }

    /*
    cout<<"grad.size(0): "<<grad.size(0)<<endl; // 64, batch_size 
    cout<<"grad.size(1): "<<grad.size(1)<<endl; // 10, 卷积核个数. 
    cout<<"grad.size(2): "<<grad.size(2)<<endl; // 12, 卷积层输入特征图高度. 
    cout<<"grad.size(3): "<<grad.size(3)<<endl; // 12, 卷积层输出特征图宽度.

    即网络某层的残差要和该层的输入大小一致!! 以前也是这么做的. grad是上一层(第l + 1层)的残差. 
    */ 

    if (!param_.no_bias) { // 卷积层使用偏置, 要求损失关于卷积层偏置的梯度.    
      Tensor<xpu, 1, DType> gbias = in_grad[conv::kBias].get<xpu, 1, DType>(s);
      // 因为偏置是1D的tensor, 所以其梯度也是1D的. 利用get函数将卷积层的in_grad[2]拉成1D的tensor, gbias.
      Assign(gbias, req[conv::kBias], sumall_except_dim<1>(grad));
      /*
      Assign赋值操作, 赋值对象时gbias, 即损失关于卷积层的偏置bias的梯度; 数目草书模式是kBias的; exp是sumall_except_dim<1>(grad).
      即除了第1维度的, 对上一层(第l + 1层)的残差进行求和. 
      grad是4D的tensor. 上一层(第l + 1层)的损失关于输入的残差!  

      这个gbias和理论是可以对上的. 
      */
    }
  }

 private:
  inline index_t InitTemp(const mshadow::Shape<4> &ishape,
                          const mshadow::Shape<4> &oshape) {
    const int ksize_y = param_.kernel[0];
    const int ksize_x = param_.kernel[1]; // 定义ksize_y和ksize_x, 分别是kernel[0]和kernel[1]. 
    shape_colunit_ = mshadow::Shape2(ishape[1] * ksize_y * ksize_x,
                                     oshape[2] * oshape[3]); // 利用ishape和oshape, 对shape_colunit_赋值. 定义temp_col. 
    shape_dstunit_ = mshadow::Shape3(param_.num_group,
                                     param_.num_filter / param_.num_group,
                                     oshape[2] * oshape[3]); // 利用oshape, 对shape_dstunit_赋值. 定义temp_dst. 
    // param_.workspace is in elements of sizeof(DType)
    // if param_.workspace is set to zero the nstep_ equals ishape[0] (batch)
    nstep_ = std::max(
        std::min(
            static_cast<index_t>(
                param_.workspace / (shape_colunit_.Size() + shape_dstunit_.Size())),
            ishape[0]),
        1U); // 对nstep_赋值. 做for循环. 
    // cout<<"nstep_: "<<nstep_<<endl; // batch_size. 

    mshadow::Shape<2> scol = mshadow::Shape2(shape_colunit_[0],
                                             shape_colunit_[1] * nstep_); // 定义一个2D的shape, 其大小是:
    // { shape_colunit_[0] } * { shape_colunit_[1] * nstep_ }. 
    mshadow::Shape<3> sdst = mshadow::Shape3(shape_dstunit_[0],
                                             shape_dstunit_[1],
                                             shape_dstunit_[2] * nstep_); // 定义一个3D的shape, 其大小为:
    // { shape_dstunit_[0] } * { shape_dstunit_[1] } * {shape_dstunit_[2] * nstep_} 
    index_t required_size = scol.Size() + sdst.Size();
    CHECK_GE(param_.workspace, required_size)
      << "\nMinimum workspace size: " << required_size * sizeof(DType) << " Bytes\n"
      << "Given: " << param_.workspace * sizeof(DType) << " Bytes";
    return required_size; // required_size就是 scol.Size() + sdst.Size(); 即上述shape2的大小(各个维数的大小相乘) + shape3的
    // 大小. 
  }

  ConvolutionParam param_;
  mshadow::Shape<2> shape_colunit_;
  mshadow::Shape<3> shape_dstunit_;
  index_t nstep_;
  /*
  shape_colunit_ , shape_dstunit_, nstep_是定义在类ConvolutionOp中的三个变量, 运行时输出一下. 
  shape_colunit_是Shape<2>定义的, 因此只有shape_colunit_[0]和shape_colunit_[1], 即是一个二维的shape;
  shape_dstunit_是Shape<3>定义的, 因此有shape_dstunit_[0], shape_dstunit_[1], shape_dstunit_[2].

  这三个量shape_colunit_, shape_dstunit_, nstep_. 
  是在运行时指定的, 是在ConvolutionOp类的私有函数InitTemp中指定的, 最新版本的convolution-inl.h中也有一个私有函数
  LayerSetUp, 用来指定一些值. 根据ishape(本层输入shape)和oshape(本层输出shape)可以确定OP中用到的一些变量的值. 
  */
};  // class ConvolutionOp
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值