希望通过这个小例子能够让更多的人熟悉Caffe的基本使用方法。其实如果是通过编写 train.prototxt 来完成一个神经网络框架的编写,是比较容易的,因为层和层之间的结构以及参数规定是写的非常清楚的。本文将不会讲解CNN的任何细节,所有解释仅与程序相关。
此处我们将采用Alexnet这个CNN网络,数据来自于Kaggle。
1.第一步首先是准备数据,此处我们使用Kaggle上提供的带标注的胸部X光片数据集,点此下载。
2.下载到本地后你会发现,这里提供了很多个csv,有一个带有诊断区域的标注,但是由于这个带有区域的标注并不是针对所有的图片,所以我们这里不采用这个。直接使用图片名称对应疾病分类的标注。
3.有了原始图片,我们需要将其转换成LMDB格式,可以加速数据的读取,Caffe使用LMDB格式来存放数据。接下来我们介绍如何使用Caffe自带工具进行格式转换。
首先我们需要编写一个txt文件,用于记录图片的部分路径。
import pandas as pd import os file_path = "/media/veronica/D/X/Data.csv" file = pd.read_csv(file_path) labels = ["No Finding","Infiltration","Atelectasis","Effusion","Nodule","Pneumothorax","Mass","Consolidation","Pleural_Thickening", "Cardiomegaly","Emphysema","Fibrosis","Edema","Pneumonia","Hernia"] index = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14] transfor = dict(zip(labels,index)) length = len(file["ImageIndex"]) with open("labels.txt","w") as f: for i in range(0, length): f.write("/media/veronica/"+file["ImageIndex"][i] + " " + str(transfor[file["FindingLabels"][i]]) + '\n') print("Written %d already , total %d "%(i+1,length)) print("Mission complete")
这个代码比较简单,只是读取一下csv文件,根据标签来进行转换,将标签值对应到0-14,这里需要注意一下,Caffe的标签值需要从0开始,图片前面路径可以写的粗略也可以写的详细一点,因为一会儿转换的时候还需要和主路径构成完整路径。
完成转换之后,我们应该 能得到一个类似下面内容的txt
/media/veronica/00000001_000.png 9 /media/veronica/00000002_000.png 0 /media/veronica/00000003_000.png 14 /media/veronica/00000003_001.png 14 /media/veronica/00000003_002.png 14 /media/veronica/00000003_004.png 14 /media/veronica/00000003_005.png 14 /media/veronica/00000003_006.png 14 /media/veronica/00000003_007.png 14 /media/veronica/00000005_000.png 0 /media/veronica/00000005_001.png 0 /media/veronica/00000005_002.png 0 /media/veronica/00000005_003.png 0 /media/veronica/00000005_004.png 0 /media/veronica/00000005_005.png 0 /media/veronica/00000005_006.png 1 /media/veronica/00000006_000.png 0
使用一下代码进行格式转换:
/home/hunter/caffe-master/build/tools/convert_imageset --resize_width=227 --resize_height=227 --shuffle /media/hunter/OS/X/image/ /media/hunter/OS/Users/Hunter/PycharmProjects/Intel/labels.txt /home/hunter/X/train_lmdb
来看一下第一个参数,是需要找到你Caffe编译之后的convert_imageset这个工具,由于我们这里下载好的数据是1024*1024,但是Alexnet的输入需要是227*227的,所以我们这里使用resize的这两个参数进行尺寸的变换。这里大家可以想一下,我们的原始图像是单通道的,但是Alexnet的输入要求是3通道,我们这里是否需要进行一个小转换呢?
--shuffle
这个参数的意思是是否需要打乱数据。
/media/hunter/OS/X/image/
这个的参数代表图片存储主路径,和之前txt的路径构成图片完整路径,转换的时候会根据这个路径将图片转成LMDB格式。
/media/hunter/OS/Users/Hunter/PycharmProjects/Intel/labels.txt
根据这个路径找到txt文件。
/home/hunter/X/train_lmdb
这就是即将存放转换后文件的路径,这里需要注意以下,train_lmdb这个文件夹会在开始转换后进行创建,务必确保这个文件在命令执行前不存在,否则会报错。
转换之后我们就可以得到训练集,测试集制作方法同上,然后开始介绍一下caffe训练需要的东西。
除了刚才提到的数据,我们还需要指定用什么样格式的网络进行训练,以及训练的时候一些参数的规定,loss的优化方式。我们首先需要定义一个train.prototxt文件,用来定义网络结构。
name: "Disease detect" layer { top: "data" top: "label" name: "data" type: "Data" data_param { source: "/home/hunter/X/train_lmdb" backend:LMDB batch_size: 16 } transform_param { #mean_file: "/home/hunter/X/mean.binaryproto" mirror: true } include: { phase: TRAIN } } layer { top: "data" top: "label" name: "data" type: "Data" data_param { source: "/home/hunter/X/train_lmdb" backend:LMDB batch_size: 16 } transform_param { #mean_file: "/home/hunter/X/mean.binaryproto" mirror: true } 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: 96 kernel_size: 11 stride: 4 weight_filler { type: "gaussian" std: 0.01 } bias_filler { type: "constant" value: 0 } } } layer { name: "relu1" type: "ReLU" bottom: "conv1" top: "conv1" } layer { name: "norm1" type: "LRN" bottom: "conv1" top: "norm1" lrn_param { local_size: 5 alpha: 0.0001 beta: 0.75 } } layer { name: "pool1" type: "Pooling" bottom: "norm1" top: "pool1" pooling_param { pool: MAX kernel_size: 3 stride: 2 } } layer { name: "conv2" type: "Convolution" bottom: "pool1" top: "conv2" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } convolution_param { num_output: 256 pad: 2 kernel_size: 5 group: 2 weight_filler { type: "gaussian" std: 0.01 } bias_filler { type: "constant" value: 0.1 } } } layer { name: "relu2" type: "ReLU" bottom: "conv2" top: "conv2" } layer { name: "norm2" type: "LRN" bottom: "conv2" top: "norm2" lrn_param { local_size: 5 alpha: 0.0001 beta: 0.75 } } layer { name: "pool2" type: "Pooling" bottom: "norm2" top: "pool2" pooling_param { pool: MAX kernel_size: 3 stride: 2 } } layer { name: "conv3" type: "Convolution" bottom: "pool2" top: "conv3" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } convolution_param { num_output: 384 pad: 1 kernel_size: 3 weight_filler { type: "gaussian" std: 0.01 } bias_filler { type: "constant" value: 0 } } } layer { name: "relu3" type: "ReLU" bottom: "conv3" top: "conv3" } layer { name: "conv4" type: "Convolution" bottom: "conv3" top: "conv4" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } convolution_param { num_output: 384 pad: 1 kernel_size: 3 group: 2 weight_filler { type: "gaussian" std: 0.01 } bias_filler { type: "constant" value: 0.1 } } } layer { name: "relu4" type: "ReLU" bottom: "conv4" top: "conv4" } layer { name: "conv5" type: "Convolution" bottom: "conv4" top: "conv5" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } convolution_param { num_output: 256 pad: 1 kernel_size: 3 group: 2 weight_filler { type: "gaussian" std: 0.01 } bias_filler { type: "constant" value: 0.1 } } } layer { name: "relu5" type: "ReLU" bottom: "conv5" top: "conv5" } layer { name: "pool5" type: "Pooling" bottom: "conv5" top: "pool5" pooling_param { pool: MAX kernel_size: 3 stride: 2 } } layer { name: "fc6" type: "InnerProduct" bottom: "pool5" top: "fc6" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } inner_product_param { num_output: 4096 weight_filler { type: "gaussian" std: 0.005 } bias_filler { type: "constant" value: 0.1 } } } layer { name: "relu6" type: "ReLU" bottom: "fc6" top: "fc6" } layer { name: "drop6" type: "Dropout" bottom: "fc6" top: "fc6" dropout_param { dropout_ratio: 0.8 } } layer { name: "fc7" type: "InnerProduct" bottom: "fc6" top: "fc7" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } inner_product_param { num_output: 4096 weight_filler { type: "gaussian" std: 0.005 } bias_filler { type: "constant" value: 0.1 } } } layer { name: "relu7" type: "ReLU" bottom: "fc7" top: "fc7" } layer { name: "drop7" type: "Dropout" bottom: "fc7" top: "fc7" dropout_param { dropout_ratio: 0.8 } } layer { name: "fc8-expr" type: "InnerProduct" bottom: "fc7" top: "fc8-expr" param { lr_mult: 10 decay_mult: 1 } param { lr_mult: 20 decay_mult: 0 } inner_product_param { num_output: 15 weight_filler { type: "gaussian" std: 0.01 } bias_filler { type: "constant" value: 0 } } } layer { name: "accuracy" type: "Accuracy" bottom: "fc8-expr" bottom: "label" top: "accuracy" include { phase: TEST } } layer { name: "loss" type: "SoftmaxWithLoss" bottom: "fc8-expr" bottom: "label" top: "loss" }
这里的话我们使用一个Alexnet的网络结构。首先前两层用来定义训练和测试时候的数据来源,当然,我这里训练和测试都使用制作好的训练集只是图个省事,大家自己做东西的时候为了测试结果的科学性还是建议不要使用训练数据来测试。mirror的含义就是是否开启镜像,顾名思义,很多图片分类使用的图片镜面对称之后同样非常真实,也可以用来训练。mean_file这个参数我这里注释掉了,通常图像分类在训练时进行一个减去均值的操作会 得到更好的效果,但是这个效果是否显著与图片类型和 质量相关,我这里没有使用。
制作均值文件方式:
sudo /home/hunter/caffe-master/build/tools/compute_image_mean /home/hunter/X/train_lmdb /home/hunter/X/mean.binaryproto
最后一个参数是保存路径。
我们这里不介绍Alexnet了,大家感兴趣可以去看他的论文。
接下来我们需要指定学习率和学习策略,也就是需要编写solver.prototxt。
net: "/home/hunter/X/train.prototxt" test_iter: 320 test_interval: 1000 # lr for fine-tuning should be lower than when starting from scratch base_lr: 0.001 lr_policy: "step" gamma: 0.1 # stepsize should also be lower, as we're closer to being done stepsize: 2000 display: 1000 max_iter: 10000 momentum: 0.9 weight_decay: 0.0005 snapshot: 2500 snapshot_prefix: "/home/hunter/X/model/" # uncomment the following to default to CPU mode solving solver_mode: GPU
这里我们来解释几个参数,第一个net是需要指向我们刚才写好的train.prototxt,momentum是一个动力项,减少陷入局部最优的概率,weight_decay是在接近训练完成的时候进行权重的减小,solver_mode是你需要指定使用CPU还是GPU进行运算,其余的可以在网上查到。
总结一下,训练阶段,我们需要LMDB格式的数据,train和solver这两个 配置文件。接下来我们进行网络训练。
/home/hunter/caffe-master/build/tools/caffe train --solver=/home/hunter/X/solver.prototxt
每个参数含义非常直观,就不再解释。
训练完成之后,我们会根据在solver.prototxt中指定的步长(Snapshot)得到若干训练好的模型文件,根据你电脑配置的不同训练时间会有所区别。得到模型后我们就可以进行应用。
一种方式是直接在终端下使用模型:
使用之前,不知道大家是否记得我们一开始制作数据的时候将文字标签转换为数字,这里我们需要再将数字转换回文字,我们需要根据一开始0-14一一对应的顺序,编写一个label.txt文件。
No Finding Infiltration Atelectasis Effusion Nodule Pneumothorax Mass Consolidation Pleural_Thickening Cardiomegaly Emphysema Fibrosis Edema Pneumonia Hernia
格式如上,此外,想要应用模型我们需要再编写一个应用时的prototxt文件。
name: "CaffeNet" input: "data" input_dim: 1 input_dim: 3 input_dim: 227 input_dim: 227 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: 96 kernel_size: 11 stride: 4 weight_filler { type: "gaussian" std: 0.01 } bias_filler { type: "constant" value: 0 } } } layer { name: "relu1" type: "ReLU" bottom: "conv1" top: "conv1" } layer { name: "norm1" type: "LRN" bottom: "conv1" top: "norm1" lrn_param { local_size: 5 alpha: 0.0001 beta: 0.75 } } layer { name: "pool1" type: "Pooling" bottom: "norm1" top: "pool1" pooling_param { pool: MAX kernel_size: 3 stride: 2 } } layer { name: "conv2" type: "Convolution" bottom: "pool1" top: "conv2" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } convolution_param { num_output: 256 pad: 2 kernel_size: 5 group: 2 weight_filler { type: "gaussian" std: 0.01 } bias_filler { type: "constant" value: 0.1 } } } layer { name: "relu2" type: "ReLU" bottom: "conv2" top: "conv2" } layer { name: "norm2" type: "LRN" bottom: "conv2" top: "norm2" lrn_param { local_size: 5 alpha: 0.0001 beta: 0.75 } } layer { name: "pool2" type: "Pooling" bottom: "norm2" top: "pool2" pooling_param { pool: MAX kernel_size: 3 stride: 2 } } layer { name: "conv3" type: "Convolution" bottom: "pool2" top: "conv3" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } convolution_param { num_output: 384 pad: 1 kernel_size: 3 weight_filler { type: "gaussian" std: 0.01 } bias_filler { type: "constant" value: 0 } } } layer { name: "relu3" type: "ReLU" bottom: "conv3" top: "conv3" } layer { name: "conv4" type: "Convolution" bottom: "conv3" top: "conv4" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } convolution_param { num_output: 384 pad: 1 kernel_size: 3 group: 2 weight_filler { type: "gaussian" std: 0.01 } bias_filler { type: "constant" value: 0.1 } } } layer { name: "relu4" type: "ReLU" bottom: "conv4" top: "conv4" } layer { name: "conv5" type: "Convolution" bottom: "conv4" top: "conv5" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } convolution_param { num_output: 256 pad: 1 kernel_size: 3 group: 2 weight_filler { type: "gaussian" std: 0.01 } bias_filler { type: "constant" value: 0.1 } } } layer { name: "relu5" type: "ReLU" bottom: "conv5" top: "conv5" } layer { name: "pool5" type: "Pooling" bottom: "conv5" top: "pool5" pooling_param { pool: MAX kernel_size: 3 stride: 2 } } layer { name: "fc6" type: "InnerProduct" bottom: "pool5" top: "fc6" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } inner_product_param { num_output: 4096 weight_filler { type: "gaussian" std: 0.005 } bias_filler { type: "constant" value: 0.1 } } } layer { name: "relu6" type: "ReLU" bottom: "fc6" top: "fc6" } layer { name: "drop6" type: "Dropout" bottom: "fc6" top: "fc6" dropout_param { dropout_ratio: 0.8 } } layer { name: "fc7" type: "InnerProduct" bottom: "fc6" top: "fc7" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } inner_product_param { num_output: 4096 weight_filler { type: "gaussian" std: 0.005 } bias_filler { type: "constant" value: 0.1 } } } layer { name: "relu7" type: "ReLU" bottom: "fc7" top: "fc7" } layer { name: "drop7" type: "Dropout" bottom: "fc7" top: "fc7" dropout_param { dropout_ratio: 0.8 } } layer { name: "fc8-expr" type: "InnerProduct" bottom: "fc7" top: "fc8-expr" param { lr_mult: 10 decay_mult: 1 } param { lr_mult: 20 decay_mult: 0 } inner_product_param { num_output: 15 weight_filler { type: "gaussian" std: 0.01 } bias_filler { type: "constant" value: 0 } } } layer { name: "prob" type: "Softmax" bottom: "fc8-expr" top: "prob" }
是不是看起来和train.prototxt非常相似?
没错,deploy.prototxt除了第一层数据层以及最后的预测和train.prototxt不一样,其他完全一样,或者说,必须一样。假如出现同一层在两个文件中名字不相同的情况,你可以试一下,预测结果会是在完全瞎猜。
接下来我们打开终端应用模型。
sudo /home/hunter/caffe-master/build/examples/cpp_classification/classification.bin /home/hunter/X/deploy.prototxt /home/hunter/X/model/_iter_3000.caffemodel /home/hunter/X/mean.binaryproto /home/hunter/X/labels.txt /home/hunter/X/3_new.png
第一个参数是我们使用Caffe的分类工具进行分类,紧接着是我们写好的deploy.prototxt文件,后面分别是,模型,均值文件以及标签的路径,最后一个是我们想要输入进行预测的图片。
运行后就会得到根据模型计算的分类结果。
还有另一种方式调用模型,不过需要配置python-caffe结构,如有不懂的大家可以看一下我的关于环境配置的文章。
要使用python来调用caffe模型,有些需要使用.npy格式的均值文件,转换方式如下:
import numpy as np import caffe blob = caffe.proto.caffe_pb2.BlobProto() data = open( 'mean.binaryproto' , 'rb' ).read() blob.ParseFromString(data) arr = np.array( caffe.io.blobproto_to_array(blob) ) out = arr[0] np.save( 'mean.npy' , out )
调用caffe的文件如下:
import caffe import numpy as np def test(my_project_root, deploy_proto): caffe_model = my_project_root + 'model/_iter_3000.caffemodel' img = my_project_root + '3.png' labels_filename = my_project_root + 'labels.txt' net = caffe.Net(deploy_proto, caffe_model, caffe.TEST) transformer = caffe.io.Transformer({'data': net.blobs['data'].data.shape}) transformer.set_transpose('data', (2,0,1)) transformer.set_raw_scale('data', 255) transformer.set_channel_swap('data', (2,1,0)) im = caffe.io.load_image(img) net.blobs['data'].data[...] = transformer.preprocess('data',im) out = net.forward() labels = np.loadtxt(labels_filename, str, delimiter='\t') prob = net.blobs['prob'].data[0].flatten() order = prob.argsort()[-1] print '图片数字为:',labels[order] if __name__ == '__main__': my_project_root = "/home/hunter/X/" deploy_proto = my_project_root + "deploy.prototxt" test(my_project_root, deploy_proto)
到此我们整个从制作数据到应用模型的过程就结束啦,但是这里给大家提几个小问题,第一,1024*1024尺寸的数据转换成227*227合理吗?第二,训练的时候如果加上均值文件效果会好多少?第三,其实这个数据集本身有不合理的地方,比如没有疾病的样本比例过大,会造成什么问题呢?如有疑问或者错误欢迎评论。
所有代码都会上传至我的Github: