模型参数

为了引出本节的话题,让我们先构造一个多层感知机。首先,导入本节中实验所需的包。

In [1]:
from mxnet import init, gluon, nd
from mxnet.gluon import nn
import sys

下面定义多层感知机。

In [2]:
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)))

运行下面代码,系统抱怨说模型参数没有初始化。

In [3]:
x = nd.random.uniform(shape=(3, 5))
try:
    net = MLP()
    net(x)
except RuntimeError as err:
    sys.stderr.write(str(err))
Parameter 'mlp0_dense0_weight' has not been initialized. Note that you should initialize parameters and create Trainer with Block.collect_params() instead of Block.params because the later does not include Parameters of nested child Blocks

作如下修改之后,模型便计算成功。

In [4]:
net.initialize()
net(x)
Out[4]:

[[ 0.00212593  0.00365805]
 [ 0.00161272  0.00441845]
 [ 0.00204872  0.00352518]]
<NDArray 3x2 @cpu(0)>

这里添加的net.initialize()对模型参数做了初始化。模型参数是深度学习计算中的重要组成部分。本节中,我们将介绍如何访问、初始化和共享模型参数。

访问模型参数

在Gluon中,模型参数的类型是Parameter。下面让我们创建一个名字叫“good_param”、形状为\(2 \times 3\)的模型参数。在默认的初始化中,模型参数中的每一个元素是一个在[-0.07, 0.07]之间均匀分布的随机数。相应地,该模型参数还有一个形状为\(2 \times 3\)的梯度,初始值为0。

In [5]:
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)
data:
[[ 0.0421275  -0.00539289  0.00286685]
 [ 0.03927409  0.02504314 -0.05344158]]
<NDArray 2x3 @cpu(0)>
grad:
[[ 0.  0.  0.]
 [ 0.  0.  0.]]
<NDArray 2x3 @cpu(0)>
name:  good_param

接下来,让我们访问本节开头定义的多层感知机net中隐藏层hidden的模型参数:权重weight和偏差bias。它们的类型也都是Parameter。我们可以看到它们的名字、形状和数据类型。

In [6]:
w = net.hidden.weight
b = net.hidden.bias
print('hidden layer name: ', net.hidden.name, '\nweight: ', w, '\nbias: ', b)
hidden layer name:  mlp0_dense0
weight:  Parameter mlp0_dense0_weight (shape=(4, 5), dtype=<class 'numpy.float32'>)
bias:  Parameter mlp0_dense0_bias (shape=(4,), dtype=<class 'numpy.float32'>)

我们同样可以访问这两个参数的值和梯度。

In [7]:
print('weight:', w.data(), '\nweight grad:', w.grad(), '\nbias:', b.data(),
      '\nbias grad:', b.grad())
weight:
[[-0.06206018  0.06491279 -0.03182812 -0.01631819 -0.00312688]
 [ 0.0408415   0.04370362  0.00404529 -0.0028032   0.00952624]
 [-0.01501013  0.05958354  0.04705103 -0.06005495 -0.02276454]
 [-0.0578019   0.02074406 -0.06716943 -0.01844618  0.04656678]]
<NDArray 4x5 @cpu(0)>
weight grad:
[[ 0.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.]]
<NDArray 4x5 @cpu(0)>
bias:
[ 0.  0.  0.  0.]
<NDArray 4 @cpu(0)>
bias grad:
[ 0.  0.  0.  0.]
<NDArray 4 @cpu(0)>

另外,我们也可以通过collect_params来访问Block里的所有参数(包括所有的子Block)。它会返回一个名字到对应Parameter的字典。在这个字典中,我们既可以用[](需要指定前缀),又可以用get()(不需要指定前缀)来访问模型参数。

In [8]:
params = net.collect_params()
print(params)
print(params['mlp0_dense0_bias'].data())
print(params.get('dense0_bias').data())
mlp0_ (
  Parameter mlp0_dense0_weight (shape=(4, 5), dtype=<class 'numpy.float32'>)
  Parameter mlp0_dense0_bias (shape=(4,), dtype=<class 'numpy.float32'>)
  Parameter mlp0_dense1_weight (shape=(2, 4), dtype=<class 'numpy.float32'>)
  Parameter mlp0_dense1_bias (shape=(2,), dtype=<class 'numpy.float32'>)
)

[ 0.  0.  0.  0.]
<NDArray 4 @cpu(0)>

[ 0.  0.  0.  0.]
<NDArray 4 @cpu(0)>

初始化模型参数

在Gluon中,模型的偏差参数总是默认初始化为0。当我们对整个模型所有参数做初始化时,默认下权重参数的所有元素为[-0.07, 0.07]之间均匀分布的随机数。我们也可以使用其他初始化方法。以下例子使用了均值为0,标准差为0.02的正态分布来随机初始化模型中所有层的权重参数。

In [9]:
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())
hidden weight:
[[ 0.01725933 -0.02177767  0.01344385  0.00272668 -0.00392631]
 [-0.03435376  0.01124353 -0.00622001  0.00689361  0.02062465]
 [ 0.00675439  0.01104854  0.01147354  0.00579418 -0.04144352]
 [-0.02262641  0.00582818  0.00072618  0.02628598 -0.00958349]]
<NDArray 4x5 @cpu(0)>
hidden bias:
[ 0.  0.  0.  0.]
<NDArray 4 @cpu(0)>
output weight:
[[-0.0179193  -0.01632507 -0.03224728  0.01471114]
 [-0.00140731 -0.02293223 -0.02087744 -0.03070692]]
<NDArray 2x4 @cpu(0)>
output bias:
[ 0.  0.]
<NDArray 2 @cpu(0)>

我们也可以把模型中任意层任意参数初始化,例如把上面模型中隐藏层的偏差参数初始化为1。

In [10]:
net.hidden.bias.initialize(init=init.One(), force_reinit=True)
print(net.hidden.bias.data())

[ 1.  1.  1.  1.]
<NDArray 4 @cpu(0)>

自定义初始化方法

下面我们自定义一个初始化方法。它通过重载_init_weight来实现自定义的初始化方法。

In [11]:
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()
Out[11]:

[[ 14.14368629  14.66310787  14.74697495  12.44425583  16.2351017 ]
 [ 11.58969593  13.38007641  11.10375118  16.74752235  16.56329536]
 [ 13.1720171   11.38182926  17.7834549   11.96582413  19.49571037]
 [ 13.68725204  16.62526894  18.20993233  10.13571644  10.97101307]]
<NDArray 4x5 @cpu(0)>

我们还可以通过Parameter.set_data来直接改写模型参数。

In [12]:
net = MLP()
net.initialize()
net(x)
print('output layer default weight:', net.output.weight.data())

w = net.output.weight
w.set_data(nd.ones(w.shape))
print('output layer modified weight:', net.output.weight.data())
output layer default weight:
[[ 0.05855297 -0.06101935 -0.0396449   0.0269461 ]
 [ 0.00912645  0.0093242   0.05111437 -0.03284547]]
<NDArray 2x4 @cpu(0)>
output layer modified weight:
[[ 1.  1.  1.  1.]
 [ 1.  1.  1.  1.]]
<NDArray 2x4 @cpu(0)>

延后的初始化

我们在本节开头定义的MLP模型的层nn.Dense(4)nn.Dense(2)中无需指定它们的输入单元个数。定义net = MLP()和输入数据x。我们在“模型构造”一节中介绍过,执行net(x)将调用netforward函数计算模型输出。在这次计算中,net也将从输入数据x的形状自动推断模型中每一层尚未指定的输入单元个数,得到模型中所有参数形状,并真正完成模型参数的初始化。因此,在上面两个例子中,我们总是在调用net(x)之后访问初始化的模型参数。

这种延后的初始化带来的一大便利是,我们在构造模型时无需指定每一层的输入单元个数。

下面,我们具体来看延后的初始化是怎么工作的。让我们新建一个网络并打印所有模型参数。这时,两个全连接层的权重的形状里都有0。它们代表尚未指定的输入单元个数。

In [13]:
net = MLP()
net.collect_params()
Out[13]:
mlp3_ (
  Parameter mlp3_dense0_weight (shape=(4, 0), dtype=<class 'numpy.float32'>)
  Parameter mlp3_dense0_bias (shape=(4,), dtype=<class 'numpy.float32'>)
  Parameter mlp3_dense1_weight (shape=(2, 0), dtype=<class 'numpy.float32'>)
  Parameter mlp3_dense1_bias (shape=(2,), dtype=<class 'numpy.float32'>)
)

然后,调用net.initialize()并打印所有模型参数。这时模型参数依然没有被初始化。

In [14]:
net.initialize()
net.collect_params()
Out[14]:
mlp3_ (
  Parameter mlp3_dense0_weight (shape=(4, 0), dtype=<class 'numpy.float32'>)
  Parameter mlp3_dense0_bias (shape=(4,), dtype=<class 'numpy.float32'>)
  Parameter mlp3_dense1_weight (shape=(2, 0), dtype=<class 'numpy.float32'>)
  Parameter mlp3_dense1_bias (shape=(2,), dtype=<class 'numpy.float32'>)
)

接下来,当模型见到输入数据x后(shape=(3, 5)),模型每一层参数的形状得以推断,参数的初始化最终完成。

In [15]:
print(x)
net(x)
net.collect_params()

[[ 0.54881352  0.59284461  0.71518934  0.84426576  0.60276335]
 [ 0.85794562  0.54488319  0.84725171  0.42365479  0.62356371]
 [ 0.64589411  0.38438171  0.4375872   0.29753461  0.89177299]]
<NDArray 3x5 @cpu(0)>
Out[15]:
mlp3_ (
  Parameter mlp3_dense0_weight (shape=(4, 5), dtype=<class 'numpy.float32'>)
  Parameter mlp3_dense0_bias (shape=(4,), dtype=<class 'numpy.float32'>)
  Parameter mlp3_dense1_weight (shape=(2, 4), dtype=<class 'numpy.float32'>)
  Parameter mlp3_dense1_bias (shape=(2,), dtype=<class 'numpy.float32'>)
)

共享模型参数

在有些情况下,我们希望模型的多个层之间共享模型参数。这时,我们可以通过Block的params来指定模型参数。在下面使用Sequential类构造的多层感知机中,模型的第二隐藏层(net[1])和第三隐藏层(net[2])共享模型参数。

In [16]:
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())

[[ 0.02520778 -0.00740245 -0.00711232  0.04849721]
 [ 0.06699993  0.0279271  -0.05373173 -0.02835883]
 [ 0.03738332  0.0439317  -0.01234518 -0.0144892 ]
 [ 0.02456146  0.05335445 -0.03502852  0.01137821]]
<NDArray 4x4 @cpu(0)>

[[ 0.02520778 -0.00740245 -0.00711232  0.04849721]
 [ 0.06699993  0.0279271  -0.05373173 -0.02835883]
 [ 0.03738332  0.0439317  -0.01234518 -0.0144892 ]
 [ 0.02456146  0.05335445 -0.03502852  0.01137821]]
<NDArray 4x4 @cpu(0)>

同样,我们也可以在使用Block构造的多层感知机中,让模型的第二隐藏层(hidden2)和第三隐藏层(hidden3)共享模型参数。

In [17]:
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())

[[ 0.05298331 -0.05103363 -0.05559913 -0.02824048]
 [-0.05706766  0.00979508 -0.02043347  0.01272219]
 [ 0.00725428  0.01040554 -0.06529249  0.02144811]
 [ 0.06565464  0.02129445 -0.02506039 -0.00960142]]
<NDArray 4x4 @cpu(0)>

[[ 0.05298331 -0.05103363 -0.05559913 -0.02824048]
 [-0.05706766  0.00979508 -0.02043347  0.01272219]
 [ 0.00725428  0.01040554 -0.06529249  0.02144811]
 [ 0.06565464  0.02129445 -0.02506039 -0.00960142]]
<NDArray 4x4 @cpu(0)>

小结

  • 我们可以很方便地访问、自定义和共享模型参数。

练习

  • 在本节任何一个例子中,net.collect_params()net.params的返回有什么不同?
  • 查阅MXNet文档,了解不同的参数初始化方式。
  • 构造一个含共享参数层的多层感知机并训练。观察每一层的模型参数。
  • 如果两个层共用一个参数,求梯度的时候会发生什么?

扫码直达讨论区