Caffe添加自定义的层

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层,重新训练得到模型文件,训练过程输出文本内容较长,仅给出其中几个部分,如下:
这里写图片描述
这里写图片描述
这里写图片描述]https://img-blog.csdn.net/20171221175046001?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2FuZ2dhb18xOTkw/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)![这里写图片描述

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则不会影响,因为其对数据未做修改。

猜你喜欢

转载自blog.csdn.net/wanggao_1990/article/details/78863669