caffe 源码 —— common.hpp/cpp
这里主要是对 caffe 框架源码进行梳理与学习(
主要是 CPU 模式下的,所以暂时还不涉及 CUDA,cudnn 编程),不同时期回看源码收获不同,水平有限,如有错误之处还请留言指正交流。
首先的切入点就是 common.hpp, 其包含在 blob.hpp
和 layer.cpp
头文件中,而 blob.hpp
和 layer.cpp
这两者基本被框架所有网络相关层包含,所以可见 common.hpp 于 caffe 的重要性。
概括来说包含了以下内容:
- 宏定义 : 主要用来实例化类模板
- 全局初始化函数 : 初始化 gflags 和 glog
- Caffe 类(单例模式类) : 随机数生成
下面我们就结合 common.cpp 来集体说说以下内容。
1. 宏定义
这里的宏定义主要是实例化制定的类模板。 caffe 主要使用了 float 和 double 两种精度的数据类型,所以在网络相关功能定义时都是使用了泛型模板编程,而在这里用 INSTANTIATE_CLASS(classname)
进行实例化。
#define INSTANTIATE_CLASS(classname) \
char gInstantiationGuard##classname; \
template class classname<float>; \
template class classname<double>
相似的还有:
#define INSTANTIATE_LAYER_GPU_FORWARD(classname) \
template void classname<float>::Forward_gpu( \
const std::vector<Blob<float>*>& bottom, \
const std::vector<Blob<float>*>& top); \
template void classname<double>::Forward_gpu( \
const std::vector<Blob<double>*>& bottom, \
const std::vector<Blob<double>*>& top);
上面是实例化了 GPU 模式下的前向操作类,同理还有反向操作类。这里还有一个值得一提是:
#define DISABLE_COPY_AND_ASSIGN(classname) \
private:\
classname(const classname&);\
classname& operator=(const classname)
这是将类的拷贝构造函数和赋值构造函数设置成 private,禁止指定类的拷贝和赋值操作来初始化另一个类
2. 全局初始化函数
函数声明: void GlobalInit(int* pargc, char*** pargv)
功能:初始化 gflags 和 glog
源码(部分):
void GlobalInit(int* pargc, char*** pargv) {
// Google flags.
::gflags::ParseCommandLineFlags(pargc, pargv, true);
// Google logging.
::google::InitGoogleLogging(*(pargv)[0]);
}
3. Caffe 类(单例模式)
Caffe 类是一个懒汉模式的单例模式,简而言之即 Caffe 类将自己的构造函数设置成 private,这样程序就只能通过唯一的成员函数来初始化实例,在创建实例前做一个判断,只有当前不存在实例时才创建,这就保证了程序中只会有一个caffe 类的实例,这就是单例模式,这种使用时候才进行创建就是所谓的懒汉模式。这里我写了一个简单版的示例:
class Caffe_demo { // 懒汉单例的 Caffe_demo 类
public:
~Caffe_demo(); // 析构函数
Caffe_demo* Get() { // 创建实例的唯一接口
if (nullptr == m_caffe) { // 如果
m_caffe = new Caffe_demo();
}
return m_caffe;
}
class RNG {
//......
};
// 其他内容....
private:
Caffe_demo(); // 构造函数
DISABLE_COPY_AND_ASSIGN(Caffe_demo); // 宏定义,见 1. 宏定义 部分
static Caffe_demo * m_caffe; // 静态变量
};
Caffe_demo * m_caffe = nullptr;
这里的其他内容是一些,查询设备号,设置设备等功能,这里就不展开叙述了。下面的重点是包含在 Caffe 类中的 RNG 类, 随机数生成器(Random Number Generator),这个类是对 Boost库 和 CUDA 中随机数生成器的一个包装,对外以 RNG 类统一显示,首先引用知乎上一段关于随机数生成的回答:
随机数在caffe中是非常重要的,最重要的应用是权值的初始化,如高斯、xavier等,初始化的好坏直接影响最终的训练结果,其他的应用如训练图像的随机crop和mirror、dropout层的神经元的选择。
然后在 RNG 作为一个统一的接口,并不是直接通过 RNG 的成员函数来实现,而是内部又复合了另一个私有 Generator 类,用于随机数的生成,即调用需要通过三级的关系。
void* Caffe::RNG::generator() {
return static_cast<void*>(generator_->rng());
}
这里的 rng()
类的定义中给了两个接口,是通过 Generator 类的构造函数实现的:
class Caffe::RNG::Generator {
public:
Generator() : rng_(new caffe::rng_t(cluster_seedgen())) {} // boost 随机生成器
explicit Generator(unsigned int seed) : rng_(new caffe::rng_t(seed)) {} // 采用给定的种子初始化
caffe::rng_t* rng() { return rng_.get(); } // 属性
private:
shared_ptr<caffe::rng_t> rng_;
};
一个是可以认为设定随机种子 explicit Generator(unsigned int seed)
,如果固定随机种子,每次就可以获取相同的随机数结果,还有就是默认的无随机数种子的 Generator()
,就采用默认的 boost 库的随机函数进行随机数生成。这里的 rng_t
其实就是 boost 库随机数的返回形式,见 rng.hpp
中:
typedef boost::mt19937 rng_t;
而这里都使用了智能指针进行管理,且 Caffe 实例中也是使用了 boost::thread_specific_ptr
这类特殊指针,其行为类似于智能指针,智能管理线程,当线程退出时,自动释放资源。
rng.hpp
这里基于上述 common.hpp 的分析,caffe 框架下 include/util 中还包含了名为 rng.hpp 的头文件,其中主要是基于 RNG 类的随机生成数,再将它们混合,来达到充分随机。使用的是 Fisher–Yates shuffle 算法,其是用来将一个有限集合打乱的算法,这个算法生成的随机排列是等概率的,同时需要非常高效。
inline rng_t* caffe_rng() { // 获取 RNG 类的随机数
return static_cast<caffe::rng_t*>(Caffe::rng_stream().generator());
}
// 使用 Fisher–Yates algorithm 算法对随机数进行混洗
template <class RandomAccessIterator, class RandomGenerator>
inline void shuffle(RandomAccessIterator begin, RandomAccessIterator end,
RandomGenerator* gen) {
typedef typename std::iterator_traits<RandomAccessIterator>::difference_type
difference_type;
typedef typename boost::uniform_int<difference_type> dist_type;
difference_type length = std::distance(begin, end);
if (length <= 0) return;
for (difference_type i = length - 1; i > 0; --i) {
dist_type dist(0, i);
std::iter_swap(begin + i, begin + dist(*gen));
}
}