Caffe源码训练的基本过程
来源:互联网 发布:矩阵的转置怎么求 编辑:程序博客网 时间:2024/05/22 10:58
Caffe简介
一般在介绍Caffe代码结构的时候,大家都会说Caffe主要由Blob Layer Net 和 Solver这几个部分组成。
Blob:::
主要用来表示网络中的数据,包括训练数据,网络各层自身的参数(包括权值、偏置以及它们的梯度),网络之间传递的数据都是通过 Blob 来实现的,同时 Blob 数据也支持在 CPU 与 GPU 上存储,能够在两者之间做同步。
Layer:::
是对神经网络中各种层的一个抽象,包括我们熟知的卷积层和下采样层,还有全连接层和各种激活函数层等等。同时每种 Layer 都实现了前向传播和反向传播,并通过 Blob 来传递数据。
Net::: 是对整个网络的表示,由各种 Layer 前后连接组合而成,也是我们所构建的网络模型。
Solver :::定义了针对 Net 网络模型的求解方法,记录网络的训练过程,保存网络模型参数,中断并恢复网络的训练过程。自定义 Solver 能够实现不同的网络求解方式。
不过在刚开始准备阅读Caffe代码的时候,就算知道了代码是由上面四部分组成还是感觉会无从下手,下面我们准备通过一个Caffe训练LeNet的实例并结合代码来解释Caffe是如何初始化网络,然后正向传播、反向传播开始训练,最终得到训练好的模型这一过程。
训练LeNet
在Caffe提供的例子里,训练LeNet网络的命令为:
- 1
- 2
- 1
- 2
其中第一个参数build/tools/caffe是Caffe框架的主要框架,由tools/caffe.cpp文件编译而来,第二个参数train表示是要训练网络,第三个参数是 solver的protobuf描述文件。在Caffe中,网络模型的描述及其求解都是通过 protobuf 定义的,并不需要通过敲代码来实现。同时,模型的参数也是通过 protobuf 实现加载和存储,包括 CPU 与 GPU 之间的无缝切换,都是通过配置来实现的,不需要通过硬编码的方式实现,有关
protobuf的具体内容可参考这篇博文:http://alanse7en.github.io/caffedai-ma-jie-xi-2/。
网络初始化
下面我们从caffe.cpp的main函数入口开始观察Caffe是怎么一步一步训练网络的。在caffe.cpp中main函数之外通过RegisterBrewFunction这个宏在每一个实现主要功能的函数之后将这个函数的名字和其对应的函数指针添加到了g_brew_map中,具体分别为train(),test(),device_query(),time()这四个函数。
在运行的时候,根据传入的参数在main函数中,通过GetBrewFunction得到了我们需要调用的那个函数的函数指针,并完成了调用。
- 1
- 2
- 1
- 2
在我们上面所说的训练LeNet的例子中,传入的第二个参数为train,所以调用的函数为caffe.cpp中的int train()函数,接下来主要看这个函数的内容。在train函数中有下面两行代码,下面的代码定义了一个指向Solver的shared_ptr。其中主要是通过调用SolverRegistry这个类的静态成员函数CreateSolver得到一个指向Solver的指针来构造shared_ptr类型的solver。而且由于C++多态的特性,尽管solver是一个指向基类Solver类型的指针,通过solver这个智能指针来调用各个成员函数会调用到各个子类(SGDSolver等)的函数。
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
因为在caffe.proto文件中默认的优化type为SGD,所以上面的代码会实例化一个SGDSolver的对象,’SGDSolver’类继承于Solver类,在新建SGDSolver对象时会调用其构造函数如下所示:
- 1
- 2
- 3
- 1
- 2
- 3
从上面代码可以看出,会先调用父类Solver的构造函数,如下所示。Solver类的构造函数通过Init(param)函数来初始化网络。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
而在Init(paran)函数中,又主要是通过InitTrainNet()和InitTestNets()函数分别来搭建训练网络结构和测试网络结构。
训练网络只能有一个,在InitTrainNet()函数中首先会设置一些基本参数,包括设置网络的状态为TRAIN,确定训练网络只有一个等,然会会通过下面这条语句新建了一个Net对象。InitTestNets()函数和InitTrainNet()函数基本类似,不再赘述。
- 1
- 2
- 1
- 2
上面语句新建了Net对象之后会调用Net类的构造函数,如下所示。可以看出构造函数是通过Init(param)函数来初始化网络结构的。
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
下面是net.cpp文件里Init()函数的主要内容(忽略具体细节),其中LayerRegistry::CreateLayer(layer_param)主要是通过调用LayerRegistry这个类的静态成员函数CreateLayer得到一个指向Layer类的shared_ptr类型指针。并把每一层的指针存放在vector
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
经过上面的过程,Net类的初始化工作基本就完成了,接着我们具体来看看上面所说的layers_[layer_id]->SetUp对每一具体的层结构进行设置,我们来看看Layer类的Setup()函数,对每一层的设置主要由下面三个函数组成:
LayerSetUp(bottom, top):由Layer类派生出的特定类都需要重写这个函数,主要功能是设置权值参数(包括偏置)的空间以及对权值参数经行随机初始化。
Reshape(bottom, top):根据输出blob和权值参数计算输出blob的维数,并申请空间。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
经过上述过程基本上就完成了初始化的工作,总体的流程大概就是新建一个Solver对象,然后调用Solver类的构造函数,然后在Solver的构造函数中又会新建Net类实例,在Net类的构造函数中又会新建各个Layer的实例,一直具体到设置每个Blob,大概就介绍完了网络初始化的工作,当然里面还有很多具体的细节,但大概的流程就是这样。
训练过程
上面介绍了网络初始化的大概流程,如上面所说的网络的初始化就是从下面一行代码新建一个solver指针开始一步一步的调用Solver,Net,Layer,Blob类的构造函数,完成整个网络的初始化。
- 1
- 2
- 3
- 1
- 2
- 3
完成初始化之后,就可以开始对网络经行训练了,开始训练的代码如下所示,指向Solver类的指针solver开始调用Solver类的成员函数Solve(),名称比较绕啊。
- 1
- 2
- 1
- 2
接下来我们来看看Solver类的成员函数Solve(),Solve函数其实主要就是调用了Solver的另一个成员函数Step()来完成实际的迭代训练过程。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
顺着来看看这个Step()函数的主要代码,首先是一个大循环设置了总的迭代次数,在每次迭代中训练iter_size x batch_size个样本,这个设置是为了在GPU的显存不够的时候使用,比如我本来想把batch_size设置为128,iter_size是默认为1的,但是会out_of_memory,借助这个方法,可以设置batch_size=32,iter_size=4,那实际上每次迭代还是处理了128个数据。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
上面Step()函数主要分为三部分:
loss += net_->ForwardBackward();
这行代码通过Net类的net_指针调用其成员函数ForwardBackward(),其代码如下所示,分别调用了成员函数Forward(&loss)和成员函数Backward()来进行前向传播和反向传播。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
前面的Forward(&loss)函数最终会执行到下面一段代码,Net类的Forward()函数会对网络中的每一层执行Layer类的成员函数Forward(),而具体的每一层Layer的派生类会重写Forward()函数来实现不同层的前向计算功能。上面的Backward()反向求导函数也和Forward()类似,调用不同层的Backward()函数来计算每层的梯度。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
UpdateSmoothedLoss();
这个函数主要做Loss的平滑。由于Caffe的训练方式是SGD,我们无法把所有的数据同时放入模型进行训练,那么部分数据产生的Loss就可能会和全样本的平均Loss不同,在必要时候将Loss和历史过程中更新的Loss求平均就可以减少Loss的震荡问题
ApplyUpdate();
这个函数是Solver类的纯虚函数,需要派生类来实现,比如SGDSolver类实现的ApplyUpdate();函数如下,主要内容包括:设置参数的学习率;对梯度进行Normalize;对反向求导得到的梯度添加正则项的梯度;最后根据SGD算法计算最终的梯度;最后的最后把计算得到的最终梯度对权值进行更新。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
等进行了所有的循环,网络的训练也算是完成了。上面大概说了下使用Caffe进行网络训练时网络初始化以及前向传播、反向传播、梯度更新的过程,其中省略了大量的细节。上面还有很多东西都没提到,比如说Caffe中Layer派生类的注册及各个具体层前向反向的实现、Solver派生类的注册、网络结构的读取、模型的保存等等大量内容。
转自:::http://buptldy.github.io/2016/10/09/2016-10-09-Caffe_Code/
- Caffe源码训练的基本过程
- Caffe训练源码基本流程
- Caffe训练源码基本流程
- Caffe训练源码基本流程
- 从Caffe源码分析训练过程
- caffe 训练过程源码层理解
- 【caffe】caffe保存训练生成的log & 绘制训练过程的loss和accuracy曲线
- Caffe 训练 cifar10 详细过程
- caffe训练过程中的可视化
- caffe绘制训练过程的loss和accuracy曲线
- caffe绘制训练过程的loss和accuracy曲线
- caffe绘制训练过程的loss和accuracy曲线
- caffe绘制训练过程的loss和accuracy曲线
- caffe绘制训练过程的loss和accuracy曲线
- caffe绘制训练过程的loss和accuracy曲线
- 生成caffe训练过程中需要的图片描述文本
- caffe绘制训练过程的loss和accuracy曲线
- caffe绘制训练过程的loss和accuracy曲线
- linux信号屏蔽
- Unity|ShaderLab笔记整理-五(逐像素漫反射+环境光 +高光反射(Phone+ BlinnPhong))
- ML之LogisticRegression
- SQL Server Binary转换
- 前端基本功jsday01
- Caffe源码训练的基本过程
- C++ template
- Jersey+Spring+Hibernate集成学习记录
- wrap()
- BZOJ 2005 能量采集 (欧拉函数)
- db2 修改表字段语句
- Ubuntu下Openfire的安装
- 素数判定c++题解
- dubbo 各个通信协议之对比