返回介绍

Caffe 源码阅读 - Net 组装

发布于 2025-02-25 23:04:57 字数 5463 浏览 0 评论 0 收藏 0

最近忙着看 TI 没有及时写文章,今天赶紧补一篇……

Net 是 Caffe 代码中一个比较核心的类,往下看它封装了所有的 Layer,构建起了整个神经网络;往上看它对外提供了前向后向计算,以及核心数据结构的访问结构,使得再上层的 Solver 可以利用 Net 比较轻松地实现 Train 和 Test 的策略。当然,正是因为它的重要性,组装 Net 是一个比较复杂的部分。这一回我们就来看看 Net 的内容。

当然,说在前面,看 Net 组装的代码有两个目的:

  1. 了解作为一个成熟的 CNN 模型框架需要考虑的一些问题;
  2. 如果想对网络结构做扩展,如写一个新的 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);

总结地说,这四句完成了:

  1. 对 bottom blob, top blob 数量的检查,父类实现。
  2. 对 Layer 内部相关变量的初始化,由具体的子类实现
  3. 传入时 bottom blob 的维度已经确定,Layer 需要根据自己要做的计算确定 top blob 的纬度。比方说这一层是卷积层,维度是 20*5*5,输入图像是 1*28*28,也就是 bottom blob 的维度,那么输入就是 20*24*24,这也是上面 log 里面算出的结果,只不过还加了一个 batch size。这个函数由具体的子类实现。
  4. 对 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 完成了以下的事情:

  1. 实例化 Layer
  2. 创建 bottom blob,top blob
  3. Setup Layer(初始化 Layer,确定 top blob 维度)
  4. 确定 layer 的 loss_weight
  5. 确定 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文