Caffe 源码阅读 - Net 组装
最近忙着看 TI 没有及时写文章,今天赶紧补一篇……
Net 是 Caffe 代码中一个比较核心的类,往下看它封装了所有的 Layer,构建起了整个神经网络;往上看它对外提供了前向后向计算,以及核心数据结构的访问结构,使得再上层的 Solver 可以利用 Net 比较轻松地实现 Train 和 Test 的策略。当然,正是因为它的重要性,组装 Net 是一个比较复杂的部分。这一回我们就来看看 Net 的内容。
当然,说在前面,看 Net 组装的代码有两个目的:
- 了解作为一个成熟的 CNN 模型框架需要考虑的一些问题;
- 如果想对网络结构做扩展,如写一个新的 Layer,其中的一些数据是如何在 Layer 和 Net 之间流动的
首先,为了使问题不那么复杂,我们先从训练模型时输出的 log 看看 Net 组装的几个关键步骤,然后再把这个过程慢慢展开,了解组装的所有细节。
Log 眼中的 Net 组装
为了更好地展示 Net 组装的一些细节,我们在这里选取了一个实际例子,就是 Caffe 的 examples 里面的 siamese model。关于这个 model 的细节这里就不多说了,感兴趣的可以去看官方或者非官方的文档,这里只提一点:这个网络除了包含其他正常网络中的一些特性之外,还具有网络参数复用的特点,在后面的分析中我们会用到。
下面我们要看的就是 Net 组装的 Log。这段 Log 一般都是大家在训练网络时一闪而过的大段 Log,当然如果它没有一闪而过而是停下来了,有可能是你的网络定义有问题爆出了错误。这段 Log 内容比较多,总体来说就是 Train 阶段和 Test 阶段的两个网络组装起来。我们重点关注其中的几个片段,来大概了解 Net 组装的一些核心内容,也是那些比较值得打印出来的内容。
首先是一个正常的卷积层 conv1,Log 如下所示(以下代码的行号可能会有不同,但位置是相近的):
layer_factory.hpp:77] Creating layer conv1
net.cpp:92] Creating Layer conv1
net.cpp:428] conv1 <- data
net.cpp:402] conv1 -> conv1
net.cpp:144] Setting up conv1
net.cpp:151] Top shape: 64 20 24 24 (737280)
net.cpp:159] Memory required for data: 3752192
这其中第一行是创建这个 Layer 实例的代码,具体的创建过程在 layer_factory 里面。为了方便创建 Layer,Caffe 采用了工厂方法的设计模式,只要提供 Layer 的名字(在配置文件中参数叫 type),就可以根据名字和对应参数实例化一个 Layer。这部分的细节只要认真看一下就会明白。
第 3,4 行显示了创建当前层的 bottom 和 top 数据的过程。这里涉及到 net.cpp 中的 AppendBottom 和 AppendTop 两个方法,因为每一个 bottom blob 和 top blob 都有名字,这里就将他们之间的关系输出在了这里。
第 5 行看上去没什么干货,但是它代表了 Layer 的 Setup 函数已经调用完成(或者 Layer 被 share)。Layer 的 Setup 函数是 Layer 初始化的关键函数,这里面涉及到以下几个具体的操作:
CheckBlobCounts(bottom, top);
LayerSetUp(bottom, top);
Reshape(bottom, top);
SetLossWeights(top);
总结地说,这四句完成了:
- 对 bottom blob, top blob 数量的检查,父类实现。
- 对 Layer 内部相关变量的初始化,由具体的子类实现
- 传入时 bottom blob 的维度已经确定,Layer 需要根据自己要做的计算确定 top blob 的纬度。比方说这一层是卷积层,维度是 20*5*5,输入图像是 1*28*28,也就是 bottom blob 的维度,那么输入就是 20*24*24,这也是上面 log 里面算出的结果,只不过还加了一个 batch size。这个函数由具体的子类实现。
- 对 Layer 是否输出 loss 以及输出 loss 要做的操作进行初始化。父类实现。必须说一句,Caffe 中关于 Loss Layer 中 Loss_weight,loss_,top.cpu_diff 的数据设定还是有点绕且有点 trick 的。
好了回到上面的 log。接下来的那一句告诉了我们 top 层应该输出的维度。这里输出了维度就是为了让不放心的朋友算一下,看看和你想的是否一样。当然,输出这句 log 的循环不是只做了这件事,它的主要工作就是设置 top blob 的 loss_weight。
最后一句计算了该层 top blob 所占用的内存。可以看出截至到这一层,内存消耗大约是 3M 多,还不算大。
好,这就是一个最典型的 Layer 的初始化,下面这个 ReLU 层就稍微有些不同了:
layer_factory.hpp:77] Creating layer relu1
net.cpp:92] Creating Layer relu1
net.cpp:428] relu1 <- ip1
net.cpp:389] relu1 -> ip1 (in-place)
net.cpp:144] Setting up relu1
net.cpp:151] Top shape: 64 500 (32000)
net.cpp:159] Memory required for data: 5769472
这里面最不同的就是第 4 行结尾的(in-place),这说明 relu 的 bottom blob 和 top blob 是同一个数据,这和我们在网络中的定义是一样的。in-place 的好处就是减少内存的操作,但是这里在统计内存消耗时并没有考虑 in-place 带来的节省。
接下来就是共享网络的 conv1_p 了:
layer_factory.hpp:77] Creating layer conv1_p
net.cpp:92] Creating Layer conv1_p
net.cpp:428] conv1_p <- data_p
net.cpp:402] conv1_p -> conv1_p
net.cpp:144] Setting up conv1_p
net.cpp:151] Top shape: 64 20 24 24 (737280)
net.cpp:159] Memory required for data: 8721664
net.cpp:488] Sharing parameters 'conv1_w' owned by layer 'conv1', param index 0
net.cpp:488] Sharing parameters 'conv1_b' owned by layer 'conv1', param index 1
这一段最有特点的是最后两句“Sharing”,因为 siamese model 中拥有参数完全相同的两个网络,所以在构建时候,第二个网络检测到参数名字已经存在,说明该层的参数和其他层共享,于是在这里打印出来告诉用户这一点。当然,这一句之前没有打印出来的内容告诉了我们,实际上 Net 类中还负责了参数相关的初始化。这部分的内容实际上还挺多,除了参数共享,还有对参数 learning rate,weight decay 的设定。
最后是最特别的一层:loss 层
net.cpp:92] Creating Layer loss
net.cpp:428] loss <- feat
net.cpp:428] loss <- feat_p
net.cpp:428] loss <- sim
net.cpp:402] loss -> loss
net.cpp:144] Setting up loss
net.cpp:151] Top shape: (1)
net.cpp:154] with loss weight 1
net.cpp:159] Memory required for data: 10742020
这一层看上去没有什么特别,该有的和前面一样,但是唯一不同的就是它的倒数第二行,这说明这一层是有 loss weight 的。至于有 loss weight 有什么用,以后我们会详细说这个事情。这里简单说一下,有 loss weight 表示这个 blob 会被用于计算 loss。
前面的 log 主要解决了网络的组装和前向的一些计算,从 log 中,我们可以看出 Net 完成了以下的事情:
- 实例化 Layer
- 创建 bottom blob,top blob
- Setup Layer(初始化 Layer,确定 top blob 维度)
- 确定 layer 的 loss_weight
- 确定 layer 的参数是否共享,不共享则创建新的
从上面的过程也可以看出,整个网络中所有的流动性变量(bottom blob,top blob)都保存在 Net 中,同时对于各层的参数,根据各层的共享关系做了标记。这样好处是集中管理了网络中的数据,方便对数据进行操作。
再往下面,我们可以截取一小段 log 来:
net.cpp:220] pool1 needs backward computation.
net.cpp:220] conv1 needs backward computation.
net.cpp:222] slice_pair does not need backward computation.
net.cpp:222] pair_data does not need backward computation.
net.cpp:264] This network produces output loss
net.cpp:277] Network initialization done.
接下来是统计一个层次是否需要进行反向传播的计算。一般来说我们的层是都需要计算的,但是也会有一些层不需要计算,比方说数据层,就像上面的 log 那样,还有就是一些希望固定的层,这个一般在 finetune 网络的时候用的上。因为反向计算一般比前向计算慢,如果有不需要计算的 Layer,直接跳过计算是可以节省时间的。
最后是整个网络产生的输出,这个输出会在训练迭代中显示出来的。
了解了这些,我们就对 Net 装载有了大概的了解,再去看它的代码就会轻松些。
最后,关于 Net 类中所有的成员变量与它们之间的关系,我们可以用下面的一张图来理解就好:
把 Net 的初始化理解后,其实 Net 以下的架构方面的问题就不多了。下面我再看看 Net 以上的东西,Solver 以及 Caffe 里“简单”的多卡训练。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论