当前位置: 代码迷 >> 综合 >> Caffe: LMDB 及其数据转换
  详细解决方案

Caffe: LMDB 及其数据转换

热度:72   发布时间:2023-12-12 08:04:41.0
from: http://www.2cto.com/kf/201607/527860.html

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.mdblock.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 开始的内存中,则结果为:

  big-endian little-endian
0x0000 0x12 0xcd
0x0001 0x23 0xab
0x0002 0xab 0x34
0x0003 0xcd 0x12