• MXNet--DMLC-Core代码解读与宏


    MXNet--DMLC-Core代码解读与宏

    dmlc-core是Distributed (Deep) Machine Learning Community的一个基础模块,这个模块用被应用到了mxnet中。dmlc-core在其中用了比软多的宏技巧,代码写得很简洁,值得大家学习。这博客中讲解了其中的宏和mxnet中是怎么向dmlc-core中注册函数和初始化参数的。

    宏(Macros)的一般用法与特殊用法

    C/C++中的宏是编译的预处理,主要用要文本替换。文本替换就有很多功能,比如用来控制编译的选项、生成代码等。在C++没有被发明之前,宏的技巧经常会被用于编程中,这些技巧对大部分人来说是难以快速理解的,毕竟代码是写给人看的,不是写给机器看的,所以很多人称这些为奇技淫巧。C++出现后,发明了继承、动态绑定、模板这些现代的面向对象编程概念之后,很多本来用宏技巧写的代码被类替换了。但如果宏用得对,可以使代码更加简洁。

    1. 标示符别名
    #define NUM 1024
    

    比如在预处理阶段:foo = (int *) malloc (NUM*sizeof(int))
    会被替换成foo = (int *) malloc (1024*sizeof(int))
    另外,宏体换行需要在行末加反斜杠

    #define ARRAY 1, 
                  2, 
                  3, 
                  NUM
    

    比如预处理阶段int x[] = { ARRAY }
    会被扩展成int x[] = { 1, 2, 3, 1024}
    一般情况下,宏定义全部是大写字母的,并不是说小写字母不可以,这只是方便阅读留下来的习惯,当大家看到全是字母都是大写时,就会知道,这是一个宏定义。

    1. 宏函数
      宏名之后带括号的宏是宏函数。用法与普通函数是一样的,但是在编译时会被展开。优点是没有普通函数保存寄存器和参数传递的开销、速度快,缺点是可执行代码体积大。这个现在一般都可能被设计成内敛函数(inline function)。
    #define max(X, Y)  ((X) > (Y) ? (X) : (Y))
    

    如在预处理时:a = max(1, 2)
    会被扩展成:a = ((1) < (2) ? (1) : (2))

    1. 字符串化(Stringification)
      在宏体中,如果宏参数前加个#,那么在宏体扩展的时候,宏参数会被扩展成字符串的形式。如:
    #define PRINT(x) 
    		do{ 
            	printf("#x = %d 
    ", x); }
            while(0)
    

    PRINT(var)
    会被扩展成:

    do{ 
    	printf("var = %d 
    ", var); }
    while(0)
    

    这种用法可以用在assert中,可以直接输出相关的信息。

    1. 连接(Concatenation)
      在宏体中,如果宏体所在标示符中有##,那么在宏体扩展的时候,宏参数会被直接替换到标示符中。如宏定义如下:
    #define COMMAND(NAME)  { #NAME, NAME ## _command }
    struct command
    {
        char *name;
        void (*function) (void);
    };
    

    在用到宏的时候的:

    struct command commands[] =
    {
        COMMAND (quit),
        COMMAND (help),
        ...
    };
    

    会被扩展成:

    struct command commands[] =
    {
        { "quit", quit_command },
        { "help", help_command },
        ...
    };
    

    这样写法会比较简洁,提高了编程的效率。

    上述的前两种用法宏的一般用法,后两种用法则是宏的特殊用法。结果这几种用法,宏可以生成很多很多很绕的技巧,比如做递归等等。

    MXNet--DMLC-Core中的宏

    在上一篇博客——mxnet的训练过程——从python到C++中提到:“当用C++写一个新的层时,都要先注册到mxnet内核dlmc中”。这个注册就是用宏来实现的,这里有两个参考的资料,一个是说了参数的数据结构,只要解读了parameter.h这个文件,详见:/dmlc-core/parameter.h;另一个是说明了参数结构是怎么工作的Parameter Structure for Machine Learning。这两个里面的东西我就不详细讲述了,下面是结合这两个来说明DMLC-Core宏的工作原理的,对参数结构的描述不如/dmlc-core/parameter.h详细。所有的代码来自dmlc-core或者mxnet内的dmlc-core中。

    编译与执行

    下载并编译dmlc-core的代码,编译出example下载的paramter可执行文件并执行:

    git clone https://github.com/dmlc/dmlc-core.git
    cd dmlc-core
    make all
    make example
    ./example/parameter num_hidden=100 name=aaa activation=relu
    

    执行结果如下:

    Docstring
    ---------
    num_hidden : int, required
        Number of hidden unit in the fully connected layer.
    learning_rate : float, optional, default=0.01
        Learning rate of SGD optimization.
    activation : {'relu', 'sigmoid'}, required
        Activation function type.
    name : string, optional, default='mnet'
        Name of the net.
    start to set parameters ...
    -----
    param.num_hidden=100
    param.learning_rate=0.010000
    param.name=aaa
    param.activation=1
    

    Parameter字类中的宏

    我们以parameter.cc为切入点,看DMLC的宏是如何扩展生成代码的:

    struct MyParam : public dmlc::Parameter<MyParam> {
      float learning_rate;
      int num_hidden;
      int activation;
      std::string name;
      // declare parameters in header file
      DMLC_DECLARE_PARAMETER(MyParam) {
        DMLC_DECLARE_FIELD(num_hidden).set_range(0, 1000)
            .describe("Number of hidden unit in the fully connected layer.");
        DMLC_DECLARE_FIELD(learning_rate).set_default(0.01f)
            .describe("Learning rate of SGD optimization.");
        DMLC_DECLARE_FIELD(activation).add_enum("relu", 1).add_enum("sigmoid", 2)
            .describe("Activation function type.");
        DMLC_DECLARE_FIELD(name).set_default("mnet")
            .describe("Name of the net.");
    
        // user can also set nhidden besides num_hidden
        DMLC_DECLARE_ALIAS(num_hidden, nhidden);
        DMLC_DECLARE_ALIAS(activation, act);
      }
    };
    
    // register it in cc file
    DMLC_REGISTER_PARAMETER(MyParam);
    

    先看下DMLC_DECLARE_PARAMETER的定义,这个定义先声明了一个函数____MANAGER__,但并没有定义,第二个是声明了函数__DECLARE__,定义在上面代码的第8到第19行,包括在大括号内。__DECLARE__这个函数体内也有用到了宏。

    #define DMLC_DECLARE_PARAMETER(PType)                                   
      static ::dmlc::parameter::ParamManager *__MANAGER__();                
      inline void __DECLARE__(::dmlc::parameter::ParamManagerSingleton<PType> *manager) 
    

    要注意的DMLC_DECLARE_FIELD是只能用在__DECLARE__这个函数内的宏,这个宏的定义如下,这个宏返回的是一个对象,.set_range这些返回的也是对象。DMLC_DECLARE_ALIAS这个是一个对齐的宏,对齐后可以两个名字没有区别,都可以用。比如DMLC_DECLARE_ALIAS(num_hidden, nhidden),那么num_hiddennhidden是一样的,之前的运行命令就可以这样执行:./example/parameter nhidden=100 name=aaa act=relu,执行的结果没有任何区别。

    #define DMLC_DECLARE_FIELD(FieldName)  this->DECLARE(manager, #FieldName, FieldName)
    #define DMLC_DECLARE_ALIAS(FieldName, AliasName)  manager->manager.AddAlias(#FieldName, #AliasName)
    

    类似于DECLARE这样的成员函数是定义在父类struct Parameter中的,之后所有的自义MyParam都要直接继承这个父类。AddAlias这个函数定义在class ParamManager中,这些函数都在同一个文件parameter.h中。

    我们继续来看下一个宏DMLC_REGISTER_PARAMETER,在上一篇博客——mxnet的训练过程——从python到C++中就提到有一个宏是注册相关层的到内核中的,这个是注册到参数到内核中。这个宏的定义以下:

    #define DMLC_REGISTER_PARAMETER(PType)                                  
      ::dmlc::parameter::ParamManager *PType::__MANAGER__() {               
        static ::dmlc::parameter::ParamManagerSingleton<PType> inst(#PType); 
        return &inst.manager;                                               
      }                                                                     
      static DMLC_ATTRIBUTE_UNUSED ::dmlc::parameter::ParamManager&         
      __make__ ## PType ## ParamManager__ =                                 
          (*PType::__MANAGER__())            
    

    这个宏定义了上面声明的__MANAGER__,这个函数新建了一个ParamManagerSingleton的实例,并返回一个ParamManager的实例。注意到inst这个变量是用static修饰的,也就是说inst(包括他的成员manager)只会被初始化一次。并且定义了一个全局的manager,按上面所说的##连接法则,这个变量的名字是__make__MyparamParamManager__

    新建一个ParamManagerSingleton的实例时,我们可以看到它的构造函数调用了上面用宏生成的函数__DECLARE__,对它的成员manager中的成员进行了赋值。

    template<typename PType>
    struct ParamManagerSingleton {
      ParamManager manager;
      explicit ParamManagerSingleton(const std::string &param_name) {
        PType param;
        param.__DECLARE__(this);
        manager.set_name(param_name);
      }
    };
    

    测试

    我们来看下主函数:

    int main(int argc, char *argv[]) {
      if (argc == 1) {
        printf("Usage: [key=value] ...
    ");
        return 0;
      }
    
      MyParam param;
      std::map<std::string, std::string> kwargs;
      for (int i = 0; i < argc; ++i) {
        char name[256], val[256];
        if (sscanf(argv[i], "%[^=]=%[^
    ]", name, val) == 2) {
          kwargs[name] = val;
        }
      }
      printf("Docstring
    ---------
    %s", MyParam::__DOC__().c_str());
      
      printf("start to set parameters ...
    ");
      param.Init(kwargs);
      printf("-----
    ");
      printf("param.num_hidden=%d
    ", param.num_hidden);
      printf("param.learning_rate=%f
    ", param.learning_rate);
      printf("param.name=%s
    ", param.name.c_str());
      printf("param.activation=%d
    ", param.activation);
      return 0;
    }
    

    这里中最主要的就是param.Init(kwargs),这个是初始化这个变量,__MANAGER__返回的正是上面生成的__make__MyparamParamManager__,然后在RunInit中对字典遍历,出现的值就赋到相应的位置上,没有出现的就用默认值,然后再检查参数是否合法等,找相应该的位置是通过这个MyParam的头地址到相应参数的地址的offset来定位的。

    template<typename Container>
    inline void Init(const Container &kwargs,
    				parameter::ParamInitOption option = parameter::kAllowHidden) {
    	PType::__MANAGER__()->RunInit(static_cast<PType*>(this),
    								  kwargs.begin(), kwargs.end(),
                                      NULL,
                                      option);
    }
    

    注册函数(层)

    fully_connected.cc用以下的方法来注册:

    MXNET_REGISTER_OP_PROPERTY(FullyConnected, FullyConnectedProp)
    .describe(R"code(Applies a linear transformation: :math:`Y = XW^T + b`.
    If ``flatten`` is set to be true, then the shapes are:
    - **data**: `(batch_size, x1, x2, ..., xn)`
    - **weight**: `(num_hidden, x1 * x2 * ... * xn)`
    - **bias**: `(num_hidden,)`
    - **out**: `(batch_size, num_hidden)`
    If ``flatten`` is set to be false, then the shapes are:
    - **data**: `(x1, x2, ..., xn, input_dim)`
    - **weight**: `(num_hidden, input_dim)`
    - **bias**: `(num_hidden,)`
    - **out**: `(x1, x2, ..., xn, num_hidden)`
    The learnable parameters include both ``weight`` and ``bias``.
    If ``no_bias`` is set to be true, then the ``bias`` term is ignored.
    )code" ADD_FILELINE)
    .add_argument("data", "NDArray-or-Symbol", "Input data.")
    .add_argument("weight", "NDArray-or-Symbol", "Weight matrix.")
    .add_argument("bias", "NDArray-or-Symbol", "Bias parameter.")
    .add_arguments(FullyConnectedParam::__FIELDS__());
    

    宏定义MXNET_REGISTER_OP_PROPERTY如下:

    #define MXNET_REGISTER_OP_PROPERTY(name, OperatorPropertyType)          
      DMLC_REGISTRY_REGISTER(::mxnet::OperatorPropertyReg, OperatorPropertyReg, name) 
      .set_body([]() { return new OperatorPropertyType(); })                
      .set_return_type("NDArray-or-Symbol") 
      .check_name()
    
    #define DMLC_REGISTRY_REGISTER(EntryType, EntryTypeName, Name)          
      static DMLC_ATTRIBUTE_UNUSED EntryType & __make_ ## EntryTypeName ## _ ## Name ## __ = 
          ::dmlc::Registry<EntryType>::Get()->__REGISTER__(#Name)           
    

    第二个宏的同样有关键字static,说明注册只发生一次。我们只要看一下::dmlc::Registry<EntryType>::Get()->__REGISTER__(#Name)这个函数,函数Get()在以下的宏被定义,这个宏在operator.ccDMLC_REGISTRY_ENABLE(::mxnet::OperatorPropertyReg)运行了。可以看到这个宏里同样有关键字static说明生成的得到的Registry是同一个。

    #define DMLC_REGISTRY_ENABLE(EntryType)                                 
      template<>                                                            
      Registry<EntryType > *Registry<EntryType >::Get() {                   
        static Registry<EntryType > inst;                                   
        return &inst;                                                       
      }
    

    再来看__REGISTER__(#Name),这个函数是向得到的同一个Registry的成员变量fmap_写入名字,并返回一个相关对象。这样就向内核中注册了一个函数,可以看到在上一篇博客——mxnet的训练过程——从python到C++提到的动态加载函数,就是通过遍历Registry中的成员来获取所有的函数。

    inline EntryType &__REGISTER__(const std::string& name) {
    	CHECK_EQ(fmap_.count(name), 0U)
            << name << " already registered";
        EntryType *e = new EntryType();
        e->name = name;
        fmap_[name] = e;
        const_list_.push_back(e);
        entry_list_.push_back(e);
        return *e;
    }
    

    【防止爬虫转载而导致的格式问题——链接】:
    http://www.cnblogs.com/heguanyou/p/7613191.html

  • 相关阅读:
    namespace for c++
    [爬虫]Python爬虫基础
    为什么局域网IP是192.168开头?
    [Python基础]Python中remove,del和pop的区别
    权限控制(delphi actionlist)
    Delphi ActionList详解
    充分利用HTML标签元素 – 简单的xtyle前端框架
    好用的侧边栏菜单/面板jQuery插件
    最新的手机/移动设备jQuery插件
    有用的jQuery布局插件推荐
  • 原文地址:https://www.cnblogs.com/heguanyou/p/7613191.html
Copyright © 2020-2023  润新知