Caffe学习系列——2用AlexNet训练自己的数据集

Contents

  1. 1. AlexNet的结构
    1. 1.1. 1.ReLu激活函数
    2. 1.2. 2.数据增强
    3. 1.3. 3.DropOut
    4. 1.4. 4.多GPU训练
    5. 1.5. 5.LRN归一化层
  2. 2. 使用AlexNet训练自己的数据集

本文主要介绍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回归目标最大化,相当于最大化了预测分布下训练样本中正确标签的对数概率平均值。
CNN体系结构示意图
AlexNet的成功原因在于:

  1. 每个卷积层及全连接层的输出都应用了ReLU非线性激活函数
  2. 使用数据增强和Dropout来防止过拟合
  3. 大数据训练:百万级ImageNet图像数据
  4. 多GPU的训练以及LRN归一化层的使用

接下来一一介绍上诉提到的AlexNet的重要之处。

1.ReLu激活函数

Sigmoid是一种常用的非线性激活函数,它能够把输入的连续值压缩到0和1之间,如果是非常大的负数,那么输出为0,如果是非常大的正数,输出为1.但它有一一些致命的缺点:

  1. 就是当输入非常大或者非常小的时候,会有饱和现象,这些神经元的梯度是接近于0的。如果你的初始值很大的话,梯度在反向传播的时候因为需要乘上一个sigmoid的导数,所以会使得梯度越来越小,这会导致网络变的很难学习
  2. Sigmoid的output不是0均值,这会导致后一层的神经元将得到上一层输出的非0均值的信号作为输入。

ReLU的数学表达式f(x)=max(0,x),由于ReLU的线性,而且不可导(ReLU的导数始终为1),所以相比于sigmoid/tanh,ReLU只需要一个阈值就可以得到激活值,而不用去计算一大堆的复杂运算,使用ReLu得到的SGD的收敛速度会比sigmoid/tanh快很多。
sigmoid:sigmoid
tanh: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的地址,测试间隔等等。

  1. 创建自己的图片数据集,放在$CaffeRoot/data下,这样便于管理。生成标签txt文本,文本的格式如下:
    /home/linbiyuan/caffe/data/wow_style/images/8796f8b306157272353df8709083a36ec0af61c2.jpg 0
    需要说明一点就是,类别标签需要从0开始(即将你相应的类别转成数字,从0往上累加),否则会出现一些问题
    由于我的图片时网站上爬下来的,所以爬的时候同时进行了处理,爬取的过程传送门见这里()。你也可以自己写Python代码,或者shell脚本生成上述文本。
    shell参考:
    获取文件夹下所有的图片名称
    1
    2
    3
    image_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
26
import 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)

  1. 生成训练集和测试集
    可以将整个数据集按3:1的比例划分成训练集和测试集,python代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    from 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()
  2. 生成lmdb文件
    在$CaffeRoot/examples/imagenet下创建一个目录,将接下来要生成或要用到的文件都放在这个目录下,便于管理
    使用imagenet下的create_imagenet.sh文件生成lmdb文件,将这个文件拷贝到自己的目录下,修改一些地方,如下:

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
#!/usr/bin/env sh
# Create the imagenet lmdb inputs
# N.B. set the path to the imagenet train + val data dirs
set -e

EXAMPLE=examples/imagenet/wowpic
#DATA=examples/imagenet/wowpic
TOOLS=build/tools

TRAIN_DATA_ROOT=examples/imagenet/wowpic/ #训练样本的存放路径
VAL_DATA_ROOT=examples/imagenet/wowpic/ #测试样本的存放路径

# Set RESIZE=true to resize the images to 256x256. Leave as false if images have
# already been resized using another tool.
RESIZE=false
if $RESIZE; then
RESIZE_HEIGHT=256 #改变图片的大小为256*256

RESIZE_WIDTH=256
else
RESIZE_HEIGHT=0
RESIZE_WIDTH=0
fi

if [ ! -d "$TRAIN_DATA_ROOT" ]; then
echo "Error: TRAIN_DATA_ROOT is not a path to a directory: $TRAIN_DATA_ROOT"
echo "Set the TRAIN_DATA_ROOT variable in create_imagenet.sh to the path" \
"where the ImageNet training data is stored."
exit 1
fi

if [ ! -d "$VAL_DATA_ROOT" ]; then
echo "Error: VAL_DATA_ROOT is not a path to a directory: $VAL_DATA_ROOT"
echo "Set the VAL_DATA_ROOT variable in create_imagenet.sh to the path" \
"where the ImageNet validation data is stored."
exit 1
fi

echo "Creating train lmdb..."
#调用convert_imageset文件转换文件格式,后面为输入参

GLOG_logtostderr=1 $TOOLS/convert_imageset \
--resize_height=$RESIZE_HEIGHT \
--resize_width=$RESIZE_WIDTH \
--shuffle \
$TRAIN_DATA_ROOT \
$EXAMPLE/pictrain.txt \
$EXAMPLE/train_lmdb

echo "Creating val lmdb..."

GLOG_logtostderr=1 $TOOLS/convert_imageset \
--resize_height=$RESIZE_HEIGHT \
--resize_width=$RESIZE_WIDTH \
--shuffle \
$VAL_DATA_ROOT \
$EXAMPLE/pictest.txt \
$EXAMPLE/test_lmdb

echo "Done."

在caffe根目录下运行该shell,文件夹中就会多出两个lmdb的文件夹
./examples/imagenet/wowpic /create_imagenet.sh

  1. 对现有图片求均值
    使用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. 修改网络的训练参数
    都需要将要修改的文件拷贝到自己的目录下,再修改。
    (1)修改train_val.prototxt
    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
    name: "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
21
net: "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 训练

  1. 开始训练
    将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减少,准确率提高,说明训练正在收敛。

  1. 训练过程遇到的问题
    Check failed: error == cudaSuccess (2 vs. 0) out of memory
    这个问题的原因是内存不够用,可以改小网络结构中的batch_size值