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()