MXNet的新接口Gluon

爲何要開發Gluon的接口

在MXNet中咱們能夠經過Sybmol模塊來定義神經網絡,並組經過Module模塊提供的一些上層API來簡化整個訓練過程。那MXNet爲何還要從新開發一套Python的API呢,是不是重複造輪子呢?答案是否認的,Gluon主要是學習了Keras、Pytorch等框架的優勢,支持動態圖(Imperative)編程,更加靈活且方便調試。而原來MXNet基於Symbol來構建網絡的方法是像TF、Caffe2同樣靜態圖的編程方法。同時Gluon也繼續了MXNet在靜態圖上的一些優化,好比節省顯存,並行效率高等,運行起來比Pytorch更快。python

更加簡潔的接口

咱們先來看一下用Gluon的接口,若是建立並組訓練一個神經網絡的,咱們以mnist數據集爲例:編程

import mxnet as mx
import mxnet.ndarray as nd
from mxnet import gluon
import mxnet.gluon.nn as nn

數據的讀取

首先咱們利用Gluon的data模塊來讀取mnist數據集json

def transform(data, label):
    return data.astype('float32') / 255, label.astype('float32')

minist_train_dataset = gluon.data.vision.MNIST(train=True, transform=transform)
minist_test_dataset = gluon.data.vision.MNIST(train=False, transform=transform)
batch_size = 64
train_data = gluon.data.DataLoader(dataset=minist_train_dataset, shuffle=True, batch_size=batch_size)
test_data = gluon.data.DataLoader(dataset=minist_train_dataset, shuffle=False, batch_size=batch_size)
num_examples = len(train_data)
print(num_examples)

訓練模型

這裏咱們使用Gluon來定義一個LeNet網絡

# Step1 定義模型
lenet = nn.Sequential()
with lenet.name_scope():
    lenet.add(nn.Conv2D(channels=20, kernel_size=5, activation='relu'))
    lenet.add(nn.MaxPool2D(pool_size=2, strides=2))
    lenet.add(nn.Conv2D(channels=50, kernel_size=5, activation='relu'))
    lenet.add(nn.MaxPool2D(pool_size=2, strides=2))
    lenet.add(nn.Flatten())
    lenet.add(nn.Dense(128, activation='relu'))
    lenet.add(nn.Dense(10))
# Step2 初始化模型參數
lenet.initialize(ctx=mx.gpu())
# Step3 定義loss
softmax_loss = gluon.loss.SoftmaxCrossEntropyLoss()
# Step4 優化
trainer = gluon.Trainer(lenet.collect_params(), 'sgd', {'learning_rate': 0.5})
def accuracy(output, label):
     return nd.mean(output.argmax(axis=1)==label).asscalar()
def evaluate_accuracy(net, data_iter):
    acc = 0
    for data, label in data_iter:
        data = data.transpose((0,3,1,2))
        data = data.as_in_context(mx.gpu())
        label = label.as_in_context(mx.gpu())
        output = net(data)
        acc += accuracy(output, label)
    return acc / len(data_iter)
import mxnet.autograd as ag
epochs = 5
for e in range(epochs):
    total_loss = 0
    for data, label in train_data:
        data = data.transpose((0,3,1,2))
        data = data.as_in_context(mx.gpu())
        label = label.as_in_context(mx.gpu())
        with ag.record():
            output = lenet(data)
            loss = softmax_loss(output, label)
        loss.backward()
        trainer.step(batch_size)
        total_loss += nd.mean(loss).asscalar()
    print("Epoch %d, test accuracy: %f, average loss: %f" % (e, evaluate_accuracy(lenet, test_data), total_loss/num_examples))

背後的英雄 nn.Block

咱們前面使用了nn.Sequential來定義一個模型,可是沒有仔細介紹它,它實際上是nn.Block的一個簡單的形式。而nn.Block是一個通常化的部件。整個神經網絡能夠是一個nn.Block,單個層也是一個nn.Block。咱們能夠(近似)無限地嵌套nn.Block來構建新的nn.Blocknn.Block主要提供3個方向的功能:app

  1. 存儲參數
  2. 描述forward如何執行
  3. 自動求導

因此nn.Sequential是一個nn.Block的容器,它經過add來添加nn.Block。它自動生成forward()函數。一個簡單實現看起來以下:框架

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.Block裏的魔法後,咱們就能夠自定咱們本身的nn.Block了,來實現不一樣的深度學習應用可能遇到的一些新的層。dom

nn.Block中參數都是以一種Parameter的對象,經過這個對象的data()grad()來訪問對應的數據和梯度。ide

my_param = gluon.Parameter('my_params', shape=(3,3))
my_param.initialize()
(my_param.data(), my_param.grad())

每一個nn.Block裏都有一個類型爲ParameterDict類型的成員變量params來保存全部這個層的參數。它其際上是一個名稱到參數映射的字典。函數

pd = gluon.ParameterDict(prefix='custom_layer_name')
pd.get('custom_layer_param1', shape=(3,3))
pd

自義咱們本身的全鏈接層

當咱們要實現的功能在Gluon.nn模塊中找不到對應的實現時,咱們能夠建立本身的層,它實際也就是一個nn.Block對象。要自定義一個nn.Block以,只須要繼承nn.Block,若是該層須要參數,則在初始化函數中作好對應參數的初始化(實際只是分配的形狀),而後再實現一個forward()函數來描述計算過程。學習

class MyDense(nn.Block):
    def __init__(self, units, in_units, **kwargs):
        super(MyDense, self).__init__(**kwargs)
        with self.name_scope():
            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)

審視模型的參數

咱們將從下面三個方面來詳細講解如何操做gluon定義的模型的參數。

  1. 初始化
  2. 讀取參數
  3. 參數的保存與加載

從上面咱們們在mnist訓練一個模型的步驟中能夠看出,當咱們定義好模型後,第一步就是須要調用initialize()對模型進行參數初始化。

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

咱們一直使用默認的initialize來初始化權重。實際上咱們能夠指定其餘初始化的方法,mxnet.initializer模塊中提供了大量的初始化權重的方法。好比很是流行的Xavier方法。

#net.initialize(init=mx.init.Xavier())
x = nd.random.normal(shape=(3,4))
net(x)

咱們能夠weightbias來訪問Dense的參數,它們是Parameter對象。

w = net[0].weight
b = net[0].bias
print('weight:', w.data())
print('weight gradient', w.grad())
print('bias:', b.data())
print('bias gradient', b.grad())

咱們也能夠經過collect_params來訪問Block裏面全部的參數(這個會包括全部的子Block)。它會返回一個名字到對應Parameter的dict。既能夠用正常[]來訪問參數,也能夠用get(),它不須要填寫名字的前綴。

params = net.collect_params()
print(params)
print(params['sequential18_dense0_weight'].data())
print(params.get('dense0_bias').data()) #不須要名字的前綴

延後的初始化

若是咱們仔細分析過整個網絡的初始化,咱們會有發現,當咱們沒有給網絡真正的輸入數據時,網絡中的不少參數是沒法確認形狀的。

net = get_net()
net.collect_params()
net.initialize()
net.collect_params()

咱們注意到參數中的weight的形狀的第二維都是0, 也就是說尚未確認。那咱們能夠確定的是這些參數確定是尚未分配內存的。

net(x)
net.collect_params()

當咱們給這個網絡一個輸入數據後,網絡中的數據參數的形狀就固定下來了。而這個時候,若是咱們給這個網絡一個不一樣shape的輸入數據,那運行中就會出現崩潰的問題。

模型參數的保存與加載

gluon.Sequential模塊提供了saveload接口來方便咱們對一個網絡的參數進行保存與加載。

filename = "mynet.params"
net.save_params(filename)
net2 = get_net()
net2.load_params(filename, mx.cpu())

Hybridize

從上面咱們使用gluon來訓練mnist,能夠看出,咱們使用的是一種命令式的編程風格。大部分的深度學習框架只在命令式與符號式間二選一。那咱們能不能拿到兩種泛式所有的優勢呢,事實上這一點能夠作到。在MXNet的GluonAPI中,咱們可使用HybridBlock或者HybridSequential來構建網絡。默認他們跟BlockSequential同樣是命令式的。但當咱們調用.hybridize()後,系統會轉撚成符號式來執行。

def get_net():
    net = nn.HybridSequential()
    with net.name_scope():
        net.add(
            nn.Dense(256, activation="relu"),
            nn.Dense(128, activation="relu"),
            nn.Dense(2)
        )
    net.initialize()
    return net

x = nd.random.normal(shape=(1, 512))
net = get_net()
net(x)
net.hybridize()
net(x)

注意到只有繼承自HybridBlock的層纔會被優化。HybridSequential和Gluon提供的層都是它的子類。若是一個層只是繼承自Block,那麼咱們將跳過優化。咱們能夠將符號化的模型的定義保存下來,在其餘語言API中加載。

x = mx.sym.var('data')
y = net(x)
print(y.tojson())

能夠看出,對於HybridBlock的模塊,既能夠把NDArray做爲輸入,也能夠把Symbol對象做爲輸入。當以Symbol做爲輸出時,它的結果就是一個Symbol對象。

相關文章
相關標籤/搜索