设计模式--工厂模式 caffe_layer注册

来源:http://www.cnblogs.com/zhouqiang/archive/2012/07/20/2601365.html

来源:http://blog.luoyetx.com/2016/02/reading-caffe-3/

工厂模式:主要用来实例化有共同接口的类,工厂模式可以动态决定应该实例化那一个类。

工厂模式的形态

工厂模式主要用一下几种形态:

1:简单工厂(Simple Factory)。

2:工厂方法(Factory Method)。

3:抽象工厂(Abstract Factory)。

简单工厂(Simple Factory)

又叫静态工厂,是工厂模式三中状态中结构最为简单的。主要有一个静态方法,用来接受参数,并根据参数来决定返回实现同一接口的不同类的实例。我们来看一个具体的例子:

假设一家工厂,几生产洗衣机,有生产冰箱,还有空调等等..

我们先为所有产品定义一个共同的产品接口

  1. public interface Product{}

接着我们让这个工厂的所有产品都必须实现此接口

  1. public class Washer implements Product{
  2. public Washer(){
  3. System.out.println("洗衣机被制造了");
  4. }
  5. }
  6. public class Icebox implements Product{
  7. public Icebox(){
  8. System.out.println("冰箱被制造了");
  9. }
  10. }
  11. public class AirCondition implements Product{
  12. public Icebox(){
  13. System.out.println("空调被制造了");
  14. }
  15. }

接下来我们来写一个工厂类,有它来负责生产以上的产品

  1. public class SimpleFactory {
  2. public static Product factory(String productName) throws Exception{
  3. if(productName.equals("Washer")){
  4. return new Washer();
  5. }else if(productName.equals("Icebox")){
  6. return new Icebox();
  7. }else if(productName.equals("AirCondition")){
  8. return new AirCondition();
  9. }else{
  10. throw new Exception("没有该产品");
  11. }
  12. }
  13. }

好了,有了这个工厂类,我们就可以开始下定单了,SimpleFactory将根据不同的定单类决定生产什么产品。

  1. public static void main(String[] args) {
  2. try {
  3. SimpleFactory.factory("Washer");
  4. SimpleFactory.factory("Icebox");
  5. SimpleFactory.factory("AirCondition");
  6. } catch (Exception e) {
  7. e.printStackTrace();
  8. }
  9. }

由上面的代码可以看出,简单工厂的核心就是一个SimpleFactory类,他拥有必要的逻辑判断能力和所有产品的创建权利,我们只需要向把定单给他,就能得到我们想要的产品。这使用起来似乎非常方便。

但,实际上,这个SimpleFactory有很多的局限。首先,我们每次想要增加一种新产品的时候,都必须修改SimpleFactory的原代码。其次,当我们拥有很多很多产品的时候,而且产品之间又存在复杂的层次关系的时候,这个类必须拥有复杂的逻辑判断能力,其代码量也将不断地激增,这对以后的维护简直就是恐怖两个字...

还有就是,整个系统都严重依赖SimpleFactory类,只要SimpleFactory类一出问题,系统就进入不能工作的状态,这也是最为致命的一点....

以上的不足将在工厂模式的另外两种状态中得到解决。

工厂方法(Factory Method)

上面的代码告诉我们,简单工厂并不简单,它是整个模式的核心,一旦他出了问题,整个模式都将受影响而不能工作,为了降低风险和为日后的维护、扩展做准备,我们需要对它进行重构,引入工厂方法。

工厂方法为工厂类定义了接口,用多态来削弱了工厂类的职能,以下是工厂接口的定义:

  1. public interface Factory{
  2. public Product create();
  3. }

我们再来定义一个产品接口

  1. public interface Product{}

一下是实现了产品接口的产品类

  1. public class Washer implements Product{
  2. public Washer(){
  3. System.out.println("洗衣机被制造了");
  4. }
  5. }
  6. public class Icebox implements Product{
  7. public Icebox(){
  8. System.out.println("冰箱被制造了");
  9. }
  10. }
  11. public class AirCondition implements Product{
  12. public Icebox(){
  13. System.out.println("空调被制造了");
  14. }
  15. }

接下来,就是工厂方法的核心部分,也就是具体创建产品对象的具体工厂类,

  1. //创建洗衣机的工厂
  2. public class CreateWasher implements Factory{
  3. public Product create(){
  4. return new Washer();
  5. }
  6. }
  7. //创建冰箱的工厂
  8. public class CreateIcebox implements Factory{
  9. public Product create(){
  10. return new Icebox();
  11. }
  12. }
  13. //创建空调的工厂
  14. public class CreateAirCondition implements Factory{
  15. public Product create(){
  16. return new AirCondition();
  17. }
  18. }

从上面创建产品对象的代码可以看出,工厂方法和简单工厂的主要区别是,简单工厂是把创建产品的职能都放在一个类里面,而工厂方法则把不同的产品放在实现了工厂接口的不同工厂类里面,这样就算其中一个工厂类出了问题,其他工厂类也能正常工作,互相不受影响,以后增加新产品,也只需要新增一个实现工厂接口工厂类,就能达到,不用修改已有的代码。但工厂方法也有他局限的地方,那就是当面对的产品有复杂的等级结构的时候,例如,工厂除了生产家电外产品,还生产手机产品,这样一来家电是手机就是两大产品家族了,这两大家族下面包含了数量众多的产品,每个产品又有多个型号,这样就形成了一个复杂的产品树了。如果用工厂方法来设计这个产品家族系统,就必须为每个型号的产品创建一个对应的工厂类,当有数百种甚至上千种产品的时候,也必须要有对应的上百成千个工厂类,这就出现了传说的类爆炸,对于以后的维护来说,简直就是一场灾难.....

抽象工厂(Factory Method)

抽象工厂:意的意图在于创建一系列互相关联或互相依赖的对象。<<Java设计模式>>

我自己觉得抽象工厂是在工厂方法的基础上引进了分类管理的概念....

工厂方法用来创建一个产品,它没有分类的概念,而抽象工厂则用于创建一系列产品,所以产品分类成了抽象工厂的重点,

我们继续用上面的例子来说明:

工厂生产的所有产品都用都用大写字母来标明它们的型号,比如冰箱,就有“冰箱-A",“冰箱-B",同样,其他的产品也都是遵守这个编号规则,于是就有了一下产品家族树

冰箱:

  1. 冰箱-A
  2. 冰箱-B

洗衣机:

  1. 洗衣机-A
  2. 洗衣机-B

我们可以为冰箱和洗衣机分别定义两个产品接口,以对他们进行分类,

  1. //洗衣机接口
  2. public interface Washer{
  3. }
  4. //冰箱接口
  5. public interface Icebox{
  6. }

接着,我们分别创建这两个接口的具体产品

  1. //洗衣机-A
  2. public class WasherA implements Washer{
  3. public WasherA(){
  4. System.out.println("洗衣机-A被制造了");
  5. }
  6. }
  7. //洗衣机-B
  8. public class WasherB implements Washer{
  9. public WasherB(){
  10. System.out.println("洗衣机-B被制造了");
  11. }
  12. }
  13. //冰箱-A
  14. public class IceboxA implements Icebox{
  15. public IceboxA(){
  16. System.out.println("冰箱-A被制造了");
  17. }
  18. }
  19. //冰箱-B
  20. public class IceboxB implements Icebox{
  21. public IceboxB(){
  22. System.out.println("冰箱-B被制造了");
  23. }
  24. }

到此,产品部分我们准备好了,接下来我们来处理工厂部分,我们先来定义工厂行为接口

  1. public interface Factory{
  2. public Washer createWasher();
  3. public Icebox createIcebox();
  4. }

接下来我创造具体的工厂类,我们根据上面产品的接口,把型号A的产品分为一类,由一个工厂来管理,把型号为B的产品有另一个工厂管理,根据这个分类,我们可以实现如下的两个具体工厂类

  1. //创建型号为A的产品工厂
  2. public class FactoryA implements Factory{
  3. //创建洗衣机-A
  4. public Washer createWasher(){
  5. return new WasherA();
  6. }
  7. //创建冰箱-A
  8. public Icebox createIcebox(){
  9. return new IceboxA();
  10. }
  11. }
  12. //创建型号为B的产品工厂
  13. public class FactoryB implements Factory{
  14. //创建洗衣机-B
  15. public Washer createWasher(){
  16. return new WasherB();
  17. }
  18. //创建冰箱-B
  19. public Icebox createIcebox(){
  20. return new IceboxB();
  21. }
  22. }

这样,我们的抽象工厂就完成了。有上面可以看出,在运用上我觉得工厂方法和抽象工厂,都有自己的应用场景,并没有什么优劣之分,但在应用抽象工厂之前,要先对创建的对象进行系统的分类,这点很重要,好的产品分类规则能为具体工厂类的选择调用和以后的扩展提供清晰的思路.

Caffe 中的 Layer 是神经网络 Net 的基本结构,Caffe 内部维护一个注册表用于查找特定 Layer 对应的工厂函数。很多同学在 Windows 下使用 Caffe 遇到的一个问题就是运行 Caffe 相关的代码时出现无法找到 Layer,但是这个问题不会在 Linux 平台上出现,这个问题跟编译器有关,同时也是跟 Caffe 注册 Layer 的机制有关。

1
2
F0203 12:50:07.581297 11524 layer_factory.hpp:78] Check failed: registry.count(type) == 1 (0 vs. 1)
Unknown layer type: Convolution (known types: )

上面的错误是无法在注册表中找到 Convolution Layer 对应的工厂函数,程序直接崩溃。下面我们就来聊聊 Caffe 的 Layer 加载机制,以及为什么在 VC 下会出现这种问题。

Caffe 的 Layer 注册表其实就是一组键值对,key 为 Layer 的类型而 value 则对应其工厂函数。下面两组宏控制了 Layer 的注册动作。

1
2
3
4
5
6
7
8
9
10
11
#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>) \
#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)

REGISTER_LAYER_CLASS 宏可以实现将特定 Layer 注册到全局注册表中,首先定义一个工厂函数用来产生 Layer 对象,然后调用 REGISTER_LAYER_CREATOR 将工厂函数和 Layer 的类型名进行注册,注册时只是用 Layer 的 float 和 double 类型,这是网络实际数据使用到的类型。两个静态变量一个对应 float,另一个对应 double,这两个变量的初始化,也就是它们的构造函数实际上完成 Layer 的注册动作。

1
2
3
4
5
6
7
8
template <typename Dtype>
class LayerRegisterer {
public:
LayerRegisterer(const string& type,
shared_ptr<Layer<Dtype> > (*creator)(const LayerParameter&)) {
LayerRegistry<Dtype>::AddCreator(type, creator);
}
};

LayerRegisterer 对象初始化时实际上又是调用相应类型的 LayerRegistry 类的静态方法 AddCreator

1
2
3
4
5
6
typedef std::map<string, Creator> CreatorRegistry;
static CreatorRegistry& Registry() {
static CreatorRegistry* g_registry_ = new CreatorRegistry();
return *g_registry_;
}

注册表类型为 CreatorRegistry,实际类型为 std::map<string, Creator>。可以通过 Registry 函数获取注册表的全局单例。而注册的过程就是一个简单的 map 操作。

1
2
3
4
5
6
7
// 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;
}

注册的过程大概就是上面说到的流程。Caffe 中的 Layer 采用静态变量初始化的方式来注册工厂函数到全局注册表中,整个注册过程依赖这些静态变量。那么问题来了,为什么 VC 中的代码无法在注册表中找到 Layer 对应的工厂函数?事实上,VC 中 Caffe 代码的全局注册表是空的,一条记录都没有,问题并不是出在这个全局注册表,而是那些完成注册动作的静态变量。由于这些静态变量存在的意义在于其构造函数完成 Layer 的注册动作,没有任何一段代码会去引用这些静态变量,这个坑在于 VC 默认会优化掉这些静态变量,那么所有这些静态变量对应的构造函数将无法执行,那么注册动作一个都不会触发,导致全局注册表为空,然后在构造网络 Net 时就会崩溃。