创建神经网络

前面的教程我们教了大家如何实现线性回归,多类Logistic回归和多层感知机。我们既展示了如何从0开始实现,也提供使用gluon的更紧凑的实现。因为前面我们主要关注在模型本身,所以只解释了如何使用gluon,但没说明他们是如何工作的。我们使用了nn.Sequential,它是nn.Block的一个简单形式,但没有深入了解它们。

本教程和接下来几个教程,我们将详细解释如何使用这两个类来定义神经网络、初始化参数、以及保存和读取模型。

我们重新把多层感知机 — 使用Gluon里的网络定义搬到这里作为开始的例子(为了简单起见,这里我们丢掉了Flatten层)。

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

net = nn.Sequential()
with net.name_scope():
    net.add(nn.Dense(256, activation="relu"))
    net.add(nn.Dense(10))

print(net)
Sequential(
  (0): Dense(None -> 256, Activation(relu))
  (1): Dense(None -> 10, linear)
)

使用 nn.Block 来定义

事实上,nn.Sequentialnn.Block的简单形式。我们先来看下如何使用nn.Block来实现同样的网络。

In [2]:
class MLP(nn.Block):
    def __init__(self, **kwargs):
        super(MLP, self).__init__(**kwargs)
        with self.name_scope():
            self.dense0 = nn.Dense(256)
            self.dense1 = nn.Dense(10)

    def forward(self, x):
        return self.dense1(nd.relu(self.dense0(x)))

可以看到nn.Block的使用是通过创建一个它子类的类,其中至少包含了两个函数。

  • __init__:创建参数。上面例子我们使用了包含了参数的dense
  • forward():定义网络的计算

我们所创建的类的使用跟前面net没有太多不一样。

In [3]:
net2 = MLP()
print(net2)
net2.initialize()
x = nd.random.uniform(shape=(4,20))
y = net2(x)
y
MLP(
  (dense0): Dense(None -> 256, linear)
  (dense1): Dense(None -> 10, linear)
)
Out[3]:

[[ 0.03126615  0.04562764  0.00039857 -0.08772386 -0.05355632  0.02904574
   0.08102557 -0.01433946 -0.04224151  0.06047882]
 [ 0.02871901  0.03652265  0.00630051 -0.05650971 -0.07189322  0.08615957
   0.05951559 -0.06045965 -0.0299026   0.05651001]
 [ 0.02147349  0.04818896  0.05321142 -0.12616856 -0.0685023   0.09096345
   0.04064304 -0.05064794 -0.02200242  0.04859561]
 [ 0.03780478  0.0751239   0.03290457 -0.11641113 -0.03254967  0.0586529
   0.02542157 -0.01697343 -0.00049652  0.05892839]]
<NDArray 4x10 @cpu(0)>
In [4]:
nn.Dense
Out[4]:
mxnet.gluon.nn.basic_layers.Dense

如何定义创建和使用nn.Dense比较好理解。接下来我们仔细看下MLP里面用的其他命令:

  • super(MLP, self).__init__(**kwargs):这句话调用nn.Block__init__函数,它提供了prefix(指定名字)和params(指定模型参数)两个参数。我们会之后详细解释如何使用。
  • self.name_scope():调用nn.Block提供的name_scope()函数。nn.Dense的定义放在这个scope里面。它的作用是给里面的所有层和参数的名字加上前缀(prefix)使得他们在系统里面独一无二。默认自动会自动生成前缀,我们也可以在创建的时候手动指定。推荐在构建网络时,每个层至少在一个name_scope()里。
In [5]:
print('default prefix:', net2.dense0.name)

net3 = MLP(prefix='another_mlp_')
print('customized prefix:', net3.dense0.name)
default prefix: mlp0_dense0
customized prefix: another_mlp_dense0

大家会发现这里并没有定义如何求导,或者是backward()函数。事实上,系统会使用autogradforward()自动生成对应的backward()函数。

nn.Block到底是什么东西?

gluon里,nn.Block是一个一般化的部件。整个神经网络可以是一个nn.Block,单个层也是一个nn.Block。我们可以(近似)无限地嵌套nn.Block来构建新的nn.Block

nn.Block主要提供这个东西

  1. 存储参数
  2. 描述forward如何执行
  3. 自动求导

那么现在可以解释nn.Sequential了吧

nn.Sequential是一个nn.Block容器,它通过add来添加nn.Block。它自动生成forward()函数,其就是把加进来的nn.Block逐一运行。

一个简单的实现是这样的:

In [6]:
class Sequential(nn.Block):
    def __init__(self, **kwargs):
        super(Sequential, self).__init__(**kwargs)
    def add(self, block):
        self._children.append(block)
    def forward(self, x):
        for block in self._children:
            x = block(x)
        return x

可以跟nn.Sequential一样的使用这个自定义的类:

In [7]:
net4 = Sequential()
with net4.name_scope():
    net4.add(nn.Dense(256, activation="relu"))
    net4.add(nn.Dense(10))

net4.initialize()
y = net4(x)
y
Out[7]:

[[-0.00411107  0.00781807  0.03506001 -0.01106469  0.09599376 -0.04190594
   0.01127483 -0.01493319  0.0716491   0.00700368]
 [ 0.01214233  0.02546027  0.03533494 -0.02328116  0.10768862 -0.01672854
  -0.02653831 -0.03458688  0.0640486  -0.00030123]
 [-0.00452384  0.00228632  0.02761049 -0.05750641  0.10328892 -0.01792853
  -0.04610601 -0.04085524  0.05824737  0.00033787]
 [-0.00518477 -0.02185423  0.02528594 -0.00436605  0.05142229 -0.02703231
   0.01939205 -0.03802725  0.02832718 -0.0172073 ]]
<NDArray 4x10 @cpu(0)>

可以看到,nn.Sequential的主要好处是定义网络起来更加简单。但nn.Block可以提供更加灵活的网络定义。考虑下面这个例子

In [8]:
class FancyMLP(nn.Block):
    def __init__(self, **kwargs):
        super(FancyMLP, self).__init__(**kwargs)
        with self.name_scope():
            self.dense = nn.Dense(256)
            self.weight = nd.random_uniform(shape=(256,20))

    def forward(self, x):
        x = nd.relu(self.dense(x))
        x = nd.relu(nd.dot(x, self.weight)+1)
        x = nd.relu(self.dense(x))
        return x

看到这里我们直接手动创建和初始了权重weight,并重复用了dense的层。测试一下:

In [9]:
fancy_mlp = FancyMLP()
fancy_mlp.initialize()
y = fancy_mlp(x)
print(y.shape)
(4, 256)

nn.Blocknn.Sequential的嵌套使用

现在我们知道了nn下面的类基本都是nn.Block的子类,他们可以很方便地嵌套使用。

In [10]:
class RecMLP(nn.Block):
    def __init__(self, **kwargs):
        super(RecMLP, self).__init__(**kwargs)
        self.net = nn.Sequential()
        with self.name_scope():
            self.net.add(nn.Dense(256, activation="relu"))
            self.net.add(nn.Dense(128, activation="relu"))
            self.dense = nn.Dense(64)

    def forward(self, x):
        return nd.relu(self.dense(self.net(x)))

rec_mlp = nn.Sequential()
rec_mlp.add(RecMLP())
rec_mlp.add(nn.Dense(10))
print(rec_mlp)
Sequential(
  (0): RecMLP(
    (net): Sequential(
      (0): Dense(None -> 256, Activation(relu))
      (1): Dense(None -> 128, Activation(relu))
    )
    (dense): Dense(None -> 64, linear)
  )
  (1): Dense(None -> 10, linear)
)

总结

不知道你同不同意,通过nn.Block来定义神经网络跟玩积木很类似。

练习

如果把RecMLP改成self.denses = [nn.Dense(256), nn.Dense(128), nn.Dense(64)]forward就用for loop来实现,会有什么问题吗?

吐槽和讨论欢迎点这里