质量声明:原创文章,内容质量问题请评论吐槽。如对您产生干扰,可私信删除。
主要参考:李沐等:动手学深度学习-伯克利教材



摘要: MXNet 基础和入门,包括基本单元、模型相关、数据处理等


MXNet 基础

导入MXNet

from mxnet import nd										# Tensor模块
from mxnet import autograd   						# 自动求梯度
from mxnet import init										# 初始化
from mxnet.gluon import nn							# 神经网络基本结构
from mxnet.gluon import loss as gloss  	# 损失函数

张量模块 nd

注:NDArray基本与NunPy保持一致,这里只列举部分函数

# 一维向量
x = nd.array([0,1,2,3,4,5])
y = nd.arange(6)
z = nd.ones_like(x)

# 多维张量
A = nd.zeros((3,4,5))                                                # Channals x M x N
B = nd.ones((3,4,5), dtype="uint8")                 # 指定dtype,默认float32
C = nd.random.normal(0, 1, shape=(3,4,5))  # 指定mu, sigma, shape, dtype, ctx

# 属性
A.shape   # 张量/向量形状
A.size        # 元素总数
A.dtype    # 数据类型

# 重构
D = x.reshape(2,3)
D = nd.reshape(x, (2,3))

# 拼接
L = nd.concat(A, C, dim=0)   # 通道连接,层数增加
V = nd.concat(A, C, dim=1)   # 行连接,各层行数增加
H = nd.concat(A, C, dim=2)   # 列连接,各层列数增加

# 元素级运算
A + C;     A - C;     A * C;     A / C;      A ** 2
nd.exp(A);    nd.sum(B);   nd.mean(C);    nd.sqrt(B)

# 矩阵乘法
nd.dot(D, D.T)

# 张量乘法
x = nd.reshape(nd.arange(12), shape=(3,2,2))
y = nd.reshape(nd.arange(12), shape=(2,2,3))
result = nd.dot(x,y)
result.shape == (3, 2, 2, 3)

# L2范数
nd.norm(B)    
nd.sqrt(nd.sum(B**2)) 

# 上述运算结果均为<NDArray>类型,转为标量
nd.norm(B).asscalar() == 7

广播机制:(可能触发) 先将复制适当元素,使二者shape一致

A = nd.ones((3,2))
B = nd.ones((1,2))
A + B

节省内存开销:

# 计算过程中不开辟新的内存
before = id(A)
nd.elemwise_add(A, A, out=A)
id(A) == before 

# 简化写法也能节省内存开销
before = id(A)
A += A
id(A) == before 

转为numpy实例:

A.asnumpy()

自动求梯度 autograd

步骤:

  1. 变量赋初值
  2. 申请存储梯度的内存 param_in.attach_grad()
  3. 记录与梯度相关的函数计算(正向) with autograd_record()
  4. 自动求梯度(反向) param_out.backward()
# 对 theta 求梯度
x = nd.ones((3,3))
theta = nd.random.normal(shape=3)
theta.attach_grad()
with autograd.record():
    y = f(x, theta)
y.backward()
# 输出梯度
print(theta.grad)

其中f( )为前向运算函数,一般为损失函数loss ={ model(x, w, b), y }. 即使函数包含Python控制流(条件/循环),也有可能计算出变量的梯度.

基本单元介绍

基本网络结构/层

class 名称 参数
nn.Dense 稠密层/全连接层 units (int) ,activation (str)
nn.Dropout 神经元随机丢弃 rate(float)
nn.Conv2D 二维卷积层 channels (int) ,kernel_size (int or tuple/list of 2 int),stridespaddingactivation
nn.BatchNorm 批量归一化 /
nn.MaxPool2D 二维最大池化层 pool_sizestridespadding
nn.Flatten
nn.Embedding

自定义网络结构/层

  • 不含模型参数的自定义层:
class CenteredLayer(nn.Block):
    def __init__(self, **kwargs):
        super(CenteredLayer, self).__init__(**kwargs)

    def forward(self, x):
        return x - x.mean()
  • 含参的自定义层:
    由于Block类自带的的成员变量params是ParameterDict字典类型,则可以通过字典方法get( )来添加参数;调用是注意给定输入个数.
class MyDense(nn.Block):
    # units为该层的输出个数,in_units为该层的输入个数
    def __init__(self, units, in_units, **kwargs):
        super(MyDense, self).__init__(**kwargs)
        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)     # 整合relu激活函数

激活函数

  1. Sigmoid函数的输出映射在(0,1)之间,可以用来做二分类,单调连续,输出范围有限,优化稳定,可以用作输出层。
  2. 求导容易
  3. 由于其软饱和性,反向传播时,很容易就会出现梯度消失的情况,从而无法完成深层网络的训练,导致训练出现问题。
  4. 激活函数计算量大,反向传播求误差梯度时,求导涉及除法。
  5. 其输出并不是以0为中心的。这个特性会导致后面网络层的输入也不是零中心的,进而影响梯度下降的运作
  • tanh y = e x p ( x ) e x p ( x ) e x p ( x ) + e x p ( x ) y = \frac{exp(x) - exp(-x)}{exp(x) + exp(-x)} y=exp(x)+exp(x)exp(x)exp(x)
  1. 比Sigmoid函数收敛速度更快。
  2. 相比Sigmoid函数,其输出以0为中心,因此实际应用中 tanh 会比 sigmoid 更好,因为 tanh 的输出均值比 sigmoid 更接近 0,SGD会更接近 natural gradient(一种二次优化技术),从而降低所需的迭代次数
  3. 还是没有改变Sigmoid函数的最大问题——由于饱和性产生的梯度消失
  • softsign y = x 1 + a b s ( x ) y = \frac{x}{1 + abs(x)} y=1+abs(x)x

  • softrelu y = l o g ( 1 + e x p ( x ) ) y = log(1 + exp(x)) y=log(1+exp(x))

  • relu y = m a x ( x , 0 ) y = max(x, 0) y=max(x,0)

  1. 由于 x>0时导数为 1,所以ReLU 能够在x>0时保持梯度不衰减,从而缓解梯度消失问题。但随着训练的推进,部分输入会落入硬饱和区,导致对应权重无法更新。这种现象被称为“神经元死亡”,影响网络的收敛性

  2. 使用 ReLU 得到的 SGD 的收敛速度会比 sigmoid/tanh 快很多

  3. ReLU的输出具有偏移现象,即输出均值恒大于零,影响网络的收敛性

  • PReLU
  1. 为了避免梯度消失,提出了LReLU,其中 a i a_i ai较小且取固定值. 但在一些实验中,LReLU对准确率并没有太大的影响。很多时候,若想应用LReLU时,必须非常小心谨慎地重复训练,选取出合适的 a i a_i ai,LReLU才能表现得比ReLU好
  2. PReLU是ReLU 和 LReLU的改进版本,具有非饱和性,负半轴斜率 a j i a_{ji} aji可学习而非固定。PReLU收敛速度快、错误率低,可以用于反向传播的训练,可以与其他层同时优化
  • ELU
  1. ELU融合了sigmoid和ReLU,具有左侧软饱性
  2. 右侧线性部分使得ELU能够缓解梯度消失,而左侧软饱能够让ELU对输入变化或噪声更鲁棒。ELU减少了正常梯度与单位自然梯度之间的差距,它的输出均值接近于零,所以收敛速度更快

参数初始化方式

  • 官方文档: mxnet.initializer 定义
    示例:net.initialize( init.Normal( sigma = 0.01 ) )

  • Normal([sigma])

  • Xavier([rnd_type, factor_type, magnitude])

损失函数

  • 官方文档:mxnet.gluon : : loss 类定义
    示例:loss = gloss.L2loss()

  • L2Loss均方误差
    L = 1 2 <munder> i </munder> l a b e l i p r e d i 2 . L = \frac{1}{2} \sum_i \vert {label}_i - {pred}_i \vert^2. L=21ilabelipredi2.

  • L1Loss
    L = <munder> i </munder> l a b e l i p r e d i . L = \sum_i \vert {label}_i - {pred}_i \vert. L=ilabelipredi.

  • SigmoidBinaryCrossEntropyLoss 二分类交叉熵
    p r o b = 1 1 + exp ( p r e d ) L = <munder> i </munder> l a b e l i log ( p r o b i ) p o s _ w e i g h t + ( 1 l a b e l i ) log ( 1 p r o b i ) prob = \frac{1}{1 + \exp(-{pred})} \\ L = - \sum_i {label}_i * \log({prob}_i) * pos\_weight + (1 - {label}_i) * \log(1 - {prob}_i) prob=1+exp(pred)1L=ilabelilog(probi)pos_weight+(1labeli)log(1probi)

  • SoftmaxCrossEntropyLoss softmax交叉熵
    p = s o f t m a x ( p r e d ) L = <munder> i </munder> <munder> j </munder> l a b e l j log p i j p = softmax({pred}) \\ L = -\sum_i \sum_j {label}_j \log p_{ij} p=softmax(pred)L=ijlabeljlogpij

  • LogisticLoss
    L = <munder> i </munder> log ( 1 + exp ( p r e d i l a b e l i ) ) L = \sum_i \log(1 + \exp(- {pred}_i \cdot {label}_i)) L=ilog(1+exp(predilabeli))

优化算法(小批量)

  • 参考博文:优化方法optimizer总结
    官方文档:mxnet.Optimizers 定义
    示例:gluon.Trainer(net.collect_params(), 'adam', {'learning_rate': 0.1, 'wd': 5})

  • 随机梯度下降 SGD:"sgd"

  • adagrad

  • adam:"adam"


模型构建、训练和预测

模型构建

net = nn.Sequential()
net.add(nn.Dense(360, activation='relu'),
        		  nn.Dropout(0.2),
                  nn.Dense(64, activation='relu'),
                  nn.Dropout(0.5),
                  nn.Dense(1))
net.initialize(init.Normal(sigma=0.01))

模型训练

  1. 构建小批量数据数据生成器 train_iter = gdata.DataLoader()
  2. 构建模型训练器 trainer = gluon.Trainer(),接口文档 gluon.Trainer
  3. 循环n次epoch训练. 在每个epoch中,按小批量训练,所有batch训练结束则输出本次epoch的损失
net=get_net()
loss=gloss.L2Loss()
num_epochs=20; batch_size=64;  learning_rate=0.01; weight_decay=0
train_iter = gdata.DataLoader(gdata.ArrayDataset(train_features, train_labels), batch_size, shuffle=True)
trainer = gluon.Trainer(net.collect_params(), 'adam', {'learning_rate': learning_rate, 'wd': weight_decay})
for epoch in range(num_epochs):
    for X, y in train_iter:
        with autograd.record():
            l = loss(net(X), y)
        l.backward()
        trainer.step(batch_size)
    train_ls.append(log_rmse(net, train_features, train_labels))

模型预测

  • 模型输出:
out = net(X)  
print(out.shape, out.dtype, out[0])

模型参数存储和读取

  • 参数保存:
filename = 'net.params'
net.save_parameters(filename)
  • 读取参数:
net = get_net()
net.load_parameters(filename)

可视化

losses, train_acc, test_acc = [],[],[]
# 可视化训练过程
idx = range(1,epochs+1)
plt.figure(figsize=(12, 4))
plt.subplot(121)
plt.xlabel("epoch")
plt.ylabel("loss")
plt.plot(idx, losses, 'o', linestyle='-')
plt.xticks(range(min(idx), max(idx)+1, 1))
plt.grid()
plt.subplot(122)
plt.xlabel("epoch")
plt.ylabel("accuracy")
plt.plot(idx, train_acc, 'o', linestyle='-', color="r", label="train accuracy")
plt.plot(idx, test_acc, 'o', linestyle='-', color="b", label="test accuracy")
plt.legend(loc="best")
plt.xticks(range(min(idx), max(idx)+1, 1))
plt.yticks(np.arange(0., 1.1, 0.1))
plt.grid()
plt.ylim([0,1.1])
plt.show()