1. 程式人生 > >學習Caffe(二)使用Caffe:Caffe載入模型+Caffe新增新層+Caffe finetune

學習Caffe(二)使用Caffe:Caffe載入模型+Caffe新增新層+Caffe finetune

如何使用Caffe

預備知識

Google Protocol Buffer

https://developers.google.com/protocol-buffers/docs/cpptutorial
Caffe資料的讀取、運算、儲存都是採用Google Protocol Buffer來進行的。PB是一種輕便、高效的結構化資料儲存格式,可以用於結構化資料序列化,很適合做資料儲存或 RPC 資料交換格式。它可用於通訊協議、資料儲存等領域的語言無關、平臺無關、可擴充套件的序列化結構資料格式。是一種效率和相容性都很優秀的二進位制資料傳輸格式,目前提供了 C++、Java、Python 三種語言的 API。Caffe採用的是C++和Python的API。

初始化網路

#include "caffe/caffe.hpp"
#include <string>
#include <vector>
using namespace caffe;

char *proto = "H:\\Models\\Caffe\\deploy.prototxt"; /* 載入CaffeNet的配置 */
Phase phase = TEST; /* or TRAIN */
Caffe::set_mode(Caffe::CPU);
// Caffe::set_mode(Caffe::GPU);
// Caffe::SetDevice(0);

//! Note: 後文所有提到的net,都是這個net
boost::shared_ptr< Net<float> > net(new caffe::Net<float>(proto, phase));

載入已訓練好的模型

char *model = "H:\\Models\\Caffe\\bvlc_reference_caffenet.caffemodel";    
net->CopyTrainedLayersFrom(model);

讀取模型中的每層的結構配置引數

char *model = "H:\\Models\\Caffe\\bvlc_reference_caffenet.caffemodel";
NetParameter param;
ReadNetParamsFromBinaryFileOrDie(model, &param);
int num_layers = param.layer_size();
for (int i = 0; i < num_layers; ++i)
{
    // 結構配置引數:name,type,kernel size,pad,stride等
    LOG(ERROR) << "Layer " << i << ":" << param.layer(i).name() << "\t" << param.layer(i).type();
    if (param.layer(i).type() == "Convolution")
    {
        ConvolutionParameter conv_param = param.layer(i).convolution_param();
        LOG(ERROR) << "\t\tkernel size: " << conv_param.kernel_size()
            << ", pad: " << conv_param.pad()
            << ", stride: " << conv_param.stride();
    }
}

讀取影象均值

char *mean_file = "H:\\Models\\Caffe\\imagenet_mean.binaryproto";
Blob<float> image_mean;
BlobProto blob_proto;
const float *mean_ptr;
unsigned int num_pixel;

bool succeed = ReadProtoFromBinaryFile(mean_file, &blob_proto);
if (succeed)
{
    image_mean.FromProto(blob_proto);
    num_pixel = image_mean.count(); /* NCHW=1x3x256x256=196608 */
    mean_ptr = (const float *) image_mean.cpu_data();
}

根據指定資料,前向傳播網路

//! Note: data_ptr指向已經處理好(去均值的,符合網路輸入影象的長寬和Batch Size)的資料
void caffe_forward(boost::shared_ptr< Net<float> > & net, float *data_ptr)
{
    Blob<float>* input_blobs = net->input_blobs()[0];
    switch (Caffe::mode())
    {
    case Caffe::CPU:
        memcpy(input_blobs->mutable_cpu_data(), data_ptr,
            sizeof(float) * input_blobs->count());
        break;
    case Caffe::GPU:
        cudaMemcpy(input_blobs->mutable_gpu_data(), data_ptr,
            sizeof(float) * input_blobs->count(), cudaMemcpyHostToDevice);
        break;
    default:
        LOG(FATAL) << "Unknown Caffe mode.";
    } 
    net->ForwardPrefilled();
}

根據Feature層的名字獲取其在網路中的Index

//! Note: Net的Blob是指,每個層的輸出資料,即Feature Maps
// char *query_blob_name = "conv1";
unsigned int get_blob_index(boost::shared_ptr< Net<float> > & net, char *query_blob_name)
{
    std::string str_query(query_blob_name);    
    vector< string > const & blob_names = net->blob_names();
    for( unsigned int i = 0; i != blob_names.size(); ++i ) 
    { 
        if( str_query == blob_names[i] ) 
        { 
            return i;
        } 
    }
    LOG(FATAL) << "Unknown blob name: " << str_query;
}

讀取網路指定Feature層資料

//! Note: 根據CaffeNet的deploy.prototxt檔案,該Net共有15個Blob,從data一直到prob    
char *query_blob_name = "conv1"; /* data, conv1, pool1, norm1, fc6, prob, etc */
unsigned int blob_id = get_blob_index(net, query_blob_name);

boost::shared_ptr<Blob<float> > blob = net->blobs()[blob_id];
unsigned int num_data = blob->count(); /* NCHW=10x96x55x55 */
const float *blob_ptr = (const float *) blob->cpu_data();

根據檔案列表,獲取特徵,並存為二進位制檔案

詳見get_features.cpp檔案:

主要包括三個步驟
- 生成檔案列表,格式與訓練用的類似,每行一個影象
包括檔案全路徑、空格、標籤(沒有的話,可以置0)
- 根據train_val或者deploy的prototxt,改寫生成feat.prototxt
主要是將輸入層改為image_data層,最後加上prob和argmax(為了輸出概率和Top1/5預測標籤)
- 根據指定引數,執行程式後會生成若干個二進位制檔案,可以用MATLAB讀取資料,進行分析

根據Layer的名字獲取其在網路中的Index

//! Note: Layer包括神經網路所有層,比如,CaffeNet共有23層
// char *query_layer_name = "conv1";
unsigned int get_layer_index(boost::shared_ptr< Net<float> > & net, char *query_layer_name)
{
    std::string str_query(query_layer_name);    
    vector< string > const & layer_names = net->layer_names();
    for( unsigned int i = 0; i != layer_names.size(); ++i ) 
    { 
        if( str_query == layer_names[i] ) 
        { 
            return i;
        } 
    }
    LOG(FATAL) << "Unknown layer name: " << str_query;
}

讀取指定Layer的權重資料

//! Note: 不同於Net的Blob是Feature Maps,Layer的Blob是指Conv和FC等層的Weight和Bias
char *query_layer_name = "conv1";
const float *weight_ptr, *bias_ptr;
unsigned int layer_id = get_layer_index(net, query_layer_name);
boost::shared_ptr<Layer<float> > layer = net->layers()[layer_id];
std::vector<boost::shared_ptr<Blob<float>  >> blobs = layer->blobs();
if (blobs.size() > 0)
{
    weight_ptr = (const float *) blobs[0]->cpu_data();
    bias_ptr = (const float *) blobs[1]->cpu_data();
}

//! Note: 訓練模式下,讀取指定Layer的梯度資料,與此相似,唯一的區別是將cpu_data改為cpu_diff

修改某層的Weight資料

const float* data_ptr;          /* 指向待寫入資料的指標, 源資料指標*/
float* weight_ptr = NULL;       /* 指向網路中某層權重的指標,目標資料指標*/
unsigned int data_size;         /* 待寫入的資料量 */
char *layer_name = "conv1";     /* 需要修改的Layer名字 */

unsigned int layer_id = get_layer_index(net, query_layer_name);    
boost::shared_ptr<Blob<float> > blob = net->layers()[layer_id]->blobs()[0];

CHECK(data_size == blob->count());
switch (Caffe::mode())
{
case Caffe::CPU:
    weight_ptr = blob->mutable_cpu_data();
    break;
case Caffe::GPU:
    weight_ptr = blob->mutable_gpu_data();
    break;
default:
    LOG(FATAL) << "Unknown Caffe mode";
}
caffe_copy(blob->count(), data_ptr, weight_ptr);

//! Note: 訓練模式下,手動修改指定Layer的梯度資料,與此相似
// mutable_cpu_data改為mutable_cpu_diff,mutable_gpu_data改為mutable_gpu_diff

儲存新的模型

char* weights_file = "bvlc_reference_caffenet_new.caffemodel";
NetParameter net_param;
net->ToProto(&net_param, false);
WriteProtoToBinaryFile(net_param, weights_file);

Caffe中新增新的層

用預訓練網路引數初始化

caffe的引數初始化是根據名字從caffemodel讀取的,只要修改名字,自己想要修改的層就能隨機初始化。

  • 修改名字,保留前面幾層的引數,同時後面的引數設定較高的學習率,基礎學習率大概0.00001左右。