Caffe1——Mnist數據集創建lmdb或leveldb類型的數據
Leveldb和lmdb簡單介紹
Caffe生成的數據分為2種格式:Lmdb和Leveldb。
它們都是鍵/值對(Key/Value Pair)嵌入式數據庫管理系統編程庫。
雖然lmdb的內存消耗是leveldb的1.1倍,但是lmdb的速度比leveldb快10%至15%,更重要的是lmdb允許多種訓練模型同時讀取同一組數據集。
因此lmdb取代了leveldb成為Caffe默認的數據集生成格式(http://blog.csdn.net/ycheng_sjtu/article/details/40361947)
LevelDb有如下一些特點:
首先,LevelDb是一個持久化存儲的KV系統,和Redis這種內存型的KV系統不同,LevelDb不會像Redis一樣狂吃內存,而是將大部分數據存儲到磁盤上。
其次,LevleDb在存儲數據時,是根據記錄的key值有序存儲的,就是說相鄰的key值在存儲文件中是依次順序存儲的,而應用可以自定義key大小比較函數,LevleDb會按照用戶定義的比較函數依序存儲這些記錄。
再次,像大多數KV系統一樣,LevelDb的操作接口很簡單,基本操作包括寫記錄,讀記錄以及刪除記錄。也支持針對多條操作的原子批量操作。
另外,LevelDb支持數據快照(snapshot)功能,使得讀取操作不受寫操作影響,可以在讀操作過程中始終看到一致的數據。
除此外,LevelDb還支持數據壓縮等操作,這對於減小存儲空間以及增快IO效率都有直接的幫助。LevelDb性能非常突出,官方網站報道其隨機寫性能達到40萬條記錄每秒,而隨機讀性能達到6萬條記錄每秒。總體來說,LevelDb的寫操作要大大快於讀操作,而順序讀寫操作則大大快於隨機讀寫操作。至於為何是這樣,看了我們后續推出的LevelDb日知錄,估計您會了解其內在原因。(http://www.cnblogs.com/haippy/archive/2011/12/04/2276064.html)
一:程序開始
在Create.sh文件通過convert_mnist_data.bin來轉換數據
- EXAMPLE=examples/mnist
- DATA=data/mnist
- BUILD=build/examples/mnist
- ……
- $BUILD/convert_mnist_data.bin $DATA/train-images-idx3-ubyte\
- $DATA/train-labels-idx1-ubyte$EXAMPLE/mnist_train_${BACKEND} --backend=${BACKEND}
通過命令行解析(gflags)解析后,以上可以理解為在編譯平台上(gcc等)運行convert_mnist_data.bin程序,程序需要4個參數:
3個mian函數參數:1訓練數據位置,2標簽數據位置,3 lmdb數據存儲位置。
1個程序中通過gflags宏定義的參數:轉換的數據類型lmdb or leveldb。
convert_mnist_data.bin是由convert_mnist_data.cpp編譯的可執行文件。
二:數據轉換流程圖
存放在硬盤中的mnist數據分為4個文件,訓練和測試數據集,訓練和測試標簽集;其中數據集中存放了兩類數據:圖片結構數據和圖片數據

三:convert_mnist_data.cpp函數分析
1.引入必要的頭文件和命名空間
#include <gflags/gflags.h>//gflags命令行參數解析的頭文件
#include <glog/logging.h>//記錄程序日志的glog頭文件
#include <google/protobuf/text_format.h>//解析proto類型文件中,解析prototxt類型的頭文件
#include <leveldb/db.h>//引入leveldb類型數據頭文件
#include <leveldb/write_batch.h>//引入leveldb類型數據寫入頭文件
#include <lmdb.h>
#include <stdint.h>
#include <sys/stat.h>
#include <fstream> // NOLINT(readability/streams)
#include <string>
#include "caffe/proto/caffe.pb.h"//解析caffe中proto類型文件的頭文件
using namespace caffe; // NOLINT(build/namespaces)
using std::string;
2.定義程序變量backend
通過宏定義字符串類型變量DEFINE_stringbackend(這個是通過gflags來定義的變量,在程序調用時,通過--backend=${BACKEND}來給變量命名)
3.main()函數
Argc為統計main函數接受的參數個數,正常調用時argc=4,argv為對應的參數值,
argv[1]=源數據路徑,arg[2]=標簽數據路徑,arg[3]=保存lmdb數據的路徑
- int main(int argc, char** argv)
- {
- const string& db_backend = FLAGS_backend; //獲取--backend=${BACKEND}參數
- if (argc != 4) {
- gflags::ShowUsageWithFlagsRestrict(argv[0],
- "examples/mnist/convert_mnist_data");
- } else {
- google::InitGoogleLogging(argv[0]);
- convert_dataset(argv[1], argv[2], argv[3], db_backend);//函數功能把源數據裝換成backend型數據,並保存在制定的路勁中
- }
- return 0;
- }
4. convert_dataset()函數
4.1讀取源數據
4.1.1打開源數據文件(文件先打開,才能讀)
- std::ifstream image_file(image_filename, std::ios::in | std::ios::binary);
- std::ifstream label_file(label_filename, std::ios::in | std::ios::binary);
- CHECK(image_file) <<"Unable to open file "<< image_filename;
- CHECK(label_file) <<"Unable to open file "<< label_filename;
//引入std命名空間中的文件讀入ifstream子空間,並創建“對象” image_file(要讀入的文件名,文件讀入的方式),此處以二進制的方式讀入image_filename中的文件
//CHECK用於檢測文件是否能夠正常打開的函數,估計是定義在上面某個頭文件里面的,具體哪個沒有找到;感覺功能類似判斷文件是否打開的函數image_file.is_open()
4.1.2定義數據結構文件
根據mnist的圖像結構,長,寬,channel,樣本個數等
- uint32_t magic; //這個magic做什么的我也不清楚,程序讀出來,CHECK后就沒在使用
- uint32_t num_items;
- uint32_t num_labels;
- uint32_t rows;
- uint32_t cols;
//uint32_t用typedef來自定義的一種數據類型,unsigned int32 ,每個int32整數占用4個字節
4.1.3讀取圖片結構數據
- image_file.read(reinterpret_cast<char*>(&magic), 4);
- magic = swap_endian(magic);//大端小端轉換
//獲取數據的結構信息,即圖片的個數,width,height;這個數據的結果信息應該是一整型數據的方式存放在源數據的前n*4個字節里面;label的n=2(magic和num_labels),image的n=4(magic,num_items,width,height)
//文件讀取通過read函數來完成,read(讀取內容的指針,讀取的字節數),這里magic是一個int32類型的整數,每個占4個字節,所以這里指定為4
//reinterpret_cast為C++中定義的強制轉換符,這里把“&magic”,即magic的地址(一個16進制的數),轉變成char類型的指針
4.2創建lmdb和leveldb相關變量
- //lmdb這個不太明白,只在 http://symas.com/mdb/doc/annotated.html上找了一些簡單的介紹,見下問lmdb處
- MDB_env *mdb_env;
- // Opaque structure for a database environment ;
- MDB_dbi mdb_dbi;
- MDB_val mdb_key, mdb_data;
- MDB_txn *mdb_txn;
- // leveldb
- leveldb::DB* db;//創建leveldb類型的指針
- leveldb::Options options;
- //感覺這個options應該是打開leveldb文件的方式,類似這種“存在就打開,不存在就創建”的文件打開方式
- options.error_if_exists = true;// 存在就報錯
- options.create_if_missing = true;// 不存在就創建
- options.write_buffer_size = 268435456; //256M
- leveldb::WriteBatch* batch = NULL;//創建leveldb類型的“實體數據”
4.3 寫入硬盤
Leveldb類型
4.3.1打開(創建)數據庫文件
- LOG(INFO) << "Opening leveldb " << db_path;
- leveldb::Status status = leveldb::DB::Open(options, db_path, &db);
- CHECK(status.ok()) << "Failed to open leveldb " << db_path<< ". Is it already existing?";
- batch = new leveldb::WriteBatch();
//通過leveldb::DB::Open()函數以options的方式,在db_path路徑下創建或者打開lmdb類型文件
4.3.2創建數據“轉移”的中間變量
- // Storing to db
- char label;
- char* pixels = new char[rows * cols];//定義char指針,指向字符串數組,字符串數組的容量為一個圖片的大小
- int count = 0;
- const int kMaxKeyLength = 10; //最大的鍵值長度
- char key_cstr[kMaxKeyLength];
- <span style="font-family: 'Microsoft YaHei';">string value; //用來獲取“鍵”的內容</span>
4.3.3創建“轉換”數據對象datum
- //設置datum數據對象的結構,其結構和源圖像結構相同
- Datum datum;
- datum.set_channels(1);
- datum.set_height(rows);
- datum.set_width(cols);
4.3.4讀取源數據值並“賦值”給datum
- image_file.read(pixels, rows * cols); //從數據中讀取rows * cols個字節,圖像中一個像素值(應該是int8類型)用一個字節表示即可
- label_file.read(&label, 1);//讀取標簽
- datum.set_data(pixels, rows*cols);//setdata函數把源圖像值放入,datum對象
- datum.set_label(label);//set_label函數把標簽值放入datum
- //snprintf(str1,size_t,"format",str),把str按照format的格式以字符串的形式寫入str1,size_t,表示寫入的字符個數
- //這里是把item_id轉換成8位長度的十進制整數,然后在變成字符串復制給key_str,如:item_id=1500(int),則key_cstr=00015000(string,\0為字符串結束標志)
- snprintf(key_cstr, kMaxKeyLength, "%08d", item_id);
- datum.SerializeToString(&value);
- //感覺是將datum中的值序列化成字符串,保存在變量value內,通過指針來給value賦值
- string keystr(key_cstr);
4.3.5將數據寫入db數據對象batch中
batch->Put(keystr, value);//通過batch中的子方法Put,把數據寫入datum中(此時在內存中)
4.3.6把db數據寫入硬盤
代碼選擇1000個樣本放入一個batch中,通過batch以批量的方式把數據寫入硬盤;寫入硬盤通過db.write()函數來實現。
- if (++count % 1000 == 0) {//每個batch為1000個樣本
- // Commit txn
- if (db_backend == "leveldb") { // leveldb
- db->Write(leveldb::WriteOptions(), batch);
- delete batch;
- batch = new leveldb::WriteBatch();
//把batch寫入到db中,然后刪除batch並重新創建,這里為什么要刪除重建有些不理解;刪除可能是為了清理變量,減少內存占用吧,之后又重建了。
4.3.7寫入最后一個batch
- if (count % 1000 != 0) {
- if (db_backend == "leveldb") { // leveldb
- db->Write(leveldb::WriteOptions(), batch);
- delete batch;
- delete db;//刪除臨時變量,清理內存占用
Lmdb類型
變量和函數說明
MDB_dbi :在數據庫環境中的一個獨立的數據句柄
MDB_env:數據庫環境的“不透明結構”,不透明類型是一種靈活的類型,他的大小是未知的
MDB_val:用於從數據庫輸入輸出的通用結構
MDB_txn:不透明結構的處理句柄,所有的數據庫操作都需要處理句柄,處理句柄可指定為只讀或讀寫
mdb_env_create(MDB_env ** env):
創建一個lmdb環境句柄,此函數給mdb_env結構分配內存;釋放內存或者關閉句柄可以通過mdb_env_close()函數來操作。在使用meb_env_create()句柄前,必須使用ndb_env_open()函數打開。
參數:env 新句柄的存儲地址
mdb_env_open(MDB_env * env,const char * path,unsigned int flags,mdb_mode_t mode )
打開環境句柄,
參數:1 env,是mdb_env_create()函數返回的環境句柄
2 path,數據庫文件隸屬的文件夾,文件夾必須存在而且是可讀的。
mdb_env_set_mapsize (MDB_env *env , size_t size )
設置當前環境的內存映射(內存地圖)的尺寸。
int mdb_txn_begin (MDB_env * env, MDB_txn * parent, unsigned int flags, MDB_txn ** txn )
在環境內創建一個用來使用的“處理”transaction句柄
參數:1,env,環境
4,MDB_txn** txn 新txn句柄存儲的地址
mdb_open
通過宏定義的方式,把mdb_open()函數用msb_dbi_open()函數替代
#define mdb_open(txn, name, flags,dbi ) mdb_dbi_open(txn,name,flags,dbi)
mdb_dbi_open(txn,name,flags,dbi)
在環境中打開一個數據庫
參數:
1,txn mdn_txn_begin()函數返回的處理句柄
2,name 要打開的數據庫名稱, 如果環境中只需要一個單獨的數據庫,這個值為null
3,flags 指定當前數據庫的操作選項
4,dbi 新的mdb_dbi句柄存儲的地址
int mdb_put (MDB_txn * txn,MDB_dbi dbi,MDB_val* key,MDB_val * data,unsigned int flags )
把數據條目保存到數據庫;函數把key/data(鍵值對)保存到數據庫
參數:
1,txn mdb_txn_begin()函數返回的transaction處理句柄
2,dbi mdb_dbi_open() 函數返回的數據庫句柄
3,key 4,data
int mdb_txn_commit ( MDB_txn * txn )
提交所有transaction操作到數據庫中;交易句柄必須是“自由的”freed;在本次調用之后,他和它本身的“光標(指針)”不能夠被在此使用;需要再一次指定txn

5.3.1創建lmdb操作環境(輸入輸出環境)
1)創建lmdb操作環境,
2)設置環境參數,
3)在存儲位置“打開”lmdb環境,
4)在環境內創建一個用來使用的“處理”transaction句柄
5)打開lmdb類型文件
- LOG(INFO) <<"Opening lmdb "<< db_path;
- CHECK_EQ(mkdir(db_path, 0744), 0)
- <<"mkdir "<< db_path <<"failed";//感覺是,檢查文件路徑的
- CHECK_EQ(mdb_env_create(&mdb_env), MDB_SUCCESS) <<"mdb_env_create failed";//感覺是創建lmdb類型數據的操作環境,並檢查
- CHECK_EQ(mdb_env_set_mapsize(mdb_env, 1099511627776), MDB_SUCCESS)
- // 1TB,感覺是設置lmdb類型操作環境參數
- <<"mdb_env_set_mapsize failed";
- CHECK_EQ(mdb_env_open(mdb_env, db_path, 0, 0664), MDB_SUCCESS)
- //感覺是在db_path處打開上面創建的操作環境
- <<"mdb_env_open failed";
- CHECK_EQ(mdb_txn_begin(mdb_env, NULL, 0, &mdb_txn), MDB_SUCCESS)
- //提交所有transaction操作到數據庫中
- <<"mdb_txn_begin failed";
- CHECK_EQ(mdb_open(mdb_txn, NULL, 0, &mdb_dbi), MDB_SUCCESS)
- //<span style="font-family: Arial, Helvetica, sans-serif;">在環境中打開一個數據庫</span>
- <<"mdb_open failed. Does the lmdb already exist? ";
5.3.2創建數據“轉移”的中間變量
5.3.3創建“轉換”數據對象datum
5.3.4讀取源數據值並“賦值”給datum
見4.3.2,4.3.3,4.3.4
5.3.5把數據放入lmdb數據類型對象mdb_data(MDB_val類型)
- { // lmdb
- //mv感覺應該是move value,應該是和write()和read()函數文件讀寫的方式一樣,以固定的字節長度按照地址進行讀寫操作
- mdb_data.mv_size = value.size();//獲取value的字節長度,類似sizeof()函數
- mdb_data.mv_data = reinterpret_cast<void*>(&value[0]);//把value的首個字符地址傳換成空類型的指針
- mdb_key.mv_size = keystr.size();
- mdb_key.mv_data = reinterpret_cast<void*>(&keystr[0]);
- //通過mdb_put函數把mdb_key和mdb_data所指向的數據,寫入到mdb_dbi(mdb_dbi個人理解,這個貌似有問題)
5.3.6 lmdb數據類型對象寫入mdb_txn中
- CHECK_EQ(mdb_put(mdb_txn, mdb_dbi, &mdb_key, &mdb_data, 0), MDB_SUCCESS)<<"mdb_put failed";
5.3.7lmdb寫入到硬盤
- 感覺是通過mdb_txn_commit函數把mdb_txn中的數據寫入到硬盤
- CHECK_EQ(mdb_txn_commit(mdb_txn), MDB_SUCCESS)<<"mdb_txn_commit failed";
- CHECK_EQ(mdb_txn_begin(mdb_env, NULL, 0, &mdb_txn), MDB_SUCCESS)<<"mdb_txn_begin failed";
- //重新設置mdb_txn的寫入位置,類似文件寫入時的app方式,就是追加(繼續)寫入
5.3.8寫入最后一個batch
- CHECK_EQ(mdb_txn_commit(mdb_txn), MDB_SUCCESS) <<"mdb_txn_commit failed";
- mdb_close(mdb_env, mdb_dbi);//關閉mdb數據對象變量
- mdb_env_close(mdb_env);//關閉mdb操作環境變量
四:大端小端轉換
CPU處理器對多字節數據的存儲方式,對二進制文件的可移植性有着決定性的影響;二進制文件里數據的排列順序與他們在計算機內存的存儲順序完全一樣。大端字節的計算機,數據的最高位存儲在最前面;小端字節的計算機上數據的最低位存儲在最前面;大端字節計算機上存儲的二進制文件無法在小端計算機上正確讀取,反之亦然。感覺mnist的數據集在制作存儲的時候官方采用的CPU的存儲方式可能和我們的CPU不一樣,所以低於mnist需要進行大端小端的轉換。
詳細介紹參考:http://www.cnblogs.com/passingcloudss/archive/2011/05/03/2035273.html
//convert big endian to little endian in C ;http://stackoverflow.com/questions/2182002/convert-big-endian-to-little-endian-in-c-without-using-provided-funcuint32_t
//大端小端轉換(大端小端為一種字節順序存儲的方式,不同的CPU有不同的存儲方式)
- uint32_t swap_endian(uint32_t val)
- {//<<為位操作符,“<<”左移一位,實際數值乘以2,整形數字4,對應二進制為:……010,4<<2 ……01000,左移兩位后,變成16
- val = ((val << 8) & 0xFF00FF00) | ((val >> 8) & 0xFF00FF); //變量之間的“&”為按照“位”,進行與操作,二進制數:1010 & 0110 =0010
- return (val << 16) | (val >> 16);// 變量之間的“|”操作符為按照“位”進行或操作,二進制數:1010 & 0110 =1110
- }
五:以上代碼注釋為個人理解,如有遺漏,錯誤還望大家多多交流,指正,以便共同學習,進步!!
