Caffe深度学习 | Deep Learning

在Caffe中加Python Layer的方法

2017-11-27  本文已影响1318人  MrGiovanni

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 系统配置

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的系统路径中,步骤是:

  1. 打开Terminator
  2. 输入vi ~/.bashrc
  3. 输入i,进入编辑模式
  4. 在打开的文件的末尾添加
    export PYTHONPATH=/path/to/my_python_layer:$PYTHONPATH
  5. 键入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的文件中,需要定义类,类的里面包括几个部分:

  1. setup( ): 用于检查输入的参数是否存在异常,初始化的功能.
  2. reshape( ): 也是初始化,设定一下参数的size
  3. forward( ): 前向传播
  4. 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加速,它也会给你打印出来你想要看的数据,一步一步的摸索数据的传递和存储,这也是我花最多时间去弄明白的地方.


祝好!

上一篇下一篇

猜你喜欢

热点阅读