初始化模型参数

我们仍然用MLP这个例子来详细解释如何初始化模型参数。

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

def get_net():
    net = nn.Sequential()
    with net.name_scope():
        net.add(nn.Dense(4, activation="relu"))
        net.add(nn.Dense(2))
    return net

x = nd.random.uniform(shape=(3,5))

我们知道如果不initialize()直接跑forward,那么系统会抱怨说参数没有初始化。

In [2]:
import sys
try:
    net = get_net()
    net(x)
except RuntimeError as err:
    sys.stderr.write(str(err))
Parameter sequential0_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 [3]:
net.initialize()
net(x)
Out[3]:

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

访问模型参数

之前我们提到过可以通过weightbias访问Dense的参数,他们是Parameter这个类:

In [4]:
w = net[0].weight
b = net[0].bias
print('name: ', net[0].name, '\nweight: ', w, '\nbias: ', b)
name:  sequential0_dense0
weight:  Parameter sequential0_dense0_weight (shape=(4, 5), dtype=<class 'numpy.float32'>)
bias:  Parameter sequential0_dense0_bias (shape=(4,), dtype=<class 'numpy.float32'>)

然后我们可以通过data来访问参数,grad来访问对应的梯度

In [5]:
print('weight:', w.data())
print('weight gradient', w.grad())
print('bias:', b.data())
print('bias gradient', 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 gradient
[[ 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 gradient
[ 0.  0.  0.  0.]
<NDArray 4 @cpu(0)>

我们也可以通过collect_params来访问Block里面所有的参数(这个会包括所有的子Block)。它会返回一个名字到对应Parameter的dict。既可以用正常[]来访问参数,也可以用get(),它不需要填写名字的前缀。

In [6]:
params = net.collect_params()
print(params)
print(params['sequential0_dense0_bias'].data())
print(params.get('dense0_weight').data())
sequential0_ (
  Parameter sequential0_dense0_weight (shape=(4, 5), dtype=<class 'numpy.float32'>)
  Parameter sequential0_dense0_bias (shape=(4,), dtype=<class 'numpy.float32'>)
  Parameter sequential0_dense1_weight (shape=(2, 4), dtype=<class 'numpy.float32'>)
  Parameter sequential0_dense1_bias (shape=(2,), dtype=<class 'numpy.float32'>)
)

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

[[-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)>

使用不同的初始函数来初始化

我们一直在使用默认的initialize来初始化权重(除了指定GPU ctx外)。它会把所有权重初始化成在[-0.07, 0.07]之间均匀分布的随机数。我们可以使用别的初始化方法。例如使用均值为0,方差为0.02的正态分布

In [7]:
from mxnet import init
params.initialize(init=init.Normal(sigma=0.02), force_reinit=True)
print(net[0].weight.data(), net[0].bias.data())

[[-0.00359026  0.0302582  -0.01496244  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]]
<NDArray 4x5 @cpu(0)>
[ 0.  0.  0.  0.]
<NDArray 4 @cpu(0)>

看得更加清楚点:

In [8]:
params.initialize(init=init.One(), force_reinit=True)
print(net[0].weight.data(), net[0].bias.data())

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

更多的方法参见init的API.

延后的初始化

我们之前提到过Gluon的一个便利的地方是模型定义的时候不需要指定输入的大小,在之后做forward的时候会自动推测参数的大小。我们具体来看这是怎么工作的。

新创建一个网络,然后打印参数。你会发现两个全连接层的权重的形状里都有0。 这是因为在不知道输入数据的情况下,我们无法判断它们的形状。

In [9]:
net = get_net()
net.collect_params()
Out[9]:
sequential1_ (
  Parameter sequential1_dense0_weight (shape=(4, 0), dtype=<class 'numpy.float32'>)
  Parameter sequential1_dense0_bias (shape=(4,), dtype=<class 'numpy.float32'>)
  Parameter sequential1_dense1_weight (shape=(2, 0), dtype=<class 'numpy.float32'>)
  Parameter sequential1_dense1_bias (shape=(2,), dtype=<class 'numpy.float32'>)
)

然后我们初始化

In [10]:
net.initialize()
net.collect_params()
Out[10]:
sequential1_ (
  Parameter sequential1_dense0_weight (shape=(4, 0), dtype=<class 'numpy.float32'>)
  Parameter sequential1_dense0_bias (shape=(4,), dtype=<class 'numpy.float32'>)
  Parameter sequential1_dense1_weight (shape=(2, 0), dtype=<class 'numpy.float32'>)
  Parameter sequential1_dense1_bias (shape=(2,), dtype=<class 'numpy.float32'>)
)

你会看到我们形状并没有发生变化,这是因为我们仍然不能确定权重形状。真正的初始化发生在我们看到数据时。

In [11]:
net(x)
net.collect_params()
Out[11]:
sequential1_ (
  Parameter sequential1_dense0_weight (shape=(4, 5), dtype=<class 'numpy.float32'>)
  Parameter sequential1_dense0_bias (shape=(4,), dtype=<class 'numpy.float32'>)
  Parameter sequential1_dense1_weight (shape=(2, 4), dtype=<class 'numpy.float32'>)
  Parameter sequential1_dense1_bias (shape=(2,), dtype=<class 'numpy.float32'>)
)

这时候我们看到shape里面的0被填上正确的值了。

共享模型参数

有时候我们想在层之间共享同一份参数,我们可以通过Block的params输出参数来手动指定参数,而不是让系统自动生成。

In [12]:
net = nn.Sequential()
with net.name_scope():
    net.add(nn.Dense(4, activation="relu"))
    net.add(nn.Dense(4, activation="relu"))
    net.add(nn.Dense(4, activation="relu", params=net[-1].params))
    net.add(nn.Dense(2))

初始化然后打印

In [13]:
net.initialize()
net(x)
print(net[1].weight.data())
print(net[2].weight.data())

[[-0.00816047 -0.03040703  0.06714214 -0.05317248]
 [-0.01967777 -0.02854037 -0.00267491 -0.05337812]
 [ 0.02641256 -0.02548236  0.05326662 -0.01200318]
 [ 0.05855297 -0.06101935 -0.0396449   0.0269461 ]]
<NDArray 4x4 @cpu(0)>

[[-0.00816047 -0.03040703  0.06714214 -0.05317248]
 [-0.01967777 -0.02854037 -0.00267491 -0.05337812]
 [ 0.02641256 -0.02548236  0.05326662 -0.01200318]
 [ 0.05855297 -0.06101935 -0.0396449   0.0269461 ]]
<NDArray 4x4 @cpu(0)>

自定义初始化方法

下面我们自定义一个初始化方法。它通过重载_init_weight来实现不同的初始化方法。(注意到Gluon里面bias都是默认初始化成0)

In [14]:
class MyInit(init.Initializer):
    def __init__(self):
        super(MyInit, self).__init__()
        self._verbose = True
    def _init_weight(self, _, arr):
        # 初始化权重,使用out=arr后我们不需指定形状
        print('init weight', arr.shape)
        nd.random.uniform(low=5, high=10, out=arr)

net = get_net()
net.initialize(MyInit())
net(x)
net[0].weight.data()
init weight (4, 5)
init weight (2, 4)
Out[14]:

[[ 9.60578823  7.87973261  5.41556263  9.64648056  6.38859272]
 [ 6.59284496  5.04678345  8.33705139  9.21170998  5.65898943]
 [ 8.23587036  8.58163643  9.20693016  6.44703054  6.32365084]
 [ 5.91595697  6.98910379  7.93256474  7.76410723  5.10053778]]
<NDArray 4x5 @cpu(0)>

当然我们也可以通过Parameter.set_data来直接改写权重。注意到由于有延后初始化,所以我们通常可以通过调用一次net(x)来确定权重的形状先。

In [15]:
net = get_net()
net.initialize()
net(x)

print('default weight:', net[1].weight.data())

w = net[1].weight
w.set_data(nd.ones(w.shape))

print('init to all 1s:', net[1].weight.data())
default weight:
[[ 0.06699993  0.0279271  -0.05373173 -0.02835883]
 [ 0.03738332  0.0439317  -0.01234518 -0.0144892 ]]
<NDArray 2x4 @cpu(0)>
init to all 1s:
[[ 1.  1.  1.  1.]
 [ 1.  1.  1.  1.]]
<NDArray 2x4 @cpu(0)>

总结

我们可以很灵活地访问和修改模型参数。

练习

  1. 研究下net.collect_params()返回的是什么?net.params呢?
  2. 如何对每个层使用不同的初始化函数
  3. 如果两个层共用一个参数,那么求梯度的时候会发生什么?

吐槽和讨论欢迎点这里