caffemodel之庖丁解牛

时间:2022-10-24 18:01:38

        很多时候算法工程师把精力更多的聚焦于当下最流行的框架。一旦出现了一些各个式样的流行框架,便迫不急待的在自己的数据集或者公共的数据集上跑。然后生成模型来验证准确率与检测速率。如果以上两者不达标便会修改数据集或者对模型调整参数。导致时下有些段子就说:“优秀的调参侠就是好的算法工程师”。暂且不论这个观点是否正确,很多时候整个深度学习过程就是这样的。

        这两天突然很好奇,我用CAFFE生成的caffemodel里面到底是些什么数据。现在有很多方法可以让模型可视化。我在网上搜索了一些方法,并且做也一点实践工作,在此时写下来供大家参考。(下面的有些部分参考了其他人的博客)


Classifier类的构造函数的时候,有一个net_指针,并且执行了一个操作:

  1. net_->CopyTrainedLayersFrom(trained_file);  

   这个CopyTrainedLayersFrom函数就是从caffemodel里面载入我们需要的参数了。然后笔者就从这个函数开始挖,进入net.cpp文件:
  1. template <typename Dtype>  
  2. void Net<Dtype>::CopyTrainedLayersFrom(const string trained_filename) {  
  3.   if (H5Fis_hdf5(trained_filename.c_str())) {  
  4.     CopyTrainedLayersFromHDF5(trained_filename);  
  5.   } else {  
  6.     CopyTrainedLayersFromBinaryProto(trained_filename);  
  7.   }  
  8. }  

   在这里很明显执行了else下面的语句,从二进制文件中去读取了参数,然后,笔者又找到了CopyTrainedLayersFromBinaryProto函数,很巧就在CopyTrainedLayersFrom函数的下方:
  1. template <typename Dtype>  
  2. void Net<Dtype>::CopyTrainedLayersFromBinaryProto(  
  3.     const string trained_filename) {  
  4.   NetParameter param;  
  5.   ReadNetParamsFromBinaryFileOrDie(trained_filename, &parm);  
  6.   CopyTrainedLayersFrom(param);  
  7. }  

   在里面首先执行了一个ReadNetParamsFromBinaryFileOrDie函数,把二进制文件中的参数读到parm里面,parm是一个NetParameter类型的,NetParameter继承了实际是一个Message,Message是proto类型的,详细的笔者后话解析。然后笔者去找了一下ReadNetParamsFromBinaryFileOrDie函数,这个函数在upgrade_proto.cpp里面:

 

  1. void ReadNetParamsFromBinaryFileOrDie(const string& param_file,  
  2.                                       NetParameter* param) {  
  3.   CHECK(ReadProtoFromBinaryFile(param_file, param))  
  4.       << "Failed to parse NetParameter file: " << param_file;  
  5.   UpgradeNetAsNeeded(param_file, param);  
  6. }  
   这个函数完成的功能是首先进行ReadProtoFromBinaryFile,然后执行了一个更新的操作。然后笔者就去找这个ReadProtoFromBinaryFile函数,这个函数在io.cpp里面:
  1. bool ReadProtoFromBinaryFile(const char* filename, Message* proto) {  
  2.   int fd = open(filename, O_RDONLY);  
  3.   CHECK_NE(fd, -1) << "File not found: " << filename;  
  4.   ZeroCopyInputStream* raw_input = new FileInputStream(fd);  
  5.   CodedInputStream* coded_input = new CodedInputStream(raw_input);  
  6.   coded_input->SetTotalBytesLimit(kProtoReadBytesLimit, 536870912);  
  7.   
  8.   bool success = proto->ParseFromCodedStream(coded_input);  
  9.   
  10.   delete coded_input;  
  11.   delete raw_input;  
  12.   close(fd);  
  13.   return success;  
  14. }  

   这个函数就比较底层了,读者朋友们可以看到,这个函数里面就使用了open函数,和一些底层的google::protobuf的数据流。在这里我们其实就明白,是先把二进制文件(caffemodel)转化成文件流,再放入proto里面。

   那么,笔者大胆猜想,能读就能写。

   其实io.cpp里面已经定义了这种接口:

  1. void WriteProtoToTextFile(const Message& proto, const char* filename) {  
  2.   int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);  
  3.   FileOutputStream* output = new FileOutputStream(fd);  
  4.   CHECK(google::protobuf::TextFormat::Print(proto, output));  
  5.   delete output;  
  6.   close(fd);  
  7. }  

   也就是说:先用ReadProtoFromBinaryFile函数将二进制文件读入proto里面,再将proto文件读入txt文件就行了。


如果大家要建立一个工程的话,下面会有几个文件请放在一个文件夹中:

 
    #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("../examples/mnist/lenet_iter_10000.caffemodel", &proto);  
       ReadProtoFromBinaryFile("/home/zy/zouyu/deeping/caffe/models/VGGNet/KITTI/SSD_300x300/KITTI_SSD_300x300_iter_80000.caffemodel", &proto);
       //把这个CAFFEMODEL通过这个函数读到proto中来。
       WriteProtoToTextFile(proto, "./test.txt");//把proto在上一个函数中读到的二进制流写到test.txt中。
        return 0;
    }
~                                                                                                                                                 
~                                                                                                                                                 
~                                                                                                                                                 
~                                                                                                                                                 
~            


其对应的CMakeLists.txt文件为:

    cmake_minimum_required (VERSION 2.8)

    project (pt_test)

    add_executable(pt_test pt.cpp)

    include_directories ( /home/zy/zouyu/deeping/caffe/include  
        /usr/local/include  
        /usr/local/cuda/include  
        /usr/include )
               //请确保你的头文件与链接库存的文件路径正确。
    target_link_libraries(pt_test  
        /home/zy/zouyu/deeping/caffe/build/lib/libcaffe.so  
        /usr/lib/x86_64-linux-gnu/libglog.so  
        /usr/lib/x86_64-linux-gnu/libboost_system.so 

接下来就是编译的过程了:

caffemodel之庖丁解牛

        执行上面的cmake .         &&      make    后会生成一个pt_test可执行文件。当你执行过后,会生成test.txt。当你打开test.txt后,你会惊喜的发现,我原来训练的模型的相应参数都在这了。

        在这个时候我的脑海中闪现一幅画面,看到了模型训练过程中图像从前端进去,经过各个卷积层、池化层、全连接层,在这一过程中利用递度下降等理论求解最小化的损失函数。最后网络停止更新,固化下来。生成了相应的MODEL,而整个训练集的DNA被以相应的形式记录了下来。如果后续要是有测试图片过来,就可以做相应的分类等工作了。

        突然想到了《老子》中的一句话:“玄之又玄,众妙之门”。


相应的文件请点击如下链接:

     look_caffemodell