caffe笔记:代码结构梳理

2017-02-10 Lu Huang 更多博文 » 博客 » GitHub »

原文链接 https://hlthu.github.io/caffe/2017/02/10/caffe-code-combing.html
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


作为一个开源工具,caffe的代码十分庞大,但是组织的还是比较好的,本文主要介绍其代码框架。由于caffe大部分使用C++写的,因此,读者需要掌握C++的基本知识,包括:封装、继承、多态等。

1. caffe目录结构

在caffe根目录下使用tree -d查看整个代码的文件夹组织情况,如下。

$ tree -d
.
├── build -> .build_release
├── cmake
│   ├── External
│   ├── Modules
│   └── Templates
├── data
│   ├── cifar10
│   ├── ilsvrc12
│   └── mnist
├── distribute
│   ├── bin
│   └── lib
├── docker
│   ├── standalone
│   │   ├── cpu
│   │   └── gpu
│   └── templates
├── docs
│   ├── images
│   ├── _layouts
│   ├── stylesheets
│   └── tutorial
│       └── fig
├── examples
│   ├── cifar10
│   │   ├── cifar10_test_lmdb
│   │   └── cifar10_train_lmdb
│   ├── cpp_classification
│   ├── feature_extraction
│   ├── finetune_flickr_style
│   ├── finetune_pascal_detection
│   ├── hdf5_classification
│   ├── imagenet
│   ├── images
│   ├── mnist
│   │   ├── mnist_test_lmdb
│   │   └── mnist_train_lmdb
│   ├── net_surgery
│   ├── pycaffe
│   │   └── layers
│   ├── siamese
│   └── web_demo
│       └── templates
├── include
│   └── caffe
│       ├── layers
│       ├── test
│       └── util
├── matlab
│   ├── +caffe
│   │   ├── imagenet
│   │   ├── private
│   │   └── +test
│   ├── demo
│   └── hdf5creation
├── models
│   ├── bvlc_alexnet
│   ├── bvlc_googlenet
│   ├── bvlc_reference_caffenet
│   ├── bvlc_reference_rcnn_ilsvrc13
│   └── finetune_flickr_style
├── my-mnist
├── python
│   └── caffe
│       ├── imagenet
│       ├── proto
│       └── test
├── scripts
│   └── travis
├── src
│   ├── caffe
│   │   ├── layers
│   │   ├── proto
│   │   ├── solvers
│   │   ├── test
│   │   │   └── test_data
│   │   └── util
│   └── gtest
└── tools
    └── extra

主要值得关注的是src/include/tools/,分别是源码、引用代码和工具。

2. 有效阅读caffe源码

建议从以下四个步骤展开:

  1. src/caffe/proto/caffe.proto开始。主要了解基本的数据结构内存对象和磁盘文件的一一对应关系。

  2. 看头文件。通过看头文件类声明来理解整个框架,掌握基本类的继承关系和基本使用方法。

  3. 有针对性的看.cpp.cu文件。有针对性地理解一些代码的具体实现,尝试在此基础上通过继承等实现自己的网络设计和算法实现。

  4. 编写各类工具。在tools/目录下已经有很多编译好的工具,可以根据需要修改,也可以尝试使用Python或MATLAB接口的caffe。

3. caffe支持的深度学习特性

卷积神经网络(Convolutional Neural Network, CNN)是深度学习技术中极具代表的网络结构之一,在图像处理领域取得了很大的成功,在国际标准的ImageNet数据集上,许多成功的模型都是基于CNN的。CNN相较于传统的图像处理算法的优点之一在于,避免了对图像复杂的前期预处理过程(提取人工特征等),可以直接输入原始图像。

下面重点介绍下CNN中的局部连接(Sparse Connectivity)和权值共享(Shared Weights)方法,理解它们很重要。

下图是一个很经典的图示,左边是全连接,右边是局部连接。

local-connect

对于一个$$1000×1000$$的输入图像而言,如果下一个隐藏层的神经元数目为$$10^6$$个,采用全连接则有$$1000×1000×10^6=10^{12}$$个权值参数,如此数目巨大的参数几乎难以训练;而采用局部连接,隐藏层的每个神经元仅与图像中$$10×10$$的局部图像相连接,那么此时的权值参数数量为$$10×10×10^6=10^8$$,将直接减少4个数量级。

尽管减少了几个数量级,但参数数量依然较多。能不能再进一步减少呢?能!方法就是权值共享。具体做法是,在局部连接中隐藏层的每一个神经元连接的是一个$$10×10$$的局部图像,因此有$$10×10$$个权值参数,将这$$10×10$$个权值参数共享给剩下的神经元,也就是说隐藏层中$$10^6$$个神经元的权值参数相同,那么此时不管隐藏层神经元的数目是多少,需要训练的参数就是这$$10×10$$个权值参数(也就是卷积核,也称滤波器的大小),如下图。

weight-share

普通的卷积神经网络(CNN)一般的输入时图像或语音,前面几层一般是卷积层和pooling层,后面还会有全连接层和非线性处理单元等。如下图所示为LeNet-5网络,也就是mnist中用到的。

CNN

这大概就是CNN的一个神奇之处,尽管只有这么少的参数,依旧有出色的性能。但是,这样仅提取了图像的一种特征,如果要多提取出一些特征,可以增加多个卷积核,不同的卷积核能够得到图像的不同映射下的特征,称之为Feature Map。如果有100个卷积核,最终的权值参数也仅为$$100×100=10^4$$个而已。另外,偏置参数也是共享的,同一种滤波器共享一个。

3.1 卷积层

卷积层是卷积核在上一级输入层上通过逐一滑动窗口计算而得,卷积核中的每一个参数都相当于传统神经网络中的权值参数,与对应的局部像素相连接,将卷积核的各个参数与对应的局部像素值相乘之和,(通常还要再加上一个偏置参数),得到卷积层上的结果。如下图所示。

conv

下面的动图能够更好地解释卷积过程:

conv2

以LeNet-5网络中的第一个卷积层为例,其卷积参数描述为:

layer {            #定义了卷积层1
  name: "conv1"
  type: "Convolution"
  bottom: "data"
  top: "conv1"
  param {
    lr_mult: 1     #权值学习速率倍乘因子,1表示与全局参数一致
  }
  param {
    lr_mult: 2     #bias学习速率倍乘因子,是全局参数的2倍
  }
  convolution_param {   #卷积计算参数
    num_output: 20      #输出20个feature map
    kernel_size: 5      #卷积kernel为5x5
    stride: 1           #卷积输出跳跃间隔,1表示连续输出
    weight_filler {     #权值使用xavier填充器
      type: "xavier"
    }
    bias_filler {       #bias使用常数填充器
      type: "constant"
    }
  }
}

3.2 pooling层

通过卷积层获得了图像的特征之后,理论上我们可以直接使用这些特征训练分类器(如softmax),但是这样做将面临巨大的计算量的挑战,而且容易产生过拟合的现象。为了进一步降低网络训练参数及模型的过拟合程度,我们对卷积层进行池化/采样(Pooling)处理。池化/采样的方式通常有以下两种:

  • Max-Pooling: 选择Pooling窗口中的最大值作为采样值;
  • Mean-Pooling: 将Pooling窗口中的所有值相加取平均,以平均值作为采样值;

pooling

以LeNet-5网络中的第一个pooling层为例,其卷积参数描述为:

layer {                 #定义了一个pooling层
  name: "pool1"
  type: "Pooling"
  bottom: "conv1"
  top: "pool1"
  pooling_param {       #pooling层参数
    pool: MAX           #最大值pooling
    kernel_size: 2      #pooling窗口2x2
    stride: 2           #下采样输出跳跃间隔2x2
  }
}

3.3 全连接层

在CNN出现之前,最早的神经网络计算类型都是全连接形式的,下图为一个DNN,除了输入输出都是全连接层。

fully-connect

从上图中可以看出,每隔节点和相邻层的所有节点都有连接关系。全连接层的主要计算类型是矩阵-向量的乘积。假设输入节点组成的向量为$$x$$,维度为$$D$$,输出节点组成的向量为$$y$$,维度为$$V$$,则该层的计算可以表示成

$$ y=Wx+b $$

其中$$W$$为$$V\times D$$的权值矩阵;$$b$$是偏置向量,也可以没有。

LeNet-5中也定义了一个全连接层,描述如下:

layer {                 #定义了一个全连接层InnerProduct
  name: "ip1"
  type: "InnerProduct"
  bottom: "pool2"
  top: "ip1"
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
  inner_product_param {
    num_output: 500    #该层输出元数个数为500
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}

与全连接层相比,卷积层在输出特征图维度时实现了权值共享,同时还有局部连接特性,都大大降低了参数的数量,这使得CNN网络的卷积层参数很少,但是计算量却比较大。

3.4 激活函数

深度神经网络之所以具有丰富的表达能力,除了”深“以外,还有一个重要因素就是非线性处理单元,称之为激活函数(Activation Function)。

下图所示为一个神经元模型,其中$$a_1$$~$$a_n$$为神经元的输入,$$b$$为偏置,$$f(·)$$为激活函数,$$t$$为输出。

nerual

下面介绍几个常用的激活函数。

Sigmoid函数

此激活函数早期广泛应用于神经网络中,但是由于梯度饱和问题,超过三层时误差就无法传到最初的层了,因此在深度学习中效果不如ReLU。其表达式为:

$$ f(x)=\frac{1}{1+e^{-ax}} $$

其导函数满足

$$ f'(x)=f(x)(1-f(x)) $$

其函数图像为

sigmoid

tanh函数

其表达式为

$$ f(x)=\frac{1-e^{-2x}}{1+e^{-2x}} $$

其导数为

$$ f'(x)=1-f^2(x) $$

其函数图像为

tanh

tanh跟sigmoid还是很像的,实际上,tanh是sigmoid的变形:

$$ \tanh(x)=2\text{sigmoid}(2x)−1 $$

与sigmoid不同的是,tanh是0均值的。因此,实际应用中,tanh会比sigmoid更好(毕竟去粗取精了嘛)。

ReLU函数

近年来,ReLU 变的越来越受欢迎。它的数学表达式如下:

$$ f(x)=\max(0,x) $$

其导函数是

$$ f'(x)=(x>0) $$

其函数图像如下:

relu2

很显然,从图左可以看出,输入信号小于0时,输出都是0,大于0的情况下,输出等于输入。下图是二维的情况下,使用ReLU之后的效果如下:

relu

有学者发现使用ReLU得到的SGD的收敛速度会比sigmoid/tanh快很多。有人说这是因为它是linear,而且sigmoid/tanh,ReLU只需要一个阈值就可以得到激活值,而不用去算一大堆复杂的运算。ReLU能够在x>0时保持梯度不衰减,从而缓解梯度消失问题。但随着训练的推进,部分输入会落入硬饱和区,导致对应权重无法更新。这种现象被称为“神经元死亡”。ReLU还经常被“诟病”的一个问题是输出具有偏移现象,即输出均值恒大于零。偏移现象和 神经元死亡会共同影响网络的收敛性。

LeNet-5中也定义了一个ReLU函数,很简单,并不需要指明一些参数。其描述如下:

layer {                #定义了一个非线性层
  name: "relu1"
  type: "ReLU"         #ReLU方法
  bottom: "ip1"
  top: "ip1"
}

以上三个激活函数的声明可以在include/caffe/layers/中的hpp文件中找到,定义则可以在/src/caffe/layers/中找到,包括.cpp.cu文件。

参考

  1. 深度学习——21天实战caffe:赵永科著,中国工信出版集团、电子工业出版社,2016年7月。
  2. 卷积神经网络(CNN)学习笔记1:基础入门
  3. 第六章 深度学习(上)
  4. 神经网络-激活函数-面面观(Activation Function)