MyException - 我的异常网
当前位置:我的异常网» C++ » MXNet-DMLC-Core代码解读与宏

MXNet-DMLC-Core代码解读与宏

www.MyException.Cn  网友分享于:2013-10-19  浏览:0次
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}
    一般情况下,宏定义全部是大写字母的,并不是说小写字母不可以,这只是方便阅读留下来的习惯,当大家看到全是字母都是大写时,就会知道,这是一个宏定义。

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

    #define max(X, Y)  ((X) > (Y) ? (X) : (Y))

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

  3. 字符串化(Stringification)
    在宏体中,如果宏参数前加个#,那么在宏体扩展的时候,宏参数会被扩展成字符串的形式。如:

    #define PRINT(x) \
        do{ \
            printf("#x = %d \n", x); }\
        while(0)

    PRINT(var)
    会被扩展成:

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

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

  4. 连接(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] ...\n");
    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], "%[^=]=%[^\n]", name, val) == 2) {
      kwargs[name] = val;
    }
  }
  printf("Docstring\n---------\n%s", MyParam::__DOC__().c_str());
  
  printf("start to set parameters ...\n");
  param.Init(kwargs);
  printf("-----\n");
  printf("param.num_hidden=%d\n", param.num_hidden);
  printf("param.learning_rate=%f\n", param.learning_rate);
  printf("param.name=%s\n", param.name.c_str());
  printf("param.activation=%d\n", 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;
}

pay

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

文章评论

程序员最害怕的5件事 你中招了吗?
程序员最害怕的5件事 你中招了吗?
60个开发者不容错过的免费资源库
60个开发者不容错过的免费资源库
那些性感的让人尖叫的程序员
那些性感的让人尖叫的程序员
那些争议最大的编程观点
那些争议最大的编程观点
初级 vs 高级开发者 哪个性价比更高?
初级 vs 高级开发者 哪个性价比更高?
我的丈夫是个程序员
我的丈夫是个程序员
科技史上最臭名昭著的13大罪犯
科技史上最臭名昭著的13大罪犯
10个调试和排错的小建议
10个调试和排错的小建议
当下全球最炙手可热的八位少年创业者
当下全球最炙手可热的八位少年创业者
漫画:程序员的工作
漫画:程序员的工作
为什么程序员都是夜猫子
为什么程序员都是夜猫子
程序猿的崛起——Growth Hacker
程序猿的崛起——Growth Hacker
鲜为人知的编程真相
鲜为人知的编程真相
编程语言是女人
编程语言是女人
10个帮程序员减压放松的网站
10个帮程序员减压放松的网站
看13位CEO、创始人和高管如何提高工作效率
看13位CEO、创始人和高管如何提高工作效率
写给自己也写给你 自己到底该何去何从
写给自己也写给你 自己到底该何去何从
程序员都该阅读的书
程序员都该阅读的书
5款最佳正则表达式编辑调试器
5款最佳正则表达式编辑调试器
总结2014中国互联网十大段子
总结2014中国互联网十大段子
一个程序员的时间管理
一个程序员的时间管理
什么才是优秀的用户界面设计
什么才是优秀的用户界面设计
做程序猿的老婆应该注意的一些事情
做程序猿的老婆应该注意的一些事情
2013年中国软件开发者薪资调查报告
2013年中国软件开发者薪资调查报告
程序员周末都喜欢做什么?
程序员周末都喜欢做什么?
 程序员的样子
程序员的样子
Web开发者需具备的8个好习惯
Web开发者需具备的8个好习惯
程序员的一天:一寸光阴一寸金
程序员的一天:一寸光阴一寸金
老程序员的下场
老程序员的下场
每天工作4小时的程序员
每天工作4小时的程序员
旅行,写作,编程
旅行,写作,编程
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
要嫁就嫁程序猿—钱多话少死的早
要嫁就嫁程序猿—钱多话少死的早
代码女神横空出世
代码女神横空出世
“肮脏的”IT工作排行榜
“肮脏的”IT工作排行榜
中美印日四国程序员比较
中美印日四国程序员比较
程序员眼里IE浏览器是什么样的
程序员眼里IE浏览器是什么样的
如何成为一名黑客
如何成为一名黑客
我是如何打败拖延症的
我是如何打败拖延症的
聊聊HTTPS和SSL/TLS协议
聊聊HTTPS和SSL/TLS协议
程序员应该关注的一些事儿
程序员应该关注的一些事儿
我跳槽是因为他们的显示器更大
我跳槽是因为他们的显示器更大
不懂技术不要对懂技术的人说这很容易实现
不懂技术不要对懂技术的人说这很容易实现
Google伦敦新总部 犹如星级庄园
Google伦敦新总部 犹如星级庄园
为啥Android手机总会越用越慢?
为啥Android手机总会越用越慢?
老美怎么看待阿里赴美上市
老美怎么看待阿里赴美上市
十大编程算法助程序员走上高手之路
十大编程算法助程序员走上高手之路
Java 与 .NET 的平台发展之争
Java 与 .NET 的平台发展之争
“懒”出效率是程序员的美德
“懒”出效率是程序员的美德
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有