本系列文章面向深度学习研发者,希望通过Image Caption Generation,一个有意思的具体任务,深入浅出地介绍深度学习的知识。本系列文章涉及到很多深度学习流行的模型,如CNN,RNN/LSTM,Attention等。本文为第13篇。

作者:李理 
目前就职于环信,即时通讯云平台和全媒体智能客服平台,在环信从事智能客服和智能机器人相关工作,致力于用深度学习来提高智能机器人的性能。

相关文章: 

李理:从Image Caption Generation理解深度学习(part I) 
李理:从Image Caption Generation理解深度学习(part II) 
李理:从Image Caption Generation理解深度学习(part III) 
李理:自动梯度求解 反向传播算法的另外一种视角 
李理:自动梯度求解——cs231n的notes 
李理:自动梯度求解——使用自动求导实现多层神经网络

李理:详解卷积神经网络

李理:Theano tutorial和卷积神经网络的Theano实现 Part1

李理:Theano tutorial和卷积神经网络的Theano实现 Part2

李理:卷积神经网络之Batch Normalization的原理及实现

李理:卷积神经网络之Dropout

李理:三层卷积网络和vgg的实现

6. 使用caffe在imagnet上训练AlexNet

接下来我们介绍一下怎么用caffe训练ILSVRC2012。我们之前为了让大家理解卷积神经网络的细节原理,所以都是自己实现。但是在实际工作中,我们更倾向于使用成熟的框架来进行深度学习。caffe是一个老牌的深度学习框架,尤其是在计算机视觉领域很受欢迎。我们这里会简单的介绍一下怎么使用caffe训练ILSVRC2012,包括怎么用pycaffe使用训练好的模型,这在后面的Image Caption里会用到。

6.1 ILSVRC2012和ImageNet介绍

我们之前也多次提到这两个词,有时也不加区分的使用它们。这里我们稍微澄清一下它们的关系和区别。

首先ImangeNet是Stanford视觉实验室标注的一个图像数据库,有上万个分类和上千万的图片。截止到本文发表,最新的数据是“14,197,122 images, 21841 synsets indexed”。我们这个系列教程中多次涉及的课程cs231n就是stanford视觉实验室李飞飞教授开设的课程。

为了让学术界有个公开标准的图像分类【也包括Location】,Stanford视觉实验室从2010年开始就开始 ILSVRC(ImageNet Large Scale Visual Recognition Challenge) ,翻译成中文就是ImageNet大规模视觉竞赛。在2012年之前,最好的top5分类错误率在20%以上,而2012年AlexNet首次在比赛中使用了深层的卷积网络,取得了16%的错误率,一时让深度学习名声大噪。之后每年都有新的好成绩出现,2014年是GoogLeNet和VGG,而2015年是ResNet參差网络。目前最好系统的top5分类错误率在5%一下了。

ILSVRC2012是2012年的比赛数据,是最流行的一个大规模分类数据,它包含1000个分类和100,000+训练数据。

6.2 caffe简介

caffe是berkeley视觉实验室开源的一个流行深度学习框架,更多细节读者可以参考一些官网资料。因为本文的目的也不是介绍各种工具,为了避免文字过于冗长,就不仔细介绍了,下面介绍训练步骤时简要的做一些解释。读者有了自己实现CNN的基础,理解怎么使用caffe应该不会太难。安装【包括cuda的支持】请参考官网,建议读者用Ubuntu的系统,安装比较简单,记得安装python的支持。

6.3 获取ILSVRC2012数据

最开始下载ILSVRC2012需要注册才能获得下载地址,现在已经完全开放,如果读者想训练ILSVRC2012,建议使用GPU,否则就得等几个月。另外机器要有比较大的硬盘空间来存放原始的数据【100多G】。

如果读者不打算自己训练,也不影响,因为网上有很多训练好的模型,我们可以直接拿过来用,因此可以跳过本节。

下载地址:http://www.image-net.org/challenges/LSVRC/2012/nonpub-downloads

这个比赛包括分类和定位,由于我们只做分类,所以只需要下载

  1. Training images (Task 1 & 2) 138GB. MD5: 1d675b47d978889d74fa0da5fadfb00e

  2. Validation images (all tasks) 6.3GB. MD5: 29b22e2961454d5413ddabcf34fc5622

  3. Test images (all tasks) 13GB. MD5: fe64ceb247e473635708aed23ab6d839

  4. Training bounding box annotations (Task 1 & 2 only)

  5. Validation bounding box annotations (all tasks)

数据比较大,可能要下载一两天,用wget可以用-c选项支持断点续传。建议北京时间白天下载,对于美国来说是晚上,速度较快,最快可以到1MB/s。下载完了记得用md5检验一下

下载完了解就行了。

训练数据是这样的目录结果:/path/to/imagenet/train/n01440764/n01440764_10026.JPEG

其中n01440764就是代表一个类别,读者可以选择一些图片看看。

测试数据是:/path/to/imagenet/val/ILSVRC2012_val_00000001.JPEG

6.4 数据预处理

1. 辅助数据下载

首先要下载一下辅助的数据,进入caffe的根目录执行 
$./data/ilsvrc12/get_ilsvrc_aux.sh

这个脚本会下载一些数据到data/ilsvrc12。包括train.txt val.txt和test.txt,这些文件包含图片路径【相对路径】和分类的对应关系

2. 缩放图片

由于图片的大小不一样,我们一般需要预先缩放图片。另外caffe使用leveldb来存储图片【为了统一管理和queue处理,上百G的图片不可能想mnist数据那样直接放到内存,我们只能把要训练的部分数据放到内存里,为了提供效率,那么需要队列这样的集中来prefetch图片到内存】,这都统一在脚本examples/imagenet/create_imagenet.sh里了。

我们需要修改一下这个脚本,主要是imagenet图片的位置,输出的目录。下面是我的配置,请参考后修改

EXAMPLE=/bigdata/lili/imagenet
DATA=data/ilsvrc12
TOOLS=build/tools

TRAIN_DATA_ROOT=/bigdata/lili/imagenet/train/
VAL_DATA_ROOT=/bigdata/lili/imagenet/val/

# Set RESIZE=true to resize the images to 256x256. Leave as false if images have
# already been resized using another tool.
RESIZE=true

然后在caffe的根目录运行这个脚本,注意这个脚本可能要运行好几个小时【我记不清楚了,反正第一天运行第二天才完成】

6.5 计算图片的mean值

就像batch normaliztion一样,我们一般需要把数据变成均值0的数据,计算mean就是这个用途。 
./examples/imagenet/make_imagenet_mean.sh 
这个脚本将生成data/ilsvrc12/imagenet_mean.binaryproto这个文件 
注意:我使用这个脚本生成的mean文件训练时不收敛,不知道什么原因,去caffe的论坛发现别人也有碰到类似的问题,请参考 https://github.com/BVLC/caffe/issues/5212https://github.com/BVLC/caffe/issues/4482。 
我后来之后上网下载了一个mean文件就能训练了。需要的读者可以从这里下载:http://dl.caffe.berkeleyvision.org/caffe_ilsvrc12.tar.gz,解压后就有imagenet_mean.binaryproto,放到data/ilsvrc12/下就行了。

6.6 修改配置文件

1. models/bvlc_reference_caffenet/train_val.prototxt

修改训练和测试的source:

data_param {    #source: "examples/imagenet/ilsvrc12_train_lmdb"
    source: "/bigdata/lili/imagenet/ilsvrc12_train_lmdb"
    batch_size: 32
    backend: LMDB  }    data_param {    #source: "examples/imagenet/ilsvrc12_val_lmdb"
    source: "/bigdata/lili/imagenet/ilsvrc12_val_lmdb"
    batch_size: 50
    backend: LMDB  }

2. models/bvlc_reference_caffenet/train_val.prototxt

修改模型snapshot目录的位置:

snapshot_prefix: "/bigdata/lili/imagenet/googlenetmodel/bvlc_googlenet"

6.7 训练

./build/tools/caffe train –solver=models/bvlc_reference_caffenet/solver.prototxt 
如果是一个GPU,一般两三天也就收敛了,我最终得到的准确率在58%左右。

7. 使用训练好的模型进行分类

caffe代码里包含一个examples/00-classification.ipynb。我们用ipython notebook就可以像之前那样工作了。当然很可能我们是在某个GPU服务器上训练的数据,包括那些图片也放在上面,那么我们可以使用ipython的 –ip参数让ipython监听在非localhost上,这样我们使用笔记本也可以访问【这其实是ipython notebook好用之处,我们不需要在服务器上用vim编写代码或者在本地和服务器直接拷贝代码了,我们用这种方式就可以在本地笔记本开发python代码,而直接在服务器上调试和运行了】

ipython notebook –ip 这个机器的内网或者外网ip 
启动后在终端会出现带token的链接: http://xxxx:8888/?token=35f30c22abf0a6c5d755e631cac5e7a9444aac829a5082b

这就在笔记本打开这个链接就行了。

7.1 cell1

初始化的代码,直接运行

7.2 cell2

设置caffe_root。直接运行应该就可以,如果有路径问题修改caffe_root为caffe的绝对路径肯定是不会有问题的。

7.3 cell3

下载模型,如果你训练了自己的模型,可以跳过这一步,否则这段代码会去下载网上已有的模型。注意模型上百M

7.4 cell4

定义和加载模型。如果你使用自己训练的模型,可以参考我的修改,否则不需要更改直接运行。

model_weights = '/bigdata/lili/imagenet/models2/caffenet_train_iter_160000.caffemodel'

7.5 cell5

数据预处理对象caffe.io.Transformer 
Our default CaffeNet is configured to take images in BGR format. Values are expected to start in the range [0, 255] and then have the mean ImageNet pixel value subtracted from them. In addition, the channel dimension is expected as the first (outermost) dimension.

默认的CaffeNet是使用BGR格式的图像【这和我们一般图像处理工具得到RBG是不一样的】。并且取值是0-255【另外一种表示方法就是0-1】,然后会减去RGB的平均值。另外Caffe的Blob是(N, C, H, W)的,我们从图像里得到的一般是(H, W, C)。

所以需要经过预处理把我们读到的数据转成Caffe需要的格式。Caffe提供了一个类caffe.io.Transformer,我们直接用这个类就可以方便的完成这些工作【当然你自己实现也是完全没有问题的,那么不需要这个类了】

# load the mean ImageNet image (as distributed with Caffe) for subtraction
mu = np.load(caffe_root + 'python/caffe/imagenet/ilsvrc_2012_mean.npy')
mu = mu.mean(1).mean(1)  # average over pixels to obtain the mean (BGR) pixel valuesprint 'mean-subtracted values:', zip('BGR', mu)

# create transformer for the input called 'data'transformer = caffe.io.Transformer({'data': net.blobs['data'].data.shape})

transformer.set_transpose('data', (2,0,1))  # move image channels to outermost dimension
transformer.set_mean('data', mu)            # subtract the dataset-mean value in each channel
transformer.set_raw_scale('data', 255)      # rescale from [0, 1] to [0, 255]
transformer.set_channel_swap('data', (2,1,0))  # swap channels from RGB to BGR

前面3行是读取mean文件。

第一行是读取mean文件,得到一个(3,256,256)的ndarray,代表BGR3个channel在每个像素点的平均值。

但是我们使用的时候是对每个channel减去同一个值域【不是每个位置减不同的值】,所以我们需要平均每个channel的256*256个值,这就可以用第二行代码mu=mu.mean(1).mean(1)来完成,这行代码的意思就是先对H求平均,得到一个(3,256)的ndarray,然后在对W求平均【注意这个时候H的维度下标从2变成1了】 
其实用mu.mean(2).mean(1)也是等价的。总之就是对每个channel的256个值求平均。

第三行只是打印出来看看,我这里打印出来是:

mean-subtracted values: [('B', 104.0069879317889), ('G', 116.66876761696767), ('R', 122.6789143406786)]

接下来构造一个caffe.io.Transformer对象,它变化的数据是输入层net.blobs[‘data’].data,所以它变换的数据的shape是net.blobs[‘data’].data.shape

对caffe不熟悉的读者可以阅读一下caffe的官方文档。

我们【下面】代码读入的图片是(N, H, W, C)的ndarray,如果一次读取一个图片,那么N就是1,H和W分别是图片的高度和宽度,而C就是3代表RGB

但是Caffe要求(N, C, H, W),所以需要下面这个transpose:

transformer.set_transpose('data', (2,0,1))

接下来这行transformer.set_mean(‘data’, mu) 告诉它要减去我们之前得到的BGR3个channel的均值

而transformer.set_raw_scale(‘data’, 255) 让它把0-1的范围缩放到0-255。

最后因为我们读入的channel顺序是RGB,而caffe要求BGR,所以再加上最后这一行:

transformer.set_channel_swap('data', (2,1,0))

7.6 cell6

这个cell显示了我们一次分类多个数据,其实也可以把batch改成1,这里只是为了演示可以一次分类多个,这会比一次分类一个加起来快。

# set the size of the input (we can skip this if we're happy
#  with the default; we can also change it later, e.g., for different batch sizes)
net.blobs['data'].reshape(50,        # batch size                          3,         # 3-channel (BGR) images                          227, 227)  # image size is 227x227

注意,我们之前训练imagenet时会把图像缩放成227*227的,参考models/bvlc_reference_caffenet/train_val.prototxt:

layer {  name: "data"
  type: "Data"
  top: "data"
  top: "label"
  include {
    phase: TRAIN  }  transform_param {    mirror: true
    crop_size: 227
    mean_file: "data/ilsvrc12/imagenet_mean.binaryproto"
  }

7.7 cell7

接下来我们从example里读取一个图片,这是一只很可爱的猫。

image = caffe.io.load_image(caffe_root + 'examples/images/cat.jpg')
transformed_image = transformer.preprocess('data', image)
plt.imshow(image)

图片描述

7.8 cell8

我们用模型来预测:

# copy the image data into the memory allocated for the netnet.blobs['data'].data[...] = transformed_image### perform classificationoutput = net.forward()

output_prob = output['prob'][0]  # the output probability vector for the first image in the batchprint 'predicted class is:', output_prob.argmax()

其实就是把data输入层放置成我们的这个图片,注意,我们之前定义的输入是(50,3,227,227),但是我们这里只传了一张(1,3,227,227)的图片,所以它会broadcasting成50个一样的图片,当然实际我们应该传人50个不同的图片。这里当然只是个演示。

然后直接调用net.forward(),然后取出output[‘prob’][0],得到第一个图片的输出概率。这个prob在哪里定义的呢?感兴趣的读者可以打开models/bvlc_reference_caffenet/deploy.prototxt阅读。

注意,我们训练和测试的时候用的是models/bvlc_reference_caffenet/train_val.prototxt,那个时候我们只关心loss(train phase)和accuracy(test phase)。而真正用来预测的时候,我们关心的是softmax的输出概率。

如果模型训练的没有问题的话,这只猫的分类应该是: 
predicted class is: 281

再往下面的内容这个系列教程不会用到,我们只要能够读取caffe的model,然后用它来预测就可以了,所以后面的cell就不一一介绍了,感兴趣的读者可以自行阅读。

8. ResNet

ResNet注意可以参考两篇论文: Deep Residual Learning for Image Recognition 和 Identity Mappings in Deep Residual Networks。 这里有不少有用的链接,包括icml16上的tutorial和一些ResNet的实现代码。

ResNet是Residual Network的缩写,翻译成中文就是残差网络。在介绍这种网络结构之前,我们来回顾一下之前的经验。

根据之前的经验,神经网络越深,效果越好。但是网络变深了之后就不好优化,模型训练时不容易收敛,当然我们可以用一些参数初始化和batch normalization的技术让我们可以训练较深的网络,比如十几层的VGG。

另外本文作者还提出了一个有意思的现象degradation问题——网络变得非常深以后,效果反而不如浅层的网络,甚至在训练数据上都不如(因此不是overfitting的问题)。【注:Inception v4那篇论文里作者似乎不太同意这个观点,他们没有用残差结构也能训练很深的网络,这说明本文作者参数没调好导致没收敛?但是他还是承认使用了残差结构确实可以加快训练的收敛速度】

作者在cifar10数据集行比较了,下图说明如果不用残差网络,深的网络效果反而不如浅的。

图片描述

56层的网络效果反而不如20层的。作者在ImageNet上也发现同样的现象。

我觉得可能原因是网络层数多,参数多,不使用ResNet比较难收敛。

为了解决训练收敛慢的问题,作者提出了残差结构的想法。

这个想法非常简单。比如一个网络(比如是两个卷积加relu)需要学习(拟合)一个H(x)函数,之前我们就是直接学习函数H(x)。而残差网络让它学习另外一个函数F(x)=H(x)-x。如下图所示:

图片描述

图上在输入和第二个relu之间增加了一个x的直接连接。当然这里要求x的维度和F(x)是一样的,否则没法相加,这种情况一般会给x再乘以一个W使得Wx的维度和F(x)一样。

就是这么一点点改动,作者通过实验验证效果就是比没有改动要好。 
首先作者在ImageNet上训练了普通的34层的网络,然后又训练了加入残差的34层网络,另外使用了VGG18作为baseline,同时也训练了带残差的18层网络,实验对比如下:

图片描述

可以看到,没有残差结果,34层的网络还不如18层。加了残差之后,34层的要比18层的好,当然18层的残差网络也比普通的18层VGG好。

之后就是训练了著名的152层的网络,在ILSVRC15上top5的error讲到了3.57%

不过15年的文章并没有怎么解释为什么加了这样的残差结构就能训练更深的网络,只是实验验证了想法而已。

16年的文章做了更深入一些的分析,分析为什么加入残差结构能使得训练更深的网络变得可能。

图片描述

如上图(a)所示残差网络就是许多残差单元(Residual Units)组成的网络。每个残差单元都是这样的形式:

图片描述

其中输入是 xl ,输出是 xl+1 (输出就是下一层l+1层的输入)。 
在15年的文章里,h(xl)=xl而f是relu。

这里我们先做一些简化,假设 h(xl)=xl以及 f(x)=x。 
我们任意考察残差网络的第l层到第L层。

图片描述

这里我们能发现这种结构很好的一些特性:

1. 任意更深的L层 xL 能表示成 xl 加上一个形如

图片描述

的残差之和。

2.

图片描述

第L层是输入加上残差。而普通的网络如果忽略BN和ReLU的话是

图片描述

如果我们用error对 xlxl 求梯度,会得到:

图片描述

因为公式复杂那一项不会总是-1,因此error从L层 ∂e/∂xL 传到l层不会消失。所以这是可以训练深层网络的关键。

前面我们假设 h(xl)=xl,也就是通路是个identity mapping。如果我们稍微变化一下让 h(xl)=λlxl会是怎么样呢? 
经过简单的推导,我们得到:

图片描述

我们看到括号里不是1而是

图片描述

如果 λi<1 则容易梯度消失,反正会爆炸,总之就没法训练好了。

出来Identity map也就是 h(x)=x ,作者对比很多其它的信息通路,结果发现效果都不好。原因就是通过Identity map,可以让任何两个层直接可以直接传到信息,也就是有一条直接的通路【说明:可以是说可能性,而不是一定会直接传递,这取决于数据的驱动让它是否这么做,就像我们后面会说LSTM相对与可以学习到更long distance的依赖一样】。

基于之上的分析,作者提出了下图b的新结构:

图片描述

图a是15年的工作,从第l层直接没有到l+1层的信息通路,因为这条通路要经过ReLU,不过可能ReLU至少在大于0时是完全可以通过的【不知道换成softmax会不会效果差很多,因为15年的文章没有用过做实验,也不好猜测】

而图b从l层到l+1层是有直接信息通路的,效果会更好。

大致的思路就讲到这里吧,如果要自己实现应该也很简单,有兴趣的读者可以自己实现一个ResNet在cifar数据上跑一跑。论文最好的结果是小于5%的错误率。上面的链接里也有很多现成的ResNet实现,读者可以参考。

9. Inception结构

主要是 Going Deeper with Convolutions、 Rethinking the Inception Architecture for Computer Vision、 Inception-v4, Inception-ResNet and the Impact of Residual Connections on Learning 这三篇论文。

  • 第一篇就是inception v1,也就是GoogLeNet

  • 第二篇是v2和v3

  • 第三篇是v4

第一篇和ResNet一样,没有太多理论依据,只是实验,建议大致阅读一下,重点阅读第二篇。

首先是Inception这个词。《盗梦空间》?这个单词的意思是”起初“,”获得学位“。 
这似乎和电影的情节没半毛钱关系,台湾翻译成《全面启动》就是这个单词的直接翻译,不过知乎的这个解释还是有些道理的。我不知道作者把这样的网络结构叫做Inception是什么意思。

Inception结构的基本思想就是用小的filter组合来替代大的filter,从而减少参数和计算量,这样相同的计算资源也就能训练更深的网络,另外一个好处就是在一些终端设备占用的内存和cpu更少。

我们看到,VGG都是使用3 3的filter,小的filter参数少,但是小的filter不能capture远距离的依赖,相当于损失了模型的表达能力。虽然我们设计网络是层次的,前面的层学习底层的特征,然后后面的层学习更高的特征。但是即使是底层特征,可能有些也没法用太小的filter来学到。所以最早的设计直觉是前面用比较大的filter比如7 7的,随着图片的变小和网络的加深,我们再使用更多更小的filter。我们可以看到7 7的计算量是3 3的5.4倍。

inception的想法就是用多个小的filter来替代大的filter。

图片描述

我们可以用两个3 3的filter stack起来替代一个5 5的filter,当然它们不是完全等价的,因为上层的3 3filter移动一步之后和之前是有重叠的部分的。

图片描述

另外如上图,我们也可以用一些非对称的比如1 3和3 1的filter组合起来替代3 3的filter

此外还有就是1 1的filter也是有用的,它可以起到”降维“压缩信息的作用,比如输入是(C1,H,W),用C2个1 1的filter后就变成了(C2,H,W)的图像。这种压缩的特点是卷积的局部的特性。

大概的思路就是这样,细节就不罗嗦了,有兴趣的读者请参考论文。

10. 总结

通过上面的介绍,我们大致了解了Image Classification里最主流和state of the art的一些卷积网络结构,当然这只是视觉的一个任务,这里没有涉及到Location/Detection和Segmentation等其它任务,所以也没有设计R-CNN fast/faster R-CNN,YOLO SSD等。有兴趣的读者有了上面的基础应该是可以阅读相关的论文了解它们的原理。关于卷积神经网络的内容就介绍到这里,下一篇文章将会讲解RNN/LSTM的相关内容,敬请关注!