Contents
本文主要介绍AlexNet网络结构以及如何使用AlexNet来训练自己的数据集。
讲到AlexNet,就不得不提ImageNet以及ImageNet LSVRC大赛,ImageNet是一项大型计算机视觉系统识别项目,是目前世界上图像识别最大的数据库,截止2016年,ImageNet提供超过10亿张手工标记的图片。从2010年,ImageNet组开始一年一度的the ImageNet Large Scale Visual Recognition Challenge (大规模视觉识别大赛,简称ILSVRC)。本文要介绍的AlexNet就是由Alex提出的网络结构模型,此网络结构赢得了2112届的冠军,同时刷新了image classification的记录,奠定了deep learning在计算机视觉中的地位。
Alex是一个大型深度卷积神经网络,其优势在于:网络增大(5个卷积层+3个全连接层+1个softmax层),同时解决过拟合(dropout,data augmentation,LRN),并且利用多GPU加速计算,在ILSVRC-2012大赛中,以top-5测试误差率15.3%取得了胜利。
AlexNet的结构
该网络包括八个带权层;前五层是卷积层,剩下三层是全连接层。最后一个全连接层的输出被送到一个1000-way的softmax层,其产生一个覆盖1000类标签的分布。这使得多分类的Logistic回归目标最大化,相当于最大化了预测分布下训练样本中正确标签的对数概率平均值。
AlexNet的成功原因在于:
- 每个卷积层及全连接层的输出都应用了ReLU非线性激活函数
- 使用数据增强和Dropout来防止过拟合
- 大数据训练:百万级ImageNet图像数据
- 多GPU的训练以及LRN归一化层的使用
接下来一一介绍上诉提到的AlexNet的重要之处。
1.ReLu激活函数
Sigmoid是一种常用的非线性激活函数,它能够把输入的连续值压缩到0和1之间,如果是非常大的负数,那么输出为0,如果是非常大的正数,输出为1.但它有一一些致命的缺点:
- 就是当输入非常大或者非常小的时候,会有饱和现象,这些神经元的梯度是接近于0的。如果你的初始值很大的话,梯度在反向传播的时候因为需要乘上一个sigmoid的导数,所以会使得梯度越来越小,这会导致网络变的很难学习
- Sigmoid的output不是0均值,这会导致后一层的神经元将得到上一层输出的非0均值的信号作为输入。
ReLU的数学表达式f(x)=max(0,x),由于ReLU的线性,而且不可导(ReLU的导数始终为1),所以相比于sigmoid/tanh,ReLU只需要一个阈值就可以得到激活值,而不用去计算一大堆的复杂运算,使用ReLu得到的SGD的收敛速度会比sigmoid/tanh快很多。
sigmoid:
tanh:
2.数据增强
减少图像数据过拟合最简单最常用的方法是使用标签-保留转换,人为地扩大数据库。AlexNet使用了数据增强的两种不同形式,这两种形式都允许转换图像用很少的计算量从原始图像中产生。第一种形式是对图像进行翻转和平移变换。训练时候,对256256的图片进行随机crop到224224,然后允许水平翻转,这样相当于将样本倍增到((256-224)^2)*2=2048。测试时候,对左上、右上、左下、右下、中间做了5次crop,然后翻转,共10个crop。之后对结果求平均。第二种形式改变训练图像中RGB通道的强度。Alex对RGB空间做PCA,然后对主成分做一个(0,0.1)的高斯扰动。使用这种方案大大增加了数据集,这样就可以增大网络结构,否则会因为数据集过小而导致过拟合,从而只能使用小的网络结构。
3.DropOut
结合预先训练好的许多不同模型。来进行预测是一种非常成功的减少测试误差的方式,但因为每一个模型的训练都需要花费好长时间,所以这种做法对于大型神经网络来说太过昂贵,AlexNet采用了Dropout技术,使得在训练中只需要花费两倍于单模型的时间。其核心思想就是以0.5的概率,将每个隐层神经元的输出设置为0,以这种方式“droppedout”的神经元既不参与前向传播,也不参与反向传播。所以每次输入一个样本就相当于该神经网络尝试了一个新的结构,并且所有这些结构之间共享权重。因为神经元不依赖于其他特定神经元而存在,所以这种技术降低了神经元复杂的互适应关系。也正因为如此,网络需要被迫学习更为鲁邦的特征,这些特征在结合其他神经元的一些不同随机子集时是有用的。在测试时,AlexNet将所神经元的输出都乘以0.5,对于获取指数级dropout网络产生的预测分布的几何平均值,这是一个合理的近似方法。AlexNet中有两个全连接层使用dropout,使收敛所需要的迭代次数大致增加了一倍,避免了大量的过拟合
4.多GPU训练
单个GTX580GPU只有3GB内存,这限制了可以在其上训练的网络的最大规模。事实证明,120万个训练样本才足以训练网络,这个网络太大了,所以不适合在一个GPU上训练,因此他们将网络分布在两个GPU上。目前的GPU特别适合跨GPU并行化,因为它们能够直接从另一个GPU的内存中读出和写入,不需要通过主机内存。AlexNet采用的并行方案是在每个GPU中放置一半核(或神经元)。还有一个额外的技巧是GPU间的通讯只在某些层进行。例如图2所示,第3层的核需要从低层中所有核映射输入,然而,第4层的核只需要从第3层中位于同一GPU的那些核映射输入。
5.LRN归一化层
(Local Response Normalization)局部响应归一化,这个层是为了防止激活函数的饱和的。与ReLU具有相同的功能,使用局部归一化方案有助于一般化。L 这种响应归一化实现了一种侧向抑制,在使用不同核计算神经元输出的过程中创造对大激活度的竞争。 从试验结果看,LRN操作可以提高网络的泛化能力,将错误率降低了大约1个百分点。
使用AlexNet训练自己的数据集
Caffe中AlexNet的网络结构及其相应的solver文件在$CaffeRoot/models/bvlc_alecnet目录下,train_val.prototxt是网络结构定义文件,里面使用标签的形式来描述,这里推荐一个网址(http://ethereon.github.io/netscope/#/preset/alexnet,该网址实现了使用图的形式来显示网络结构。solver.prototxt是训练时的一些配置,包括迭代次数,初始学习率,输出caffemodel的地址,测试间隔等等。
- 创建自己的图片数据集,放在$CaffeRoot/data下,这样便于管理。生成标签txt文本,文本的格式如下:
/home/linbiyuan/caffe/data/wow_style/images/8796f8b306157272353df8709083a36ec0af61c2.jpg 0
需要说明一点就是,类别标签需要从0开始(即将你相应的类别转成数字,从0往上累加),否则会出现一些问题
由于我的图片时网站上爬下来的,所以爬的时候同时进行了处理,爬取的过程传送门见这里()。你也可以自己写Python代码,或者shell脚本生成上述文本。
shell参考:
获取文件夹下所有的图片名称1
2
3image_path='/home/linbiyuan/caffe/data/wow_style/img/*.jpg'
image_label='0'
for image_name in $image_path;do python inputTxt.py $image_name $image_label;done
python代码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
26import argparse
import sys,os
def writeouput(result,path):
if os.path.isfile(path):
f=open(path,'a')
f.write(result+'\n')
f.close()
else:
f=open(path,'w')
f.write(result+'\n')
f.close()
def main(argv):
parser = argparse.ArgumentParser()
parser.add_argument(
"image_name",
help="Input image, directory, or npy."
)
parser.add_argument(
"image_label",
help="Output npy filename."
)
args=parser.parse_args()
writeouput(args.image_name+" "+args.image_label,'/home/linbiyuan/caffe/data/wow_style/all.txt')
if __name__ == '__main__':
main(sys.argv)
生成训练集和测试集
可以将整个数据集按3:1的比例划分成训练集和测试集,python代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20from numpy.matlib import random
dataSet=[]
fileIn=open('/home/linbiyuan/caffe/data/wow_style/all.txt')
for line in fileIn.readlines():
dataSet.append(line.strip())
random.shuffle(dataSet)
pos=int(len(dataSet)*.75)
traindata=dataSet[:pos]
testdata=dataSet[pos:]
f=open('/home/linbiyuan/caffe/data/wow_style/pictrain.txt','w')
for row in traindata:
f.write(row+'\n')
f.close()
f=open('/home/linbiyuan/caffe/data/wow_style/pictest.txt','w')
for row in testdata:
f.write(row+'\n')
f.close()生成lmdb文件
在$CaffeRoot/examples/imagenet下创建一个目录,将接下来要生成或要用到的文件都放在这个目录下,便于管理
使用imagenet下的create_imagenet.sh文件生成lmdb文件,将这个文件拷贝到自己的目录下,修改一些地方,如下:
1 | #!/usr/bin/env sh |
在caffe根目录下运行该shell,文件夹中就会多出两个lmdb的文件夹
./examples/imagenet/wowpic /create_imagenet.sh
- 对现有图片求均值
使用imagenet中的make_imagenet_mean.sh对训练图片数据求均值,将其拷贝到你的文件目录下,修改下路径:1
2
3
4
5
6
7
8
9
10
11
12
13#!/usr/bin/env sh
# Compute the mean image from the imagenet training lmdb
# N.B. this is available in data/ilsvrc12
EXAMPLE=examples/imagenet/wowpic
# DATA=data/ilsvrc12
TOOLS=build/tools
$TOOLS/compute_image_mean $EXAMPLE/train_lmdb \ \ #调用compute_iamge_mean计算均值,其cpp代码在tool目录下
$EXAMPLE/imagenet_mean.binaryproto
echo "Done."
在caffe根目录下运行:
./examples/imagenet/wowpic /make_imagenet_mean.sh
- 修改网络的训练参数
都需要将要修改的文件拷贝到自己的目录下,再修改。
(1)修改train_val.prototxt1
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
41name: "AlexNet"
layer {
name: "data"
type: "Data"
top: "data"
top: "label"
include {
phase: TRAIN
}
transform_param {
mirror: true
crop_size: 227
mean_file: "examples/imagenet/wowpic/imagenet_mean.binaryproto" #均值二进制文件的存放目录
}
data_param {
source: "examples/imagenet/wowpic/train_lmdb" #数据的存放目录
batch_size: 50 #批处理的数据的量,可根据你的GPU大小来更改。理论上batch小是不会影响收敛的。小batch主要的问题是在FC层的计算可能会不是很efficient,但是数学上没有问题
backend: LMDB
}
}
layer {
name: "data"
type: "Data"
top: "data"
top: "label"
include {
phase: TEST
}
transform_param {
mirror: false
crop_size: 227
mean_file: "examples/imagenet/wowpic/imagenet_mean.binaryproto" #上同
}
data_param {
source: "examples/imagenet/wowpic/test_lmdb" #上同
batch_size: 5
backend: LMDB
}
}
(2)修改solver.prototxt,根据自己的情况修改1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21net: "examples/imagenet/wowpic/train_val.prototxt" #网络模型文件路径
test_iter: 100 #test的迭代次数,批处理大小为50, 100*50为测试集个数
test_interval: 100 #训练时每迭代500次测试一次
base_lr: 0.01 #学习率
lr_policy: "step" #学习率的改变策略
gamma: 0.1 #学习率每次改变的值
stepsize: 500 #步长,即没迭代500,base_lr=base_lr*gamma
display: 20 #每迭代20次显示,前面调参的时候,可以多显示,真正训练可调高该数值
max_iter: 1000 #最大迭代次数
momentum: 0.9 #动量
weight_decay: 0.0005 #权重衰减
snapshot: 200 #每迭代200次存储一次Caffemodel。即训练好的模型
snapshot_prefix: "examples/imagenet/wowpic" #训练好的模型存放的位置
solver_mode: GPU #使用GPU 训练
- 开始训练
将train_caffenet.sh放到自己的文件目录下:1
2
3#!/usr/bin/env sh
#set -e
./build/tools/caffe train --solver=examples/imagenet/wowpic/solver.prototxt
在CAFFE根目录下运行:./examples/imagenet/wowpic/train_caffenet.sh。至此训练过程就开始。执行过程是:首先初始化参数,输出solver.prototxt中的内容;然后,初始化网络结构,即train_val中的内容,接下来就是进入训练过程。可以看到,在经过一段时间的迭代后:loss减少,准确率提高,说明训练正在收敛。
- 训练过程遇到的问题
Check failed: error == cudaSuccess (2 vs. 0) out of memory
这个问题的原因是内存不够用,可以改小网络结构中的batch_size值