caffe官方的层更新的速度还是比较慢,有时不得不自己实现新的层。本文以实现L2 normalize为例(详细代码,可以查看CaffeLayers),简单记录下实现的过程。
实现新的层,主要步骤如下:
在实现的过程中顺便对C++的语法进行简要的复习。
我们需要定义NormalizeLayer
这个类,这个类需要从抽象类Layer
中继承。声明需要改写的虚函数。LayerSetUp()
、type()
、 限制该层的输入bottom和top个数
和 Forward_cpu()
、 Forward_gpu()
Backward_cpu()
、Backward_gpu()
等。
NormalizeLayer()
构造函数NormalizeLayer()
用Layerparameter& param作为形参,并调用父类的构造函数实现。
explicit NormalizeLayer(const Layerparameter ¶m)
:Layer<Dtype>(param){}
注: explicit 可以避免构造函数被调用造成隐式转换,用explicit声明的构造函数只能用于直接初始化
LayerSetUp()
完成网络建立时,从prototxt中读取Layer的参数,可通过this->layerparam 访问该层的参数并对类的成员变量进行初始化并为网络的权重参数分配存储。 NormalizeLayer中没有参数,所以这一步可以省略掉。
virtual void LayerSetUp(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>&top);
type()
type()
函数是一个返回层的层的种类的inline函数,形式如
virtual inline const char* type() const {return "Normalize";}
Reshape()
Reshape()
与LayerSetUp的功能类似,但是LayerSetUp()
只在网络初始化时被调用,而reshape是在每次前向计算前,根据Bottom的大小动态计算Top的大小并分配存储(只要与Bottom大小相关的都要重新分配存储)。在此处,需要重新分配存储的是top和成员变量, 通过调用Blob的Reshape函数即可。
virtual void Reshape(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top);
Blobs数目相关
virtual inline int ExactNumBottomBlobs() const {return 1;}
virtual inline int ExactNumTopBlobs() const {return 1;}
前向后项传播(CPU&GPU)
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);
类成员变量的声明
//保留前向的中间变量,可以减少反向传播中相同表达式的计算量
//squared_: bottom的平方(Elememt-wise)
//norm_: 由样本特征的L2范数构成的向量(size: num)
//sum_multiplier_: GPU实现辅助变量
Blob<Dtype> sum_multiplier_, norm_, squared_;
实现normalize_layer.hpp中相关函数的定义, 在normalize_layer.cpp
中,我们需要对前向Forward_cpu
,后向Backward_cpu
,Reshape
进行实现。
Reshape
template <typename Dtype>
void NormalizeLayer<Dtype>::Reshape(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top){
top[0]->Reshape(bottom[0]->num(), bottom[0]->channels(),
bottom[0]->height(), bottom[0]->width());
squared_.Reshape(bottom[0]->num(), bottom[0]->channels(),
bottom[0]->height(), bottom[0]->width());
}
Forward_cpu
前向操作比较简单即 y=x/||x||
template <typename Dtype>
void NormalizeLayer<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();
Dtype* squared_data = squared_.mutable_cpu_data();
int n = bottom[0]->num();
int d = bottom[0]->count() / n;
caffe_sqr<Dtype>(n*d, bottom_data, squared_data);
for (int i=0; i<n; ++i) {
Dtype normsqr = caffe_cpu_asum<Dtype>(d, squared_data+i*d)+(Dtype)1e-10;
caffe_cpu_scale<Dtype>(d, pow(normsqr, -0.5), bottom_data+i*d, top_data+i*d);
}
}
Backward_cpu
这里先简单介绍下NormalizeLayer的反向求导。
$E=f(y_{1},y_{2},...y_{n})$
$E=f(y_{1}(x_{1},s(x_{1},x_{2},...,x_{i})),y_{2}(x_{2},s(x_{1},x_{2},...,x_{i},...x_{n})),y_{i}(x_{i},s(x_{1},x_{2},...,x_{i},...x_{n}))$
其中$s =\left \| x\right \| = \sqrt{x_{1}^{2}+x_{2}^{2}+...+x_{n}^{2}},y_{i}=\frac{x_{i}}{s}$,n表示向量的维度
$\frac{\partial E}{\partial x_{i}} =\frac{\partial E}{\partial y_{i}}*\frac{\partial y_{i}}{\partial x_{i}}+\sum_{j=1}^{n} \frac{\partial E}{\partial y_{j}}*\frac{\partial y_{j}}{\partial s}*\frac{\partial s}{\partial x_{i}} =\frac{\partial E}{\partial y_{i}}*(\frac{1}{s}+\sum\_{j=1}^{n}(-\frac{x_{j}}{s^2})*(\frac{x_{j}}{s}))=\frac{\partial E}{\partial y_{i}}*\frac{1}{s}(1-\sum_{j=1}^{n}y_{j}^{2})=\frac{\partial E}{\partial y_{i}}*\frac{1}{s}(1-y \cdot y)$
void NormalizeLayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,
const vector<bool>& propagate_down,const vector<Blob<Dtype>*>& bottom){
const Dtype* top_diff = top[0]->cpu_diff();
const Dtype* top_data = top[0]->cpu_data();
const Dtype* bottom_data = bottom[0]->cpu_data();
Dtype* bottom_diff = bottom[0]->mutable_cpu_diff();
int n = top[0]->num();
int d = top[0]->count() / n;
for (int i=0; i<n; ++i) {
Dtype a = caffe_cpu_dot(d, top_data+i*d, top_diff+i*d);
caffe_cpu_scale(d, a, top_data+i*d, bottom_diff+i*d);
caffe_sub(d, top_diff+i*d, bottom_diff+i*d, bottom_diff+i*d);
a = caffe_cpu_dot(d, bottom_data+i*d, bottom_data+i*d);
caffe_cpu_scale(d, Dtype(pow(a, -0.5)), bottom_diff+i*d, bottom_diff+i*d);
}
}
实例化模板类
#ifdef CPU_ONLY
STUB_GPU(NormalizeLayer);
#endif
INSTANTIATE_CLASS(NormalizeLayer);
REGISTER_LAYER_CLASS(Normalize);
STUB_GPU 定义GPU的前向后向操作
INSTANTIATE_CLASS 实例化模板类
REGISTER_LAYER_CLASS 完成类的注册(工厂模式)
caffe中将大部分数学操作都实现了GPU和CPU两种版本,例如,caffe_cpu_dot
和 caffe_gpu_dot
,所以一般的层的GPU实现只需要把CPU实现的函数改成对应的GPU版本就可以。
由于Normalize该层没有需要设置的参数,所以caffe.proto并不需要修改.
为了验证自己编写层的正确性,我们通过在test中测试层的前向和反向操作。caffe中运用Google C++ Testing Framework 来进行测试。关于gtest,可以参考gtest in caffe
编写完测试文件,重新编译,进行测试
$ make
$ make test
$ make runtest GTEST_FILTER='NormalizeLayerTest/*'
Note:最后一条命令行不要漏了/*
!
评论 (0)