李沐 代码

本文是李沐关于深度学习之Mxnet的视频讲解概述,涵盖了从基础知识如NDArray和自动求导,到线性回归、逻辑回归、多层感知机、卷积神经网络的实现,再到正则化、GPU计算、卷积神经网络结构(如AlexNet、VGGNet)、批量归一化、网络中的网络NIN、GoogleNet、Resnet、DenseNet等,最后讨论了图片增强和使用Gluon实现SSD的目标检测。通过实例和练习,深入浅出地讲解了深度学习的核心概念和操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

博文 http://bestzhangjin.com/2017/10/13/deeplearn/

目录

 

深度学习之Mxnet--李沐视频

1.1.前言

1.2.使用NDArray来处理数据

1.3.使用autograd自动求导

1.4.从0开始线性回归

1.5.使用Gluon实现线性回归

1.6.从0开始多类逻辑回归

1.7.Gluon版多类逻辑回归

2.1.从0开始多层感知机

2.2.使用Gluon多层感知机

2.3.从0开始正则化

2.3.使用Gluon正则化

2.4.使用GPU来计算

2.5.从0开始卷积神经网络

2.6.使用gluon卷积神经网络

3.1.创建神经网络

3.2.初始化模型参数

3.3.序列化读写模型

3.4.设计自定义层

3.5.dropout

3.6.使用Gluon丢弃法(Dropout)

3.7.深度卷积神经网络和AlexNet

3.8.VGGNet

4.1. 从0开始批量归一化BatchNorm

4.2.gluon版batchnorm

4.3.网络中的网络NIN

4.4.GoogleNet

4.5.Resnet

4.6.DenseNet

4.7.图片增强

8.1 使用Gluon实现SSD


深度学习之Mxnet--李沐视频

发表于 2017-10-13   |   分类于 深度学习   |  

1.1.前言

资料详见动手学深度学习

1.2.使用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
# coding=utf-8

from mxnet import ndarray as nd
import numpy as np
##具体看NDArray API

nd.zeros((3, 4))  #3行和4列的2D数组 全0
x=nd.ones((3,4)) #全1
#或者从python的数组直接构造
nd.array([[1,2],[2,3]]) #[[ 1.  2.] [ 2.  3.]]
## 创建随机数 - 深度学习常用
y = nd.random_normal(0, 1, shape=(3, 4)) #均值0方差1的正态分布3x4矩阵
print y.shape # (3L, 4L)
print y.size # 12

##数学操作  nd.dot(x,y)为矩阵乘法 区别于*对应元素相乘
x+y
x*y
print nd.exp(y) ##指数
print nd.dot(x,y.T) #x与y的转置进行矩阵乘法

##Numpy与NDArray转换
x = np.ones((2,3))
y = nd.array(x)  # numpy -> mxnet
z = y.asnumpy()  # mxnet -> numpy
print([z, y])

1.3.使用autograd自动求导

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
# coding=utf-8

from mxnet import ndarray as nd
import mxnet.autograd as ag ##自动求导
import numpy as np

x = nd.array([[1, 2], [3, 4]])
#对需要求导的变量需要通过NDArray的方法`attach_grad()`来要求系统申请对应的空间
x.attach_grad()

#默认条件下,MXNet不会自动记录和构建用于求导的计算图,
# 我们需要使用autograd里的`record()`函数来显式的要求MXNet记录我们需要求导的程序。
with ag.record():
    y = x * 2
    z = y * x  ##z=2乘以x的平方 [[  2.   8.] [ 18.  32.]]

#接下来我们可以通过z.backward()来进行求导。
# 如果z不是一个标量,那么z.backward()等价于nd.sum(z).backward()
z.backward() ## None
print x.grad  # z针对x求导的结果 [[  4.   8.] [ 12.  16.]]
## x.grad == 4*x
print x.grad == 4*x  ## [[ 1.  1.] [ 1.  1.]]

## 对控制流求导
def f(a):
    b = a * 2
    ## nd.norm(b)-b中所有数据的平方之和取根号 asscalar()--转换成标量
    while nd.norm(b).asscalar() < 1000:
        b = b * 2
    if nd.sum(b).asscalar() > 0:
        c = b
    else:
        c = 100 * b
    return c

a = nd.random_normal(shape=3)
a.attach_grad()
with ag.record():
    c = f(a)
c.backward()
print a.grad # [ 512.  512.  512.]
print a.grad==c/a # [ 1.  1.  1.]

1.4.从0开始线性回归

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# coding=utf-8

from mxnet import ndarray as nd
from mxnet import autograd
import random

num_inputs = 2
num_examples = 1000

true_w = [2, -3.4]
true_b = 4.2

X = nd.random_normal(shape=(num_examples, num_inputs))
y = true_w[0] * X[:, 0] + true_w[1] * X[:, 1] + true_b
y += .01 * nd.random_normal(shape=y.shape) ##加噪声

# 注意到`X`的每一行是一个长度为2的向量,而`y`的每一行是一个长度为1的向量(标量)。

print(X[0], y[0]) # [[ 2.21220636  1.16307867],[ 4.6620779]]

# 当我们开始训练神经网络的时候,我们需要不断读取数据块。
# 这里我们定义一个函数它每次返回batch_size个随机的样本和对应的目标。
# 我们通过python的yield来构造一个迭代器。
batch_size = 10
def data_iter():
    # 产生一个随机索引
    idx = list(range(num_examples))
    random.shuffle(idx) ##索引打乱顺序
    for i in range(0, num_examples, batch_size):
        j = nd.array(idx[i:min(i+batch_size,num_examples)]) #随机的batch个索引
        yield nd.take(X, j), nd.take(y, j) #根据索引拿到数据

for data, label in data_iter():
    print(data, label)
    break

n=0
for data, label in data_iter():
    n=n+1
print n ## 100 每次拿10组数据 拿100次拿完


#下面我们随机初始化模型参数
w = nd.random_normal(shape=(num_inputs, 1))
b = nd.zeros((1,))
params = [w, b]
print params # [[ 0.72455114] [ 0.13263007]], [ 0.]]

# 之后训练时我们需要对这些参数求导来更新它们的值,所以我们需要创建它们的梯度
for param in params:
    param.attach_grad()

## 定义模型
# 线性模型就是将输入和模型做乘法再加上偏移:
def net(X):
    return nd.dot(X, w) + b

## 损失函数
#我们使用常见的平方误差来衡量预测目标和真实目标之间的差距。
def square_loss(yhat, y):
    # 注意这里我们把y变形成yhat的形状来避免自动广播
    return (yhat - y.reshape(yhat.shape)) ** 2

#虽然线性回归有显试解,但绝大部分模型并没有。
# 所以我们这里通过随机梯度下降来求解。每一步,
# 我们将模型参数沿着梯度的反方向走特定距离,这个距离一般叫学习率。
def SGD(params, lr):
    for param in params:
        param[:] = param - lr * param.grad

## 训练
# 现在我们可以开始训练了。训练通常需要迭代数据数次,
# 一次迭代里,我们每次随机读取固定数个数据点,计算梯度并更新模型参数。
epochs = 10
learning_rate = .001
for e in range(epochs):
    total_loss = 0
    for data, label in data_iter():
        with autograd.record():
            output = net(data)
            loss = square_loss(output, label)
        loss.backward()
        SGD(params, learning_rate)
        total_loss += nd.sum(loss).asscalar()
    print("Epoch %d, average loss: %f" % (e, total_loss/num_examples))

print true_w,w  #[2, -3.4]  [[ 1.99984801] [-3.40017033]]
print true_b,b  #4.2  [ 4.19996023]

1.5.使用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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# coding=utf-8

from mxnet import ndarray as nd
from mxnet import autograd
from mxnet import gluon

## 创建数据集
num_inputs = 2
num_examples = 1000
true_w = [2, -3.4]
true_b = 4.2

X = nd.random_normal(shape=(num_examples, num_inputs))
y = true_w[0] * X[:, 0] + true_w[1] * X[:, 1] + true_b
y += .01 * nd.random_normal(shape=y.shape)

## 数据读取
batch_size = 10
dataset = gluon.data.ArrayDataset(X, y)
data_iter = gluon.data.DataLoader(dataset, batch_size, shuffle=True)

for data, label in data_iter:
    print(data, label)
    break

## 定义模型
# 当我们手写模型的时候,我们需要先声明模型参数,然后再使用它们来构建模型。
# 但gluon提供大量提前定制好的层,使得我们只需要主要关注使用哪些层来构建模型。
# 例如线性模型就是使用对应的Dense层。
#虽然我们之后会介绍如何构造任意结构的神经网络,
# 构建模型最简单的办法是利用Sequential来所有层串起来。
# 首先我们定义一个空的模型:
net = gluon.nn.Sequential()

#然后我们加入一个Dense层,它唯一必须要定义的参数就是输出节点的个数,在线性模型里面是1.
#这里我们并没有定义说这个层的输入节点是多少,这个在之后真正给数据的时候系统会自动赋值。
net.add(gluon.nn.Dense(1))
print net  #Sequential((0): Dense(1, linear))

# 在使用前net我们必须要初始化模型权重,这里我们使用默认随机初始化方法
# (之后我们会介绍更多的初始化方法)。
net.initialize()

## 损失函数
# gluon提供了平方误差函数:
square_loss = gluon.loss.L2Loss()

## 优化
#同样我们无需手动实现随机梯度下降,我们可以用创建一个Trainer的实例,
# 并且将模型参数传递给它就行。
trainer = gluon.Trainer(
    net.collect_params(), 'sgd', {'learning_rate': 0.1})

## 训练
# 这里的训练跟前面没有太多区别,唯一的就是我们不再是调用SGD,
# 而是trainer.step来更新模型。
epochs = 5
batch_size = 10
for e in range(epochs):
    total_loss = 0
    for data, label in data_iter:
        with autograd.record():
            output = net(data)
            loss = square_loss(output, label)
        loss.backward()
        trainer.step(batch_size) ##往前走一步
        total_loss += nd.sum(loss).asscalar()
    print("Epoch %d, average loss: %f" % (e, total_loss/num_examples))

# 比较学到的和真实模型。我们先从`net`拿到需要的层,然后访问其权重和位移。
dense = net[0]
print true_w, dense.weight.data() ##[2, -3.4]  [[ 1.99912584 -3.39986587]]
print true_b, dense.bias.data() ##4.2  [ 4.19950867]

1.6.从0开始多类逻辑回归

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# coding=utf-8

from mxnet import gluon
from mxnet import ndarray as nd

#这里我们用MNIST分类数字
# 我们通过gluon的data.vision模块自动下载这个数据。
def transform(data, label):
    return data.astype('float32')/255, label.astype('float32')
mnist_train = gluon.data.vision.MNIST(train=True, transform=transform)
mnist_test = gluon.data.vision.MNIST(train=False, transform=transform)

# 打印一个样本的形状和它的标号
data, label = mnist_train[0]
print ('example shape: ', data.shape, 'label:', label) ##('example shape: ', (28L, 28L, 1L), 'label:', 5.0)

## 数据读取
#虽然我们可以像前面那样通过yield来定义获取批量数据函数,
# 这里我们直接使用gluon.data的DataLoader函数,它每次yield一个批量。
# 注意到这里我们要求每次从训练数据里读取一个由随机样本组成的批量,
# 但测试数据则不需要这个要求。
batch_size = 256
train_data = gluon.data.DataLoader(mnist_train, batch_size, shuffle=True)
test_data = gluon.data.DataLoader(mnist_test, batch_size, shuffle=False)


## 初始化模型参数
# 跟线性模型一样,每个样本会表示成一个向量。我们这里数据是 28 * 28 大小的图片,
# 所以输入向量的长度是 28 * 28 = 784。
# 因为我们要做多类分类,我们需要对每一个类预测这个样本属于此类的概率。
# 因为这个数据集有10个类型,所以输出应该是长为10的向量。
# 这样,我们需要的权重将是一个 784 * 10 的矩阵:
num_inputs = 784
num_outputs = 10
W = nd.random_normal(shape=(num_inputs, num_outputs))
b = nd.random_normal(shape=num_outputs)
params = [W, b]

#同之前一样,我们要对模型参数附上梯度:
for param in params:
    param.attach_grad()

## 定义模型
# 在线性回归教程里,我们只需要输出一个标量yhat使得尽可能的靠近目标值。
# 但在这里的分类里,我们需要属于每个类别的概率。
# 这些概率需要值为正,而且加起来等于1.
# 而如果简单的使用 Y=WX,我们不能保证这一点。
# 一个通常的做法是通过softmax函数来将任意的输入归一化成合法的概率值。
from mxnet import nd
def softmax(X):
    exp = nd.exp(X)  ##全部变为正的
    # 假设exp是矩阵,这里对行进行求和,并要求保留axis 1,
    # 就是返回 (nrows, 1) 形状的矩阵
    partition = exp.sum(axis=1, keepdims=True)
    return exp / partition  ##每个行除以它的行的和

#可以看到,对于随机输入,我们将每个元素变成了非负数,而且每一行加起来为1。
X = nd.random_normal(shape=(2,5))
print X
# [[ 0.79687113  0.85240501  0.61860603  0.47654876  0.74863517]
# [ 0.55032933 -0.22566749 -2.11320662 -0.95748073  0.32560727]]
X_prob = softmax(X)
print(X_prob)
# [[ 0.21869159  0.23117994  0.18298376  0.1587515   0.20839317]
# [ 0.39214477  0.1804826   0.02733301  0.08681861  0.31322101]]
print(X_prob.sum(axis=1)) # [ 0.99999994  1.        ] ([1,1])

#现在我们可以定义模型了:
def net(X):
    return softmax(nd.dot(X.reshape((-1,num_inputs)), W) + b)

## 交叉熵损失函数
# 我们需要定义一个针对预测为概率值的损失函数。其中最常见的是交叉熵损失函数,
# 它将两个概率分布的负交叉熵作为目标值,最小化这个值等价于最大化这两个概率的相似度。
# 具体来说,我们先将真实标号表示成一个概率分布,例如如果y=1,
# 那么其对应的分布就是一个除了第二个元素为1其他全为0的长为10的向量,
# 也就是 yvec=[0, 1, 0, 0, 0, 0, 0, 0, 0, 0]。
# 那么交叉熵就是yvec[0]*log(yhat[0])+...+yvec[n]*log(yhat[n])。
# 注意到yvec里面只有一个1,那么前面等价于log(yhat[y])。
# 所以我们可以定义这个损失函数了
def cross_entropy(yhat, y):
    return - nd.pick(nd.log(yhat), y)

#给定一个概率输出,我们将预测概率最高的那个类作为预测的类,
# 然后通过比较真实标号我们可以计算精度:
def accuracy(output, label):
    return nd.mean(output.argmax(axis=1)==label).asscalar()


#我们可以评估一个模型在这个数据上的精度。
def evaluate_accuracy(data_iterator, net):
    acc = 0.
    for data, label in data_iterator:
        output = net(data)
        acc += accuracy(output, label)
    return acc / len(data_iterator)

#因为我们随机初始化了模型,所以这个模型的精度应该大概是1/num_outputs = 0.1.
print evaluate_accuracy(test_data, net) #0.09814453125

def SGD(params, lr):
    for param in params:
        param[:] = param - lr * param.grad

## 训练
import sys
sys.path.append('..')
from mxnet import autograd

learning_rate = .1
for epoch in range(5):
    train_loss = 0.
    train_acc = 0.
    for data, label in train_data:
        with autograd.record():
            output = net(data)
            loss = cross_entropy(output, label)
        loss.backward()
        # 将梯度做平均,这样学习率会对batch size不那么敏感
        SGD(params, learning_rate/batch_size)

        train_loss += nd.mean(loss).asscalar()
        train_acc += accuracy(output, label)

    test_acc = evaluate_accuracy(test_data, net)
    print("Epoch %d. Loss: %f, Train acc %f, Test acc %f" % (
        epoch, train_loss/len(train_data), train_acc/len(train_data), test_acc))

## 预测
#训练完成后,现在我们可以演示对输入图片的标号的预测

data, label = mnist_test[0:9]
print('true labels')
print(label)

predicted_labels = net(data).argmax(axis=1)
print('predicted labels')
print(predicted_labels.asnumpy())

尝试增大学习率,你会发现结果马上回变成很糟糕,精度基本徘徊在随机的0.1左右。这是为什么呢?提示:

  • 打印下output看看是不是有有什么异常
  • 前面线性回归还好好的,这里我们在net()里加了什么呢?
  • 如果给exp输入个很大的数会怎么样?
  • 即使解决exp的问题,求出来的导数是不是还是不稳定?

请仔细想想再去对比下小伙伴之一@pluskid早年写的一篇blog解释这个问题,看看你想的是不是不一样。

1.7.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
59
60
61
62
63
64
65
66
67
68
69
70
71
# coding=utf-8

from mxnet import gluon
from mxnet import ndarray as nd
from mxnet import autograd
from mxnet import image

batch_size = 256
#这里我们用MNIST分类数字
def load_data_fashion_mnist(batch_size, resize=None):
    """download the fashion mnist dataest and then load into memory"""
    def transform_mnist(data, label):
        if resize:
            # resize to resize x resize
            data = image.imresize(data, resize, resize)
        # change data from height x weight x channel to channel x height x weight
        return nd.transpose(data.astype('float32'), (2,0,1))/255, label.astype('float32')
    mnist_train = gluon.data.vision.MNIST(train=True, transform=transform_mnist)
    mnist_test = gluon.data.vision.MNIST(train=False, transform=transform_mnist)
    train_data = gluon.data.DataLoader(mnist_train, batch_size, shuffle=True)
    test_data = gluon.data.DataLoader(mnist_test, batch_size, shuffle=False)
    return (train_data, test_data)

def transform(data, label):
    return data.astype('float32')/255, label.astype('float32')
mnist_train,mnist_test  = load_data_fashion_mnist(batch_size)

# 我们先使用Flatten层将输入数据转成batch_size的矩阵?
# 然后输入到10个输出节点的全连接层。
# 照例我们不需要制定每层输入的大小,gluon会做自动推导。
net = gluon.nn.Sequential()
with net.name_scope():
    net.add(gluon.nn.Flatten())
    net.add(gluon.nn.Dense(10))
net.initialize()

# 然后通过比较真实标号我们可以计算精度:
def accuracy(output, label):
    return nd.mean(output.argmax(axis=1)==label).asscalar()

#我们可以评估一个模型在这个数据上的精度。
def evaluate_accuracy(data_iterator, net):
    acc = 0.
    for data, label in data_iterator:
        output = net(data)
        acc += accuracy(output, label)
    return acc / len(data_iterator)

## Softmax和交叉熵损失函数
#如果你做了上一章的练习,那么你可能意识到了分开定义Softmax和交叉熵会有数值不稳定性
softmax_cross_entropy = gluon.loss.SoftmaxCrossEntropyLoss()

## 优化
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.1})

## 训练
for epoch in range(5):
    train_loss = 0.
    train_acc = 0.
    for data, label in mnist_train:
        with autograd.record():
            output = net(data)
            loss = softmax_cross_entropy(output, label)
        loss.backward()
        trainer.step(batch_size)

        train_loss += nd.mean(loss).asscalar()
        train_acc += accuracy(output, label)
    test_acc = evaluate_accuracy(mnist_test, net)
    print("Epoch %d. Loss: %f, Train acc %f, Test acc %f" % (
        epoch, train_loss/len(mnist_train), train_acc/len(mnist_train), test_acc))

2.1.从0开始多层感知机

前面我们介绍了包括线性回归和多类逻辑回归的数个模型,它们的一个共同点是全是只含有一个输入层,一个输出层。这一节我们将介绍多层神经网络,就是包含至少一个隐含层的网络。

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# coding=utf-8

from mxnet import gluon
from mxnet import ndarray as nd
from mxnet import autograd
from mxnet import image


#这里我们用MNIST分类数字
def load_data_fashion_mnist(batch_size, resize=None):
    """download the fashion mnist dataest and then load into memory"""
    def transform_mnist(data, label):
        if resize:
            # resize to resize x resize
            data = image.imresize(data, resize, resize)
        # change data from height x weight x channel to channel x height x weight
        return nd.transpose(data.astype('float32'), (2,0,1))/255, label.astype('float32')
    mnist_train = gluon.data.vision.MNIST(train=True, transform=transform_mnist)
    mnist_test = gluon.data.vision.MNIST(train=False, transform=transform_mnist)
    train_data = gluon.data.DataLoader(mnist_train, batch_size, shuffle=True)
    test_data = gluon.data.DataLoader(mnist_test, batch_size, shuffle=False)
    return (train_data, test_data)


batch_size = 256
train_data, test_data = load_data_fashion_mnist(batch_size)

# 这里我们定义一个只有一个隐含层的模型,这个隐含层输出256个节点。
num_inputs = 28*28
num_outputs = 10
num_hidden = 256
weight_scale = .01

##定义两层
W1 = nd.random_normal(shape=(num_inputs, num_hidden), scale=weight_scale)
b1 = nd.zeros(num_hidden)
W2 = nd.random_normal(shape=(num_hidden, num_outputs), scale=weight_scale)
b2 = nd.zeros(num_outputs)

params = [W1, b1, W2, b2]
for param in params:
    param.attach_grad()

## 激活函数
#如果我们就用线性操作符来构造多层神经网络,那么整个模型仍然只是一个线性函数。
# 为了让我们的模型可以拟合非线性函数,我们需要在层之间插入非线性的激活函数。这里我们使用ReLU
def relu(X):
    return nd.maximum(X, 0)

## 定义模型
#我们的模型就是将层(全连接)和激活函数(Relu)串起来:
def net(X):
    X = X.reshape((-1, num_inputs))
    h = relu(nd.dot(X, W1) + b1)
    output = nd.dot(h, W2) + b2
    return output

## Softmax和交叉熵损失函数
#在多类Logistic回归里我们提到分开实现Softmax和交叉熵损失函数可能导致数值不稳定。
# 这里我们直接使用Gluon提供的函数
softmax_cross_entropy = gluon.loss.SoftmaxCrossEntropyLoss()

# 然后通过比较真实标号我们可以计算精度:
def accuracy(output, label):
    return nd.mean(output.argmax(axis=1)==label).asscalar()

#我们可以评估一个模型在这个数据上的精度。
def evaluate_accuracy(data_iterator, net):
    acc = 0.
    for data, label in data_iterator:
        output = net(data)
        acc += accuracy(output, label)
    return acc / len(data_iterator)

def SGD(params, lr):
    for param in params:
        param[:] = param - lr * param.grad

## 训练
learning_rate = .5
for epoch in range(20):
    train_loss = 0.
    train_acc = 0.
    for data, label in train_data:
        with autograd.record():
            output = net(data)
            loss = softmax_cross_entropy(output, label)
        loss.backward()
        SGD(params, learning_rate/batch_size)

        train_loss += nd.mean(loss).asscalar()
        train_acc += accuracy(output, label)

    test_acc = evaluate_accuracy(test_data, net)
    print("Epoch %d. Loss: %f, Train acc %f, Test acc %f" % (
        epoch, train_loss/len(train_data),
        train_acc/len(train_data), test_acc))

 

可以看到,加入一个隐含层后我们将精度提升了不少。

练习

  • 我们使用了 weight_scale 来控制权重的初始化值大小,增大或者变小这个值会怎么样?
  • 尝试改变 num_hiddens 来控制模型的复杂度
  • 尝试加入一个新的隐含层
    注意:针对不同的数据,模型可能并非越复杂越好
    测试结果:加大num_hiddens得到了更好的结果,添加隐藏层后更加容易过拟合

2.2.使用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
59
60
61
62
63
# coding=utf-8

from mxnet import gluon
from mxnet import ndarray as nd
from mxnet import autograd
from mxnet import image

#这里我们用MNIST分类数字
def load_data_fashion_mnist(batch_size, resize=None):
    """download the fashion mnist dataest and then load into memory"""
    def transform_mnist(data, label):
        if resize:
            # resize to resize x resize
            data = image.imresize(data, resize, resize)
        # change data from height x weight x channel to channel x height x weight
        return nd.transpose(data.astype('float32'), (2,0,1))/255, label.astype('float32')
    mnist_train = gluon.data.vision.MNIST(train=True, transform=transform_mnist)
    mnist_test = gluon.data.vision.MNIST(train=False, transform=transform_mnist)
    train_data = gluon.data.DataLoader(mnist_train, batch_size, shuffle=True)
    test_data = gluon.data.DataLoader(mnist_test, batch_size, shuffle=False)
    return (train_data, test_data)

def accuracy(output, label):
    return nd.mean(output.argmax(axis=1)==label).asscalar()
def evaluate_accuracy(data_iterator, net):
    acc = 0.
    for data, label in data_iterator:
        output = net(data)
        acc += accuracy(output, label)
    return acc / len(data_iterator)

## 定义模型
#唯一的区别在这里,我们加了一行进来。
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(10))
net.initialize()

## 读取数据并训练
batch_size = 256
train_data, test_data = load_data_fashion_mnist(batch_size)

softmax_cross_entropy = gluon.loss.SoftmaxCrossEntropyLoss()
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.5})

for epoch in range(5):
    train_loss = 0.
    train_acc = 0.
    for data, label in train_data:
        with autograd.record():
            output = net(data)
            loss = softmax_cross_entropy(output, label)
        loss.backward()
        trainer.step(batch_size)

        train_loss += nd.mean(loss).asscalar()
        train_acc += accuracy(output, label)

    test_acc = evaluate_accuracy(test_data, net)
    print("Epoch %d. Loss: %f, Train acc %f, Test acc %f" % (
        epoch, train_loss/len(train_data), train_acc/len(train_data), test_acc))

通过Gluon我们可以更方便地构造多层神经网络。
练习

  • 尝试多加入几个隐含层,对比从0开始的实现。
  • 尝试使用一个另外的激活函数,可以使用help(nd.Activation)或者线上文档查看提供的选项。

2.3.从0开始正则化

本章从0开始介绍如何的正则化来应对过拟合问题。
L2范数正则化
这里我们引入$L_2$范数正则化。不同于在训练时仅仅最小化损失函数(Loss),我们在训练时其实在最小化
img1
直观上,L2范数正则化试图惩罚较大绝对值的参数值。训练模型时,如果λ=0则没有正则化,需要注意的是,测试模型时,λ必须为0。

高维线性回归
我们使用高维线性回归为例来引入一个过拟合问题。
img2
需要注意的是,我们用以上相同的数据生成函数来生成训练数据集和测试数据集。为了观察过拟合,我们特意把训练数据样本数设低,例如n=20,同时把维度升高,例如p=200.

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# coding=utf-8

from mxnet import ndarray as nd
from mxnet import autograd
from mxnet import gluon

num_train = 20
num_test = 100
num_inputs = 200

## 生成数据集
# 这里定义模型真实参数。
true_w = nd.ones((num_inputs, 1)) * 0.01
true_b = 0.05

#我们接着生成训练和测试数据集。
X = nd.random.normal(shape=(num_train + num_test, num_inputs))
y = nd.dot(X, true_w)
y += .01 * nd.random.normal(shape=y.shape)

X_train, X_test = X[:num_train, :], X[num_train:, :]
y_train, y_test = y[:num_train], y[num_train:]

#当我们开始训练神经网络的时候,我们需要不断读取数据块。
# 这里我们定义一个函数它每次返回batch_size个随机的样本和对应的目标。
# 我们通过python的yield来构造一个迭代器。
import random
batch_size = 1
def data_iter(num_examples):
    idx = list(range(num_examples))
    random.shuffle(idx)
    for i in range(0, num_examples, batch_size):
        j = nd.array(idx[i:min(i+batch_size,num_examples)])
        yield X.take(j), y.take(j)

## 初始化模型参数
# 下面我们随机初始化模型参数。
# 之后训练时我们需要对这些参数求导来更新它们的值,所以我们需要创建它们的梯度。
def get_params():
    w = nd.random.normal(shape=(num_inputs, 1))*0.1
    b = nd.zeros((1,))
    for param in (w, b):
        param.attach_grad()
    return (w, b)

#下面我们定义L2正则化。注意有些时候大家对偏移加罚,有时候不加罚。
# 通常结果上两者区别不大。这里我们演示对偏移也加罚的情况:
def L2_penalty(w, b):
    return (w**2).sum() + b**2

## 定义训练和测试
#下面我们定义剩下的所需要的函数。这个跟之前的教程大致一样,
# 主要是区别在于计算`loss`的时候我们加上了L2正则化,以及我们将训练和测试损失都画了出来。
import matplotlib as mpl
mpl.rcParams['figure.dpi']= 120
import matplotlib.pyplot as plt

def net(X, lambd, w, b):
    return nd.dot(X, w) + b

def square_loss(yhat, y):
    return (yhat - y.reshape(yhat.shape)) ** 2

def SGD(params, lr):
    for param in params:
        param[:] = param - lr * param.grad

def test(params, X, y):
    return square_loss(net(X, 0, *params), y).mean().asscalar()

def train(lambd):
    epochs = 10
    learning_rate = 0.002
    params = get_params()
    train_loss = []
    test_loss = []
    for e in range(epochs):
        for data, label in data_iter(num_train):
            with autograd.record():
                output = net(data, lambd, *params)
                loss = square_loss(##loss加上了正则化
                    output, label) + lambd * L2_penalty(*params)
            loss.backward()
            SGD(params, learning_rate)
        train_loss.append(test(params, X_train, y_train))
        test_loss.append(test(params, X_test, y_test))
    plt.plot(train_loss)
    plt.plot(test_loss)
    plt.legend(['train', 'test'])
    plt.show()
    return 'learned w[:10]:', params[0][:10], 'learend b:', params[1]

##过拟合
# train(0)
##使用正则
train(2)

 

过拟合
img3
使用正则
img4

2.3.使用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
59
60
61
62
63
64
65
66
67
68
69
70
71
# coding=utf8

from mxnet import ndarray as nd
from mxnet import autograd
from mxnet import gluon

num_train = 20
num_test = 100
num_inputs = 200

true_w = nd.ones((num_inputs, 1)) * 0.01
true_b = 0.05
X = nd.random.normal(shape=(num_train + num_test, num_inputs))
y = nd.dot(X, true_w)
y += .01 * nd.random.normal(shape=y.shape)
X_train, X_test = X[:num_train, :], X[num_train:, :]
y_train, y_test = y[:num_train], y[num_train:]

## 定义训练和测试
#跟前一样定义训练模块。你也许发现了主要区别,`Trainer`
#有一个新参数
#wd。我们通过优化算法的wd参数(weightdecay)实现对模型的正则化。这相当于L2范数正则化。
import matplotlib as mpl

mpl.rcParams['figure.dpi'] = 120
import matplotlib.pyplot as plt

batch_size = 1
dataset_train = gluon.data.ArrayDataset(X_train, y_train)
data_iter_train = gluon.data.DataLoader(dataset_train, batch_size, shuffle=True)
square_loss = gluon.loss.L2Loss()


def test(net, X, y):
    return square_loss(net(X), y).mean().asscalar()


def train(weight_decay):
    learning_rate = 0.005
    epochs = 10

    net = gluon.nn.Sequential()
    with net.name_scope():
        net.add(gluon.nn.Dense(1))
    net.initialize()

    # 注意到这里 'wd'
    trainer = gluon.Trainer(net.collect_params(), 'sgd', {
        'learning_rate': learning_rate, 'wd': weight_decay})###参数wd

    train_loss = []
    test_loss = []
    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))
    plt.plot(train_loss)
    plt.plot(test_loss)
    plt.legend(['train', 'test'])
    plt.show()

    return ('learned w[:10]:', net[0].weight.data()[:, :10],
            'learned b:', net[0].bias.data())

train(0)
train(2)

2.4.使用GPU来计算

MXNet使用Context来指定使用哪个设备来存储和计算。默认会将数据开在主内存,然后利用CPU来计算,这个由mx.cpu()来表示。GPU则由mx.gpu()来表示。注意mx.cpu()表示所有的物理CPU和内存,意味着计算上会尽量使用多有的CPU核。但mx.gpu()只代表一块显卡和其对应的显卡内存。如果有多块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
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
71
# coding=utf-8

import mxnet as mx
import sys

print [mx.cpu(), mx.gpu(), mx.gpu(1)]  #[cpu(0), gpu(0), gpu(1)]

#每个NDArray都有一个`context`属性来表示它存在哪个设备上,默认会是`cpu`。
# 这是为什么前面每次我们打印NDArray的时候都会看到`@cpu(0)`这个标识。
from mxnet import nd
x = nd.array([1,2,3])
print x.context  #cpu(0)

### GPU上创建内存
#我们可以在创建的时候指定创建在哪个设备上
# (如果GPU不能用或者没有装MXNet GPU版本,这里会有error):
a = nd.array([1,2,3], ctx=mx.gpu())
b = nd.zeros((3,2), ctx=mx.gpu())
c = nd.random.uniform(shape=(2,3), ctx=mx.gpu())
print (a,b,c) ##a,b,c都在gpu上

#我们可以通过`copyto`和`as_in_context`来在设备直接传输数据。

y = x.copyto(mx.gpu())
z = x.as_in_context(mx.gpu())
print (y, z)

#这两个函数的主要区别是,如果源和目标的context一致(都在gpu上)
# `as_in_context`不复制,而`copyto`总是会新建内存:
yy = y.as_in_context(mx.gpu())
zz = z.copyto(mx.gpu())
print (yy is y, zz is z) #(True, False)

#计算会在数据的`context`上执行。所以为了使用GPU,我们只需要事先将数据放在上面就行了。
# 结果会自动保存在对应的设备上:
print nd.exp(z + 2) * y ##gpu上

#注意所有计算要求输入数据在同一个设备上。不一致的时候系统不进行自动复制。
# 这个设计的目的是因为设备之间的数据交互通常比较昂贵,
# 我们希望用户确切的知道数据放在哪里,而不是隐藏这个细节。
# 下面代码尝试将CPU上`x`和GPU上的`y`做运算。
# #异常require all inputs live on the same context.
#     # But the first argument is on cpu(0) while the 2-th argument is on gpu(0)
# try:
#     x + y
# except mx.MXNetError as err:
#     sys.stderr.write(str(err))


### 默认会复制回CPU的操作
#如果某个操作需要将NDArray里面的内容转出来,例如打印或变成numpy格式,
# 如果需要的话系统都会自动将数据copy到主内存。
print(y)
print(y.asnumpy())
print(y.sum().asscalar())

## Gluon的GPU计算
#同NDArray类似,Gluon的大部分函数可以通过`ctx`指定设备。
# 下面代码将模型参数初始化在GPU上:
from mxnet import gluon
net = gluon.nn.Sequential()
net.add(gluon.nn.Dense(1))

net.initialize(ctx=mx.gpu())  ####gpu,不给默认cpu

# 输入GPU上的数据,会在GPU上计算结果
data = nd.random.uniform(shape=[3,2], ctx=mx.gpu())
print net(data)  ##gpu(0)

#确认下权重:
print net[0].weight.data()

通过context我们可以很容易在不同的设备上计算。
练习

  • 试试大一点的计算任务,例如大矩阵的乘法,看看CPU和GPU的速度区别。如果是计算量很小的任务呢?
  • 试试CPU和GPU之间传递数据的速度
  • GPU上如何读写模型呢?

2.5.从0开始卷积神经网络

前面讲的把图片拉成了一个向量,行相关像素间信息保留了,但是列信息丢失啦,本节用卷积。
之前的教程里,在输入神经网络前我们将输入图片直接转成了向量。这样做有两个不好的地方:

  • 在图片里相近的像素在向量表示里可能很远,从而模型很难捕获他们的空间关系。
  • 对于大图片输入,模型可能会很大。例如输入是256x256x3的照片(仍然远比手机拍的小),输入层是1000,那么这一层的模型大小是将近1GB.

这一节我们介绍卷积神经网络,其有效了解决了上述两个问题。

卷积神经网络是指主要由卷积层构成的神经网络。
卷积层跟前面的全连接层类似,但输入和权重不是做简单的矩阵乘法,而是使用每次作用在一个窗口上的卷积。下图演示了输入是一个4x4矩阵,使用一个3x3的权重,计算得到2x2结果的过程。每次我们采样一个跟权重一样大小的窗口,让它跟权重做按元素的乘法然后相加。通常我们也是用卷积的术语把这个权重叫kernel或者filter。
img4
我们可以控制如何移动窗口,和在边缘的时候如何填充窗口。下图演示了stride=2pad=1
img5
utils.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# coding=utf-8

from mxnet import ndarray as nd
import mxnet as mx

def SGD(params, lr):
    for param in params:
        param[:] = param - lr * param.grad

def accuracy(output, label):
    return nd.mean(output.argmax(axis=1)==label).asscalar()

def evaluate_accuracy(data_iterator, net, ctx=mx.cpu()):
    acc = 0.
    for data, label in data_iterator:
        output = net(data.as_in_context(ctx))
        acc += accuracy(output, label.as_in_context(ctx))
    return acc / len(data_iterator)

 

cnnscratch.py

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# coding=utf-8

from mxnet import nd
from mxnet import gluon
from mxnet import autograd
from mxnet import image

# 输入输出数据格式是 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))
## kernel为后面两个维度大小 这里是2x2
out = nd.Convolution(data, w, b, kernel=w.shape[2:], num_filter=w.shape[1])

print('input:', data, '\n\nweight:', w, '\n\nbias:', b, '\n\noutput:', out)
## input: [[[[ 0.  1.  2.][ 3.  4.  5.][ 6.  7.  8.]]]]
## weight:[[[[ 0.  1.][ 2.  3.]]]] bias:', [ 1.]
## output:', [[[[ 20.  26.][ 38.  44.]]]]
##计算: 20=0x0+1x1+3x2+4x3+b

# 我们可以控制如何移动窗口,和在边缘的时候如何填充窗口。如stride=2和pad=1。
# stride=(2,2)每次移动两格  pad=(1,1)边缘补充0
out = nd.Convolution(data, w, b, kernel=w.shape[2:], num_filter=w.shape[1],
                     stride=(2,2), pad=(1,1))
print('input:', data, '\n\nweight:', w, '\n\nbias:', b, '\n\noutput:', out)
#输出:[[[[  1.   9.] [ 22.  44.]]]] 因为周围填充了0

#当输入数据有多个通道的时候,每个通道会有对应的权重,然后会对每个通道做卷积之后在通道之间求和
#两个通道情况
w = nd.arange(8).reshape((1,2,2,2))
data = nd.arange(18).reshape((1,2,3,3))
out = nd.Convolution(data, w, b, kernel=w.shape[2:], num_filter=w.shape[0])
print('input:', data, '\n\nweight:', w, '\n\nbias:', b, '\n\noutput:', out)
## ('input:',
# [[[[  0.   1.   2.][  3.   4.   5.][  6.   7.   8.]]
#   [[  9.  10.  11.][ 12.  13.  14.][ 15.  16.  17.]]]]
# weight:[[[[ 0.  1.][ 2.  3.]]  [[ 4.  5.] [ 6.  7.]]]]
# bias:[ 1.]
#output [[[[ 269.  297.] [ 353.  381.]]]]
# 输出为两个通道卷积之后取和

######### 下一层输入需要?
# 当输入需要多通道时,每个输出通道有对应权重,然后每个通道上做卷积。
w = nd.arange(16).reshape((2,2,2,2))
data = nd.arange(18).reshape((1,2,3,3))
b = nd.array([1,2])
out = nd.Convolution(data, w, b, kernel=w.shape[2:], num_filter=w.shape[0])
print('input:', data, '\n\nweight:', w, '\n\nbias:', b, '\n\noutput:', out)
## input: [[[[  0.   1.   2.][  3.   4.   5.][  6.   7.   8.]]
#           [[  9.  10.  11.][ 12.  13.  14.][ 15.  16.  17.]]]]
# weight: [[[[  0.   1.][  2.   3.]][[  4.   5.][  6.   7.]]]
#          [[[  8.   9.][ 10.  11.]][[ 12.  13.][ 14.  15.]]]]
# bias: [ 1.  2.]
# output: [[[[  269.   297.][  353.   381.]]
#          [[  686.   778.] [  962.  1054.]]]]
# 输出为 多个输出(这里为2个是不同weight(这里2个)分别与input卷积得到


#### 池化层(pooling)
# 因为卷积层每次作用在一个窗口,它对位置很敏感。池化层能够很好的缓解这个问题。
# 它跟卷积类似每次看一个小窗口,然后选出窗口里面最大的元素,或者平均元素作为输出。
data = nd.arange(18).reshape((1,2,3,3))
max_pool = nd.Pooling(data=data, pool_type="max", kernel=(2,2)) #kernel=(2,2)每次作2x2矩阵
avg_pool = nd.Pooling(data=data, pool_type="avg", kernel=(2,2)) #max/avg去最大或者平均
print('data:', data, '\n\nmax pooling:', max_pool, '\n\navg pooling:', avg_pool)


####################################
#下面我们可以开始使用这些层构建模型了。

def load_data_fashion_mnist(batch_size, resize=None):
    """download the fashion mnist dataest and then load into memory"""
    def transform_mnist(data, label):
        if resize:
            # resize to resize x resize
            data = image.imresize(data, resize, resize)
        # change data from height x weight x channel to channel x height x weight
        return nd.transpose(data.astype('float32'), (2,0,1))/255, label.astype('float32')
    mnist_train = gluon.data.vision.MNIST(train=True, transform=transform_mnist)
    mnist_test = gluon.data.vision.MNIST(train=False, transform=transform_mnist)
    train_data = gluon.data.DataLoader(mnist_train, batch_size, shuffle=True)
    test_data = gluon.data.DataLoader(mnist_test, batch_size, shuffle=False)
    return (train_data, test_data)
batch_size = 256
train_data, test_data = load_data_fashion_mnist(batch_size)

## 定义模型
# 因为卷积网络计算比全连接要复杂,这里我们默认使用GPU来计算。
# 如果GPU不能用,默认使用CPU。
import mxnet as mx

try:
    ctx = mx.gpu()
    _ = nd.zeros((1,), ctx=ctx)
except:
    ctx = mx.cpu()
print ctx

# 我们使用MNIST常用的LeNet,它有两个卷积层,之后是两个全连接层。
# 注意到我们将权重全部创建在ctx上:
weight_scale = .01

# output channels = 20, kernel = (5,5) ##卷积1
W1 = nd.random_normal(shape=(20,1,5,5), scale=weight_scale, ctx=ctx)
b1 = nd.zeros(W1.shape[0], ctx=ctx)
# output channels = 50, kernel = (3,3)  ##卷积2
W2 = nd.random_normal(shape=(50,20,3,3), scale=weight_scale, ctx=ctx)
b2 = nd.zeros(W2.shape[0], ctx=ctx)
# output dim = 128
W3 = nd.random_normal(shape=(1250, 128), scale=weight_scale, ctx=ctx)
b3 = nd.zeros(W3.shape[1], ctx=ctx)
# output dim = 10
W4 = nd.random_normal(shape=(W3.shape[1], 10), scale=weight_scale, ctx=ctx)
b4 = nd.zeros(W4.shape[1], ctx=ctx)

params = [W1, b1, W2, b2, W3, b3, W4, b4]
for param in params:
    param.attach_grad()

# 卷积模块通常是“卷积层-激活层-池化层”。然后转成2D矩阵输出给后面的全连接层。
def net(X, verbose=False):
    X = X.as_in_context(W1.context) ########数据都放在与W1相同的设备(cpu/gpu)
    # 第一层卷积
    h1_conv = nd.Convolution(data=X, weight=W1, bias=b1, kernel=W1.shape[2:], num_filter=W1.shape[0])
    h1_activation = nd.relu(h1_conv)
    h1 = nd.Pooling(data=h1_activation, pool_type="max", kernel=(2,2), stride=(2,2))
    # 第二层卷积
    h2_conv = nd.Convolution(data=h1, weight=W2, bias=b2, kernel=W2.shape[2:], num_filter=W2.shape[0])
    h2_activation = nd.relu(h2_conv)
    h2 = nd.Pooling(data=h2_activation, pool_type="max", kernel=(2,2), stride=(2,2))
    h = nd.flatten(h2)
    # 第一层全连接
    h3_linear = nd.dot(h, W3) + b3
    h3 = nd.relu(h3_linear)
    # 第二层全连接
    h4_linear = nd.dot(h3, W4) + b4
    if verbose:
        print('1st conv block:', h1.shape)
        print('2nd conv block:', h2.shape)
        print('flatten:', h.shape)
        print('1st dense:', h3.shape)
        print('2nd dense:', h4_linear.shape)
        print('output:', h4_linear)
        # ('1st conv block:', (256L, 20L, 12L, 12L))
        # ('2nd conv block:', (256L, 50L, 5L, 5L))
        # ('flatten:', (256L, 1250L))
        # ('1st dense:', (256L, 128L))
        # ('2nd dense:', (256L, 10L))
        # ('output:', ##256x10
        #  [[-1.40773540e-04 - 3.10097334e-06   2.64148621e-05..., 1.76987160e-04
        #    7.64008873e-05   9.32873518e-05]...]
    return h4_linear

# 测试一下,输出中间结果形状(当然可以直接打印结果)和最终结果。
for data, _ in train_data:
    net(data, verbose=True)
    break

## 训练
# 跟前面没有什么不同的,除了这里我们使用`as_in_context`将`data`和`label`都放置在需要的设备上。
from mxnet import autograd as autograd
from utils import SGD, accuracy, evaluate_accuracy
from mxnet import gluon

softmax_cross_entropy = gluon.loss.SoftmaxCrossEntropyLoss()
learning_rate = .2
for epoch in range(10):
    train_loss = 0.
    train_acc = 0.
    for data, label in train_data:
        label = label.as_in_context(ctx)
        with autograd.record():
            output = net(data)
            loss = softmax_cross_entropy(output, label)
        loss.backward()
        SGD(params, learning_rate/batch_size)

        train_loss += nd.mean(loss).asscalar()
        train_acc += accuracy(output, label)

    test_acc = evaluate_accuracy(test_data, net, ctx)
    print("Epoch %d. Loss: %f, Train acc %f, Test acc %f" % (
        epoch, train_loss/len(train_data),
        train_acc/len(train_data), test_acc))

 

结论
可以看到卷积神经网络比前面的多层感知的分类精度更好。事实上,如果你看懂了这一章,那你基本知道了计算视觉里最重要的几个想法。LeNet早在90年代就提出来了。不管你相信不相信,如果你5年前懂了这个而且开了家公司,那么你很可能现在已经把公司作价几千万卖个某大公司了。幸运的是,或者不幸的是,现在的算法已经更加高级些了,接下来我们会看到一些更加新的想法。

练习

  • 试试改改卷积层设定,例如filter数量,kernel大小
  • 试试把池化层从max改到avg
  • 如果你有GPU,那么尝试用CPU来跑一下看看
  • 你可能注意到比前面的多层感知机慢了很多,那么尝试计算下这两个模型分别需要多少浮点计算。例如nxm和mxk的矩阵乘法需要浮点运算2nmk。

2.6.使用gluon卷积神经网络

utils.py

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
# coding=utf-8

from mxnet import gluon
from mxnet import autograd
from mxnet import nd
from mxnet import image
import mxnet as mx

def SGD(params, lr):
    for param in params:
        param[:] = param - lr * param.grad

def accuracy(output, label):
    return nd.mean(output.argmax(axis=1)==label).asscalar()

def evaluate_accuracy(data_iterator, net, ctx=mx.cpu()):
    acc = 0.
    for data, label in data_iterator:
        output = net(data.as_in_context(ctx))
        acc += accuracy(output, label.as_in_context(ctx))
    return acc / len(data_iterator)

def load_data_fashion_mnist(batch_size, resize=None):
    """download the fashion mnist dataest and then load into memory"""
    def transform_mnist(data, label):
        if resize:
            # resize to resize x resize
            data = image.imresize(data, resize, resize)
        # change data from height x weight x channel to channel x height x weight
        return nd.transpose(data.astype('float32'), (2,0,1))/255, label.astype('float32')
    mnist_train = gluon.data.vision.MNIST(
        train=True, transform=transform_mnist)
    mnist_test = gluon.data.vision.MNIST(
        train=False, transform=transform_mnist)
    train_data = gluon.data.DataLoader(
        mnist_train, batch_size, shuffle=True)
    test_data = gluon.data.DataLoader(
        mnist_test, batch_size, shuffle=False)
    return (train_data, test_data)

def train(train_data, test_data, net, loss, trainer, ctx, num_epochs, print_batches=None):
    """Train a network"""
    for epoch in range(num_epochs):
        train_loss = 0.
        train_acc = 0.
        batch = 0
        for data, label in train_data:
            label = label.as_in_context(ctx)
            with autograd.record():
                output = net(data.as_in_context(ctx))
                L = loss(output, label)
            L.backward()

            trainer.step(data.shape[0])

            train_loss += nd.mean(L).asscalar()
            train_acc += accuracy(output, label)

            batch += 1
            if print_batches and batch % print_batches == 0:
                print("Batch %d. Loss: %f, Train acc %f" % (
                    batch, train_loss/batch, train_acc/batch
                ))

        test_acc = evaluate_accuracy(test_data, net, ctx)
        print("Epoch %d. Loss: %f, Train acc %f, Test acc %f" % (
            epoch, train_loss/batch, train_acc/batch, test_acc
        ))

 

cnngluon.py

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
# coding=utf-8

from mxnet.gluon import nn
import mxnet as mx
from mxnet import image
from mxnet import gluon
from mxnet import autograd
import utils

# 定义模型
# 下面是LeNet在Gluon里的实现,注意到我们不再需要实现去计算每层的输入大小,
# 尤其是接在卷积后面的那个全连接层。
net = nn.Sequential()
with net.name_scope():
    net.add(
        nn.Conv2D(channels=20, kernel_size=5, activation='relu'),
        nn.MaxPool2D(pool_size=2, strides=2),
        nn.Conv2D(channels=50, kernel_size=3, activation='relu'),
        nn.MaxPool2D(pool_size=2, strides=2),
        nn.Flatten(),
        nn.Dense(128, activation="relu"),
        nn.Dense(10)
    )

## 获取数据和训练
# 剩下的跟上一章没什么不同,我们重用`utils.py`里定义的函数。
# 初始化
ctx = mx.gpu()
net.initialize(ctx=ctx)
print('initialize weight on', ctx)

# 获取数据
batch_size = 256
train_data, test_data = utils.load_data_fashion_mnist(batch_size)

# 训练
loss = gluon.loss.SoftmaxCrossEntropyLoss()
trainer = gluon.Trainer(net.collect_params(),
                        'sgd', {'learning_rate': 0.1})
utils.train(train_data, test_data, net, loss,
            trainer, ctx, num_epochs=10)

 

3.1.创建神经网络

前面的教程我们教了大家如何实现线性回归,多类Logistic回归和多层感知机。我们既展示了如何从0开始实现,也提供使用gluon的更紧凑的实现。因为前面我们主要关注在模型本身,所以只解释了如何使用gluon,但没说明他们是如何工作的。我们使用了nn.Sequential,它是nn.Block的一个简单形式,但没有深入了解它们。
本教程和接下来几个教程,我们将详细解释如何使用这两个类来定义神经网络、初始化参数、以及保存和读取模型。
我们重新把使用Gluon多层感知机里的网络定义搬到这里作为开始的例子(为了简单起见,这里我们丢掉了Flatten层)。

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# coding=utf-8

from mxnet import nd
from mxnet.gluon import nn

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

print(net)

## 使用 `nn.Block` 来定义
#事实上,`nn.Sequential`是`nn.Block`的简单形式。
# 我们先来看下如何使用`nn.Block`来实现同样的网络。
## 使用nn.block定义更加灵活
# 可以看到`nn.Block`的使用是通过创建一个它子类的类,其中至少包含了两个函数。
# `__init__`:创建参数。上面例子我们使用了包含了参数的`dense`层
# `forward()`:定义网络的计算
class MLP(nn.Block):## 定义MLP为nn.block的一个子class
    def __init__(self, **kwargs):##初始化函数 self相当与自己的一个class
        super(MLP, self).__init__(**kwargs)##调用父类nn.block的初始化函数
        with self.name_scope():
            self.dense0 = nn.Dense(256) ##创建dense layer
            self.dense1 = nn.Dense(10)

    # 注意:这里必须为foreard函数,不能改 调用forward会自动调用backward求导
    def forward(self, x):##创建一个forward函数  输入x时如下操作
        return self.dense1(nd.relu(self.dense0(x)))

# 我们所创建的类的使用跟前面`net`没有太多不一样。
net2 = MLP()
print(net2)
net2.initialize()
x = nd.random.uniform(shape=(4,20))
y = net2(x)
print y

# 如何定义创建和使用`nn.Dense`比较好理解。接下来我们仔细看下`MLP`里面用的其他命令:
# `super(MLP, self).__init__(**kwargs)`:这句话调用`nn.Block`的 `__init__`函数,
# 它提供了`prefix`(指定名字)和`params`(指定模型参数)两个参数。我们会之后详细解释如何使用。
# `self.name_scope()`:调用`nn.Block`提供的`name_scope()`函数。
# `nn.Dense`的定义放在这个`scope`里面。
# 它的作用是给里面的所有层和参数的名字加上前缀(prefix)使得他们在系统里面
# 独一无二。默认自动会自动生成前缀,我们也可以在创建的时候手动指定。

print('default prefix:', net2.dense0.name) #('default prefix:', 'mlp0_dense0')
net3 = MLP(prefix='another_mlp_')
print('customized prefix:', net3.dense0.name) #('customized prefix:', 'another_mlp_dense0')

# 大家会发现这里并没有定义如何求导,或者是`backward()`函数。
# 事实上,系统会使用`autograd`对`forward()`自动生成对应的`backward()`函数。
# 在`gluon`里,`nn.Block`是一个一般化的部件。整个神经网络可以是一个`nn.Block`,单个层也是一个`nn.Block`。我们可以(近似)无限地嵌套`nn.Block`来构建新的`nn.Block`。
# `nn.Block`主要提供这个东西
# 1. 存储参数
# 2. 描述`forward`如何执行
# 3. 自动求导

## 那么现在可以解释`nn.Sequential`了吧
# `nn.Sequential`是一个`nn.Block`容器,它通过`add`来添加`nn.Block`。
# 它自动生成`forward()`函数,其就是把加进来的`nn.Block`逐一运行。
# 一个简单的实现是这样的:
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.Sequential`一样的使用这个自定义的类:
net4 = Sequential()
with net4.name_scope():
    net4.add(nn.Dense(256, activation="relu"))
    net4.add(nn.Dense(10))

net4.initialize()
y = net4(x)
print y

# 可以看到,`nn.Sequential`的主要好处是定义网络起来更加简单。
# 但`nn.Block`可以提供更加灵活的网络定义。考虑下面这个例子
class FancyMLP(nn.Block):
    def __init__(self, **kwargs):
        super(FancyMLP, self).__init__(**kwargs)
        with self.name_scope():
            self.dense = nn.Dense(256)
            self.weight = nd.random_uniform(shape=(256,20))##创建weight

    def forward(self, x):
        x = nd.relu(self.dense(x))
        x = nd.relu(nd.dot(x, self.weight)+1) ##手动添加一个乘法
        x = nd.relu(self.dense(x))
        return x

# 看到这里我们直接手动创建和初始了权重`weight`,并重复用了`dense`的层。测试一下:
fancy_mlp = FancyMLP()
fancy_mlp.initialize()
y = fancy_mlp(x)
print(y.shape)##(4L, 256L)

## `nn.Block`和`nn.Sequential`的嵌套使用
# 现在我们知道了`nn`下面的类基本都是`nn.Block`的子类,他们可以很方便地嵌套使用。
class RecMLP(nn.Block):
    def __init__(self, **kwargs):
        super(RecMLP, self).__init__(**kwargs)
        self.net = nn.Sequential()
        with self.name_scope():
            self.net.add(nn.Dense(256, activation="relu"))
            self.net.add(nn.Dense(128, activation="relu"))
            self.dense = nn.Dense(64)

    def forward(self, x):
        return nd.relu(self.dense(self.net(x)))

rec_mlp = nn.Sequential()
rec_mlp.add(RecMLP())
rec_mlp.add(nn.Dense(10))
print(rec_mlp)

 

3.2.初始化模型参数

我们仍然用MLP这个例子来详细解释如何初始化模型参数。

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# coding=utf-8

from mxnet.gluon import nn
from mxnet import nd

def get_net():
    net = nn.Sequential()
    with net.name_scope():
        net.add(nn.Dense(4, activation="relu"))
        net.add(nn.Dense(2))
    return net

x = nd.random.uniform(shape=(3,5))

# # 我们知道如果不`initialize()`直接跑forward,那么系统会抱怨说参数没有初始化。
# import sys
# try:
#     net = get_net()
#     net(x)
# except RuntimeError as err:
#     sys.stderr.write(str(err))
# ## Parameter sequential0_dense0_bias has not been initialized.

# 正确的打开方式是这样
net = get_net()
net.initialize()
print net(x)

## 访问模型参数
# 之前我们提到过可以通过`weight`和`bias`访问`Dense`的参数,他们是`Parameter`这个类:
w = net[0].weight
b = net[0].bias
print('name: ', net[0].name, '\nweight: ', w, '\nbias: ', b)

# 然后我们可以通过`data`来访问参数,`grad`来访问对应的梯度
print('weight:', w.data()) ##w的数值
print('weight gradient', w.grad()) #w的梯度
print('bias:', b.data())
print('bias gradient', b.grad())

# 我们也可以通过`collect_params`来访问Block里面所有的参数(这个会包括所有的子Block)。
# 它会返回一个名字到对应Parameter的dict。既可以用正常`[]`来访问参数,也可以用`get()`,它不需要填写名字的前缀。
params = net.collect_params()
print(params)
print(params['sequential0_dense0_bias'].data())
print(params.get('dense0_weight').data())

## 使用不同的初始函数来初始化
# 我们一直在使用默认的`initialize`来初始化权重(除了指定GPU `ctx`外)。它会把所有权重初始化成在`[-0.07, 0.07]`之间均匀分布的随机数。
# 我们可以使用别的初始化方法。例如使用均值为0,方差为0.02的正态分布
from mxnet import init
params.initialize(init=init.Normal(sigma=0.02), force_reinit=True) ##为了防止误操作初始化覆盖参数 需要force_reinit=True强制初始化
print(net[0].weight.data(), net[0].bias.data())

# 看得更加清楚点:  全部初始化为1
params.initialize(init=init.One(), force_reinit=True)
print(net[0].weight.data(), net[0].bias.data())
# [[ 1.  1.  1.  1.  1.]
#  [ 1.  1.  1.  1.  1.]
#  [ 1.  1.  1.  1.  1.]
#  [ 1.  1.  1.  1.  1.]]
# [ 0.  0.  0.  0.]

# 更多的方法参见[init的API](https://mxnet.incubator.apache.org/api/python/optimization.html#the-mxnet-initializer-package).
# 下面我们自定义一个初始化方法。
class MyInit(init.Initializer):
    def __init__(self):
        super(MyInit, self).__init__()
        self._verbose = True
    def _init_weight(self, _, arr):
        # 初始化权重,使用out=arr后我们不需指定形状
        print('init weight', arr.shape)
        nd.random.uniform(low=5, high=10, out=arr)
    def _init_bias(self, _, arr):
        print('init bias', arr.shape)
        # 初始化偏移
        arr[:] = 2

# FIXME: init_bias doesn't work
params.initialize(init=MyInit(), force_reinit=True)
print(net[0].weight.data(), net[0].bias.data())

## 延后的初始化
# 我们之前提到过Gluon的一个便利的地方是模型定义的时候不需要指定输入的大小,在之后做forward的时候会自动推测参数的大小。我们具体来看这是怎么工作的。
# 新创建一个网络,然后打印参数。你会发现两个全连接层的权重的形状里都有0。 这是因为在不知道输入数据的情况下,我们无法判断它们的形状。
net = get_net()
print(net.collect_params())
# Parameter sequential1_dense0_weight(shape=(4, 0), dtype= < type 'numpy.float32' >)
# Parameter sequential1_dense0_bias(shape=(4,), dtype= < type 'numpy.float32' >)
# Parameter sequential1_dense1_weight(shape=(2, 0), dtype= < type 'numpy.float32' >)
# Parameter sequential1_dense1_bias(shape=(2,), dtype= < type 'numpy.float32' >)

# 然后我们初始化
net.initialize(init=MyInit())
# 你会看到我们并没有看到MyInit打印的东西,这是因为我们仍然不知道形状。真正的初始化发生在我们看到数据时。
print net(x)
# 这时候我们看到shape里面的0被填上正确的值了。
print(net.collect_params())
# Parameter sequential1_dense0_weight(shape=(4L, 5L), dtype= < type 'numpy.float32' >)
# Parameter sequential1_dense0_bias(shape=(4L,), dtype= < type 'numpy.float32' >)
# Parameter sequential1_dense1_weight(shape=(2L, 4L), dtype= < type 'numpy.float32' >)
# Parameter sequential1_dense1_bias(shape=(2L,), dtype= < type 'numpy.float32' >)

## 避免延后初始化
# 有时候我们不想要延后初始化,这时候可以在创建网络的时候指定输入大小。
net = nn.Sequential()
with net.name_scope():
    net.add(nn.Dense(4, in_units=5, activation="relu")) ##输入input 5
    net.add(nn.Dense(2, in_units=4))

net.initialize(MyInit())
# ('init weight', (4L, 5L))
# ('init weight', (2L, 4L))


## 共享模型参数
# 有时候我们想在层之间共享同一份参数,我们可以通过Block的`params`输出参数来手动指定参数,而不是让系统自动生成。
net = nn.Sequential()
with net.name_scope():
    net.add(nn.Dense(4, in_units=4, activation="relu"))
    ## 参数与最后一层共用 即与上一层参数相同
    net.add(nn.Dense(4, in_units=4, activation="relu", params=net[-1].params)) #net[-1].params net最后一层的参数
    net.add(nn.Dense(2, in_units=4))

# 初始化然后打印
net.initialize(MyInit())
## 如下两个参数相同
print(net[0].weight.data())
print(net[1].weight.data())

 

我们可以很灵活地访问和修改模型参数。

练习

  1. 研究下net.collect_params()返回的是什么?net.params呢?
  2. 如何对每个层使用不同的初始化函数
  3. 如果两个层共用一个参数,那么求梯度的时候会发生什么?

3.3.序列化读写模型

但即使知道了所有这些,我们还没有完全准备好来构建一个真正的机器学习系统。这是因为我们还没有讲如何读和写模型。因为现实中,我们通常在一个地方训练好模型,然后部署到很多不同的地方。我们需要把内存中的训练好的模型存在硬盘上好下次使用。

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
# coding=utf-8

## 读写NDArrays
# 作为开始,我们先看看如何读写NDArray。虽然我们可以使用Python的序列化包例如`Pickle`,不过我们更倾向直接`save`和`load`,
# 通常这样更快,而且别的语言,例如R和Scala也能用到。
from mxnet import nd

x = nd.ones(3)
y = nd.zeros(4)
filename = "../data/test1.params"
nd.save(filename, [x, y])

# 读回来
a, b = nd.load(filename)
print(a, b)

# 不仅可以读写单个NDArray,NDArray list,dict也是可以的:
mydict = {"x": x, "y": y}
filename = "../data/test2.params"
nd.save(filename, mydict)
c = nd.load(filename)
print(c)

## 读写Gluon模型的参数
# 跟NDArray类似,Gluon的模型(就是`nn.Block`)提供便利的`save_params`和`load_params`函数来读写数据。
# 我们同前一样创建一个简单的多层感知机
from mxnet.gluon import nn

def get_net():
    net = nn.Sequential()
    with net.name_scope():
        net.add(nn.Dense(10, activation="relu"))
        net.add(nn.Dense(2))
    return net

net = get_net()
net.initialize()
x = nd.random.uniform(shape=(2,10))
print(net(x))
#[[ 0.00205935 -0.00979935]
 # [ 0.00107034 -0.00423382]]

# 下面我们把模型参数存起来
filename = "../data/mlp.params"
net.save_params(filename)

# 之后我们构建一个一样的多层感知机,但不像前面那样随机初始化,我们直接读取前面的模型参数。
# 这样给定同样的输入,新的模型应该会输出同样的结果。
import mxnet as mx
net2 = get_net()
net2.load_params(filename, mx.cpu())  # FIXME, gluon will support default ctx later
print(net2(x))
#[[ 0.00205935 -0.00979935]
 # [ 0.00107034 -0.00423382]]

 

通过load_paramssave_params可以很方便的读写模型参数。

3.4.设计自定义层

神经网络的一个魅力是它有大量的层,例如全连接、卷积、循环、激活,和各式花样的连接方式。我们之前学到了如何使用Gluon提供的层来构建新的层(nn.Block)继而得到神经网络。虽然Gluon提供了大量的层的定义,但我们仍然会遇到现有层不够用的情况。

这时候的一个自然的想法是,我们不是学习了如何只使用基础数值运算包NDArray来实现各种的模型吗?它提供了大量的底层计算函数足以实现即使不是100%那也是95%的神经网络吧。

但每次都从头写容易写到怀疑人生。实际上,即使在纯研究的领域里,我们也很少发现纯新的东西,大部分时候是在现有模型的基础上做一些改进。所以很可能大部分是可以沿用前面的而只有一部分是需要自己来实现。

这个教程我们将介绍如何使用底层的NDArray接口来实现一个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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# coding=utf-8

# 我们先来看如何定义一个简单层,它不需要维护模型参数。事实上这个跟前面介绍的如何使用nn.Block没什么区别。
# 下面代码定义一个层将输入减掉均值。

from mxnet import 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()
print layer(nd.array([1,2,3,4,5])) ##[-2. -1.  0.  1.  2.]

# 我们也可以用它来构造更复杂的神经网络:
net = nn.Sequential()
with net.name_scope():
    net.add(nn.Dense(128))
    net.add(nn.Dense(10))
    net.add(CenteredLayer())
# 确认下输出的均值确实是0:
net.initialize()
y = net(nd.random.uniform(shape=(4, 8)))
print y.mean() ##[  2.32830647e-11] (因为用的浮点32为约等于0)

## 带模型参数的自定义层
# 虽然`CenteredLayer`可能会告诉实现自定义层大概是什么样子,但它缺少了重要的一块,就是它没有可以学习的模型参数。
# 记得我们之前访问`Dense`的权重的时候是通过`dense.weight.data()`,这里`weight`是一个`Parameter`的类型。
# 我们可以显示的构建这样的一个参数。
from mxnet import gluon
my_param = gluon.Parameter("exciting_parameter_yay", shape=(3,3))
# 这里我们创建一个3x3大小的参数并取名为"exciting_parameter_yay"。然后用默认方法初始化打印结果。
my_param.initialize()
print (my_param.data(), my_param.grad())
# [[ 0.02332029  0.04696382  0.03078182]
#  [ 0.00755873  0.03193929 -0.0059346 ]
#  [-0.00809445  0.01710822 -0.03057443]]
# [[ 0.  0.  0.]
#  [ 0.  0.  0.]
#  [ 0.  0.  0.]]

# 通常自定义层的时候我们不会直接创建Parameter,而是用过Block自带的一个ParamterDict类型的成员变量`params`,顾名思义,
# 这是一个由字符串名字映射到Parameter的字典。
pd = gluon.ParameterDict(prefix="block1_")
pd.get("exciting_parameter_yay", shape=(3,3))
print pd
#block1_ (Parameter block1_exciting_parameter_yay (shape=(3, 3), dtype=<type 'numpy.float32'>))

# 现在我们看下如果如果实现一个跟`Dense`一样功能的层,它概念跟前面的`CenteredLayer`的主要区别是我们
# 在初始函数里通过`params`创建了参数:
class MyDense(nn.Block):
    def __init__(self, units, in_units, **kwargs):#in_units输入大小 units输出大小
        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() ##WX+b
        return nd.relu(linear)

# 我们创建实例化一个对象来看下它的参数,这里我们特意加了前缀`prefix`,这是`nn.Block`初始化函数自带的参数。
dense = MyDense(5, in_units=10, prefix='o_my_dense_')
print dense.params

# 它的使用跟前面没有什么不一致:
dense.initialize()
print dense(nd.random.uniform(shape=(2,10))) #batch=2 长度=10输入 输出为batch不变 长度=5
# [[ 0.          0.          0.05519049  0.01345633  0.07244172]
#  [ 0.          0.          0.06741175  0.01634707  0.0257601 ]]

# 我们构造的层跟Gluon提供的层用起来没太多区别:
net = nn.Sequential()
with net.name_scope():
    net.add(MyDense(32, in_units=64))
    net.add(MyDense(2, in_units=32))
net.initialize()
print net(nd.random.uniform(shape=(2,64)))
# [[ 0.          0.        ]
#  [ 0.02434103  0.        ]]

 

仔细的你可能还是注意到了,我们这里指定了输入的大小,而Gluon自带的Dense则无需如此。我们已经在前面节介绍过了这个延迟初始化如何使用。但如果实现一个这样的层我们将留到后面介绍了hybridize后。
现在我们知道了如何把前面手写过的层全部包装了Gluon能用的Block,之后再用到的时候就可以飞起来了!

练习

  1. 怎么修改自定义层里参数的默认初始化函数。
  2. (这个比较难),在一个代码Cell里面输入nn.Dense??,看看它是怎么实现的。为什么它就可以支持延迟初始化了。

3.5.dropout

丢弃法(Dropout)— 从0开始
前面我们介绍了多层神经网络,就是包含至少一个隐含层的网络。我们也介绍了正则法来应对过拟合问题。在深度学习中,一个常用的应对过拟合问题的方法叫做丢弃法(Dropout)。本节以多层神经网络为例,从0开始介绍丢弃法。
由于丢弃法的概念和实现非常容易,在本节中,我们先介绍丢弃法的概念以及它在现代神经网络中是如何实现的。然后我们一起探讨丢弃法的本质。

丢弃法的概念
在现代神经网络中,我们所指的丢弃法,通常是对输入层或者隐含层做以下操作:

  • 随机选择一部分该层的输出作为丢弃元素;
  • 把丢弃元素乘以0;
  • 把非丢弃元素拉伸。

丢弃法的本质
了解了丢弃法的概念与实现,那你可能对它的本质产生了好奇。
如果你了解集成学习,你可能知道它在提升弱分类器准确率上的威力。一般来说,在集成学习里,我们可以对训练数据集有放回地采样若干次并分别训练若干个不同的分类器;测试时,把这些分类器的结果集成一下作为最终分类结果。
事实上,丢弃法在模拟集成学习。试想,一个使用了丢弃法的多层神经网络本质上是原始网络的子集(节点和边)。举个例子,它可能长这个样子。
img

丢弃法的实现

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# coding=utf-8

# 丢弃法的实现很容易,例如像下面这样。这里的标量`drop_probability`
# 定义了一个`X`(`NDArray`类)中任何一个元素被丢弃的概率。-----元素置0
from mxnet import nd

def dropout(X, drop_probability):
    keep_probability = 1 - drop_probability
    assert 0 <= keep_probability <= 1
    # 这种情况下把全部元素都丢弃,全部元素置0。
    if keep_probability == 0:
        return X.zeros_like()

    # 随机选择一部分该层的输出作为丢弃元素。
    ##随机产生产生0~1之间的数,小于keep_probability变1 大于keep_probability变0 之后乘以X就会随机将某些值变0
    mask=nd.random.uniform(0, 1.0, X.shape, ctx=X.context)>1-keep_probability  #>1-keep_probability等效于小于keep_probability
	# 保证 E[dropout(X)] == X 期望值不变
    scale = 1 / keep_probability
    return mask * X * scale

# 我们运行几个实例来验证一下。
A = nd.arange(8).reshape((2,4))
print dropout(A, 0.0) #[[ 0.  1.  2.  3.][ 4.  5.  6.  7.]]
print dropout(A, 0.5) #[[  0.   2.   4.   6.][  8.  10.   0.   0.]]
print dropout(A, 1.0) # [[ 0.  0.  0.  0.] [ 0.  0.  0.  0.]]

# 我们在训练神经网络模型时一般随机采样一个批量的训练数据。丢弃法实质上是对每一个这样的数据集分别训练一个原神经网络子集的分类器。
# 与一般的集成学习不同,这里每个原神经网络子集的分类器用的是同一套参数。因此丢弃法只是在模拟集成学习。
# 我们刚刚强调了,原神经网络子集的分类器在不同的训练数据批量上训练并使用同一套参数。因此,使用丢弃法的神经网络实质上是对输入层和
# 隐含层的参数做了正则化:学到的参数使得原神经网络不同子集在训练数据上都尽可能表现良好。
# 下面我们动手实现一下在多层神经网络里加丢弃层。

## 数据获取
import utils
batch_size = 256
train_data, test_data = utils.load_data_fashion_mnist(batch_size)

## 含两个隐藏层的多层感知机
# 这里我们定义一个包含两个隐含层的模型,两个隐含层都输出256个节点。我们定义激活函数Relu并直接使用Gluon提供的交叉熵损失函数。
num_inputs = 28*28
num_outputs = 10
num_hidden1 = 256
num_hidden2 = 256
weight_scale = .01

W1 = nd.random_normal(shape=(num_inputs, num_hidden1), scale=weight_scale)
b1 = nd.zeros(num_hidden1)
W2 = nd.random_normal(shape=(num_hidden1, num_hidden2), scale=weight_scale)
b2 = nd.zeros(num_hidden2)
W3 = nd.random_normal(shape=(num_hidden2, num_outputs), scale=weight_scale)
b3 = nd.zeros(num_outputs)
params = [W1, b1, W2, b2, W3, b3]
for param in params:
    param.attach_grad()

## 定义包含丢弃层的模型
# 我们的模型就是将层(全连接)和激活函数(Relu)串起来,并在应用激活函数后添加丢弃层。每个丢弃层的元素丢弃概率可以分别设置。
# 一般情况下,我们推荐把更靠近输入层的元素丢弃概率设的更小一点。这个试验中,我们把第一层全连接后的元素丢弃概率设为0.2,
# 把第二层全连接后的元素丢弃概率设为0.5。
drop_prob1 = 0.1
drop_prob2 = 0.4

def net(X):
    X = X.reshape((-1, num_inputs))
    # 第一层全连接。
    h1 = nd.relu(nd.dot(X, W1) + b1)
    # 在第一层全连接后添加丢弃层。
    h1 = dropout(h1, drop_prob1)
    # 第二层全连接。
    h2 = nd.relu(nd.dot(h1, W2) + b2)
    # 在第二层全连接后添加丢弃层。
    h2 = dropout(h2, drop_prob2)
    return nd.dot(h2, W3) + b3

## 训练
from mxnet import autograd
from mxnet import gluon

softmax_cross_entropy = gluon.loss.SoftmaxCrossEntropyLoss()
learning_rate = .5
for epoch in range(5):
    train_loss = 0.
    train_acc = 0.
    for data, label in train_data:
        with autograd.record():
            output = net(data)
            loss = softmax_cross_entropy(output, label)
        loss.backward()
        utils.SGD(params, learning_rate/batch_size)

        train_loss += nd.mean(loss).asscalar()
        train_acc += utils.accuracy(output, label)

    test_acc = utils.evaluate_accuracy(test_data, net)
    print("Epoch %d. Loss: %f, Train acc %f, Test acc %f" % (
        epoch, train_loss/len(train_data),
        train_acc/len(train_data), test_acc)

 

我们可以通过使用丢弃法对神经网络正则化。
练习

  • 尝试不使用丢弃法,看看这个包含两个隐含层的多层感知机可以得到什么结果。
  • 我们推荐把更靠近输入层的元素丢弃概率设的更小一点。想想这是为什么?如果把本节教程中的两个元素丢弃参数对调会有什么结果?

3.6.使用Gluon丢弃法(Dropout)

本章介绍如何使用Gluon在训练和测试深度学习模型中使用丢弃法(Dropout)。
定义模型并添加丢弃层
有了Gluon,我们模型的定义工作变得简单了许多。我们只需要在全连接层后添加gluon.nn.Dropout层并指定元素丢弃概率。一般情况下,我们推荐把
更靠近输入层的元素丢弃概率设的更小一点。这个试验中,我们把第一层全连接后的元素丢弃概率设为0.2,把第二层全连接后的元素丢弃概率设为0.5。

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
# coding=utf-8

from mxnet.gluon import nn

net = nn.Sequential()
drop_prob1 = 0.2
drop_prob2 = 0.5

with net.name_scope():
    net.add(nn.Flatten())
    # 第一层全连接。
    net.add(nn.Dense(256, activation="relu"))
    # 在第一层全连接后添加丢弃层。
    net.add(nn.Dropout(drop_prob1))########通过`Gluon`我们可以更方便地构造多层神经网络并使用丢弃法
    # 第二层全连接。
    net.add(nn.Dense(256, activation="relu"))
    # 在第二层全连接后添加丢弃层。
    net.add(nn.Dropout(drop_prob2))
    net.add(nn.Dense(10))
net.initialize()

## 读取数据并训练
import utils
from mxnet import nd
from mxnet import autograd
from mxnet import gluon

batch_size = 256
train_data, test_data = utils.load_data_fashion_mnist(batch_size)
softmax_cross_entropy = gluon.loss.SoftmaxCrossEntropyLoss()
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.5})

for epoch in range(5):
    train_loss = 0.
    train_acc = 0.
    for data, label in train_data:
        with autograd.record():
            output = net(data)
            loss = softmax_cross_entropy(output, label)
        loss.backward()
        trainer.step(batch_size)

        train_loss += nd.mean(loss).asscalar()
        train_acc += utils.accuracy(output, label)

    test_acc = utils.evaluate_accuracy(test_data, net)
    print("Epoch %d. Loss: %f, Train acc %f, Test acc %f" % (
        epoch, train_loss/len(train_data),
        train_acc/len(train_data), test_acc))

 

效果比3.5好!

3.7.深度卷积神经网络和AlexNet

原版的AlexNet有每层大小为4096个节点的全连接层们。这两个巨大的全连接层带来将近1GB的模型大小。由于早期GPU显存的限制,最早的AlexNet包括了双数据流的设计,以让网络中一半的节点能存入一个GPU。这两个数据流,也就是说两个GPU只在一部分层进行通信,这样达到限制GPU同步时的额外开销的效果。有幸的是,GPU在过去几年得到了长足的发展,除了一些特殊的结构外,我们也就不再需要这样的特别设计了。
下面的Gluon代码定义了(稍微简化过的)Alexnet

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
# coding=utf-8

from mxnet.gluon import nn

net = nn.Sequential()
with net.name_scope():
    net.add(
        # 第一阶段
        nn.Conv2D(channels=96, kernel_size=11,strides=4, activation='relu'),
        nn.MaxPool2D(pool_size=3, strides=2),
        # 第二阶段
        nn.Conv2D(channels=256, kernel_size=5,padding=2, activation='relu'),
        nn.MaxPool2D(pool_size=3, strides=2),
        # 第三阶段
        nn.Conv2D(channels=384, kernel_size=3,padding=1, activation='relu'),
        nn.Conv2D(channels=384, kernel_size=3,padding=1, activation='relu'),
        nn.Conv2D(channels=256, kernel_size=3,padding=1, activation='relu'),
        nn.MaxPool2D(pool_size=3, strides=2),
        # 第四阶段
        nn.Flatten(),
        nn.Dense(4096, activation="relu"),
        nn.Dropout(.5),
        # 第五阶段
        nn.Dense(4096, activation="relu"),
        nn.Dropout(.5),
        # 第六阶段
        nn.Dense(10) ##当时是1000
    )

## 读取数据
# Alexnet使用Imagenet数据,其中输入图片大小一般是224x224。因为Imagenet数据训练时间过长,
# 我们还是用前面的MNIST来演示。读取数据的时候我们额外做了一步将数据扩大到原版Alexnet使用的224x224。
import utils
train_data, test_data = utils.load_data_fashion_mnist(batch_size=64, resize=224)

## 训练
# 这时候我们可以开始训练。相对于前面的LeNet,我们做了如下三个改动:
# 1. 我们使用`Xavier`来初始化参数
# 2. 使用了更小的学习率
from mxnet import init
from mxnet import gluon
import mxnet as mx

ctx = mx.gpu()
net.initialize(ctx=ctx, init=init.Xavier()) ##初始化用了Xavier()而不是默认的随机初始化

loss = gluon.loss.SoftmaxCrossEntropyLoss()
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.01})
utils.train(train_data, test_data, net, loss,trainer, ctx, num_epochs=5)

 

效果比以前的好些!

3.8.VGGNet

我们从Alexnet看到网络的层数的激增。这个意味着即使是用Gluon手动写代码一层一层的堆每一层也很麻烦,更不用说从0开始了。幸运的是编程语言提供了很好的方法来解决这个问题:函数和循环。如果网络结构里面有大量重复结构,那么我们可以很紧凑来构造这些网络。第一个使用这种结构的深度网络是VGG。

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# coding=utf-8

# VGG的一个关键是使用很多有着相对小的kernel(3x3)的卷积层然后接上一个池化层,
# 之后再将这个模块重复多次。下面我们先定义一个这样的块

from mxnet.gluon import nn

def vgg_block(num_convs, channels):
    out = nn.Sequential()
    for _ in range(num_convs):
        out.add(nn.Conv2D(channels=channels, kernel_size=3,padding=1, activation='relu'))
    out.add(nn.MaxPool2D(pool_size=2, strides=2))
    return out

# 我们实例化一个这样的块,里面有两个卷积层,每个卷积层输出通道是128:
from mxnet import nd

blk = vgg_block(2, 128)
blk.initialize()
x = nd.random.uniform(shape=(2,3,16,16))
y = blk(x)
print y.shape  ##(2L, 128L, 8L, 8L)

# 可以看到经过一个这样的块后,长宽会减半,通道也会改变。然后我们定义如何将这些块堆起来:
def vgg_stack(architecture):
    out = nn.Sequential()
    for (num_convs, channels) in architecture:
        out.add(vgg_block(num_convs, channels))
    return out

# 这里我们定义一个最简单的一个VGG结构,它有8个卷积层,和跟Alexnet一样的3个全连接层。这个网络又称VGG 11.
num_outputs = 10
architecture = ((1,64), (1,128), (2,256), (2,512), (2,512))
net = nn.Sequential()
with net.name_scope():
    net.add(
        vgg_stack(architecture),
        nn.Flatten(),
        nn.Dense(4096, activation="relu"),
        nn.Dropout(.5),
        nn.Dense(4096, activation="relu"),
        nn.Dropout(.5),
        nn.Dense(num_outputs))
print net
# Sequential(
#   (0): Sequential(
#     (0): Sequential(
#       (0): Conv2D(64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
#       (1): MaxPool2D(size=(2, 2), stride=(2, 2), padding=(0, 0), ceil_mode=False)
#     )
#     (1): Sequential(
#       (0): Conv2D(128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
#       (1): MaxPool2D(size=(2, 2), stride=(2, 2), padding=(0, 0), ceil_mode=False)
#     )
#     (2): Sequential(
#       (0): Conv2D(256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
#       (1): Conv2D(256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
#       (2): MaxPool2D(size=(2, 2), stride=(2, 2), padding=(0, 0), ceil_mode=False)
#     )
#     (3): Sequential(
#       (0): Conv2D(512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
#       (1): Conv2D(512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
#       (2): MaxPool2D(size=(2, 2), stride=(2, 2), padding=(0, 0), ceil_mode=False)
#     )
#     (4): Sequential(
#       (0): Conv2D(512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
#       (1): Conv2D(512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
#       (2): MaxPool2D(size=(2, 2), stride=(2, 2), padding=(0, 0), ceil_mode=False)
#     )
#   )
#   (1): Flatten
#   (2): Dense(4096, Activation(relu))
#   (3): Dropout(p = 0.5)
#   (4): Dense(4096, Activation(relu))
#   (5): Dropout(p = 0.5)
#   (6): Dense(10, linear)
# )

## 模型训练
# 这里跟Alexnet的训练代码一样除了我们只将图片扩大到96x96来节省些计算,和默认使用稍微大点的学习率。
import utils
from mxnet import gluon
from mxnet import init
import mxnet as mx

train_data, test_data = utils.load_data_fashion_mnist(batch_size=64, resize=96)
ctx = mx.gpu()
net.initialize(ctx=ctx, init=init.Xavier())
loss = gluon.loss.SoftmaxCrossEntropyLoss()
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.05})
utils.train(train_data, test_data, net, loss,trainer, ctx, num_epochs=1)

 

#通过使用重复的元素,我们可以通过循环和函数来定义模型。使用不同的配置(architecture)可以得到一系列不同的模型。
练习

  • 尝试多跑几轮,看看跟LeNet/Alexnet比怎么样?
  • 尝试下构造VGG其他常用模型,例如VGG16, VGG19. (提示:可以参考VGG论文里的表1。)
  • 把图片从默认的224x224降到96x96有什么影响?

4.1. 从0开始批量归一化BatchNorm

在实际应用中,我们通常将输入数据的每个样本或者每个特征进行归一化,就是将均值变为0方差变为1,来使得数值更稳定。
它对很深的神经网络能够训练,对learningrate不那么敏感。
批量归一化对每层都归一化。
批量归一化试图对深度学习模型的某一层所使用的激活函数的输入进行归一化:使批量呈标准正态分布(均值为0,标准差为1)。
批量归一化通常应用于输入层或任意中间层。

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# coding=utf-8

# 我们现在来动手实现一个简化的批量归一化层。实现时对全连接层和二维卷积层两种情况做了区分。
# 对于全连接层,很明显我们要对每个批量进行归一化。然而这里需要注意的是,对于二维卷积,我们要对每个通道进行归一化,
# 并需要保持四维形状使得可以正确地广播。
#均值变0 方差变1
from mxnet import nd
import mxnet as mx
def pure_batch_norm(X, gamma, beta, eps=1e-5):
    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) ##rgb的话通道为3

    # 均一化
    X_hat = (X - mean) / nd.sqrt(variance + eps)
    # reshape 拉升和偏移
    ##Y=gamma*X(均一化后的数据)+beta 不想归一化时可以利用训练的gamma,beta还原数据
    return gamma.reshape(mean.shape) * X_hat + beta.reshape(mean.shape)

X=mx.ndarray.random_normal(shape=(2,3,8,8))
print X
print X.mean(axis=(0,2,3),keepdims=True) #[[[[ 0.04687225]] [[-0.05943033]] [[-0.2027849 ]]]]

# 下面我们检查一下。我们先定义全连接层的输入是这样的。每一行是批量中的一个实例。
A = nd.arange(6).reshape((3,2))
print A #[[ 0.  1.][ 2.  3.][ 4.  5.]]

# 我们希望批量中的  每一列  都被归一化。结果符合预期。
## ????全部一起均一化呢???
print pure_batch_norm(A, gamma=nd.array([1,1]), beta=nd.array([0,0]))
#[[-1.22474265 -1.22474265][ 0.          0.        ] [ 1.22474265  1.22474265]]##均值0 方差1

# 下面我们定义二维卷积网络层的输入是这样的。
B = nd.arange(18).reshape((1,2,3,3))
print B
# 结果也如预期那样,我们对每个通道做了归一化。
print pure_batch_norm(B, gamma=nd.array([1,1]), beta=nd.array([0,0]))


## 批量归一化层
# 你可能会想,既然训练时用了批量归一化,那么测试时也该用批量归一化吗?其实这个问题乍一想不是很好回答,因为:
# * 不用的话,训练出的模型参数很可能在测试时就不准确了;
# * 用的话,万一测试的数据就只有一个数据实例就不好办了。
# 事实上,在测试时我们还是需要继续使用批量归一化的,只是需要做些改动。
# 在测试时,我们需要把原先训练时用到的批量均值和方差替换成**整个**训练数据的均值和方差。
# 但是当训练数据极大时,这个计算开销很大。因此,我们用移动平均的方法来近似计算

# 为了方便讨论批量归一化层的实现,我们先看下面这段代码来理解``Python``变量可以如何修改。
## 拿到全部训练数据的均值方差供测试数据用
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)

ctx = mx.gpu()
weight_scale = .01

# output channels = 20, kernel = (5,5)
c1 = 20
W1 = nd.random.normal(shape=(c1,1,5,5), scale=weight_scale, ctx=ctx)
b1 = nd.zeros(c1, ctx=ctx)
# batch norm 1  gamma beta也是需要学
gamma1 = nd.random.normal(shape=c1, scale=weight_scale, ctx=ctx)
beta1 = nd.random.normal(shape=c1, scale=weight_scale, ctx=ctx)
moving_mean1 = nd.zeros(c1, ctx=ctx)
moving_variance1 = nd.zeros(c1, ctx=ctx)
# output channels = 50, kernel = (3,3)
c2 = 50
W2 = nd.random_normal(shape=(c2,c1,3,3), scale=weight_scale, ctx=ctx)
b2 = nd.zeros(c2, ctx=ctx)
# batch norm 2
gamma2 = nd.random.normal(shape=c2, scale=weight_scale, ctx=ctx)
beta2 = nd.random.normal(shape=c2, scale=weight_scale, ctx=ctx)
moving_mean2 = nd.zeros(c2, ctx=ctx)
moving_variance2 = nd.zeros(c2, ctx=ctx)
# output dim = 128
o3 = 128
W3 = nd.random.normal(shape=(1250, o3), scale=weight_scale, ctx=ctx)
b3 = nd.zeros(o3, ctx=ctx)
# output dim = 10
W4 = nd.random_normal(shape=(W3.shape[1], 10), scale=weight_scale, ctx=ctx)
b4 = nd.zeros(W4.shape[1], ctx=ctx)

# 注意这里moving_*是不需要更新的
params = [W1, b1, gamma1, beta1,
          W2, b2, gamma2, beta2,
          W3, b3, W4, b4]

for param in params:
    param.attach_grad()

# 下面定义模型。我们添加了批量归一化层。特别要注意我们添加的位置:在卷积层后,在激活函数前。
def net(X, is_training=False, verbose=False):
    X = X.as_in_context(W1.context) ##GPU or CPU
    # 第一层卷积
    h1_conv = nd.Convolution(data=X, weight=W1, bias=b1, kernel=W1.shape[2:], num_filter=c1)
    ### 添加了批量归一化层
    h1_bn = batch_norm(h1_conv, gamma1, beta1, is_training,moving_mean1, moving_variance1)
    h1_activation = nd.relu(h1_bn)
    h1 = nd.Pooling(data=h1_activation, pool_type="max", kernel=(2,2), stride=(2,2))
    # 第二层卷积
    h2_conv = nd.Convolution(data=h1, weight=W2, bias=b2, kernel=W2.shape[2:], num_filter=c2)
    ### 添加了批量归一化层
    h2_bn = batch_norm(h2_conv, gamma2, beta2, is_training,moving_mean2, moving_variance2)
    h2_activation = nd.relu(h2_bn)
    h2 = nd.Pooling(data=h2_activation, pool_type="max", kernel=(2,2), stride=(2,2))
    h2 = nd.flatten(h2)
    # 第一层全连接
    h3_linear = nd.dot(h2, W3) + b3
    h3 = nd.relu(h3_linear)
    # 第二层全连接
    h4_linear = nd.dot(h3, W4) + b4
    if verbose:
        print('1st conv block:', h1.shape)
        print('2nd conv block:', h2.shape)
        print('1st dense:', h3.shape)
        print('2nd dense:', h4_linear.shape)
        print('output:', h4_linear)
    return h4_linear

# 下面我们训练并测试模型。
from mxnet import autograd
from mxnet import gluon
import utils

batch_size = 256
train_data, test_data = utils.load_data_fashion_mnist(batch_size)
softmax_cross_entropy = gluon.loss.SoftmaxCrossEntropyLoss()
learning_rate = 0.2
for epoch in range(5):
    train_loss = 0.
    train_acc = 0.
    for data, label in train_data:
        label = label.as_in_context(ctx)
        with autograd.record():
            output = net(data, is_training=True)
            loss = softmax_cross_entropy(output, label)
        loss.backward()
        utils.SGD(params, learning_rate/batch_size)

        train_loss += nd.mean(loss).asscalar()
        train_acc += utils.accuracy(output, label)

    test_acc = utils.evaluate_accuracy(test_data, net, ctx)
    print("Epoch %d. Loss: %f, Train acc %f, Test acc %f" % (
            epoch, train_loss/len(train_data), train_acc/len(train_data), test_acc))

 

4.2.gluon版batchnorm

使用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
59
60
# coding=utf-8

# 有了`Gluon`,我们模型的定义工作变得简单了许多。我们只需要添加`nn.BatchNorm`层并指定对二维卷积的通道(`axis=1`)进行批量归一化。
from mxnet.gluon import nn

net = nn.Sequential()
with net.name_scope():
    # 第一层卷积
    net.add(nn.Conv2D(channels=20, kernel_size=5))
    ### 添加了批量归一化层
    net.add(nn.BatchNorm(axis=1))##########################
    net.add(nn.Activation(activation='relu'))
    net.add(nn.MaxPool2D(pool_size=2, strides=2))
    # 第二层卷积
    net.add(nn.Conv2D(channels=50, kernel_size=3))
    ### 添加了批量归一化层
    net.add(nn.BatchNorm(axis=1))
    net.add(nn.Activation(activation='relu'))
    net.add(nn.MaxPool2D(pool_size=2, strides=2))
    net.add(nn.Flatten())
    # 第一层全连接
    net.add(nn.Dense(128, activation="relu"))
    # 第二层全连接
    net.add(nn.Dense(10))

## 模型训练
# 剩下的代码跟之前没什么不一样。
import utils
from mxnet import autograd
from mxnet import gluon
from mxnet import nd
import mxnet as mx
# from mxnet import init

ctx = mx.gpu()
net.initialize(ctx=ctx)

batch_size = 256
train_data, test_data = utils.load_data_fashion_mnist(batch_size)

softmax_cross_entropy = gluon.loss.SoftmaxCrossEntropyLoss()
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.2})

for epoch in range(5):
    train_loss = 0.
    train_acc = 0.
    for data, label in train_data:
        label = label.as_in_context(ctx)
        with autograd.record():
            output = net(data.as_in_context(ctx))
            loss = softmax_cross_entropy(output, label)
        loss.backward()
        trainer.step(batch_size)

        train_loss += nd.mean(loss).asscalar()
        train_acc += utils.accuracy(output, label)
    test_acc = utils.evaluate_accuracy(test_data, net, ctx)
    print("Epoch %d. Loss: %f, Train acc %f, Test acc %f" % (
        epoch, train_loss/len(train_data),
        train_acc/len(train_data), test_acc))

 

4.3.网络中的网络NIN

首先一点是注意到卷积神经网络一般分成两块,一块主要由卷积层构成,另一块主要是全连接层。在Alexnet里我们看到如何把卷积层块和全连接层分别加深加宽从而得到深度网络。另外一个自然的想法是,我们可以串联数个卷积层块和全连接层块来构建深度网络。
nin
不过这里的一个难题是,卷积的输入输出是4D矩阵,然而全连接是2D。同时在卷积神经网络里我们提到如果把4D矩阵转成2D做全连接,这个会导致全连接层有过多的参数。NiN提出只对通道层做全连接并且像素之间共享权重来解决上述两个问题。就是说,我们使用kernel大小是1x1的卷积。

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
# coding=utf-8

from mxnet.gluon import nn

def mlpconv(channels, kernel_size, padding,strides=1, max_pooling=True):
    out = nn.Sequential()
    with out.name_scope():### NIN
        out.add(
            nn.Conv2D(channels=channels, kernel_size=kernel_size,strides=strides, padding=padding,activation='relu'),
            nn.Conv2D(channels=channels, kernel_size=1,padding=0, strides=1, activation='relu'),
            nn.Conv2D(channels=channels, kernel_size=1,padding=0, strides=1, activation='relu'))
        if max_pooling:
            out.add(nn.MaxPool2D(pool_size=3, strides=2))
    return out

# 测试一下:
from mxnet import nd

blk = mlpconv(64, 3, 0)
blk.initialize()

x = nd.random.uniform(shape=(32, 3, 16, 16))
y = blk(x)
print y.shape ##(32L, 64L, 6L, 6L)

# NiN的卷积层的参数跟Alexnet类似,使用三组不同的设定
# kernel: 11x11, channels: 96
# kernel: 5x5, channels: 256
# kernel: 3x3, channels: 384
# 除了使用了1x1卷积外,NiN在最后不是使用全连接,而是使用通道数为输出类别个数的
# `mlpconv`,外接一个平均池化层来将每个通道里的数值平均成一个标量。
net = nn.Sequential()
with net.name_scope():
    net.add(
        mlpconv(96, 11, 0, strides=4),
        mlpconv(256, 5, 2),
        mlpconv(384, 3, 1),
        nn.Dropout(.5),
        # 目标类为10类
        mlpconv(10, 3, 1, max_pooling=False),
        # 输入为 batch_size x 10 x 5 x 5, 通过AvgPool2D转成
        # batch_size x 10 x 1 x 1。
        nn.AvgPool2D(pool_size=5),
        # 转成 batch_size x 10
        nn.Flatten()
    )

## 获取数据并训练
# 跟Alexnet类似,但使用了更大的学习率。
import utils
from mxnet import gluon
from mxnet import init
import mxnet as mx

train_data, test_data = utils.load_data_fashion_mnist(batch_size=64, resize=224)
ctx = mx.gpu()
net.initialize(ctx=ctx, init=init.Xavier())
loss = gluon.loss.SoftmaxCrossEntropyLoss()
trainer = gluon.Trainer(net.collect_params(),'sgd', {'learning_rate': 0.1})
utils.train(train_data, test_data, net, loss,trainer, ctx, num_epochs=10)

 

这种“一卷卷到底”最后加一个平均池化层的做法也成为了深度卷积神经网络的常用设计。

4.4.GoogleNet

在2014年的Imagenet竞赛里,Google的研究人员利用一个新的网络结构取得很大的优先。这个叫做GoogleLeNet的网络虽然在名字上是向LeNet致敬,但网络结构里很难看到LeNet的影子。它颠覆的大家对卷积神经网络串联一系列层的固定做法。下图是其论文对GoogLeNet的可视化
googlenet
可以看到其中有多个四个并行卷积层的块。这个块一般叫做Inception,其基于Network in network的思想做了很大的改进。我们先看下如何定义一个下图所示的Inception块。
Inception

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# coding=utf-8

from mxnet.gluon import nn
from mxnet import nd


class Inception(nn.Block):
    def __init__(self, n1_1, n2_1, n2_3, n3_1, n3_5, n4_1, **kwargs):
        super(Inception, self).__init__(**kwargs)
        with self.name_scope():
            # path 1
            self.p1_conv_1 = nn.Conv2D(n1_1, kernel_size=1,activation='relu')
            # path 2
            self.p2_conv_1 = nn.Conv2D(n2_1, kernel_size=1,activation='relu')
            self.p2_conv_3 = nn.Conv2D(n2_3, kernel_size=3, padding=1,activation='relu')
            # path 3
            self.p3_conv_1 = nn.Conv2D(n3_1, kernel_size=1,activation='relu')
            self.p3_conv_5 = nn.Conv2D(n3_5, kernel_size=5, padding=2,activation='relu')
            # path 4
            self.p4_pool_3 = nn.MaxPool2D(pool_size=3, padding=1,strides=1)
            self.p4_conv_1 = nn.Conv2D(n4_1, kernel_size=1,activation='relu')

    def forward(self, x):
        p1 = self.p1_conv_1(x)
        p2 = self.p2_conv_3(self.p2_conv_1(x))
        p3 = self.p3_conv_5(self.p3_conv_1(x))
        p4 = self.p4_conv_1(self.p4_pool_3(x))
        return nd.concat(p1, p2, p3, p4, dim=1)

# 可以看到Inception里有四个并行的线路。
# 1. 单个1x1卷积。
# 2. 1x1卷积接上3x3卷积。通常前者的通道数少于输入通道,这样减少后者的计算量。后者加上了`padding=1`使得输出的长宽的输入一致
# 3. 同2,但换成了5x5卷积
# 4. 和1类似,但卷积前用了最大池化层
# 最后将这四个并行线路的结果在通道这个维度上合并在一起。
# 测试一下:
incp = Inception(64, 96, 128, 16, 32, 32)
incp.initialize()

x = nd.random.uniform(shape=(32,3,64,64))
print incp(x).shape ##(32L, 256L, 64L, 64L)  256=64+128+32+32

# GoogLeNet将数个Inception串联在一起。注意到原论文里使用了多个输出,为了简化我们这里就使用一个输出。
# 为了可以更方便的查看数据在内部的形状变化,我们对每个块使用一个`nn.Sequential`,然后再把所有这些块连起来。
class GoogLeNet(nn.Block):
    def __init__(self, num_classes, verbose=False, **kwargs):
        super(GoogLeNet, self).__init__(**kwargs)
        self.verbose = verbose
        with self.name_scope():
            # block 1
            b1 = nn.Sequential()
            b1.add(
                nn.Conv2D(64, kernel_size=7, strides=2,padding=3, activation='relu'),
                nn.MaxPool2D(pool_size=3, strides=2)
            )
            # block 2
            b2 = nn.Sequential()
            b2.add(
                nn.Conv2D(64, kernel_size=1),
                nn.Conv2D(192, kernel_size=3, padding=1),
                nn.MaxPool2D(pool_size=3, strides=2)
            )

            # block 3
            b3 = nn.Sequential()
            b3.add(
                Inception(64, 96, 128, 16, 32, 32),
                Inception(128, 128, 192, 32, 96, 64),
                nn.MaxPool2D(pool_size=3, strides=2)
            )

            # block 4
            b4 = nn.Sequential()
            b4.add(
                Inception(192, 96, 208, 16, 48, 64),
                Inception(160, 112, 224, 24, 64, 64),
                Inception(128, 128, 256, 24, 64, 64),
                Inception(112, 144, 288, 32, 64, 64),
                Inception(256, 160, 320, 32, 128, 128),
                nn.MaxPool2D(pool_size=3, strides=2)
            )

            # block 5
            b5 = nn.Sequential()
            b5.add(
                Inception(256, 160, 320, 32, 128, 128),
                Inception(384, 192, 384, 48, 128, 128),
                nn.AvgPool2D(pool_size=2)
            )
            # block 6
            b6 = nn.Sequential()
            b6.add(
                nn.Flatten(),
                nn.Dense(num_classes)
            )
            # chain blocks together
            self.net = nn.Sequential()
            self.net.add(b1, b2, b3, b4, b5, b6)

    def forward(self, x):
        out = x
        for i, b in enumerate(self.net):
            out = b(out)
            if self.verbose:
                print('Block %d output: %s' % (i + 1, out.shape))
        return out

# 我们看一下每个块对输出的改变。
net = GoogLeNet(10, verbose=True)
net.initialize()

x = nd.random.uniform(shape=(4, 3, 96, 96))
y = net(x)

## 获取数据并训练
# 跟VGG一样我们使用了较小的输入$96\times 96$来加速计算。
import utils
from mxnet import gluon
from mxnet import init
import mxnet as mx

train_data, test_data = utils.load_data_fashion_mnist(batch_size=64, resize=96)

ctx = mx.gpu()
net = GoogLeNet(10)
net.initialize(ctx=ctx, init=init.Xavier())

loss = gluon.loss.SoftmaxCrossEntropyLoss()
trainer = gluon.Trainer(net.collect_params(),'sgd', {'learning_rate': 0.01})
utils.train(train_data, test_data, net, loss,trainer, ctx, num_epochs=10)

 

GoogLeNet加入了更加结构化的Inception块来使得我们可以使用更大的通道,更多的层,同时控制计算量和模型大小在合理范围内。
练习
GoogLeNet有数个后续版本,尝试实现他们并运行看看有什么不一样

4.5.Resnet

ResNet有效的解决了深度卷积神经网络难训练的问题。这是因为在误差反传的过程中,梯度通常变得越来越小,从而权重的更新量也变小。这个导致远离损失函数的层训练缓慢,随着层数的增加这个现象更加明显。之前有两种常用方案来尝试解决这个问题:

  1. 按层训练。先训练靠近数据的层,然后慢慢的增加后面的层。但效果不是特别好,而且比较麻烦。
  2. 使用更宽的层(增加输出通道)而不是更深来增加模型复杂度。但更宽的模型经常不如更深的效果好。
    ResNet通过增加跨层的连接来解决梯度逐层回传时变小的问题。虽然这个想法之前就提出过了,但ResNet真正的把效果做好了。
    下图演示了一个跨层的连接。
    residual
    最底下那层的输入不仅仅是输出给了中间层,而且其与中间层结果相加进入最上层。这样在梯度反传时,最上层梯度可以直接跳过中间层传到最下层,从而避免最下层梯度过小情况。
    为什么叫做残差网络呢?我们可以将上面示意图里的结构拆成两个网络的和,一个一层,一个两层,最下面层是共享的。
    residual2
    在训练过程中,左边的网络因为更简单所以更容易训练。这个小网络没有拟合到的部分,或者说残差,则被右边的网络抓取住。所以直观上来说,即使加深网络,跨层连接仍然可以使得底层网络可以充分的训练,从而不会让训练更难。
    Residual块:
    ResNet沿用了VGG的那种全用3x3卷积,但在卷积和池化层之间加入了批量归一层来加速训练。每次跨层连接跨过两层卷积。这里我们定义一个这样的残差块。注意到如果输入的通道数和输出不一样时(same_shape=False),我们使用一个额外的1x1卷积来做通道变化,同时使用strides=2来把长宽减半。
    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
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    
    # coding=utf-8
    
    from mxnet.gluon import nn
    from mxnet import nd
    
    class Residual(nn.Block):
        def __init__(self, channels, same_shape=True, **kwargs):
            super(Residual, self).__init__(**kwargs)
            self.same_shape = same_shape
            with self.name_scope():
                strides = 1 if same_shape else 2
                self.conv1 = nn.Conv2D(channels, kernel_size=3, padding=1,strides=strides)
                self.bn1 = nn.BatchNorm()
                self.conv2 = nn.Conv2D(channels, kernel_size=3, padding=1)
                self.bn2 = nn.BatchNorm()
                if not same_shape:
                    self.conv3 = nn.Conv2D(channels, kernel_size=1,strides=strides)
    
        def forward(self, x):
            out = nd.relu(self.bn1(self.conv1(x)))
            out = self.bn2(self.conv2(out))
            if not self.same_shape:
                x = self.conv3(x)
            return nd.relu(out + x)
    
    # 输入输出通道相同:
    blk = Residual(3)
    blk.initialize()
    x = nd.random.uniform(shape=(4, 3, 6, 6))
    print blk(x).shape ##(4L, 3L, 6L, 6L)
    
    # 输入输出通道不同:
    blk2 = Residual(8, same_shape=False)
    blk2.initialize()
    print blk2(x).shape ##(4L, 8L, 3L, 3L)
    
    #构建ResNet
    # 类似GoogLeNet主体是由Inception块串联而成,ResNet的主体部分串联多个Residual块。下面我们定义18层的ResNet。
    # 同样为了阅读更加容易,我们这里使用了多个`nn.Sequential`。另外注意到一点是,这里我们没用池化层来减小数据长宽,
    # 而是通过有通道变化的Residual块里面的使用`strides=2`的卷积层。
    class ResNet(nn.Block):
        def __init__(self, num_classes, verbose=False, **kwargs):
            super(ResNet, self).__init__(**kwargs)
            self.verbose = verbose
            with self.name_scope():
                # block 1
                b1 = nn.Conv2D(64, kernel_size=7, strides=2)
                # block 2
                b2 = nn.Sequential()
                b2.add(
                    nn.MaxPool2D(pool_size=3, strides=2),
                    Residual(64),
                    Residual(64)
                )
                # block 3
                b3 = nn.Sequential()
                b3.add(
                    Residual(128, same_shape=False),
                    Residual(128)
                )
                # block 4
                b4 = nn.Sequential()
                b4.add(
                    Residual(256, same_shape=False),
                    Residual(256)
                )
                # block 5
                b5 = nn.Sequential()
                b5.add(
                    Residual(512, same_shape=False),
                    Residual(512)
                )
                # block 6
                b6 = nn.Sequential()
                b6.add(
                    nn.AvgPool2D(pool_size=3),
                    nn.Dense(num_classes)
                )
                # chain all blocks together
                self.net = nn.Sequential()
                self.net.add(b1, b2, b3, b4, b5, b6)
    
        def forward(self, x):
            out = x
            for i, b in enumerate(self.net):
                out = b(out)
                if self.verbose:
                    print('Block %d output: %s'%(i+1, out.shape))
            return out
    
    # 这里演示数据在块之间的形状变化:
    net = ResNet(10, verbose=True)
    net.initialize()
    
    x = nd.random.uniform(shape=(4, 3, 96, 96))
    y = net(x)
    
    ## 获取数据并训练
    # 跟前面类似,但因为有批量归一化,所以使用了较大的学习率。
    import utils
    from mxnet import gluon
    from mxnet import init
    import mxnet as mx
    
    train_data, test_data = utils.load_data_fashion_mnist(batch_size=64, resize=96)
    
    ctx = mx.gpu()
    net = ResNet(10)
    net.initialize(ctx=ctx, init=init.Xavier())
    
    loss = gluon.loss.SoftmaxCrossEntropyLoss()
    trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.05})
    utils.train(train_data, test_data, net, loss,trainer, ctx, num_epochs=10)
    

结论
ResNet使用跨层通道使得训练非常深的卷积神经网络成为可能。同样它使用很简单的卷积层配置,使得其拓展更加简单。

练习

  • 这里我们实现了ResNet 18,原论文中还讨论了更深的配置。尝试实现它们。(提示:参考论文中的表1)
  • 原论文中还介绍了一个“bottleneck”架构,尝试实现它
  • ResNet作者在接下来的一篇论文讨论了将Residual块里面的Conv->BN->Relu结构改成了BN->Relu->Conv(参考论文图1),尝试实现它

4.6.DenseNet

ResNet的跨层连接思想影响了接下来的众多工作。这里我们介绍其中的一个:DenseNet。下图展示了这两个的主要区别:
densenet
可以看到DenseNet里来自跳层的输出不是通过加法(+)而是拼接(concat)来跟目前层的输出合并。因为是拼接,所以底层的输出会保留的进入上面所有层。这是为什么叫“稠密连接”的原因
稠密块(Dense Block):
我们先来定义一个稠密连接块。DenseNet的卷积块使用ResNet改进版本的BN->Relu->Conv。每个卷积的输出通道数被称之为growth_rate,这是因为假设输出为in_channels,而且有layers层,那么输出的通道数就是in_channels+growth_rate*layers

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# coding=utf-8

from mxnet import nd
from mxnet.gluon import nn


def conv_block(channels):
    out = nn.Sequential()
    out.add(
        nn.BatchNorm(),
        nn.Activation('relu'),
        nn.Conv2D(channels, kernel_size=3, padding=1)
    )
    return out


class DenseBlock(nn.Block):
    def __init__(self, layers, growth_rate, **kwargs):
        super(DenseBlock, self).__init__(**kwargs)
        self.net = nn.Sequential()
        for i in range(layers):
            self.net.add(conv_block(growth_rate))

    def forward(self, x):
        for layer in self.net:
            out = layer(x)
            x = nd.concat(x, out, dim=1)
        return x

# 我们验证下输出通道数是不是符合预期。
dblk = DenseBlock(2, 10)
dblk.initialize()

x = nd.random.uniform(shape=(4,3,8,8))
print dblk(x).shape  ##(4L, 23L, 8L, 8L)


## 过渡块(Transition Block)
# 因为使用拼接的缘故,每经过一次过渡块输出通道数可能会激增。为了控制模型复杂度,
# 这里引入一个过渡块,它不仅把输入的长宽减半,同时也使用1x1卷积来改变通道数。
def transition_block(channels):
    out = nn.Sequential()
    out.add(
        nn.BatchNorm(),
        nn.Activation('relu'),
        nn.Conv2D(channels, kernel_size=1),
        nn.AvgPool2D(pool_size=2, strides=2)
    )
    return out

# 验证一下结果:
tblk = transition_block(10)
tblk.initialize()
print tblk(x).shape ##(4L, 10L, 4L, 4L)

# DenseNet的主体就是交替串联稠密块和过渡块。它使用全局的`growth_rate`使得配置更加简单。过渡层每次都将通道数减半。
# 下面定义一个121层的DenseNet。
init_channels = 64
growth_rate = 32
block_layers = [6, 12, 24, 16]
num_classes = 10

def dense_net():
    net = nn.Sequential()
    with net.name_scope():
        # first block
        net.add(
            nn.Conv2D(init_channels, kernel_size=7, strides=2, padding=3),
            nn.BatchNorm(),
            nn.Activation('relu'),
            nn.MaxPool2D(pool_size=3, strides=2, padding=1)
        )
        # dense blocks
        channels = init_channels
        for i, layers in enumerate(block_layers): #i,layers---0 6   1 12   2 24   3 16
            net.add(DenseBlock(layers, growth_rate))
            channels += layers * growth_rate
            if i != len(block_layers)-1:##前三个每个denseblock后channels减半  最后一个不用
                net.add(transition_block(channels//2))
        # last block
        net.add(
            nn.BatchNorm(),
            nn.Activation('relu'),
            nn.AvgPool2D(pool_size=1),
            nn.Flatten(),
            nn.Dense(num_classes)
        )
    return net

## 获取数据并训练
# 因为这里我们使用了比较深的网络,所以我们进一步把输入减少到32x32来训练。
import utils
from mxnet import gluon
from mxnet import init
import mxnet as mx

train_data, test_data = utils.load_data_fashion_mnist(batch_size=64, resize=32)
ctx = mx.gpu()
net = dense_net()
net.initialize(ctx=ctx, init=init.Xavier())

loss = gluon.loss.SoftmaxCrossEntropyLoss()
trainer = gluon.Trainer(net.collect_params(),'sgd', {'learning_rate': 0.1})
utils.train(train_data, test_data, net, loss,trainer, ctx, num_epochs=10)

 

Desnet通过将ResNet里的+替换成concat从而获得更稠密的连接。
练习

  • DesNet论文中提交的一个优点是其模型参数比ResNet更小,想想为什么?
  • DesNet被人诟病的一个问题是内存消耗过多。真的会这样吗?可以把输入换成$224\times 224$(需要改最后的AvgPool2D大小),来看看实际(GPU)内存消耗。
  • 这里的FashionMNIST有必要用100+层的网络吗?尝试将其改简单看看效果。

4.7.图片增强

图片增强通过一系列的随机变化生成大量“新”的样本,从而减低过拟合的可能。现在在深度卷积神经网络训练中,图片增强是必不可少的一部分。
常用增强方法:
我们首先读取一张400x500的图片作为样例
img5
水平方向翻转图片是最早也是最广泛使用的一种增强。
以.5的概率做翻转

1
aug = image.HorizontalFlipAug(.5) ##左右变换对人没有啥区别,对卷积比较难看,可以用

 

img6
随机裁剪一个块 200 x 200 的区域

1
aug = image.RandomCropAug([200,200]) ##随机裁剪可以一定概率将物体放在各个位置出现,降低卷积对位置的敏感

 

img7
我们也可以随机裁剪一块随机大小的区域
随机裁剪,要求保留至少0.1的区域,随机长宽比在.5和2之间。 常用一些 有剪切和变形
最后将结果resize到200x200

1
aug = image.RandomSizedCropAug((200,200), .1, (.5,2))

 

img8
颜色变化
形状变化外的一个另一大类是变化颜色。
随机将亮度增加或者减小在0-50%间的一个量

1
aug = image.BrightnessJitterAug(.5)

 

img9
随机色调变化 ##模拟不同光照情况下

1
aug = image.HueJitterAug(.5)

 

img10

cifar10使用增强

1
2
3
4
# 对于训练图片我们随机水平翻转和剪裁。对于测试图片仅仅就是中心剪裁。
# CIFAR10图片尺寸是32x32x3,我们剪裁成28x28x3.
train_augs = [image.HorizontalFlipAug(.5),image.RandomCropAug((28,28))]
test_augs = [image.CenterCropAug((28,28))]

 

img11
使用增强训练结果

1
2
3
4
5
6
7
8
9
10
Epoch 0. Loss: 1.483978, Train acc 0.469281, Test acc 0.447093
Epoch 1. Loss: 1.069175, Train acc 0.619841, Test acc 0.582377
Epoch 2. Loss: 0.883304, Train acc 0.689666, Test acc 0.685621
Epoch 3. Loss: 0.770580, Train acc 0.729875, Test acc 0.721717
Epoch 4. Loss: 0.688039, Train acc 0.759827, Test acc 0.766515
Epoch 5. Loss: 0.629892, Train acc 0.779352, Test acc 0.720134
Epoch 6. Loss: 0.578624, Train acc 0.799772, Test acc 0.774426
Epoch 7. Loss: 0.535755, Train acc 0.812544, Test acc 0.766021
Epoch 8. Loss: 0.507056, Train acc 0.820888, Test acc 0.783525
Epoch 9. Loss: 0.476048, Train acc 0.835066, Test acc 0.792623

 

不使用增强训练结果

1
2
3
4
5
6
7
8
9
10
Epoch 0. Loss: 1.440451, Train acc 0.486553, Test acc 0.565269
Epoch 1. Loss: 0.978142, Train acc 0.654356, Test acc 0.623418
Epoch 2. Loss: 0.758181, Train acc 0.733584, Test acc 0.683643
Epoch 3. Loss: 0.590786, Train acc 0.790821, Test acc 0.703323
Epoch 4. Loss: 0.454785, Train acc 0.840873, Test acc 0.714498
Epoch 5. Loss: 0.352663, Train acc 0.877777, Test acc 0.653481
Epoch 6. Loss: 0.248027, Train acc 0.913731, Test acc 0.722903
Epoch 7. Loss: 0.176261, Train acc 0.938675, Test acc 0.732892
Epoch 8. Loss: 0.118719, Train acc 0.959439, Test acc 0.696697
Epoch 9. Loss: 0.091157, Train acc 0.969106, Test acc 0.705103

 

完整代码:
resnet18

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
from mxnet.gluon import nn
class Residual(nn.HybridBlock):
    def __init__(self, channels, same_shape=True, **kwargs):
        super(Residual, self).__init__(**kwargs)
        self.same_shape = same_shape
        with self.name_scope():
            strides = 1 if same_shape else 2
            self.conv1 = nn.Conv2D(channels, kernel_size=3, padding=1,
                                  strides=strides)
            self.bn1 = nn.BatchNorm()
            self.conv2 = nn.Conv2D(channels, kernel_size=3, padding=1)
            self.bn2 = nn.BatchNorm()
            if not same_shape:
                self.conv3 = nn.Conv2D(channels, kernel_size=1,
                                      strides=strides)

    def hybrid_forward(self, F, x):
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        if not self.same_shape:
            x = self.conv3(x)
        return F.relu(out + x)

def resnet18(num_classes):
    net = nn.HybridSequential()
    with net.name_scope():
        net.add(
            nn.BatchNorm(),
            nn.Conv2D(64, kernel_size=3, strides=1),
            nn.MaxPool2D(pool_size=3, strides=2),
            Residual(64),
            Residual(64),
            Residual(128, same_shape=False),
            Residual(128),
            Residual(256, same_shape=False),
            Residual(256),
            nn.GlobalAvgPool2D(),
            nn.Dense(num_classes)
        )
    return net

 

imageaugmentation

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# coding=utf-8


import matplotlib.pyplot as plt
from mxnet import image

from four import utils

img = image.imdecode(open('../data/cat1.jpg', 'rb').read())
plt.imshow(img.asnumpy())
plt.show()

# 接下来我们定义一个辅助函数,给定输入图片`img`的增强方法`aug`,它会运行多次并画出结果。
def apply(img, aug, n=3):
    _, figs = plt.subplots(n, n, figsize=(8,8))
    for i in range(n):
        for j in range(n):
            # 转成float,一是因为aug需要float类型数据来方便做变化。
            # 二是这里会有一次copy操作,因为有些aug直接通过改写输入
            #(而不是新建输出)获取性能的提升
            x = img.astype('float32') ##将图片默认的int8转float32方便计算
            # 有些aug不保证输入是合法值,所以做一次clip
            y = aug(x).clip(0,254)
            # 显示浮点图片时imshow要求输入在[0,1]之间
            figs[i][j].imshow(y.asnumpy()/255.0)
            figs[i][j].axes.get_xaxis().set_visible(False)
            figs[i][j].axes.get_yaxis().set_visible(False)

### 变形
# 水平方向翻转图片是最早也是最广泛使用的一种增强。
# 以.5的概率做翻转
aug = image.HorizontalFlipAug(.5) ##左右变换对人没有啥区别,对卷积比较难看,可以用
apply(img, aug)
plt.show() ##此行可以让上面的apply函数的imshow显示

# 样例图片里我们关心的猫在图片正中间,但一般情况下可能不是这样。前面我们提到池化层能弱化卷积层对目标位置的敏感度,
# 但也不能完全解决这个问题。一个常用增强方法是随机的截取其中的一块。
# 注意到随机截取一般会缩小输入的形状。如果原始输入图片过小,导致没有太多空间进行随机裁剪,
# 通常做法是先将其放大的足够大的尺寸。所以如果你的原始图片足够大,建议不要事先将它们裁到网络需要的大小。
# 随机裁剪一个块 200 x 200 的区域
aug = image.RandomCropAug([200,200]) ##随机裁剪可以一定概率将物体放在各个位置出现,降低卷积对位置的敏感
apply(img, aug)
plt.show()

# 我们也可以随机裁剪一块随机大小的区域
# 随机裁剪,要求保留至少0.1的区域,随机长宽比在.5和2之间。 ################################常用一些 有剪切和变形
# 最后将结果resize到200x200
aug = image.RandomSizedCropAug((200,200), .1, (.5,2))
apply(img, aug)
plt.show()

### 颜色变化
# 形状变化外的一个另一大类是变化颜色。
# 随机将亮度增加或者减小在0-50%间的一个量
aug = image.BrightnessJitterAug(.5)
apply(img, aug)
plt.show()

# 随机色调变化 ##模拟不同光照情况下
aug = image.HueJitterAug(.5)
apply(img, aug)
plt.show()

# ## 如何使用
# 通常使用时我们会将数个增强方法一起使用。注意到图片增强通常只是针对训练数据,对于测试数据则用得较小。后者常用的是做5次随机剪裁,
# 然后讲5张图片的预测结果做均值。
# 下面我们使用CIFAR10来演示图片增强对训练的影响。我们这里不使用前面一直用的FashionMNIST,这是因为这个数据的图片基本已经对齐好了,
# 而且是黑白图片,所以不管是变形还是变色增强效果都不会明显。
# ### 数据读取
# 我们首先定义一个辅助函数可以对图片按顺序应用数个增强:
def apply_aug_list(img, augs):
    for f in augs:
        img = f(img)
    return img
# 对于训练图片我们随机水平翻转和剪裁。对于测试图片仅仅就是中心剪裁。
# CIFAR10图片尺寸是32x32x3,我们剪裁成28x28x3.
train_augs = [image.HorizontalFlipAug(.5),image.RandomCropAug((28,28))]
test_augs = [image.CenterCropAug((28,28))]

# 然后定义数据读取,这里跟前面的FashionMNIST类似,但在`transform`中加入了图片增强:
from mxnet import gluon
from mxnet import nd

def get_transform(augs):
    def transform(data, label):
        data = data.astype('float32')
        if augs is not None:
            data = apply_aug_list(data, augs)############
        data = nd.transpose(data, (2, 0, 1)) / 255.0
        return data, label.astype('float32')
    return transform


def get_data(batch_size, train_augs, test_augs=None):
    cifar10_train = gluon.data.vision.CIFAR10(train=True, transform=get_transform(train_augs))
    cifar10_test = gluon.data.vision.CIFAR10(train=False, transform=get_transform(test_augs))
    train_data = gluon.data.DataLoader(cifar10_train, batch_size, shuffle=True)
    test_data = gluon.data.DataLoader(cifar10_test, batch_size, shuffle=False)
    return (train_data, test_data)

# 画出前几张看看
train_data, _ = get_data(36, train_augs)
for imgs, _ in train_data:
    break
_, figs = plt.subplots(6, 6, figsize=(6, 6))
for i in range(6):
    for j in range(6):
        x = nd.transpose(imgs[i * 3 + j], (1, 2, 0))
        figs[i][j].imshow(x.asnumpy())
        figs[i][j].axes.get_xaxis().set_visible(False)
        figs[i][j].axes.get_yaxis().set_visible(False)
plt.show()


# 训练 我们使用[ResNet 18]训练。并且训练代码整理成一个函数使得可以重读调用:

from mxnet import init
import mxnet as mx

def train(train_augs, test_augs, learning_rate=.1):
    batch_size = 128
    num_epochs = 10
    ctx = mx.gpu(0)
    loss = gluon.loss.SoftmaxCrossEntropyLoss()
    train_data, test_data = get_data(
        batch_size, train_augs, test_augs)
    net = utils.resnet18(10)
    net.initialize(ctx=ctx, init=init.Xavier())
    net.hybridize()
    trainer = gluon.Trainer(net.collect_params(),
                            'sgd', {'learning_rate': learning_rate})
    utils.train(
        train_data, test_data, net, loss, trainer, ctx, num_epochs)

# 使用增强:
train(train_augs, test_augs)

# 不使用增强:
# train(test_augs, test_augs)

# 可以看到使用增强后,训练精度提升更慢,但测试精度比不使用更好。
## 总结
# 图片增强可以有效避免过拟合。
## 练习
# 尝试换不同的增强方法试试。

 

8.1 使用Gluon实现SSD

本章利用介绍的SSD来检测野生皮卡丘
pikachu
数据集下载:
训练数据rec下载
训练数据idx下载
测试数据rec下载

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
# coding=utf-8

### 读取数据集
# 我们使用`image.ImageDetIter`来读取数据。
# 这是针对物体检测的迭代器,(Det表示Detection)。它跟`image.ImageIter`使用很类似。
# 主要不同是它返回的标号不是单个图片标号,而是每个图片里所有物体的标号,以及其对用的边框。
from mxnet import image
from mxnet import nd

data_dir='../data/pikachu/'
data_shape = 256
batch_size = 32
rgb_mean = nd.array([123, 117, 104])

def get_iterators(data_shape, batch_size):
    class_names = ['pikachu']
    num_class = len(class_names)
    train_iter = image.ImageDetIter(
        batch_size=batch_size,
        data_shape=(3, data_shape, data_shape),
        path_imgrec=data_dir+'train.rec',
        path_imgidx=data_dir+'train.idx',
        shuffle=True,
        mean=True,
        rand_crop=1,
        min_object_covered=0.95,
        max_attempts=200)
    val_iter = image.ImageDetIter(
        batch_size=batch_size,
        data_shape=(3, data_shape, data_shape),
        path_imgrec=data_dir+'val.rec',
        shuffle=False,
        mean=True)
    return train_iter, val_iter, class_names, num_class

train_data, test_data, class_names, num_class = get_iterators(data_shape, batch_size)


# 我们读取一个批量。可以看到标号的形状是`batch_size x num_object_per_image x 5`。这里数据里每个图片里面只有一个标号。
# 每个标号由长为5的数组表示,第一个元素是其对用物体的标号,其中`-1`表示非法物体,仅做填充使用。后面4个元素表示边框。
batch = train_data.next()
print(batch) #DataBatch: data shapes: [(32L, 3L, 256L, 256L)] label shapes: [(32L, 1L, 5L)] # 1L 因为每张图只有一个pikachu 如果有多个的化将取最多的那个数


# 图示数据
# 我们画出几张图片和其对应的标号。可以看到比卡丘的角度大小位置在每张图图片都不一样。不过也注意到这个数据集是直接将二次元动漫皮卡丘跟三次元背景相结合。
# 可能通过简单判断区域的色彩直方图就可以有效的区别是不是有我们要的物体。我们用这个简单数据集来演示SSD是如何工作的。实际中遇到的数据集通常会复杂很多。
import matplotlib as mpl
mpl.rcParams['figure.dpi']= 120
import matplotlib.pyplot as plt

def box_to_rect(box, color, linewidth=3):
    """convert an anchor box to a matplotlib rectangle"""
    box = box.asnumpy()
    return plt.Rectangle(
        (box[0], box[1]), box[2]-box[0], box[3]-box[1],
        fill=False, edgecolor=color, linewidth=linewidth)

_, figs = plt.subplots(3, 3, figsize=(6,6))
for i in range(3):
    for j in range(3):
        img, labels = batch.data[0][3*i+j], batch.label[0][3*i+j] ###########
        img = img.transpose((1, 2, 0)) + rgb_mean
        img = img.clip(0,255).asnumpy()/255
        fig = figs[i][j]
        fig.imshow(img)
        for label in labels:
            rect = box_to_rect(label[1:5]*data_shape,'red',2)
            fig.add_patch(rect)
        fig.axes.get_xaxis().set_visible(False)
        fig.axes.get_yaxis().set_visible(False)
plt.show()

## SSD模型
### 锚框:默认的边界框
# 因为边框可以出现在图片中的任何位置,并且可以有任意大小。为了简化计算,SSD跟Faster R-CNN一样使用一些默认的边界框,或者称之为锚框(anchor box),做为搜索起点。
# 具体来说,对输入的每个像素,以其为中心采样数个有不同形状和不同比例的边界框。假设输入大小是 w x h,
# - 给定大小 s in (0,1],那么生成的边界框形状是 [ws,hs]
# - 给定比例 r > 0,那么生成的边界框形状是 [w*sqrt{r},h\sqrt{r}]
# 在采样的时候我们提供 n 个大小(`sizes`)和 m 个比例(`ratios`)。为了计算简单这里不生成nm个锚框,而是n+m-1个。其中第 i 个锚框使用
# - `sizes[i]`和`ratios[0]` 如果 i <= n
# - `sizes[0]`和`ratios[i-n]` 如果 i>n

# 我们可以使用`contribe.ndarray`里的`MultiBoxPrior`来采样锚框。这里锚框通过左下角和右上角两个点来确定,而且被标准化成了区间[0,1]的实数。
from mxnet import nd
from mxnet.ndarray.contrib import MultiBoxPrior

# shape: batch x channel x height x weight
n = 40
x = nd.random.uniform(shape=(1, 3, n, n))
y = MultiBoxPrior(x, sizes=[.5,.25,.1], ratios=[1,2,.5])
boxes = y.reshape((n, n, -1, 4))
print(boxes.shape) ##(40L, 40L, 5L, 4L)
# The first anchor box centered on (20, 20)
# its format is (x_min, y_min, x_max, y_max)
# 我们可以画出以`(20,20)`为中心的所有锚框:
colors = ['blue', 'green', 'red', 'black', 'magenta']
plt.imshow(nd.ones((n, n, 3)).asnumpy())
anchors = boxes[20, 20, :, :]
for i in range(anchors.shape[0]):
    plt.gca().add_patch(box_to_rect(anchors[i,:]*n, colors[i]))
plt.show()


### 预测物体类别
# 对每一个锚框我们需要预测它是不是包含了我们感兴趣的物体,还是只是背景。这里我们使用一个3x3的卷积层来做预测,加上`pad=1`使用它的输出和输入一样。
# 同时输出的通道数是`num_anchors*(num_classes+1)`,每个通道对应一个锚框对某个类的置信度。假设输出是`Y`,
# 那么对应输入中第n个样本的第(i,j)像素的置信值是在`Y[n,:,i,j]`里。具体来说,对于以`(i,j)`为中心的第`a`个锚框,
# - 通道 `a*(num_class+1)` 是其只包含背景的分数
# - 通道 `a*(num_class+1)+1+b` 是其包含第`b`个物体的分数
# 我们定义个一个这样的类别分类器函数:
from mxnet.gluon import nn
def class_predictor(num_anchors, num_classes):
    """return a layer to predict classes"""
    return nn.Conv2D(num_anchors * (num_classes + 1), 3, padding=1)

cls_pred = class_predictor(5, 10) # 5个框 10类物体
cls_pred.initialize()
x = nd.zeros((2, 3, 20, 20))
y = cls_pred(x)
print y.shape ### (2L, 55L, 20L, 20L) #55=5*(10+1)


### 预测边界框
# 因为真实的边界框可以是任意形状,我们需要预测如何从一个锚框变换成真正的边界框。这个变换可以由一个长为4的向量来描述。同上一样,
# 我们用一个有`num_anchors * 4`通道的卷积。假设输出是Y,那么对应输入中第 n 个样本的第 (i,j)
# 像素为中心的锚框的转换在`Y[n,:,i,j]`里。具体来说,对于第`a`个锚框,它的变换在`a*4`到`a*4+3`通道里。
def box_predictor(num_anchors):
    """return a layer to predict delta locations"""
    return nn.Conv2D(num_anchors * 4, 3, padding=1)

box_pred = box_predictor(10)
box_pred.initialize()
x = nd.zeros((2, 3, 20, 20))
y = box_pred(x)
print y.shape  # (2L, 40L, 20L, 20L)

### 减半模块
# 我们定义一个卷积块,它将输入特征的长宽减半,以此来获取多尺度的预测。它由两个`Conv-BatchNorm-Relu`组成,
# 我们使用填充为1的3*3卷积使得输入和输入有同样的长宽,然后再通过跨度为2的最大池化层将长宽减半。
def down_sample(num_filters):
    """stack two Conv-BatchNorm-Relu blocks and then a pooling layer
    to halve the feature size"""
    out = nn.HybridSequential()
    for _ in range(2):
        out.add(nn.Conv2D(num_filters, 3, strides=1, padding=1))
        out.add(nn.BatchNorm(in_channels=num_filters))
        out.add(nn.Activation('relu'))
    out.add(nn.MaxPool2D(2))
    return out

blk = down_sample(10)
blk.initialize()
x = nd.zeros((2, 3, 20, 20))
y = blk(x)
print y.shape #(2L, 10L, 10L, 10L)


### 合并来自不同层的预测输出
# 前面我们提到过SSD的一个重要性质是它会在多个层同时做预测。每个层由于长宽和锚框选择不一样,导致输出的数据形状会不一样。这里我们用物体类别预测作为样例,边框预测是类似的。
# 我们首先创建一个特定大小的输入,然后对它输出类别预测。然后对输入减半,再输出类别预测。
x = nd.zeros((2, 8, 20, 20))
print('x:', x.shape)

cls_pred1 = class_predictor(5, 10)
cls_pred1.initialize()
y1 = cls_pred1(x)
print('Class prediction 1:', y1.shape) #(2L, 55L, 20L, 20L))

ds = down_sample(16)
ds.initialize()
x = ds(x)
print('x:', x.shape) # (2L, 16L, 10L, 10L))

cls_pred2 = class_predictor(3, 10)
cls_pred2.initialize()
y2 = cls_pred2(x)
print('Class prediction 2:', y2.shape) #(2L, 33L, 10L, 10L))

# 可以看到`y1`和`y2`形状不同。为了之后处理简单,我们将不同层的输入合并成一个输出。首先我们将通道移到最后的维度,然后将其展成2D数组。
# 因为第一个维度是样本个数,所以不同输出之间是不变,我们可以将所有输出在第二个维度上拼接起来。
def flatten_prediction(pred):
    return pred.transpose(axes=(0,2,3,1)).flatten()

def concat_predictions(preds):
    return nd.concat(*preds, dim=1)

flat_y1 = flatten_prediction(y1)
print('Flatten class prediction 1', flat_y1.shape) # (2L, 22000L)
flat_y2 = flatten_prediction(y2)
print('Flatten class prediction 2', flat_y2.shape) # (2L, 3300L)
y = concat_predictions([flat_y1, flat_y2])
print('Concat class predictions', y.shape) # (2L, 25300L)

### 主体网络
# 主体网络用来从原始像素抽取特征。通常前面介绍的用来图片分类的卷积神经网络,例如ResNet,都可以用来作为主体网络。这里为了示范,我们简单叠加几个减半模块作为主体网络。
def body():
    out = nn.HybridSequential()
    for nfilters in [16, 32, 64]:
        out.add(down_sample(nfilters))
    return out

bnet = body()
bnet.initialize()
x = nd.random.uniform(shape=(2,3,256,256))
y = bnet(x)
print y.shape # (2L, 64L, 32L, 32L)

### 创建一个玩具SSD模型
# 现在我们可以创建一个玩具SSD模型了。我们称之为玩具是因为这个网络不管是层数还是锚框个数都比较小,仅仅适合之后我们使用的一个小数据集。但这个模型不会影响我们介绍SSD。
# 这个网络包含四块。主体网络,三个减半模块,以及五个物体类别和边框预测模块。其中预测分别应用在在主体网络输出,减半模块输入,和最后的全局池化层上。
def toy_ssd_model(num_anchors, num_classes):
    downsamplers = nn.Sequential()
    for _ in range(3):
        downsamplers.add(down_sample(128))

    class_predictors = nn.Sequential()
    box_predictors = nn.Sequential()
    for _ in range(5):
        class_predictors.add(class_predictor(num_anchors, num_classes))
        box_predictors.add(box_predictor(num_anchors))

    model = nn.Sequential()
    model.add(body(), downsamplers, class_predictors, box_predictors)
    return model

### 计算预测
# 给定模型和每层预测输出使用的锚框大小和形状,我们可以定义前向函数。
def toy_ssd_forward(x, model, sizes, ratios, verbose=False):
    body, downsamplers, class_predictors, box_predictors = model
    anchors, class_preds, box_preds = [], [], []
    # feature extraction
    x = body(x)
    for i in range(5):
        # predict
        anchors.append(MultiBoxPrior(x, sizes=sizes[i], ratios=ratios[i]))
        class_preds.append(flatten_prediction(class_predictors[i](x)))
        box_preds.append(flatten_prediction(box_predictors[i](x)))
        if verbose:
            print('Predict scale', i, x.shape, 'with',anchors[-1].shape[1], 'anchors')
        # down sample
        if i < 3:
            x = downsamplers[i](x)
        elif i == 3:
            x = nd.Pooling(
                x, global_pool=True, pool_type='max',
                kernel=(x.shape[2], x.shape[3]))
    # concat date
    return (concat_predictions(anchors),
            concat_predictions(class_preds),
            concat_predictions(box_preds))

### 完整的模型
from mxnet import gluon
class ToySSD(gluon.Block):
    def __init__(self, num_classes, verbose=False, **kwargs):
        super(ToySSD, self).__init__(**kwargs)
        # anchor box sizes and ratios for 5 feature scales
        self.sizes = [[.2,.272], [.37,.447], [.54,.619], [.71,.79], [.88,.961]]
        self.ratios = [[1,2,.5]]*5  #[[1, 2, 0.5], [1, 2, 0.5], [1, 2, 0.5], [1, 2, 0.5], [1, 2, 0.5]]
        self.num_classes = num_classes
        self.verbose = verbose
        num_anchors = len(self.sizes[0]) + len(self.ratios[0]) - 1
        # use name_scope to guard the names
        with self.name_scope():
            self.model = toy_ssd_model(num_anchors, num_classes)

    def forward(self, x):
        anchors, class_preds, box_preds = toy_ssd_forward(
            x, self.model, self.sizes, self.ratios,
            verbose=self.verbose)
        # it is better to have class predictions reshaped for softmax computation
        class_preds = class_preds.reshape(shape=(0, -1, self.num_classes+1))
        return anchors, class_preds, box_preds


# 我们看看一下输入图片的形状是如何改变的,已经输出的形状。
net = ToySSD(num_classes=2, verbose=True)
net.initialize()
x = batch.data[0][0:1]
print('Input:', x.shape)
anchors, class_preds, box_preds = net(x)
print('Output achors:', anchors.shape)
print('Output class predictions:', class_preds.shape)
print('Output box predictions:', box_preds.shape)
#
# ('Input:', (1L, 3L, 256L, 256L))
# ('Predict scale', 0, (1L, 64L, 32L, 32L), 'with', 4096L, 'anchors')
# ('Predict scale', 1, (1L, 128L, 16L, 16L), 'with', 1024L, 'anchors')
# ('Predict scale', 2, (1L, 128L, 8L, 8L), 'with', 256L, 'anchors')
# ('Predict scale', 3, (1L, 128L, 4L, 4L), 'with', 64L, 'anchors')
# ('Predict scale', 4, (1L, 128L, 1L, 1L), 'with', 4L, 'anchors')
# ('Output achors:', (1L, 5444L, 4L))
# ('Output class predictions:', (1L, 5444L, 3L))
# ('Output box predictions:', (1L, 21776L))

### 损失函数
# 虽然每张图片里面通常只有几个标注的边框,但SSD会生成大量的锚框。可以想象很多锚框都不会框住感兴趣的物体,就是说跟任何对应感兴趣物体的表框的IoU都小于某个阈值。
# 这样就会产生大量的负类锚框,或者说对应标号为0的锚框。对于这类锚框有两点要考虑的:
# 1. 边框预测的损失函数不应该包括负类锚框,因为它们并没有对应的真实边框
# 1. 因为负类锚框数目可能远多于其他,我们可以只保留其中的一些。而且是保留那些目前预测最不确信它是负类的,就是对类0预测值排序,选取数值最小的哪一些困难的负类锚框。
# 我们可以使用`MultiBoxTarget`来完成上面这两个操作。

from mxnet.contrib.ndarray import MultiBoxTarget
def training_targets(anchors, class_preds, labels):
    class_preds = class_preds.transpose(axes=(0,2,1))
    return MultiBoxTarget(anchors, labels, class_preds)

out = training_targets(anchors, class_preds, batch.label[0][0:1])
print out

# 它返回三个`NDArray`,分别是
# 1. 预测的边框跟真实边框的偏移,大小是`batch_size x (num_anchors*4)`
# 1. 用来遮掩不需要的负类锚框的掩码,大小跟上面一致
# 1. 锚框的真实的标号,大小是`batch_size x num_anchors`
# 我们可以计算这次只选中了多少个锚框进入损失函数:
print out[1].sum()/4  #[ 36.]


# 然后我们可以定义需要的损失函数了。
# 对于分类问题,最常用的损失函数是之前一直使用的交叉熵。这里我们定义一个类似于交叉熵的损失,不同于交叉熵的定义 $\log(p_j)$,
# 这里 $j$ 是真实的类别,且 $p_j$ 是对于的预测概率。我们使用一个被称之为关注损失的函数,给定正的$\gamma$和$\alpha$,它的定义是
# $$ - \alpha (1-p_j)^{\gamma} \log(p_j) $$
# 下图我们演示不同$\gamma$导致的变化。可以看到,增加$\gamma$可以使得对正类预测值比较大时损失变小。

import numpy as np

def focal_loss(gamma, x):
    return - (1-x)**gamma*np.log(x)

x = np.arange(0.01, 1, .01)
gammas = [0,.25,.5,1]
for i,g in enumerate(gammas):
    plt.plot(x, focal_loss(g,x), colors[i])

plt.legend(['gamma='+str(g) for g in gammas])
plt.show()

# 这个自定义的损失函数可以简单通过继承`gluon.loss.Loss`来实现。
class FocalLoss(gluon.loss.Loss):
    def __init__(self, axis=-1, alpha=0.25, gamma=2, batch_axis=0, **kwargs):
        super(FocalLoss, self).__init__(None, batch_axis, **kwargs)
        self._axis = axis
        self._alpha = alpha
        self._gamma = gamma

    def hybrid_forward(self, F, output, label):
        output = F.softmax(output)
        pj = output.pick(label, axis=self._axis, keepdims=True)
        loss = - self._alpha * ((1 - pj) ** self._gamma) * pj.log()
        return loss.mean(axis=self._batch_axis, exclude=True)


cls_loss = FocalLoss()
print cls_loss

# 对于边框的预测是一个回归问题。通常可以选择平方损失函数(L2损失)$f(x) = x ^ 2$。
# 但这个损失对于比较大的误差的惩罚很高。我们可以采用稍微缓和一点绝对损失函数(L1损失)$f(x) = | x |$,
# 它是随着误差线性增长,而不是平方增长。但这个函数在0点处倒是不唯一,因此可能会影响收敛。一个通常的解决办法是在0点附近使用平方函数使得它更加平滑。
# 它被称之为平滑L1损失函数。它通过一个参数$\sigma$来控制平滑的区域:
#
# $$
# f(x) =
# \begin
# {cases}
# (\sigma
# x) ^ 2 / 2, &  \text
# { if}x < 1 /\sigma ^ 2\ \
#     | x | -0.5 /\sigma ^ 2, &  \text
# {otherwise}
# \end
# {cases}
# $$

# 我们图示不同的$\sigma$的平滑L1损失和L2损失的区别。
scales = [.5, 1, 10]
x = nd.arange(-2, 2, 0.1)

for i, s in enumerate(scales):
    y = nd.smooth_l1(x, scalar=s)
    plt.plot(x.asnumpy(), y.asnumpy(), color=colors[i])
plt.plot(x.asnumpy(), (x ** 2).asnumpy(), color=colors[len(scales)])
plt.legend(['scale=' + str(s) for s in scales] + ['Square loss'])
plt.show()

# 我们同样通过继承`Loss`来定义这个损失。同时它接受一个额外参数`mask`,这是用来屏蔽掉不需要被惩罚的负例样本。
class SmoothL1Loss(gluon.loss.Loss):
    def __init__(self, batch_axis=0, **kwargs):
        super(SmoothL1Loss, self).__init__(None, batch_axis, **kwargs)

    def hybrid_forward(self, F, output, label, mask):
        loss = F.smooth_l1((output - label) * mask, scalar=1.0)
        return loss.mean(self._batch_axis, exclude=True)

box_loss = SmoothL1Loss()
print box_loss


### 评估测量
#
# 对于分类好坏我们可以沿用之前的分类精度。评估边框预测的好坏的一个常用是是平均绝对误差。记得在[线性回归]
# (.. / chapter_supervised - learning / linear - regression - scratch.md)我们使用了平均平方误差。
# 但跟上面对损失函数的讨论一样,平方误差对于大的误差给予过大的值,从而数值上过于敏感。平均绝对误差就是将二次项替换成绝对值,具体来说就是预测的边框和真实边框在4个维度上的差值的绝对值。
from mxnet import metric
cls_metric = metric.Accuracy()
box_metric = metric.MAE()


### 初始化模型和训练器
from mxnet import init
from mxnet import gpu
ctx = gpu(0)
# the CUDA implementation requres each image has at least 3 lables.
# Padd two -1 labels for each instance
train_data.reshape(label_shape=(3, 5))
train_data = test_data.sync_label_shape(train_data)

net = ToySSD(num_class)
net.initialize(init.Xavier(magnitude=2), ctx=ctx)
trainer = gluon.Trainer(net.collect_params(),'sgd', {'learning_rate': 0.1, 'wd': 5e-4})

### 训练模型
# 训练函数跟前面的不一样在于网络会有多个输出,而且有两个损失函数。
import time
from mxnet import autograd
for epoch in range(100):
    # reset data iterators and metrics
    train_data.reset()
    cls_metric.reset()
    box_metric.reset()
    tic = time.time()
    for i, batch in enumerate(train_data):
        x = batch.data[0].as_in_context(ctx)
        y = batch.label[0].as_in_context(ctx)
        with autograd.record():
            anchors, class_preds, box_preds = net(x)
            box_target, box_mask, cls_target = training_targets(
                anchors, class_preds, y)
            # losses
            loss1 = cls_loss(class_preds, cls_target)
            loss2 = box_loss(box_preds, box_target, box_mask)
            loss = loss1 + loss2
        loss.backward()
        trainer.step(batch_size)
        # update metrics
        cls_metric.update([cls_target], [class_preds.transpose((0, 2, 1))])
        box_metric.update([box_target], [box_preds * box_mask])

    # print('Epoch %2d, train %s %.2f, %s %.5f, time %.1f sec' % (epoch, cls_metric.get(), box_metric.get(), time.time() - tic))
    print('Epoch %2d, train %s %.2f, %s %.5f, time %.1f sec' % (
            epoch, cls_metric.get()[0], cls_metric.get()[1], box_metric.get()[0], box_metric.get()[1], time.time() - tic))

## 预测
# 在预测阶段,我们希望能把图片里面所有感兴趣的物体找出来。我们先定一个数据读取和预处理函数。
def process_image(fname):
    with open(fname, 'rb') as f:
        im = image.imdecode(f.read())
    # resize to data_shape
    data = image.imresize(im, data_shape, data_shape)
    # minus rgb mean
    data = data.astype('float32') - rgb_mean
    # convert to batch x channel x height xwidth
    return data.transpose((2, 0, 1)).expand_dims(axis=0), im

# 然后我们跟训练那样预测表框和其对应的物体。但注意到因为我们对每个像素都会生成数个锚框,这样我们可能会预测出大量相似的表框,从而导致结果非常嘈杂。
# 一个办法是对于IoU比较高的两个表框,我们只保留预测执行度比较高的那个。这个算法(称之为non maximum suppression)在`MultiBoxDetection`里实现了。下面我们实现预测函数:
from mxnet.contrib.ndarray import MultiBoxDetection
def predict(x):
    anchors, cls_preds, box_preds = net(x.as_in_context(ctx))
    cls_probs = nd.SoftmaxActivation(
        cls_preds.transpose((0, 2, 1)), mode='channel')
    return MultiBoxDetection(cls_probs, box_preds, anchors,force_suppress=True, clip=False)

# 预测函数会输出所有边框,每个边框由`[class_id, confidence, xmin, ymin, xmax, ymax]`表示。其中`class_id = -1`表示要么这个边框被预测只含有背景,或者被去重掉了。
x, im = process_image('../data/pikachu.jpg')
out = predict(x)
print out.shape

# 最后我们将预测出置信度超过某个阈值的边框画出来:
mpl.rcParams['figure.figsize'] = (6, 6)

def display(im, out, threshold=0.5):
    plt.imshow(im.asnumpy())
    for row in out:
        row = row.asnumpy()
        class_id, score = int(row[0]), row[1]
        if class_id < 0 or score < threshold:
            continue
        color = colors[class_id % len(colors)]
        box = row[2:6] * np.array([im.shape[0], im.shape[1]] * 2)
        rect = box_to_rect(nd.array(box), color, 2)
        plt.gca().add_patch(rect)

        text = class_names[class_id]
        plt.gca().text(box[0], box[1],
                       '{:s} {:.2f}'.format(text, score),
                       bbox=dict(facecolor=color, alpha=0.5),
                       fontsize=10, color='white')
    plt.show()

display(im, out[0], threshold=0.5)


## 结论
# 物体检测比分类要困难很多。因为我们不仅要预测物体类别,还要找到它们的位置。这一章我们展示我们还是可以在合理篇幅里实现SSD算法。

#Mxnet

<think>好的,用户想学习PyTorch相关的神经网络基础知识,特别是李沐的教程。首先,我需要整理李沐教程中的核心知识点,确保覆盖神经网络的基础内容。用户提供的引用中有提到层和块、初始化方法、Sequential模块的使用,这些应该是重点。 接下来,我需要按照用户的格式要求来组织回答。用户要求行内数学公式用$...$,独立公式用$$...$$,并且用中文回答。同时需要生成相关问题,并正确添加引用标识。 首先,要介绍神经网络的基本组件,比如层、激活函数、损失函数。引用[3]和[4]提到了Sequential模块和层的使用,这部分应该详细说明。比如用代码示例展示如何用nn.Sequential构建网络,并解释每个层的作用。 然后,初始化方法也很重要。引用[2]和[4]提到了PyTorch的初始化方法,包括内置初始化和自定义方法。需要举例说明如何使用nn.init模块进行均匀分布或正态分布的初始化,并解释初始化对训练的影响。 前向传播和反向传播是基础中的基础,必须包含。这里可能需要数学公式,比如线性层的计算$y = Wx + b$,以及激活函数如ReLU的定义。同时要解释自动求导机制,比如autograd模块的工作原理。 李沐教程中可能还涉及数据加载和训练流程,所以需要介绍DataLoader和优化器的使用。引用[3]中的代码示例可以用来展示如何准备数据和定义优化器。 最后,根据用户提供的引用内容,生成相关问题。问题需要围绕教程中的知识点,比如初始化方法的影响、Sequential模块的作用、激活函数的选择等,确保覆盖用户可能想深入了解的方向。 需要检查所有LaTeX语法是否正确,确保行内公式用$包裹,独立公式单独成段。同时引用标识要正确对应,比如关于初始化的部分引用[2],层和块的部分引用[3][4]。 可能还需要补充一些扩展内容,如自定义块和保存模型,这些在教程中可能提到,但用户没有直接引用,不过根据常规知识,这些属于基础知识的一部分。需要确保信息准确,不添加不相关的内容。 最后,确保回答结构清晰,分点说明,代码示例正确,问题相关且具有针对性。整个过程要保持自然,符合用户要求的系统指令和格式。</think>根据李沐教授的PyTorch神经网络基础教程,以下是核心知识点整理: ### 一、神经网络基础组件 1. **层与块** 神经网络通过堆叠层(layer)或块(block)构建,例如全连接层`nn.Linear`和激活函数层`nn.ReLU`可组合成多层感知机: ```python net = nn.Sequential( nn.Linear(20, 256), # 输入维度20,输出维度256 nn.ReLU(), # 非线性激活函数 nn.Linear(256, 10) # 输出层维度10 ) ``` 前向传播时执行顺序为:线性变换→激活函数→线性变换[^3][^4] 2. **参数初始化** PyTorch提供多种初始化方法,如均匀分布和正态分布: ```python nn.init.uniform_(layer.weight, a=-0.1, b=0.1) # 均匀分布初始化 nn.init.normal_(layer.bias, mean=0, std=0.01) # 正态分布初始化 ``` 初始化直接影响模型收敛速度,需要根据网络结构选择合适方法[^2] 3. **前向传播机制** 通过定义`forward()`函数实现数据流向控制。对于线性层,计算过程为: $$ y = Wx + b $$ 其中$W \in \mathbb{R}^{m \times n}$为权重矩阵,$b \in \mathbb{R}^m$为偏置向量[^3] ### 二、关键实现细节 1. **自动求导系统** PyTorch通过`autograd`模块自动计算梯度,使用`.backward()`触发反向传播: ```python loss = nn.MSELoss(output, target) loss.backward() # 自动计算梯度 ``` 2. **数据加载与处理** 使用`DataLoader`实现批量数据加载: ```python from torch.utils.data import DataLoader loader = DataLoader(dataset, batch_size=32, shuffle=True) ``` 3. **训练流程模板** ```python optimizer = torch.optim.SGD(net.parameters(), lr=0.01) # 定义优化器 for epoch in range(100): for X, y in loader: optimizer.zero_grad() output = net(X) loss = F.cross_entropy(output, y) loss.backward() optimizer.step() ``` ### 三、扩展知识 1. **自定义网络块** 通过继承`nn.Module`创建自定义模块: ```python class CustomBlock(nn.Module): def __init__(self): super().__init__() self.hidden = nn.Linear(20, 256) def forward(self, x): return self.hidden(x) ``` 2. **模型保存与加载** ```python torch.save(net.state_dict(), 'model.pth') # 保存 net.load_state_dict(torch.load('model.pth')) # 加载 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值