MXNet的新接口Gluon


為什么要開發Gluon的接口

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

更加簡潔的接口

我們先來看一下用Gluon的接口,如果創建並組訓練一個神經網絡的,我們以mnist數據集為例:

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

數據的讀取

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

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個方向的功能:

  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了,來實現不同的深度學習應用可能遇到的一些新的層。

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

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對象。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM