caffe笔记:代码结构梳理
原文链接 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源码
建议从以下四个步骤展开:
从
src/caffe/proto/caffe.proto
开始。主要了解基本的数据结构内存对象和磁盘文件的一一对应关系。看头文件。通过看头文件类声明来理解整个框架,掌握基本类的继承关系和基本使用方法。
有针对性的看
.cpp
和.cu
文件。有针对性地理解一些代码的具体实现,尝试在此基础上通过继承等实现自己的网络设计和算法实现。编写各类工具。在
tools/
目录下已经有很多编译好的工具,可以根据需要修改,也可以尝试使用Python或MATLAB接口的caffe。
3. caffe支持的深度学习特性
卷积神经网络(Convolutional Neural Network, CNN)是深度学习技术中极具代表的网络结构之一,在图像处理领域取得了很大的成功,在国际标准的ImageNet数据集上,许多成功的模型都是基于CNN的。CNN相较于传统的图像处理算法的优点之一在于,避免了对图像复杂的前期预处理过程(提取人工特征等),可以直接输入原始图像。
下面重点介绍下CNN中的局部连接(Sparse Connectivity)和权值共享(Shared Weights)方法,理解它们很重要。
下图是一个很经典的图示,左边是全连接,右边是局部连接。
对于一个$$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$$个权值参数(也就是卷积核,也称滤波器的大小),如下图。
普通的卷积神经网络(CNN)一般的输入时图像或语音,前面几层一般是卷积层和pooling层,后面还会有全连接层和非线性处理单元等。如下图所示为LeNet-5网络,也就是mnist中用到的。
这大概就是CNN的一个神奇之处,尽管只有这么少的参数,依旧有出色的性能。但是,这样仅提取了图像的一种特征,如果要多提取出一些特征,可以增加多个卷积核,不同的卷积核能够得到图像的不同映射下的特征,称之为Feature Map。如果有100个卷积核,最终的权值参数也仅为$$100×100=10^4$$个而已。另外,偏置参数也是共享的,同一种滤波器共享一个。
3.1 卷积层
卷积层是卷积核在上一级输入层上通过逐一滑动窗口计算而得,卷积核中的每一个参数都相当于传统神经网络中的权值参数,与对应的局部像素相连接,将卷积核的各个参数与对应的局部像素值相乘之和,(通常还要再加上一个偏置参数),得到卷积层上的结果。如下图所示。
下面的动图能够更好地解释卷积过程:
以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窗口中的所有值相加取平均,以平均值作为采样值;
以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,除了输入输出都是全连接层。
从上图中可以看出,每隔节点和相邻层的所有节点都有连接关系。全连接层的主要计算类型是矩阵-向量的乘积。假设输入节点组成的向量为$$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$$为输出。
下面介绍几个常用的激活函数。
Sigmoid函数
此激活函数早期广泛应用于神经网络中,但是由于梯度饱和问题,超过三层时误差就无法传到最初的层了,因此在深度学习中效果不如ReLU。其表达式为:
$$ f(x)=\frac{1}{1+e^{-ax}} $$
其导函数满足
$$ f'(x)=f(x)(1-f(x)) $$
其函数图像为
tanh函数
其表达式为
$$ f(x)=\frac{1-e^{-2x}}{1+e^{-2x}} $$
其导数为
$$ f'(x)=1-f^2(x) $$
其函数图像为
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) $$
其函数图像如下:
很显然,从图左可以看出,输入信号小于0时,输出都是0,大于0的情况下,输出等于输入。下图是二维的情况下,使用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
文件。
参考
- 深度学习——21天实战caffe:赵永科著,中国工信出版集团、电子工业出版社,2016年7月。
- 卷积神经网络(CNN)学习笔记1:基础入门
- 第六章 深度学习(上)
- 神经网络-激活函数-面面观(Activation Function)