Operators in MXNet-FullyConnected

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

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

/*!
 * Copyright (c) 2015 by Contributors
 * \file fully_connect_op-inl.h
 * \brief fully connect operator and symbol
*/
#ifndef MXNET_OPERATOR_FULLY_CONNECTED_INL_H_
#define MXNET_OPERATOR_FULLY_CONNECTED_INL_H_

#include <dmlc/logging.h>
#include <dmlc/parameter.h>
#include <mxnet/operator.h>
#include <map>
#include <vector>
#include <string>
#include <utility>
#include "./operator_common.h"


namespace mxnet {
namespace op {

// Declare enumeration of input order to make code more intuitive.
// These enums are only visible within this header
namespace fullc {
enum FullyConnectedOpInputs {kData, kWeight, kBias}; 
/*
全连接的输入包括:
上层输入数据: kData; 本层连接权重: kWeight; 本层连接偏置: kBias. 

在fully_connected-inl.h上先加上#include<iostream>, 然后再将kData, kOut, KBais输出, 再输出Shape的一些值. 与猜想一样, 在前向或
反向的过程中, kData, kOut, KBais是int型的数. 为0, 1, 2等数.  
*/ 
enum FullyConnectedOpOutputs {kOut}; // 输出: kOut. 
}  // fullc

struct FullyConnectedParam : public dmlc::Parameter<FullyConnectedParam> {
  int num_hidden;
  bool no_bias; 
  /*
  全连接层的参数: 
  num_hidden: 本层(全连接层)的结点个数.
  no_bias: 全连接层是否使用偏置. 
  */
  DMLC_DECLARE_PARAMETER(FullyConnectedParam) { // #define DMLC_DECLARE_PARAMETER(PType)
    // TODO(bing) add support for boolean
    DMLC_DECLARE_FIELD(num_hidden).set_lower_bound(1) // 全连接层的结点个数最少是1. set_lower_bound设置下界. 
    .describe("Number of hidden nodes of the output.");
    /*
    DMLC_DECLARE_FIELD(num_hidden).set_lower_bound(1) 利用宏DMLC_DECLARE_FIELD对全连接层的参数num_hidden进行描述, 并且参数有
    默认值或最小, 最大值.  
    */
    DMLC_DECLARE_FIELD(no_bias).set_default(false)
    .describe("Whether to disable bias parameter.");
    /*
    DMLC_DECLARE_FIELD(no_bias).set_lower_bound(1) 利用宏DMLC_DECLARE_FIELD对全连接层的参数no_bias进行描述, 并且参数有
    默认值或最小, 最大值. 默认使用偏置. 
    */
  }
};

/**
 * \brief This is the implementation of fully connected operator.
 * \tparam xpu The device that the op will be executed on.
 */

/*
仅是全连接层, 无激活函数层:
a^(l) = W' * z^(l)
z^(l + 1) = a^(l)
a^(l + 1) = W' * z^(l + 1)
*/

template<typename xpu, typename DType> 
/*
全连接层的计算只有一种形式, 这里全连接层不包含激活函数, 即输出out = W .* X + b. 因此在定义FullyConnectedOp类时的模板参数只有
xpu和DType. 

在make mxnet的时候, 屏幕会根据config.mk给出xpu和DType的值. 
[with xpu = mshadow::op::cpu, DType = float] 
*/ 
class FullyConnectedOp : public Operator {
 public:
  explicit FullyConnectedOp(FullyConnectedParam p) {
    this->param_ = p;
    /*
    C++中的explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的, 跟
    它相对应的另一个关键字是implicit, 意思是隐藏的, 类构造函数默认情况下即声明为implicit(隐式).

    this指针是一个隐含于每一个成员函数中的特殊指针. 它指向正在被该成员函数操作的那个对象. 

    p是FullyConnectedParam全连接层参数类的对象, 将p赋值给param_. 这和单纯的赋值不一样, param_就是p, 可以看做是指向p的指针.
    */
  }

  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) {
    using namespace mshadow;
    using namespace mshadow::expr;
    if (req[fullc::kOut] == kNullOp) return; // 全连接层的数据操作模式不能是kNullOp, 即什么都不做. 
    CHECK_EQ(req[fullc::kOut], kWriteTo); 
    /*
    case kNullOp:                     \
          break;                          \
        case kWriteTo:                    \
        case kWriteInplace:               \
          (out) = (exp);                  \
          break;                          \
        case kAddTo:                      \
          (out) += (exp); 
    */

    size_t expected = param_.no_bias ? 2 : 3;
    /*
    param_.no_bias是否为真, 如果为真则expected为2, 否则为3. 
    */

    CHECK_EQ(in_data.size(), expected);
    CHECK_EQ(out_data.size(), 1);
    // TODO(bing): check the BLAS Handle, be careful
    // maybe need blas handle from context
    // TODO(bing): judge shape to remove flatten op
    Stream<xpu> *s = ctx.get_stream<xpu>();
#if defined(__CUDACC__)
    CHECK_EQ(s->blas_handle_ownership_, Stream<xpu>::OwnHandle)
        << "Must init CuBLAS handle in stream";
#endif  // __CUDACC__

    // std::cout<<"in_data kData: "<<fullc::kData<<std::endl; 这里fullc::kData是0, 即数据data是in_data[0]. 
    const TShape& ishape = in_data[fullc::kData].shape_;

    // std::cout<<"out_data kOut: "<<fullc::kOut<<std::endl; 这里fullc::kOut是0, 即数据data是out_data[0].
    /*
    在mxnet中, kData是0. 代表数据. 
    */ 
    const TShape& oshape = out_data[fullc::kOut].shape_;
    /*
    定义输入数据in_data[0]的shape和输出数据out_data[0]的shape.

    TShape mxnet::TBlob::shape_, 可以用来定义Tensor的shape. TBlob类的成员, 返回值类型是TShape. 
    */

    // std::cout<<"in_data kData: "<<fullc::kData<<std::endl; 这里fullc::kData是0, 即数据data是in_data[0].
    // std::cout<<"ishape: "<<ishape[0]<<" "<<ishape.ndim()<<std::endl; ishape[0]是64, 64是batch_size, 即批训练量的大小.  
    // ishape.ndim()是2, 即ishape是一个2维的. 
    Tensor<xpu, 2, DType> data = in_data[fullc::kData].get_with_shape<xpu, 2, DType>(
        Shape2(ishape[0], ishape.ProdShape(1, ishape.ndim())), s);
    /*
    将in_data[0](输入数据)拉成2维的张量. 这里将TBlob数据拉成Tensor数据时没有使用FlatTo2D, 而是用了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, 将TBlob拉成一个Tensor. 如果shape和存储的大小不一致时, 会报错.

    在https://raw.githubusercontent.com/jdeng/gomxnet/master/mxnet.cc可以找到Shape1, Shape2, Shape3, Shape4的定义:
    Shape2定义如下:
    MSHADOW_XINLINE Shape<2> Shape2(index_t s0, index_t s1) {
        Shape<2> s; s[0] = s0; s[1] = s1;
        return s;
    } 因此, Shape2就是Shape<dim>类型的函数, Shape2返回Shape<2>的对象. 正好和get_with_shape函数的第一个参数相对应.

    Shape2(ishape[0], ishape.ProdShape(1, ishape.ndim())):
    index_t是一种索引类型(typedef mshadow::index_t mxnet::index_t), 其中s0是ishape[0]. s1是ishape.ProdShape(1, ishape.ndim()).

    index_t的定义在mshadow/mshadow/base.h下.
    typedef unsigned index_t; 
    unsigned a; 与unsigned int a; 是同样的. 这里省略了int, 只能和unsigned int等价.  

    ProdShape是类TShape类下的成员函数: 可以通过寻找TShape类来找到该函数:

    index_t mxnet::TShape::ProdShape(int dimstart, int dimend )const 
    生成一个索引, 索引属于[dimstart,dimend), 返回值类型是index_t, 即是一个索引类型. 因此, 
    ishape.ProdShape(1, ishape.ndim())就产生一个[1, 2)的索引.   
    */    

    // std::cout<<"in_data kWeight: "<<fullc::kWeight<<std::endl; fullc::kWeight是1, 在in_data中kWeight代表1, 权重.    
    Tensor<xpu, 2, DType> wmat = in_data[fullc::kWeight].get<xpu, 2, DType>(s);
    /*
    将本层(第l层)的权重in_data[kWeight]拉成2维的张量. 这次并没有使用get_with_shape, 而是使用get函数:
    mshadow::Tensor<Device, dim, DType> mxnet::TBlob::get(mshadow::Stream<Device> *stream = NULL)const 
    */

    // std::cout<<"out_data kOut: "<<fullc::kOut<<std::endl; fullc::kOut是0, kData和kOut均是0.
    // std::cout<<"oshape: "<<oshape[0]<<" "<<oshape.ndim()<<std::endl; oshape和ishape一样, 64 2. 
    Tensor<xpu, 2, DType> out = out_data[fullc::kOut].get_with_shape<xpu, 2, DType>(
        Shape2(oshape[0], oshape.ProdShape(1, oshape.ndim())), s);  
    /*
    将out_data[0](输出数据)拉成2维的张量. 这里将TBlob数据拉成Tensor数据时没有使用FlatTo2D, 而是用了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, 将TBlob拉成一个Tensor. 如果shape和存储的大小不一致时, 会报错.

    定义out时和定义data的方法是一致的. 一个数本层(第l层)的输入, 一个是本层(第l层)的输出. 
    */    

    out = dot(data, wmat.T());
    /*计算输出out. 
    根据全连接层的前向传播操作可知: 全连接层的输出 = W.*输入 + 偏置.

    因此如果全连接层没有偏置项, 那么 out = data .* weight. 

    dot函数: http://mxnet.io/api/python/symbol.html, 计算两个数组的点乘之积. 向量点乘(1D)或矩阵乘法(2D)... 

    wmat.T()是转置, 即权重的转置. 
    */

    if (!param_.no_bias) { // 如果使用偏置, 那么out += ... + bias. 
      // std::cout<<"in_data kData: "<<fullc::kBias<<std::endl; fullc::kBias是2, 代表偏置. 
      Tensor<xpu, 1, DType> bias = in_data[fullc::kBias].get<xpu, 1, DType>(s);
      /*
      将本层(第l层)的偏置in_data[kBias]拉成1维的张量. 这次并没有使用get_with_shape, 而是使用get函数:
      mshadow::Tensor<Device, dim, DType> mxnet::TBlob::get(mshadow::Stream<Device> *stream = NULL)const 

      1维的张量即是一个向量. 
      */

      out += repmat(bias, data.size(0));
      /*这里也做一下输出, 看一下mxnet的全连接是如何实现的. 
      // std::cout<<"data.size: "<<data.size<<std::endl; Tensor张量的大小不能这样输出. 
      // std::cout<<"bias: "<<bias<<std::endl; Tensor张量不能这样输出. 

      std::cout<<"data.size(0): "<<data.size(0)<<std::endl; 输出为64, 即batch_size. 

      =====================================================批处理过程========================================================
      批处理的过程, 批处理大小是batch_size. 网络(上次更新完参数)->输入batch_size个样本进行正向传播, 
      批处理时的正向传播是一次性将batch-size个样本输入, 得到batch_size个输出(简单地, 将batch_size看成1即可)
      ->利用batch_size个标签进行反向传播, 更新网络的参数->得到更新完参数的网络->下一个batch的样本... 
      ====================================================批处理过程=========================================================

      因此批处理时的全连接层data里有batch_size个样本的数据, out也是batch_size个输出, 但是网络就一个, 因此权重wmat.T()就一个.
      然后全连接层如果有偏置项, 再加上偏置项即可. 

      out一共是batch_size个样本的输出, 因此没有个输出都应该加上一个bias, 而bias是一个列向量, 因此先将bias进行复制, 利用函数
      repmat进行复制, 一共复制batch_size个, 然后再和 W.*X进行相加即可.
      */
    }
  }

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

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

    using namespace mshadow;
    using namespace mshadow::expr;
    CHECK_EQ(out_grad.size(), 1);
    size_t expected = param_.no_bias ? 2 : 3;
    /*
    定义expected, 若全连接层的没有偏置, 则expected是2, 如果有偏置, expected是3. 即expected代表了in_data里TBolb对象的个数. 
    默认no_bias是false, 即有偏置项. 
    */

    // CHECK宏加以断言标记, 来保证程序的严谨性.
    CHECK(in_data.size() == expected && in_grad.size() == expected);
    // in_data[0], in_data[1], in_data[2]...
    CHECK_EQ(req.size(), expected);
    // 对于数据data, weight, bias有不同的数据操作模式.  

    // TODO(bing): check the BLAS Handle, be careful
    //  maybe need blas handle from context
    Stream<xpu> *s = ctx.get_stream<xpu>();

        // std::cout<<"in_data kData: "<<fullc::kData<<std::endl; 这里fullc::kData是0, 即数据data是in_data[0]. 
    const TShape& ishape = in_data[fullc::kData].shape_;

    // std::cout<<"out_data kOut: "<<fullc::kOut<<std::endl; 这里fullc::kOut是0, 即数据data是out_data[0].
    /*
    在mxnet中, kData是0. 代表数据. 
    */ 
    const TShape& oshape = out_grad[fullc::kOut].shape_;
    /*
    定义输入数据in_data[0]的shape和输出残差out_grad[0]的shape.

    TShape mxnet::TBlob::shape_, 可以用来定义Tensor的shape. TBlob类的成员, 返回值类型是TShape. 
    */

    // std::cout<<"in_data kData: "<<fullc::kData<<std::endl; 这里fullc::kData是0, 即数据data是in_data[0].
    // std::cout<<"ishape: "<<ishape[0]<<" "<<ishape.ndim()<<std::endl; ishape[0]是64, 64是batch_size, 即批训练量的大小.  
    // ishape.ndim()是2, 即ishape是一个2维的. 
    Tensor<xpu, 2, DType> data = in_data[fullc::kData].get_with_shape<xpu, 2, DType>(
        Shape2(ishape[0], ishape.ProdShape(1, ishape.ndim())), s);
    /*
    将in_data[0](本层(第l层)的输入数据)拉成2维的张量. 这里将TBlob数据拉成Tensor数据时没有使用FlatTo2D, 而是用了get_with_shape. 
    */    

    // std::cout<<"in_data kWeight: "<<fullc::kWeight<<std::endl; fullc::kWeight是1, 在in_data中kWeight代表1, 权重.    
    Tensor<xpu, 2, DType> wmat = in_data[fullc::kWeight].get<xpu, 2, DType>(s);
    /*
    将本层(第l层)的权重in_data[kWeight]拉成2维的张量. 这次并没有使用get_with_shape, 而是使用get函数.
    */

    // std::cout<<"out_data kOut: "<<fullc::kOut<<std::endl; fullc::kOut是0, kData和kOut均是0.
    // std::cout<<"oshape: "<<oshape[0]<<" "<<oshape.ndim()<<std::endl; oshape和ishape一样, 64 2. 
    Tensor<xpu, 2, DType> grad = out_grad[fullc::kOut].get_with_shape<xpu, 2, DType>(
        Shape2(oshape[0], oshape.ProdShape(1, oshape.ndim())), s);  
    /*
    将out_grad[0](残差)拉成2维的张量. 这里将TBlob数据拉成Tensor数据时没有使用FlatTo2D, 而是用了get_with_shape. 
    grad就代表上一层(第l + 1层的残差). 
    */  

#if defined(__CUDACC__)
    CHECK_EQ(s->blas_handle_ownership_, Stream<xpu>::OwnHandle)
        << "Must init CuBLAS handle in stream";
#endif

    //  backprop
    CHECK_NE(req[fullc::kWeight], kWriteInplace) << "cannot write weight inplace";
    /*
    #define CHECK_NE(val1, val2) CHECK_OP(_NE, !=, val1, val2)
    权重梯度的数据操作模式不能是kWriteInplace. 一般情况下, 所有的out_data的数据操作类型应该是kWriteTo, 在计算表示gradient
    的tensor的时候, 我们最好是将梯度累加起来, req的类型应该是kAddTo, 表示应该调用+=操作.  
    */

    // gradient of weight
    Tensor<xpu, 2, DType> gwmat = in_grad[fullc::kWeight].get<xpu, 2, DType>(s);
    Assign(gwmat, req[fullc::kWeight], dot(grad.T(), data));
    /*计算本层(第l层)权重的梯度, 这里仅是全连接层, 并没有激活函数作用.
    in_grad是本层(第l层)的梯度TBlob, 将in_grad[1](第l层关于权重的梯度)拉成2维的张量. 即gwmat代表第l层的损失 J 关于权重的梯度.
    权重是矩阵.

    赋值操作, 数据操作模式是req[fullc::kWeight], 应该是kAddTo, 表示应该调用+=操作. 

    根据http://ufldl.stanford.edu/wiki/index.php/反向传导算法 计算损失 J 关于权重和偏置的最后结果, 损失关于第l层权重的梯度是:
    delta^(l + 1) * [a^(l)]'. 
    上一层(第l + 1层)的残差 * [本层(第l层)的输出数据]'. 因此失关于第l层损权重的梯度是: grad.T() 和 data 做点积. 矩阵乘法. 
    */

    // gradient of bias
    if (!param_.no_bias) {
      Tensor<xpu, 1, DType> gbias = in_grad[fullc::kBias].get<xpu, 1, DType>(s);
      Assign(gbias, req[fullc::kBias], sum_rows(grad));
    }
    /*计算本层(第l层)偏置的梯度, 这里仅是全连接层, 并没有激活函数作用.
    in_grad是本层(第l层)的梯度TBlob, 将in_grad[2](第l层关于权重的偏置)拉成1维的张量. 即gbias代表第l层的损失 J 关于偏置的梯度.
    偏置是向量. 

    如果本层(第l层)全连接层使用偏置, 损关于第l层偏置的梯度是: delta^(l + 1).
    上一层(第l + 1层)的残差. 

    赋值操作, 数据操作模式是req[fullc::kWeight]. 损失关于本层(第l层)偏置的梯度是sum_rows(grad). 
    */

    // gradient of data
    Tensor<xpu, 2, DType> gdata = in_grad[fullc::kData].get_with_shape<xpu, 2, DType>(
        Shape2(ishape[0], ishape.ProdShape(1, ishape.ndim())), s);
    Assign(gdata, req[fullc::kData], dot(grad, wmat));
    /*计算本层(第l层)data的梯度. 即损失J关于FC层的残差, 这个残差并不会对下一次的FC层的前向传播产生影响, 但是会利用gdata计算FC
    层前一层(第l - 1)层的残差.

    in_grad是本层(第l层)的梯度TBlob, 将in_grad[0](第l层关于权重的data)拉成2维的张量. 即gdata代表第l层的损失 J 关于data的梯度.
    data是矩阵. 
    值操作, 数据操作模式是req[fullc::kData]. 本层(第l层)损失关于data的梯度是: W' * delta^(l + 1). FC层没有激活函数! 
    本层的权重wmat和上一层(第l + 1层)的梯度做点乘. 这里应该是做矩阵乘法. 
    */
  }

 private:
  FullyConnectedParam param_;
};  // class FullyConnectedOp

猜你喜欢

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