在Caffe中加入PythonLayer

2018-03-29  本文已影响0人  Jimory

下面是一些参考链接

[1]Caffe Python Layer

[2]Using Python Layers in your Caffe models with DIGITS

[3]What is a “Python” layer in caffe?

[4]caffe python layer

[5]Building custom Caffe layer in python

[6]Aghdam, Hamed Habibi, and Elnaz Jahani Heravi. Guide to Convolutional Neural Networks: A Practical Application to Traffic-Sign Detection and Classification. Springer, 2017.

[7]Softmax with Loss Layer

1. 准备工作

1.1 系统配置

Ubuntu 16.04 LTS

Caffe:https://github.com/BVLC/caffe

Python 2.7.14

1.2 编译Caffe

按照一般的caffe编译流程(可参考官网,也可参考Install caffe in Ubuntu)就好,唯一的区别就是在Makefile.config中,把这一行修改一下:

# WITH_PYTHON_LAYER := 1

改成

WITH_PYTHON_LAYER := 1

说明我们是要使用python_layer这个功能的。然后编译成功后,在Terminator中输入:

$ caffe$ python>>> import caffe

像这样,没有给你报错,说明caffe和python_layer都编译成功啦.

1.3 添加Python路径

1.3.1 在~/.bashrc文件中加入路径

写自己的python layer势必需要.py文件,为了让caffe运行的时候可以找到你的py文件,接下来需要把py文件的路径加到python的系统路径中,步骤是:

打开Terminator

输入vi ~/.bashrc

输入i,进入编辑模式

在打开的文件的末尾添加

export PYTHONPATH=/path/to/my_python_layer:$PYTHONPATH

键入esc,:wq,回车,即可保存退出

如果这部分没有看明白,需要上网补一下如何在Linux环境中用vim语句修改文档的知识. 实质上就是修改一个在~/路径下的叫.bashrc的文档.

1.3.2 在要训练的.sh脚本文件中加入路径

首先将PythonLayer放入任意位置(要知道其所在路径),然后在训练网络的.sh脚本文件中加入PythonLayer脚本路径

export PYTHONPATH=/home/用户名/pythonlayer文件夹名

2. 修改代码

首先我们定义一个要实现的目标:训练过程中,在Softmax层和Loss层之间,加入一个Python Layer,使得这个Layer的输入等于输出. 换句话说,这个Layer没有起到一点作用,正向传播的时候y=x,反向传播的时候导数y'=1. 因此训练的结果应该和没加很相似.

2.1 train_val.prototxt

这个文档是Caffe训练的时候,定义数据和网络结构用的,所以如果用添加新的层,需要在这里定义. 第一步是在网络结构的定义中找到添加Python Layer的位置,根据问题的定义,Python Layer应该在softmax和loss层之间,不过网上的prototxt大多会把这两个层合并在一起定义,成为了

layer{name:"loss"type:"SoftmaxWithLoss"bottom:"fc8_2"bottom:"label"top:"loss"}

我们需要把这个层拆开,变成softmax层和loss层,根据Caffe提供的官方文档,我们知道SoftmaxWithLoss是softmax层和MultinomialLogisticLoss的合并.

The softmax loss layer computes the multinomial logistic loss of the softmax of its inputs. [7]

那拆开后的代码就是

layer{name:"output_2"type:"Softmax"bottom:"fc8_2"top:"output_2"}layer{name:"loss"type:"MultinomialLogisticLoss"bottom:"output_2"bottom:"label"top:"loss"}

拆完了以后就只需要把你定义的Python Layer加到它们中间就好了,注意这个层的输出和输出,输入是bottom,输出是top,这两个值需要和上一层的softmax输出和下一层的loss输入对接好,就像这样(请仔细看注释和代码):

layer {# softmax层name:"output_2"type:"Softmax"bottom:"fc8_2"# 是上一层Fully Connected Layer的输出top:"output_2"# 是Softmax的输出,Python Layer的输入}layer {type:"Python"name:"output"bottom:"output_2"# 要和Softmax输出保持一致top:"output"# Python Layer的输出python_param {    module:"my_layer"# 调用的Python代码的文件名# 也就是1.3中添加的Python路径中有一个python文件叫my_layer.py# Caffe通过这个文件名和Python系统路径,找到你写的python代码文件layer:"MyLayer"# my_layer.py中定义的一个类,在下文中会讲到# MyLayer是类的名字param_str:'{ "x1": 1, "x2": 2 }'# 额外传递给my_layer.py的值# 如果没有要传递的值,可以不定义. 相当于给python的全局变量# 当Python Layer比较复杂的时候会需要用到.}}layer {  name:"loss"type:"MultinomialLogisticLoss"bottom:"output"# 要和Python Layer输出保持一致bottom:"label"# loss层的另一个输入# 因为要计算output和label间的距离top:"loss"# loss层的输出,即loss值}

加完以后的参数传递如图

2.2 my_layer.py

重头戏其实就是这一部分,以上说的都是相对固定的修改,不存在什么算法层面的改动,但是python里面不一样,可以实现很多调整和试验性的试验. 最最基本的就是加入一个上面定义的那个"可有可无"的Python Layer.

在Python的文件中,需要定义类,类的里面包括几个部分:

setup( ): 用于检查输入的参数是否存在异常,初始化的功能.

reshape( ): 也是初始化,设定一下参数的size

forward( ): 前向传播

backward( ): 反向传播

结构如下:

importcaffeclassMyLayer(caffe.Layer):defsetup(self, bottom, top):passdefreshape(self, bottom, top):passdefforward(self, bottom, top):passdefbackward(self, top, propagate_down, bottom):pass

根据需要慢慢地填充这几个函数,关于这方面的知识,我很推荐阅读"Guide to Convolutional Neural Networks: A Practical Application to Traffic-Sign Detection and Classification." 中的这个章节 [6].

setup()的定义:

def setup(self, bottom, top):# 功能1: 检查输入输出是否有异常iflen(bottom) !=1:        raiseException("异常:输入应该就一个值!")iflen(top) !=1:        raiseException("异常:输出应该就一个值!")# 功能2: 初始化一些变量,后续可以使用self.delta = np.zeros_like(bottom[0].data, dtype=np.float32)# 功能3: 接受train_val.prototxt中设置的变量值params =eval(self.param_str)self.x1 = int(params["x1"])self.x2 = int(params["x2"])

reshape()的定义:

defreshape(self, bottom, top):# 功能1: 修改变量的sizetop[0].reshape(*bottom[0].data.shape)# 看了很多材料,我感觉这个函数就是比较鸡肋的那种.# 这个函数就像格式一样,反正写上就好了...# 不知道还有其他什么功能了,欢迎补充!

forward()的定义:

这个函数可以变的花样就多了,如果是要定义不同的loss function,可以参考[1],稍微高级一点的可以参考[2],这里就实现一个y=x的简单功能.

defforward(self, bottom, top):# 目标:y = x# bottom相当于输入x# top相当于输出ytop[0].data[...] = bottom[0].data[:]# 哈哈哈哈,是不是感觉被骗了,一行代码就完事儿了:-)

了解bottom中数据的存储结构是比较重要的,因为参考文档不多,我只能通过print out everything来了解bottom里面究竟存着些什么. 回想在2.1的prototxt中,我们有定义输入Python Layer的都有什么(bottom). bottom可以有多个定义,如果像例子中的只有一个bottom: "output_2",那么bottom[0].data中就存着output_2的值,当二分类问题时也就是两列,一列是Softmax后属于label 0的概率,一列是Softmax后属于label 1的概率. 当bottom定义了多个输入的时候,即

layer{type:"Python"name:"output"bottom:"output_2"bottom:"label"top:"output"python_param {    ...  }}

那么按照顺序,bottom[0].data中依旧存着output_2,bottom[1].data中存着label值,以此类推,可以定义到bottom[n],想用的时候调用bottom[n].data就可以了. top[n].data和bottom的情况类似,也是取决于prototxt中的定义.

想象一下,既然你可以掌控了output_2和label和其他你需要的任何值(通过bottom或者param_str定义),是不是可以在这个forward()函数里面大展身手了?

是的.

但是同时,也要负责计算这个前馈所带来的梯度,可以自己定义变量存起来,网上修改loss函数的例子就是拿self.diff来存梯度的,不过在这个例子中,因为梯度是1,所以我没有管它.

backward()的定义:

defbackward(self, top, propagate_down, bottom):# 由于是反向传播,top和bottom的意义就和前向传播反一反# top:从loss层传回来的值# bottom:反向层的输出foriinrange(len(propagate_down)):ifnotpropagate_down[i]:continuebottom[i].diff[...] = top[i].diff[:]# 其实还要乘以这个层的导数,但是由于y=x的导数是1.# 所以无所谓了,直接把loss值一动不动的传递下来就好.

对于top和bottom在forward()和backward()函数中不同的意义,不要懵...

top[i].diff是从loss层返回来的梯度,以二分类为例,它的结构是两列,一列是label 0的梯度,一列是label 1的梯度. 因此在backward()正常情况是需要把top[i].diff乘以self.diff的,也就是在forward()中算好的Python Layer自身的梯度. 然后赋值给bottom[i].diff,反向传播到上一层.

关于propagate_down这个东西,我认为是定义是否在这个层做反向传播(权值更新)的,也就是在迁移学习中,如果要固定不更新某一层的参数,就是用propagate_down来控制的. 不用管它,反正用默认的代码就好了.

总的来说,要实现y=x这么一个层,需要写的python代码就是:

import caffeclassMyLayer(caffe.Layer):defsetup(self, bottom, top):        passdefreshape(self, bottom, top):        top[0].reshape(*bottom[0].data.shape)defforward(self, bottom, top):        top[0].data[...] = bottom[0].data[:]defbackward(self,self, top, propagate_down, bottom):foriinrange(len(propagate_down)):ifnotpropagate_down[i]:                continue            bottom[i].diff[...] = top[i].diff[:]

3. 结语

我认为要在Caffe中写好一个Python Layer,最重要的是抓住两点

1)处理好prototxt到python文件的参数传递

2)不能忘了在forward()中计算反向传播梯度

接下来就是一些代码理解和学术创新的事情了,懂得如何写Python Layer,在运动Caffe的过程中就多开了一扇窗,从此不再只是调整solver.prototxt,还有在train_val.prototxt中组合卷积层/池化层/全连接/Residual Unit/Dense Unit这些低级的修改.

更多的细节可以参考最前面的几个参考链接还有自己的理解实践. 在实践过程中,超级建议print所有你不了解的数据结构,例如forward()中的bottom,top; backward()中的bottom,top,即便Caffe用GPU加速,它也会给你打印出来你想要看的数据,一步一步的摸索数据的传递和存储,这也是我花最多时间去弄明白的地方.

该文章主要引用于 :引用文章

上一篇下一篇

猜你喜欢

热点阅读