请以“仰望星空与脚踏实地”作为题目,写一篇不少于800字的文章。除诗歌外,文体不限。

                                  ——2010·北京卷

仰望星空

规范性

Caffe诞生于12年末,如果偏要形容一下这个框架,可以用"须敬如师长"。

这是一份相当规范的代码,这个规范,不应该是BAT规范,那得是Google规范。

很多自称码农的人应该好好学习这份代码,改改自己丑陋的C++编程习惯。

下面列出几条重要的规范准则:

★const

先说说const问题,Google为了增加代码的可读性,明确要求:

不做修改的量(涵盖函数体内、函数参数列表),必须以const标记。

相对的,对于那些改变的量,可选择用mutable标记。

因为mutable关键词不是很常用,所以一般在自设函数中使用。

严格的const不在于担心变量是否被误修改,而在于给代码阅读者一个清晰的思路:

这个值不会改变,这个值肯定要改变。

★引用

"引用"是C/C++设计的一个败笔,因为C/C++默认是深拷贝,这在大内存数据结构操作的时候,

容易让新手程序员写出弱智低能的代码。假设Datum结构A使用了2G内存,令:

Datum B=A;

那么,内存会占用4G空间,而且,我们大概需要几秒的时间去拷贝A的2G内存。

这个几秒看起来不是很成问题,但是在多线程编程中,两个异步线程共享数据:

如果你不用引用会怎么样?

很有趣,这个复制再赋值的操作会被CPU中断,变成无效指令。

这在Caffe的多线程I/O设计架构中,是个关键点。

另外,对于基本数据类型(char/int/float/double),引用是没用必要的。

但是,string、vector<int>等容器,引用就相当有必要了。

★const引用

const引用最常见于函数参数列表,用于传递常、大数据结构量。

与此相对的,如果你要修改一个大数据结构量,应当在参数列表中传入指针,而不是引用。

传入引用来修改是C规范,传入指针来修改是C++规范,Caffe严格遵照C++规范,这点要明确。

★常成员函数

常成员函数,在OO里通常容易被新手忽略掉。(Java就没那么复杂),通常写作:

void xxx() const,目的是:

const标记住传入成员函数的this指针。

常成员函数其实不是必要的,但是在一定情况下,就会变成必要的。

这个情况相当有趣,而且在Caffe中也经常发生:

void xxx(const Blob& blob){
  blob.count();
}

如果我们遵照Google的编程规范,用const引用锁定传入的Blob。

那么,blob.count()这个成员函数的调用就会被编译器的语义分析为:成员变量不可修改。

如果你的代码写成这样,那就会被编译器拦下,错误信息为:this指针不一致。

class Blob{
public:
int count() {} //错误
int count() const {} //正确
};

★public、private、protected

OO的封装性是比较难定位的一个规范,成员变量及成员函数如何访问权限是个问题。

Caffe严格遵照标准的OO封装概念:方法是public,变量是private或者是protected。

区别private和protected就一句话:

private成员变量或是函数,不可能被继承。通常只用在本Class独有,而派生类不直使用的函数/变量上。

比如im2col和col2im,这两个为卷积做Patch预变换的函数。

protected和private的成员函数和成员变量都不可能从外部被访问,应当在public里专门设置访问接口。

并且接口根据需要,恰当使用const标记,避免越权访问。

有趣的是,如果这么做,会增加相当多的代码量,而且都是一些复制粘贴的废品代码。

为了避免这种情况,Google开发了Protocol Buffer,将数据结构大部分访问接口自动生成,且独立安排。

这样,在主体代码里,我们不会因为数据的访问接口的规范,而导致阅读代码十分头疼(想想那一扫下来的废代码)。

独立性

如果你研究过Word2Vec的源码,应该就知道,为什么Word2Vec必须跑在Linux下。。

因为Mikolov同学在写代码的时候,用了POSIX OS的API函数pThread,来实现内核级线程。

这为跨平台带来麻烦,一份优秀的跨平台代码,必须具有相当出色的平台独立性。

在这点上,Caffe使用了C++最强大的Boost库,来避免对OS API函数的使用。

Boost库,又称为C++三千佳丽的后宫,内涵1W+头文件,完整编译完大小达3.3G,相当庞大。

它的代码来自世界上顶级的C++开发者,是C++最忠实的第三方库,并且是ISO C++新规范的唯一来源。

Boost在Caffe中的主要作用是提供OS独立的内核级线程。

当然,已经于C++11中被列入规范的boost::shared_ptr其实也算。

还有一个十分精彩的boost::thread_specific_ptr,也在Caffe中起到了核心作用。

不足之处也有,而且其中一处还成了Bug,那就是API函数之一的open。

Linux的open默认是以二进制打开的,而Windows则是以文本形式打开的。

移植到Windows时,需要补上 O_BINARY作为flag。

异构性

大家都知道Caffe能跑GPU,一个关键点是:

它是在何处,又是怎么进行CPU与GPU分离的?

这个模型实际上应当算是CUDA标准模型。

由于内存显存不能跨着访问(一个在北桥,一个在南桥),又要考虑的CPU和GPU的平衡。

所以,数据的读取、转换不仅要被平摊到CPU上,而且应当设计成多线程,多线程的生产者消费者模型。

并且具有一定的多重缓冲能力,这样保证最大化CPU/GPU的计算力。

在一个机器学习系统当中,我们要珍惜计算设备的每一个时钟周期,切实做到计算力的最大化利用。

设计模式

实际使用的设计模式只有两个。

第一个是MVC,这个其实是迫不得已。

异构编程决定着,数据、视图、控制三大块必须独立开来。

但视图和控制并不是很明显,在设计接口/可视化GUI的时候,将凸显重要性。

第二个称为工厂模式,这是一个存在于Java的概念,尽管C++也可以模仿。

具体来说,工厂模式是为了弥补面向对象型编译语言的不足,会被OO的多态所需要。

以Caffe为例:

我们当前有一个基类指针Layer* layer;

在程序运行之前,计算机并不知道这个指针究竟要指向何种派生类。是卷积层?Pooling层?ReLU层?

鬼才知道。一个愚蠢的方法:

if(type==CONV) {....}
else if(type==POOLING) {....}
else if(type==RELU} {.....}
else {ERROR}

看起来,还是可以接受的,但是在软件工程专业看来,这种模式相当得蠢。

工厂模式借鉴了工厂管理产品的经验,将各种类型存在数据库中,需要时,拿出来看看。

这种模式相当得灵活,当然,在Caffe中作用不是很大,仅仅是为了花式好看。

要实现这个模式,你只需要一个关联容器(C++/JAVA),字典容器(Python)。

将string与创建指针绑定即可。

C/C++中有函数指针的说法,如:

typedef boost::shared_ptr< Layer<Dtype> > (*NEW_FUNC)(const LayerParameter& );

经过typdef之后,NEW_FUNC就可以指向函数:

boost::shared_ptr< Layer<Dtype> > xxx(const LayerParameter& x);
NEW_FUNC yyy=boost::shared_ptr< Layer<Dtype> > xxx(const LayerParameter& x); yyy(); //相当于xxx()
xxx();

需要访问工厂时,我们只需要访问这个代替工厂管理数据库的容器,而不是幼稚地使用if(.....)

序列化与反序列化

如果Caffe不使用Protocol Buffer,那么代码量将扩大一倍。

这不是危言耸听,在传统系统级程序设计中,序列化与反序列化一直是一个码农问题。

尤其是在机器学习系统中,复杂多变的数据结构,给序列化和反序列化带来巨大麻烦。

Protocol Buffer在序列化阶段,是一个高效的编码器,能将数据最小体积序列化。

而在反序列化阶段,它是一个强大的解码器,支持二进制/文本两类数据的解析与结构反序列化。

其中,从文本反序列化意义颇大,这就形成了Caffe著名的文本配置文件prototxt,用于net和solver。

相对灵活的配置方式,尤其适合超大规模神经网络,这点在早期机器学习系统中独领风骚(很多人认为这比图形界面还要方便)。

据说写库狂人都是用宏狂人。

C/C++提供了强大了自定义宏函数(#define),Caffe通过宏,大概减少了1000~2000行代码。

宏函数大致有如下几种:

① #define DISABLE_COPY_AND_ASSIGN(classname)

俗称禁止拷贝和赋值宏,如果你熟悉Qt,就会发现,Qt中大部分数据结构都用了这个宏来保护。

这个宏算是最没用的宏,用在了所有Caffe大型数据结构上(Blob、Layer、Net、Solver)

目的是禁止两个大型数据结构直接复制、构造、然后赋值。

实际上,Caffe也没有去编写复制构造函数代码,所以最终还是会被编译器拦下。

前面以及说过了,两个大型数据结构之间的复制会是什么样的下场,这是绝对应该被禁止的。

如果你要使用一个数据结构,请用指针或是引用指向它。

如果你有乱赋值的编程陋习,请及时打上这个宏,避免自己手贱。反之,可以暂时无视它。

当然,从库的完整性角度,这个宏是明智的。

Java/Python不需要这个宏,因为Java对大型数据结构,默认是浅拷贝,也就是直接引用。

而Python,这个没有数据类型的奇怪语言,则默认全部是浅拷贝。

②#define INSTANTIATE_CLASS(classname)

非常非常非常重要的宏,重要的事说三遍。

由于Caffe采用分离式模板编程方法(据说也是Google倡导的)

模板未类型实例化的定义空间和实例化的定义空间是不同的。

实际上,编译器并不会理睬分离在cpp里的未实例化的定义代码,而是将它放置在一个虚拟的空间。

一旦一段明确类型的代码,访问这段虚拟代码空间,就会被编译器拦截。

如果你想要让模板的声明和定义分离编写,就需要在cpp定义文件里,将定义指定明确的类型,实例化。

这个宏的作用正是如此。(Google编程习惯的宏吧)。

更详细的用法,将在后续文章中详细介绍。

③#define INSTANTIATE_LAYER_GPU_FUNCS(classname)

通样是实例化宏,专门写这个宏的原因,是因为NVCC编译器相当傲娇。

打在cpp文件里的INSTANTIATE_CLASS宏,NVCC在编译cu文件时,可不会知道。

所以,你需要在cu文件里,为这些函数再次实例化。

其实也没几个函数,也就是forward_gpu和backward_gpu

④#define NOT_IMPLEMENTED

俗称偷懒宏,你要是这段代码不想写了,打个NOT_IMPLEMENTED就行了。

就是宣告:“老子就是不想写这段代码,留空,留空!”

但是注意,宏封装了LOG(FATAL),这是个Assert(断言),会引起CPU硬件中断。

一旦代码空间转到你没写的这段,整个程序就会被终止。

所以,偷懒有度,还是认真写代码吧。

⑤#define REGISTER_LAYER_CLASS(type)

Layer工厂模式用的宏,也就是将这个Layer的信息写到工厂的管理数据库里。

此宏省了不少代码,在使用工厂之前,记得要为每个成品(Layer)打上这个宏。

命名空间

Caffe为了与Boost等库接轨,几乎为所有结构提供了以caffe为关键字的命名空间。

设置命名空间的主要目的是防止Caffe的函数、变量与其他库产生冲突。

在我们的山寨过程中,为了代码的简洁,将忽略全部的命名空间。

命名法

Caffe中普遍采用下划线命名法。

我们对其作出了部分修改,整体采用两种命名法:

①针对变量而言: 采用下划线命名法

②针对函数而言:采用驼峰命名法

脚踏实地

编程手册

Caffe几乎是C++ Primer 第五版的鲜活例子,如果你需要读懂它,经常翻一翻C++ Primer是一个不错的主意。

(另:不要阅读C++ Primer Plus,它的作者仅仅是一个普通教师,

而C++ Primer作者则包含C++协发明者、ISO C++委员会的人,是权威圣经)

耐心阅读和模仿代码

注意你接触的是一个系统级程序,Windows还是全球5000位微软工程师开发的。

系统级程序相当庞大和复杂,切记不要心浮气躁,不要以套库的心理去学习。

更不要认为,看看高层代码就可以了,这简直是噩梦,最后你会发现你根本读不懂。

来一个响亮的名字

为自己的工程取个名字是一件有趣的事,本项目默认名为:Dragon。

因为深度神经网络活像一头蠢龙。

从零开始山寨Caffe·壹:仰望星空与脚踏实地的更多相关文章

  1. 从零开始山寨Caffe·陆:IO系统(一)

    你说你学过操作系统这门课?写个无Bug的生产者和消费者模型试试! ——你真的学好了操作系统这门课嘛? 在第壹章,展示过这样图: 其中,左半部分构成了新版Caffe最恼人.最庞大的IO系统. 也是历来最 ...

  2. 从零开始山寨Caffe·零:必先利其器

    工作环境 巧妇有了米炊 众所周知,Caffe是在Linux下写的,所以长久以来,大家都认为跑Caffe,先装Linux. niuzhiheng大神发起了caffe-windows项目(解决了一些编译. ...

  3. 从零开始山寨Caffe·玖:BlobFlow

    听说Google出了TensorFlow,那么Caffe应该叫什么? ——BlobFlow 神经网络时代的传播数据结构 我的代码 我最早手写神经网络的时候,Flow结构是这样的: struct Dat ...

  4. 从零开始山寨Caffe·伍:Protocol Buffer简易指南

    你为Class外访问private对象而苦恼嘛?你为设计序列化格式而头疼嘛? ——欢迎体验Google Protocol Buffer 面向对象之封装性 历史遗留问题 面向对象中最矛盾的一个特性,就是 ...

  5. 从零开始山寨Caffe·叁:全局线程管理器

    你需要一个管家,随手召唤的那种,想吃啥就吃啥. ——设计一个全局线程管理器 一个机器学习系统,需要管理一些公共的配置信息,如何存储这些配置信息,是一个难题. 设计模式 MVC框架 在传统的MVC编程框 ...

  6. 从零开始山寨Caffe·拾贰:IO系统(四)

    消费者 回忆:生产者提供产品的接口 在第捌章,IO系统(二)中,生产者DataReader提供了外部消费接口: class DataReader { public: ......... Blockin ...

  7. 从零开始山寨Caffe·拾:IO系统(三)

    数据变形 IO(二)中,我们已经将原始数据缓冲至Datum,Datum又存入了生产者缓冲区,不过,这离消费,还早得很呢. 在消费(使用)之前,最重要的一步,就是数据变形. ImageNet Image ...

  8. 从零开始山寨Caffe·捌:IO系统(二)

    生产者 双缓冲组与信号量机制 在第陆章中提到了,如何模拟,以及取代根本不存的Q.full()函数. 其本质是:除了为生产者提供一个成品缓冲队列,还提供一个零件缓冲队列. 当我们从外部给定了固定容量的零 ...

  9. 从零开始山寨Caffe·柒:KV数据库

    你说你会关系数据库?你说你会Hadoop? 忘掉它们吧,我们既不需要网络支持,也不需要复杂关系模式,只要读写够快就行.    ——论数据存储的本质 浅析数据库技术 内存数据库——STL的map容器 关 ...

随机推荐

  1. 修改Excel2013默认模版(启动模版和新建Sheet模版)

    1.  C:\Windows\ShellNew\EXCEL12.XLSX 设置好格式后另存为, 然后复制过来覆盖掉,如果覆盖不了,注意修改所有者权限 2. 新建文件保存为模版文件Sheet.xltx, ...

  2. ActiveMQ 即时通讯服务 浅析

      一. 概述与介绍 ActiveMQ 是Apache出品,最流行的.功能强大的即时通讯和集成模式的开源服务器.ActiveMQ 是一个完全支持JMS1.1和J2EE 1.4规范的 JMS Provi ...

  3. 原生JavaScript技巧

    时常在技术论坛有看见一些比较好的示例,于是就出于一种收集并学习的态度,于是就保留下来啦~  当然现在展示的也只是一部分,先放一部分出来尝尝鲜~~~

  4. HDOJ 4652 Dice

      期望DP +数学推导 Dice Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others ...

  5. 【荐】PHP Session和Cookie,Session阻塞,Session垃圾回收,Redis共享Session,不推荐Memcached保存Session

    什么是 Session 在 web 应用开发中,Session 被称为会话.主要被用于保存某个访问者的数据. 由于 HTTP 无状态的特点,服务端是不会记住客户端的,对服务端来说,每一个请求都是全新的 ...

  6. Go - 数组 和 切片(array、slice)

    一.数组 与其他大多数语言类似,Go语言的数组也是一个元素类型相同的定长的序列. (1)数组的创建 数组有 3 种创建方式: 1) [length]Type 2) [length]Type{value ...

  7. vue2.0学习(二)

    1.关于模板渲染,当需要渲染多个元素时可以 <ul> <template v-for="item in items"> <li>{{ item. ...

  8. 在OSX和Windows版本Docker上运行GUI程序

    看到很多人在Docker问题区讨论:如何在OS X和Windows的Docker上运行GUI程序, 随手记录几个参考资料: https://github.com/docker/docker/issue ...

  9. Add Two Numbers LeetCode Java

    You are given two linked lists representing two non-negative numbers. The digits are stored in rever ...

  10. Two Sum Leetcode Java

    Given an array of integers, return indices of the two numbers such that they add up to a specific ta ...