最近在研究chainer网络的caffe实现,顺便也体验一下caffe。对于caffe训练过程的基本认识为搭积木,按顺序写好net.prototxt即可。但是有些时候会遇到没有想要的那块儿积木,这个时候就得自己造一造。
我期望的目的是实现一个 Y=XXT 的层,将我当前层输出的Blob数据做一个矩阵相乘的操作,然后再送去计算loss。无奈找了半天,caffe里确实没有这样一个层,就准备自己写一下。好在InnerProduct的功能其实和这个矩阵乘是一样的,差异在于InnerProduct里面的权重是自动初始化并且梯度调节的。而我想要的矩阵乘,X自身的转置就是其权重。但是这也是好事儿,我可以参考InnerProduct来实现。
关于实现自定义层,可以参考官方文档,下面我也将对照官方文档进行步骤说明。在这之前,也可以参考L2正则化层的实现,这个就讲得更实例一些。
官方文档看着挺长,实际总结一下就如下几步:
那么就顺着说吧,先在include/caffe/layers/your_layer.hpp
目录下新建一个hpp文件,里面实现type()虚函数,和控制Blob个数的虚函数,Blob个数可以是动态的,也可以是写死的。
//blob number
virtual inline int ExactNumBottomBlobs() const { return 1; }
virtual inline int ExactNumTopBlobs() const { return 1; }
//type
virtual inline const char* type() const { return "Matmul"; }
其中ExactNumBottomBlobs()
和ExactNumTopBlobs()
控制我的bottom Blob和top Blob个数为1,type()
返回我的layer名字Matlul。
当然另外一种偷懒的办法,可以把/caffe/layers/inner_product_layer.hpp
复制过来,然后改改名字,删掉不用执行的接口,比如为了简便这个Matmul层就直接CPU实现啦,与GPU相关的一律咔掉。
这是整个自定义layer的重点。
首先LayerSetup方法我就把它当成每个layer的初始化后进行的参数赋值(虽然基类Layer的构造函数并没有参数赋值的功能),一些会用到的参数都在这里初始化,比如:
M_ = bottom[0]->num(); //rows of A
N_ = bottom[0]->num(); //cols of B
K_ = bottom[0]->channels(); //rows of A = cols of B
W_ = sqrt(K_);
//for value check
//LOG(INFO) << "M:" << M_ <<" K_:" << K_ << " W_:" << W_ ;
其中M_,N_,K_的取值,是为了下面调用caffe_cblas内建函数caffe_cpu_gemm
做矩阵乘时的接口所需要的,这也是参考InnerProduct_layer的写法。
Reshape方法,在这里的作用是定义输出的top Blob的size,如果不规定好,程序不知道输出的Blob具有什么size,也就无法进行下去了。
int myints[] = {M_, M_};
vector top_shape (myints, myints + sizeof(myints) / sizeof(int) );
top[0]->Reshape(top_shape); //reshape top
其中最主要的就是讲top的size规定好,实现的代码我写得不优雅,功能倒是可以实现。
Forward_cpu的函数主要负责当前layer前向传播时输出数据的计算,由于矩阵相乘在InnerProduct_layer中也有用到,而且这是就是最基本的运算,所以可以调用内建函数实现
const Dtype* bottom_data = bottom[0]->cpu_data();
Dtype* top_data = top[0]->mutable_cpu_data();
const Dtype* weight = bottom[0]->cpu_data();
caffe_cpu_gemm(CblasNoTrans, CblasTrans,
M_, N_, K_, (Dtype)1.,
bottom_data, weight,
(Dtype)0., top_data);
只想说明一下mutable前缀的变量是可以修改的,即我们输出的top数据要修改,所以改变的数据都是mutable部分。关于caffe_cpu_gemm
的详细信息,可以参考一下官方文档或者seven_first的中文解析。
Backward_cpu函数用于计算当前layer后向传播时对bottom层偏导数的计算,对于当前这个矩阵相乘的layer,同样可以用一次caffe_cpu_gemm
实现,具体代码段如下,backward的推导见下一节。
const Dtype* top_diff = top[0]->cpu_diff();
Dtype* bottom_diff = bottom[0]->mutable_cpu_diff();
const Dtype* weight = bottom[0]->cpu_data();
caffe_cpu_gemm(CblasNoTrans, CblasNoTrans,
M_, K_, N_, (Dtype)1.,
top_diff, weight,
(Dtype)0., bottom_diff);
代码部分写完了,要想你的protext文件中能够调用自定义的层,需要对你的层进行“注册”,让它可以实例化。主要的步骤就是在your_layer.cpp文件的末尾,添加如下两行:
INSTANTIATE_CLASS(MatmulLayer);
REGISTER_LAYER_CLASS(Matmul);
如果要添加别的名字的layer,就更换一下即可。这样就可以在你的prototxt里面通过type = "Matmul"
来调用Matmul层了。
最后,记得把你的caffe重新编译一下,才可以使你的改动生效哦。
这一部分主要是推导一下矩阵相乘的偏导数,用于Backward计算过程中输出给bottom_cpu_diff()
。用到的也是高数中基本的矩阵求导和链式法则。
条件:
Reshape_layer
从NxCxHxW维变换为Cx(HxW=k)维。
XT 为
X 的转置,具有kxC的维度。
将链式求导法则
caffe_cpu_gemm
进行计算。
1.主要就是介绍了一个简单地caffe自定义layer的实现,由于做的过程中基本都是调用CBLAS的内建函数,写起来并不复杂,主要是公式推导以及自定义layer的过程走通了,希望对大家有帮助。
2.在做实验的时候,可以通过在Forward_cpu函数中调用LOG(INFO) << "value is" <