在Caffe中加Python Layer的方法
Author: Zongwei Zhou | 周纵苇
Weibo: @MrGiovanni
Email: zongweiz@asu.edu
Acknowledgement: Md Rahman Siddiquee (mrahmans@asu.edu)
Caffe的参考文档非常少,自己改代码需要查阅网上好多好多对的错的讨论. 这篇文章主要讲怎么自己编写python layer,从而逼格很高地在caffe中实现自己的想法. 所谓的python layer,其实就是一个自己编写的层,用python来实现. 因为近来深度学习方向要发顶会和顶刊,光是用用Caffe,Tensorflow在数据集里头跑个网络已经基本不可能啦,需要具备修改底层代码的能力.
网上的参考资料大多是教你怎么写一个python layer来修改loss function(部分链接需要翻墙~):
[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路径
写自己的python layer势必需要.py文件,为了让caffe运行的时候可以找到你的py文件,接下来需要把py文件的路径加到python的系统路径中,步骤是:
- 打开Terminator
- 输入
vi ~/.bashrc
- 输入
i
,进入编辑模式 - 在打开的文件的末尾添加
export PYTHONPATH=/path/to/my_python_layer:$PYTHONPATH
- 键入
esc
,:wq
,回车,即可保存退出
如果这部分没有看明白,需要上网补一下如何在Linux环境中用vim语句修改文档的知识. 实质上就是修改一个在~/路径下的叫.bashrc的文档.
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( ): 反向传播
结构如下:
import caffe
class MyLayer(caffe.Layer):
def setup(self, bottom, top):
pass
def reshape(self, bottom, top):
pass
def forward(self, bottom, top):
pass
def backward(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: 检查输入输出是否有异常
if len(bottom) != 1:
raise Exception("异常:输入应该就一个值!")
if len(top) != 1:
raise Exception("异常:输出应该就一个值!")
# 功能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()的定义:
def reshape(self, bottom, top):
# 功能1: 修改变量的size
top[0].reshape(*bottom[0].data.shape)
# 看了很多材料,我感觉这个函数就是比较鸡肋的那种.
# 这个函数就像格式一样,反正写上就好了...
# 不知道还有其他什么功能了,欢迎补充!
forward()的定义:
这个函数可以变的花样就多了,如果是要定义不同的loss function,可以参考[1],稍微高级一点的可以参考[2],这里就实现一个y=x的简单功能.
def forward(self, bottom, top):
# 目标:y = x
# bottom相当于输入x
# top相当于输出y
top[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()的定义:
def backward(self, top, propagate_down, bottom):
# 由于是反向传播,top和bottom的意义就和前向传播反一反
# top:从loss层传回来的值
# bottom:反向层的输出
for i in range(len(propagate_down)):
if not propagate_down[i]:
continue
bottom[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 caffe
class MyLayer(caffe.Layer):
def setup(self, bottom, top):
pass
def reshape(self, bottom, top):
top[0].reshape(*bottom[0].data.shape)
def forward(self, bottom, top):
top[0].data[...] = bottom[0].data[:]
def backward(self, self, top, propagate_down, bottom):
for i in range(len(propagate_down)):
if not propagate_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加速,它也会给你打印出来你想要看的数据,一步一步的摸索数据的传递和存储,这也是我花最多时间去弄明白的地方.
祝好!