深度学习与计算机视觉 实践学习(3)最简单的图片分类——手写数字识别(基于Caffe和LeNet-5)

最简单的图片分类——手写数字识别

LeNet-5在MNIST数据集上训练做手写数字识别——图片分类中的Hello World

1. 准备数据——MNIST

在大多数框架的例子中,用MNIST训练LeNet-5的例子都被脚本高度封装了。只需要执行脚本就可以完成从下载数据到训练的过程。比如在MXNet中,直接到mxnet/example下执行train_mnist.py即可,Caffe中也有类似的shell脚本。

然后这样是不利于初学者了解到底发生了什么。本文将数据准备的部分剥离开,把每个训练都具体到一张图片,然后从头开始完整地过一遍流程。了解这个流程,基本上就了解了如何从图片数据开始到训练一个模型进行分类。

在Linux直接用wget下载即可:

>> wget http://deeplearning.net/data/mnist/mnist.pkl.gz

下载下来的mnist.pkl.gz这个压缩包中其实是数据的训练集、验证集和测试集用pickle导出的文件被压缩为gzip格式,所以用python中的gzip模块当成文件就可以读取。其中每个数据集是一个元组,第一个元素存储的是手写数字图片,表示每张图片是长度为28*28=784的一维浮点型numpy数组,这个数组就是单通道灰度图片按行展开得到,最大值为1,代表白色部分,最小值为0,代表黑色部分。元组中的第二个元素是图片对应的标签,是一个一维的整型numpy数组,按照下标位置对应图片中的数字。基于以上将数据集转换成图片的代码如下,

import os
import pickle, gzip
from matplotlib import pyplot
print('Loading data from mnist.pkl.gz ...')
with gzip.open('mnist.pkl.gz', 'rb') as f:
  train_set, valid_set, test_set = pickle.load(f)
imgs_dir = 'mnist'
os.system('mkdir -p {}'.format(imgs_dir))
datasets = {'train': train_set, 'val': valid_set, 'test': test_set}
for dataname, dataset in datasets.items():
  print('Converting {} dataset ...'.format(data_dir))
  for i, (img, label) in enumerate(zip(*dataset)):
    filename = '{:0>6d}_{}.jpg'.format(i, label)
    filepath = os.sep.join([data_dir, filename])
    img = img.reshape((28, 28))
    pyplot.imsave(filepath, img, cmap='gray')
    if(i+1) % 10000 == 0:
       print('{} images converted!'.format(i+1))

这个脚本首先创建了一个叫mnist的文件夹,然后在mnist下创建3个子文件夹train、val和test,分别包含训练图片、验证图片和测试图片,分别用来保存对应的3个数据集转换后产生的图片。每个文件的命名规则为第一个字段是序号,第二个字段是数字的值,保存为JPG格式。

2.基于Caffe和LeNet-5训练一个用于手写数字识别的模型,并对模型进行评估和测试

(1)制作LMDB

如果是基于Caffe实现,需要先制作LMDB数据,LMDB是Caffe中最常用的一种数据库格式,全称Lightning Memory-Mapped Database(闪电般快速的内存映射型数据库)。除了快,LMDB还支持多程序同时对数据进行读取,这是相比Caffe更早支持的LevelDB的优点。现在LMDB差不多是Caffe用来训练图片最常用的数据格式。

Caffe提供了专门为图像分类任务将图片转换为LMDB的官方工具,路径为caffe/build/tools/convert_imageset。要使用这个工具,第一步是生成一个图片文件路径的列表,每一行是文件路径和对应标签(的下标),用space键或者制表符(Tab)分开。

将前面MNIST文件夹下生成的3个文件夹,train,val和test中的图片路径和对应标签,转换为上面的格式,代码如下,

import os
import sys
input_path = sys.argv[1].rstrip(os.sep)
output_path = sys.argv[2]
filenames = os.listdir(input_path)
with open(output_path, 'w') as f:
    for filename in filenames:
        filepath = os.sep.join([input_path, filename])
        label = filename[:filename.rfind('.')].split('_')[1]
        line = '{} {}\n'.format(filepath, label)
        f.write(line)

把这个文件保存为gen_caffe_imglist.py,然后依次执行下面命令:

>> python gen_caffe_imglist.py mnist/train train.txt

>> python gen_caffe_imglist.py mnist/val val.txt

>> python gen_caffe_imglist.py mnist/test test.txt

这样就生成了3个数据集的文件列表和对应标签。然后直接调用convert_imageset就可以制作lmdb了。

>> /path/to/caffe/build/tools/convert_imageset ./ train.txt train_lmdb --gray --shuffle

>> /path/to/caffe/build/tools/convert_imageset ./ val.txt train_lmdb --gray --shuffle

>> /path/to/caffe/build/tools/convert_imageset ./ test.txt train_lmdb --gray --shuffle

其中,--gray是单通道读取灰度图的选项,--shuffle是个常用的选项,作用是打乱文件列表顺序,但是在本例可有可无,因为本来顺序就是乱的。执行这个工具就是读取图片为opencv的Mat,然后保存到lmdb中。更多convert_imageset的用法可以执行下面命令或者参考源码:

>> /path/to/caffe/build/tools/convert_imageset -h

(2)训练LeNet-5

与Caffe官方例子的版本没有区别,只是输入的数据层 变成了自制的LMDB,用于描述数据源和网络结构的lenet_train_val.prototxt如下,

name: "LeNet"
layer {
  name: "mnist"
  type: "Data"
  top: "data"
  include {
    phase: TRAIN
  }

  transform_param {
    mean_value: 128
    scale: 0.00390625
  }
  data_param {
    source: "../data/train_lmdb"
    batch_size: 50
    backend: LMDB
  }
}

layer {
  name: "mnist"
  type: "Data"
  top: "data"
  include {
    phase: TEST
  }

  transform_param {
    mean_value: 128
    scale: 0.00390625
  }
  data_param {
    source: "../data/val_lmdb"
    batch_size: 100
    backend: LMDB
  }
}

layer {
  name: "conv1"
  type: "Convolution"
  bottom: "data"
  top: "conv1"
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
  convolution_param {
    num_output: 20
    kernel_size: 5
    stride: 1
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}

layer {
  name: "pool1"
  type: "Pooling"
  bottom: "conv1"
  top: "pool1"
  pooling_param {
    pool: MAX
    kernel_size: 2
    stride: 2
  }
}

layer {
  name: "conv2"
  type: "Convolution"
  bottom: "pool1"
  top: "conv2"
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
  convolution_param {
    num_output: 50
    kernel_size: 5
    stride: 1
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}

layer {
  name: "pool2"
  type: "Pooling"
  bottom: "conv2"
  top: "pool2"
  pooling_param {
    pool: MAX
    kernel_size: 2
    stride: 2
  }
}

layer {
  name: "ip1"
  type: "InnerProduct"
  bottom: "pool2"
  top: "ip1"
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
  inner_product_param {
    num_output: 500
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}

layer {
  name: "relu1"
  type: "ReLU"
  bottom: "ip1"
  top: "relu1"
}

layer {
  name: "ip2"
  type: "InnerProduct"
  bottom: "ip1"
  top: "ip2"
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
  inner_product_param {
    num_output: 500
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}

layer{
  name: "accuracy"
  type: "Accuracy"
  bottom: "ip2"
  bottom: "label"
  top: "accuracy"
  include {
    phase: TEST
  }
}

layer {
  name: "loss"
  type: "SoftmaxWithLoss"
  bottom: "ip2"
  bottom: "label"
  top: "loss"
}

数据层的参数,指定均值和缩放比例的作用是,数据减去mean_value然后乘以scale,具体到mnist图片,就是把0~255之间的值缩放到-0.5~0.5,帮助收敛;卷积核的学习率为基础学习率乘以lr_mult,偏置的学习率为基础学习率乘以lr_mult;weight_filler用于初始化参数,xavier是一种初始化方法,源于Bengio组2010年论文《Understanding the difficulty of training deep feedforward neural networks》;在卷积层后面接Pooling层;ReLU单元比起原版的Sigmoid有更好的收敛效果;Accuracy层只是用在验证/测试阶段,用于计算分类的准确率。

除了网络结构和数据,还需要配置一个lenet_solver.prototxt,

net: "lenet_train_val.prototxt"
test_iter: 100
test_interval: 500
base_lr: 0.01
momentum: 0.9
weight_decay: 0.0005
lr_policy: "inv"
gamma: 0.0001
power: 0.75
display: 100
max_iter: 36000
snapshot: 5000
snapshot_prefix: "mnist_lenet"
solver_mode: GPU

更多Solver的详细内容可以参考Caffe官网http://caffe.berkeleyvision.org/tutorial/solver.html

接下来,就可以调用如下命令进行训练啦,

>> /path/to/caffe/build/tools/caffe train -solver lenet_solver.prototxt -gpu 0 -log_dir ./

或者双短线开头的参数命令,

>> /path/to/caffe/build/tools/caffe train --solver=lenet_solver.prototxt --gpu=0 --log_dir=./

不过第二种方式无法使用终端的自动补全,所以没有第一种方式方便哦!

其中,gpu参数是指定要用哪块GPU训练(如果有多块的话,比如一台多卡GPU服务器),如果确实需要,可以用-gpu all参数对所有卡进行训练。log_dir参数指定输出log文件的路径,前提是这个路径必须提前存在。执行命令后会看到打印。

注意因为指定了TEST的数据层,所以输出里按照solver中指定的间隔会输出当前模型在val_lmdb上的准确率和loss。训练完毕,就会生成几个以caffemodel和solverstate结尾的文件,这个就是模型参数和solver状态在指定迭代次数以及训练结束时的存档,名字前缀就是在lenet_solver.prototxt中指定的前缀。当然同时生成的还有log文件,命名是:

caffe.[主机名].[域名].[用户名].log.INFO.[年月日]-[时分秒].[微秒]

Caffe官方也有提供可视化log文件的工具,在caffe\tools\extra下有个plot_training_log.py.example,把这个文件复制一份命名为plot_training_log.py,就可以用来画图,这个脚本的输入参数分别是,图的类型、生成图片的路径和log的路径。

其中,图片类型的输入和对应类型如下:

0:测试准确率 vs. 迭代次数

1:测试准确率 vs. 训练时间(秒)

2:测试loss vs. 迭代次数

3:测试准确率 vs. 迭代次数

4:测试准确率 vs. 训练时间(秒)

5:测试loss vs. 迭代次数

6:测试准确率 vs. 训练时间(秒)

7:测试loss vs. 迭代次数

另外,这个脚本log文件必须以.log结尾。我们用mv命令把log文件名改成mnist_train.log,比如像看看测试准确率和测试的loss随迭代次数的变化,依次执行,

>> python plot_training_log.py 0 test_acc_vs_iters.png mnist_train.log

>> python plot_training_log.py 2 test_loss_vs_iters.png mnist_train.log

(3)测试和评估

测试模型准确率

训练好模型之后,就需要对模型进行测试和评估。其实在训练过程中,每迭代500次就已经在val_lmdb上对模型进行了准确率的评估。不过MNIST除了验证集外还有一个测试集,对于数据以测试集为准进行评估。

评估模型性能

一般来说主要是评估速度和内存占用。

(4)识别手写数字

有了训练好的模型,就可以用来识别手写数字了。我们测试用的是test数据集的图片和之前生成的列表。

(5)增加平移和旋转扰动

直接在样本基础上做扰动增加数据,只是数据增加的方法之一,并且不是一个好的方案,因为增加的数据量有限,并且还要占用原有样本额外的硬盘空间。最好的方法是训练的时候实时对数据进行扰动,这样等效于无限多的随机扰动。其实Caffe的数据层已经自带了最基础的数据扰动功能,不过只限于随机裁剪和随机镜像,并不是很好用。Github上有一些开源的第三方实现的实时扰动的Caffe层,会包含各种常见的数据扰动方式,只需要到github的搜索框中搜caffe augmentation就能找到很多。

(缺少的部分待后续补充)

猜你喜欢

转载自blog.csdn.net/Fan0920/article/details/107562357