博文 http://bestzhangjin.com/2017/10/13/deeplearn/
目录
深度学习之Mxnet--李沐视频
发表于 2017-10-13 | 分类于 深度学习 |
1.1.前言
资料详见动手学深度学习
1.2.使用NDArray来处理数据
| |
1.3.使用autograd自动求导
| |
1.4.从0开始线性回归
| |
1.5.使用Gluon实现线性回归
| |
1.6.从0开始多类逻辑回归
| |
尝试增大学习率,你会发现结果马上回变成很糟糕,精度基本徘徊在随机的0.1左右。这是为什么呢?提示:
- 打印下output看看是不是有有什么异常
- 前面线性回归还好好的,这里我们在net()里加了什么呢?
- 如果给exp输入个很大的数会怎么样?
- 即使解决exp的问题,求出来的导数是不是还是不稳定?
请仔细想想再去对比下小伙伴之一@pluskid早年写的一篇blog解释这个问题,看看你想的是不是不一样。
1.7.Gluon版多类逻辑回归
| |
2.1.从0开始多层感知机
前面我们介绍了包括线性回归和多类逻辑回归的数个模型,它们的一个共同点是全是只含有一个输入层,一个输出层。这一节我们将介绍多层神经网络,就是包含至少一个隐含层的网络。
| |
可以看到,加入一个隐含层后我们将精度提升了不少。
练习
- 我们使用了
weight_scale
来控制权重的初始化值大小,增大或者变小这个值会怎么样? - 尝试改变
num_hiddens
来控制模型的复杂度 - 尝试加入一个新的隐含层
注意:针对不同的数据,模型可能并非越复杂越好
测试结果:加大num_hiddens得到了更好的结果,添加隐藏层后更加容易过拟合
2.2.使用Gluon多层感知机
| |
通过Gluon我们可以更方便地构造多层神经网络。
练习
- 尝试多加入几个隐含层,对比从0开始的实现。
- 尝试使用一个另外的激活函数,可以使用
help(nd.Activation)
或者线上文档查看提供的选项。
2.3.从0开始正则化
本章从0开始介绍如何的正则化来应对过拟合问题。
L2范数正则化
这里我们引入$L_2$范数正则化。不同于在训练时仅仅最小化损失函数(Loss),我们在训练时其实在最小化
直观上,L2范数正则化试图惩罚较大绝对值的参数值。训练模型时,如果λ=0则没有正则化,需要注意的是,测试模型时,λ必须为0。
高维线性回归
我们使用高维线性回归为例来引入一个过拟合问题。
需要注意的是,我们用以上相同的数据生成函数来生成训练数据集和测试数据集。为了观察过拟合,我们特意把训练数据样本数设低,例如n=20,同时把维度升高,例如p=200.
| |
2.3.使用Gluon正则化
| |
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开始)。
| |
通过context
我们可以很容易在不同的设备上计算。
练习
- 试试大一点的计算任务,例如大矩阵的乘法,看看CPU和GPU的速度区别。如果是计算量很小的任务呢?
- 试试CPU和GPU之间传递数据的速度
- GPU上如何读写模型呢?
2.5.从0开始卷积神经网络
前面讲的把图片拉成了一个向量,行相关像素间信息保留了,但是列信息丢失啦,本节用卷积。
之前的教程里,在输入神经网络前我们将输入图片直接转成了向量。这样做有两个不好的地方:
- 在图片里相近的像素在向量表示里可能很远,从而模型很难捕获他们的空间关系。
- 对于大图片输入,模型可能会很大。例如输入是256x256x3的照片(仍然远比手机拍的小),输入层是1000,那么这一层的模型大小是将近1GB.
这一节我们介绍卷积神经网络,其有效了解决了上述两个问题。
卷积神经网络是指主要由卷积层构成的神经网络。
卷积层跟前面的全连接层类似,但输入和权重不是做简单的矩阵乘法,而是使用每次作用在一个窗口上的卷积。下图演示了输入是一个4x4矩阵,使用一个3x3的权重,计算得到2x2结果的过程。每次我们采样一个跟权重一样大小的窗口,让它跟权重做按元素的乘法然后相加。通常我们也是用卷积的术语把这个权重叫kernel或者filter。
我们可以控制如何移动窗口,和在边缘的时候如何填充窗口。下图演示了stride=2
和pad=1
。
utils.py
| |
cnnscratch.py
| |
结论
可以看到卷积神经网络比前面的多层感知的分类精度更好。事实上,如果你看懂了这一章,那你基本知道了计算视觉里最重要的几个想法。LeNet早在90年代就提出来了。不管你相信不相信,如果你5年前懂了这个而且开了家公司,那么你很可能现在已经把公司作价几千万卖个某大公司了。幸运的是,或者不幸的是,现在的算法已经更加高级些了,接下来我们会看到一些更加新的想法。
练习
- 试试改改卷积层设定,例如filter数量,kernel大小
- 试试把池化层从
max
改到avg
- 如果你有GPU,那么尝试用CPU来跑一下看看
- 你可能注意到比前面的多层感知机慢了很多,那么尝试计算下这两个模型分别需要多少浮点计算。例如nxm和mxk的矩阵乘法需要浮点运算2nmk。
2.6.使用gluon卷积神经网络
utils.py
| |
cnngluon.py
| |
3.1.创建神经网络
前面的教程我们教了大家如何实现线性回归,多类Logistic回归和多层感知机。我们既展示了如何从0开始实现,也提供使用gluon
的更紧凑的实现。因为前面我们主要关注在模型本身,所以只解释了如何使用gluon
,但没说明他们是如何工作的。我们使用了nn.Sequential
,它是nn.Block
的一个简单形式,但没有深入了解它们。
本教程和接下来几个教程,我们将详细解释如何使用这两个类来定义神经网络、初始化参数、以及保存和读取模型。
我们重新把使用Gluon多层感知机里的网络定义搬到这里作为开始的例子(为了简单起见,这里我们丢掉了Flatten层)。
| |
3.2.初始化模型参数
我们仍然用MLP这个例子来详细解释如何初始化模型参数。
| |
我们可以很灵活地访问和修改模型参数。
练习
- 研究下
net.collect_params()
返回的是什么?net.params
呢? - 如何对每个层使用不同的初始化函数
- 如果两个层共用一个参数,那么求梯度的时候会发生什么?
3.3.序列化读写模型
但即使知道了所有这些,我们还没有完全准备好来构建一个真正的机器学习系统。这是因为我们还没有讲如何读和写模型。因为现实中,我们通常在一个地方训练好模型,然后部署到很多不同的地方。我们需要把内存中的训练好的模型存在硬盘上好下次使用。
| |
通过load_params
和save_params
可以很方便的读写模型参数。
3.4.设计自定义层
神经网络的一个魅力是它有大量的层,例如全连接、卷积、循环、激活,和各式花样的连接方式。我们之前学到了如何使用Gluon提供的层来构建新的层(nn.Block
)继而得到神经网络。虽然Gluon提供了大量的层的定义,但我们仍然会遇到现有层不够用的情况。
这时候的一个自然的想法是,我们不是学习了如何只使用基础数值运算包NDArray
来实现各种的模型吗?它提供了大量的底层计算函数足以实现即使不是100%那也是95%的神经网络吧。
但每次都从头写容易写到怀疑人生。实际上,即使在纯研究的领域里,我们也很少发现纯新的东西,大部分时候是在现有模型的基础上做一些改进。所以很可能大部分是可以沿用前面的而只有一部分是需要自己来实现。
这个教程我们将介绍如何使用底层的NDArray
接口来实现一个Gluon
的层,从而可以以后被重复调用。
| |
仔细的你可能还是注意到了,我们这里指定了输入的大小,而Gluon自带的Dense
则无需如此。我们已经在前面节介绍过了这个延迟初始化如何使用。但如果实现一个这样的层我们将留到后面介绍了hybridize后。
现在我们知道了如何把前面手写过的层全部包装了Gluon能用的Block,之后再用到的时候就可以飞起来了!
练习
- 怎么修改自定义层里参数的默认初始化函数。
- (这个比较难),在一个代码Cell里面输入
nn.Dense??
,看看它是怎么实现的。为什么它就可以支持延迟初始化了。
3.5.dropout
丢弃法(Dropout)— 从0开始
前面我们介绍了多层神经网络,就是包含至少一个隐含层的网络。我们也介绍了正则法来应对过拟合问题。在深度学习中,一个常用的应对过拟合问题的方法叫做丢弃法(Dropout)。本节以多层神经网络为例,从0开始介绍丢弃法。
由于丢弃法的概念和实现非常容易,在本节中,我们先介绍丢弃法的概念以及它在现代神经网络中是如何实现的。然后我们一起探讨丢弃法的本质。
丢弃法的概念
在现代神经网络中,我们所指的丢弃法,通常是对输入层或者隐含层做以下操作:
- 随机选择一部分该层的输出作为丢弃元素;
- 把丢弃元素乘以0;
- 把非丢弃元素拉伸。
丢弃法的本质
了解了丢弃法的概念与实现,那你可能对它的本质产生了好奇。
如果你了解集成学习,你可能知道它在提升弱分类器准确率上的威力。一般来说,在集成学习里,我们可以对训练数据集有放回地采样若干次并分别训练若干个不同的分类器;测试时,把这些分类器的结果集成一下作为最终分类结果。
事实上,丢弃法在模拟集成学习。试想,一个使用了丢弃法的多层神经网络本质上是原始网络的子集(节点和边)。举个例子,它可能长这个样子。
丢弃法的实现
| |
我们可以通过使用丢弃法对神经网络正则化。
练习
- 尝试不使用丢弃法,看看这个包含两个隐含层的多层感知机可以得到什么结果。
- 我们推荐把更靠近输入层的元素丢弃概率设的更小一点。想想这是为什么?如果把本节教程中的两个元素丢弃参数对调会有什么结果?
3.6.使用Gluon丢弃法(Dropout)
本章介绍如何使用Gluon
在训练和测试深度学习模型中使用丢弃法(Dropout)。
定义模型并添加丢弃层
有了Gluon
,我们模型的定义工作变得简单了许多。我们只需要在全连接层后添加gluon.nn.Dropout
层并指定元素丢弃概率。一般情况下,我们推荐把
更靠近输入层的元素丢弃概率设的更小一点。这个试验中,我们把第一层全连接后的元素丢弃概率设为0.2,把第二层全连接后的元素丢弃概率设为0.5。
| |
效果比3.5好!
3.7.深度卷积神经网络和AlexNet
原版的AlexNet有每层大小为4096个节点的全连接层们。这两个巨大的全连接层带来将近1GB的模型大小。由于早期GPU显存的限制,最早的AlexNet包括了双数据流的设计,以让网络中一半的节点能存入一个GPU。这两个数据流,也就是说两个GPU只在一部分层进行通信,这样达到限制GPU同步时的额外开销的效果。有幸的是,GPU在过去几年得到了长足的发展,除了一些特殊的结构外,我们也就不再需要这样的特别设计了。
下面的Gluon代码定义了(稍微简化过的)Alexnet
| |
效果比以前的好些!
3.8.VGGNet
我们从Alexnet看到网络的层数的激增。这个意味着即使是用Gluon手动写代码一层一层的堆每一层也很麻烦,更不用说从0开始了。幸运的是编程语言提供了很好的方法来解决这个问题:函数和循环。如果网络结构里面有大量重复结构,那么我们可以很紧凑来构造这些网络。第一个使用这种结构的深度网络是VGG。
| |
#通过使用重复的元素,我们可以通过循环和函数来定义模型。使用不同的配置(architecture
)可以得到一系列不同的模型。
练习
- 尝试多跑几轮,看看跟LeNet/Alexnet比怎么样?
- 尝试下构造VGG其他常用模型,例如VGG16, VGG19. (提示:可以参考VGG论文里的表1。)
- 把图片从默认的224x224降到96x96有什么影响?
4.1. 从0开始批量归一化BatchNorm
在实际应用中,我们通常将输入数据的每个样本或者每个特征进行归一化,就是将均值变为0方差变为1,来使得数值更稳定。
它对很深的神经网络能够训练,对learningrate不那么敏感。
批量归一化对每层都归一化。
批量归一化试图对深度学习模型的某一层所使用的激活函数的输入进行归一化:使批量呈标准正态分布(均值为0,标准差为1)。
批量归一化通常应用于输入层或任意中间层。
| |
4.2.gluon版batchnorm
使用Gluon我们可以很轻松地添加批量归一化层。
| |
4.3.网络中的网络NIN
首先一点是注意到卷积神经网络一般分成两块,一块主要由卷积层构成,另一块主要是全连接层。在Alexnet里我们看到如何把卷积层块和全连接层分别加深加宽从而得到深度网络。另外一个自然的想法是,我们可以串联数个卷积层块和全连接层块来构建深度网络。
不过这里的一个难题是,卷积的输入输出是4D矩阵,然而全连接是2D。同时在卷积神经网络里我们提到如果把4D矩阵转成2D做全连接,这个会导致全连接层有过多的参数。NiN提出只对通道层做全连接并且像素之间共享权重来解决上述两个问题。就是说,我们使用kernel大小是1x1的卷积。
| |
这种“一卷卷到底”最后加一个平均池化层的做法也成为了深度卷积神经网络的常用设计。
4.4.GoogleNet
在2014年的Imagenet竞赛里,Google的研究人员利用一个新的网络结构取得很大的优先。这个叫做GoogleLeNet的网络虽然在名字上是向LeNet致敬,但网络结构里很难看到LeNet的影子。它颠覆的大家对卷积神经网络串联一系列层的固定做法。下图是其论文对GoogLeNet的可视化
可以看到其中有多个四个并行卷积层的块。这个块一般叫做Inception,其基于Network in network的思想做了很大的改进。我们先看下如何定义一个下图所示的Inception块。
| |
GoogLeNet加入了更加结构化的Inception块来使得我们可以使用更大的通道,更多的层,同时控制计算量和模型大小在合理范围内。
练习
GoogLeNet有数个后续版本,尝试实现他们并运行看看有什么不一样
- v1: 本节介绍的是最早版本:Going Deeper with Convolutions
- v2: 加入和Batch Normalization:Accelerating Deep Network Training by Reducing Internal Covariate Shift
- v3: 对Inception做了调整:Rethinking the Inception Architecture for Computer Vision
- v4: 基于ResNet加入了Residual Connections:Inception-ResNet and the Impact of Residual Connections on Learning
4.5.Resnet
ResNet有效的解决了深度卷积神经网络难训练的问题。这是因为在误差反传的过程中,梯度通常变得越来越小,从而权重的更新量也变小。这个导致远离损失函数的层训练缓慢,随着层数的增加这个现象更加明显。之前有两种常用方案来尝试解决这个问题:
- 按层训练。先训练靠近数据的层,然后慢慢的增加后面的层。但效果不是特别好,而且比较麻烦。
- 使用更宽的层(增加输出通道)而不是更深来增加模型复杂度。但更宽的模型经常不如更深的效果好。
ResNet通过增加跨层的连接来解决梯度逐层回传时变小的问题。虽然这个想法之前就提出过了,但ResNet真正的把效果做好了。
下图演示了一个跨层的连接。
最底下那层的输入不仅仅是输出给了中间层,而且其与中间层结果相加进入最上层。这样在梯度反传时,最上层梯度可以直接跳过中间层传到最下层,从而避免最下层梯度过小情况。
为什么叫做残差网络呢?我们可以将上面示意图里的结构拆成两个网络的和,一个一层,一个两层,最下面层是共享的。
在训练过程中,左边的网络因为更简单所以更容易训练。这个小网络没有拟合到的部分,或者说残差,则被右边的网络抓取住。所以直观上来说,即使加深网络,跨层连接仍然可以使得底层网络可以充分的训练,从而不会让训练更难。
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里来自跳层的输出不是通过加法(+
)而是拼接(concat
)来跟目前层的输出合并。因为是拼接,所以底层的输出会保留的进入上面所有层。这是为什么叫“稠密连接”的原因
稠密块(Dense Block):
我们先来定义一个稠密连接块。DenseNet的卷积块使用ResNet改进版本的BN->Relu->Conv
。每个卷积的输出通道数被称之为growth_rate
,这是因为假设输出为in_channels
,而且有layers
层,那么输出的通道数就是in_channels+growth_rate*layers
。
| |
Desnet通过将ResNet里的+
替换成concat
从而获得更稠密的连接。
练习
- DesNet论文中提交的一个优点是其模型参数比ResNet更小,想想为什么?
- DesNet被人诟病的一个问题是内存消耗过多。真的会这样吗?可以把输入换成$224\times 224$(需要改最后的
AvgPool2D
大小),来看看实际(GPU)内存消耗。 - 这里的FashionMNIST有必要用100+层的网络吗?尝试将其改简单看看效果。
4.7.图片增强
图片增强通过一系列的随机变化生成大量“新”的样本,从而减低过拟合的可能。现在在深度卷积神经网络训练中,图片增强是必不可少的一部分。
常用增强方法:
我们首先读取一张400x500的图片作为样例
水平方向翻转图片是最早也是最广泛使用的一种增强。
以.5的概率做翻转
| |
| |
我们也可以随机裁剪一块随机大小的区域
随机裁剪,要求保留至少0.1的区域,随机长宽比在.5和2之间。 常用一些 有剪切和变形
最后将结果resize到200x200
| |
颜色变化
形状变化外的一个另一大类是变化颜色。
随机将亮度增加或者减小在0-50%间的一个量
| |
| |
cifar10使用增强
| |
| |
不使用增强训练结果
| |
完整代码:
resnet18
| |
imageaugmentation
| |
8.1 使用Gluon实现SSD
本章利用介绍的SSD来检测野生皮卡丘
数据集下载:
训练数据rec下载
训练数据idx下载
测试数据rec下载
| |