本文主要讲解protobuf的基本用法(caffe使用protobuf所用到的语法)
protobuf简介
protobuf功能是把某种数据结构的信息以某种格式保存起来。它主要用于文件存储以及传输协议格式等场合。protobuf的优点主要有
- 性能好/效率高
- 向前兼容和向前兼容
- 支持多种语言
protobuf简单用法
假如我们有一个问题是关于:存储一个人的名字(name)以及唯一表示符(id)和邮箱(email)以及它的电话号码(number)和此电话号码所在的类型(PhoneType)。并且需要将其保存在二进制文件中或者txt文件中,如果需要还需要将其从二进制文件中或者txt文件中读取,我们如何使用protobuf去实现它呢?
- 首先我们需要创建一个caffe.proto文件,文件中的内容为:
#caffe.proto
syntax = "proto2";
package caffe;
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
syntax声明使用的语法规则,proto2相当于python3和python2的区别,所以为了正确解析,我们需要声明正确的proto版本。
message表示消息,相当于c++中的一个类Person。required表示必须字段,而optional表示可选字段,repeated表示可重复字段(每一个人都有多个电话号码),这些关键字紧接着的关键字(string int32 etc.)为我们声明的变量的类型。
- 第二部需要创建一个主函数(main.cpp),主要功能是演示序列化一个Person类,并且写入文件中,main.cpp文件的主要内容为:
#include <iostream>
#include <string>
#include "caffe.pb.h"
#include <google/protobuf/io/zero_copy_stream_impl.h>
#include <glog/logging.h>
#include <google/protobuf/io/coded_stream.h>
#include <google/protobuf/io/zero_copy_stream_impl.h>
#include <google/protobuf/text_format.h>
#include <fcntl.h>
#include <unistd.h>
#include <fstream>
const int kProtoReadBytesLimit = INT_MAX;
using google::protobuf::io::FileInputStream;
using google::protobuf::io::FileOutputStream;
using google::protobuf::io::ZeroCopyInputStream;
using google::protobuf::io::CodedInputStream;
using google::protobuf::io::ZeroCopyOutputStream;
using google::protobuf::io::CodedOutputStream;
using google::protobuf::Message;
using namespace std;
//从txt文件中读取proto消息函数 成功返回true
bool ReadProtoFromTextFile(const char* filename, Message* proto) {
//以只读方式(O_RDONLY)打开filename文件
int fd = open(filename, O_RDONLY);
//使用glog库中的CHECK_NE函数检查是否读取文件成功
CHECK_NE(fd, -1) << "File not found: " << filename;
//使用FileInputStream流读取文件
FileInputStream* input = new FileInputStream(fd);
//使用Text格式解析input输入,并将读取到的数据赋值到proto上
bool success = google::protobuf::TextFormat::Parse(input, proto);
//释放input对象
delete input;
//关闭stream流
close(fd);
//返回是否成功
return success;
}
//将proto消息写入txt文件中
void WriteProtoToTextFile(const Message& proto, const char* filename) {
//0644表示文件的权限
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
FileOutputStream* output = new FileOutputStream(fd);
CHECK(google::protobuf::TextFormat::Print(proto, output));
delete output;
close(fd);
}
//从二进制文件中读取proto消息函数 成功返回true
bool ReadProtoFromBinaryFile(const char* filename, Message* proto) {
int fd = open(filename, O_RDONLY);
CHECK_NE(fd, -1) << "File not found: " << filename;
ZeroCopyInputStream* raw_input = new FileInputStream(fd);
CodedInputStream* coded_input = new CodedInputStream(raw_input);
//proto文件读取二进制默认最大大小为64M,所以我们需要设置通过SetTotalBytesLimit函数设置一下最大值,其中kProtoReadBytesLimit设置上限,而第二个参数536870912设置读取文件超过这个值会产生警报
coded_input->SetTotalBytesLimit(kProtoReadBytesLimit, 536870912);
bool success = proto->ParseFromCodedStream(coded_input);
delete coded_input;
delete raw_input;
close(fd);
return success;
}
//向二进制文件中写入proto消息函数
void WriteProtoToBinaryFile(const Message& proto, const char* filename) {
fstream output(filename, ios::out | ios::trunc | ios::binary);
CHECK(proto.SerializeToOstream(&output));
}
int main(int argc, char* argv[]) {
GOOGLE_PROTOBUF_VERIFY_VERSION;
//调用caffe.pb.h中的Person类,并设置id以及name,通过调用add_phone()来添加number
caffe::Person person;
person.set_id(10421);
person.set_name("hopo");
person.set_email("2381892713@qq.com");
//添加person中的phone 默认类型为HOME
caffe::Person::PhoneNumber* phonenumber = person.add_phone();
phonenumber->set_number("17601001773");
phonenumber = person.add_phone();
phonenumber->set_number("13261009856");
phonenumber->set_type(caffe::Person::PhoneType::Person_PhoneType_WORK);
phonenumber = person.add_phone();
phonenumber->set_number("15660195460");
phonenumber->set_type(caffe::Person::PhoneType::Person_PhoneType_MOBILE);
WriteProtoToTextFile(person, "../re.txt");
string serializedStr;
//序列化person到serializedStr中
person.SerializeToString(&serializedStr);
cout<<"serialization result: "<<serializedStr<<endl;
cout<<endl<<"debugString: "<<person.DebugString();
//通过serializedStr反序列化
//从serializedStr中读取person
caffe::Person deserializedPerson;
if(!deserializedPerson.ParseFromString(serializedStr)) {
cerr << "Failed to parse student." <<endl;
return -1;
}
cout<<"person id: "<<deserializedPerson.id()<<endl;
cout<<"person name: "<<deserializedPerson.name()<<endl;
cout<<"person email: "<<deserializedPerson.email()<<endl;
for (int i = 0; i < deserializedPerson.phone_size(); i++){
const caffe::Person_PhoneNumber phone_number = deserializedPerson.phone(i);
switch (phone_number.type()) {
case caffe::Person::HOME:
cout<<"Home phone #: ";
break;
case caffe::Person::WORK:
cout<<"Work #: ";
break;
case caffe::Person::MOBILE:
cout<<"Mobile #: ";
break;
}
cout<<phone_number.number()<<endl;
}
google::protobuf::ShutdownProtobufLibrary();
}
在main.cpp文件中,我们通过ReadProtoFromTextFile(),WriteProtoToTextFile(),ReadProtoFromBinaryFile(), WriteProtoToBinaryFile()四个函数分别实现的是从txt中读取, 写入txt文件,从二进制文件中读取,写入二进制文件中。
- cmake文件通过caffe.protof文件生成caffe.pb.h和caffe.pb.cc文件,CMakeLists.txt文件内容如下:
cmake_minimum_required(VERSION 3.12)
project(protobuf)
set(CMAKE_CXX_STANDARD 14)
#寻找Protobuf库文件
find_package(Protobuf REQUIRED)
include_directories(${PROTOBUF_INCLUDE_DIRS})
include_directories(${CMAKE_CURRENT_BINARY_DIR})
#通过student.proto文件分别
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS caffe.proto)
add_executable(protobuf main.cpp ${PROTO_SRCS} ${PROTO_HDRS})
target_link_libraries(protobuf glog ${PROTOBUF_LIBRARIES})
其中的protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS caffe.proto)将通过caffe.protof文件生成caffe.pb.h和caffe.pb.cc,生成文件的路径为运行CMakeLists.txt文件所在文件夹。由于我使用的为CLion,最终生成的文件路径在cmake-build-debug文件夹下,所以我需要包含CMAKE_CURRENT_BINARY_DIR所指向的路径,通过include_directories(${CMAKE_CURRENT_BINARY_DIR})包含。同时PROTO_SRCS和PROTO_HDRS变量分别表示生成的头文件以及源文件,也就是caffe.pb.cc和caffe.pb.h文件
总结
学习protobuf主要原因是学习caffe源码,在caffe中,主要通过protobuf读取模型以及写入模型,所以仅仅需要知道protobuf的基本用法。而上述函数中的ReadProtoFromTextFile()函数是直接从caffe源码中copy过来的,如果你想将caffe中的caffemodel文件转换成可读文件,可以通过ReadProtoFromBinaryFile()从.caffemodel读取文件,然后通过WriteProtoToTextFile()函数写入到txt文件中,最终打开生成txt文件我们就可以看到可视化的网络模型。可以发现caffemode仅仅比deploy.txt网络多出来网络中的参数。
下面是实现读取caffemodel文件,然后写入txt文件中的具体过程:
我们重新创建一个main.cpp文件,文件内容如下:
#include <caffe/caffe.hpp>
#include <google/protobuf/io/coded_stream.h>
#include <google/protobuf/io/zero_copy_stream_impl.h>
#include <google/protobuf/text_format.h>
#include <algorithm>
#include <iosfwd>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <iostream>
#include "caffe/common.hpp"
#include "caffe/proto/caffe.pb.h"
#include "caffe/util/io.hpp"
using namespace caffe;
using namespace std;
using google::protobuf::io::FileInputStream;
using google::protobuf::io::FileOutputStream;
using google::protobuf::io::ZeroCopyInputStream;
using google::protobuf::io::CodedInputStream;
using google::protobuf::io::ZeroCopyOutputStream;
using google::protobuf::io::CodedOutputStream;
using google::protobuf::Message;
int main()
{
//NetParameter为caffe目录下caffe.pb.h文件中的
NetParameter proto;
ReadProtoFromBinaryFile("/home/cvlab/files/caffe-master/data/mnist/lenet_iter_10000.caffemodel", &proto);
WriteProtoToTextFile(proto, "/home/cvlab/files/caffe-master/data/mnist/test.txt");
return 0;
}
需要注意的是:传入函数中的参数为&proto,但是proto是一个抽象类,所以我们需要通过声明一个NetParameter来传入到函数中,此参数在caffe/proto/caffe.pb.h文件中,然后在CMakeLists.txt中需要声明包含caffe目录caffe.pb.h所在的目录。最终重新编译运行,即可获得最终的结果。