-
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.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."
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_items image_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; // 打开 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; } // 将读取数据保存至 db char 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 函数, 把标签值放入 datum datum.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_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 数据写入硬盘 // 选择 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") { // leveldb db->Write(leveldb::WriteOptions(), batch); delete batch; delete db; // 删除临时变量,清理内存占用 } else if (db_backend == "lmdb") { // lmdb CHECK_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_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.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_txnmdb_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: datamdb_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
Caffe: LMDB 及其数据转换
最新推荐文章于 2023-11-11 14:32:19 发布