Operators in MXNet-Convolution
来源:互联网 发布:陕西师大远程网络教育 编辑:程序博客网 时间:2024/05/22 15:09
本篇文章将对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 00 7 8 9 0 0 0 0 0 0conv2(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
0 0
- Operators in MXNet-Convolution
- Operators in MXNet-Activation
- Operators in MXNet-BatchNorm
- Operators in MXNet-FullyConnected
- Operators in MXNet-Pooling
- Operators in MXNet-Dropout
- Convolution operation in CNN
- Convolution in Caffe
- 目标跟踪之ECO:Efficient Convolution Operators for Tracking
- ECO论文翻译:Efficient Convolution Operators for Tracking论文翻译
- Operators in java
- Conversion Operators in OpenCascade
- Conversion Operators in OpenCascade
- Understanding Convolution in Deep Learning
- Understanding Convolution in Deep Learning
- [cv] convolution in frequency domain
- Convolution
- Convolution
- [每日问答]正负样本不平衡应该怎么处理?
- apache 安装与配置
- 2017中国餐饮报告部分
- 请教Spark 中 combinebyKey 和 reduceByKey的传入函数参数的区别?
- 5-11
- Operators in MXNet-Convolution
- 一天一个算法: 前缀,中缀,后缀表达式的转换
- 【转载】Android开发错误:Error:” ” is not translated in “en” (English) [MissingTranslation]如何处理?
- git push之后删除node_modules
- iOS相机相册调用 — UIImagePickerController
- openwrt最专业的网站
- Android利用极光推送获取到消息并缓存至本地
- Some file crunching failed, see logs for details解决方案 以及.9patch点9图片的报错的详细修改方法
- 15.输入两棵二叉树A,B,判断B是不是A的子结构