小喵的嘮叨話:小喵最近在做人臉識別的工作,打算將湯曉鷗前輩的DeepID,DeepID2等算法進行實驗和復現。DeepID的方法最簡單,而DeepID2的實現卻略微復雜,並且互聯網上也沒有比較好的資源。因此小喵在試驗之后,確定了實驗結果的正確性之后,才准備寫這篇博客,分享給熱愛Deep Learning的小伙伴們。
小喵的博客:http://www.miaoerduo.com
博客原文:http://www.miaoerduo.com/deep-learning/基於caffe的deepid2實現(上).html
能夠看到這篇博客的小伙伴們,相信已經對Deep Learning有了比較深入的了解。因此,小喵對親作了如下的假定:
- 了解Deep Learning的基本知識
- 目前在做人臉識別的相關工作
- 仔細閱讀過DeepID和DeepID2的論文
- 使用Caffe作為訓練框架
- 即使不滿足上述4個條件,也會持之以恆的學習
如果親發現對上述的條件都不滿足的話,那么這篇博文可能內容還是略顯枯澀乏味,你可以從了解Caffe開始,慢慢學習。
相關資源:
- DeepID:http://mmlab.ie.cuhk.edu.hk/pdf/YiSun_CVPR14.pdf
- DeepID2:http://papers.nips.cc/paper/5416-analog-memories-in-a-balanced-rate-based-network-of-e-i-neurons
- Caffe:http://caffe.berkeleyvision.org/
由於篇幅較大,這里會分成幾個部分,依次講解。
一、設計我們獨特的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中,有這么一句話。
我們需要使用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。這里還有一些問題需要注意:
- 對輸入的參數沒有檢驗。
- 沒有對讀取圖像等操作做異常處理。因此如果很不幸地讀到的圖片路徑不合法,那么程序突然死掉都是有可能的。。。小喵的數據都是可以讀的,所以木有問題。
- 在選取正負樣本對的時候,對於正樣本對,只有樣本對應的label中的圖片數大於5的時候,才選正樣本(小喵的訓練數據每個人都有至少幾十張圖片,所以木有出現問題),如果樣本比較少的話,可以更改這個數(特別是有測試集的時候,測試集通常數目都很少,小喵訓練的時候都是不用測試集的,因為會死循環。。。)。對於選取負樣本對的時候,只是隨便選了兩張圖片,而並沒有真的保證這一對是不同label,這里考慮到訓練數據是比較多的,所以不大可能選中同一個label的樣本,因此可以近似代替負樣本對。
- 這里有個減均值的操作,這個均值文件是經過特殊轉換求出的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~~
轉載請注明出處~