在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.Sequential
來定義一個模型,可是沒有仔細介紹它,它實際上是nn.Block
的一個簡單的形式。而nn.Block
是一個通常化的部件。整個神經網絡能夠是一個nn.Block
,單個層也是一個nn.Block
。咱們能夠(近似)無限地嵌套nn.Block
來構建新的nn.Block
。nn.Block
主要提供3個方向的功能:app
forward
如何執行因此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定義的模型的參數。
從上面咱們們在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)
咱們能夠weight
和bias
來訪問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
模塊提供了save
和load
接口來方便咱們對一個網絡的參數進行保存與加載。
filename = "mynet.params" net.save_params(filename) net2 = get_net() net2.load_params(filename, mx.cpu())
從上面咱們使用gluon來訓練mnist,能夠看出,咱們使用的是一種命令式的編程風格。大部分的深度學習框架只在命令式與符號式間二選一。那咱們能不能拿到兩種泛式所有的優勢呢,事實上這一點能夠作到。在MXNet的GluonAPI中,咱們可使用HybridBlock
或者HybridSequential
來構建網絡。默認他們跟Block
和Sequential
同樣是命令式的。但當咱們調用.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
對象。