当前位置 博文首页 > wanggao的专栏:Caffe添加自定义的层

    wanggao的专栏:Caffe添加自定义的层

    作者:[db:作者] 时间:2021-09-09 09:53

    1、介绍

    在使用Caffe时,可能已有的层不满足需求,需要实现自己的层,最好的方式是修改caffe.proto文件,增加对应cpp、h、cu的声明和实现,编译caffe库即可。
    本文参考http://blog.csdn.net/kkk584520/article/details/52721838实现,使用caffe-windows版本编译,将遇到的问题写下。

    本文实现的Layer名称为AllPassLayer(肯定不能和已有的层同名),将输入到该层的数据不做任何改变直接输出到下一层。AllPassLayer层的Forward和Backwark实现非常容易,直接将输入的blob数据复制到输出的blob中,因此可以加入到任何已有网络中,不影响训练和测试的结果。

    2、AallPassLayer代码实现

    添加新的层AallPassLayer实现,需要同其他的Layer类一样,分成声明和实现两个部分,对应放在.hpp和.cpp文件中,如果有cuda实现,还应有.cu文件。其中.hpp头文件放在/caffe-windows/include/caffe/layers/文件夹下,而 .cpp 和 .cu 放入/caffe-windows/src/caffe/layers下。为方便起见,这里仅实现CPU上的代码。
    需要注意的地方,已经添加了注释。

    2.1、头文件all_pass_layer.hpp

    // added by wanggao   TEST  [12/20/2017 14:20 wg]
    
    #ifndef CAFFE_ALL_PASS_LAYER_HPP_  
    #define CAFFE_ALL_PASS_LAYER_HPP_  
    
    #include <vector>  
    
    #include "caffe/blob.hpp"  
    #include "caffe/layer.hpp"  
    #include "caffe/proto/caffe.pb.h" 
    
    #include "caffe/layers/neuron_layer.hpp"
    
    namespace caffe {
    template <typename Dtype>
    class AllPassLayer : public NeuronLayer<Dtype> { //注意:从NeuronLayer继承
    public:
    	explicit AllPassLayer(const LayerParameter& param)
    		: NeuronLayer<Dtype>(param) {}
    	virtual inline const char* type() const { return "AllPass"; }
    
    protected:
    	virtual void Forward_cpu(const vector<Blob<Dtype>*>& bottom,
    		const vector<Blob<Dtype>*>& top);
    	virtual void Forward_gpu(const vector<Blob<Dtype>*>& bottom,
    		const vector<Blob<Dtype>*>& top);
    	virtual void Backward_cpu(const vector<Blob<Dtype>*>& top,
    		const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom);
    	virtual void Backward_gpu(const vector<Blob<Dtype>*>& top,
    		const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom);
    };
    
    }  // namespace caffe  
    #endif  // CAFFE_ALL_PASS_LAYER_HPP_  
    

    2.2、实现文件all_pass_layer.cpp

    // added by wanggao   TEST  [12/20/2017 14:20 wg]
    
    #include <algorithm>  
    #include <vector>  
    
    #include "caffe/layers/all_pass_layer.hpp"  
    
    #include <iostream>  
    using namespace std;
    #define DEBUG_AP(str) cout<<str<<endl  
    namespace caffe {
    
    template <typename Dtype>
    void AllPassLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
    	const vector<Blob<Dtype>*>& top) {
    	const Dtype* bottom_data = bottom[0]->cpu_data();
    	Dtype* top_data = top[0]->mutable_cpu_data();
    	const int count = bottom[0]->count();
    
    	//for (int i = 0; i < count; ++i) {
    	//	top_data[i] = bottom_data[i];
    	//}
    	caffe_copy(count, bottom_data, top_data);
    
    	//注意:获取参数的两个变量 "all_pass_param" 和 "key"
    	DEBUG_AP("Here is All Pass Layer, forwarding.");
    	DEBUG_AP(this->layer_param_.all_pass_param().key());
    }
    
    template <typename Dtype>
    void AllPassLayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,
    	const vector<bool>& propagate_down,
    	const vector<Blob<Dtype>*>& bottom) {
    	if (propagate_down[0]) {
    		//const Dtype* bottom_data = bottom[0]->cpu_data();
    		const Dtype* top_diff = top[0]->cpu_diff();
    		Dtype* bottom_diff = bottom[0]->mutable_cpu_diff();
    		const int count = bottom[0]->count();
    		//for (int i = 0; i < count; ++i) {
    		//	bottom_diff[i] = top_diff[i];
    		//}
    		caffe_copy(count, top_diff, bottom_diff);
    	}
    	//同上
    	DEBUG_AP("Here is All Pass Layer, backwarding.");
    	DEBUG_AP(this->layer_param_.all_pass_param().key());
    }
    
    #ifdef CPU_ONLY  
    STUB_GPU(AllPassLayer);
    #endif  
    
    //注意:下面2行对层进行注册,必不可少;注意宏参数不能弄错
    INSTANTIATE_CLASS(AllPassLayer); // 类的名称 AllPassLayer
    REGISTER_LAYER_CLASS(AllPass);   // 层的名称 AllPass
    }  // namespace caffe  
    

    3、修改caffe.proto文件

    某些情况下我们训练或者测试时,不需要从ptototxt的layer中获取参数,比如split层,concat层等,如果我们这里的.cpp实现文件中注释掉四个DEBUG_AP行,就是一个不带参数的层,那么这里就不用修改caffe.proto文件,直接可以编译caffe.lib库了。
    当我们的层需要指定参数时,如卷积层Convolution的参数convolution_param中卷积核数量num_output、卷积核尺寸kernel_size、步长stride等,需要在cpp代码中使用,我们就必须先修改caffe.proto文件,利用ProtoBuffer生成caffe.pb.h和caffe.pb.cc文件,编译生成caffe.lib库。
    下面针对AllPassLayer层,对caffe.proto文件进行修改。

    第一步 找到message LayerParameter { … } 部分代码,并在其中新添
    optional AllPassParameter all_pass_param = 147;,增加后的效果如下

    // NOTE
    // Update the next available ID when you add a new LayerParameter field.
    //
    // LayerParameter next available layer-specific ID: 147 (last added: recurrent_param)
    message LayerParameter {
      optional string name = 1; // the layer name
      optional string type = 2; // the layer type
      repeated string bottom = 3; // the name of each bottom blob
      repeated string top = 4; // the name of each top blob
      //... 中间部分省略
      optional ThresholdParameter threshold_param = 128;
      optional TileParameter tile_param = 138;
      optional WindowDataParameter window_data_param = 129;
    
      // 注意:下面为新增加的一行
      optional AllPassParameter all_pass_param = 147;
    }
    

    注意新增加行中的ID数值147,可以为其他值(如200,500等),但是不能与当前已经存在的层参数ID相同。在Note中已经提示下一个可用的参数ID为147。

    第二步 添加 message AllPassParameter { },指定参数名称

    message AllPassParameter {  
      optional float key = 1 [default = 0];  
    }  
    

    注意:在上面2个步骤中,可以看到两个参数 “all_pass_param” 和 "key"是和cpp中调用参数的代码对应起来的(后面讲解中,这2个参数还与.prototxt有关)

    EBUG_AP(this->layer_param_.all_pass_param().key());
    

    最后,完成上面2个步骤后,就可以成功编译生成caffe.lib库了。

    4、测试

    下面从2个部分测试我们新加层后的caffe运行效果。

    4.1、 修改deploy.prototxt文件

    在mnist手写字符体识别中,我们用到lenet网络训练生成模型并对单幅图片进行了分类测试(跳转链接)。这里我们对测试时使用的prototxt进行修改,在data和conv前2层中间插入我们的层AllPassLayer。原letnet前2层如下图:
    这里写图片描述

    修改后(增加部分用框标出)的为下图:
    这里写图片描述
    修改好之后,直接调用之前训练好的模型对单张图片分类,结果如下:
    这里写图片描述
    运行结果正常,并且打印出了我们在cpp代码中执行输出的字符串和从prototxt中读取的参数值。

    4.2、对训练文件.prototxt修改

    同4.1中的步骤,修改训练用到的prototxt文件,在data和conv1中添加AllPass层,重新训练得到模型文件,训练过程输出文本内容较长,仅给出其中几个部分,如下:
    这里写图片描述
    这里写图片描述
    这里写图片描述

    5、问题总结

    (1)新增的类AllPassLayer派生于NeuronLayer类,若修改派生于Layer类,会出现以下错误,定位在 REGISTER_LAYER_CLASS(AllPass);语句。

    错误	C2259 “caffe::AllPassLayer<float>”: 不能实例化抽象类 caffe E:\deep learning\caffe-windows-CPU\src\caffe\layers\all_pass_layer.cpp 54
    错误	C2259 “caffe::AllPassLayer<double>”: 不能实例化抽象类 caffe E:\deep learning\caffe-windows-CPU\src\caffe\layers\all_pass_layer.cpp 54
    

    原因:Layer中的纯虚函数virtual void Reshape(const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top)在AllPassLayer没有实现,因此也认为其为抽象类,不能实例化。
    解决办法:在AllPassLayer类中实现这个方法,函数体这里可以为空;另外,可以从NeuronLayer类继承,该类已经实现了reshape方法(输出与输入的shape相同)。

    (2)当新增加的层不需要从外部(prototxt)读取参数,就不需要修改caffe.proto文件,添加好.hpp\.cpp\.cu等文件,并使用宏注册参数即可编译成功。

    (3)训练保存好模型之后,在测试的deploy.prototxt中添加层时,若添加的层在caffe中已经实现,并且能够对应上输入输出的shape,就可以正常进行测试。但是,可能会对结果造成影响,因为新的层可能对数据进行了修改。这里的AllPass则不会影响,因为其对数据未做修改。

    cs