基於Caffe的DeepID2實現(上)


小喵的嘮叨話:小喵最近在做人臉識別的工作,打算將湯曉鷗前輩的DeepID,DeepID2等算法進行實驗和復現。DeepID的方法最簡單,而DeepID2的實現卻略微復雜,並且互聯網上也沒有比較好的資源。因此小喵在試驗之后,確定了實驗結果的正確性之后,才准備寫這篇博客,分享給熱愛Deep Learning的小伙伴們。

小喵的博客:http://www.miaoerduo.com

博客原文:http://www.miaoerduo.com/deep-learning/基於caffe的deepid2實現(上).html

能夠看到這篇博客的小伙伴們,相信已經對Deep Learning有了比較深入的了解。因此,小喵對親作了如下的假定:

  1. 了解Deep Learning的基本知識
  2. 目前在做人臉識別的相關工作
  3. 仔細閱讀過DeepID和DeepID2的論文
  4. 使用Caffe作為訓練框架
  5. 即使不滿足上述4個條件,也會持之以恆的學習

如果親發現對上述的條件都不滿足的話,那么這篇博文可能內容還是略顯枯澀乏味,你可以從了解Caffe開始,慢慢學習。

相關資源:

由於篇幅較大,這里會分成幾個部分,依次講解。

一、設計我們獨特的Data層

在DeepID2中,有兩種監督信號。一是Identity signal,這和DeepID中的實現方法一樣,用給定label的人臉數據,進行分類的訓練,這里使用softmax_with_loss層來實現(softmax+cross-entropy loss)。這里不再介紹。

另一種就是verification signal,也就是人臉比對的監督。這里要求,輸入的數據時成對存在,每一對都有一個公共的label,是否是同一個類別。如果是同一個identity,則要求他們的特征更接近,如果是不同的identity,則要求他們的特征盡可能遠離。

不論最終怎么實現,我們的第一步是確定的,構造合適的數據。

使用Caffe訓練的時候,第一步是打Batch,將訓練數據寫入LMDB或者LevelDB數據庫中,訓練的時候Caffe會從數據庫中讀取圖片,因此一個簡單的實現方法就是構造許多的pair,然后打Batch的時候就能保證每對圖片都是相連的,然后在訓練的時候做一些小Trick就可以實現。

但是就如上面所說,打Batch的同時,圖片的順序就已經是確定的了,因此網絡輸入的圖片pair也是固定的,這樣似乎就缺乏了一些靈活性。

那么如果動態的構造我們的訓練數據呢?

設計我們獨特的data層。

這里為了方便,使用Python來拓展Caffe的功能。Python是一門簡潔的語言,非常適合做這種工作。不過Caffe中如果使用了Python的層,那么就不能使用多GPU了,這點需要注意(希望以后能增加這個支持)。

1)讓你的Caffe支持Python拓展。

在Caffe根目錄的Makefile.config中,有這么一句話。

with_python_layer

我們需要使用Python層,因此需要取消這個注釋。

之后Make一下你的Caffe和pycaffe。

這樣Caffe就支持Python層了。

2)編寫data層

基於Python的data層的編寫,Caffe是給了一個簡單的例子的,在/caffe_home/examples/pycaffe/layers/中。

我們簡單的照着這個例子來寫。

首先,我們定義自己需要的參數。

這里,我們需要:

  • batch_size: batch的大小,沒什么好解釋的,要求這個數是大於0的偶數
  • mean_file:圖像的均值文件的路徑
  • scale:圖像減均值后變換的尺度
  • image_root_dir:訓練數據的根目錄
  • source:訓練數據的list路徑
  • crop_size:圖像crop的大小
  • ratio:正樣本所占的比例(0~1)

caffe在train.prototxt中定義網絡結構的時候,可以傳入這些參數。我們目前只需要知道,這些參數一定可以獲取到,就可以了。另外,source表示訓練數據的list的文件地址,這里用到的訓練數據的格式和Caffe打batch的數據一樣。

file_path1    label1

file_path2    label2

這樣的格式。

Data層的具體實現,首先需要繼承caffe.Layer這個類,之后實現setup, forward, backward和reshape,不過data層並不需要backward和reshape。setup主要是為了初始化各種參數,並且設置top的大小。對於Data層來說,forward則是生成數據和label。

閑話少說,代碼來見。

  1 #-*- encoding: utf-8 -*_
  2 
  3 import sys
  4 import caffe
  5 import numpy as np
  6 import os
  7 import os.path as osp
  8 import random
  9 import cv2
 10 
 11 class ld2_data_layer(caffe.Layer):
 12     """
 13     這個python的data layer用於動態的構造訓練deepID2的數據
 14     每次forward會產生多對數據,每對數據可能是相同的label或者不同的label
 15     """
 16     def setup(self, bottom, top):
 17         self.top_names = ['data', 'label']
 18 
 19         # 讀取輸入的參數
 20         params = eval(self.param_str)
 21         print "init data layer"
 22         print params
 23 
 24         self.batch_size = params['batch_size']  # batch_size
 25         self.ratio = float(params['ratio'])
 26         self.scale = float(params['scale'])
 27         assert self.batch_size > 0 and self.batch_size % 2 == 0, "batch size must be 2X"
 28         assert self.ratio > 0 and self.ratio < 1, "ratio must be in (0, 1)"
 29         self.image_root_dir = params['image_root_dir']
 30         self.mean_file = params['mean_file']       
 31         self.source = params['source']
 32         self.crop_size = params['crop_size']
 33 
 34         top[0].reshape(self.batch_size, 3, params['crop_size'], params['crop_size'])
 35         top[1].reshape(self.batch_size, 1)
 36         self.batch_loader = BatchLoader(self.image_root_dir, self.mean_file, self.scale, self.source, self.batch_size, self.ratio)
 37 
 38     def forward(self, bottom, top):
 39         blob, label_list = self.batch_loader.get_mini_batch()
 40         top[0].data[...] = blob
 41         top[1].data[...] = label_list
 42 
 43     def backward(self, bottom, top):
 44         pass
 45 
 46     def reshape(self, bottom, top):
 47         pass
 48 
 49 class BatchLoader(object):
 50 
 51     def __init__(self, root_dir, mean_file, scale, image_list_path, batch_size, ratio):
 52         print "init batch loader"
 53         self.batch_size = batch_size
 54         self.ratio = ratio # true pair / false pair
 55         self.image2label = {}   # key:image_name    value:label
 56         self.label2images = {}  # key:label         value: image_name array
 57         self.images = []        # store all image_name
 58         self.mean = np.load(mean_file)
 59         self.scale = scale
 60         self.root_dir = root_dir
 61         with open(image_list_path) as fp:
 62             for line in fp:
 63                 data = line.strip().split()
 64                 image_name = data[0]
 65                 label = data[-1]
 66                 self.images.append(image_name)
 67                 self.image2label[image_name] = label
 68                 if label not in self.label2images:
 69                     self.label2images[label] = []
 70                 self.label2images[label].append(image_name)
 71         self.labels = self.label2images.keys()
 72         self.label_num = len(self.labels)
 73         self.image_num = len(self.image2label)
 74         print "init batch loader over"
 75 
 76     def get_mini_batch(self):
 77         image_list, label_list = self._get_batch(self.batch_size / 2)
 78         cv_image_list = map(lambda image_name: (self.scale * (cv2.imread(os.path.join(self.root_dir, image_name)).astype(np.float32, copy=False).transpose((2, 0, 1)) - self.mean)), image_list)
 79         blob = np.require(cv_image_list)
 80         label_blob = np.require(label_list, dtype=np.float32).reshape((self.batch_size, 1))
 81         return blob, label_blob
 82     
 83     def _get_batch(self, pair_num):
 84         image_list = []
 85         label_list = []
 86         for pair_idx in xrange(pair_num):
 87             if random.random() < self.ratio: # true pair
 88                 while True:
 89                     label_idx = random.randint(0, self.label_num - 1)
 90                     label = self.labels[label_idx]
 91                     if len(self.label2images[label]) > 5:
 92                         break
 93                 first_idx = random.randint(0, len(self.label2images[label]) - 1)
 94                 second_id = random.randint(0, len(self.label2images[label]) - 2)
 95                 if second_id >= first_idx:
 96                     second_id += 1
 97                 image_list.append(self.label2images[label][first_idx])
 98                 image_list.append(self.label2images[label][second_id])
 99                 label_list.append(int(label))
100                 label_list.append(int(label))
101             else:                   # false pair
102                 for i in xrange(2):
103                     image_id = random.randint(0, self.image_num - 1)
104                     image_name = self.images[image_id]
105                     label = self.image2label[image_name]
106                     image_list.append(image_name)
107                     label_list.append(int(label))
108         return image_list, label_list

上述的代碼可以根據給定的list,batch size,ratio等參數生成符合要求的data和label。這里還有一些問題需要注意:

  1. 對輸入的參數沒有檢驗。
  2. 沒有對讀取圖像等操作做異常處理。因此如果很不幸地讀到的圖片路徑不合法,那么程序突然死掉都是有可能的。。。小喵的數據都是可以讀的,所以木有問題。
  3. 在選取正負樣本對的時候,對於正樣本對,只有樣本對應的label中的圖片數大於5的時候,才選正樣本(小喵的訓練數據每個人都有至少幾十張圖片,所以木有出現問題),如果樣本比較少的話,可以更改這個數(特別是有測試集的時候,測試集通常數目都很少,小喵訓練的時候都是不用測試集的,因為會死循環。。。)。對於選取負樣本對的時候,只是隨便選了兩張圖片,而並沒有真的保證這一對是不同label,這里考慮到訓練數據是比較多的,所以不大可能選中同一個label的樣本,因此可以近似代替負樣本對。
  4. 這里有個減均值的操作,這個均值文件是經過特殊轉換求出的numpy的數組。Caffe生成的均值文件是不能直接用的,但是可以通過仿照Caffe中Classifier中的寫法來代替(caffe.io.Transformer工具)。另外這里的圖片數據和均值文件是一樣大小的,但實際上可能並不一定相等。如果需要對輸入圖片做各種隨機化的操作,還需要自己修改代碼。

至此,我們就完成了一個簡單的Data層了。

那么在么調用自己的data層呢?

這里有一個十分簡單的寫法。在我們用來訓練的prototxt中,將Data層的定義改成如下的方式:

 1 layer {
 2     name: "data"
 3     type: "Python"
 4     top: "data"
 5     top: "label"
 6     include {
 7         phase: TRAIN
 8     }    
 9     python_param {
10         module: "id2_data_layer"
11         layer: "ld2_data_layer"
12         param_str: "{'crop_size' : 128, 'batch_size' : 96, 'mean_file': '/your/data/root/mean.npy', 'scale': 0.0078125, 'source': '/path/to/your/train_list', 'image_root_dir': '/path/to/your/image_root/'}"    }
13 }

python_param中的這三個參數需要注意:

module:模塊名,我們先前編寫的data層,本身就是一個文件,也就是一個模塊,因此模塊名就是文件名。

layer:層的名字,我們在文件中定義的類的名字。這里比較巧合,module和layer的名字相同。

param_str:所有的需要傳給data層的參數都通過這個參數來傳遞。這里簡單的使用了Python字典的格式的字符串,在data層中使用eval來執行(o(╯□╰)o  這其實並不是一個好習慣),從而獲取參數,當然也可以使用別的方式來傳遞,比如json或者xml等。

最后,你在訓練的時候可能會報錯,說找不到你剛剛的層,或者找不到caffe,只需要把這個層的代碼所在的文件夾的路徑加入到PYTHONPATH中即可。

export PYTHONPATH=PYTHONPATH:/path/to/your/layer/:/path/to/caffe/python

這樣就完成了我們的Data層的編寫,是不是非常簡單?

 

如果您覺得本文對您有幫助,那請小喵喝杯茶吧~~O(∩_∩)O~~

%e6%89%93%e8%b5%8f

 

 

轉載請注明出處~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM