在Caffe中加入PythonLayer

下面是一些参考链接

[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加速,它也会给你打印出来你想要看的数据,一步一步的摸索数据的传递和存储,这也是我花最多时间去弄明白的地方.

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354