Caffemodel解析

因为工作需要最近一直在琢磨Caffe,纯粹新手,写博客供以后查阅方便,请大神们批评指正!

Caffe中,数据的读取、运算、存储都是采用Google Protocol Buffer来进行的,所以首先来较为详细的介绍下Protocol Buffer(PB)。

PB是一种轻便、高效的结构化数据存储格式,可以用于结构化数据串行化,很适合做数据存储或 RPC 数据交换格式。它可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。是一种效率和兼容性都很优秀的二进制数据传输格式,目前提供了 C++、Java、Python 三种语言的 API。Caffe采用的是C++和Python的API。

接下来,我用一个简单的例子来说明一下。

使用PB和 C++ 编写一个十分简单的例子程序。该程序由两部分组成。第一部分被称为Writer,第二部分叫做Reader。Writer 负责将一些结构化的数据写入一个磁盘文件,Reader则负责从该磁盘文件中读取结构化数据并打印到屏幕上。准备用于演示的结构化数据是HelloWorld,它包含两个基本数据:

ID,为一个整数类型的数据;

Str,这是一个字符串。

首先我们需要编写一个proto文件,定义我们程序中需要处理的结构化数据,Caffe是定义在caffe.proto文件中。在PB的术语中,结构化数据被称为 Message。proto文件非常类似java或C语言的数据定义。代码清单 1 显示了例子应用中的proto文件内容。

清单 1. proto 文件

 1 package lm;
 2
 3 message helloworld
 4
 5  {
 6
 7     required int32     id = 1;   // ID
 8
 9     required string    str = 2;  // str
10
11     optional int32     opt = 3;  // optional field
12
13  }

一个比较好的习惯是认真对待proto文件的文件名。比如将命名规则定于如下: packageName.MessageName.proto

在上例中,package名字叫做 lm,定义了一个消息helloworld,该消息有三个成员,类型为int32的id,另一个为类型为string的成员str。optional是一个可选的成员,即消息中可以不包含该成员,required表明是必须包含该成员。一般在定义中会出现如下三个字段属性:

对于required的字段而言,初值是必须要提供的,否则字段的便是未初始化的。 在Debug模式的buffer库下编译的话,序列化话的时候可能会失败,而且在反序列化的时候对于该字段的解析会总是失败的。所以,对于修饰符为required的字段,请在序列化的时候务必给予初始化。

对于optional的字段而言,如果未进行初始化,那么一个默认值将赋予该字段,当然也可以指定默认值。

对于repeated的字段而言,该字段可以重复多个,谷歌提供的这个 addressbook例子便有个很好的该修饰符的应用场景,即每个人可能有多个电话号码。在高级语言里面,我们可以通过数组来实现,而在proto定义文件中可以使用repeated来修饰,从而达到相同目的。当然,出现0次也是包含在内的。

写好proto文件之后就可以用PB编译器(protoc)将该文件编译成目标语言了。本例中我们将使用C++。假设proto文件存放在 $SRC_DIR 下面,您也想把生成的文件放在同一个目录下,则可以使用如下命令:

1  protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto

命令将生成两个文件:

lm.helloworld.pb.h, 定义了C++ 类的头文件;

lm.helloworld.pb.cc,C++类的实现文件。

在生成的头文件中,定义了一个 C++ 类 helloworld,后面的 Writer 和 Reader 将使用这个类来对消息进行操作。诸如对消息的成员进行赋值,将消息序列化等等都有相应的方法。

如前所述,Writer将把一个结构化数据写入磁盘,以便其他人来读取。假如我们不使用 PB,其实也有许多的选择。一个可能的方法是将数据转换为字符串,然后将字符串写入磁盘。转换为字符串的方法可以使用 sprintf(),这非常简单。数字 123 可以变成字符串”123”。这样做似乎没有什么不妥,但是仔细考虑一下就会发现,这样的做法对写Reader的那个人的要求比较高,Reader 的作者必须了解Writer 的细节。比如”123”可以是单个数字 123,但也可以是三个数字 1、2 和 3等等。这么说来,我们还必须让Writer定义一种分隔符一样的字符,以便Reader可以正确读取。但分隔符也许还会引起其他的什么问题。最后我们发现一个简单的Helloworld 也需要写许多处理消息格式的代码。

如果使用 PB,那么这些细节就可以不需要应用程序来考虑了。使用PB,Writer 的工作很简单,需要处理的结构化数据由 .proto 文件描述,经过上一节中的编译过程后,该数据化结构对应了一个 C++ 的类,并定义在 lm.helloworld.pb.h 中。对于本例,类名为lm::helloworld。

Writer 需要include该头文件,然后便可以使用这个类了。现在,在Writer代码中,将要存入磁盘的结构化数据由一个lm::helloworld类的对象表示,它提供了一系列的 get/set 函数用来修改和读取结构化数据中的数据成员,或者叫field。

当我们需要将该结构化数据保存到磁盘上时,类 lm::helloworld 已经提供相应的方法来把一个复杂的数据变成一个字节序列,我们可以将这个字节序列写入磁盘。

对于想要读取这个数据的程序来说,也只需要使用类 lm::helloworld 的相应反序列化方法来将这个字节序列重新转换会结构化数据。这同我们开始时那个“123”的想法类似,不过PB想的远远比我们那个粗糙的字符串转换要全面,因此,我们可以放心将这类事情交给PB吧。程序清单 2 演示了 Writer 的主要代码。

清单 2. Writer 的主要代码

 1  #include "lm.helloworld.pb.h"
 2
 3 …
 4
 5  int main(void)
 6
 7  {
 8
 9   lm::helloworld msg1;
10
11   msg1.set_id(101);          //设置id
12
13   msg1.set_str(“hello”);   //设置str
14
15   // 向磁盘中写入数据流fstream
16
17   fstream output("./log", ios::out | ios::trunc | ios::binary);
18
19   if (!msg1.SerializeToOstream(&output)) {
20
21       cerr << "Failed to write msg." << endl;
22
23       return -1;
24
25   }
26
27   return 0;
28
29  }

Msg1 是一个helloworld类的对象,set_id()用来设置id的值。SerializeToOstream将对象序列化后写入一个fstream流。我们可以写出Reader代码,程序清单3列出了 reader 的主要代码。

清单 3. Reader的主要代码

 1 #include "lm.helloworld.pb.h"
 2
 3 …
 4
 5  void ListMsg(const lm::helloworld & msg) {
 6
 7   cout << msg.id() << endl;
 8
 9   cout << msg.str() << endl;
10
11  }
12
13  int main(int argc, char* argv[]) {
14
15   lm::helloworld msg1;
16
17   {
18
19     fstream input("./log", ios::in | ios::binary);
20
21     if (!msg1.ParseFromIstream(&input)) {
22
23       cerr << "Failed to parse address book." << endl;
24
25       return -1;
26
27     }
28
29   }
30
31   ListMsg(msg1);
32
33   …
34
35  }

同样,Reader 声明类helloworld的对象msg1,然后利用ParseFromIstream从一个fstream流中读取信息并反序列化。此后,ListMsg中采用get方法读取消息的内部信息,并进行打印输出操作。

运行Writer和Reader的结果如下:

 >writer 
 >reader 
 101 
 Hello

Reader 读取文件 log 中的序列化信息并打印到屏幕上。这个例子本身并无意义,但只要稍加修改就可以将它变成更加有用的程序。比如将磁盘替换为网络 socket,那么就可以实现基于网络的数据交换任务。而存储和交换正是PB最有效的应用领域。

到这里为止,我们只给出了一个简单的没有任何用处的例子。在实际应用中,人们往往需要定义更加复杂的 Message。我们用“复杂”这个词,不仅仅是指从个数上说有更多的 fields 或者更多类型的 fields,而是指更加复杂的数据结构:嵌套 Message,Caffe.proto文件中定义了大量的嵌套Message。使得Message的表达能力增强很多。代码清单 4 给出一个嵌套 Message 的例子。

清单 4. 嵌套 Message 的例子

 1  message Person {
 2   required string name = 1;
 3   required int32 id = 2;        // Unique ID number for this person.
 4   optional string email = 3;
 5   enum PhoneType {
 6     MOBILE = 0;
 7     HOME = 1;
 8     WORK = 2;
 9   }
10
11   message PhoneNumber {
12     required string number = 1;
13     optional PhoneType type = 2 [default = HOME];
14   }
15   repeated PhoneNumber phone = 4;
16  }

在 Message Person 中,定义了嵌套消息 PhoneNumber,并用来定义 Person 消息中的 phone 域。这使得人们可以定义更加复杂的数据结构。

以上部分参考网址:http://www.ibm.com/developerworks/cn/linux/l-cn-gpb/

在Caffe中也是类似于上例中的Writer和Reader去读写PB数据的。接下来,具体说明下Caffe中是如何存储Caffemodel的。在Caffe主目录下的solver.cpp文件中的一段代码展示了Caffe是如何存储Caffemodel的,代码清单5如下:

清单 5. Caffemodel存储代码

 1 template <typename Dtype>
 2
 3 void Solver<Dtype>::Snapshot() {
 4
 5   NetParameter net_param;    // NetParameter为网络参数类
 6
 7   // 为了中间结果,也会写入梯度值
 8
 9   net_->ToProto(&net_param, param_.snapshot_diff());
10
11   string filename(param_.snapshot_prefix());
12
13   string model_filename, snapshot_filename;
14
15   const int kBufferSize = 20;
16
17   char iter_str_buffer[kBufferSize];
18
19   // 每训练完1次,iter_就加1
20
21 snprintf(iter_str_buffer, kBufferSize, "_iter_%d", iter_ + 1);
22
23   filename += iter_str_buffer;
24
25   model_filename = filename + ".caffemodel"; //XX_iter_YY.caffemodel
26
27   LOG(INFO) << "Snapshotting to " << model_filename;
28
29   // 向磁盘写入网络参数
30
31   WriteProtoToBinaryFile(net_param, model_filename.c_str());
32
33   SolverState state;
34
35   SnapshotSolverState(&state);
36
37   state.set_iter(iter_ + 1);    //set
38
39   state.set_learned_net(model_filename);
40
41   state.set_current_step(current_step_);
42
43   snapshot_filename = filename + ".solverstate";
44
45   LOG(INFO) << "Snapshotting solver state to " << snapshot_filename;
46
47   // 向磁盘写入网络state
48
49   WriteProtoToBinaryFile(state, snapshot_filename.c_str());
50
51 }

在清单5代码中,我们可以看到,其实Caffemodel存储的数据也就是网络参数net_param的PB,Caffe可以保存每一次训练完成后的网络参数,我们可以通过XX.prototxt文件来进行参数设置。在这里的 WriteProtoToBinaryFile函数与之前HelloWorld例子中的Writer函数类似,在这就不在贴出。那么我们只要弄清楚NetParameter类的组成,也就明白了Caffemodel的具体数据构成。在caffe.proto这个文件中定义了NetParameter类,如代码清单6所示。

清单6. Caffemodel存储代码

  1 message NetParameter {
  2
  3   optional string name = 1;   // 网络名称
  4
  5   repeated string input = 3;  // 网络输入input blobs
  6
  7   repeated BlobShape input_shape = 8; // The shape of the input blobs
  8
  9   // 输入维度blobs,4维(num, channels, height and width)
 10
 11   repeated int32 input_dim = 4;
 12
 13   // 网络是否强制每层进行反馈操作开关
 14
 15 // 如果设置为False,则会根据网络结构和学习率自动确定是否进行反馈操作
 16
 17   optional bool force_backward = 5 [default = false];
 18
 19 // 网络的state,部分网络层依赖,部分不依赖,需要看具体网络
 20
 21   optional NetState state = 6;
 22
 23   // 是否打印debug log
 24
 25   optional bool debug_info = 7 [default = false];
 26
 27   // 网络层参数,Field Number 为100,所以网络层参数在最后
 28
 29   repeated LayerParameter layer = 100;
 30
 31   // 弃用: 用 ‘layer‘ 代替
 32
 33   repeated V1LayerParameter layers = 2;
 34
 35 }
 36
 37 // Specifies the shape (dimensions) of a Blob.
 38
 39 message BlobShape {
 40
 41   repeated int64 dim = 1 [packed = true];
 42
 43 }
 44
 45 message BlobProto {
 46
 47   optional BlobShape shape = 7;
 48
 49   repeated float data = 5 [packed = true];
 50
 51   repeated float diff = 6 [packed = true];
 52
 53   optional int32 num = 1 [default = 0];
 54
 55   optional int32 channels = 2 [default = 0];
 56
 57   optional int32 height = 3 [default = 0];
 58
 59   optional int32 width = 4 [default = 0];
 60
 61 }
 62
 63
 64
 65 // The BlobProtoVector is simply a way to pass multiple blobproto instances
 66
 67 around.
 68
 69 message BlobProtoVector {
 70
 71   repeated BlobProto blobs = 1;
 72
 73 }
 74
 75 message NetState {
 76
 77   optional Phase phase = 1 [default = TEST];
 78
 79   optional int32 level = 2 [default = 0];
 80
 81   repeated string stage = 3;
 82
 83 }
 84
 85 message LayerParameter {
 86
 87   optional string name = 1;   // the layer name
 88
 89   optional string type = 2;   // the layer type
 90
 91   repeated string bottom = 3; // the name of each bottom blob
 92
 93   repeated string top = 4;    // the name of each top blob
 94
 95   // The train/test phase for computation.
 96
 97   optional Phase phase = 10;
 98
 99   // Loss weight值:float
100
101   // 每一层为每一个top blob都分配了一个默认值,通常是0或1
102
103   repeated float loss_weight = 5;
104
105   // 指定的学习参数
106
107   repeated ParamSpec param = 6;
108
109   // The blobs containing the numeric parameters of the layer.
110
111   repeated BlobProto blobs = 7;
112
113   // included/excluded.
114
115   repeated NetStateRule include = 8;
116
117   repeated NetStateRule exclude = 9;
118
119   // Parameters for data pre-processing.
120
121   optional TransformationParameter transform_param = 100;
122
123   // Parameters shared by loss layers.
124
125   optional LossParameter loss_param = 101;
126
127   // 各种类型层参数
128
129   optional AccuracyParameter accuracy_param = 102;
130
131   optional ArgMaxParameter argmax_param = 103;
132
133   optional ConcatParameter concat_param = 104;
134
135   optional ContrastiveLossParameter contrastive_loss_param = 105;
136
137   optional ConvolutionParameter convolution_param = 106;
138
139   optional DataParameter data_param = 107;
140
141   optional DropoutParameter dropout_param = 108;
142
143   optional DummyDataParameter dummy_data_param = 109;
144
145   optional EltwiseParameter eltwise_param = 110;
146
147   optional ExpParameter exp_param = 111;
148
149   optional HDF5DataParameter hdf5_data_param = 112;
150
151   optional HDF5OutputParameter hdf5_output_param = 113;
152
153   optional HingeLossParameter hinge_loss_param = 114;
154
155   optional ImageDataParameter image_data_param = 115;
156
157   optional InfogainLossParameter infogain_loss_param = 116;
158
159   optional InnerProductParameter inner_product_param = 117;
160
161   optional LRNParameter lrn_param = 118;
162
163   optional MemoryDataParameter memory_data_param = 119;
164
165   optional MVNParameter mvn_param = 120;
166
167   optional PoolingParameter pooling_param = 121;
168
169   optional PowerParameter power_param = 122;
170
171   optional PythonParameter python_param = 130;
172
173   optional ReLUParameter relu_param = 123;
174
175   optional SigmoidParameter sigmoid_param = 124;
176
177   optional SoftmaxParameter softmax_param = 125;
178
179   optional SliceParameter slice_param = 126;
180
181   optional TanHParameter tanh_param = 127;
182
183   optional ThresholdParameter threshold_param = 128;
184
185   optional WindowDataParameter window_data_param = 129;
186
187 }

那么接下来的一段代码来演示如何解析Caffemodel,我解析用的model为MNIST手写库训练后的model,Lenet_iter_10000.caffemodel。

清单7. Caffemodel解析代码

 1  #include <stdio.h>
 2  #include <string.h>
 3  #include <fstream>
 4  #include <iostream>
 5  #include "proto/caffe.pb.h"
 6
 7  using namespace std;
 8  using namespace caffe;
 9
10  int main(int argc, char* argv[])
11  {
12
13   caffe::NetParameter msg;
14
15   fstream input("lenet_iter_10000.caffemodel", ios::in | ios::binary);
16   if (!msg.ParseFromIstream(&input))
17   {
18     cerr << "Failed to parse address book." << endl;
19     return -1;
20   }
21   printf("length = %d\n", length);
22   printf("Repeated Size = %d\n", msg.layer_size());
23
24   ::google::protobuf::RepeatedPtrField< LayerParameter >* layer = msg.mutable_layer();
25   ::google::protobuf::RepeatedPtrField< LayerParameter >::iterator it = layer->begin();
26   for (; it != layer->end(); ++it)
27   {
28     cout << it->name() << endl;
29     cout << it->type() << endl;
30     cout << it->convolution_param().weight_filler().max() << endl;
31   }
32
33   return 0;
34  }

参考网址:http://www.cnblogs.com/stephen-liu74/archive/2013/01/04/2842533.html

 

时间: 2024-10-06 13:49:41

Caffemodel解析的相关文章

Caffe中对MNIST执行train操作执行流程解析

之前在 http://blog.csdn.net/fengbingchun/article/details/49849225 中简单介绍过使用Caffe train MNIST的文章,当时只是仿照caffe中的example实现了下,下面说一下执行流程,并精简代码到仅有10余行: 1.        先注册所有层,执行layer_factory.hpp中类LayerRegisterer的构造函数,类LayerRegistry的AddCreator和Registry静态函数:关于Caffe中Lay

【转】Caffe初试(十)命令行解析

caffe的运行提供三种接口:C++接口(命令行).Python接口和matlab接口.本文先对命令行进行解析,后续会依次介绍其它两种接口. caffe的C++主程序(caffe.cpp)放在根目录下的tools文件夹内,当然还有一些其它的功能文件,如:convert_imageset.cpp,train_net.cpp,test_net.cpp等也放在这个文件夹内.经过编译后,这些文件都被编译成了可执行文件,放在了./build/tools/文件夹内.因此我们要执行caffe程序,都需要加./

Caffe学习系列(10):命令行解析

caffe的运行提供三种接口:c++接口(命令行).python接口和matlab接口.本文先对命令行进行解析,后续会依次介绍其它两个接口. caffe的c++主程序(caffe.cpp)放在根目录下的tools文件夹内, 当然还有一些其它的功能文件,如:convert_imageset.cpp, train_net.cpp, test_net.cpp等也放在这个文件夹内.经过编译后,这些文件都被编译成了可执行文件,放在了 ./build/tools/ 文件夹内.因此我们要执行caffe程序,都

faster-rcnn 之训练脚本解析:./tools/train_faster_rcnn_alt_opt.py

[说明]:欢迎加入:faster-rcnn 交流群 238138700,本文分析faster-rcnn 训练的python脚本: [debug]:我是把__main__修改了下,放在一个自己定义的函数里面,然后调用debug一步步看执行效果的,读者不妨也这样做,可以清晰看到程序是如何运行的: #!/usr/bin/env python # -------------------------------------------------------- # Faster R-CNN # Copyr

caffe(10) 命令行解析

caffe的运行提供三种接口:c++接口(命令行).python接口和matlab接口.本文先对命令行进行解析,后续会依次介绍其它两个接口. caffe的c++主程序(caffe.cpp)放在根目录下的tools文件夹内, 当然还有一些其它的功能文件,如:convert_imageset.cpp, train_net.cpp, test_net.cpp等也放在这个文件夹内.经过编译后,这些文件都被编译成了可执行文件,放在了 ./build/tools/ 文件夹内.因此我们要执行caffe程序,都

C++工程编译之“error LNK2001: 无法解析的外部符号”

今天一整天都在折腾“error LNK2001: 无法解析的外部符号”,就在头疼不已的时候,总算是找到问题原因了:各个动态链接库的编译方式必须统一才行,要不然很容易对库函数的引用产生冲突.简单来说就是,如果使用的第三方函数库编译方式采用/MD,那么主工程也应该使用/MD.我使用了libevent,而主工程默认采用/MT,所以需要忽略一大堆的函数库,我还纳闷呢,怎么会这么奇怪!!今天总算是解决了长久以来的困惑了. 下面引用一篇文章的描述:[Z]VC运行库版本不同导致链接.LIB静态库时发生重复定义

防止恶意解析——禁止通过IP直接访问网站

一.什么是恶意解析 一般情况下,要使域名能访问到网站需要两步,第一步,将域名解析到网站所在的主机,第二步,在web服务器中将域名与相应的网站绑定.但是,如果通过主机IP能直接访问某网站,那么把域名解析到这个IP也将能访问到该网站,而无需在主机上绑定,也就是说任何人将任何域名解析到这个IP就能访问到这个网站.可能您并不介意通过别人的域名访问到您的网站,但是如果这个域名是未备案域名呢?一旦被查出,封IP.拔线甚至罚款的后果都是需要您来承担的.某些别有用心的人,通过将未备案域名解析到别人的主机上,使其

.NET深入解析LINQ框架(五:IQueryable、IQueryProvider接口详解)

阅读目录: 1.环路执行对象模型.碎片化执行模型(假递归式调用) 2.N层对象执行模型(纵横向对比链式扩展方法) 3.LINQ查询表达式和链式查询方法其实都是空壳子 4.详细的对象结构图(对象的执行原理) 5.IQueryable<T>与IQueryProvider一对一的关系能否改成一对多的关系 6.完整的自定义查询 1]. 环路执行对象模型.碎片化执行模型(假递归式调用) 这个主题扯的可能有点远,但是它关系着整个LINQ框架的设计结构,至少在我还没有搞懂LINQ的本意之前,在我脑海里一直频

.NET深入解析LINQ框架(一:LINQ优雅的前奏)

阅读目录: 1.LINQ简述 2.LINQ优雅前奏的音符 2.1.隐式类型 (由编辑器自动根据表达式推断出对象的最终类型) 2.2.对象初始化器 (简化了对象的创建及初始化的过程) 2.3.Lambda表达式 (对匿名方法的改进,加入了委托签名的类型推断并很好的与表达式树的结合) 2.4.扩展方法 (允许在不修改类型的内部代码的情况下为类型添加独立的行为) 2.5.匿名类型 (由对象初始化器推断得出的类型,该类型在编译后自动创建) 2.6.表达式目录树(用数据结构表示程序逻辑代码) 3.LINQ