1. 程式人生 > >caffe中關於layer定義的筆記

caffe中關於layer定義的筆記

很多初次閱讀caffe原始碼的同學可能不知道其中的Layer具體是如何被定義的。假如我自己寫了個新的XXXLayer,caffe是怎麼知道它的存在的呢?怎麼呼叫它的建構函式的呢?caffe為了管理各種各樣的Layer,實現了叫做“工廠模式”的設計方法。它的長處是,我們在新增自己的Layer時不需要修改caffe的核心程式碼;但缺點也很明顯,就是使用了許多高階的C++語言特性,程式碼不太讓人看得懂,尤其是我這種C++基礎不太好的。本著學習的目的,也為了便於其他新手更快地入門,我簡短地總結一下caffe中Layer的定義過程。

0. 關於“工廠模式”的概述

什麼是工廠模式呢?

很簡單,假如有一個基類Product,兩個子類Car,Plane,都繼承Product。現在需要一個函式,要求返回Product的例項,同時要求:傳入"car"引數時返回Car類物件,傳入"plane"時返回Plane物件。很簡單,用if就能搞定:

class Product{};
class Car:Product{};
class Plane:Product{};
Product* make(string what){
    if(what=="car") return new Car();
    if(what=="plane") return new Plane();
    return null;
}

這就是一個最簡單的工廠模式。這個函式能在執行時根據變數返回不同子類的物件。

這就是為什麼caffe要用到工廠模式。我們知道caffe裡面有很多種Layer,有卷積的,有全連線的,還有管資料和算loss的。這些XXXLayer都是Layer類的子類。我們知道caffe要在執行時根據prototxt去決定建立什麼型別的Layer,自然要用到這種模式。那麼問題來了,為什麼不直接寫一串if,而非要寫一堆讓人看不懂的巨集呢?原因在於,假如我們要在上面的例子里加一個新的子類Boat,那就必然要修改if的部分。這等於是為了加一個新類而修改了核心程式碼,caffe不希望發生這種事。那麼,caffe的工廠模式是怎麼實現的呢?

1. 從REGISTER_LAYER_CLASS(XXX)入手

在每個XXXLayer的cpp中,應該都能找到一個REGISTER_LAYER_CLASS的巨集。正是這個巨集定義使caffe能找到並例項化我們自己的類。它定義在layer_factory.hpp中,是這樣的:

#define REGISTER_LAYER_CLASS(type)                                             \
  template <typename Dtype>                                                    \
  shared_ptr<Layer<Dtype> > Creator_##type##Layer(const LayerParameter& param) \
  {                                                                            \
    return shared_ptr<Layer<Dtype> >(new type##Layer<Dtype>(param));           \
  }                                                                            \
  REGISTER_LAYER_CREATOR(type, Creator_##type##Layer)

暈了,這些##是什麼鬼,C++還可以這麼寫麼。其實這是巨集的一些語法。'##'代表的是把巨集引數和##前後的內容連起來。具體到這裡,假如我自己定義了MyAwesomeLayer,

最後就會加上REGISTER_LAYER_CLASS(MyAwesome)。這就等價於寫了以下程式碼:

template <typename Dtype>                                                    
shared_ptr<Layer<Dtype> > Creator_MyAwesomeLayer(const LayerParameter& param){                                                                            
	return shared_ptr<Layer<Dtype> >(new MyAwesomeLayer<Dtype>(param));           
}
REGISTER_LAYER_CREATOR(MyAwesome, Creator_MyAwesomeLayer)

看起來是定義了一個函式,這個函式接受param(關於層的一些引數),new一個我們的MyAwesomeLayer的物件,然後以Layer的指標返回回來。是不是有點意思了呢?

這個函式之後又用了一個巨集,我們看看它幹了什麼:

#define REGISTER_LAYER_CREATOR(type, creator)                                  \
  static LayerRegisterer<float> g_creator_f_##type(#type, creator<float>);     \
  static LayerRegisterer<double> g_creator_d_##type(#type, creator<double>);   \

暈了,這一個#又是什麼意思啊。這其實也是巨集的語法,它代表把巨集的引數變成字串。具體到這裡,就等於寫了以下程式碼:

static LayerRegisterer<float> g_creator_f_MyAwesome("MyAwesome", Creator_MyAwesomeLayer<float>);
static LayerRegisterer<double> g_creator_d_MyAwesome("MyAwesome", Creator_MyAwesomeLayer<double>);

這裡建立了兩個LayerRegisterer類的靜態物件,給建構函式傳了兩個引數。一個是字串,是我們的Layer的名字,另一個是剛剛建立的Creator_MyAwesomeLayer方法的函式指標。建立這兩個物件是在做什麼呢?看它們的定義:
template <typename Dtype>
class LayerRegisterer {
public:
  LayerRegisterer(const string& type,
                  shared_ptr<Layer<Dtype> > (*creator)(const LayerParameter&)) {
    // LOG(INFO) << "Registering layer type: " << type;
    LayerRegistry<Dtype>::AddCreator(type, creator);
  }
};

其實建立它們的過程不過是呼叫了AddCreator這個函式,把傳入的字串和函式指標又傳給了它。
  // Adds a creator.
  static void AddCreator(const string& type, Creator creator) {
    CreatorRegistry& registry = Registry();
    CHECK_EQ(registry.count(type), 0)
        << "Layer type " << type << " already registered.";
    registry[type] = creator;
  }

這裡面是通過Registry方法拿到了一個registry指標,關鍵步驟是:registry["MyAwesome"] = creator。這看著像是一個類似Python裡的字典的東西,也就是個雜湊表,鍵是Layer的型別,值是一個能返回這個Layer物件的函式。有點意思!繼續找Registry的定義:
  typedef shared_ptr<Layer<Dtype> > (*Creator)(const LayerParameter&);
  typedef std::map<string, Creator> CreatorRegistry;

  static CreatorRegistry& Registry() {
    static CreatorRegistry* g_registry_ = new CreatorRegistry();
    return *g_registry_;
  }

這一段裡面有個大坑。假如要實現工廠模式,剛剛提到的那個“字典”不應該是全域性的麼,我們不應該一直往裡加東西麼?怎麼這裡每次都返回一個新的呢?這裡一定要注意那個static。static CreatorRegistry* g_registry_ = new CreatorRegistry() 其實只在最初執行了一次,g_registry_放在靜態記憶體裡,函式結束了是不會消失的!後面每次呼叫並不會執行new,返回的都是同一個東西的指標!

另外,typedef shared_ptr<Layer<Dtype>> (*Creator)(const LayerParameter&)這句話也有點莫名其妙,這個typedef是啥意思。其實,這和typedef int MYINT是類似的。後者是給int類起了個別名叫MYINT,然後就能這樣寫:MYINT a=10; 前者其實是給一類函式指標起了個別名,這類函式指標都是以LayerParameter&做引數,返回shared_ptr<<Layer<Dtype>>。 假如這樣寫: Creator pcreator;, 那pcreator就是一個這樣的函式指標了。

2. caffe如何呼叫我們的層

看了1之後,不難發現,原來caffe的工廠模式,不過是往一個雜湊表裡面放了一些(型別名-->建立該類物件的函式)這樣的對映罷了!上面所有這些操作都幾乎是在編譯時以及程式執行的最最最開始進行的,所以caffe在真正執行的時候就能找到我們自己定義的層了。來看看是怎麼找到的吧。看net.cpp的96行左右的位置:

layers_.push_back(LayerRegistry<Dtype>::CreateLayer(layer_param));
這句話位於一個for迴圈裡面,就是根據讀到的prototxt,一個一個地建立Layer物件了。來看看CreateLayer方法:
  static shared_ptr<Layer<Dtype> > CreateLayer(const LayerParameter& param) {
    LOG(INFO) << "Creating layer " << param.name();
    const string& type = param.type();
    CreatorRegistry& registry = Registry();
    CHECK_EQ(registry.count(type), 1) << "Unknown layer type: " << type
                                      << " (known types: " << LayerTypeList() << ")";
    return registry[type](param);
  }

這不正是呼叫了我們一開始放進字典裡面的函式,以此根據Layer的型別名稱字串建立指定的Layer物件的麼?

3. 其他

Layer的cpp中還有一個巨集:

INSTANTIATE_CLASS(MyAwesomeLayer);
這是在幹什麼呢?通過檢視定義,發現這等價於寫了這些程式碼:
char gInstantiationGuardMyAwesomeLayer;
//explicit instantiation of template class
template class MyAwesomeLayer<float>;
template class MyAwesomeLayer<double>;

定義了一個字元,然後又是兩句莫名其妙的程式碼。其實,這兩個是顯式地初始化兩個MyAwesomeLayer類,一個是float的,一個是double的。template其實可以看做"類的類",類需要例項化才能得到物件,同樣模板也要這樣例項化一下才能得到對於不同資料型別的類定義。

大神寫程式碼喜歡用一些高階的玩意,然後菜雞就看不太懂;不過其實就那麼點東西,學一學還是能學會的。希望寫下來能幫到和我一樣菜的菜雞。菜雞不一定能變大神,但努努力還是能變成不那麼菜的菜雞的。共勉!