Caffe: LMDB 及其数据转换
Preface
這兩天文章也看了不少,Caffe、Theano、Torch 也都用過。其實個人認為,這本書對于已經深入這個領域已定時間的人來說,幫助不大。本書講述的只是“術“,有點像深度學習的說明書,講的很淺。
但是翻了一翻,還是有點收獲的,這個 MNIST 手寫數字識別是深度學習入門很經典的例子。基本上所有的深度學習框架,在讓初學者入門使用的時候都有這個例子。
我一直對 Caffe 中使用的 LMDB、LEVELDB 數據組織比較疑惑,很多時候不明白該怎么樣組織圖像數據、以及其對應的標簽。之前都是按照別人的代碼生成的,自己其實懵懵的。所以,我想通過 MNIST 輸入數據生成過程,熟悉一下 LMDB、LEVELDB 的基本使用方法。
那下面就進入正題,下面是我看的筆記。
?
MNIST 數據集
MINIST(Mixed National Institute of Stanfords and Technology)是一個大型的手寫數字數據庫,廣泛用于機器學習領域的訓練和測試,由紐約大學 Yann LeCun 教授整理。MNIST 包括 60000個訓練集和 10000個測試集,每張圖都已經進行尺寸歸一化,數字居中處理,固定尺寸為 28×28。如下圖所示:
?
MNIST 數據格式描述
MNIST 具體的文件格式描述如下面的表所示:
MNIST 原始數據文件?
?
訓練集圖片文件格式描述(train-images-idx3-ubyte)?
?
訓練集標簽文件格式描述(train-labels-idx1-ubyte)?
?
測試集圖片文件格式描述(t10k-images-idx3-ubyte)?
?
測試集標簽文件格式描述(t10k-labels-idx1-ubyte)?
注意:圖片文件中像素按行組織,像素值 0表示背景(白色),像素值 255表示前景(黑色)。
?
轉換格式、create_mnist_data.cpp 源碼解析
先說一下 Caffe 為什么采用 LMDB、LEVELDB,而不是直接讀取原始數據?
原因是,一方面,數據類型多種多樣,有二進制文件、文本文件、編碼后的圖像文件(如 JPEG、PNG、網絡爬取的數據等),不可能用一套代碼實現所有類型的輸入數據讀取,轉換為統一格式可以簡化數據讀取層的實現;
另一方面,使用 LMDB、LEVELDB 可以提高磁盤 IO 利用率。
?
轉換格式
下載到的原始數據為二進制文件,需要轉換為 LEVELDB 或 LMDB 才能被 Caffe 識別。
我們 Git 得到的 Caffe 中,在 examples/mnist/ 下有一個腳本文件:create_mnist.sh ,這個就可以將原始的二進制數據,生成 LMDB 格式數據。
運行后,會生成 examples/mnist/mnist_train_lmdb/ 和 examples/mnist/mnist_test_lmdb/ 這兩個目錄。每個目錄下都有兩個文件:data.mdb 和 lock.mdb。
看一下腳本文件:create_mnist.sh 里面是什么:
#!/usr/bin/env sh # This script converts the mnist data into lmdb/leveldb format, # depending on the value assigned to $BACKEND.EXAMPLE=examples/mnist DATA=data/mnist BUILD=build/examples/mnistBACKEND="lmdb"echo "Creating ${BACKEND}..."rm -rf $EXAMPLE/mnist_train_${BACKEND} rm -rf $EXAMPLE/mnist_test_${BACKEND}$BUILD/convert_mnist_data.bin $DATA/train-images-idx3-ubyte \$DATA/train-labels-idx1-ubyte $EXAMPLE/mnist_train_${BACKEND} --backend=${BACKEND} $BUILD/convert_mnist_data.bin $DATA/t10k-images-idx3-ubyte \$DATA/t10k-labels-idx1-ubyte $EXAMPLE/mnist_test_${BACKEND} --backend=${BACKEND}echo "Done."?
create_mnist_data.cpp 源碼解析
可以看到,上面腳本最核心的部分,就是調用 convert_mnist_data.bin 這個可執行程序,對應的源文件為 examples/mnist/convert_mnist_data.cpp,對這個源代碼的解讀如下,深入這段代碼可以更清楚的了解 LMDB 是如何生成的。
// 這段代碼將 MNIST 數據集轉換為(默認的)lmdb 或者 leveldb(--backend=leveldb) 格式,用于在使用 caffe 的時候讀取數據 // 使用方法: // convert_mnist_data [FLAGS] input_image_file input_label_file output_db_file// gflags: 命令行參數解析頭文件 #include // glog: 記錄程序日志頭文件 #include // 解析 *.prototxt 文件 #include #include #include #include #include #include #include // NOLINT(readability/streams) #include // 解析caffe中proto類型文件的頭文件 #include "caffe/proto/caffe.pb.h"using namespace caffe; // NOLINT(build/namespace) using std::string;// GFLAGS 工具定義命令行選項 backend, 默認值為 lmdb, 即: --backend=lmdb DEFINE_string(backend, "lmdb", "The backend for storing the result");// 大小端轉換, MNIST 原始數據文件中 32 位整型值為大端存儲, C/C++ 變量為小端存儲,因此需要加入轉換機制 uint32_t swap_endian(uint32_t val) {val = ((val << 8) & 0xFF00FF00) | ((val >> 8) & 0xFF00FF);return (val << 16) | (val >> 16); }// 轉換數據集函數 void convert_dataset(const char* image_filename, const char* label_filename,const char* db_path, const string& db_backend) {// 用 C++ 輸入文件流以二進制方式打開// 定義, 打開圖像文件 對象: image_file(讀入的文件名, 讀入方式), 此處以二進制的方式std::ifstream image_file(image_filename, std::ios::in | std::ios::binary);// 定義, 打開標簽文件 對象: label_file(讀入的文件名, 讀入方式), 此處以二進制的方式std::ifstream label_file(label_filename, std::ios::in | std::ios::binary);// CHECK: 用于檢測文件能否正常打開函數CHECK(image_file) << "Unable to open file " << image_filename;CHECK(label_file) << "Unable to open file " << label_filename;// 讀取魔數與基本信息// uint32_t 用 typedef 來自定義的一種數據類型, unsigned int32, 每個int32整數占用4個字節, 這樣做是為了程序的可擴展性uint32_t magic; // 魔數uint32_t num_items; // 文件包含條目總數 uint32_t num_labels; // 標簽值uint32_t rows; // 行數uint32_t cols; // 列數// 讀取魔數: magic// image_file.read( 讀取內容的指針, 讀取的字節數 ) , magic 是一個 int32 類型的整數,每個占 4 個字節,所以這里指定為 4// reinterpret_cast 為 C++ 中定義的強制轉換符, 這里把 &magic, 即 magic 的地址(一個 16 進制的數), 轉變成 char 類型的指針image_file.read(reinterpret_cast(&magic), 4);// 大端到小端的轉換magic = swap_endian(magic);// 校驗圖像文件中魔數是否為 2051, 不是則報錯CHECK_EQ(magic, 2051) << "Incorrect image file magic.";// 同理, 校驗標簽文件中的魔數是否為 2049, 不是則報錯label_file.read(reinterpret_cast(&magic), 4);magic = swap_endian(magic);CHECK_EQ(magic, 2049) << "Incorrect label file magic.";// 讀取圖片的數量: num_itemsimage_file.read(reinterpret_cast(&num_items), 4);num_items = swap_endian(num_items); // 大端到小端轉換// 讀取圖片標簽的數量label_file.read(reinterpret_cast(&num_labels), 4);num_labels = swap_endian(num_labels); // 大端到小端轉換// 圖片數量應等于其標簽數量, 檢查兩者是否相等CHECK_EQ(num_items, num_labels);// 讀取圖片的行大小image_file.read(reinterpret_cast(&rows), 4);rows = swap_endian(rows); // 大端到小端轉換// 讀取圖片的列大小image_file.read(reinterpret_cast(&cols), 4);cols = swap_endian(cols); // 大端到小端轉換// lmdb 相關句柄MDB_env *mdb_env;MDB_dbi mdb_dbi;MDB_val mdb_key, mdb_data;MDB_txn *mdb_txn;// leveldb 相關句柄leveldb::DB* db;leveldb::Options options;options.error_if_exists = true;options.create_if_missing = true;options.write_buffer_size = 268435456;level::WriteBatch* batch = NULL;// 打開 dbif (db_backend == "leveldb") { // leveldbLOG(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();}else if (db_backend == "lmdb") { // lmdbLOG(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) << "mdb_env_set_mapsize failed"; // 1TBCHECK_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;}// 將讀取數據保存至 dbchar label;char* pixels = new char[rows * cols];int count = 0;const int kMaxKeyLength = 10;char key_cstr[kMaxKeyLength];string value;// 設置datum數據對象的結構,其結構和源圖像結構相同Datum datum;datum.set_channels(1);datum.set_height(rows);datum.set_width(cols);// 輸出 Log, 輸出圖片總數LOG(INFO) << "A total of " << num_items << " items.";// 輸出 Log, 輸出圖片的行、列大小LOG(INFO) << "Rows: " << rows << " Cols: " << cols;// 讀取圖片數據以及 label 存入 protobuf 定義好的數據結構中,// 序列化成字符串儲存到數據庫中,// 這里為了減少單次操作帶來的帶寬成本(驗證數據包完整等), // 每 1000 次執行一次操作for (int item_id = 0; item_id < num_items; ++item_id) {// 從數據中讀取 rows * cols 個字節, 圖像中一個像素值(應該是 int8 類型)用一個字節表示即可image_file.read(pixels, rows * cols);// 讀取標簽label_file.read(&label, 1);// set_data 函數, 把源圖像值放入 datum 對象datum.set_data(pixels, rows*cols);// set_label 函數, 把標簽值放入 datumdatum.set_label(label);// 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);// 放到數據庫中if (db_backend == "leveldb") { // leveldb// 通過 batch 中的子方法 Put, 把數據寫入 datum 中(此時在內存中)batch->Put(keystr, value);} else if (db_backend == "lmdb") { // lmdb// mv 應該是 move value, 應該是和 write() 和 read() 函數文件讀寫的方式一樣, 以固定的子節長度按照地址進行讀寫操作// 獲取 value 的子節長度, 類似 sizeof() 函數mdb_data.mv_size = value.size()// 把 value 的首個字符地址轉換成空類型的指針mdb_data.mv_data = reinterpret_cast(&value[0]);mdb_key.mv_size = keystr.size();mdb_key.mv_data = reinterpret_cast(&keystr[0]);// 通過 mdb_put 函數把 mdb_key 和 mdb_data 所指向的數據, 寫入到 mdb_dbiCHECK_EQ(mdb_put(mdb_txn, mdb_dbi, &mdb_key, &mdb_data, 0), MDB_SUCCESS) << "mdb_put failed";} else {LOG(FATAL) << "Unknown db backend " << db_back_end;}// 把 db 數據寫入硬盤// 選擇 1000 個樣本放入一個 batch 中,通過 batch 以批量的方式把數據寫入硬盤// 寫入硬盤通過 db.write() 函數來實現if (++count % 1000 == 0) {// 批量提交更改if(db_backend == "leveldb") { // leveldb// 把batch寫入到 db 中,然后刪除 batch 并重新創建db->Write(leveldb::WriteOptions(), batch);delete batch;batch = new leveldb::WriteBatch();} else if (db_backend == "lmdb") { // lmdb// 通過 mdb_txn_commit 函數把 mdb_txn 數據寫入到硬盤CHECK_EQ(mdb_txn_commit(mdb_txn), MDB_SUCCESS) << "mdb_txn_commit failed";// 重新設置 mdb_txn 的寫入位置, 追加(繼續)寫入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;}} // if (++count % 1000 == 0) } // for (int item_id = 0; item_id < num_items; ++item_id)// 寫最后一個 batch if (count % 1000 != 0) {if (db_backend == "leveldb") { // leveldbdb->Write(leveldb::WriteOptions(), batch);delete batch;delete db; // 刪除臨時變量,清理內存占用} else if (db_backend == "lmdb") { // lmdbCHECK_EQ(mdb_txn_commit(mdb_txn), MDB_SUCCESS) << "mdb_txn_commit failed";// 關閉 mdb 數據對象變量mdb_close(mdb_env, mdb_dbi);// 關閉 mdb 操作環境變量mdb_env_close(mdb_env);} else {LOG(FATAL) << "Unknown db backend " << db_backend;}LOG(ERROE) << "Processed " << count << " files.";}delete[] pixels; } // void convert_dataset(const char* image_filename, const char* label_filename, const char* db_path, const string& db_backend)int main(int argc, char** argv) { #ifndef GFLAGS_GFLAGS_Hnamespace gflags = google; #endifgflags::SetUsageMessage("This script converts the MNIST dataset to \n""the lmdb/leveldb format used by Caffe to load data. \n""Usage:\n"" convert_mnist_data [FLAGS] input_image_file input_label_file ""output_db_file\n""The MNIST dataset could be downloaded at\n"" http://yann.lecun.com/exdb/mnist/\n""You should gunzip them after downloading,""or directly use the data/mnist/get_mnist.sh\n");gflags::ParseCommandLineFlags(&argc, &argv, true);// FLAGS_backend 在前面通過 DEFINE_string 定義,是字符串類型const string& db_backend = FLAGS_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);}return 0; }?
LMDB 相關句柄
| MDB_dbi mdb_dbi | 在數據庫環境中的一個獨立的數據句柄 |
| MDB_env *mdb_env | 數據庫環境的“不透明結構”,不透明類型是一種靈活的類型,他的大小是未知的 |
| MDB_val mdb_key, mdb_data | 用于從數據庫輸入輸出的通用結構 |
| MDB_txn *mdb_txn | 不透明結構的處理句柄,所有的數據庫操作都需要處理句柄,處理句柄可指定為只讀或讀寫 |
?
LMDB 相關函數
mdb_env_create ( &mdb_env )
MDB_env *mdb_env, 環境句柄
創建一個 lmdb 環境句柄,此函數給 mdb_env 結構分配內存;
釋放內存或者關閉句柄可以通過 mdb_env_close( ) 函數來操作。在使用 mdb_env_create( ) 句柄前,必須使用 mdb_env_open( ) 函數打開
mdb_env_open ( mdb_env, db_path, 0, 0664)
打開環境句柄,其中:
mdb_env, 是 mdb_env_create ( ) 函數返回的環境句柄
db_path, 數據庫文件隸屬的文件夾,文件夾必須存在而且是可讀的。
mdb_env_set_mapsize ( mdb_env, 1099511627776 )
設置當前環境的內存映射(內存地圖)的尺寸
mdb_txn_begin ( mdb_env, NULL, 0, &mdb_txn )
在環境內創建一個用來使用的“處理” transaction 句柄
MDB_env *mdb_ env, 環境句柄
MDB_txn *mdb_txn
mdb_open( mdb_txn, NULL, 0, &mdb_dbi )
mdb_put ( mdb_txn, mdb_dbi, &mdb_key, &mdb_data, 0 )
把數據條目保存到數據庫;函數把 key / data(鍵值對) 保存到數據庫
MDB_txn *mdb_txn
MDB_dbi mdb_dbi
MDB_val mdb_key: key
MDB_val mdb_data: data
mdb_txn_commit ( mdb_txn )
提交所有 transaction 操作到數據庫中;
交易句柄必須是 “自由的”;
在本次調用之后,他和它本身的“光標(指針)”不能夠被在此使用,需要再一次指定 txn
?
LMDB 流程圖
?
小端存儲、大端存儲(Little-Endian、Big-Endian)
上面的源碼中,有一個函數是進行大端存儲到小端存儲的轉換的。這部分沒有計算機匯編的基礎,一開始一頭霧水……參考的一篇博客:http://www.cnblogs.com/passingcloudss/archive/2011/05/03/2035273.html
不同的CPU有不同的字節序類型,這些字節序是指整數在內存中保存的順序。最常見的有兩種:
1. Little-endian:將低序字節存儲在起始地址(低位編址)
2. Big-endian:將高序字節存儲在起始地址(高位編址)
LE(little-endian):
最符合人的思維的字節序,地址低位存儲值的低位 ,地址高位存儲值的高位 。
這種存儲最符合人的思維的字節序,因為從人的第一觀感來說,低位值小,就應該放在內存地址小的地方,也即內存地址低位。反之,高位值就應該放在內存地址大的地方,也即內存地址高位
BE(big-endian):
最直觀的字節序,地址低位存儲值的高位,地址高位存儲值的低位
為什么說直觀,不要考慮對應關系,只需要把內存地址從左到右按照由低到高的順序寫出,把值按照通常的高位到低位的順序寫出。兩者對照,一個字節一個字節的填充進去 。
注:×86系列的 CPU 都是 Little-Endian 的字節序。
例子1:在內存中雙字 0x01020304(DWORD) 的存儲方式:
??內存地址為:4000 4001 4002 4003
??小端存儲: 04 03 02 01
??大端存儲: 01 02 03 04
注:每個地址存 1 個字節,每個字有 4 字節。2 位 16 進制數是 1 個字節(0xFF = 11111111)。
例子2:如果我們將 0x1234abcd 寫入到以 0x0000 開始的內存中,則結果為:
| 0x0000 | 0x12 | 0xcd |
| 0x0001 | 0x23 | 0xab |
| 0x0002 | 0xab | 0x34 |
| 0x0003 | 0xcd | 0x12 |
?
總結
以上是生活随笔為你收集整理的Caffe: LMDB 及其数据转换的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Ubuntu 16.04 LTS, 64
- 下一篇: caffe web demo 搭建