一、面向过程与面向对象的优缺点

面向过程使用mxnet,就是使用gluon封装好的对象,不加改动的表达机器学习的逻辑过程,其特点是方便、快捷,缺点是不够灵活(虽然可以应对90%以上的问题了),面向对象基于继承、多态的性质,对原有的gluon类进行了继承重写,并在不改变应用接口的情况下(基于多态),灵活的改写原有类,使之更加符合用户特殊需求。本文从自定义模型、自定义层、自定义初始化三个方面说明gluon的继承重写,这三个基本操作足够用户随心所欲的创造模型了。

二、自定义模型

1、定义静态模型

静态模型就是实例化后模型的结构就不能随便改变了,其代码如下:

from mxnet import nd
from mxnet.gluon import nnclass MLP(nn.Block):# 声明带有模型参数的层,这里声明了两个全连接层def __init__(self, **kwargs):# 调用MLP父类Block的构造函数来进行必要的初始化。这样在构造实例时还可以指定其他函数# 参数,如“模型参数的访问、初始化和共享”一节将介绍的模型参数paramssuper(MLP, self).__init__(**kwargs)self.hidden = nn.Dense(256, activation='relu')  # 隐藏层self.output = nn.Dense(10)  # 输出层# 定义模型的前向计算,即如何根据输入x计算返回所需要的模型输出def forward(self, x):return self.output(self.hidden(x))X = nd.random.uniform(shape=(2, 20))
net = MLP()
net.initialize()
net(X)

2、定义动态模型

动态模型就是在实例化以后,后续可以根据需要随时修改模型结构,下面只定义一个增加网络层的功能。

class MySequential(nn.Block):def __init__(self, **kwargs):super(MySequential, self).__init__(**kwargs)def add(self, block):# block是一个Block子类实例,假设它有一个独一无二的名字。我们将它保存在Block类的# 成员变量_children里,其类型是OrderedDict。当MySequential实例调用# initialize函数时,系统会自动对_children里所有成员初始化self._children[block.name] = blockdef forward(self, x):# OrderedDict保证会按照成员添加时的顺序遍历成员for block in self._children.values():x = block(x)return xnet = MySequential()
net.add(nn.Dense(256, activation='relu'))
net.add(nn.Dense(10))
net.initialize()
net(X)

三、定义tensor流

tensor流就是tensor之间是怎样运算的,gluon默认的tensor流是简单的tensor乘法运算,自定义就对tengsor流使用if判断、for循环手段,构造出更加复杂的tensor流,这一点在后面的卷积网络、循环网络中频繁使用。

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 /= 2if x.norm().asscalar() < 0.8:x *= 10return x.sum()net = FancyMLP()
net.initialize()
net(X)

说明

  1. 以上三个方法是可以结合起来使用的,基于这三点用户可以使用gluon构造出各种卷积、循环网络。
  2. 以上三种继承方式中,forward函数必须定义重写,否则出现下面的错误,就是没找到forward propagation。
print(net(X))
out = self.forward(*args)
raise NotImplementedError
NotImplementedError

三、自定义层

层与模型没有本质区别,从语言角度讲是一样的,二者的数据结构都是tensor+forward,只是用途不同而已。层可以理解为整个模型的一层或一部分,是一段网络,层的作用用来构造模型。

1、gluon的层

Dense层:forward = (X * weight + bias).relu()

g_layer = nn.Dense(2)
g_layer.initialize(init=init.One())
X = nd.array([1, 2, 3, 4]).reshape((1, 4))
y = g_layer(X)
print('weight of g_layer:', g_layer.weight.data())
print('bias of g_layer:', g_layer.bias.data())
print('X:', X)
print('g_layer(X):', y)
print('structure of g_layer:', g_layer)"""
# output
weight of g_layer: 
[[1. 1. 1. 1.][1. 1. 1. 1.]]
<NDArray 2x4 @cpu(0)>
bias of g_layer: 
[0. 0.]
<NDArray 2 @cpu(0)>
X: 
[[1. 2. 3. 4.]]
<NDArray 1x4 @cpu(0)>
g_layer(X): 
[[10. 10.]]
<NDArray 1x2 @cpu(0)>
structure of g_layer: Dense(4 -> 2, linear)
"""

说明:

  1. 再次强调一遍,层和模型的要素是tensor + forward,上面的g_layer是gluon默认的forward,即进行简单的乘法运算(X * tensor);
  2. 因为上面的层从模型的角度看只有一个层,所以查看参数的时候使用g_layer.weight.data(),而不是g_layer[0].weight.data();

2、自定义无参数层

from mxnet import gluon, nd
from mxnet.gluon import nnclass CenteredLayer(nn.Block):def __init__(self, **kwargs):super(CenteredLayer, self).__init__(**kwargs)def forward(self, x):return x - x.mean()
layer = CenteredLayer()
layer(nd.array([1, 2, 3, 4, 5]))

说明: 与上面的g_layer没有区别,都是tensor+forward,这里layer.weight.data()就会报错,因为是0个层;

3、自定义含参数层

自定义的层的意思是tensor也要自定义,tensor就是weight + bias;

class MyDense(nn.Block):def __init__(self, units, in_units, **kwargs):super(MyDense, self).__init__(**kwargs)self.weight1 = self.params.get('haha_weight', shape=(in_units, units))self.bias1 = self.params.get('haha_bias', shape=(units,))def forward(self, x):linear = nd.dot(x, self.weight1.data()) + self.bias1.data()return nd.relu(linear)if __name__ == '__main__':dense = MyDense(units=3, in_units=5)dense.initialize()dense(nd.random.uniform(shape=(2, 5)))print(dense.weight1.data()[0])"""
[0.0068339  0.01299825 0.0301265 ]
<NDArray 3 @cpu(0)>
"""

说明:从这个代码中可以看出一个层的本质就是一段网络;

4、层的应用

net = nn.Sequential()
net.add(MyDense(8, in_units=64),MyDense(1, in_units=8))
net.initialize()
y = net(nd.random.uniform(shape=(2, 64)))
print('self_define tensor:', net[0].weight1.data()[0])"""
self_define tensor: 
[0.0068339  0.01299825 0.0301265  0.04819721 0.01438687 0.050112390.00628365 0.04861524]
<NDArray 8 @cpu(0)>
"""

四、自定义初始化

1、_init_weight在做什么?

# -*- coding: utf-8 -*-
from mxnet import init, nd
from mxnet.gluon import nnclass MyInit(init.Initializer):def _init_weight(self, name, data):print('Init', name, data.shape)if __name__ == '__main__':net = nn.Sequential()net.add(nn.Dense(256, activation='relu'),nn.Dense(256, activation='relu'),nn.Dense(10))net.initialize(init=MyInit())X = nd.random.uniform(shape=(2, 20))print('---------1---------')Y = net(X)print('---------2---------')net.initialize(init=MyInit(), force_reinit=True)"""
# output
---------1---------
Init dense0_weight (256, 20)
Init dense1_weight (256, 256)
Init dense2_weight (10, 256)
---------2---------
Init dense0_weight (256, 20)
Init dense1_weight (256, 256)
Init dense2_weight (10, 256)
"""

2、怎么使用_init_weight自定义初始化?

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() >= 5net.initialize(MyInit(), force_reinit=True)
net[0].weight.data()[0]

说明:上面仅说明对weight初始化,gulon也提供了_init_bias,但是最后还是强制bias=0,也就是重写的_init_bias没有被调用,从机器学习的角度讲,bias一般初始化为0;