Preface
這兩天概覽了一下卜居(趙永科)的《深度學(xué)習(xí) 21天實(shí)戰(zhàn)caffe》,進(jìn)入深度學(xué)習(xí)挺長(zhǎng)時(shí)間的了,。文章也看了不少,,Caffe、Theano,、Torch 也都用過(guò),。其實(shí)個(gè)人認(rèn)為,這本書(shū)對(duì)于已經(jīng)深入這個(gè)領(lǐng)域已定時(shí)間的人來(lái)說(shuō),,幫助不大,。本書(shū)講述的只是“術(shù)“,有點(diǎn)像深度學(xué)習(xí)的說(shuō)明書(shū),,講的很淺,。
但是翻了一翻,還是有點(diǎn)收獲的,,這個(gè) MNIST 手寫(xiě)數(shù)字識(shí)別是深度學(xué)習(xí)入門很經(jīng)典的例子,。基本上所有的深度學(xué)習(xí)框架,,在讓初學(xué)者入門使用的時(shí)候都有這個(gè)例子,。
我一直對(duì) Caffe 中使用的 LMDB、LEVELDB 數(shù)據(jù)組織比較疑惑,,很多時(shí)候不明白該怎么樣組織圖像數(shù)據(jù),、以及其對(duì)應(yīng)的標(biāo)簽。之前都是按照別人的代碼生成的,,自己其實(shí)懵懵的,。所以,我想通過(guò) MNIST 輸入數(shù)據(jù)生成過(guò)程,,熟悉一下 LMDB,、LEVELDB 的基本使用方法,。
熟悉了 C++ 版本的轉(zhuǎn) lmdb 方式后,我會(huì)解析一下 Python 版本的 lmdb 轉(zhuǎn)換過(guò)程,。
最后 Reference 部分,,列出了我這里面參考的文章。
MNIST 及其轉(zhuǎn) LMDB 數(shù)據(jù)庫(kù)源碼 create_mnist_data
MINIST(Mixed National Institute of Stanfords and Technology)是一個(gè)大型的手寫(xiě)數(shù)字數(shù)據(jù)庫(kù),,廣泛用于機(jī)器學(xué)習(xí)領(lǐng)域的訓(xùn)練和測(cè)試,,由紐約大學(xué) Yann LeCun 教授整理。MNIST 包括 60000 個(gè)訓(xùn)練集和 10000 個(gè)測(cè)試集,,每張圖都已經(jīng)進(jìn)行尺寸歸一化,,數(shù)字居中處理,固定尺寸為 28×28,。如下圖所示:
MNIST 數(shù)據(jù)格式描述
MNIST 具體的文件格式描述如下面的表所示:
MNIST 原始數(shù)據(jù)文件
訓(xùn)練集圖片文件格式描述(train-images-idx3-ubyte)
訓(xùn)練集標(biāo)簽文件格式描述(train-labels-idx1-ubyte)
測(cè)試集圖片文件格式描述(t10k-images-idx3-ubyte)
測(cè)試集標(biāo)簽文件格式描述(t10k-labels-idx1-ubyte)
注意:圖片文件中像素按行組織,像素值 0 表示背景(白色),,像素值 255 表示前景(黑色),。
轉(zhuǎn)換格式、create_mnist_data.cpp 源碼解析
先說(shuō)一下 Caffe 為什么采用 LMDB,、LEVELDB,,而不是直接讀取原始數(shù)據(jù)?
原因是,,一方面,,數(shù)據(jù)類型多種多樣,有二進(jìn)制文件,、文本文件,、編碼后的圖像文件(如 JPEG、PNG,、網(wǎng)絡(luò)爬取的數(shù)據(jù)等),,不可能用一套代碼實(shí)現(xiàn)所有類型的輸入數(shù)據(jù)讀取,轉(zhuǎn)換為統(tǒng)一格式可以簡(jiǎn)化數(shù)據(jù)讀取層的實(shí)現(xiàn),;
另一方面,,使用 LMDB、LEVELDB 可以提高磁盤 IO 利用率,。
下載到的原始數(shù)據(jù)為二進(jìn)制文件,,需要轉(zhuǎn)換為 LEVELDB 或 LMDB 才能被 Caffe 識(shí)別。
我們 Git 得到的 Caffe 中,,在 examples/mnist/ 下有一個(gè)腳本文件:create_mnist.sh ,,這個(gè)就可以將原始的二進(jìn)制數(shù)據(jù),生成 LMDB 格式數(shù)據(jù),。
運(yùn)行后,,會(huì)生成 examples/mnist/mnist_train_lmdb/ 和 examples/mnist/mnist_test_lmdb/ 這兩個(gè)目錄。每個(gè)目錄下都有兩個(gè)文件: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/mnist
BACKEND="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."
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
create_mnist_data.cpp 源碼解析
可以看到,,上面腳本最核心的部分,,就是調(diào)用 convert_mnist_data.bin 這個(gè)可執(zhí)行程序,對(duì)應(yīng)的源文件為 examples/mnist/convert_mnist_data.cpp ,,對(duì)這個(gè)源代碼的解讀如下,,深入這段代碼可以更清楚的了解 LMDB 是如何生成的。
// 這段代碼將 MNIST 數(shù)據(jù)集轉(zhuǎn)換為(默認(rèn)的)lmdb 或者 leveldb(--backend=leveldb) 格式,,用于在使用 caffe 的時(shí)候讀取數(shù)據(jù)
// 使用方法:
// convert_mnist_data [FLAGS] input_image_file input_label_file output_db_file
// gflags: 命令行參數(shù)解析頭文件
#include <gflags/gflags.h>
// glog: 記錄程序日志頭文件
#include <glog/logging.h>
// 解析 *.prototxt 文件
#include <google/protobuf/text_format.h>
#include <leveldb/db.h>
#include <leveldb/write_batch.h>
#include <lmdb.h>
#include <stdint.h>
#include <sys/stat.h>
#include <fstream> // NOLINT(readability/streams)
#include <string>
// 解析caffe中proto類型文件的頭文件
#include "caffe/proto/caffe.pb.h"
using namespace caffe; // NOLINT(build/namespace)
using std::string;
// GFLAGS 工具定義命令行選項(xiàng) backend, 默認(rèn)值為 lmdb, 即: --backend=lmdb
DEFINE_string(backend, "lmdb", "The backend for storing the result");
// 大小端轉(zhuǎn)換, MNIST 原始數(shù)據(jù)文件中 32 位整型值為大端存儲(chǔ), C/C++ 變量為小端存儲(chǔ),,因此需要加入轉(zhuǎn)換機(jī)制
uint32_t swap_endian(uint32_t val) {
val = ((val << 8) & 0xFF00FF00) | ((val >> 8) & 0xFF00FF);
return (val << 16) | (val >> 16);
}
// 轉(zhuǎn)換數(shù)據(jù)集函數(shù)
void convert_dataset(const char* image_filename, const char* label_filename,
const char* db_path, const string& db_backend) {
// 用 C++ 輸入文件流以二進(jìn)制方式打開(kāi)
// 定義, 打開(kāi)圖像文件 對(duì)象: image_file(讀入的文件名, 讀入方式), 此處以二進(jìn)制的方式
std::ifstream image_file(image_filename, std::ios::in | std::ios::binary);
// 定義, 打開(kāi)標(biāo)簽文件 對(duì)象: label_file(讀入的文件名, 讀入方式), 此處以二進(jìn)制的方式
std::ifstream label_file(label_filename, std::ios::in | std::ios::binary);
// CHECK: 用于檢測(cè)文件能否正常打開(kāi)函數(shù)
CHECK(image_file) << "Unable to open file " << image_filename;
CHECK(label_file) << "Unable to open file " << label_filename;
// 讀取魔數(shù)與基本信息
// uint32_t 用 typedef 來(lái)自定義的一種數(shù)據(jù)類型, unsigned int32, 每個(gè)int32整數(shù)占用4個(gè)字節(jié), 這樣做是為了程序的可擴(kuò)展性
uint32_t magic; // 魔數(shù)
uint32_t num_items; // 文件包含條目總數(shù)
uint32_t num_labels; // 標(biāo)簽值
uint32_t rows; // 行數(shù)
uint32_t cols; // 列數(shù)
// 讀取魔數(shù): magic
// image_file.read( 讀取內(nèi)容的指針, 讀取的字節(jié)數(shù) ) , magic 是一個(gè) int32 類型的整數(shù),每個(gè)占 4 個(gè)字節(jié),,所以這里指定為 4
// reinterpret_cast 為 C++ 中定義的強(qiáng)制轉(zhuǎn)換符, 這里把 &magic, 即 magic 的地址(一個(gè) 16 進(jìn)制的數(shù)), 轉(zhuǎn)變成 char 類型的指針
image_file.read(reinterpret_cast<char*>(&magic), 4);
// 大端到小端的轉(zhuǎn)換
magic = swap_endian(magic);
// 校驗(yàn)圖像文件中魔數(shù)是否為 2051, 不是則報(bào)錯(cuò)
CHECK_EQ(magic, 2051) << "Incorrect image file magic.";
// 同理, 校驗(yàn)標(biāo)簽文件中的魔數(shù)是否為 2049, 不是則報(bào)錯(cuò)
label_file.read(reinterpret_cast<char*>(&magic), 4);
magic = swap_endian(magic);
CHECK_EQ(magic, 2049) << "Incorrect label file magic.";
// 讀取圖片的數(shù)量: num_items
image_file.read(reinterpret_cast<char*>(&num_items), 4);
num_items = swap_endian(num_items); // 大端到小端轉(zhuǎn)換
// 讀取圖片標(biāo)簽的數(shù)量
label_file.read(reinterpret_cast<char*>(&num_labels), 4);
num_labels = swap_endian(num_labels); // 大端到小端轉(zhuǎn)換
// 圖片數(shù)量應(yīng)等于其標(biāo)簽數(shù)量, 檢查兩者是否相等
CHECK_EQ(num_items, num_labels);
// 讀取圖片的行大小
image_file.read(reinterpret_cast<char*>(&rows), 4);
rows = swap_endian(rows); // 大端到小端轉(zhuǎn)換
// 讀取圖片的列大小
image_file.read(reinterpret_cast<char*>(&cols), 4);
cols = swap_endian(cols); // 大端到小端轉(zhuǎn)換
// lmdb 相關(guān)句柄
MDB_env *mdb_env;
MDB_dbi mdb_dbi;
MDB_val mdb_key, mdb_data;
MDB_txn *mdb_txn;
// leveldb 相關(guān)句柄
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;
// 打開(kāi) db
if (db_backend == "leveldb") { // leveldb
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();
}else 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) << "mdb_env_set_mapsize failed"; // 1TB
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;
}
// 將讀取數(shù)據(jù)保存至 db
char label;
char* pixels = new char[rows * cols];
int count = 0;
const int kMaxKeyLength = 10;
char key_cstr[kMaxKeyLength];
string value;
// 設(shè)置datum數(shù)據(jù)對(duì)象的結(jié)構(gòu),,其結(jié)構(gòu)和源圖像結(jié)構(gòu)相同
Datum datum;
datum.set_channels(1);
datum.set_height(rows);
datum.set_width(cols);
// 輸出 Log, 輸出圖片總數(shù)
LOG(INFO) << "A total of " << num_items << " items.";
// 輸出 Log, 輸出圖片的行、列大小
LOG(INFO) << "Rows: " << rows << " Cols: " << cols;
// 讀取圖片數(shù)據(jù)以及 label 存入 protobuf 定義好的數(shù)據(jù)結(jié)構(gòu)中,
// 序列化成字符串儲(chǔ)存到數(shù)據(jù)庫(kù)中,
// 這里為了減少單次操作帶來(lái)的帶寬成本(驗(yàn)證數(shù)據(jù)包完整等),
// 每 1000 次執(zhí)行一次操作
for (int item_id = 0; item_id < num_items; ++item_id) {
// 從數(shù)據(jù)中讀取 rows * cols 個(gè)字節(jié), 圖像中一個(gè)像素值(應(yīng)該是 int8 類型)用一個(gè)字節(jié)表示即可
image_file.read(pixels, rows * cols);
// 讀取標(biāo)簽
label_file.read(&label, 1);
// set_data 函數(shù), 把源圖像值放入 datum 對(duì)象
datum.set_data(pixels, rows*cols);
// set_label 函數(shù), 把標(biāo)簽值放入 datum
datum.set_label(label);
// snprintf(str1, size_t, "format", str), 把 str 按照 format 的格式以字符串的形式寫(xiě)入 str1, size_t 表示寫(xiě)入的字符個(gè)數(shù)
// 這里是把 item_id 轉(zhuǎn)換成 8 位長(zhǎng)度的十進(jìn)制整數(shù),,然后在變成字符串復(fù)制給 key_str, 如:item_id=1500(int), 則 key_cstr = 00015000(string, \0為字符串結(jié)束標(biāo)志)
snprintf(key_cstr, kMaxKeyLength, "%08d", item_id);
datum.SerializeToString(&value);
// 感覺(jué)是將 datum 中的值序列化成字符串,,保存在變量 value 內(nèi),通過(guò)指針來(lái)給 value 賦值
string keystr(key_cstr);
// 放到數(shù)據(jù)庫(kù)中
if (db_backend == "leveldb") { // leveldb
// 通過(guò) batch 中的子方法 Put, 把數(shù)據(jù)寫(xiě)入 datum 中(此時(shí)在內(nèi)存中)
batch->Put(keystr, value);
} else if (db_backend == "lmdb") { // lmdb
// mv 應(yīng)該是 move value, 應(yīng)該是和 write() 和 read() 函數(shù)文件讀寫(xiě)的方式一樣, 以固定的子節(jié)長(zhǎng)度按照地址進(jìn)行讀寫(xiě)操作
// 獲取 value 的子節(jié)長(zhǎng)度, 類似 sizeof() 函數(shù)
mdb_data.mv_size = value.size()
// 把 value 的首個(gè)字符地址轉(zhuǎn)換成空類型的指針
mdb_data.mv_data = reinterpret_cast<void*>(&value[0]);
mdb_key.mv_size = keystr.size();
mdb_key.mv_data = reinterpret_cast<void*>(&keystr[0]);
// 通過(guò) mdb_put 函數(shù)把 mdb_key 和 mdb_data 所指向的數(shù)據(jù), 寫(xiě)入到 mdb_dbi
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_back_end;
}
// 把 db 數(shù)據(jù)寫(xiě)入硬盤
// 選擇 1000 個(gè)樣本放入一個(gè) batch 中,,通過(guò) batch 以批量的方式把數(shù)據(jù)寫(xiě)入硬盤
// 寫(xiě)入硬盤通過(guò) db.write() 函數(shù)來(lái)實(shí)現(xiàn)
if (++count % 1000 == 0) {
// 批量提交更改
if(db_backend == "leveldb") { // leveldb
// 把batch寫(xiě)入到 db 中,,然后刪除 batch 并重新創(chuàng)建
db->Write(leveldb::WriteOptions(), batch);
delete batch;
batch = new leveldb::WriteBatch();
} else if (db_backend == "lmdb") { // lmdb
// 通過(guò) mdb_txn_commit 函數(shù)把 mdb_txn 數(shù)據(jù)寫(xiě)入到硬盤
CHECK_EQ(mdb_txn_commit(mdb_txn), MDB_SUCCESS) << "mdb_txn_commit failed";
// 重新設(shè)置 mdb_txn 的寫(xiě)入位置, 追加(繼續(xù))寫(xiě)入
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)
// 寫(xiě)最后一個(gè) batch
if (count % 1000 != 0) {
if (db_backend == "leveldb") { // leveldb
db->Write(leveldb::WriteOptions(), batch);
delete batch;
delete db; // 刪除臨時(shí)變量,清理內(nèi)存占用
} else if (db_backend == "lmdb") { // lmdb
CHECK_EQ(mdb_txn_commit(mdb_txn), MDB_SUCCESS) << "mdb_txn_commit failed";
// 關(guān)閉 mdb 數(shù)據(jù)對(duì)象變量
mdb_close(mdb_env, mdb_dbi);
// 關(guān)閉 mdb 操作環(huán)境變量
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_H
namespace gflags = google;
#endif
gflags::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./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 在前面通過(guò) 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;
}
- 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
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 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
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
LMDB 句柄
變量 |
說(shuō)明 |
MDB_dbi mdb_dbi |
環(huán)境中一個(gè)數(shù)據(jù)庫(kù)的句柄 |
MDB_env *mdb_env |
整個(gè)數(shù)據(jù)環(huán)境的句柄 |
MDB_val mdb_key, mdb_data |
存放要輸入進(jìn)數(shù)據(jù)庫(kù)的數(shù)據(jù)值 |
MDB_txn *mdb_txn |
數(shù)據(jù)庫(kù)事物操作的句柄 |
LMDB 流程圖
小端存儲(chǔ),、大端存儲(chǔ)(Little-Endian、Big-Endian)
上面的源碼中,,有一個(gè)函數(shù)是進(jìn)行大端存儲(chǔ)到小端存儲(chǔ)的轉(zhuǎn)換的,。這部分沒(méi)有計(jì)算機(jī)匯編的基礎(chǔ),一開(kāi)始一頭霧水……參考的一篇博客:http://www.cnblogs.com/passingcloudss/archive/2011/05/03/2035273.html
不同的CPU有不同的字節(jié)序類型,,這些字節(jié)序是指整數(shù)在內(nèi)存中保存的順序,。最常見(jiàn)的有兩種:
1. Little-endian:將低序字節(jié)存儲(chǔ)在起始地址(低位編址)
2. Big-endian:將高序字節(jié)存儲(chǔ)在起始地址(高位編址)
LE(little-endian):
最符合人的思維的字節(jié)序,地址低位存儲(chǔ)值的低位 ,,地址高位存儲(chǔ)值的高位 ,。
這種存儲(chǔ)最符合人的思維的字節(jié)序,因?yàn)閺娜说牡谝挥^感來(lái)說(shuō),,低位值小,,就應(yīng)該放在內(nèi)存地址小的地方,也即內(nèi)存地址低位,。反之,,高位值就應(yīng)該放在內(nèi)存地址大的地方,也即內(nèi)存地址高位
BE(big-endian):
最直觀的字節(jié)序,,地址低位存儲(chǔ)值的高位,,地址高位存儲(chǔ)值的低位
為什么說(shuō)直觀,,不要考慮對(duì)應(yīng)關(guān)系,只需要把內(nèi)存地址從左到右按照由低到高的順序?qū)懗?,把值按照通常的高位到低位的順序?qū)懗?。兩者?duì)照,一個(gè)字節(jié)一個(gè)字節(jié)的填充進(jìn)去 ,。
注:×86 系列的 CPU 都是 Little-Endian 的字節(jié)序,。
例子1:在內(nèi)存中雙字 0x01020304(DWORD) 的存儲(chǔ)方式:
內(nèi)存地址為:4000 4001 4002 4003
小端存儲(chǔ): 04 03 02 01
大端存儲(chǔ): 01 02 03 04
注:每個(gè)地址存 1 個(gè)字節(jié),每個(gè)字有 4 字節(jié),。2 位 16 進(jìn)制數(shù)是 1 個(gè)字節(jié)(0xFF = 11111111),。
例子2:如果我們將 0x1234abcd 寫(xiě)入到以 0x0000 開(kāi)始的內(nèi)存中,則結(jié)果為:
|
big-endian |
little-endian |
0x0000 |
0x12 |
0xcd |
0x0001 |
0x23 |
0xab |
0x0002 |
0xab |
0x34 |
0x0003 |
0xcd |
0x12 |
Python 讀寫(xiě) LMDB 格式圖像數(shù)據(jù)
我想這部分才是很多人關(guān)心的,,因?yàn)槲覀兪褂?caffe,,將圖像數(shù)據(jù)轉(zhuǎn)換為 caffe 可以識(shí)別的數(shù)據(jù)格式是第一步。同時(shí)大多數(shù)都是通過(guò) python 接口來(lái)轉(zhuǎn)換數(shù)據(jù)格式的,。
LMDB 數(shù)據(jù)庫(kù)
Caffe 使用 LMDB 的情況大約有兩類:
- 第一類是 DataLayer 層中 使用的 訓(xùn)練集,、驗(yàn)證集、測(cè)試集,;
- 第二類 就是
./caffe/build/tools/extract_feature.bin 這種特征提取工具提取特征后,輸出的特征文件,。
LMDB 的全稱是 Lighting Memory-Mapped Database(閃電般的內(nèi)存映射數(shù)據(jù)庫(kù)) ,。它文件結(jié)構(gòu)簡(jiǎn)單,一個(gè)文件夾,,里面一個(gè)數(shù)據(jù)文件,,一個(gè)鎖文件。數(shù)據(jù)隨意復(fù)制,,隨意傳輸,。它的訪問(wèn)簡(jiǎn)單,不需要運(yùn)行單獨(dú)的數(shù)據(jù)管理進(jìn)程,。只要在訪問(wèn)的代碼里引用 LMDB 庫(kù),,訪問(wèn)時(shí)給文件路徑即可。
Caffe 中使用的數(shù)據(jù)較為很簡(jiǎn)單,,就是大量的矩陣/向量平鋪開(kāi)來(lái),。數(shù)據(jù)之間沒(méi)有什么關(guān)聯(lián),數(shù)據(jù)內(nèi)沒(méi)有復(fù)雜的對(duì)象結(jié)構(gòu),,就是向量和矩陣,。既然數(shù)據(jù)并不復(fù)雜,Caffe 就選擇了 LMDB 這個(gè)簡(jiǎn)單的數(shù)據(jù)庫(kù)來(lái)存放數(shù)據(jù),。
上面提到了,,Caffe 使用 LMDB 數(shù)據(jù)庫(kù)有兩點(diǎn)原因:
一方面是因?yàn)閿?shù)據(jù)源的格式多樣性,,有文本文件、二進(jìn)制文件圖像文件等等,,不可能用一個(gè)代碼完成上述所有的數(shù)據(jù)格式,。因此,通過(guò) LMDB 數(shù)據(jù)庫(kù),,轉(zhuǎn)化成統(tǒng)一的數(shù)據(jù)格式可以簡(jiǎn)化數(shù)據(jù)讀取層的實(shí)現(xiàn),。
第二個(gè)方面就是使用 LMDB 數(shù)據(jù)庫(kù)可以大大的節(jié)約磁盤 IO 的時(shí)間開(kāi)銷。因?yàn)樽x取大量小文件的時(shí)間開(kāi)銷是相當(dāng)大的,,尤其是在機(jī)械硬盤上,。
數(shù)據(jù)庫(kù)單文件還能減少數(shù)據(jù)集復(fù)制、傳輸過(guò)程的開(kāi)銷,。因?yàn)槲覀兌加羞^(guò)體會(huì),,一個(gè)具有幾萬(wàn)個(gè)、幾十萬(wàn)個(gè)文件的數(shù)據(jù)集,,不管是直接復(fù)制,,還是打開(kāi)再解包,過(guò)程都巨慢無(wú)比,。LMDB 只有一個(gè)文件,,你的介質(zhì)有多快,就能復(fù)制多快,,不會(huì)因?yàn)槲募喽牧钊诵乃椤?
Caffe 中 Datum 數(shù)據(jù)結(jié)構(gòu)
Caffe 并不是把向量和矩陣直接放進(jìn)數(shù)據(jù)庫(kù)的,,而是將數(shù)據(jù)通過(guò) caffe.proto 里定義的一個(gè) datum 類來(lái)封裝的。數(shù)據(jù)庫(kù)里存放的是一個(gè)個(gè) datum 序列化成的字符串,。Datum 的定義如下:
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];
}
一個(gè) Datum 有三個(gè)維度,,channnels 、height ,、width ,,可以看作是少了 num 維度的 Blob 。
存放數(shù)據(jù)的地方有兩個(gè):bytes data ,、float_data ,,分別存放整數(shù)型和浮點(diǎn)型數(shù)據(jù)。圖像數(shù)據(jù)一般是整形,,放在 bytes data 中,,特征向量一般是浮點(diǎn)型,存放在 float_data 中,。
label 里存放的是類別標(biāo)簽,,是整數(shù)型。
encoded 標(biāo)識(shí)數(shù)據(jù)是否需要被解碼,,因?yàn)槔锩婵赡艽娣诺氖?JPEG 或者 PNG 之類經(jīng)過(guò)編碼的數(shù)據(jù),。
Datum 這個(gè)數(shù)據(jù)結(jié)構(gòu)將數(shù)據(jù)和標(biāo)簽封裝在一起,,兼容整形和浮點(diǎn)型數(shù)據(jù)。經(jīng)過(guò) protobuf 編譯后,,可以在 Python 和 C++ 中都提供高效的訪問(wèn),。
同時(shí) protobuf 還為它提供了序列化、反序列化的功能,。存放進(jìn) LMDB 的就是 Datum 序列化生成的字符串,。
Caffe 中將圖像寫(xiě)入 LMDB 數(shù)據(jù)庫(kù)
我上面解析的 create_mnist_data.cpp 代碼對(duì)于這部分是很有用的,特別是 LMDB 流程圖中的 lmdb 數(shù)據(jù)操作函數(shù),,如打開(kāi)一個(gè) lmdb 數(shù)據(jù)庫(kù),,寫(xiě)入數(shù)據(jù)等操作,python 中的使用類似,,但比 C++ 的要簡(jiǎn)潔許多 ,。
下面通過(guò)代碼來(lái)說(shuō)明吧,這段代碼是一個(gè)大牛寫(xiě)的教程:《A Practical Introduction to Deep Learning with Caffe and Python》,,寫(xiě)的很清晰,。
import os
import glob
import random
import numpy as np
import cv2
import caffe
from caffe.proto import caffe_pb2
import lmdb
#Size of images
IMAGE_WIDTH = 227
IMAGE_HEIGHT = 227
# train_lmdb、validation_lmdb 路徑
train_lmdb = '/home/chenxp/Documents/vehicleID/val/train_lmdb'
validation_lmdb = '/home/chenxp/Documents/vehicleID/val/validation_lmdb'
# 如果存在了這個(gè)文件夾, 先刪除
os.system('rm -rf ' + train_lmdb)
os.system('rm -rf ' + validation_lmdb)
# 讀取圖像
train_data = [img for img in glob.glob("/home/chenxp/Documents/vehicleID/val/query/*jpg")]
test_data = [img for img in glob.glob("/home/chenxp/Documents/vehicleID/val/query/*jpg")]
# Shuffle train_data
# 打亂數(shù)據(jù)的順序
random.shuffle(train_data)
# 圖像的變換, 直方圖均衡化, 以及裁剪到 IMAGE_WIDTH x IMAGE_HEIGHT 的大小
def transform_img(img, img_width=IMAGE_WIDTH, img_height=IMAGE_HEIGHT):
#Histogram Equalization
img[:, :, 0] = cv2.equalizeHist(img[:, :, 0])
img[:, :, 1] = cv2.equalizeHist(img[:, :, 1])
img[:, :, 2] = cv2.equalizeHist(img[:, :, 2])
#Image Resizing, 三次插值
img = cv2.resize(img, (img_width, img_height), interpolation = cv2.INTER_CUBIC)
return img
def make_datum(img, label):
#image is numpy.ndarray format. BGR instead of RGB
return caffe_pb2.Datum(
channels=3,
width=IMAGE_WIDTH,
height=IMAGE_HEIGHT,
label=label,
data=np.rollaxis(img, 2).tobytes()) # or .tostring() if numpy < 1.9
# 打開(kāi) lmdb 環(huán)境, 生成一個(gè)數(shù)據(jù)文件,,定義最大空間, 1e12 = 1000000000000.0
in_db = lmdb.open(train_lmdb, map_size=int(1e12))
with in_db.begin(write=True) as in_txn: # 創(chuàng)建操作數(shù)據(jù)庫(kù)句柄
for in_idx, img_path in enumerate(train_data):
if in_idx % 6 == 0: # 只處理 5/6 的數(shù)據(jù)作為訓(xùn)練集
continue # 留下 1/6 的數(shù)據(jù)用作驗(yàn)證集
# 讀取圖像. 做直方圖均衡化,、裁剪操作
img = cv2.imread(img_path, cv2.IMREAD_COLOR)
img = transform_img(img, img_width=IMAGE_WIDTH, img_height=IMAGE_HEIGHT)
if 'cat' in img_path: # 組織 label, 這里是如果文件名稱中有 'cat', 標(biāo)簽就是 0
label = 0 # 如果圖像名稱中沒(méi)有 'cat', 有的是 'dog', 標(biāo)簽則為 1
else: # 這里方, label 需要自己去組織
label = 1 # 每次情況可能不一樣, 靈活點(diǎn)
datum = make_datum(img, label)
# '{:0>5d}'.format(in_idx):
# lmdb的每一個(gè)數(shù)據(jù)都是由鍵值對(duì)構(gòu)成的,
# 因此生成一個(gè)用遞增順序排列的定長(zhǎng)唯一的key
in_txn.put('{:0>5d}'.format(in_idx), datum.SerializeToString()) #調(diào)用句柄,寫(xiě)入內(nèi)存
print '{:0>5d}'.format(in_idx) + ':' + img_path
# 結(jié)束后記住釋放資源,,否則下次用的時(shí)候打不開(kāi),。。,。
in_db.close()
# 創(chuàng)建驗(yàn)證集 lmdb 格式文件
print '\nCreating validation_lmdb'
in_db = lmdb.open(validation_lmdb, map_size=int(1e12))
with in_db.begin(write=True) as in_txn:
for in_idx, img_path in enumerate(train_data):
if in_idx % 6 != 0:
continue
img = cv2.imread(img_path, cv2.IMREAD_COLOR)
img = transform_img(img, img_width=IMAGE_WIDTH, img_height=IMAGE_HEIGHT)
if 'cat' in img_path:
label = 0
else:
label = 1
datum = make_datum(img, label)
in_txn.put('{:0>5d}'.format(in_idx), datum.SerializeToString())
print '{:0>5d}'.format(in_idx) + ':' + img_path
in_db.close()
print '\nFinished processing all images'
- 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
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 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
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
再展示一段生成 lmdb 的代碼,來(lái)源自:http:///2015/04/28/creating-lmdb-in-python/
這段代碼并沒(méi)有用真實(shí)的圖像數(shù)據(jù)來(lái)生成,,二是用 numpy 中的 np.zeros() 生成了圖像格式的數(shù)據(jù):
import numpy as np
import lmdb
import caffe
N = 1000
# Let's pretend this is interesting data
X = np.zeros((N, 3, 32, 32), dtype=np.uint8)
y = np.zeros(N, dtype=np.int64)
# We need to prepare the database for the size. We'll set it 10 times
# greater than what we theoretically need. There is little drawback to
# setting this too big. If you still run into problem after raising
# this, you might want to try saving fewer entries in a single
# transaction.
map_size = X.nbytes * 10
env = lmdb.open('mylmdb', map_size=map_size)
with env.begin(write=True) as txn:
# txn is a Transaction object
for i in range(N):
datum = caffe.proto.caffe_pb2.Datum()
datum.channels = X.shape[1]
datum.height = X.shape[2]
datum.width = X.shape[3]
datum.data = X[i].tobytes() # or .tostring() if numpy < 1.9
datum.label = int(y[i])
str_id = '{:08}'.format(i)
# The encode is only essential in Python 3
txn.put(str_id.encode('ascii'), datum.SerializeToString())
- 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
- 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
運(yùn)行上一段代碼,,會(huì)生成下面兩個(gè)文件:
Caffe 從 LMDB 數(shù)據(jù)庫(kù)中讀取數(shù)據(jù)
下面就是從生成好的 lmdb 中讀取數(shù)據(jù)了:
import numpy as np
import caffe
import lmdb
import cv2
# 打開(kāi) lmdb 數(shù)據(jù)庫(kù), 指定好位置
env = lmdd.open('mylmdb', readonly=True)
with env.begin() as txn:
raw_datum = txn.get(b'00000000')
datum = caffe.proto.caffe_pb2.Datum()
datum.ParseFromString(raw_datum)
flat_x = np.fromstring(datum.data, dtype=np.uint8)
x = flat_x.reshape(datum.channels, datum.height, datum.width)
y = datum.label
print(datum.channels)
print 'label = ' + str(y) # y 為整型, 需要轉(zhuǎn)成字符串
# C x H x W 轉(zhuǎn)換到 H x W x C, 才能在 cv2 中顯示
img = cv2.transpose(img, (1, 2, 0)) # 或者: img = x.transpose(1, 2, 0)
cv2.imshow("Image", img)
cv2.waitKey(0)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
輸出為:
下圖是輸出的圖像……別笑……那是因?yàn)樯厦娲a用 np.zeros() 生成的太小了:
可以迭代讀取 <key, value> :
with env.open() as txn:
cursor = txn.cursor()
for key, value in cursor:
print(key, value)
下面代碼用迭代循環(huán) txn.cursor() 讀取:
import caffe
from caffe.proto import caffe_pb2
import lmdb
import cv2
import numpy as np
lmdb_env = lmdb.open('mylmdb', readonly=True) # 打開(kāi)數(shù)據(jù)文件
lmdb_txn = lmdb_env.begin() # 生成處理句柄
lmdb_cursor = lmdb_txn.cursor() # 生成迭代器指針
datum = caffe_pb2.Datum() # caffe 定義的數(shù)據(jù)類型
for key, value in lmdb_cursor: # 循環(huán)獲取數(shù)據(jù)
datum.ParseFromString(value) # 從 value 中讀取 datum 數(shù)據(jù)
label = datum.label
data = caffe.io.datum_to_array(datum)
print data.shape
print datum.channels
image = data.transpose(1, 2, 0)
cv2.imshow('cv2.png', image)
cv2.waitKey(0)
cv2.destroyAllWindows()
lmdb_env.close()
- 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
- 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
Reference
- 《深度學(xué)習(xí) 21天實(shí)戰(zhàn) Caffe》, 卜居
- Caffe1——Mnist數(shù)據(jù)集創(chuàng)建lmdb或leveldb類型的數(shù)據(jù)
- caffe源碼閱讀(1): 數(shù)據(jù)加載
- 愚見(jiàn)caffe中的LeNet
- 小端格式和大端格式(Little-Endian&Big-Endian)
- Creating an LMDB database in Python
- A Practical Introduction to Deep Learning with Caffe and Python
- 中科院自動(dòng)化所博士@beanfrog:Write/Read lmdb file for caffe with python
- 利用caffe與lmdb讀寫(xiě)圖像數(shù)據(jù)
- Caffe中LMDB的使用
- Caffe: Reading LMDB from Python
|