1. 程式人生 > >【caffe速成】caffe影象分類從模型自定義到測試

【caffe速成】caffe影象分類從模型自定義到測試

文章首發於微信公眾號《與有三學AI》

這是給大家準備的caffe速成例子

這一次我們講講 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 是一個非常優秀的開源框架,有必要去細讀它的原始碼。

至於怎麼讀 Caffe 的程式碼,建議閱讀我寫的Caffe程式碼閱讀系列內容。

04 總結

雖然現在很多人沒有從 Caffe 開始學,但是希望提升自己 C++ 水平和更深刻理解深度學習中的一些原始碼的,建議從 Caffe 開始學起。

同時,在我的知乎專欄也會開始同步更新這個模組,歡迎來交流

注:部分圖片來自網路

—END—

打一個小廣告,我在gitchat開設了一些課程和chat,歡迎交流。