共计 5012 个字符,预计需要花费 13 分钟才能阅读完成。
Caffe 是一个基于 c ++/cuda 语言的深度学习框架,开发者能够利用它自由的组成自己想要的网络。目前支持卷积神经网络和全连接神经网络(人工神经网络)。Linux 上,c++ 可以通过命令行来操作接口,matlab、Python 有专门的接口,运算支持 gpu 也支持 cpu,目前版本能够支持多 gpu,但是分布式多机版本仍在开发中。大量的研究者都在采用 caffe 的架构,并且也得到了很多有效的成果。2013 年 9 月 -12 月,贾扬清在伯克利大学准备毕业论文的时候开发了 caffe 最初的版本,后期有其他的牛人加入之后,近两年的不断优化,到现在成了最受欢迎的深度学习框架。近期,caffe2 也开源了,但是仍旧在开发。本文主要主要基于源代码的层面来对 caffe 进行解读,并且给出了几个自己在测试的过程中感兴趣的东西。
Ubuntu 16.04 下 Matlab2014a+Anaconda2+OpenCV3.1+Caffe 安装 http://www.linuxidc.com/Linux/2016-07/132860.htm
Ubuntu 16.04 系统下 CUDA7.5 配置 Caffe 教程 http://www.linuxidc.com/Linux/2016-07/132859.htm
Caffe 在 Ubuntu 14.04 64bit 下的安装 http://www.linuxidc.com/Linux/2015-07/120449.htm
Caffe + Ubuntu 14.04 64bit + CUDA 6.5 配置说明 http://www.linuxidc.com/Linux/2015-04/116444.htm
1. 如何调试
为了能够调试,首先要在 makefile 的配置文件中将 DEBUG 选项设置为 1,这步谨慎选择,debug 版本会在打印输出的时候输出大量的每个阶段耗时,也可直接从整个项目的 caffe.cpp 入手来查看源文件。编译好可调试的版本之后,执行下面的指令可以启动调试。
gdb --args ./build/tools/caffe train –solver=examples/cifar10/cifar10_full_solver.prototxt
调试过程中需要注意的一个问题是,源码中使用了函数指针,执行下一步很容易就跳过了,所以要在合适的时机使用 s 来进入函数。
2. 第三方库
gflags 是 Google 出的一个能够简化命令行参数处理的工具,在 c ++ 代码中定义实际意义,在命令行中将参数传进去。例如下面的例子中,c++ 的代码中声明这样的内容,DEFINE_string 是一个 string 类型,括号内的 solver 就是一个 flag,这个 flag 从命令行中读取的参数就会解析成 string,存在 FLAGS_solver 中,使用时当成正常的 string 使用即可。在命令行调用时(参见调试部分举出的例子),用 -solver=xxxxx,将实际的值给传递进去。这里的 string 可以替换成 int32/int64/bool 等。
DEFINE_string(solver,“”,“The solver definition protocol buffer text file.”);
需要注意的时,这个定义过程只能在一个文件中定义一次,其他文件要是想用的话可以有两种选择,一种是直接在需要的文件中 declare,一种方式在一个头文件中 declare,其他文件要用就直接 include。声明方式如下:
DECLARE_bool(solver);
假如需要设置 bool 变量为 false,一个简便的方法是在变量前面加上 no,即变成 -nosolver。此外,–会导致解析停止,例如下面的式子中,f1 是 flag,它的值为 1,但是 f2 并不是 2。
foo -f1 1 – -f2 2
Google Protocol Buffer(简称 Protobuf)是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个.proto 文件。它是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化、序列化结构数据,自己定义一次数据如何结构化,目前提供了 c ++、java、python 三种语言的 API。相对于 xml 优点在于简单、体积小、读取处理时间快、更少产生歧义、更容易产生易于编程的类。
就 caffe 而言,这个工具的用处体现在生成 caffe 所需的 参数类,这些类能从以.prototxt 结尾的文件中解析参数,然后对应生成 Net、Layer 的参数。自己定义序列化文件 a.proto,文件内容如图 1,以关键词 message 来定义一个类,本图中它是卷积层的参数类,这个类的成员类型有 bool 和 uint32 等,也可用自己定义的类型。等号后面的数字是一个唯一的编号 tag,来区分这些不同参数,在官方文档中这些称为 field。可以在一个.proto 中定义多个 message,注释风格与 c /c++ 一致。定义好的 proto 进行编译后生成.h 和.cc 对应 c ++ 的头文件和源文件。
图 2 是编译后自动生成的文件,可以看到生成了 ConvolutionalParameter 类。
这个工具也是谷歌出品,用来打印初始化、运行时的信息,记录意外中断等。使用先要初始化 google 的 logging 库。一般在 caffe 中常见的 LOG(INFO)…和 CHECK(XXX)…都是它执行的。相关的内容可以参考下面的图片,图 3 是标出颜色的是代码中用到了的打印,图 4 是对应打印到屏幕上的信息。
lmdb 是一个读取速度快、轻量级的数据库,支持多线程、多进程并发,数据由 key-value 对存储。caffe 还提供 leveldb 的接口,本文只讨论 python 实现的 lmdb。在这个数据库中存放的是序列化生成的字符串。caffe 提供脚本文件先生成 lmdb 格式的数据,这个脚本文件会生成一个文件夹,文件夹下包括两个文件,一个数据文件,一个 lock 文件。然后调用训练网络的 DataLayer 层来读取 lmdb 格式的数据。图 5 是定义 ldmb 数据库类型,图 6 是将数据序列化再存入数据库中。
3.caffe 基本结构
这是 caffe 的数据存储类 blob,它实现了关于一个变量的所有相关信息和相关操作。存储数据的方式可以看成是一个 N 维的 c 数组,存储空间连续。例如存储图片是 4 维 (num, channel, height, width), 变量(n,k,h,w) 在数组中存储位置为((n*K+k)*H+h)*W+w。相应的四维参数保存为(out_channel, in_channel, filter_size, filter_size)。blob 有以下三个特征:
- 两块数据,一个是原始 data,一个是求导值 diff
- 两种内存分配方式,一种是分配在 cpu 上,一种是分配在 gpu 上,通过前缀 cpu、gpu 来区分
- 两种访问方式,一种是不能改变数据,一种能改变数据
其中让人眼前一亮的是 data 和 diff 的设计,其实在卷积网络中,很多情况下一个变量不仅有它自身的值,另外还有 cost function 对它的导数,采用过多的变量来保存这两个信息还不如将它们放在一起直观,下图是源码 blob.hpp 中的定义。
caffe 根据不同的功能将它们包装成不同的 Layer,例如卷积、pooling、非线性变换、数据层等等。具体有多少种 layer 及其内容参考官方文档即可,本文主要讨论它的实现,它的实现分为三个部分,也可参考演示图 8:
- setup,初始化每一层,和它对应的连接关系
- forward,由 bottom 求 top
- backward,由 top 的梯度求 bottom 的梯度,有参数的求参数梯度
而前向传播、后向传播的函数也分别有两种实现方式,一种基于 gpu 一种基于 cpu。Forward 函数,参数分别是两个存放 blob 指针的 vector,分别是 bottom、top。通过指针数组的方式能够实现多个输入多个输出。值得一提的是,caffe 的卷积部分采用了将数据进行变换,变成矩阵之后再用矩阵乘法来实现卷积,cudnn 也是采用这样的方式,经过我的实验,确实这种方式比直接实现 cuda kernel 要快一些。caffe 大部分底层实现都是用 blas 或者 cublas 处理的。
Net 它将不同的层正确的连接起来,是层和它们之间连接的集合。通过 Net::Init()来初始化模型,构造 blobs 和 layers,调用 layers 的 setup 函数。Net 的 Forward 函数内部调用了 ForwardPrefilled,并且调用了 ForwardFromTo,它从给定的层数 id(start)到 end 来调用 Layer 对象的 Forward 函数。
Solver 是控制网络的关键所在,它的具体功能包括解析传递的 prototxt、执行 train、调用网络前向传播计算输出和 loss、后向传播计算梯度、根据不同优化方式更新参数(可能不止有 learning rate 这种参数,而是由 alpha、beta 构成的更新方式)等。在解析.prototxt 时,首先初始化 NetParameter 对象,用于放置全部的网络参数,然后在初始化训练网络的时候,通过 net 变量给出的 proto 文件地址,来解析并获取网络的层次结构参数。其中的函数 solve 会根据命令行传递进来的参数来解析并恢复之前保存好的网络文件和权重等,恢复上次执行的 iteration 次数、loss 等。当网络参数配置好,需要恢复的文件处理完成就调用 net.cpp 的 Forward 函数开始执行网络。Forward 会返回这一次迭代的 loss,然后打印出来。接下来会调用 ApplyUpdata 函数,它会根据不同的策略来改变当前权重的学习率大小,再更新权重。此外,solver 还提供保存快照的功能。
4. 运行实例
假如是自己的图片数据,可以按照如下的方法来进行分类。我全部采用的 c ++,改源代码比较方便。
- 将图片整理成 train 和 test 两个文件夹,并将图片的名称和 label 保存到一个 txt 中
- 将数据变成 lmdb 格式,采用的是 convert_imagenet 这个工具
- 生成均值处理后的图片,采用 compute_image_mean 这个工具
- 修改模型并执行 train
此外,我还测试过一维数据,并且修改了 convert_imagenet.cpp 源码,将数据读入 lmdb,大致代码如下:
datum.set_channels(num_channels);
datum.set_height(num_height);
datum.set_width(num_width);
datum.clear_data();
datum.set_encoded(false);
datum.set_data(lines[line_id].first);
datum.set_label(lines[line_id].second);
通过改这个代码,可以将一维数据读入网络,进行处理。此外,在执行这个一维数据的过程中,也出了一个错,报错信息”Too big key/data, key is empty, or wrong DUPFIXED size”,这个问题是因为 lmdb 是保存的 key-value 对,而 lmdb 对 key 的长度进行了限制,长度不能超过 512,但是我在传递的时候 key 的值给多了,因此得到了解决。
5. 小结
通过阅读源码可以看到,caffe 作为一个架构,层次、思路、需要解决的问题都非常清晰,它的高效体现在很多方面,不仅采用了读取快速的 lmdb,而且计算部分基本上都是用很高效的 blas 库完成的。而它的数据、层次、网络的构成和执行是分开控制的,这点就提供了比较大的灵活性,唯一的遗憾就是安装比较繁琐,总是会出现某个依赖包没装好的情况。总的来说,caffe 在科研领域使用的非常广泛,大量的研究都是基于 caffe 预训练好的 imagenet 的网络而得到了很好的进展,作者这种分享的精神值得肯定。
本文永久更新链接地址:http://www.linuxidc.com/Linux/2016-07/133220.htm