Caffe實現多標簽輸入,添加數據層(data layer)


因為之前遇到了sequence learning問題(CRNN),里面涉及到一張圖對應多個標簽。Caffe源碼本身是不支持多類標簽數據的輸入的。
如果之前習慣調用腳本create_imagenet.sh,將原始數據轉換成lmdb數據格式,在這里就會遇到坑。我們去看convert_imageset源碼,我們就會發現它是把最后一個空格前面的當作輸入,最后一個空格之后的當作標簽,那當然無法多標簽啊。

通常解決辦法

  1. 換框架,換一個能支持多標簽分類問題的,例如mxnet,但我覺得你既然選擇用Caffe來解決問題,估計也不會換。
  2. HDF5+Slice Layer實現,因為Caffe中要求一個hdf5文件大小不超過2GB,所以文件如果較大的話,需要生成多個hdf5文件,所以需要用到Slice Layer。 參考生成hdf5文件用於多標簽訓練
  3. 用兩個data的輸入(兩個LMDB),一個只輸出圖片,一個只輸出標簽,這種方法相對前面兩種要難一些.
  4. 修改Caffe源碼caffe 實現多標簽輸入
  5. 其實我個人總結的是,數據層的添加可以考慮用python,因為比較簡單、快,也不會影響效率,計算層的添加還是需要用C++來寫的。

本文解決方案

我采用的方案就是用python將數據轉換成lmdb格式,然后在prototxt中定義采用python module的方式,去讀取之前轉換的lmdb數據。

具體步驟

1. 前期數據准備

前期的數據准備和單分類一樣,只不過現在我們有多個標簽了,那么我就在train.txt和val.txt中,用空格將多個標簽分隔開。例如 image1.jpg label1 label2 label3 label4

2. 數據轉lmdb格式

#!/usr/bin/python
# -*- coding: utf-8 -*-

import numpy as np
import lmdb
import sys, os
import caffe
from skimage import io
import cv2
import random

train_path = 'train.txt'                     # 訓練集標簽
val_path = 'val.txt'						 # 驗證集標簽
train_lmdb = '/path/to/your/data_train_lmdb' # 生成lmdb格式訓練集數據的路徑,到目錄級別就可以了
val_lmdb = '/path/to/your/data_val_lmdb'     # 生成lmdb格式驗證集數據的路徑,到目錄級別就可以了

# 加載train.txt
def load_txt(txt, shuffle):
    if txt == None:  
	print "txtpath!!!"  
	exit(0)  
    if not os.path.exists(txt):  
	print "the txt is't exists"  
	exit(0)
    
    # 將數據按行存入list中
    file_content = []  
    with open(txt, 'r') as fr:
	for line in fr.readlines():
	    line = line.strip()
	    file_content.append([_ for _ in line.split(' ')])
	# shuffle數據
    if shuffle:
	random.shuffle(file_content)
    return file_content

if __name__ == '__main__':
    content = []    
    # 這里定義了要處理的文件目錄,因為我們有train data 和 val data,所以我們需要把val_path和val_lmdb改成train_path和train_lmdb再執行一次這個腳本。
    content = load_txt(val_path, True)
    env = lmdb.Environment(val_lmdb, map_size=int(1e12)) 
    with env.begin(write=True) as txn: 
	for i in range(len(content)):  
	    pic_path = content[i][0]
	    # 采用skimage庫的方式來讀文件
	    img_file = io.imread(pic_path, as_grey=True)  
	    # 如果采用opencv的方式來讀文件,那么下面也要改成mat轉string的方式
	    #img_file = cv2.imread(pic_path, 0)
            data = np.zeros(( img_file.shape[0], img_file.shape[1]), dtype=np.uint8)
	    data = img_file
	    # 因為lmdb是鍵值數據庫,所以我們采用將鍵和值都設置為字符串格式
	    str_id = "image-%09d" %(i) 
	    cv2.imencode('.jpg', data)
	    txn.put(str_id.encode('ascii'), cv2.imencode('.jpg', data)[1].tostring())

		# 這里的多標簽采用的是空格分隔,到時候讀lmdb數據庫的時候,也用空格解析就可以了
	    multi_labels = ""
	    for _ in content[i][1:len(content[i])]:
		multi_labels += _
		multi_labels += " "
            multi_labels += content[i][-1]  
	    
	    # 鍵和值都是字符串格式
	    str_id = "label-%09d" %(i) 
	    #txn.put(str_id.encode('ascii'), multi_labels)
	    txn.put(str_id, multi_labels)
	    #txn.put(str_id, multi_labels)
	
     	str_id = "num-samples"
	txn.put(str_id, str(len(content)))
	#txn.put(str_id.encode('ascii'), str(len(content)))
	print str(len(content))

分別設置train和val執行這個腳本兩次,得到的就是兩個目錄,里面包含lmdb格式的訓練集和驗證集,這就回到了我們熟悉的方式,因為之前直接調用自帶腳本得到的結果也是這樣。

3. 定義dataLayer

這步的作用就是,在prototxt會定義input是采用這個dataLayer將數據讀入的。
具體做法將上一步生成的lmdb數據讀出來就可以了。
我們先來看看官方給的python接口格式。

# 這是一個lossLayer的例子
import caffe
import numpy as np


class EuclideanLossLayer(caffe.Layer):
    """
    Compute the Euclidean Loss in the same manner as the C++ EuclideanLossLayer
    to demonstrate the class interface for developing layers in Python.
    """
	
	# 設置參數
    def setup(self, bottom, top):
        # check input pair
        if len(bottom) != 2:
            raise Exception("Need two inputs to compute distance.")

    def reshape(self, bottom, top):
        # check input dimensions match
        if bottom[0].count != bottom[1].count:
            raise Exception("Inputs must have the same dimension.")
        # difference is shape of inputs
        self.diff = np.zeros_like(bottom[0].data, dtype=np.float32)
        # loss output is scalar
        top[0].reshape(1)

	# 前向計算方式
    def forward(self, bottom, top):
        self.diff[...] = bottom[0].data - bottom[1].data
        top[0].data[...] = np.sum(self.diff**2) / bottom[0].num / 2.

	# 反向傳播方式
    def backward(self, top, propagate_down, bottom):
        for i in range(2):
            if not propagate_down[i]:
                continue
            if i == 0:
                sign = 1
            else:
                sign = -1
            bottom[i].diff[...] = sign * self.diff / bottom[i].num

那么我們知道接口長什么樣以后,我們就開始依葫蘆畫瓢了。別急,先來看看prototxt怎么定義參數的,因為到時候這個決定了我們要向data Layer中傳入什么參數。先看看官方接口

4. 定義prototxt

message PythonParameter {
  optional string module = 1;
  optional string layer = 2;
  // This value is set to the attribute `param_str` of the `PythonLayer` object
  // in Python before calling the `setup()` method. This could be a number,
  // string, dictionary in Python dict format, JSON, etc. You may parse this
  // string in `setup` method and use it in `forward` and `backward`.
  optional string param_str = 3 [default = '']; # 這里比較關鍵,也就是我們通過這個參數,來決定如何讀取lmdb數據的
  // DEPRECATED
  optional bool share_in_parallel = 4 [default = false];
}

我給一個實例代碼

layer {
  name: "data"
  type: "Python"
  top: "data"
  top: "label"
  include {
    phase: TRAIN
  }
  python_param {
     module: "dataLayer"
     layer: "CRNNDataLayer"
     param_str: "{'data' : '/path/to/your/data_train_lmdb', 'batch_size' : 128}"
  }
}

我們可以看到它會去調用dataLayer這個python模塊,那么就需要定義dataLayer,具體實現如下。

import sys
import caffe
from caffe import layers as L, params as P
from caffe.coord_map import crop
import numpy as np
import os
import cv2
import lmdb
import random
import timeit
import os

class CRNNDataLayer(caffe.Layer):
    def setup(self, bottom, top):
        params = eval(self.param_str)
        # 讀prototxt中的參數
        self.lmdb = lmdb.open(params['data']).begin(buffers=True).cursor()
        # 這個是生成lmdb數據的時候,定義的樣本的總個數
        c=self.lmdb.get('num-samples')
#        print '['+str(c)+']'
        self.max_num = int(str(c))
        self.batch_size = int(params['batch_size'])
        # two tops: data and label
        if len(top) != 2:
            raise Exception("Need to define two tops: data and label.")
        # data layers have no bottoms
        if len(bottom) != 0:
            raise Exception("Do not define a bottom.")

    def reshape(self, bottom, top):
        # load image + label image pair
        start = timeit.timeit()
        self.data,self.label = self.load_data()
        end = timeit.timeit()
#        print 'time used for reshape',end-start
        # reshape tops to fit (leading 1 is for batch dimension)
        top[0].reshape(*self.data.shape)
        top[1].reshape(*self.label.shape)        

    
    def forward(self, bottom, top):
        # assign output
        top[0].data[...] = self.data
        top[1].data[...] = self.label

	# 因為是data layer,所以不需要定義backward
    def backward(self, top, propagate_down, bottom):
        pass


    def load_data(self):
    	# 采用隨機讀入的方式
        rnd = random.randint(0,self.max_num-self.batch_size-1)
        # 先初始化一個多維數組,用於存放讀入的數據,在這里設置batch size, channel, height, width
        img_list= np.zeros((self.batch_size, channel, height, width),
                           dtype = np.float32)
        # 先初始化一個多維數組,用於存放標簽數據,設置batch size, label size(每張圖對應的標簽的個數)
        label_seq = np.ones((self.batch_size, label_size), dtype = np.float32)
        
        j = 0
        i = 0
#        print 'loading data ...'
        while i < self.batch_size:
#            rnd = random.randint(0,self.max_num-self.batch_size-1)
            imageKey = 'image-%09d' % (rnd + j)
            labelKey = 'label-%09d' % (rnd + j)
            try:
                img_array = np.asarray(bytearray(self.lmdb.get(imageKey)), dtype=np.uint8)
                #imgdata = cv2.imdecode(img_array, 0)
		        imgdata = cv2.imdecode(np.fromstring(img_array, np.uint8), cv2.CV_LOAD_IMAGE_GRAYSCALE)
		        # 設置resize的width和height
                image = cv2.resize(imgdata, width,height))
                image = (image - 128.0)/128
                img_list[i] = image
                label = str(self.lmdb.get(labelKey))
                #numbers = np.array(map(lambda x: float(ascii2label(ord(x))), label))
				 label_list = label.split(" ")
				 label_list = [int(_) for _ in label_list]
				 # 這里把標簽依次放入數組中
                label_seq[i, :len(label_list)] = label_list
                i+=1
            except Exception as e:
		print e
            j+=1
#        print 'data loaded'
        return img_list,label_seq

5. 重新編譯caffe

因為我們添加了一個python module,那么我們要在環境變量中,設置這個module,不然會出現找不到的情況。

vim ~/.bash_profile
export PYTHONPATH=$PYTHONPATH:(添加dataLayer.py所在目錄)
source ~/.bash_profile

編譯

WITH_PYTHON_LAYER=1 make && make pycaffe

大功告成

本人親測以上方式是可行的。


免責聲明!

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



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