From bad57f0bcca9ce1258b689d902ae1b90159c49c0 Mon Sep 17 00:00:00 2001 From: Mu Li Date: Sun, 6 May 2018 12:55:35 -0700 Subject: [PATCH] update block (#244) * update block * update parameters * update block * update serialization * update gitignore * remove name-scope * update use-gpu --- .gitignore | 3 + chapter_gluon-basics/block.md | 174 ++++++++------------- chapter_gluon-basics/deferred-init.md | 72 +++++++++ chapter_gluon-basics/index.md | 1 + chapter_gluon-basics/parameters.md | 212 +++++++++----------------- chapter_gluon-basics/serialization.md | 82 +++++----- chapter_gluon-basics/use-gpu.md | 93 ++++++----- 7 files changed, 300 insertions(+), 337 deletions(-) create mode 100644 chapter_gluon-basics/deferred-init.md diff --git a/.gitignore b/.gitignore index 2fd91203c..1f021b680 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ build/ *.json *.params *.DS_Store +/chapter_gluon-basics/x +/chapter_gluon-basics/xy +/chapter_gluon-basics/mydict diff --git a/chapter_gluon-basics/block.md b/chapter_gluon-basics/block.md index 0424843dc..ecb44b61f 100644 --- a/chapter_gluon-basics/block.md +++ b/chapter_gluon-basics/block.md @@ -1,190 +1,150 @@ # 模型构造 -回忆在[“多层感知机——使用Gluon”](../chapter_supervised-learning/mlp-gluon.md)一节中我们是如何实现一个单隐藏层感知机。我们首先构造Sequential实例,然后依次添加两个全连接层。其中第一层的输出大小为256,即隐藏层单元个数是256;第二层的输出大小为10,即输出层单元个数是10。 +回忆在[“多层感知机——使用Gluon”](../chapter_supervised-learning/mlp-gluon.md)一节中我们是如何实现一个单隐藏层感知机。我们首先构造Sequential实例,然后依次添加两个全连接层。其中第一层的输出大小为256,即隐藏层单元个数是256;第二层的输出大小为10,即输出层单元个数是10。这个简单例子已经包含了深度学习模型计算的方方面面,接下来的小节我们将围绕这个例子展开。 -```{.python .input n=1} -from mxnet import nd -from mxnet.gluon import nn - -net = nn.Sequential() -with net.name_scope(): - net.add(nn.Dense(256, activation='relu')) - net.add(nn.Dense(10)) -``` +我们之前都是用了Sequential类来构造模型。这里我们另外一种基于Block类的模型构造方法,它让构造模型更加灵活,也将让你能更好的理解Sequential的运行机制。 - +## 继承Block类来构造模型 -接下来,对模型初始化并做一次计算。 +Block类是`gluon.nn`里提供的一个模型构造类,我们可以继承它来定义我们想要的模型。例如,我们在这里构造一个同前提到的相同的多层感知机。这里定义的MLP类重载了Block类的两个函数:`__init__`和`forward`. -```{.python .input n=2} -net.initialize() -x = nd.random.uniform(shape=(2, 20)) -print(net(x)) -print('hidden layer: ', net[0]) -print('output layer: ', net[1]) -``` - -这里`net`的输入数据`x`包含2个样本,每个样本的特征向量长度为20,因此它的形状是`(2, 20)`。在按照默认方式初始化好模型参数后,`net`计算得到一个$2 \times 10$的矩阵作为模型的输出。其中4是数据样本个数,10是输出层单元个数。实际上,这个简单例子已经包含了神经网络实现的方方面面,接下来的小节我们将围绕这个例子展开。 - -之前我们都是用了Sequential类来构造模型。这里我们将从Block类开始,它提供一种更灵活的模型构造方式,也使得你能更好的理解Sequential类的运行机制。 - -## 使用Block构造模型 - -在Gluon中,Block是一个类。我们可以创建它的子类来构造模型。例如,我们构造一个跟前面提到的相同的多层感知机。 +```{.python .input n=1} +from mxnet import nd +from mxnet.gluon import nn -```{.python .input n=3} class MLP(nn.Block): + # 声明带有模型参数的层,这里我们声明了两个全链接层。 def __init__(self, **kwargs): + # 调用 MLP 父类 Block 的构造函数来进行必要的初始化。这样在构造实例时还可以指定 + # 其他函数参数,例如下下一节将介绍的模型参数 params. super(MLP, self).__init__(**kwargs) - with self.name_scope(): - self.hidden = nn.Dense(256, activation='relu') - self.output = nn.Dense(10) - + # 隐藏层。 + self.hidden = nn.Dense(256, activation='relu') + # 输出层。 + self.output = nn.Dense(10) + # 定义模型的前向计算,即如何根据输出计算输出。 def forward(self, x): return self.output(self.hidden(x)) ``` -这里,我们通过创建Block的子类构造模型。任意一个Block的子类至少实现以下两个函数: - -* `__init__`:创建模型的参数。在上面的例子里,模型的参数被包含在了两个`Dense`层里。 -* `forward`:定义模型的计算。 - -接下来我们解释一下MLP类用的其他命令: +我们可以实例化MLP类得到`net`,其使用跟[“多层感知机——使用Gluon”](../chapter_supervised-learning/mlp-gluon.md)一节中通过Sequential类构造的`net`一致。下面代码初始化`net`并传入输入数据`x`做一次前向计算。 -* `super(MLP, self).__init__(**kwargs)`:这句话调用MLP父类Block的构造函数`__init__`。这样,我们在调用`MLP`的构造函数时还可以指定函数参数`prefix`(名字前缀)或`params`(模型参数,下一节会介绍)。这两个函数参数将通过`**kwargs`传递给Block的构造函数。 - -* `with self.name_scope()`:本例中的两个Dense层和其中模型参数的名字前面都将带有模型名前缀。该前缀可以通过构造函数参数`prefix`指定。若未指定,该前缀将自动生成。我们建议,在构造模型时将每个层至少放在一个`name_scope()`里。 - -我们可以实例化MLP类得到`net2`,并让`net2`根据输入数据`x`做一次计算。其中,`y = net2(x)`明确调用了MLP实例中的`__call__`函数(从Block继承得到)。在Gluon中,这将进一步调用`MLP`中的`forward`函数从而完成一次模型计算。 - -```{.python .input n=4} +```{.python .input n=2} +x = nd.random.uniform(shape=(2,20)) net = MLP() net.initialize() -print(net(x)) -print('hidden layer name with default prefix:', net.hidden.name) -print('output layer name with default prefix:', net.output.name) -``` - -在上面的例子中,隐藏层和输出层的名字前都加了默认前缀。接下来我们通过`prefix`指定它们的名字前缀。 - -```{.python .input n=5} -net = MLP(prefix='my_mlp_') -print('hidden layer name with "my_mlp_" prefix:', net.hidden.name) -print('output layer name with "my_mlp_" prefix:', net.output.name) -``` - -接下来,我们重新定义MLP_NO_NAMESCOPE类。它和`MLP`的区别就是不含`with self.name_scope():`。这是,隐藏层和输出层的名字前都不再含指定的前缀`prefix`。 - -```{.python .input n=6} -class MLP_NO_NAMESCOPE(nn.Block): - def __init__(self, **kwargs): - super(MLP_NO_NAMESCOPE, self).__init__(**kwargs) - self.hidden = nn.Dense(256, activation='relu') - self.output = nn.Dense(10) - - def forward(self, x): - return self.output(self.hidden(x)) - -net = MLP_NO_NAMESCOPE(prefix='my_mlp_') -print('hidden layer name without prefix:', net.hidden.name) -print('output layer name without prefix:', net.output.name) +net(x) ``` -需要指出的是,在Gluon里,Block是一个一般化的部件。整个神经网络可以是一个Block,单个层也是一个Block。我们还可以反复嵌套Block来构建新的Block。 +其中,`net(x)`会调用了MLP继承至Block的`__call__`函数,这个函数将调用MLP定义的`forward`函数来完成前向计算。 -Block主要提供模型参数的存储、模型计算的定义和自动求导。你也许已经发现了,以上Block的子类中并没有定义如何求导,或者是`backward`函数。事实上,MXNet会使用`autograd`对`forward`自动生成相应的`backward`函数。 +我们无需在这里定义反向传播函数,系统将通过自动求导,参考[“自动求梯度”](../chapter_crashcourse/autograd.md)一节,来自动生成`backward`函数。 +注意到我们不是将Block叫做层或者模型之类的名字,这是因为它是一个可以自由组建的部件。它的子类既可以一个层,例如Gluon提供的Dense类,也可以是一个模型,我们定义的MLP类,或者是模型的一个部分,例如我们会在之后介绍的ResNet的残差块。我们下面通过两个例子说明它。 -### Sequential类是Block的子类 +## Sequential类继承自Block类 -在Gluon里,Sequential类是Block的子类。Sequential类或实例也可以被看作是一个Block的容器:通过`add`函数来添加Block。在`forward`函数里,Sequential实例把添加进来的Block逐一运行。 +当模型的前向计算就是简单串行计算模型里面各个层的时候,我们可以将模型定义变得更加简单,这个就是Sequential类的目的,它通过`add`函数来添加Block子类实例,前向计算时就是将添加的实例逐一运行。下面我们实现一个跟Sequential类有相同功能的类,这样你可以看的更加清楚它的运行机制。 -一个简单的实现是这样的: - -```{.python .input n=7} +```{.python .input n=3} class MySequential(nn.Block): def __init__(self, **kwargs): super(MySequential, self).__init__(**kwargs) def add(self, block): - self._children[str(len(self._children))] = block + # block 是一个 Block 子类实例,假设它有一个独一无二的名字。我们将它保存在 + # Block 类的成员变量 _children 里,其类型是 OrderedDict. 当调用 + # initialize 函数时,系统会自动对 _children 里面所有成员初始化。 + self._children[block.name] = block def forward(self, x): + # OrderedDict 保证会按照插入时的顺序便利元素。 for block in self._children.values(): x = block(x) return x ``` -它的使用和`Sequential`类很相似: +我们用MySequential类来实现前面的MLP类: -```{.python .input n=8} +```{.python .input n=4} net = MySequential() -with net.name_scope(): - net.add(nn.Dense(256, activation='relu')) - net.add(nn.Dense(10)) +net.add(nn.Dense(256, activation='relu')) +net.add(nn.Dense(10)) net.initialize() net(x) ``` -### 构造更复杂的模型 +你会发现这里MySequential类的使用跟[“多层感知机——使用Gluon”](../chapter_supervised-learning/mlp-gluon.md)一节中Sequential类使用一致。 + +## 构造复杂的模型 + +虽然Sequential类可以使得模型构造更加简单,不需要定义`forward`函数,但直接继承Block类可以极大的拓展灵活性。下面我们构造一个稍微复杂点的网络: -与`Sequential`类相比,继承Block可以构造更复杂的模型。下面是一个例子。 +1. 在前向计算中使用了NDArray函数和Python的控制流 +1. 多次调用同一层 -```{.python .input n=9} +```{.python .input n=5} class FancyMLP(nn.Block): def __init__(self, **kwargs): super(FancyMLP, self).__init__(**kwargs) - self.rand_weight = nd.random_uniform(shape=(10, 20)) - with self.name_scope(): - self.dense = nn.Dense(10, activation='relu') + # 不会被更新的随机权重。 + self.rand_weight = nd.random.uniform(shape=(20, 20)) + self.dense = nn.Dense(20, activation='relu') def forward(self, x): x = self.dense(x) + # 使用了 nd 包下 relu 和 dot 函数。 x = nd.relu(nd.dot(x, self.rand_weight) + 1) + # 重用了 dense,等价于两层网络但共享了参数。 x = self.dense(x) - return x + # 控制流,这里我们需要调用 asscalar 来返回标量进行比较。 + while x.norm().asscalar() > 1: + x /= 2 + if x.norm().asscalar() < 0.8: + x *= 10 + return x.sum() ``` 在这个`FancyMLP`模型中,我们使用了常数权重`rand_weight`(注意它不是模型参数)、做了矩阵乘法操作(`nd.dot`)并重复使用了相同的`Dense`层。测试一下: -```{.python .input n=10} +```{.python .input n=6} net = FancyMLP() net.initialize() net(x) ``` -由于`Sequential`类是Block的子类,它们还可以嵌套使用。下面是一个例子。 +由于FancyMLP和Sequential都是Block的子类,我们可以嵌套调用他们。 -```{.python .input n=12} +```{.python .input n=7} class NestMLP(nn.Block): def __init__(self, **kwargs): super(NestMLP, self).__init__(**kwargs) self.net = nn.Sequential() - with self.name_scope(): - self.net.add(nn.Dense(64, activation='relu')) - self.net.add(nn.Dense(32, activation='relu')) - self.dense = nn.Dense(16, activation='relu') + self.net.add(nn.Dense(64, activation='relu'), + nn.Dense(32, activation='relu')) + self.dense = nn.Dense(16, activation='relu') def forward(self, x): return self.dense(self.net(x)) net = nn.Sequential() -net.add(NestMLP()) -net.add(nn.Dense(10)) +net.add(NestMLP(), nn.Dense(20), FancyMLP()) + net.initialize() -print(net(x)) +net(x) ``` ## 小结 -* 我们可以通过Block来构造复杂的模型。 -* `Sequential`是Block的子类。 +* 我们可以通过继承Block类来构造复杂的模型。 +* Sequential是Block的子类。 ## 练习 -* 比较使用`Sequential`和使用Block构造模型的方式。如果希望访问模型中某一层(例如隐藏层)的某个属性(例如名字),这两种方式有什么不同? -* 如果把`NestMLP`中的`self.net`和`self.dense`改成`self.denses = [nn.Dense(64, activation='relu'), nn.Dense(32, activation='relu'), nn.Dense(16)]`,并在`forward`中用for循环实现相同计算,会有什么问题吗? +* 在FancyMLP类里我们重用了`dense`,这样对输入形状有了一定要求,尝试改变下输入数据形状试试 +* 如果我们去掉FancyMLP里面的`asscalar`会有什么问题? +* 在NestMLP里假设我们改成 `self.net=[nn.Dense(64, activation='relu'),nn.Dense(32, activation='relu')]`,而不是用Sequential类来构造,会有什么问题? ## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/986) diff --git a/chapter_gluon-basics/deferred-init.md b/chapter_gluon-basics/deferred-init.md new file mode 100644 index 000000000..e1687355c --- /dev/null +++ b/chapter_gluon-basics/deferred-init.md @@ -0,0 +1,72 @@ +# 模型参数的延后初始化 + +如果你注意到了上节练习,你会发现在`net.initialize()`后和`net(x)`前模型参数的形状都是空。直觉上`initialize`会完成了所有参数初始化过程,然而Gluon中这是不一定的。我们这里详细讨论这个话题。 + +## 延后的初始化 + +注意到前面使用Gluon的章节里,我们在创建全连接层时都没有指定输入大小。例如在一直使用的多层感知机例子里,我们创建了输出大小为256的隐藏层。但是当在调用`initialize`函数的时候,我们并不知道这个层的参数到底有多大,因为它的输入大小仍然是未知。只有在当我们将形状是`(2,20)`的`x`输入进网络时,我们这时候才知道这一层的参数大小应该是`(256,20)`。所以这个时候我们才能真正开始初始化参数。 + +让我们使用上节定义的MyInit类来清楚的演示这一个过程。下面我们创建多层感知机,然后使用MyInit实例来进行初始化。 + +```{.python .input n=22} +from mxnet import init, nd +from mxnet.gluon import nn + +class MyInit(init.Initializer): + def _init_weight(self, name, data): + print('Init', name, data.shape) + # 实际的初始化逻辑在此省略了。 + +net = nn.Sequential() +net.add(nn.Dense(256, activation='relu')) +net.add(nn.Dense(10)) + +net.initialize(init=MyInit()) +``` + +注意到MyInit在调用时会打印信息,但当前我们并没有看到相应的日志。下面我们执行前向计算。 + +```{.python .input n=25} +x = nd.random.uniform(shape=(2,20)) +y = net(x) +``` + +这时候系统根据输入`x`的形状自动推测数所有层参数形状,例如隐藏层大小是`(256,20)`,并创建参数。之后调用MyInit实例来进行初始方法,然后再进行前向计算。 + +当然,这个初始化只会在第一次执行被调用。之后我们再运行`net(x)`时则不会重新初始化,即我们不会再次看到MyInit实例的输出。 + +```{.python .input} +y = net(x) +``` + +我们将这个系统将真正的参数初始化延后到获得了足够信息到时候称之为延后初始化。它可以让模型创建更加简单,因为我们只需要定义每个层的输出大小,而不用去推测它们的的输入大小。这个对于之后将介绍的多达数十甚至数百层的网络尤其有用。 + +当然正如本节开头提到到那样,延后初始化也可能会造成一定的困解。在调用第一次前向计算之前我们无法直接操作模型参数。例如无法使用`data`和`set_data`函数来获取和改写参数。所以经常我们会额外调用一次`net(x)`来是的参数被真正的初始化。 + +## 避免延后初始化 + +当系统在调用`initialize`函数时能够知道所有参数形状,那么延后初始化就不会发生。我们这里给两个这样的情况。 + +第一个是模型已经被初始化过,而且我们要对模型进行重新初始化时。因为我们知道参数大小不会变,所以能够立即进行重新初始化。 + +```{.python .input} +net.initialize(init=MyInit(), force_reinit=True) +``` + +第二种情况是我们在创建层到时候指定了每个层的输入大小,使得系统不需要额外的信息来推测参数形状。下例中我们通过`in_units`来指定每个全连接层的输入大小,使得初始化能够立即进行。 + +```{.python .input} +net = nn.Sequential() +net.add(nn.Dense(256, in_units=20, activation='relu')) +net.add(nn.Dense(10, in_units=256)) + +net.initialize(init=MyInit()) +``` + +## 小结 + +* 在调用`initialize`函数时,系统可能将真正的初始化延后到后面,例如前向计算时,来执行。这样到主要好处是让模型定义可以更加简单。 + +## 练习 + +* 如果在下一次`net(x)`前改变`x`形状,包括批量大小和特征大小,会发生什么? diff --git a/chapter_gluon-basics/index.md b/chapter_gluon-basics/index.md index 586224380..e5fd13ddd 100644 --- a/chapter_gluon-basics/index.md +++ b/chapter_gluon-basics/index.md @@ -9,6 +9,7 @@ block parameters + deferred-init custom-layer serialization use-gpu diff --git a/chapter_gluon-basics/parameters.md b/chapter_gluon-basics/parameters.md index 4d5d8b6ed..2ef740b2d 100644 --- a/chapter_gluon-basics/parameters.md +++ b/chapter_gluon-basics/parameters.md @@ -1,212 +1,136 @@ -# 模型参数 +# 模型参数的访问、初始化和共享 -为了引出本节的话题,让我们先构造一个多层感知机。首先,导入本节中实验所需的包。 +在之前的小节里我们一直在使用默认的初始函数,`net.initialize()`,来初始话模型参数。我们也同时介绍过如何访问模型参数的简单方法。这一节我们将深入讲解模型参数的访问和初始化,以及如何在多个层之间共享同一份参数。 + +我们首先定义同前的多层感知机、初始化权重和计算前向结果。同前比一点不同的是,在这里我们从MXNet中导入了`init`这个包,它包含了多种模型初始化方法。 ```{.python .input n=1} -from mxnet import init, gluon, nd +from mxnet import init, nd from mxnet.gluon import nn -import sys -``` -下面定义多层感知机。 +net = nn.Sequential() +net.add(nn.Dense(256, activation='relu')) +net.add(nn.Dense(10)) +net.initialize() -```{.python .input} -class MLP(nn.Block): - def __init__(self, **kwargs): - super(MLP, self).__init__(**kwargs) - with self.name_scope(): - self.hidden = nn.Dense(4) - self.output = nn.Dense(2) - - def forward(self, x): - return self.output(nd.relu(self.hidden(x))) +x = nd.random.uniform(shape=(2,20)) +y = net(x) ``` -运行下面代码,系统抱怨说模型参数没有初始化。 +## 访问模型参数 + +我们知道可以通过`[]`来访问Sequential类构造出来的网络的特定层。对于带有模型参数的层,我们可以通过Block类的`params`属性来得到它包含的所有参数。例如我们查看隐藏层的参数: ```{.python .input n=2} -x = nd.random.uniform(shape=(3, 5)) -try: - net = MLP() - net(x) -except RuntimeError as err: - sys.stderr.write(str(err)) +net[0].params ``` -作如下修改之后,模型便计算成功。 +可以看到我们得到了一个由参数名称映射到参数的字典。第一个参数的名称为`dense0_weight`,它由`net[0]`的名称(`dense0_`)和自己的变量名(`weight`)组成。而且可以看到它参数的形状为`(256, 20)`,且数据类型为32位浮点数。 + +为了访问特定参数,我们既可以通过名字来访问字典里的元素,也可以直接使用它的变量名。下面两种方法是等价的,但通常后者的代码可读性更好。 ```{.python .input n=3} -net.initialize() -net(x) +(net[0].params['dense0_weight'], net[0].weight) ``` -这里添加的`net.initialize()`对模型参数做了初始化。模型参数是深度学习计算中的重要组成部分。本节中,我们将介绍如何访问、初始化和共享模型参数。 - -## 访问模型参数 - -在Gluon中,模型参数的类型是Parameter类。下面让我们创建一个名字叫“good_param”、形状为$2 \times 3$的模型参数。在默认的初始化中,模型参数中的每一个元素是一个在`[-0.07, 0.07]`之间均匀分布的随机数。相应地,该模型参数还有一个形状为$2 \times 3$的梯度,初始值为0。 +Gluon里参数类型为Parameter类,其包含参数权重和它对应的梯度,它们可以分别通过`data`和`grad`函数来访问。因为我们随机初始化了权重,所以它是一个由随机数组成的形状为`(256, 20)`的NDArray. ```{.python .input n=4} -my_param = gluon.Parameter("good_param", shape=(2, 3)) -my_param.initialize() -print('data: ', my_param.data(), '\ngrad: ', my_param.grad(), - '\nname: ', my_param.name) +net[0].weight.data() ``` -接下来,让我们访问本节开头定义的多层感知机`net`中隐藏层`hidden`的模型参数:权重`weight`和偏差`bias`。它们的类型也都是Parameter类。我们可以看到它们的名字、形状和数据类型。 +梯度的形状跟权重一样。但我们还没有进行反向传播计算,所以它的值全为0. ```{.python .input n=5} -w = net.hidden.weight -b = net.hidden.bias -print('hidden layer name: ', net.hidden.name, '\nweight: ', w, '\nbias: ', b) +net[0].weight.grad() ``` -我们同样可以访问这两个参数的值和梯度。 +类似我们可以访问其他的层的参数。例如输出层的偏差权重: ```{.python .input n=6} -print('weight:', w.data(), '\nweight grad:', w.grad(), '\nbias:', b.data(), - '\nbias grad:', b.grad()) +net[1].bias.data() ``` -另外,我们也可以通过`collect_params`来访问Block里的所有参数(包括所有的子Block)。它会返回一个名字到对应Parameter实例的字典。在这个字典中,我们既可以用`[]`(需要指定前缀),又可以用`get`函数(不需要指定前缀)来访问模型参数。 +最后,我们可以使用Block类提供的`collect_params`函数来获取这个实例包含的所有的参数,它的返回同样是一个参数名称到参数的字典。 -```{.python .input n=7} -params = net.collect_params() -print(params) -print(params['mlp0_dense0_bias'].data()) -print(params.get('dense0_bias').data()) +```{.python .input n=11} +net.collect_params() ``` ## 初始化模型参数 -在Gluon中,模型的偏差参数总是默认初始化为0。当我们对整个模型所有参数做初始化时,默认下权重参数的所有元素为[-0.07, 0.07]之间均匀分布的随机数。我们也可以使用其他初始化方法。以下例子使用了均值为0,标准差为0.02的正态分布来随机初始化模型中所有层的权重参数。 +当使用默认的模型初始化,Gluon会将权重参数元素初始化为`[-0.07, 0.07]`之间均匀分布的随机数,偏差参数则全为0. 但经常我们需要使用其他的方法来初始话权重,MXNet的[`init`模块](https://mxnet.incubator.apache.org/api/python/optimization/optimization.html#module-mxnet.initializer)里提供了多种预设的初始化方法。例如下面例子我们将权重参数初始化成均值为0,标准差为0.01的正态分布随机数。 -```{.python .input n=8} -params = net.collect_params() -params.initialize(init=init.Normal(sigma=0.02), force_reinit=True) -print('hidden weight: ', net.hidden.weight.data(), '\nhidden bias: ', - net.hidden.bias.data(), '\noutput weight: ', net.output.weight.data(), - '\noutput bias: ',net.output.bias.data()) +```{.python .input n=7} +# 非首次对模型初始化需要指定 force_reinit +net.initialize(init=init.Normal(sigma=0.01), force_reinit=True) +net[0].weight.data()[0] ``` -我们也可以把模型中任意层任意参数初始化,例如把上面模型中隐藏层的偏差参数初始化为1。 +如果想只对某个特定参数进行初始化,我们可以调用Paramter类的`initialize`函数,它的使用跟Block类提供的一致。下例中我们对第一个隐藏层的权重使用Xavier方法来进行初始化。 -```{.python .input n=9} -net.hidden.bias.initialize(init=init.One(), force_reinit=True) -print(net.hidden.bias.data()) +```{.python .input n=8} +net[0].weight.initialize(init=init.Xavier(), force_reinit=True) +net[0].weight.data()[0] ``` -### 自定义初始化方法 +## 自定义初始化方法 -下面我们自定义一个初始化方法。它通过重载`_init_weight`来实现自定义的初始化方法。 +有时候我们需要的初始化方法并没有在`init`模块中提供,这时我们有两种方法来自定义参数初始化。一种是实现一个Initializer类的子类使得我们可以跟前面使用`init.Normal`那样使用它。在这个方法里,我们只需要实现`_init_weight`这个函数,将其传入的NDArray修改成需要的内容。下面例子里我们把权重初始化成`[-10,-5]`和`[5,10]`两个区间里均匀分布的随机数。 -```{.python .input n=13} +```{.python .input n=9} class MyInit(init.Initializer): - def __init__(self): - super(MyInit, self).__init__() - self._verbose = True - def _init_weight(self, _, arr): - # 初始化权重,使用out=arr后我们不需指定形状。 - nd.random.uniform(low=10, high=20, out=arr) - -net = MLP() -net.initialize(MyInit()) -net(x) -net.hidden.weight.data() -``` + def _init_weight(self, name, data): + print('Init', name, data.shape) + data[:] = nd.random.uniform(low=-10, high=10, shape=data.shape) + data *= data.abs() >= 5 -我们还可以通过`Parameter.set_data`来直接改写模型参数。 +net.initialize(MyInit(), force_reinit=True) +net[0].weight.data()[0] +``` -```{.python .input n=14} -net = MLP() -net.initialize() -net(x) -print('output layer default weight:', net.output.weight.data()) +第二种方法是我们通过Parameter类的`set_data`函数来直接改写模型参数。例如下例中我们将隐藏层参数在现有的基础上加1. -w = net.output.weight -w.set_data(nd.ones(w.shape)) -print('output layer modified weight:', net.output.weight.data()) +```{.python .input n=10} +net[0].weight.set_data(net[0].weight.data()+1) +net[0].weight.data()[0] ``` -## 延后的初始化 +## 共享模型参数 -我们在本节开头定义的MLP类里的层`nn.Dense(4)`和`nn.Dense(2)`中无需指定它们的输入单元个数。给定`net = MLP()`和输入数据`x`。我们在[“模型构造”](block.md)一节中介绍过,执行`net(x)`将调用`net`的`forward`函数计算模型输出。在这次计算中,`net`也将从输入数据`x`的形状自动推断模型中每一层尚未指定的输入单元个数,得到模型中所有参数形状,并真正完成模型参数的初始化。因此,在上面两个例子中,我们总是在调用`net(x)`之后访问初始化的模型参数。这种延后的初始化带来的一大便利是,我们在构造模型时无需指定每一层的输入单元个数。 +在有些情况下,我们希望在多个层之间共享模型参数。我们在[“模型构造”](./block.md)这一节看到了如何在Block类里`forward`函数里多次调用同一个类来完成。这里将介绍另外一个方法,它在构造层的时候指定使用特定的参数。如果不同层使用同一份参数,那么它们不管是在前向计算还是反向传播时都会共享共同的参数。 -下面,我们具体来看延后的初始化是怎么工作的。让我们新建一个网络并打印所有模型参数。这时,两个全连接层的权重的形状里都有0。它们代表尚未指定的输入单元个数。 +在下面例子里,我们让模型的第二隐藏层和第三隐藏层共享模型参数。 ```{.python .input} -net = MLP() -net.collect_params() -``` - -然后,调用`net.initialize()`并打印所有模型参数。这时模型参数依然没有被初始化。 +from mxnet import nd +from mxnet.gluon import nn -```{.python .input} +net = nn.Sequential() +shared = nn.Dense(8, activation='relu') +net.add(nn.Dense(8, activation='relu'), + shared, + nn.Dense(8, activation='relu', params=shared.params), + nn.Dense(10)) net.initialize() -net.collect_params() -``` -接下来,当模型见到输入数据`x`后(`shape=(3, 5)`),模型每一层参数的形状得以推断,参数的初始化最终完成。 - -```{.python .input n=12} -print(x) +x = nd.random.uniform(shape=(2,20)) net(x) -net.collect_params() -``` -## 共享模型参数 - -在有些情况下,我们希望模型的多个层之间共享模型参数。这时,我们可以通过Block的`params`来指定模型参数。在下面使用Sequential类构造的多层感知机中,模型的第二隐藏层(`net[1]`)和第三隐藏层(`net[2]`)共享模型参数。 - -```{.python .input n=15} -net = nn.Sequential() -with net.name_scope(): - net.add(nn.Dense(4, activation='relu')) - net.add(nn.Dense(4, activation='relu')) - # 通过params指定需要共享的模型参数。 - net.add(nn.Dense(4, activation='relu', params=net[1].params)) - net.add(nn.Dense(2)) - -net.initialize() -net(x) -print(net[1].weight.data()) -print(net[2].weight.data()) +net[1].weight.data()[0] == net[2].weight.data()[0] ``` -同样,我们也可以在使用Block构造的多层感知机中,让模型的第二隐藏层(`hidden2`)和第三隐藏层(`hidden3`)共享模型参数。 - -```{.python .input} -class MLP_SHARE(nn.Block): - def __init__(self, **kwargs): - super(MLP_SHARE, self).__init__(**kwargs) - with self.name_scope(): - self.hidden1 = nn.Dense(4, activation='relu') - self.hidden2 = nn.Dense(4, activation='relu') - # 通过params指定需要共享的模型参数。 - self.hidden3 = nn.Dense(4, activation='relu', - params=self.hidden2.params) - self.output = nn.Dense(2) - - def forward(self, x): - return self.output(self.hidden3(self.hidden2(self.hidden1(x)))) - -net = MLP_SHARE() -net.initialize() -net(x) -print(net.hidden2.weight.data()) -print(net.hidden3.weight.data()) -``` +我们在构造第三隐藏层时通过`params`来指定它使用第二隐藏层的参数。由于模型参数里包含了梯度,所以在反向传播计算时,第二隐藏层和第三隐藏层的梯度都会被累加在`shared.params.grad()`里。 ## 小结 -* 我们可以很方便地访问、自定义和共享模型参数。 +* 我们有多种方法来访问、初始化和共享模型参数。 ## 练习 -* 在本节任何一个例子中,`net.collect_params()`和`net.params`的返回有什么不同? * 查阅[MXNet文档](https://mxnet.incubator.apache.org/api/python/model.html#initializer-api-reference),了解不同的参数初始化方式。 -* 构造一个含共享参数层的多层感知机并训练。观察每一层的模型参数。 -* 如果两个层共用一个参数,求梯度的时候会发生什么? +* 尝试在`net.initialize()`后和`net(x)`前访问模型参数,看看会发生什么。 +* 构造一个含共享参数层的多层感知机并训练。观察每一层的模型参数和梯度计算。 ## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/987) diff --git a/chapter_gluon-basics/serialization.md b/chapter_gluon-basics/serialization.md index e3d956e4c..e747912eb 100644 --- a/chapter_gluon-basics/serialization.md +++ b/chapter_gluon-basics/serialization.md @@ -5,59 +5,64 @@ ## 读写NDArrays -首先,导入本节中实验所需的包。 +我们首先看如何读写NDArray。我们可以直接使用`save`和`load`函数分别存储和读取NDArray。下面是例子我们创建`x`,并将其存在文件名同为`x`的文件里。 ```{.python .input} from mxnet import nd from mxnet.gluon import nn -``` -我们看看如何读写NDArray。我们可以直接使用`save`和`load`函数分别存储和读取NDArray。事实上,MXNet支持跨语言(例如R和Scala)的存储和读取。 +x = nd.ones(3) +nd.save('x', x) +``` -下面是存储NDArray的例子。 +然后我们再将数据从文件读回内存。 -```{.python .input n=2} -x = nd.ones(3) -y = nd.zeros(4) -filename = "../data/test1.params" -nd.save(filename, [x, y]) +```{.python .input} +x2 = nd.load('x') +x2 ``` -读取并打印上面存储的NDArray。 +同样我们可以存储一列NArray并读回内存。 -```{.python .input n=3} -a, b = nd.load(filename) -print(a, b) +```{.python .input n=2} +y = nd.zeros(4) +nd.save('xy', [x, y]) +x2, y2 = nd.load('xy') +(x2, y2) ``` -我们也可以存储和读取含NDArray的词典。 +或者是一个从字符串到NDArray的字典。 ```{.python .input n=4} -mydict = {"x": x, "y": y} -filename = "../data/test2.params" -nd.save(filename, mydict) -c = nd.load(filename) -print(c) +mydict = {'x': x, 'y': y} +nd.save('mydict', mydict) +mydict2 = nd.load('mydict') +mydict2 ``` ## 读写Gluon模型的参数 -在[“模型构造”](block.md)一节中,我们了解了Gluon模型通常是个Block。与NDArray类似,Block提供了`save_params`和`load_params`函数来读写模型参数。 +Block类提供了`save_params`和`load_params`函数来读写模型参数。它实际做的事情就是将所有参数保存成一个名称到NDArray的字典到文件。读取的时候会根据参数名称找到对应的NDArray并赋值。下面的例子我们首先创建一个多层感知机,初始化后将模型参数保存到文件里。 下面,我们创建一个多层感知机。 ```{.python .input n=6} -def get_net(): - net = nn.Sequential() - with net.name_scope(): - net.add(nn.Dense(10, activation="relu")) - net.add(nn.Dense(2)) - return net - -net = get_net() +class MLP(nn.Block): + def __init__(self, **kwargs): + super(MLP, self).__init__(**kwargs) + self.hidden = nn.Dense(256, activation='relu') + self.output = nn.Dense(10) + def forward(self, x): + return self.output(self.hidden(x)) + +net = MLP() net.initialize() -x = nd.random.uniform(shape=(2, 10)) -print(net(x)) + +# 由于延后初始化,我们需要先运行一次前向计算才能实际初始化模型参数。 +x = nd.random.uniform(shape=(2, 20)) +y = net(x) + +net.save_params('mlp.params') ``` 下面我们把该模型的参数存起来。 @@ -67,13 +72,18 @@ filename = "../data/mlp.params" net.save_params(filename) ``` -然后,我们构建一个同`net`一样的多层感知机`net2`。这一次,`net2`不像`net`那样随机初始化,而是直接读取`net`的模型参数。这样,给定同样的输入`x`,`net2`会输出同样的计算结果。 +然后,我们再实例化一次我们定义的多层感知机。但跟前面不一样是我们不是随机初始化模型参数,而是直接读取保存在文件里的参数。 ```{.python .input n=8} -import mxnet as mx -net2 = get_net() -net2.load_params(filename, mx.cpu(0)) -print(net2(x)) +net2 = MLP() +net2.load_params('mlp.params') +``` + +因为这两个实例都有同样的参数,那么对同一个`x`的计算结果将会是一样。 + +```{.python .input} +y2 = net2(x) +y2 == y ``` ## 小结 @@ -84,7 +94,7 @@ print(net2(x)) ## 练习 * 即使无需把训练好的模型部署到不同的设备,存储模型参数在实际中还有哪些好处? - +* 如果注释掉`with self.name_scope()`这一行,我们还能正确读取模型参数吗? ## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/1255) diff --git a/chapter_gluon-basics/use-gpu.md b/chapter_gluon-basics/use-gpu.md index 62e7cc94f..c50a251e7 100644 --- a/chapter_gluon-basics/use-gpu.md +++ b/chapter_gluon-basics/use-gpu.md @@ -1,81 +1,83 @@ # GPU计算 -目前为止,我们一直在使用CPU计算。的确,绝大部分的计算设备都有CPU。然而,CPU的设计目的是处理通用的计算。对于复杂的神经网络和大规模的数据来说,使用单块CPU计算可能不够高效。 +目前为止我们一直在使用CPU计算。的确,绝大部分的计算设备都有CPU。然而CPU的设计目的是处理通用的计算。对于复杂的神经网络和大规模的数据来说,使用CPU来计算可能不够高效。 -本节中,我们将介绍如何使用单块Nvidia GPU来计算。 - -首先,需要确保至少有一块Nvidia显卡已经安装好了。然后,下载安装显卡驱动和[CUDA](https://developer.nvidia.com/cuda-downloads)(推荐下载8.0,CUDA自带了驱动)。Windows用户还需要设一下PATH: - -> `set PATH=C:\Program Files\NVIDIA Corporation\NVSMI;%PATH%` - -这些准备工作都完成后,下面就可以通过`nvidia-smi`来查看显卡信息了。 +本节中,我们将介绍如何使用单块Nvidia GPU来计算。首先,需要确保至少有一块Nvidia显卡已经安装好了。然后,下载[CUDA](https://developer.nvidia.com/cuda-downloads)并按照提示设置好相应的路径。这些准备工作都完成后,下面就可以通过`nvidia-smi`来查看显卡信息了。 ```{.python .input n=1} !nvidia-smi ``` -接下来,我们需要确认安装了MXNet的GPU版本。如果装了MXNet的CPU版本,我们需要卸载它。例如 +可以看到我们使用的机器上面有两块Tesla M60,每块卡有7.6GB内存。 -> `pip uninstall mxnet` +接下来,我们需要确认安装了MXNet的GPU版本。如果装了MXNet的CPU版本,我们需要先卸载它。例如我们可以使用`pip uninstall mxnet`。然后根据CUDA的版本安装对应的MXNet版本。假设你安装了CUDA 9.1,那么我们可以通过`pip install --pre mxnet-cu91`来安装支持CUDA 9.1的MXNet版本。 -为了使用MXNet的GPU版本,我们需要根据CUDA版本安装`mxnet-cu75`、`mxnet-cu80`或者`mxnet-cu90`。例如 +## 计算设备 -> `pip install --pre mxnet-cu80` +MXNet使用`context`来指定用来存储和计算的设备,例如可以是CPU或者GPU。默认情况下,MXNet会将数据创建在主内存,然后利用CPU来计算。在MXNet中,CPU和GPU可分别由`cpu()`和`gpu()`来表示。需要注意的是,`mx.cpu()`表示所有的物理CPU和内存。这意味着计算上会尽量使用所有的CPU核。但`mx.gpu()`只代表一块显卡和相应的显卡内存。如果有多块GPU,我们用`mx.gpu(i)`来表示第$i$块GPU($i$从0开始)。 -## 处理器 - -使用MXNet的GPU版本和之前没什么不同。下面导入本节中实验所需的包。 ```{.python .input} import mxnet as mx -from mxnet import gluon, nd -import sys -``` - -MXNet使用`context`来指定用来存储和计算的CPU/GPU。默认情况下,MXNet会将数据开在主内存,然后利用CPU来计算。在MXNet中,CPU和GPU可分别由`mx.cpu()`和`mx.gpu()`来表示。需要注意的是,`mx.cpu()`表示所有的物理CPU和内存。这意味着计算上会尽量使用所有的CPU核。但`mx.gpu()`只代表一块显卡和相应的显卡内存。如果有多块GPU,我们用`mx.gpu(i)`来表示第$i$块GPU($i$从0开始)。 +from mxnet import nd +from mxnet.gluon import nn -```{.python .input n=3} [mx.cpu(), mx.gpu(), mx.gpu(1)] ``` ## NDArray的GPU计算 -每个NDArray都有一个`context`属性来表示它存在哪个CPU/GPU上。默认情况下,NDArray存在CPU上。因此,之前我们每次打印NDArray的时候都会看到`@cpu(0)`这个标识。 +默认情况下,NDArray存在CPU上。因此,之前我们每次打印NDArray的时候都会看到`@cpu(0)`这个标识。 ```{.python .input n=4} x = nd.array([1,2,3]) -print('x: ', x, '\ncontext of x: ', x.context) +x +``` + +我们可以通过NDArray的`context`属性来查看其所在的设备。 + +```{.python .input} +x.context ``` ### GPU上的存储 -我们可以在创建NDArray的时候通过`ctx`指定存储的CPU/GPU。 +我们有多种方法将NDArray放置在GPU上。例如我们可以在创建NDArray的时候通过`ctx`指定存储设备。下面我们将`a`创建在GPU 0上。注意到在打印`a`时,设备信息变成了`@gpu(0)`。创建在GPU上时我们会只用GPU内存,你可以通过`nvidia-smi`查看GPU内存使用情况。通常你需要确保不要创建超过GPU内存上限的数据。 ```{.python .input n=5} a = nd.array([1, 2, 3], ctx=mx.gpu()) -b = nd.zeros((3, 2), ctx=mx.gpu()) -# 假设至少存在2块GPU。如果不存在则会报错。 -c = nd.random.uniform(shape=(2, 3), ctx=mx.gpu(1)) -print('a: ', a, '\nb: ', b, '\nc: ', c) +a +``` + +假设你至少有两块GPU,下面代码将会在GPU 1上创建随机数组 + +```{.python .input} +b = nd.random.uniform(shape=(2, 3), ctx=mx.gpu(1)) +b ``` -我们可以通过`copyto`和`as_in_context`函数在CPU/GPU之间传输数据。 +除了在创建时指定,我们也可以通过`copyto`和`as_in_context`函数在设备之间传输数据。下面我们将CPU上的`x`复制到GPU 0上。 ```{.python .input n=7} y = x.copyto(mx.gpu()) +y +``` + +```{.python .input} z = x.as_in_context(mx.gpu()) -print('x: ', x, '\ny: ', y, '\nz: ', z) +z ``` -需要区分的是,如果源变量和目标变量的`context`一致,`as_in_context`使目标变量和源变量共享源变量的内存,而`copyto`总是为目标变量新创建内存。 +需要区分的是,如果源变量和目标变量的`context`一致,`as_in_context`使目标变量和源变量共享源变量的内存 ```{.python .input n=8} -y_target = y.as_in_context(mx.gpu()) -z_target = z.copyto(mx.gpu()) -print('y: ', y, '\ny_target: ', y_target) -print('z: ', z, '\nz_target: ', z_target) -print('y_target and y share memory? ', y_target is y) -print('z_target and z share memory? ', z_target is z) +y.as_in_context(mx.gpu()) is y +``` + +而`copyto`总是为目标变量新创建内存。 + +```{.python .input} +y.copyto(mx.gpu()) is y ``` ### GPU上的计算 @@ -83,24 +85,16 @@ print('z_target and z share memory? ', z_target is z) MXNet的计算会在数据的`context`上执行。为了使用GPU计算,我们只需要事先将数据放在GPU上面。而计算结果会自动保存在相同的GPU上。 ```{.python .input n=9} -nd.exp(z + 2) * y +(z + 2).exp() * y ``` 注意,MXNet要求计算的所有输入数据都在同一个CPU/GPU上。这个设计的原因是不同CPU/GPU之间的数据交互通常比较耗时。因此,MXNet希望用户确切地指明计算的输入数据都在同一个CPU/GPU上。例如,如果将CPU上的`x`和GPU上的`y`做运算,会出现错误信息。 -### 其他复制到主内存的操作 - -当我们打印NDArray或将NDArray转换成NumPy格式时,MXNet会自动将数据复制到主内存。 - -```{.python .input n=11} -print(y) -print(y.asnumpy()) -print(y.sum().asscalar()) -``` +当我们打印NDArray或将NDArray转换成NumPy格式时,如果数据不在主内存里,MXNet会自动将其先复制到主内存,从而带来隐形的传输开销。 ## Gluon的GPU计算 -同NDArray类似,Gluon的大部分函数可以通过`ctx`指定CPU/GPU。下面代码将模型参数初始化在GPU上。 +同NDArray类似,Gluon的模型可以在初始化时通过`ctx`指定设备。下面代码将模型参数初始化在GPU上。 ```{.python .input n=12} net = gluon.nn.Sequential() @@ -111,8 +105,7 @@ net.initialize(ctx=mx.gpu()) 当输入是GPU上的NDArray时,Gluon会在相同的GPU上计算结果。 ```{.python .input n=13} -data = nd.random.uniform(shape=[3, 2], ctx=mx.gpu()) -net(data) +net(y) ``` 确认一下模型参数存储在相同的GPU上。