动手写一个Caffe层:矩阵相乘Matmul

动手写一个Caffe层:矩阵相乘Matmul


  • 背景
  • 实现
    • 前向传播实现
    • 后向传播实现
  • backward推导
  • 小结

背景

最近在研究chainer网络的caffe实现,顺便也体验一下caffe。对于caffe训练过程的基本认识为搭积木,按顺序写好net.prototxt即可。但是有些时候会遇到没有想要的那块儿积木,这个时候就得自己造一造。

我期望的目的是实现一个 Y=XXT 的层,将我当前层输出的Blob数据做一个矩阵相乘的操作,然后再送去计算loss。无奈找了半天,caffe里确实没有这样一个层,就准备自己写一下。好在InnerProduct的功能其实和这个矩阵乘是一样的,差异在于InnerProduct里面的权重是自动初始化并且梯度调节的。而我想要的矩阵乘,X自身的转置就是其权重。但是这也是好事儿,我可以参考InnerProduct来实现。


实现

关于实现自定义层,可以参考官方文档,下面我也将对照官方文档进行步骤说明。在这之前,也可以参考L2正则化层的实现,这个就讲得更实例一些。
官方文档看着挺长,实际总结一下就如下几步:

  • 实现your_layer.hpp
    • layer type 定义
    • layer blob 定义
    • 前向/后向接口定义
  • 实现your_layer.cpp
    • LayerSetup实现
    • Reshape实现
    • 前向传播Forward_cpu实现
    • 后向传播Backward_cpu实现
  • 写对应的GPU实现版本(可选)
  • 定义相应的layer_param(可选)
  • 实例化your_layer
  • 写对应testbench(可选)

实现your_layer.hpp

那么就顺着说吧,先在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相关的一律咔掉。

实现your_layer.cpp

这是整个自定义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推导

这一部分主要是推导一下矩阵相乘的偏导数,用于Backward计算过程中输出给bottom_cpu_diff()。用到的也是高数中基本的矩阵求导和链式法则。
条件:

X=x11x21xc1x12x22xc2x1kx2kxck[c×k](1)

XT=x11x12x1kx21x22x2kxc1xc2xck[k×c],(2)

其中X作为bottom_blob,经过caffe的 Reshape_layer从NxCxHxW维变换为Cx(HxW=k)维。 XT X 的转置,具有kxC的维度。
Y=XXT=i=1kx1ix1ii=1kx2ix1ii=1kxcix1ii=1kx1ix2ii=1kx2ix2ii=1kxcix2ii=1kx1ixcii=1kx2ixcii=1kxcixci[c×c](3)

EY=dif11dif21difc1dif12dif22difc2dif1cdif2cdifcc[c×c],(4)

其中元素对矩阵求偏导的结果 EY 是相同size的矩阵形式,而且保存在Blob_top_[g]/cpu_diff()中,是已知量,而且由于Y矩阵的对称性,其偏导数矩阵 EY 仍然也是对称的,有
difij=difji(5)

目标:
EX=Ex11Ex21Exc1Ex12Ex22Exc2Ex1kEx2kExck[c×k](6)

将链式求导法则

Exij=m/n=1cEymnymnxij=m/n=1cdifmnymnxij,(7)
应用于Y矩阵,可以得到对应元素的偏导。
一开始看不出规律,我就先计算了 Ex11Ex12 ,如果仔细按照()计算,可以发现 Ex11=2i=1cdif1ixi1 Ex12=2i=1cdif1ixi2 。进而推广到一般情况 Exij
xij 相关的矩阵Y中数据为第i行和第i列(由于X矩阵的列标j在相乘过程中的累加操作已经消除掉作用了,这里需要仔细想想):
p=1kxipx1pp=1kxipx2pp=1kx1pxipp=1kx2pxipp=1kxipxipp=1kxcpxipp=1kxipxcp[c×c](8)

将公式(5)代入公式(7)有:
Exij=m/n=1cdifmnymnxij=difi1x1j+dif1ix1j+difi2x2j+dif2ix2j++dificxcj+difcixcj=2(difi1x1j+difi2x2j+dificxcj)=2p=1cdifipxpj(9)

所以可以 得到
EX[c×k]=EY[c×c]X[c×k](10)

其实从矩阵的size也可以验证,公式所述的方式是对应正确的。ok,既然Backward也是一个矩阵相乘的方式,在上一节中的后向传播也可以沿用 caffe_cpu_gemm进行计算。


小结

1.主要就是介绍了一个简单地caffe自定义layer的实现,由于做的过程中基本都是调用CBLAS的内建函数,写起来并不复杂,主要是公式推导以及自定义layer的过程走通了,希望对大家有帮助。
2.在做实验的时候,可以通过在Forward_cpu函数中调用LOG(INFO) << "value is" <的方式,打印log帮助你调试。


你可能感兴趣的:(MachineLearning)