模型构造

上一篇中,模型构造是首先需构造Sequential实例,然后添加各层。MXNet还可以通过继承Block类来构造模型。

下面一个例子中,init函数声明带有模型参数的层,函数使用get_constant方法创建训练中不被迭代的参数,即常数参数。forward函数定义模型的前向计算,通过输入x最终返回输出内容。

class FancyMLP(nn.Block):
    def __init__(self, **kwargs):
        super(FancyMLP, self).__init__(**kwargs)
        # 使用get_constant创建的随机权重参数不会在训练中被迭代(即常数参数)
        self.rand_weight = self.params.get_constant(
            'rand_weight', nd.random.uniform(shape=(20, 20)))
        self.dense = nn.Dense(20, activation='relu')

    def forward(self, x):
        x = self.dense(x)
        # 使用创建的常数参数,以及NDArray的relu函数和dot函数
        x = nd.relu(nd.dot(x, self.rand_weight.data()) + 1)
        # 复用全连接层。等价于两个全连接层共享参数
        x = self.dense(x)
        # 控制流,这里我们需要调用asscalar函数来返回标量进行比较
        while x.norm().asscalar() > 1:
            x /= 2
        if x.norm().asscalar() < 0.8:
            x *= 10
        return x.sum()

模型参数访问

使用Sequential类构造的神经网络,可以通过方括号[]来访问网络的任一层。既可以通过名字来访问字典里的元素,也可以直接使用它的变量名。以下两种方法是等价的,可以显示参数的shape和类型。具体参数和梯度的数值可以分别通过data函数和grad函数来访问。

# 获取参数的名称、shape和类型
net[0].weight
net[0].params['dense0_weight']
# 获取具体数值
net[0].weight.data()
net[0].weight.grad()

最后,我们可以使用collect_params函数来获取net变量所有嵌套的层所包含的所有参数。此函数还可以通过正则表达式来匹配参数名,从而筛选需要的参数。

net.collect_params()
# 筛选以weight结尾的参数
net.collect_params('.*weight')

初始化

MXNet提供了多种初始化方法,下面将权重参数初始化成均值为0、标准差为0.01的正态分布随机数。

# 非首次对模型初始化需要指定force_reinit为真
net.initialize(init=init.Normal(sigma=0.01), force_reinit=True)

此外,MXNet使用延后初始化,让模型获得足够信息后才进行初始化。这样我们无须人工推测每个层的输入个数,让模型构造更加简单。

自定义层

我们还可以通过Block类自定义神经网络中的层。在定义含模型参数的层时,利用Block类自带的ParameterDict类型的成员变量params,通过get函数创建参数实例。以下实现了一个含权重参数和偏差参数的全连接层。in_units和units分别代表输入和输出个数。

class MyDense(nn.Block):
    # units为该层的输出个数,in_units为该层的输入个数
    def __init__(self, units, in_units, **kwargs):
        super(MyDense, self).__init__(**kwargs)
        self.weight = self.params.get('weight', shape=(in_units, units))
        self.bias = self.params.get('bias', shape=(units,))

    def forward(self, x):
        linear = nd.dot(x, self.weight.data()) + self.bias.data()
        return nd.relu(linear)

读取和存储

使用save函数和load函数分别存储和读取NDArray。

x = nd.ones(3)
nd.save('x', x)
x2 = nd.load('x')
# 存储一列NDArray并读回内存
nd.save('xy', [x, y])
x2, y2 = nd.load('xy')
# 存储并读取从字符串映射到NDArray的字典
mydict = {'x': x, 'y': y}
nd.save('mydict', mydict)
mydict2 = nd.load('mydict')

GPU计算

通过copyto函数和as_in_context函数在设备之间传输数据。两者的区别是,copyto函数总是为目标变量开新的内存或显存,而如果源变量和目标变量的context(CPU或GPU)一致,as_in_context函数使目标变量和源变量共享源变量的内存或显存。

# 将内存上的NDArray变量x复制到GPU上
y = x.copyto(mx.gpu())
z = x.as_in_context(mx.gpu())