[深度学习框架]Gluon

19年4月3日的最新正式版:MXNet 1.4

《动手学深度学习》

深度学习简介

深度学习是具有多级表示的表征学习方法
在机器学习的众多研究方向中,表征学习关注如何自动找出表示数据的合适方式,以便更好地将输入变换为正确的输出

成功的机器学习有四个要素:

  • 数据
  • 转换数据的模型
  • 衡量模型好坏的损失函数
  • 调整模型权重来最小化损失函数的优化算法

预备知识

NDArray与numpy

GPU支持、自动求导、异步支持

1
2
3
4
5
6
7
from mxnet import ndarray as nd
import numpy as np

x = np.ones((2,3))

y = nd.array(x) # numpy -> mxnet
z = y.asnumpy() # mxnet -> numpy

NDArray的自动求导

1
2
3
4
5
6
7
8
9
10
11
12
import mxnet.ndarray as nd
import mxnet.autograd as ag

x = nd.array([[1, 2], [3, 4]])
x.attach_grad() # 申请存储梯度所需要的内存
with ag.record(): # 要求MXNet记录与求梯度有关的计算
y = x * 2
z = y * x
z.backward() # 自动求梯度

print('x.grad: ', x.grad)
x.grad == 4*x

深度学习基础

线性回归-ndarray版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
from mxnet import autograd, nd
import random

# 1. 生成数据集
num_inputs = 2
num_examples = 1000

true_w = [2, -3.4]
true_b = 4.2

# 公式: y=X⋅w+b+ϵ
# features是训练数据特征,labels是标签
features = nd.random.normal(scale=1, shape=(num_examples, num_inputs)) # (1000, 2)
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b # (1000,)
labels += nd.random.normal(scale=0.01, shape=labels.shape)

# 2. 读取数据
batch_size = 10

# 每次返回batch_size(批量大小)个随机样本的特征和标签
def data_iter(batch_size, features, labels):
num_examples = len(features)
indices = list(range(num_examples)) # 产生一个随机索引
random.shuffle(indices) # 样本的读取顺序是随机的
for i in range(0, num_examples, batch_size):
j = nd.array(indices[i: min(i + batch_size, num_examples)])
yield features.take(j), labels.take(j) # take函数根据索引返回对应元素, 通过yield构造迭代器

# 3. 定义模型
def linreg(X, w, b):
return nd.dot(X, w) + b

# 4. 初始化模型参数
w = nd.random_normal(shape=(num_inputs, 1)) # (2, 1)
b = nd.zeros((1,)) # (1,)

# 创建参数的梯度
w.attach_grad()
b.attach_grad()

# 5. 定义损失函数
def squared_loss(yhat, y):
# 注意这里把y变形成yhat的形状来避免矩阵形状的自动转换
return (yhat - y.reshape(yhat.shape)) ** 2

# 6. 定义优化算法
def sgd(params, lr, batch_size):
for param in params:
param[:] = param - lr * param.grad / batch_size # 自动求梯度模块计算得来的梯度是一个批量样本的梯度和, 将它除以批量大小来得到平均值

# 7. 训练模型
num_epochs = 3
lr = 0.03
net = linreg
loss = squared_loss

for epoch in range(num_epochs): # 训练模型一共需要num_epochs个迭代周期
# 在每一个迭代周期中,会使用训练数据集中所有样本一次(假设样本数能够被批量大小整除)。X
# 和y分别是小批量样本的特征和标签
for X, y in data_iter(batch_size, features, labels):
with autograd.record():
l = loss(net(X, w, b), y) # l是有关小批量X和y的损失
l.backward() # 小批量的损失对模型参数求梯度
sgd([w, b], lr, batch_size) # 使用小批量随机梯度下降迭代模型参数
train_l = loss(net(features, w, b), labels)
print('epoch %d, loss %f' % (epoch + 1, train_l.mean().asnumpy()))

# 8. 比较结果(真实的模型参数、学到的模型参数)
true_w, w
true_b, b

线性回归-gluon版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from mxnet import autograd, nd
from mxnet.gluon import data as gdata
from mxnet.gluon import nn
from mxnet import init
from mxnet.gluon import loss as gloss
from mxnet import gluon

# 1. 生成数据集
num_inputs = 2
num_examples = 1000

true_w = [2, -3.4]
true_b = 4.2

# 公式: y=X⋅w+b+ϵ
# features是训练数据特征,labels是标签
features = nd.random.normal(scale=1, shape=(num_examples, num_inputs)) # (1000, 2)
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b # (1000,)
labels += nd.random.normal(scale=0.01, shape=labels.shape)

# 2. 读取数据
batch_size = 10

# 将训练数据的特征和标签组合
dataset = gdata.ArrayDataset(features, labels)

# 随机读取小批量
data_iter = gdata.DataLoader(dataset, batch_size, shuffle=True)

# 3. 定义模型
net = nn.Sequential()
net.add(nn.Dense(1)) # 全连接层是一个Dense实例, 定义该层输出个数为1

# 4. 初始化模型参数
net.initialize(init.Normal(sigma=0.01))

# 5. 定义损失函数
loss = gloss.L2Loss() # 平方损失又称L2范数损失

# 6. 定义优化算法
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.03})

# 7. 训练模型
num_epochs = 3

for epoch in range(num_epochs):
for X, y in data_iter:
with autograd.record():
l = loss(net(X), y)
l.backward()
trainer.step(batch_size)
l = loss(net(features), labels)
print('epoch %d, loss: %f' % (epoch + 1, l.mean().asnumpy()))

# 8. 比较结果(真实的模型参数、学到的模型参数)
dense = net[0]
true_w, dense.weight.data()
true_b, dense.bias.data()

softmax回归

分开定义softmax运算和交叉熵损失函数可能会造成数值不稳定
jupyter中,scratch版是分开定义的,gluon版用的是gluon提供的

1
2
# Gluon提供了一个包括softmax运算和交叉熵损失计算的函数
loss = gloss.SoftmaxCrossEntropyLoss()

多层感知机(MLP)

非线性激活函数relu

1
2
3
4
5
6
net = gluon.nn.Sequential()
with net.name_scope():
net.add(gluon.nn.Flatten())
net.add(gluon.nn.Dense(256, activation="relu"))
net.add(gluon.nn.Dense(256, activation="relu"))
net.add(gluon.nn.Dense(10))

欠拟合和过拟合

训练误差:机器学习模型在训练数据集上表现出的误差
泛化误差:在任意一个测试数据样本上表现出的误差的期望值

统计学习理论的一个假设是:独立同分布假设(训练数据集和测试数据集里的每一个数据样本都是从同一个概率分布中相互独立地生成出的)

重要结论:训练误差的降低不一定意味着泛化误差的降低。机器学习既需要降低训练误差,又需要降低泛化误差

欠拟合:机器学习模型无法得到较低训练误差
过拟合:机器学习模型的训练误差远小于其在测试数据集上的误差

防止过拟合:

  1. 增大训练数据量
  2. 使用合适的模型
  3. 正则化:L2范数正则化
  4. 丢弃法(dropout)

直观上,L2范数正则化试图惩罚较大绝对值的参数值

在训练神经网络模型时一般随机采样一个批量的训练数据。丢弃法实质上是对每一个这样的数据集分别训练一个原神经网络子集的分类器。与一般的集成学习不同,这里每个原神经网络子集的分类器用的是同一套参数。因此丢弃法只是在模拟集成学习
一般情况下,推荐把更靠近输入层的元素丢弃概率设的更小一点。

1
2
3
# 正则化(weight decay参数)
trainer = gluon.Trainer(net.collect_params(), 'sgd', {
'learning_rate': learning_rate, 'wd': weight_decay})
1
2
3
4
5
6
7
8
9
10
11
12
# dropout
with net.name_scope():
net.add(nn.Flatten())
# 第一层全连接。
net.add(nn.Dense(256, activation="relu"))
# 在第一层全连接后添加丢弃层。
net.add(nn.Dropout(drop_prob1))
# 第二层全连接。
net.add(nn.Dense(256, activation="relu"))
# 在第二层全连接后添加丢弃层。
net.add(nn.Dropout(drop_prob2))
net.add(nn.Dense(10)

kaggle-gluon-kfold(没跑???)

使用GPU

MXNet使用Context来指定使用哪个设备来存储和计算
默认会将数据开在主内存,然后利用CPU来计算
mx.cpu():表示所有的物理CPU和内存,意味着计算上会尽量使用多有的CPU核
mx.gpu():只代表一块显卡和其对应的显卡内存,用mx.gpu(i)来表示第i块GPU(i从0开始)

注意:所有计算要求输入数据在同一个设备上。不一致的时候系统不进行自动复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import mxnet as mx
from mxnet import nd

# 在创建数组时,指定创建在哪个设备上
a = nd.zeros((1,2), ctx=mx.cpu())
b = nd.zeros((2,3), ctx=mx.gpu())

# 通过copyto和as_in_context来在设备直接传输数据
# 如果源和目标的context一致,as_in_context不复制,而copyto总是会新建内存
a2 = a.copyto(mx.gpu())
a3 = a.as_in_context(mx.gpu())

# 不在同一个设备上的数据运算会报错
nd.dot(a, b) # 报错
nd.dot(a, b.as_in_context(mx.cpu())) # 成功
nd.dot(a.as_in_context(mx.gpu()), b) # 成功

# 默认会复制回CPU的操作
print(b)
print(b.asnumpy())
print(b.sum().asscalar())

兼容CPU、GPU的ctx

1
2
3
4
5
6
7
8
def try_gpu():
"""如果GPU存在, 返回mx.gpu(0); 否则返回 mx.cpu()"""
try:
ctx = mx.gpu()
_ = nd.array([0], ctx=ctx)
except:
ctx = mx.cpu()
return ctx

需要指定ctx的:

  • 数据
  • 模型、权重(网络初始化)

深度学习计算

创建神经网络

nn.Sequential
nn.Block

nn.Block主要提供的功能:

  1. 存储参数
  2. 描述forward如何执行
  3. 自动求导

nn.Block和nn.Sequential的嵌套使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from mxnet import nd
from mxnet.gluon import nn

# nn.Sequential实现版
net = nn.Sequential()
with net.name_scope():
net.add(nn.Dense(256, activation="relu"))
net.add(nn.Dense(10))

print(net)

# nn.Block实现版
class MLP(nn.Block):
def __init__(self, **kwargs):
super(MLP, self).__init__(**kwargs)
with self.name_scope():
self.dense0 = nn.Dense(256)
self.dense1 = nn.Dense(10)

def forward(self, x):
return self.dense1(nd.relu(self.dense0(x)))

net2 = MLP()
print(net2)

# 前向推断
x = nd.random.uniform(shape=(4,20))
net.initialize()
net2.initialize()
net(x), net2(x)

解释:
nn.Block的使用是通过创建一个它子类的类,其中至少包含了两个函数。

  • __init__:创建参数。上面例子使用了包含了参数的dense层。
  • forward():定义网络的计算。系统会使用autogradforward()自动生成对应的backward()函数。
  • super(MLP, self).__init__(**kwargs):这句话调用nn.Block__init__函数,它提供了prefix(指定名字)和params(指定模型参数)两个参数。
  • self.name_scope():调用nn.Block提供的name_scope()函数。nn.Dense的定义放在这个scope里面。它的作用是给里面的所有层和参数的名字加上前缀(prefix)使得他们在系统里面独一无二。默认自动会自动生成前缀,也可以在创建的时候手动指定。推荐在构建网络时,每个层至少在一个name_scope()里。

初始化模型参数

搭建好模型后,需要先net.initialize(),再跑forward

访问模型参数

1
2
3
4
5
6
7
8
9
10
11
12
13
# 对于Sequential
net[0].name
net[0].weight
net[0].weight.data()
net[0].weight.grad()
net[0].bias
net[0].bias.data()
net[0].bias.grad()

# 对于Sequential和Block
params = net.collect_params()
params['sequential0_dense0_weight'].data()
params.get('dense0_weight').data() # 不需要填写名字的前缀

使用不同的初始函数来初始化

1
2
3
from mxnet import init
params.initialize(init=init.Normal(sigma=0.02), force_reinit=True)
params['sequential0_dense0_weight'].initialize(init=init.One(), force_reinit=True)

小结:此时的params.initialize()与net.initialize()效果是等同的

延后的初始化

Gluon的便利:模型定义的时候不需要指定输入的大小,在之后做forward的时候会自动推测参数的大小

共享模型参数

想在层之间共享同一份参数,可以通过Block的params输出参数来手动指定参数,而不是让系统自动生成。

1
2
3
4
5
6
net = nn.Sequential()
with net.name_scope():
net.add(nn.Dense(4, activation="relu"))
net.add(nn.Dense(4, activation="relu"))
net.add(nn.Dense(4, activation="relu", params=net[1].params)) # 使用上一层的参数
net.add(nn.Dense(2))

自定义初始化方法

通过实现一个Initializer类的子类,重载_init_weight来实现不同的初始化方法。
注意:Gluon里面bias都是默认初始化成0

☆可以通过Parameter.set_data来直接改写权重。
注意:由于有延后初始化,所以通常可以通过调用一次net(x)来确定权重的形状先。

1
2
3
4
5
6
7
8
9
10
11
12
13
from mxnet import init

class MyInit(init.Initializer):
def _init_weight(self, name, data):
print('Init', name, data.shape)
data[:] = nd.random.uniform(low=-10, high=10, shape=data.shape)
data *= data.abs() >= 5 # 令权重有一半概率初始化为0,有另一半概率初始化为 [−10,−5][−10,−5] 和 [5,10][5,10] 两个区间里均匀分布的随机数

net.initialize(MyInit(), force_reinit=True)
net[0].weight.data()

net[0].weight.set_data(nd.ones(net[0].weight.shape))
net[0].weight.data()

序列化——读写模型

读写NDArrays

nd.save()
nd.load()
可以用list、dict包裹NDArrays,然后使用save()保存

1
2
3
4
5
6
from mxnet import nd

x = nd.ones(3)
nd.save('x.params', x) # 存储

x2 = nd.load('x.params') # 读取

读写Gluon模型的参数

net.save_parameters()
net2.load_parameters()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from mxnet import nd
from mxnet.gluon import nn

class MLP(nn.Block):
def __init__(self, **kwargs):
super(MLP, self).__init__(**kwargs)
self.hidden = nn.Dense(256, activation='relu')
self.output = nn.Dense(10)

def forward(self, x):
return self.output(self.hidden(x))

net = MLP()
net.initialize()
X = nd.random.uniform(shape=(2, 20))
Y = net(X) # 需要先运行一次前向计算才能实际初始化模型参数

net.save_parameters('mlp.params') # 存储

net2 = MLP()
net2.load_parameters('mlp.params') # 读取

设计自定义层

使用底层的NDArray接口来实现一个Gluon的层。
自定义层需要继承nn.Block,跟前面介绍的如何使用nn.Block没什么区别。

不含模型参数的自定义层

1
2
3
4
5
6
7
8
9
10
11
12
from mxnet import gluon, nd
from mxnet.gluon import nn

class CenteredLayer(nn.Block):
def __init__(self, **kwargs):
super(CenteredLayer, self).__init__(**kwargs)

def forward(self, x):
return x - x.mean()

layer = CenteredLayer()
layer(nd.array([1, 2, 3, 4, 5]))

含模型参数的自定义层

在自定义含模型参数的层时,可以利用Block类自带的ParameterDict类型的成员变量params

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)

dense = MyDense(units=3, in_units=5)
dense.initialize()
dense(nd.random.uniform(shape=(2, 5)))

dense.params # 类似字典类型
dense.params.keys()
dense.params['mydense0_weight'].data()
dense.params['mydense0_weight'].set_data( nd.ones(dense.params['mydense0_weight'].data().shape) ) # 重点

卷积神经网络

本章概览

两种层:

  • 卷积层
  • 池化层

四个参数

  • 填充
  • 步幅
  • 输入通道
  • 输出通道

具有代表性的深度卷积神经网络:

  1. LeNet
  2. AlexNet
  3. VGG
  4. NiN
  5. GoogLeNet
  6. ResNet
  7. DenseNet

训练和设计深度模型的两个思路:

  1. 批量归一化(batch normalization)
  2. 残差网络

CNN基础

卷积(convolution)层:计算输入和核的互相关性
互相关(cross-correlation)运算:按元素相乘并求和
二维卷积层:将输入和卷积核做互相关运算,并加上一个标量偏差来得到输出

特征图(feature map):二维卷积层输出的二维数组可以看作是输入在空间维度(宽和高)上某一级的表征
感受野(receptive field):影响元素x的前向计算的所有可能输入区域(可能大于输入的实际尺寸)

卷积核的模型参数:卷积核、标量偏差
卷积层的超参数:填充、步幅
在默认情况下,填充为0,步幅为1

池化(pooling)层:为了缓解卷积层对位置的过度敏感性
二维池化层:直接计算池化窗口内元素的最大值或者平均值

卷积模块:“卷积层-激活层-池化层”

卷积层

默认

输入形状:
卷积核窗口形状:
输出形状将:

填充(padding)

在高的两侧一共填充行,在宽的两侧一共填充
输出形状:
也就是说,输出的高和宽会分别增加

常常设置来使输入和输出具有相同的高和宽

步幅(stride)

当高上步幅为$s_h$,宽上步幅为$s_w$时
输出形状:

如果设置$p_h=k_h-1$和$p_w=k_w-1$
输出形状:$\lfloor(n_h+s_h-1)/s_h\rfloor × \lfloor(n_w+s_w-1)/s_w\rfloor$

更进一步,如果输入的高和宽能分别被高和宽上的步幅整除
输出形状:$(n_h/s_h) × (n_w/s_w)$。

多输入通道

当输入数据含多个通道时,需要构造一个输入通道数与输入数据的通道数相同的卷积核,从而能够与含多通道的输入数据做互相关运算。

假设输入数据的通道数为$c_i$,那么卷积核的输入通道数同样为$c_i$。
设卷积核窗口形状为$k_h × k_w$。
当$c_i = 1$时,卷积核只包含一个形状为$k_h × k_w$的二维数组。
当$c_i > 1$时,将会为每个输入通道各分配一个形状为$k_h × k_w$的核数组。把这$c_i$个数组在输入通道维上连结,即得到一个形状为$c_i × k_h × k_w$的卷积核。由于输入和卷积核各有$c_i$个通道,可以在各个通道上对输入的二维数组和卷积核的二维核数组做互相关运算,再将这$c_i$个互相关运算的二维输出按通道相加,得到一个二维数组。这就是含多个通道的输入数据与多输入通道的卷积核做二维互相关运算的输出。

多输出通道

当输入通道有多个时,因为对各个通道的结果做了累加,所以不论输入通道数是多少,输出通道数总是为1。
设卷积核输入通道数$c_i$、输出通道数$c_o$,高$k_h$、宽$k_w$。
如果希望得到含多个通道的输出,可以为每个输出通道分别创建形状为$c_i × k_h × k_w$的核数组。将它们在输出通道维上连结,卷积核的形状即$c_o × c_i × k_h × k_w$。在做互相关运算时,每个输出通道上的结果由卷积核在该输出通道上的核数组与整个输入数组计算而来。

1×1卷积层

讨论卷积窗口形状为$1 × 1$($k_h=k_w=1$)的多通道卷积层。
通常称之为$1 × 1$卷积层,并将其中的卷积运算称为$1 × 1$卷积。
因为使用了最小窗口,$1 × 1$卷积失去了卷积层可以识别高和宽维度上相邻元素构成的模式的功能。实际上,$1 × 1$卷积的主要计算发生在通道维上
上图展示了使用输入通道数为3、输出通道数为2的$1 × 1$卷积核的互相关计算。值得注意的是,输入和输出具有相同的高和宽。输出中的每个元素来自输入中在高和宽上相同位置的元素在不同通道之间的按权重累加。

假设将通道维当作特征维,将高和宽维度上的元素当成数据样本,那么$1 × 1$卷积层的作用与全连接层等价
$1 × 1$卷积层常被当作保持高和宽维度形状不变的全连接层使用。于是,可以通过调整网络层之间的通道数来控制模型复杂度

吴恩达讲解(第四门课 卷积神经网络-2.5 网络中的网络以及1×1卷积)
如果是一张6×6×32的图片,那么使用1×1过滤器进行卷积效果更好。具体来说,1×1卷积所实现的功能是遍历这36个单元格,计算左图中32个数字和过滤器中32个数字的元素积之和,然后应用ReLU非线性函数

我们以其中一个单元为例,它是这个输入层上的某个切片,用这36个数字乘以这个输入层上1×1切片,得到一个实数,像这样把它画在输出中。
这个1×1×32过滤器中的32个数字可以这样理解,一个神经元的输入是32个数字(输入图片中左下角位置32个通道中的数字),即相同高度和宽度上某一切片上的32个数字,这32个数字具有不同通道,乘以32个权重(将过滤器中的32个数理解为权重),然后应用ReLU非线性函数,在这里输出相应的结果。
一般来说,如果过滤器不止一个,而是多个,就好像有多个输入单元,其输入内容为一个切片上所有数字,输出结果是6×6过滤器数量
所以1×1卷积可以从根本上理解为对这32个不同的位置都应用一个全连接层,全连接层的作用是输入32个数字(过滤器数量标记为#filters,在这36个单元上重复此过程),输出结果是 6×6×#filters(过滤器数量),以便在输入层上实施一个非平凡计算。

小结:使用池化层压缩宽度和高度,通过1×1卷积层压缩维度(通道数)

池化层

默认

填充、步幅

同卷积层一样,池化层也可以在输入的高和宽两侧的填充并调整窗口的移动步幅来改变输出形状。

多通道

处理多通道输入数据时,池化层对每个输入通道分别池化,而不是像卷积层那样将各通道的输入按通道相加。
这意味着池化层的输出通道数与输入通道数相等。

代码片段:卷积

1
from mxnet import nd

默认

1
2
3
4
5
6
7
8
9
# 输入输出数据格式是 batch x channel x height x width,这里batch和channel都是1
# 权重格式是 output_channels x in_channels x height x width,这里input_filter和output_filter都是1。
# 默认(如图"输入3_卷积2_输出2")
w = nd.arange(4).reshape((1,1,2,2))
b = nd.array([0])
data = nd.arange(9).reshape((1,1,3,3))
out = nd.Convolution(data, w, b, kernel=w.shape[2:], num_filter=w.shape[1]) # (1,1,2,2)

print('input:', data, '\n\nweight:', w, '\n\nbias:', b, '\n\noutput:', out)

填充

1
2
3
4
5
# 填充(如图"输入3_卷积2_填充2_输出4")
out = nd.Convolution(data, w, b, kernel=w.shape[2:], num_filter=w.shape[1],
pad=(1,1)) # (1,1,4,4)

print('input:', data, '\n\nweight:', w, '\n\nbias:', b, '\n\noutput:', out)

步幅

1
2
3
4
5
# 步幅(如图"输入3_卷积2_填充2_步长(3,2)_输出2")
out = nd.Convolution(data, w, b, kernel=w.shape[2:], num_filter=w.shape[1],
stride=(3,2), pad=(1,1)) # (宽,高) # (1,1,2,2)

print('input:', data, '\n\nweight:', w, '\n\nbias:', b, '\n\noutput:', out)

多输入通道

1
2
3
4
5
6
7
8
9
# 多输入通道(卷积核的第2个维度)
# (如图"输入通道为2")
w = nd.arange(8).reshape((1,2,2,2))
w[:,1,:,:] = w[:,0,:,:]+1
data = nd.arange(18).reshape((1,2,3,3))
data[:,1,:,:] = data[:,0,:,:]+1
out = nd.Convolution(data, w, b, kernel=w.shape[2:], num_filter=w.shape[0]) # (1,1,2,2)

print('input:', data, '\n\nweight:', w, '\n\nbias:', b, '\n\noutput:', out)

多输出通道

1
2
3
4
5
6
7
8
9
10
11
12
# 多输出通道(卷积核的第1个维度),注意修改b
# (如图"输入通道为3_输出通道为2_(1×1)卷积层")
w = nd.arange(24).reshape((3,2,2,2))
w[:,1,:,:] = w[:,0,:,:]+1
w[1,:,:,:] = w[0,:,:,:]+1
w[2,:,:,:] = w[0,:,:,:]+2
b = nd.array([0,0,0])
data = nd.arange(18).reshape((1,2,3,3))
data[:,1,:,:] = data[:,0,:,:]+1
out = nd.Convolution(data, w, b, kernel=w.shape[2:], num_filter=w.shape[0]) # (1,3,2,2)

print('input:', data, '\n\nweight:', w, '\n\nbias:', b, '\n\noutput:', out)

1×1卷积层

1
2
3
4
5
6
7
# 1×1卷积层(如图"输入通道为3_输出通道为2_(1×1)卷积层")
w = nd.arange(6).reshape((2,3,1,1))
b = nd.array([0,0])
data = nd.arange(27).reshape((1,3,3,3))
out = nd.Convolution(data, w, b, kernel=w.shape[2:], num_filter=w.shape[0]) # (1,2,3,3)

print('input:', data, '\n\nweight:', w, '\n\nbias:', b, '\n\noutput:', out)

代码片段:池化

1
from mxnet import nd

默认

1
2
3
4
5
6
7
8
# 池化层
# 默认(如图"池化")
data = nd.arange(9).reshape((1,1,3,3))

max_pool = nd.Pooling(data=data, pool_type="max", kernel=(2,2)) # (1,1,2,2)
avg_pool = nd.Pooling(data=data, pool_type="avg", kernel=(2,2)) # (1,1,2,2)

print('data:', data, '\n\nmax pooling:', max_pool, '\n\navg pooling:', avg_pool)

填充、步幅

1
2
3
4
5
6
7
8
9
10
11
# 填充、步幅
data = nd.arange(16).reshape((1,1,4,4))
max_pool1 = nd.Pooling(data=data, pool_type="max", kernel=(3,3),
stride=(3,3)) # (1,1,1,1)
max_pool2 = nd.Pooling(data=data, pool_type="max", kernel=(3,3),
pad=(1,1), stride=(2,2)) # (1,1,2,2)
max_pool3 = nd.Pooling(data=data, pool_type="max", kernel=(2,3),
pad=(1,2), stride=(2,3)) # (1,1,3,2)

print('data:', data, '\n\nmax pooling1:', max_pool1,
'\n\nmax pooling2:', max_pool2, '\n\nmax pooling3:', max_pool3)

多通道

1
2
3
4
5
6
7
# 多通道
data = nd.arange(32).reshape((1,2,4,4))
data[:,1,:,:] = data[:,0,:,:]+1
max_pool = nd.Pooling(data=data, pool_type="max", kernel=(3,3),
pad=(1,1), stride=(2,2)) # (1,2,2,2)

print('data:', data, '\n\nmax pooling:', max_pool)

代码片段:通过数据学习核数组

导入需要的包

1
2
from mxnet import autograd, nd
from mxnet.gluon import nn

生成数据(模拟检测边缘)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1. 生成数据(模拟检测边缘)
X = nd.ones((6, 8))
X[:, 2:6] = 0
X = X.reshape((1,1,6,8))
w_true = nd.array([1,-1]).reshape((1,1,1,2))
b_true = nd.array([1])
Y = nd.Convolution(X, w_true, b_true, kernel=w_true.shape[2:], num_filter=w_true.shape[1])

print('input:', X, '\n\nweight:', w_true, '\n\nbias:', b_true, '\n\noutput:', Y)

# 二维卷积层使用4维输入输出,格式为(样本, 通道, 高, 宽),这里批量大小(批量中的样本数)和通
# 道数均为1
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))

仅依赖nd、ag实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 要学习的权重
w = nd.array([0,0]).reshape((1,1,1,2))
b = nd.array([0])

# 创建参数的梯度
w.attach_grad()
b.attach_grad()

# 2. 训练模型
num_epochs = 500
lr = 0.001

for i in range(num_epochs):
with autograd.record():
Y_hat = nd.Convolution(X, w, b, kernel=w.shape[2:], num_filter=w.shape[1])
l = (Y_hat - Y) ** 2
l.backward()
w[:] -= lr * w.grad
b[:] -= lr * b.grad
if (i + 1) % 50 == 0:
print('batch %d, loss %.3f' % (i + 1, l.sum().asscalar()))

# 3. 比较结果(学到的模型参数、真实的模型参数)
w, b, w_true, b_true

仅依赖nn、ag实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 构造一个输出通道数为1(将在“多输入通道和多输出通道”一节介绍通道),核数组形状是(1, 2)的二维卷积层
conv2d = nn.Conv2D(1, kernel_size=(1, 2))
conv2d.initialize()

# 2. 训练模型
num_epochs = 500
lr = 0.001

for i in range(500):
with autograd.record():
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2
l.backward()
conv2d.weight.data()[:] -= lr * conv2d.weight.grad()
conv2d.bias.data()[:] -= lr * conv2d.bias.grad()
if (i + 1) % 50 == 0:
print('batch %d, loss %.3f' % (i + 1, l.sum().asscalar()))

# 3. 比较结果(学到的模型参数、真实的模型参数)
conv2d.weight.data(), conv2d.bias.data(), w_true, b_true

代码片段:1×1卷积层与全连接层

思路:1×1卷积层与全连接层计算所得的结果相同

导入需要的包

1
2
from mxnet import nd
from mxnet import autograd

1×1卷积层

1
2
3
4
5
6
7
# 1×1卷积层
w = nd.arange(6).reshape((2,3,1,1))
b = nd.array([0,0])
data = nd.arange(27).reshape((1,3,3,3))
out = nd.Convolution(data, w, b, kernel=w.shape[2:], num_filter=w.shape[0]) # (1,2,3,3)

print('input:', data, '\n\nweight:', w, '\n\nbias:', b, '\n\noutput:', out)

BP验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 新建权重、创建参数的梯度
w_pred = nd.zeros_like(w)
b_pred = nd.zeros_like(b)
w_pred.attach_grad()
b_pred.attach_grad()

# 训练模型
num_epochs = 10000
lr = 1e-5

X = data
Y = out

for i in range(num_epochs):
with autograd.record():
Y_hat = nd.Convolution(X, w_pred, b_pred, kernel=w.shape[2:], num_filter=w.shape[0])
l = (Y_hat - Y) ** 2
l.backward()
w_pred[:] -= lr * w_pred.grad
b_pred[:] -= lr * b_pred.grad
if (i + 1) % 1000 == 0:
print('batch %d, loss %.5f' % (i + 1, l.sum().asscalar()))

# 比较结果(真实的模型参数、学到的模型参数)
w, b, w_pred, b_pred

全连接层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 全连接层
data_fc = data.transpose((0,2,3,1)) # 重点 (1,3,3,3)
w_fc = w.reshape(2,3) # (2,3)
b_fc = nd.array([0,0])

def get_out(data_fc, w_fc, b_fc):
out_fc1 = nd.dot(data_fc, w_fc[0])+b_fc[0]
out_fc2 = nd.dot(data_fc, w_fc[1])+b_fc[1]
out_fc = nd.concat(out_fc1, out_fc2, dim=0)
return out_fc

out_fc = get_out(data_fc, w_fc, b_fc)

data_fc, w_fc, b_fc, out_fc

BP验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 新建权重、创建参数的梯度
w_fc_pred = nd.zeros_like(w_fc)
b_fc_pred = nd.zeros_like(b_fc)
w_fc_pred.attach_grad()
b_fc_pred.attach_grad()

# 训练模型
num_epochs = 10000
lr = 1e-5

X = data_fc
Y = out_fc

for i in range(num_epochs):
with autograd.record():
Y_hat = get_out(data_fc, w_fc_pred, b_fc_pred)
l = (Y_hat - Y) ** 2
l.backward()
w_fc_pred[:] -= lr * w_fc_pred.grad
b_fc_pred[:] -= lr * b_fc_pred.grad
if (i + 1) % 1000 == 0:
print('batch %d, loss %.5f' % (i + 1, l.sum().asscalar()))

# 比较结果(真实的模型参数、学到的模型参数)
w, b, w_fc_pred, b_fc_pred

循环神经网络

优化算法

计算性能

小结:

  1. 混合式编程:使用nn.HybridBlock,并加上net.hybridize(),自动命令式->符号式,实现优化
  2. 异步计算:默认异步计算,为了内存占用小,使用每个小批量训练或预测时至少使用一个同步函数
  3. 自动并行计算:默认
  4. 多GPU计算:数据并行,在net.initialize()gutils.split_and_load()d2l.evaluate_accuracy()中指定ctx即可

Hybridize:命令式和符号式混合编程

混合式编程取两者之长

  • 命令式编程:更方便、容易debug
  • 符号式编程:更高效、更容易移植

大部分的深度学习框架通常在命令式和符号式之间二选一:

  • 符号式:Theano、TensorFlow
  • 命令式:Chainer、PyTorch

Gluon通过提供混合式编程取两者之长:

  • 用纯命令式编程进行开发和调试
  • 当需要产品级别的计算性能和部署时,可以将大部分命令式程序转换成符号式程序来运行

混合式编程中,可以通过使用HybridBlock类或者HybridSequential类构建模型。
默认情况下,它们和Block类或者Sequential类一样依据命令式编程的方式执行。
当我们调用hybridize函数后,Gluon会转换成依据符号式编程的方式执行。
事实上,绝大多数模型都可以接受这样的混合式编程的执行方式。

注意:只有继承自HybridBlock的层才会被优化
HybridSequentialGluon提供的层都是它的子类。如果一个层只是继承自Block,那么将跳过优化。

通过HybridBlock深入理解hybridize工作机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from mxnet.gluon import nn
from mxnet import nd

class HybridNet(nn.HybridBlock):
def __init__(self, **kwargs):
super(HybridNet, self).__init__(**kwargs)
with self.name_scope():
self.fc1 = nn.Dense(10)
self.fc2 = nn.Dense(2)

def hybrid_forward(self, F, x):
print(F)
print(x)
x = F.relu(self.fc1(x))
print(x)
return self.fc2(x)

net = HybridNet()
net.initialize()
x = nd.random.normal(shape=(1, 4))
y = net(x) # mxnet.ndarray

net.hybridize() # 命令式->符号式 重点
y = net(x) # mxnet.symbol

解释:

  • F变成了symbol.
  • 即使输入数据还是NDArray的类型,但hybrid_forward里不论是输入还是中间输出,全部变成了Symbol
  • net.hybridize()后print()没有执行:
    因为第一次net(x)的时候,会先将输入替换成Symbol来构建符号式的程序,之后运行的时候系统将不再访问Python的代码,而是直接在C++后端执行这个符号式程序。这是为什么hybridze后会变快的一个原因。
    但它可能的问题是我们损失写程序的灵活性。因为Python的代码只执行一次,而且是符号式的执行,那么使用print来调试,或者使用iffor来做复杂的控制都不可能了。

延迟执行(异步计算)

MXNet使用异步计算来提升计算性能。理解它的工作原理既有助于开发更高效的程序,又有助于在内存资源有限的情况下主动降低计算性能从而减小内存开销

MXNet中的异步计算

广义上讲,MXNet包括用户直接用来交互前端系统用来执行计算的后端
例如,用户可以使用不同的前端语言编写MXNet程序,如Python、R、Scala和C++。
无论使用何种前端编程语言,MXNet程序的执行主要都发生在C++实现的后端
换句话说,用户写好的前端MXNet程序会传给后端执行计算。后端有自己的线程在队列中不断收集任务并执行它们

MXNet通过前端线程和后端线程的交互实现异步计算。异步计算指,前端线程无须等待当前指令从后端线程返回结果就继续执行后面的指令。
为了便于解释,假设Python前端线程调用以下4条指令。

在异步计算中,Python前端线程执行前3条语句的时候,仅仅是把任务放进后端的队列里就返回了。当最后一条语句需要打印计算结果时,Python前端线程会等待C++后端线程把变量c的结果计算完。
此设计的一个好处是,这里的Python前端线程不需要做实际计算。因此,无论Python的性能如何,它对整个程序性能的影响很小。只要C++后端足够高效,那么不管前端编程语言性能如何,MXNet都可以提供一致的高性能

除非我们需要打印或者保存计算结果,否则我们基本无须关心目前结果在内存中是否已经计算好了。
只要数据是保存在NDArray里并使用MXNet提供的运算符,MXNet将默认使用异步计算来获取高计算性能

用同步函数让前端等待计算结果

同步函数:让前端等待后端计算结果

  • print
  • nd.NDArray.wait_to_read():等待直到特定结果完成
  • nd.waitall():等待所有前面结果完成。测试性能常用方法
  • asnumpy函数、asscalar函数:任何将NDArray转换成其他不支持异步计算的数据结构的操作

使用异步计算提升计算性能

如果使用异步计算(MXNet默认使用异步计算),由于每次循环中前端都无须等待后端返回计算结果,可以提升一定的计算性能

异步计算对内存的影响

在通常实现的模型训练过程中,会在每个小批量上评测一下模型(使用同步函数asscalar或者asnumpy),如模型的损失或者精度。

如果去掉这些同步函数,前端会将大量的小批量计算任务在极短的时间内丢给后端,从而可能导致占用更多内存
在每个小批量上都使用同步函数时,前端在每次迭代时仅会将一个小批量的任务丢给后端执行计算,并通常会减小内存占用

建议:使用每个小批量训练或预测时至少使用一个同步函数,从而避免在短时间内将过多计算任务丢给后端。

自动并行计算

MXNet后端会自动构建计算图。通过计算图,系统可以知道所有计算的依赖关系,并可以选择将没有依赖关系的多个任务并行执行来获得计算性能的提升。

自动并行主要的用途:

  • CPU和GPU的并行计算
  • 计算和通信的并行计算
    (运行和通信之间有依赖关系:y[i]必须先在GPU上计算好才能从显存复制到CPU使用的内存。所幸的是,在计算y[i]的时候系统可以复制y[i-1],从而减少计算和通信的总运行时间。)

(一个运算符(如dot)会用到所有CPU单块GPU上全部的计算资源,故不讨论)

多GPU计算

数据并行目前是深度学习里使用最广泛的将模型训练任务划分到多块GPU的方法。

小批量随机梯度下降为例来介绍数据并行是如何工作的:

  1. 假设一台机器上有$k$块GPU。给定需要训练的模型,每块GPU及其相应的显存将分别独立维护一份完整的模型参数
  2. 在模型训练的任意一次迭代中,给定一个随机小批量,将该批量中的样本划分成$k$份并分给每块显卡的显存一份
  3. 然后,每块GPU将根据相应显存所分到的小批量子集和所维护的模型参数分别计算模型参数的本地梯度
  4. 接下来,把$k$块显卡的显存上的本地梯度相加,便得到当前的小批量随机梯度。
  5. 之后,每块GPU都使用这个小批量随机梯度分别更新相应显存所维护的那一份完整的模型参数

在多GPU时,通常需要增加批量大小使得每个GPU能得到足够多的任务来保证性能。
但一个大的批量大小可能使得收敛变慢。这时候的一个常用做法是将学习率增大些

详细:
《动手学深度学习》8.4.多GPU计算
《动手学深度学习》8.5.多GPU计算的简洁实现

计算机视觉

自然语言处理

参考

官方

博客

内容 描述

安装:
pip install mxnet-cu90

升级:
pip install —upgrade mxnet-cu90

软件包版本:
pip install mxnet_cu90==1.4
pip install gluoncv==0.4

1
2
3
4
5
6
7
8
9
10
11
Ubuntu终端:
cd /home/amax/xw/xgy/gluon/d2l-zh-0.61
source activate gluon
jupyter notebook --NotebookApp.contents_manager_class='notedown.NotedownContentsManager' --ip="*"


Win10的cmd:
ssh -L8888:localhost:8888 amax@172.18.146.25

浏览器
localhost:8888

小技巧

自动推断每层的输出维度、权重维度

以LeNet为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from mxnet import nd
from mxnet.gluon import nn

net = nn.Sequential()
net.add(nn.Conv2D(channels=6, kernel_size=5, activation='sigmoid'),
nn.MaxPool2D(pool_size=2, strides=2),
nn.Conv2D(channels=16, kernel_size=5, activation='sigmoid'),
nn.MaxPool2D(pool_size=2, strides=2),
nn.Dense(120, activation='sigmoid'), # Dense会默认将(批量大小, 通道, 高, 宽)形状的输入转换成(批量大小, 通道 * 高 * 宽)形状的输入
nn.Dense(84, activation='sigmoid'),
nn.Dense(10))

X = nd.random.uniform(shape=(1, 1, 28, 28))
net.initialize()
for layer in net:
X = layer(X)
print(layer.name, 'output shape:\t', X.shape)
try: # pooling层没有权重,用try遇到异常不会退出
print('weight shape:', layer.weight.shape, '\tbias shape:', layer.bias.shape)
except:
pass

# 另一种查看网络结构的方法
net.collect_params()

抛出异常

1
2
3
4
5
6
import sys
try:
net = get_net()
net(x)
except RuntimeError as err:
sys.stderr.write(str(err))

导入上一层目录的包

1
2
import sys
sys.path.append('..')

计时类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from mxnet import nd
import time

class Benchmark():
def __init__(self, prefix=None):
self.prefix = prefix + ' ' if prefix else ''

def __enter__(self):
self.start = time.time()

def __exit__(self, *args):
print('%stime: %.4f sec' % (self.prefix, time.time() - self.start))

with Benchmark('Workloads are queued.'):
x = nd.random.uniform(shape=(2000, 2000))