这一次我们讲讲 Caffe 这个主流的开源框架从训练到测试出结果的全流程。到此,我必须假设大家已经有了深度学习的基础知识并了解卷积网络的工作原理。
相关的代码、数据都在我们 Git 上,希望大家 Follow 一下这个 Git 项目,后面会持续更新不同框架下的任务。
https://github.com/longpeng2008/LongPeng_ML_Course
这一篇我们说一个分类任务,给大家准备了 500 张微笑的图片、500 张非微笑的图片,放置在 data 目录下,图片预览如下,已经缩放到 60*60 的大小:
这是非微笑的图片:
这是微笑的图片:
01 Caffe 是什么
Caffe 是以 C++/CUDA 代码为主,最早的深度学习框架之一,比 TensorFlow、Mxnet、Pytorch 等都更早,支持命令行、Python 和 Matlab 接口,单机多卡、多机多卡等都可以很方便的使用,CPU 和 GPU 之间无缝切换。
对于入门级别的任务,如图像分类,Caffe 上手的成本最低,几乎不需要写一行代码就可以开始训练,所以我推荐 Caffe 作为入门学习的框架。
Caffe 相对于 TensorFlow 等使用 pip 一键安装的方式来说,编译安装稍微麻烦一些,但其实依旧很简单,我们以 Ubuntu 16.04 为例子,官网的安装脚本足够用了,它有一些依赖库。
sudo apt-get install libprotobuf-dev libleveldb-dev libsnappy-dev libopencv-dev libhdf5-serial-dev protobuf-compiler sudo apt-get install --no-install-recommends libboost-all-devsudo apt-get install libatlas-base-dev sudo apt-get install libgflags-dev libgoogle-glog-dev liblmdb-dev
装完之后,到 Git 上 clone 代码,修改 Makefile.config 就可以进行编译安装,如果其中有任何问题,多 Google,还有什么问题,留言吧。当然,对于有 GPU 的读者,还需要安装 cuda 以及 Nvidia 驱动。
02 Caffe 训练Caffe 完成一个训练,必要准备以下资料:一个是 train.prototxt 作为网络配置文件,另一个是 solver.prototxt 作为优化参数配置文件,再一个是训练文件 list。
另外,在大多数情况下,需要一个预训练模型作为权重的初始化。
(1) 准备网络配置文件
我们准备了一个 3*3 的卷积神经网络,它的 train.prototxt 文件是这样的:
name: "mouth" layer { name: "data" type: "ImageData" top: "data" top: "clc-label" image_data_param { source: "all_shuffle_train.txt" batch_size: 96 shuffle: true } transform_param { mean_value: 104.008 mean_value: 116.669 mean_value: 122.675 crop_size: 48 mirror: true } include: { phase: TRAIN} } layer { name: "data" type: "ImageData" top: "data" top: "clc-label" image_data_param { source: "all_shuffle_val.txt" batch_size: 30 shuffle: false } transform_param { mean_value: 104.008 mean_value: 116.669 mean_value: 122.675 crop_size: 48 mirror: false } include: { phase: TEST} } layer { name: "conv1" type: "Convolution" bottom: "data" top: "conv1" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } convolution_param { num_output: 12 pad: 1 kernel_size: 3 stride: 2 weight_filler { type: "xavier" std: 0.01 } bias_filler { type: "constant" value: 0.2 } } } layer { name: "relu1" type: "ReLU" bottom: "conv1" top: "conv1" } layer { name: "conv2" type: "Convolution" bottom: "conv1" top: "conv2" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } convolution_param { num_output: 20 kernel_size: 3 stride: 2 pad: 1 weight_filler { type: "xavier" std: 0.1 } bias_filler { type: "constant" value: 0.2 } } } layer { name: "relu2" type: "ReLU" bottom: "conv2" top: "conv2" } layer { name: "conv3" type: "Convolution" bottom: "conv2" top: "conv3" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } convolution_param { num_output: 40 kernel_size: 3 stride: 2 pad: 1 weight_filler { type: "xavier" std: 0.1 } bias_filler { type: "constant" value: 0.2 } } } layer { name: "relu3" type: "ReLU" bottom: "conv3" top: "conv3" } layer { name: "ip1-mouth" type: "InnerProduct" bottom: "conv3" top: "pool-mouth" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } inner_product_param { num_output: 128 weight_filler { type: "xavier" } bias_filler { type: "constant" value: 0 } } } layer { bottom: "pool-mouth" top: "fc-mouth" name: "fc-mouth" type: "InnerProduct" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 1 } inner_product_param { num_output: 2 weight_filler { type: "xavier" } bias_filler { type: "constant" value: 0 } } } layer { bottom: "fc-mouth" bottom: "clc-label" name: "loss" type: "SoftmaxWithLoss" top: "loss" } layer { bottom: "fc-mouth" bottom: "clc-label" top: "acc" name: "acc" type: "Accuracy" include { phase: TRAIN } include { phase: TEST } }
可以看出,Caffe 的这个网络配置文件,每一个卷积层,都是以 layer{} 的形式定义,layer 的bottom、top 就是它的输入输出,type 就是它的类型,有的是数据层、有的是卷积层、有的是 loss 层。
我们采用 netscope 来可视化一下这个模型。
从上面看很直观的看到,网络的输入层是 data 层,后面接了3个卷积层,其中每一个卷积层都后接了一个 relu 层,最后 ip1-mouth、fc-mouth 是全连接层。Loss 和 acc 分别是计算 loss 和 acc 的层。
各层的配置有一些参数,比如 conv1 有卷积核的学习率、卷积核的大小、输出通道数、初始化方法等,这些可以后续详细了解。
(2) 准备训练 list
我们看上面的 data layer,可以到
image_data_param 里面有
source: "all_shuffle_train.txt"
它是什么呢,就是输入用于训练的 list,它的内容是这样的:
../../../../datas/mouth/1/182smile.jpg 1 ../../../../datas/mouth/1/435smile.jpg 1 ../../../../datas/mouth/0/40neutral.jpg 0 ../../../../datas/mouth/1/206smile.jpg 1 ../../../../datas/mouth/0/458neutral.jpg 0 ../../../../datas/mouth/0/158neutral.jpg 0 ../../../../datas/mouth/1/322smile.jpg 1 ../../../../datas/mouth/1/83smile.jpg 1 ../../../../datas/mouth/0/403neutral.jpg 0 ../../../../datas/mouth/1/425smile.jpg 1 ../../../../datas/mouth/1/180smile.jpg 1 ../../../../datas/mouth/1/233smile.jpg 1 ../../../../datas/mouth/1/213smile.jpg 1 ../../../../datas/mouth/1/144smile.jpg 1 ../../../../datas/mouth/0/327neutral.jpg 0
格式就是,图片的名字 + 空格 + label,这就是 Caffe 用于图片分类默认的输入格式。
(3) 准备优化配置文件:
net: "./train.prototxt" test_iter: 100 test_interval: 10 base_lr: 0.00001 momentum: 0.9 type: "Adam" lr_policy: "fixed" display: 100 max_iter: 10000 snapshot: 2000 snapshot_prefix: "./snaps/conv3_finetune" solver_mode: GPU
介绍一下上面的参数。
net 是网络的配置路径。test_interval是指训练迭代多少次之后,进行一次测试。test_iter是测试多少个batch,如果它等于 1,就说明只取一个 batchsize 的数据来做测试,如果 batchsize 太小,那么对于分类任务来说统计出来的指标也不可信,所以最好一次测试,用到所有测试数据。因为,常令test_iter*test_batchsize=测试集合的大小。
base_lr、momentum、type、lr_policy是和学习率有关的参数,base_lr和lr_policy决定了学习率大小如何变化。type 是优化的方法,以后再谈。max_iter是最大的迭代次数,snapshot 是每迭代多少次之后存储迭代结果,snapshot_prefix为存储结果的目录,caffe 存储的模型后缀是 .caffemodel。solver_mode可以指定用 GPU 或者 CPU 进行训练。
(4) 训练与结果可视化
我们利用 C++ 的接口进行训练,命令如下:
SOLVER=./solver.prototxt WEIGHTS=./init.caffemodel ../../../../libs/Caffe_Long/build/tools/caffe train -solver $SOLVER -weights $WEIGHTS -gpu 0 2>&1 | tee log.txt
其中,caffe train 就是指定训练。我们可以利用脚本可视化一下训练结果,具体参考git项目:
03 Caffe 测试
上面我们得到了训练结果,下面开始采用自己的图片进行测试。
train.prototxt 与 test.prototxt 的区别
训练时的网络配置与测试时的网络配置是不同的,测试没有 acc 层,也没有 loss 层,取输出的 softmax 就是分类的结果。同时,输入层的格式也有出入,不需要再输入 label,也不需要指定图片 list,但是要指定输入尺度,我们看一下 test.prototxt 和可视化结果。
name: "mouth" layer { name: "data" type: "Input" top: "data" input_param { shape: { dim: 1 dim: 3 dim: 48 dim: 48 } } } layer { name: "conv1" type: "Convolution" bottom: "data" top: "conv1" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } convolution_param { num_output: 12 pad: 1 kernel_size: 3 stride: 2 weight_filler { type: "xavier" std: 0.01 } bias_filler { type: "constant" value: 0.2 } } } layer { name: "relu1" type: "ReLU" bottom: "conv1" top: "conv1" } layer { name: "conv2" type: "Convolution" bottom: "conv1" top: "conv2" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } convolution_param { num_output: 20 kernel_size: 3 stride: 2 pad: 1 weight_filler { type: "xavier" std: 0.1 } bias_filler { type: "constant" value: 0.2 } } } layer { name: "relu2" type: "ReLU" bottom: "conv2" top: "conv2" } layer { name: "conv3" type: "Convolution" bottom: "conv2" top: "conv3" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } convolution_param { num_output: 40 kernel_size: 3 stride: 2 pad: 1 weight_filler { type: "xavier" std: 0.1 } bias_filler { type: "constant" value: 0.2 } } } layer { name: "relu3" type: "ReLU" bottom: "conv3" top: "conv3" } layer { name: "ip1-mouth" type: "InnerProduct" bottom: "conv3" top: "pool-mouth" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } inner_product_param { num_output: 128 weight_filler { type: "xavier" } bias_filler { type: "constant" value: 0 } } } layer { bottom: "pool-mouth" top: "fc-mouth" name: "fc-mouth" type: "InnerProduct" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 1 } inner_product_param { num_output: 2 weight_filler { type: "xavier" } bias_filler { type: "constant" value: 0 } } } layer { bottom: "fc-mouth" name: "loss" type: "Softmax" top: "prob" }
使用 Python 进行测试
由于 Python 目前广泛使用,下面使用 Python 来进行测试,它要做的就是导入模型、导入图片、输出结果。
下面是所有的代码,我们详细解释下:
---代码段1,这一段,我导入一些基本库,同时导入caffe的路径--- #_*_ coding:utf8 import sys sys.path.insert(0, '../../../../../libs/Caffe_Long/python/') import caffe import os,shutil import numpy as np from PIL import Image as PILImage from PIL import ImageMath import matplotlib.pyplot as plt import time import cv2 ---代码段2,这一段,我们添加一个参数解释器,方便参数管理--- debug=True import argparse def parse_args(): parser = argparse.ArgumentParser(description='test resnet model for portrait segmentation') parser.add_argument('--model', dest='model_proto', help='the model', default='test.prototxt', type=str) parser.add_argument('--weights', dest='model_weight', help='the weights', default='./test.caffemodel', type=str) parser.add_argument('--testsize', dest='testsize', help='inference size', default=60,type=int) parser.add_argument('--src', dest='img_folder', help='the src image folder', type=str, default='./') parser.add_argument('--gt', dest='gt', help='the gt', type=int, default=0) args = parser.parse_args() return args def start_test(model_proto,model_weight,img_folder,testsize): ---代码段3,这一段,我们就完成了网络的初始化--- caffe.set_device(0) #caffe.set_mode_cpu() net = caffe.Net(model_proto, model_weight, caffe.TEST) imgs = os.listdir(img_folder) pos = 0 neg = 0 for imgname in imgs: ---代码段4,这一段,是读取图片并进行预处理,还记得我们之前的训练,是采用 BGR 的输入格式,减去了图像均值吧,同时,输入网络的图像,也需要 resize 到相应尺度。预处理是通过 caffe 的类,transformer 来完成,set_mean 完成均值,set_transpose 完成维度的替换,因为 caffe blob 的格式是 batch、channel、height、width,而 numpy 图像的维度是 height、width、channel 的顺序--- imgtype = imgname.split('.')[-1] imgid = imgname.split('.')[0] if imgtype != 'png' and imgtype != 'jpg' and imgtype != 'JPG' and imgtype != 'jpeg' and imgtype != 'tif' and imgtype != 'bmp': print imgtype,"error" continue imgpath = os.path.join(img_folder,imgname) img = cv2.imread(imgpath) if img is None: print "---------img is empty---------",imgpath continue img = cv2.resize(img,(testsize,testsize)) transformer = caffe.io.Transformer({'data': net.blobs['data'].data.shape}) transformer.set_mean('data', np.array([104.008,116.669,122.675])) transformer.set_transpose('data', (2,0,1)) ---代码段5,这一段,就得到了输出结果了,并做一些可视化显示--- out = net.forward_all(data=np.asarray([transformer.preprocess('data', img)])) result = out['prob'][0] print "---------result prob---------",result,"-------result size--------",result.shape probneutral = result[0] print "prob neutral",probneutral probsmile = result[1] print "prob smile",probsmile problabel = -1 probstr = 'none' if probneutral > probsmile: probstr = "neutral:"+str(probneutral) pos = pos + 1 else: probstr = "smile:"+str(probsmile) neg = neg + 1 if debug: showimg = cv2.resize(img,(256,256)) cv2.putText(showimg,probstr,(30,50),cv2.FONT_HERSHEY_SIMPLEX,1,(0,0,255),1) cv2.imshow("test",showimg) k = cv2.waitKey(0) if k == ord('q'): break print "pos=",pos print "neg=",neg if __name__ == '__main__': args = parse_args() start_test(args.model_proto,args.model_weight,args.img_folder,args.testsize)
经过前面的介绍,我们已经学会了 Caffe 的基本使用,但是我们不能停留于此。Caffe 是一个非常优秀的开源框架,有必要去细读它的源代码。
04 总结虽然现在很多人没有从 Caffe 开始学,但是希望提升自己 C++ 水平和更深刻理解深度学习中的一些源码的,建议从 Caffe 开始学起。
本文配套GitHub代码地址为:
https://github.com/longpeng2008/yousan.ai