模型参数的访问、初始化和共享

在之前的小节里我们一直在使用默认的初始函数,net.initialize(),来初始化模型参数。我们也同时介绍过如何访问模型参数的简单方法。这一节我们将深入讲解模型参数的访问和初始化,以及如何在多个层之间共享同一份参数。

我们首先定义同前的多层感知机、初始化权重和计算前向结果。与之前不同的是,在这里我们从 MXNet 中导入了 init 这个包,它包含了多种模型初始化方法。

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

net = nn.Sequential()
net.add(nn.Dense(256, activation='relu'))
net.add(nn.Dense(10))
net.initialize()

x = nd.random.uniform(shape=(2,20))
y = net(x)

访问模型参数

我们知道可以通过 [] 来访问 Sequential 类构造出来的网络的特定层。对于带有模型参数的层,我们可以通过 Block 类的 params 属性来得到它包含的所有参数。例如我们查看隐藏层的参数:

In [2]:
net[0].params
Out[2]:
dense0_ (
  Parameter dense0_weight (shape=(256, 20), dtype=float32)
  Parameter dense0_bias (shape=(256,), dtype=float32)
)

可以看到我们得到了一个由参数名称映射到参数实例的字典。第一个参数的名称为 dense0_weight,它由 net[0] 的名称(dense0_)和自己的变量名(weight)组成。而且可以看到它参数的形状为 (256, 20),且数据类型为 32 位浮点数。

为了访问特定参数,我们既可以通过名字来访问字典里的元素,也可以直接使用它的变量名。下面两种方法是等价的,但通常后者的代码可读性更好。

In [3]:
net[0].params['dense0_weight'], net[0].weight
Out[3]:
(Parameter dense0_weight (shape=(256, 20), dtype=float32),
 Parameter dense0_weight (shape=(256, 20), dtype=float32))

Gluon 里参数类型为 Parameter 类,其包含参数权重和它对应的梯度,它们可以分别通过 datagrad 函数来访问。因为我们随机初始化了权重,所以它是一个由随机数组成的形状为 (256, 20) 的 NDArray.

In [4]:
net[0].weight.data()
Out[4]:

[[ 0.06700657 -0.00369488  0.0418822  ..., -0.05517294 -0.01194733
  -0.00369594]
 [-0.03296221 -0.04391347  0.03839272 ...,  0.05636378  0.02545484
  -0.007007  ]
 [-0.0196689   0.01582889 -0.00881553 ...,  0.01509629 -0.01908049
  -0.02449339]
 ...,
 [ 0.00010955  0.0439323  -0.04911506 ...,  0.06975312  0.0449558
  -0.03283203]
 [ 0.04106557  0.05671307 -0.00066976 ...,  0.06387014 -0.01292654
   0.00974177]
 [ 0.00297424 -0.0281784  -0.06881659 ..., -0.04047417  0.00457048
   0.05696651]]
<NDArray 256x20 @cpu(0)>

梯度的形状跟权重一样。但由于我们还没有进行反向传播计算,所以它的值全为 0.

In [5]:
net[0].weight.grad()
Out[5]:

[[ 0.  0.  0. ...,  0.  0.  0.]
 [ 0.  0.  0. ...,  0.  0.  0.]
 [ 0.  0.  0. ...,  0.  0.  0.]
 ...,
 [ 0.  0.  0. ...,  0.  0.  0.]
 [ 0.  0.  0. ...,  0.  0.  0.]
 [ 0.  0.  0. ...,  0.  0.  0.]]
<NDArray 256x20 @cpu(0)>

类似我们可以访问其他的层的参数。例如输出层的偏差权重:

In [6]:
net[1].bias.data()
Out[6]:

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

最后,我们可以 collect_params 函数来获取 net 实例所有嵌套(例如通过 add 函数嵌套)的层所包含的所有参数。它返回的同样是一个参数名称到参数实例的字典。

In [7]:
net.collect_params()
Out[7]:
sequential0_ (
  Parameter dense0_weight (shape=(256, 20), dtype=float32)
  Parameter dense0_bias (shape=(256,), dtype=float32)
  Parameter dense1_weight (shape=(10, 256), dtype=float32)
  Parameter dense1_bias (shape=(10,), dtype=float32)
)

初始化模型参数

当使用默认的模型初始化,Gluon 会将权重参数元素初始化为 [-0.07, 0.07] 之间均匀分布的随机数,偏差参数则全为 0. 但经常我们需要使用其他的方法来初始话权重,MXNet 的 init 模块里提供了多种预设的初始化方法。例如下面例子我们将权重参数初始化成均值为 0,标准差为 0.01 的正态分布随机数。

In [8]:
# 非首次对模型初始化需要指定 force_reinit。
net.initialize(init=init.Normal(sigma=0.01), force_reinit=True)
net[0].weight.data()[0]
Out[8]:

[ 0.01074176  0.00066428  0.00848699 -0.0080038  -0.00168822  0.00936328
  0.00357444  0.00779328 -0.01010307 -0.00391573  0.01316619 -0.00432926
  0.0071536   0.00925416 -0.00904951 -0.00074684  0.0082254  -0.01878511
  0.00885884  0.01911872]
<NDArray 20 @cpu(0)>

如果想只对某个特定参数进行初始化,我们可以调用 Paramter 类的 initialize 函数,它的使用跟 Block 类提供的一致。下例中我们对第一个隐藏层的权重使用 Xavier 初始化方法。

In [9]:
net[0].weight.initialize(init=init.Xavier(), force_reinit=True)
net[0].weight.data()[0]
Out[9]:

[ 0.00512482 -0.06579044 -0.10849719 -0.09586414  0.06394844  0.06029618
 -0.03065033 -0.01086642  0.01929168  0.1003869  -0.09339568 -0.08703034
 -0.10472868 -0.09879824 -0.00352201 -0.11063069 -0.04257748  0.06548801
  0.12987629 -0.13846186]
<NDArray 20 @cpu(0)>

自定义初始化方法

有时候我们需要的初始化方法并没有在 init 模块中提供。这时,我们可以实现一个 Initializer 类的子类使得我们可以跟前面使用 init.Normal 那样使用它。通常,我们只需要实现 _init_weight 这个函数,将其传入的 NDArray 修改成需要的内容。下面例子里我们把权重初始化成 [-10,-5][5,10] 两个区间里均匀分布的随机数。

In [10]:
class MyInit(init.Initializer):
    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

net.initialize(MyInit(), force_reinit=True)
net[0].weight.data()[0]
Init dense0_weight (256, 20)
Init dense1_weight (10, 256)
Out[10]:

[-5.36596727  7.57739449  8.98637581 -0.          8.8275547   0.
  5.98405075 -0.          0.          0.          7.48575974 -0.         -0.
  6.89100075  6.97887039 -6.11315536  0.          5.46652031 -9.73526287
  9.48517227]
<NDArray 20 @cpu(0)>

此外,我们还可以通过 Parameter 类的 set_data 函数来直接改写模型参数。例如下例中我们将隐藏层参数在现有的基础上加 1。

In [11]:
net[0].weight.set_data(net[0].weight.data() + 1)
net[0].weight.data()[0]
Out[11]:

[ -4.36596727   8.57739449   9.98637581   1.           9.8275547    1.
   6.98405075   1.           1.           1.           8.48575974   1.           1.
   7.89100075   7.97887039  -5.11315536   1.           6.46652031
  -8.73526287  10.48517227]
<NDArray 20 @cpu(0)>

共享模型参数

在有些情况下,我们希望在多个层之间共享模型参数。我们在 “模型构造” 这一节看到了如何在 Block 类里 forward 函数里多次调用同一个类来完成。这里将介绍另外一个方法,它在构造层的时候指定使用特定的参数。如果不同层使用同一份参数,那么它们不管是在前向计算还是反向传播时都会共享共同的参数。

在下面例子里,我们让模型的第二隐藏层和第三隐藏层共享模型参数。

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

x = nd.random.uniform(shape=(2,20))
net(x)

net[1].weight.data()[0] == net[2].weight.data()[0]
Out[12]:

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

我们在构造第三隐藏层时通过 params 来指定它使用第二隐藏层的参数。由于模型参数里包含了梯度,所以在反向传播计算时,第二隐藏层和第三隐藏层的梯度都会被累加在 shared.params.grad() 里。

小结

  • 我们有多种方法来访问、初始化和共享模型参数。

练习

  • 查阅 MXNet 文档,了解不同的参数初始化方式。
  • 尝试在 net.initialize() 后和 net(x) 前访问模型参数,看看会发生什么。
  • 构造一个含共享参数层的多层感知机并训练。观察每一层的模型参数和梯度计算。

扫码直达 讨论区