Caffe: LMDB 及其数据转换

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 原始数据文件

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/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_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-endianlittle-endian
0x00000x120xcd
0x00010x230xab
0x00020xab0x34
0x00030xcd0x12

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值