MXNet--DMLC-Core代码解读与宏
MXNet--DMLC-Core代码解读与宏
dmlc-core是Distributed (Deep) Machine Learning Community的一个基础模块,这个模块用被应用到了mxnet中。dmlc-core在其中用了比软多的宏技巧,代码写得很简洁,值得大家学习。这博客中讲解了其中的宏和mxnet中是怎么向dmlc-core中注册函数和初始化参数的。
宏(Macros)的一般用法与特殊用法
C/C++中的宏是编译的预处理,主要用要文本替换。文本替换就有很多功能,比如用来控制编译的选项、生成代码等。在C++没有被发明之前,宏的技巧经常会被用于编程中,这些技巧对大部分人来说是难以快速理解的,毕竟代码是写给人看的,不是写给机器看的,所以很多人称这些为奇技淫巧。C++出现后,发明了继承、动态绑定、模板这些现代的面向对象编程概念之后,很多本来用宏技巧写的代码被类替换了。但如果宏用得对,可以使代码更加简洁。
- 标示符别名
#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}
一般情况下,宏定义全部是大写字母的,并不是说小写字母不可以,这只是方便阅读留下来的习惯,当大家看到全是字母都是大写时,就会知道,这是一个宏定义。
- 宏函数
宏名之后带括号的宏是宏函数。用法与普通函数是一样的,但是在编译时会被展开。优点是没有普通函数保存寄存器和参数传递的开销、速度快,缺点是可执行代码体积大。这个现在一般都可能被设计成内敛函数(inline function)。
#define max(X, Y) ((X) > (Y) ? (X) : (Y))
如在预处理时:a = max(1, 2)
会被扩展成:a = ((1) < (2) ? (1) : (2))
- 字符串化(Stringification)
在宏体中,如果宏参数前加个#,那么在宏体扩展的时候,宏参数会被扩展成字符串的形式。如:
#define PRINT(x) \
do{ \
printf("#x = %d \n", x); }\
while(0)
如PRINT(var)
:
会被扩展成:
do{ \
printf("var = %d \n", var); }\
while(0)
这种用法可以用在assert中,可以直接输出相关的信息。
- 连接(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_hidden
与nhidden
是一样的,之前的运行命令就可以这样执行:./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 ¶m_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;
}
【防止爬虫转载而导致的格式问题——链接】:
http://www.cnblogs.com/heguanyou/p/7613191.html
MXNet--DMLC-Core代码解读与宏的更多相关文章
- Android MVP模式 谷歌官方代码解读
Google官方MVP Sample代码解读 关于Android程序的构架, 当前(2016.10)最流行的模式即为MVP模式, Google官方提供了Sample代码来展示这种模式的用法. Repo ...
- 在Mac上使用Visual Studio Code开发/调试.NET Core代码
.Net Core 1.0终于发布了,Core的一大卖点就是跨平台.这个跨平台不只是跨平台运行,而且可以跨平台开发.今天抽空研究了下在Mac下如何使用VS Code来开发.NET Core程序,并且调 ...
- 优秀开源代码解读之JS与iOS Native Code互调的优雅实现方案
简介 本篇为大家介绍一个优秀的开源小项目:WebViewJavascriptBridge. 它优雅地实现了在使用UIWebView时JS与ios 的ObjC nativecode之间的互调,支持消息发 ...
- SoftmaxLayer and SoftmaxwithLossLayer 代码解读
SoftmaxLayer and SoftmaxwithLossLayer 代码解读 Wang Xiao 先来看看 SoftmaxWithLoss 在prototext文件中的定义: layer { ...
- Hybrid----优秀开源代码解读之JS与iOS Native Code互调的优雅实现方案-备
本篇为大家介绍一个优秀的开源小项目:WebViewJavascriptBridge. 它优雅地实现了在使用UIWebView时JS与ios 的ObjC nativecode之间的互调,支持消息发送.接 ...
- Jsoup代码解读之六-防御XSS攻击
Jsoup代码解读之八-防御XSS攻击 防御XSS攻击的一般原理 cleaner是Jsoup的重要功能之一,我们常用它来进行富文本输入中的XSS防御. 我们知道,XSS攻击的一般方式是,通过在页面输入 ...
- Jsoup代码解读之五-实现一个CSS Selector
Jsoup代码解读之七-实现一个CSS Selector 当当当!终于来到了Jsoup的特色:CSS Selector部分.selector也是我写的爬虫框架webmagic开发的一个重点.附上一张s ...
- Jsoup代码解读之四-parser
Jsoup代码解读之四-parser 作为Java世界最好的HTML 解析库,Jsoup的parser实现非常具有代表性.这部分也是Jsoup最复杂的部分,需要一些数据结构.状态机乃至编译器的知识.好 ...
- Jsoup代码解读之三-Document的输出
Jsoup代码解读之三-Document的输出 Jsoup官方说明里,一个重要的功能就是output tidy HTML.这里我们看看Jsoup是如何输出HTML的. HTML相关知识 分析代码前 ...
随机推荐
- Jmeter之接口测试
最近才入职新公司,好几天没有写博客了,经过一个朋友提醒,刚刚好觉得用Jmeter来做接口测试真的是再好不过了.下面就详细讲解下这两天我利用Jmeter做的接口测试. [安装Jmeter] 详细见博文: ...
- Wifi长距离传输
本人从事Linux驱动开发.现在要实现两端之间wifi长距离(1km左右)传输视频数据(全向天线)的功能.目前用的平台是Atheros AR9342.我在网上查到一些资料是关于禁用802.11的CSM ...
- .NET CORE 学习笔记之安装EF【Microsoft.EntityFrameworkCore】扩展报错
最近在学习.NET CORE ,刚开始就遇到问题了. 安装EF框架的试试就报错, 报错如下: 错误 程序包还原失败.正在回滚“XXX”的程序包更改. 找了好久的方案,网上也没搜到对应的问题和方案,然而 ...
- JAVA实用案例之邮件发送
最近有朋友问邮件怎么发送,就简单写了个demo,因为懒得找jar包,所以项目是创建的maven工程,具体的maven引用的jar如下: <dependency> <groupId&g ...
- 日志log4j配置详情,日志log具体到你想不到
一.Log4j简介Log4j有三个主要的组件:Loggers(记录器),Appenders (输出源)和Layouts(布局). 1.LoggersLoggers组件在此系统中被分为五个级别:DEBU ...
- http长连接与短连接
详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytp23 http长连接与短连接 一.长连接与短连接: 长连接:client方与 ...
- EF增删改查+使用Expression进行动态排序分页
注:以下部分来自<ASP.NET MVC 企业级实战>一书的摘抄和改写以及部分个人学习心得. EF简单增删改查 增加 public static int Add() { using (No ...
- Java中ArrayList remove会遇到的坑
前言 平时最常用的莫过于ArrayList和HashMap了,面试的时候也是问答的常客.先不去管容量.负载因子什么的,就是简单的使用也会遇到坑. Remove 元素 经常遇到的一个场景是:遍历list ...
- JAVA基础-集合(一)
一.结构 Collection接口为一些单列集合的根接口,其常用子接口为List接口.Set接口.List接口常用实现子类为ArrayList(数组)LinkedList(链表).Set接口常用实现子 ...
- python 从视频中提取图片,并保存在硬盘上
使用python的moviepy库来提取视频中的图片,按照视频每帧一个图片的方式来保存. extract images from video, than save them to disk from ...