天天看點

Caffe2——cifar10資料集建立lmdb或leveldb類型的資料

cifar10資料集和mnist資料集存儲方式不同,cifar10資料集把标簽和圖像資料以bin檔案的方式存放在同一個檔案内,這種存放方式使得每個子cifar資料bin檔案的結構相同,是以cifar轉換資料代碼比mnist的代碼更加的子產品化,分為源資料讀取子產品(image_read函數),把lmdb(leveldb)資料轉換的變量聲明,句柄(函數)調用都放到定義的caffe::db子空間中,這樣簡化了代碼,而且使得代碼更加清晰。

一:程式開始

和轉換mnist資料不同的是,cifar并沒有使用gflags指令行解析工具;是以也沒有通過gflags的宏定義來指定要轉換的資料類型,而是把轉換的類型參數直接作為main()函數的參數(這種方式便于了解)。

在Create.sh檔案中,調用convert_cifar_data.bin語句為:

./build/examples/cifar10/convert_cifar_data.bin$DATA $EXAMPLE $DBTYPE

convert_cifar_data.bin程式,程式需要3個參數,分别為源資料路徑,lmdb(leveldb)存儲路徑,要轉換的資料類型lmdb or leveldb

二:資料轉換流程圖

Caffe2——cifar10資料集建立lmdb或leveldb類型的資料

三:convert_cifar_data.cpp函數分析

1引入必要的頭檔案和命名空間

#include <fstream> 
#include <string>
#include "boost/scoped_ptr.hpp"
#include "glog/logging.h"
#include "google/protobuf/text_format.h"
#include "stdint.h"
#include "caffe/proto/caffe.pb.h"
#include "caffe/util/db.hpp"
           

頭檔案和convert_mnist_data.cpp的差別:

1,沒有引入gflags指令行解析工具;

2,沒有引入leveldb和lmdb的資料頭檔案

3,引入了"boost/scoped_ptr.hpp"智能指針頭檔案

4,引入"caffe/util/db.hpp"頭檔案,裡面包裝了對lmdb和leveldb資料對象的操作内容

using caffe::Datum;
using boost::scoped_ptr;
using std::string;
namespace db = caffe::db;
           

命名空間差別:

1,沒有引入全部caffe命名空間,而是局部引入了兩個caffe命名空間下的子空間 caffe::Datum和caffe::db

2,引入boost::scoped_ptr;智能指針命名空間,智能指針,它能夠保證在離開作用域後對象被自動釋放;在mnist資料轉換代碼中,經常出現delete batch等删除臨時變量的指令,通過智能指針可以自動删除過期的變量,對于控制程式記憶體占用很實用。

2 main()函數

接收參數,調用轉換函數convet_dataset()

3 convet_dataset()函數

3.1以智能指針的方式建立db::DB類型的對象 train_db

scoped_ptr<db::DB>  train_db(db::GetDB(db_type));
           

智能指針的建立方式類似泛型的格式,上面通過db.cpp内定義的命名的子命名空間中db的“成員函數”GetDB函數來初始化train_db對象

3.2 建立lmdb資料對象

3.2.1建立環境;設定環境參數,打開環境

調用tran_db對象的open方法,以“對db::NEW的方式,建立lmdb(leveldb)類型檔案

train_db->Open(output_folder+ "/cifar10_train_" + db_type,db::NEW);
           

db命名空間中open函數具體實作代碼:

void LMDB::Open(const string& source, Mode mode) {
  MDB_CHECK(mdb_env_create(&mdb_env_));//建立lmdb操作環境
 MDB_CHECK(mdb_env_set_mapsize(mdb_env_, LMDB_MAP_SIZE));//設定環境内訓映射
  if (mode == NEW) {
    CHECK_EQ(mkdir(source.c_str(),0744), 0) << "mkdir " << source <<"failed";
  }//檢查檔案
  int flags = 0;
  if (mode == READ) {
    flags = MDB_RDONLY | MDB_NOTLS;
  }
  MDB_CHECK(mdb_env_open(mdb_env_,source.c_str(), flags, 0664));//打開建立的環境
  LOG(INFO) << "Openedlmdb " << source;
}
           

3.2.2建立并打開transaction操作句柄,打開資料庫句柄

調用db命名空間中的Transaction方法,來建立句柄對象txn

scoped_ptr<db::Transaction>  txn(train_db->NewTransaction());

db命名空間中NewTransaction()函數代碼

//在lmdb環境中建立操作句柄

LMDBTransaction* LMDB::NewTransaction() {
  MDB_txn* mdb_txn;
  MDB_CHECK(mdb_txn_begin(mdb_env_,NULL, 0, &mdb_txn));//建立操作句柄
  MDB_CHECK(mdb_dbi_open(mdb_txn,NULL, 0, &mdb_dbi_));//打開資料庫環境
  return new LMDBTransaction(&mdb_dbi_,mdb_txn);
}
           

3.3 定義資料結構檔案

const int kCIFARSize =32;
const intkCIFARImageNBytes = 3072; //32*32=1024,RGB各占一個位元組,感覺應該為uint8_t,0~255,
const intkCIFARBatchSize = 10000; //cifar共計5萬個訓練樣本,分成5份batches,每份1萬個
const int kCIFARTrainBatches= 5;
  // Data buffer
  int label;
  charstr_buffer[kCIFARImageNBytes]; //定義字元數組,一個數組可以存放一張圖檔的資料
  Datum datum;
  datum.set_channels(3);
  datum.set_height(kCIFARSize);
  datum.set_width(kCIFARSize);
           

3.4 打開源資料檔案

下載下傳的Cifar資料存放在6個bin檔案内,從data_batch_1.bin到data_batch_5.bin;本文以循環的方式分别讀取每個bin檔案。每個bin檔案存儲1萬張圖檔

for (int fileid = 0;fileid < kCIFARTrainBatches; ++fileid) {
snprintf(str_buffer, kCIFARImageNBytes, "/data_batch_%d.bin", fileid + 1);
std::ifstream data_file((input_folder + str_buffer).c_str(),std::ios::in| std::ios::binary);
    CHECK(data_file) << "Unable to open train file #" <<fileid + 1;
         //str_buffer=/data_batch_1.bin,等等,但str_buffer是個字元數組
         //以二進制和流輸入的方式打開檔案data/cifar10/data_batch_1.bin
         //c_str() 以 char* 形式傳回 string 内含字元串
           

3.5 讀取源資料檔案

和mnist不同的是,mnist源資料集有4個檔案;mnist讀取資料時,分别調用檔案讀取函數read(),感覺這是由于mnist源資料中label資料和image資料中存儲的内容不統一,image檔案中除了存儲圖像資料外,還存儲了圖像結構資料;而圖像結構資料和圖像資料讀取的方式不一樣,而且還涉及到大端小端的轉換;是以沒有定義一個統一的圖像讀取函數來讀取;本項由于image和标簽資料都存儲在同一個bin檔案中,是以可以定義統一的圖檔讀取函數read_image來讀取源資料内容。

for (int itemid = 0;itemid < kCIFARBatchSize; ++itemid) {
      read_image(&data_file, &label,str_buffer);
//調用read_image函數從.bin檔案讀取資料,通過指針指派給label和str_buffer
void read_image(std::ifstream* file,int* label, char*buffer) {
         charlabel_char;
         file->read(&label_char, 1);
//讀取label_char的内容;CIFAR10資料應該是一個類似結構體的資料對,有label和data兩個屬性,其中label用label_char來定義的
         *label = label_char; //把label_char的值,給label
         file->read(buffer,kCIFARImageNBytes);
         return;
         }
           

3.6 讀取的資料指派到“轉換”資料對象datum,并序列化

datum.set_label(label);
datum.set_data(str_buffer,kCIFARImageNBytes);
string out;
CHECK(datum.SerializeToString(&out));
           

3.7 把資料寫入資料庫

int length =snprintf(str_buffer, kCIFARImageNBytes, "%05d",fileid *kCIFARBatchSize + itemid);
           

//上一行代碼有兩個作用:

1,把fileid * kCIFARBatchSize + itemid的值指派給str_buffer,此處的指派為每個樣本(圖檔)的id,

2,給length指派,此處length=5

string out;
txn->Put(string(str_buffer, length),out);//string(str_buffer, length)用來截取str_buffer的前length個字元;
           

//db命名空間中,Put函數代碼;

void LMDBTransaction::Put(conststring& key,const string& value) {
  MDB_val mdb_key, mdb_value;//聲明MDB_val不透明類型資料結構“對象”
  mdb_key.mv_data = const_cast<char*>(key.data());//通過指針的方式給mdb_key指派
  mdb_key.mv_size = key.size();
  mdb_value.mv_data = const_cast<char*>(value.data());
  mdb_value.mv_size = value.size();
  MDB_CHECK(mdb_put(mdb_txn_, *mdb_dbi_,&mdb_key, &mdb_value, 0));
//通過mdb_put()句柄把mdb_key和mdb_value中的資料,寫入資料庫中
}
           

3.8 把資料庫寫入lmdb檔案并關閉寫入環境

//這個commit函數和close函數,不是在caffe:db命名空間中定義的函數,估計是caffe命名空間中自帶的函數。

txn->Commit();
train_db->Close();
           

3.9用上面類似的方法把測試集寫入lmdb檔案中

四,相關檔案

convert_cifar10_data.cpp檔案

// This script converts the CIFAR dataset to the leveldb format used
// by caffe to perform classification.
// Usage:
//    convert_cifar_data input_folder output_db_file
// The CIFAR dataset could be downloaded at
//    http://www.cs.toronto.edu/~kriz/cifar.html

#include <fstream>  // NOLINT(readability/streams),檔案輸入輸出必備的檔案流
#include <string>

#include "boost/scoped_ptr.hpp"//智能指針
#include "glog/logging.h"//用于日志記錄,具體記錄什麼不是很清楚,
#include "google/protobuf/text_format.h"//用于解析.prototxt檔案的
#include "stdint.h"

#include "caffe/proto/caffe.pb.h" //解析.prototxt檔案的頭檔案
#include "caffe/util/db.hpp" //db.cpp檔案中定義了NewTransaction(),Open()等leveldb和lmdb操作函數

using caffe::Datum;
using boost::scoped_ptr;//是一個簡單的智能指針,它能夠保證在離開作用域後對象被自動釋放。
using std::string;
namespace db = caffe::db;//引入caffe命名空間中的db子命名空間

const int kCIFARSize = 32;
const int kCIFARImageNBytes = 3072;//32*32=1024,RGB各占一個位元組,感覺應該為uint8_t,0~255,
const int kCIFARBatchSize = 10000;//cifar共計5萬個訓練樣本,分成5份batches,每份1萬個,
const int kCIFARTrainBatches = 5;


 void read_image(std::ifstream* file, int* label, char* buffer) {
	char label_char;
	file->read(&label_char, 1);//讀取label_char的内容;CIFAR10資料應該是一個類似結構體的資料對,有label和data兩個屬性,其中label用label_char來定義的
	*label = label_char;//把label_char的值,給label
	file->read(buffer, kCIFARImageNBytes);
	return;
	}

//以值引用的方式傳遞參數(string& input_folder),
void convert_dataset(const string& input_folder, const string& output_folder,
    const string& db_type) {
  scoped_ptr<db::DB> train_db(db::GetDB(db_type));//以智能指針的方式建立db::DB類型的對象 train_db ,這個db::DB是什麼東西有些不清楚,db.cpp中并沒有發現這個DB類型的命名空間。
  train_db->Open(output_folder + "/cifar10_train_" + db_type, db::NEW);//調用tran_db對象的open方法,以“對db::NEW的方式,建立(或打開)檔案
  scoped_ptr<db::Transaction> txn(train_db->NewTransaction());//這個transaction暫時不清楚是幹什麼用的
  // Data buffer
  int label;
  char str_buffer[kCIFARImageNBytes];//定義字元數組,一個數組可以存放一張圖檔的資料
  Datum datum;
  datum.set_channels(3);
  datum.set_height(kCIFARSize);
  datum.set_width(kCIFARSize);

  LOG(INFO) << "Writing Training data";
  for (int fileid = 0; fileid < kCIFARTrainBatches; ++fileid) {//依次周遊每個batches,共計5個
    // Open files
    LOG(INFO) << "Training Batch " << fileid + 1;
    snprintf(str_buffer, kCIFARImageNBytes, "/data_batch_%d.bin", fileid + 1); //str_buffer=/data_batch_1.bin,等等,但str_buffer是個字元數組
    std::ifstream data_file((input_folder + str_buffer).c_str(),//以二進制和流輸入的方式打開檔案data/cifar10/data_batch_1.bin
        std::ios::in | std::ios::binary);//c_str() 以 char* 形式傳回 string 内含字元串
    CHECK(data_file) << "Unable to open train file #" << fileid + 1;
    for (int itemid = 0; itemid < kCIFARBatchSize; ++itemid) {
      read_image(&data_file, &label, str_buffer);//調用read_image函數從.bin檔案讀取資料,給label和str_buffer指派
      datum.set_label(label);
      datum.set_data(str_buffer, kCIFARImageNBytes);
      int length = snprintf(str_buffer, kCIFARImageNBytes, "%05d",
          fileid * kCIFARBatchSize + itemid);//給str_buffer指派,此處的指派為每個樣本(圖檔)的id,length=5;其實是把str_buffer的前5個字元指派為id
      string out;
      CHECK(datum.SerializeToString(&out));
      txn->Put(string(str_buffer, length), out);//string(str_buffer, length)用來截取str_buffer的前length個字元;
    }
  }
  txn->Commit();
  train_db->Close();

  LOG(INFO) << "Writing Testing data";
  scoped_ptr<db::DB> test_db(db::GetDB(db_type));
  test_db->Open(output_folder + "/cifar10_test_" + db_type, db::NEW);
  txn.reset(test_db->NewTransaction());
  // Open files
  std::ifstream data_file((input_folder + "/test_batch.bin").c_str(),
      std::ios::in | std::ios::binary);
  CHECK(data_file) << "Unable to open test file.";
  for (int itemid = 0; itemid < kCIFARBatchSize; ++itemid) {
    read_image(&data_file, &label, str_buffer);
    datum.set_label(label);
    datum.set_data(str_buffer, kCIFARImageNBytes);
    int length = snprintf(str_buffer, kCIFARImageNBytes, "%05d", itemid);
    string out;
    CHECK(datum.SerializeToString(&out));
    txn->Put(string(str_buffer, length), out);
  }
  txn->Commit();
  test_db->Close();
}

int main(int argc, char** argv) {
  if (argc != 4) {
    printf("This script converts the CIFAR dataset to the leveldb format used\n"
           "by caffe to perform classification.\n"
           "Usage:\n"
           "    convert_cifar_data input_folder output_folder db_type\n"
           "Where the input folder should contain the binary batch files.\n"
           "The CIFAR dataset could be downloaded at\n"
           "    http://www.cs.toronto.edu/~kriz/cifar.html\n"
           "You should gunzip them after downloading.\n");
  } else {
    google::InitGoogleLogging(argv[0]);
    convert_dataset(string(argv[1]), string(argv[2]), string(argv[3]));
	//sh檔案傳遞的參數:./build/examples/cifar10/convert_cifar_data.bin $DATA $EXAMPLE $DBTYPE ,依次為argv[0] argv[1] argv[2] argv[3];
	//即執行程式名稱,原始資料存放位置,轉換後資料儲存的位置,轉換的資料類型lmdb,以上參數都是以字元串形式進行傳遞的。
  }
  return 0;
}
           

db.cpp 檔案

裡面定義了caffe名字空間和其子空間db

#include "caffe/util/db.hpp"

#include <sys/stat.h>
#include <string>

namespace caffe { namespace db {

const size_t LMDB_MAP_SIZE = 1099511627776;  // 1 TB

//在制定位置以options方式建立(或打開)leveldb類型資料檔案,并檢查是否打開成功
void LevelDB::Open(const string& source, Mode mode) {
  leveldb::Options options;//建立leveldb中的options類型對象
  options.block_size = 65536;
  options.write_buffer_size = 268435456;
  options.max_open_files = 100;
  options.error_if_exists = mode == NEW;//mode=NEW時,是建立新leveldb類型檔案,是以如果該檔案以存在則報錯
  options.create_if_missing = mode != READ;//
  leveldb::Status status = leveldb::DB::Open(options, source, &db_);//通過leveldb空間中的DB子空間中的Open函數來建立(或打開)leveldb類型檔案
  CHECK(status.ok()) << "Failed to open leveldb " << source
                     << std::endl << status.ToString();
  LOG(INFO) << "Opened leveldb " << source;
}


//Open函數主要負責,建立環境;設定環境參數,打開環境
void LMDB::Open(const string& source, Mode mode) {
  MDB_CHECK(mdb_env_create(&mdb_env_));//建立lmdb操作環境
  MDB_CHECK(mdb_env_set_mapsize(mdb_env_, LMDB_MAP_SIZE));//設定環境内訓映射
  if (mode == NEW) {
    CHECK_EQ(mkdir(source.c_str(), 0744), 0) << "mkdir " << source << "failed";
  }//檢查檔案
  int flags = 0;
  if (mode == READ) {
    flags = MDB_RDONLY | MDB_NOTLS;
  }
  MDB_CHECK(mdb_env_open(mdb_env_, source.c_str(), flags, 0664));//打開建立的環境
  LOG(INFO) << "Opened lmdb " << source;
}

LMDBCursor* LMDB::NewCursor() {
  MDB_txn* mdb_txn;
  MDB_cursor* mdb_cursor;
  MDB_CHECK(mdb_txn_begin(mdb_env_, NULL, MDB_RDONLY, &mdb_txn));
  MDB_CHECK(mdb_dbi_open(mdb_txn, NULL, 0, &mdb_dbi_));
  MDB_CHECK(mdb_cursor_open(mdb_txn, mdb_dbi_, &mdb_cursor));
  return new LMDBCursor(mdb_txn, mdb_cursor);
}

//在lmdb環境中建立操作句柄
LMDBTransaction* LMDB::NewTransaction() {
  MDB_txn* mdb_txn;
  MDB_CHECK(mdb_txn_begin(mdb_env_, NULL, 0, &mdb_txn));//建立操作句柄
  MDB_CHECK(mdb_dbi_open(mdb_txn, NULL, 0, &mdb_dbi_));//打開資料庫環境
  return new LMDBTransaction(&mdb_dbi_, mdb_txn);
}

void LMDBTransaction::Put(const string& key, const string& value) {
  MDB_val mdb_key, mdb_value;
  mdb_key.mv_data = const_cast<char*>(key.data());
  mdb_key.mv_size = key.size();
  mdb_value.mv_data = const_cast<char*>(value.data());
  mdb_value.mv_size = value.size();
  MDB_CHECK(mdb_put(mdb_txn_, *mdb_dbi_, &mdb_key, &mdb_value, 0));
}

DB* GetDB(DataParameter::DB backend) {
  switch (backend) {
  case DataParameter_DB_LEVELDB:
    return new LevelDB();
  case DataParameter_DB_LMDB:
    return new LMDB();
  default:
    LOG(FATAL) << "Unknown database backend";
  }
}

//建立cafe::db“命名空間”類型對象,cafe::db“命名空間”中包含了各種資料操作函數
DB* GetDB(const string& backend) {
  if (backend == "leveldb") {
    return new LevelDB();
  } else if (backend == "lmdb") {
    return new LMDB();
  } else {
    LOG(FATAL) << "Unknown database backend";
  }
}

}  // namespace db
}  // namespace caffe
           

五, 以上代碼注釋為個人了解,如有遺漏,錯誤還望大家多多交流,指正,以便共同學習,進步!!