MXNet是基礎,Gluon是封裝,兩者猶如TensorFlow和Keras,不過得益於動態圖機制,兩者交互比TensorFlow和Keras要方便得多,其基礎操作和pytorch極為相似,但是方便不少,有pytorch基礎入門會很簡單。注意和TensorFlow不同,MXNet的圖片維度是 batch x channel x height x width 。
MXNet的API主要分為3層,最基礎的時mxnet.ndarray(NDArray API),它以近似numpy數組的形式記錄了諸多基礎的函數式的操作,支持自動求導和GPU加速應該是它針對numpy的最主要改進;然后是mxnet.Symbol(Symbol API)模塊,它是MXNet符號式編程的基石,與mxnet.model(Module API)模塊相互搭配可以靈活、快速地構建網絡,這一層比較類似TensorFlow;最后就是Gloun(Gluon API)模塊,它更近似於Keras,高度的封裝了代碼。
市面上的大部分教程(包含框架作者的"動手學習深度學習")都以ndarray的簡要上手為引入,以Gloun為主要學習內容,不過,如果經常瀏覽MXNet開源項目的話,會發現,實際上Symbol的使用才是主流。我對於Symbol的了解也很膚淺,有希望學習MXNet的新人如果看到這段引文希望能有針對的避開我掉進去的坑。
庫導入寫法,
from mxnet import ndarray as nd from mxnet import autograd from mxnet import gluon import mxnet as mx
實際上第一個包 from mxnet import nd 也行,簡化工作很厲害……
MXNet.ndarray
mxnet.ndarray是整個科學計算系統的基礎,整體API和numpy的nparray一致,這一點類似於pytorch,不過不同於pytorch內置變量、張量等不同數據類型,mxnet簡化了只有ndarray一種,通過mxnet.autograd可以直接實現求導,十分便捷.
自動求導
x = nd.arange(4).reshape((4, 1))
# 標記需要自動求導的量
x.attach_grad()
# 有自動求導就需要記錄計算圖
with autograd.record():
y = 2 * nd.dot(x.T, x)
# 反向傳播輸出
y.backward()
# 獲取梯度
print('x.grad: ', x.grad)
設備
array.copyto() # 傳入設備則復制進設備,傳入array則覆蓋(也會進入傳入的設備)
array.as_in_context() # 修改設備
nd轉化為數字
nd.asscalar()
nd與np數組互化
y = nd.array(x) # NumPy轉換成NDArray。
z = y.asnumpy() # NDArray轉換成NumPy。
節約內存的加法
nd.elemwise_add(x, y, out=z)
持久化
nd.save(file, [arr1, arr2, ……])
nd.load(file)
層實現
拉伸
nd.flatten(array)
relu激活
內置,
nd.nn.relu()
手動實現,
def relu(X):
return nd.maximum(X, 0)
卷積層
# 輸入輸出數據格式是 batch x channel x height x width,這里batch和channel都是1 # 權重格式是 output_channels x in_channels x height x width,這里input_filter和output_filter都是1。 w = nd.arange(4).reshape((1,1,2,2)) b = nd.array([1]) data = nd.arange(9).reshape((1,1,3,3)) out = nd.Convolution(data, w, b, kernel=w.shape[2:], num_filter=w.shape[1], stride=(2,2), pad=(1,1))
池化層
data = nd.arange(18).reshape((1,2,3,3)) max_pool = nd.Pooling(data=data, pool_type="max", kernel=(2,2)) avg_pool = nd.Pooling(data=data, pool_type="avg", kernel=(2,2))
全連接層
# 變量生成
w = nd.random.normal(scale=1, shape=(num_inputs, 1))
b = nd.zeros(shape=(1,))
params = [w, b]
# 變量掛載梯度
for param in params:
param.attach_grad()
# 實現全連接
def net(X, w, b):
return nd.dot(X, w) + b
批量歸一化層
在測試時我們還是需要繼續使用批量歸一化的,只是需要做些改動。在測試時,我們需要把原先訓練時用到的批量均值和方差替換成整個訓練數據的均值和方差。但 是當訓練數據極大時,這個計算開銷很大。因此,我們用移動平均的方法來近似計算(參見實現中的moving_mean和moving_variance)。
def batch_norm(X, gamma, beta, is_training, moving_mean, moving_variance,
eps = 1e-5, moving_momentum = 0.9):
assert len(X.shape) in (2, 4)
# 全連接: batch_size x feature
if len(X.shape) == 2:
# 每個輸入維度在樣本上的平均和方差
mean = X.mean(axis=0)
variance = ((X - mean)**2).mean(axis=0)
# 2D卷積: batch_size x channel x height x width
else:
# 對每個通道算均值和方差,需要保持4D形狀使得可以正確的廣播
mean = X.mean(axis=(0,2,3), keepdims=True)
variance = ((X - mean)**2).mean(axis=(0,2,3), keepdims=True)
# 變形使得可以正確的廣播
moving_mean = moving_mean.reshape(mean.shape)
moving_variance = moving_variance.reshape(mean.shape)
# 均一化
if is_training:
X_hat = (X - mean) / nd.sqrt(variance + eps)
#!!! 更新全局的均值和方差
moving_mean[:] = moving_momentum * moving_mean + (
1.0 - moving_momentum) * mean
moving_variance[:] = moving_momentum * moving_variance + (
1.0 - moving_momentum) * variance
else:
#!!! 測試階段使用全局的均值和方差
X_hat = (X - moving_mean) / nd.sqrt(moving_variance + eps)
# 拉升和偏移
return gamma.reshape(mean.shape) * X_hat + beta.reshape(mean.shape)
Droupout
def dropout(X, drop_probability):
keep_probability = 1 - drop_probability
assert 0 <= keep_probability <= 1
# 這種情況下把全部元素都丟棄。
if keep_probability == 0:
return X.zeros_like()
# 隨機選擇一部分該層的輸出作為丟棄元素。
mask = nd.random.uniform(
0, 1.0, X.shape, ctx=X.context) < keep_probability
# 保證 E[dropout(X)] == X
scale = 1 / keep_probability
return mask * X * scale
SGD實現
def sgd(params, lr, batch_size):
for param in params:
param[:] = param - lr * param.grad / batch_size
Gluon
內存數據集加載
import mxnet as mx
from mxnet import autograd, nd
import numpy as np
num_inputs = 2
num_examples = 1000
true_w = [2, -3.4]
true_b = 4.2
features = nd.random.normal(scale=1, shape=(num_examples, num_inputs))
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b
labels += nd.random.normal(scale=0.01, shape=labels.shape)
from mxnet.gluon import data as gdata
batch_size = 10
dataset = gdata.ArrayDataset(features, labels)
data_iter = gdata.DataLoader(dataset, batch_size, shuffle=True)
for X, y in data_iter:
print(X, y)
break
[[-1.74047375 0.26071024] [ 0.65584248 -0.50490594] [-0.97745866 -0.01658815] [-0.55589193 0.30666101] [-0.61393601 -2.62473822] [ 0.82654613 -0.00791582] [ 0.29560572 -1.21692061] [-0.35985938 -1.37184834] [-1.69631028 -1.74014604] [ 1.31199837 -1.96280086]] <NDArray 10x2 @cpu(0)> [ -0.14842382 7.22247267 2.30917668 2.0601418 11.89551163 5.87866735 8.94194221 8.15139961 6.72600317 13.50252151] <NDArray 10 @cpu(0)>
模型定義
- 序列模型生成
- 層填充
- 初始化模型參數
net = gluon.nn.Sequential()
with net.name_scope():
net.add(gluon.nn.Dense(1))
net.collect_params().initialize(mx.init.Normal(sigma=1)) # 模型參數初始化選擇normal分布
優化器:gluon.Trainer
wd參數為模型添加了L2正則化,機制為:w = w - lr*grad - wd*w
trainer = gluon.Trainer(net.collect_params(), 'sgd', {
'learning_rate': learning_rate, 'wd': weight_decay})
trainer.step(batch_size)需要運行在每一次反向傳播之后,會更新參數,一次模擬的訓練過程如下,
for e in range(epochs):
for data, label in data_iter_train:
with autograd.record():
output = net(data)
loss = square_loss(output, label)
loss.backward()
trainer.step(batch_size)
train_loss.append(test(net, X_train, y_train))
test_loss.append(test(net, X_test, y_test))
設備
net.collect_params().reset_ctx()可以重置model設備
持久化
net.save_params(file)
net.load_params(file, ctx) # 可以指定加載設備
層函數API:gluon.nn
拉伸
nn.Flatten()
卷積層
nn.Conv2D(1, kernel_size=(1, 2)) # 輸出通道1,卷積核橫2縱
nn.Conv2D(channels=6, kernel_size=5, activation='sigmoid')
池化層
最大池化層
nn.MaxPool2D(pool_size=2, strides=2)
全連接層
nn.Dense(256, activation="relu") # 參數表示輸出節點數
激活函數
nn.Activation("relu")
批量歸一化層
nn.BatchNorm(axis=1) # 卷積層后的BN層對每個通道求一個平均
損失函數class API:gluon.loss
交叉熵
loss = gluon.loss.SoftmaxCrossEntropyLoss()
