转自:https://blog.csdn.net/jiongnima/article/details/72904526
在本文正式开始之前,笔者要先和各位读者朋友们道个歉。因为身为研一小白的笔者实在身不由己,除了各种任务之外,还要应付繁忙的课程,忙于各种考试像一只咸鱼。因此耽误了博文的撰写,对不起各位读者朋友,笔者在忙完6月进入研二之后一定再接再厉。下面开始干货~
本篇还是一个插播的博客,旨在向大家分享caffemodel里面记录的信息。在我们使用caffe框架训练网络时,最终总会生成一个caffemodel,我们大概知道里面记录了模型的参数。而作为初学者的笔者,总是不分青红皂白地就直接在模型的验证阶段就调用了。从来没有去看过caffemodel里面到底讲了什么,那么,本篇中笔者就解析一下caffemodel。
有读者朋友未免会问,笔者是出于什么目的想解析caffemodel的呢?这说来话长,是笔者最近做的项目需要将model拆开并进行改动,因此需要去解析caffemodel。那么笔者是怎么摸索的呢?各位读者朋友们是否还记得,笔者前两期的博客里面提及的caffe官方提供的classification.cpp文件,里面在执行Classifier类的构造函数的时候,有一个net_指针,并且执行了一个操作:
net_->CopyTrainedLayersFrom(trained_file);
这个CopyTrainedLayersFrom函数就是从caffemodel里面载入我们需要的参数了。然后笔者就从这个函数开始挖,进入net.cpp文件:
template <typename Dtype>
void Net<Dtype>::CopyTrainedLayersFrom(const string trained_filename) {
if (H5Fis_hdf5(trained_filename.c_str())) {
CopyTrainedLayersFromHDF5(trained_filename);
} else {
CopyTrainedLayersFromBinaryProto(trained_filename);
}
}
在这里很明显执行了else下面的语句,从二进制文件中去读取了参数,然后,笔者又找到了CopyTrainedLayersFromBinaryProto函数,很巧就在CopyTrainedLayersFrom函数的下方:
template <typename Dtype>
void Net<Dtype>::CopyTrainedLayersFromBinaryProto(
const string trained_filename) {
NetParameter param;
ReadNetParamsFromBinaryFileOrDie(trained_filename, &parm);
CopyTrainedLayersFrom(param);
}
在里面首先执行了一个ReadNetParamsFromBinaryFileOrDie函数,把二进制文件中的参数读到parm里面,parm是一个NetParameter类型的,NetParameter继承了实际是一个Message,Message是proto类型的,详细的笔者后话解析。然后笔者去找了一下ReadNetParamsFromBinaryFileOrDie函数,这个函数在upgrade_proto.cpp里面:
void ReadNetParamsFromBinaryFileOrDie(const string& param_file,
NetParameter* param) {
CHECK(ReadProtoFromBinaryFile(param_file, param))
<< "Failed to parse NetParameter file: " << param_file;
UpgradeNetAsNeeded(param_file, param);
}
这个函数完成的功能是首先进行ReadProtoFromBinaryFile,然后执行了一个更新的操作。然后笔者就去找这个ReadProtoFromBinaryFile函数,这个函数在io.cpp里面:
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);
coded_input->SetTotalBytesLimit(kProtoReadBytesLimit, 536870912);
bool success = proto->ParseFromCodedStream(coded_input);
delete coded_input;
delete raw_input;
close(fd);
return success;
}
这个函数就比较底层了,读者朋友们可以看到,这个函数里面就使用了open函数,和一些底层的google::protobuf的数据流。在这里我们其实就明白,是先把二进制文件(caffemodel)转化成文件流,再放入proto里面。
那么,笔者大胆猜想,能读就能写。
其实io.cpp里面已经定义了这种接口:
void WriteProtoToTextFile(const Message& proto, const char* filename) {
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);
}
也就是说:先用ReadProtoFromBinaryFile函数将二进制文件读入proto里面,再将proto文件读入txt文件就行了。
笔者迫不及待地拿lenet网络训练生成的caffemodel做了个测试:
#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 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;
}
笔者训练了一个lenet训练生成的二进制文件,并将其写入了一个名为test.txt的文件中。
其对应的CMakeLists.txt文件为:
cmake_minimum_required (VERSION 2.8)
project (pt_test)
add_executable(pt_test pt.cpp)
include_directories ( /home/cvlab/files/caffe-master/include
/usr/local/include
/usr/local/cuda/include
/usr/include )
target_link_libraries(pt_test
/home/cvlab/files/caffe-master/build/lib/libcaffe.so
/usr/lib/x86_64-linux-gnu/libglog.so
/usr/lib/x86_64-linux-gnu/libboost_system.so
)
编译执行:
然后我们打开test.txt文件可以见到:
一共43万多行,记录了lenet-5的网络参数。因为lenet-5使用了全连接层,因此参数规模是庞大的(全连接层的参数约占了总体参数规模的90%)。
同时也可以看到,读出的二进制文件中参数规格是按照caffe.proto中协定的格式来的。
到此,我们就能清晰地看到caffemodel中记载的数据和格式了。
欢迎阅读笔者后续博客,各位读者朋友的支持与鼓励是我最大的动力!
written by jiong
故上兵伐谋,其次伐交,其次伐兵,其下攻城;攻城之法为不得已。
————————————————
版权声明:本文为CSDN博主「jiongnima」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/jiongnima/article/details/72904526