最简单的图片分类——手写数字识别
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就能找到很多。
(缺少的部分待后续补充)