LMDB使用说明_ldd教程
Jetbrains全系列IDE穩定放心使用
http://rayz0620.github.io/2015/05/25/lmdb_in_caffe/
官方的extract_feature.bin很好用,但是輸出的特征是放在LMDB里的。以前嫌LMDB麻煩,一直都圖方便直接用ImageDataLayer來讀原始圖像。這次繞不過去了,就順便研究了一下Caffe對LMDB的使用,一些心得寫下來和大家分享一下。提取特征的內容下一篇再寫。
Caffe中DataLayer默認的數據格式是LMDB。許多example中提供的輸入數據是LMDB格式。使用extract_features.bin提取特征時支持的輸出格式之一也是LMDB。LMDB在Caffe的IO功能中有相當重要的地位。因此,搞明白如何存取Caffe的LMDB數據,對于我們使用Caffe是很有幫助的。
LMDB
Caffe使用LMDB來存放訓練/測試用的數據集,以及使用網絡提取出的feature(為了方便,以下還是統稱數據集)。數據集的結構很簡單,就是大量的矩陣/向量數據平鋪開來。數據之間沒有什么關聯,數據內沒有復雜的對象結構,就是向量和矩陣。既然數據并不復雜,Caffe就選擇了LMDB這個簡單的數據庫來存放數據。
LMDB的全稱是Lightning Memory-Mapped Database,閃電般的內存映射數據庫。它文件結構簡單,一個文件夾,里面一個數據文件,一個鎖文件。數據隨意復制,隨意傳輸。它的訪問簡單,不需要運行單獨的數據庫管理進程,只要在訪問數據的代碼里引用LMDB庫,訪問時給文件路徑即可。
圖像數據集歸根究底從圖像文件而來。既然有ImageDataLayer可以直接讀取圖像文件,為什么還要用數據庫來放數據集,增加讀寫的麻煩呢?我認為,Caffe引入數據庫存放數據集,是為了減少IO開銷。讀取大量小文件的開銷是非常大的,尤其是在機械硬盤上。LMDB的整個數據庫放在一個文件里,避免了文件系統尋址的開銷。LMDB使用內存映射的方式訪問文件,使得文件內尋址的開銷非常小,使用指針運算就能實現。數據庫單文件還能減少數據集復制/傳輸過程的開銷。一個幾萬,幾十萬文件的數據集,不管是直接復制,還是打包再解包,過程都無比漫長而痛苦。LMDB數據庫只有一個文件,你的介質有多塊,就能復制多快,不會因為文件多而慢如蝸牛。
Caffe中的LMDB數據
接下來要介紹Caffe是如何使用LMDB存放數據的。
Caffe中的LMDB數據大約有兩類:一類是輸入DataLayer的訓練/測試數據集;另一類則是extract_feature輸出的特征數據。
Datum數據結構
首先需要注意的是,Caffe并不是把向量和矩陣直接放進數據庫的,而是將數據通過caffe.proto里定義的一個datum類來封裝。數據庫里放的是一個個的datum序列化成的字符串。Datum的定義摘錄如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
message Datum {
optional int32 channels = 1;
optional int32 height = 2;
optional int32 width = 3;
// the actual image data, in bytes
optional bytes data = 4;
optional int32 label = 5;
// Optionally, the datum could also hold float data.
repeated float float_data = 6;
// If true data contains an encoded image that need to be decoded
optional bool encoded = 7 [default = false];
}
|
一個Datum有三個維度,channels,height,和width,可以看做是少了num維度的Blob。存放數據的地方有兩個:byte_data和float_data,分別存放整數型和浮點型數據。圖像數據一般是整形,放在byte_data里,特征向量一般是浮點型,放在float_data里。label存放數據的類別標簽,是整數型。encoded標識數據是否需要被解碼(里面有可能放的是JPEG或者PNG之類經過編碼的數據)。
Datum這個數據結構將數據和標簽封裝在一起,兼容整形和浮點型數據。經過Protobuf編譯后,可以在Python和C++中都提供高效的訪問。同時Protubuf還為它提供了序列化與反序列化的功能。存放進LMDB的就是Datum序列化生成的字符串。
Caffe中讀寫LMDB的代碼
要想知道Caffe是如何使用LMDB的,最好的方法當然是去看Caffe的代碼。Caffe中關于LMDB的代碼有三類:生成數據集、讀取數據集、生成特征向量。接下來就分別針對三者進行分析。
生成數據集
生成數據集的代碼在examples,隨數據集提供,比如MNIST。
首先,創建訪問LMDB所需的一些變量:
1 2 3 4 5 |
MDB_env *mdb_env; MDB_dbi mdb_dbi; MDB_val mdb_key, mdb_data; MDB_txn *mdb_txn; ... |
mdb_env是整個數據庫環境的句柄,mdb_dbi是環境中一個數據庫的句柄,mdb_key和mdb_data用來存放向數據庫中輸入數據的“值”。mdb_txn是數據庫事物操作的句柄,”txn”是”transaction”的縮寫。
然后,創建數據庫環境,創建并打開數據庫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
if (db_backend == "lmdb") { // 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";
CHECK_EQ(mdb_env_set_mapsize(mdb_env, 1099511627776), MDB_SUCCESS) // 1TB
<< "mdb_env_set_mapsize failed";
CHECK_EQ(mdb_env_open(mdb_env, db_path, 0, 0664), MDB_SUCCESS)
<< "mdb_env_open failed";
CHECK_EQ(mdb_txn_begin(mdb_env, NULL, 0, &mdb_txn), MDB_SUCCESS)
<< "mdb_txn_begin failed";
CHECK_EQ(mdb_open(mdb_txn, NULL, 0, &mdb_dbi), MDB_SUCCESS)
<< "mdb_open failed. Does the lmdb already exist? ";
} else {
LOG(FATAL) << "Unknown db backend " << db_backend;
}
|
第3行代碼為數據庫創建文件夾,如果文件夾已經存在,程序會報錯退出。也就是說,程序不會覆蓋已有的數據庫。已有的數據庫如果不要了,需要手動刪除。第13行處創建并打開了一個數據庫。需要注意的是,LMDB的一個環境中是可以有多個數據庫的,數據庫之間以名字區分。mdb_open()的第二個參數實際上就是數據庫的名稱(char *)。當一個環境中只有一個數據庫的時候,這個參數可以給NULL。
最后,為每一個圖像創建Datum對象,向對象內寫入數據,然后將其序列化成字符串,將字符串放入數據庫中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
Datum datum;
datum.set_channels(1);
datum.set_height(rows);
datum.set_width(cols);
for (int item_id = 0; item_id < num_items; ++item_id) {
image_file.read(pixels, rows * cols);
label_file.read(&label, 1);
datum.set_data(pixels, rows*cols);
datum.set_label(label);
snprintf(key_cstr, kMaxKeyLength, "%08d", item_id);
datum.SerializeToString(&value);
string keystr(key_cstr);
// Put in db
if (db_backend == "lmdb") { // lmdb
mdb_data.mv_size = value.size();
mdb_data.mv_data = reinterpret_cast<void*>(&value[0]);
mdb_key.mv_size = keystr.size();
mdb_key.mv_data = reinterpret_cast<void*>(&keystr[0]);
CHECK_EQ(mdb_put(mdb_txn, mdb_dbi, &mdb_key, &mdb_data, 0), MDB_SUCCESS)
<< "mdb_put failed";
} else {
LOG(FATAL) << "Unknown db backend " << db_backend;
}
if (++count % 1000 == 0) {
// Commit txn
if (db_backend == "lmdb") { // lmdb
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";
} else {
LOG(FATAL) << "Unknown db backend " << db_backend;
}
}
}
|
放入數據的Key是圖像的編號,前面補0至8位。需要注意的是18至21行,MDB_val類型的mdb_data和mdb_key中存放的是數據來源的指針,以及數據的長度。第20行的mdb_put()函數將數據存入數據庫。每隔1000個圖像commit一次數據庫。只有commit之后,數據才真正寫入磁盤。
讀取數據集
Caffe中讀取LMDB數據集的代碼是DataLayer,用在網絡的最下層,提供數據。DataLayer采用順序遍歷的方式讀取數據,不支持打亂數據順序,只能隨機跳過前若干個數據。
首先,在DataLayer的DataLayerSetUp方法中,打開數據庫,并獲取迭代器cursor_:
1 2 3 |
db_.reset(db::GetDB(this->layer_param_.data_param().backend())); db_->Open(this->layer_param_.data_param().source(), db::READ); cursor_.reset(db_->NewCursor()); |
然后,在每一次的數據預取時,InternalThreadEntry()方法中,從數據庫中讀取字符串,反序列化為Datum對象,再從Datum對象中取出數據:
1 2 |
Datum datum; datum.ParseFromString(cursor_->value()); |
其中,cursor_->value()獲取序列化后的字符串。datum.ParseFromString()方法對字符串進行反序列化。
最后,要將cursor_向前推進:
1 2 3 4 5 |
cursor_->Next();
if (!cursor_->valid()) {
DLOG(INFO) << "Restarting data prefetching from start."
cursor_->SeekToFirst();
}
|
如果cursor->valid()返回false,說明數據庫已經遍歷到頭,這時需要將cursor_重置回數據庫開頭。
不支持樣本隨機排序應該是DataLayer的致命弱點。如果數據庫的key能夠統一,其實可以通過對key隨機枚舉的方式實現。
總結
以上是生活随笔為你收集整理的LMDB使用说明_ldd教程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 机器学习--最基础的最常用的聚类算法
- 下一篇: 振动分析软件有哪些_excel控件按钮怎