Caffe1——Mnist数据集创建lmdb或leveldb类型的数据
Leveldb和lmdb简单介绍
Caffe生成的数据分为2种格式:Lmdb和Leveldb。
它们都是键/值对(Key/Value Pair)嵌入式数据库管理系统编程库。
虽然lmdb的内存消耗是leveldb的1.1倍,但是lmdb的速度比leveldb快10%至15%,更重要的是lmdb允许多种训练模型同时读取同一组数据集。
因此lmdb取代了leveldb成为Caffe默认的数据集生成格式(http://blog.csdn.net/ycheng_sjtu/article/details/40361947)
LevelDb有如下一些特点:
首先,LevelDb是一个持久化存储的KV系统,和Redis这种内存型的KV系统不同,LevelDb不会像Redis一样狂吃内存,而是将大部分数据存储到磁盘上。
其次,LevleDb在存储数据时,是根据记录的key值有序存储的,就是说相邻的key值在存储文件中是依次顺序存储的,而应用可以自定义key大小比较函数,LevleDb会按照用户定义的比较函数依序存储这些记录。
再次,像大多数KV系统一样,LevelDb的操作接口很简单,基本操作包括写记录,读记录以及删除记录。也支持针对多条操作的原子批量操作。
另外,LevelDb支持数据快照(snapshot)功能,使得读取操作不受写操作影响,可以在读操作过程中始终看到一致的数据。
除此外,LevelDb还支持数据压缩等操作,这对于减小存储空间以及增快IO效率都有直接的帮助。LevelDb性能非常突出,官方网站报道其随机写性能达到40万条记录每秒,而随机读性能达到6万条记录每秒。总体来说,LevelDb的写操作要大大快于读操作,而顺序读写操作则大大快于随机读写操作。至于为何是这样,看了我们后续推出的LevelDb日知录,估计您会了解其内在原因。(http://www.cnblogs.com/haippy/archive/2011/12/04/2276064.html)
一:程序开始
在Create.sh文件通过convert_mnist_data.bin来转换数据
[plain] view plaincopy
- EXAMPLE=examples/mnist
- DATA=data/mnist
- BUILD=build/examples/mnist
- ……
- $BUILD/convert_mnist_data.bin $DATA/train-images-idx3-ubyte\
- $DATA/train-labels-idx1-ubyte$EXAMPLE/mnist_train_${BACKEND} --backend=${BACKEND}
通过命令行解析(gflags)解析后,以上可以理解为在编译平台上(gcc等)运行convert_mnist_data.bin程序,程序需要4个参数:
3个mian函数参数:1训练数据位置,2标签数据位置,3 lmdb数据存储位置。
1个程序中通过gflags宏定义的参数:转换的数据类型lmdb or leveldb。
convert_mnist_data.bin是由convert_mnist_data.cpp编译的可执行文件。
二:数据转换流程图
存放在硬盘中的mnist数据分为4个文件,训练和测试数据集,训练和测试标签集;其中数据集中存放了两类数据:图片结构数据和图片数据
三:convert_mnist_data.cpp函数分析
1.引入必要的头文件和命名空间
#include <gflags/gflags.h>//gflags命令行参数解析的头文件
#include <glog/logging.h>//记录程序日志的glog头文件
#include <google/protobuf/text_format.h>//解析proto类型文件中,解析prototxt类型的头文件
#include <leveldb/db.h>//引入leveldb类型数据头文件
#include <leveldb/write_batch.h>//引入leveldb类型数据写入头文件
#include <lmdb.h>
#include <stdint.h>
#include <sys/stat.h>
#include <fstream> // NOLINT(readability/streams)
#include <string>
#include "caffe/proto/caffe.pb.h"//解析caffe中proto类型文件的头文件
using namespace caffe; // NOLINT(build/namespaces)
using std::string;
2.定义程序变量backend
通过宏定义字符串类型变量DEFINE_stringbackend(这个是通过gflags来定义的变量,在程序调用时,通过--backend=${BACKEND}来给变量命名)
3.main()函数
Argc为统计main函数接受的参数个数,正常调用时argc=4,argv为对应的参数值,
argv[1]=源数据路径,arg[2]=标签数据路径,arg[3]=保存lmdb数据的路径
[cpp] view plaincopy
- int main(int argc, char** argv)
- {
- const string& db_backend = FLAGS_backend; //获取--backend=${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);//函数功能把源数据装换成backend型数据,并保存在制定的路劲中
- }
- return 0;
- }
4. convert_dataset()函数
4.1读取源数据
4.1.1打开源数据文件(文件先打开,才能读)
[cpp] view plaincopy
- std::ifstream image_file(image_filename, std::ios::in | std::ios::binary);
- std::ifstream label_file(label_filename, std::ios::in | std::ios::binary);
- CHECK(image_file) <<"Unable to open file "<< image_filename;
- CHECK(label_file) <<"Unable to open file "<< label_filename;
//引入std命名空间中的文件读入ifstream子空间,并创建“对象” image_file(要读入的文件名,文件读入的方式),此处以二进制的方式读入image_filename中的文件
//CHECK用于检测文件是否能够正常打开的函数,估计是定义在上面某个头文件里面的,具体哪个没有找到;感觉功能类似判断文件是否打开的函数image_file.is_open()
4.1.2定义数据结构文件
根据mnist的图像结构,长,宽,channel,样本个数等
[cpp] view plaincopy
- uint32_t magic; //这个magic做什么的我也不清楚,程序读出来,CHECK后就没在使用
- uint32_t num_items;
- uint32_t num_labels;
- uint32_t rows;
- uint32_t cols;
//uint32_t用typedef来自定义的一种数据类型,unsigned int32 ,每个int32整数占用4个字节
4.1.3读取图片结构数据
[cpp] view plaincopy
- image_file.read(reinterpret_cast<char*>(&magic), 4);
- magic = swap_endian(magic);//大端小端转换
//获取数据的结构信息,即图片的个数,width,height;这个数据的结果信息应该是一整型数据的方式存放在源数据的前n*4个字节里面;label的n=2(magic和num_labels),image的n=4(magic,num_items,width,height)
//文件读取通过read函数来完成,read(读取内容的指针,读取的字节数),这里magic是一个int32类型的整数,每个占4个字节,所以这里指定为4
//reinterpret_cast为C++中定义的强制转换符,这里把“&magic”,即magic的地址(一个16进制的数),转变成char类型的指针
4.2创建lmdb和leveldb相关变量
[cpp] view plaincopy
- //lmdb这个不太明白,只在 http://symas.com/mdb/doc/annotated.html上找了一些简单的介绍,见下问lmdb处
- MDB_env *mdb_env;
- // Opaque structure for a database environment ;
- MDB_dbi mdb_dbi;
- MDB_val mdb_key, mdb_data;
- MDB_txn *mdb_txn;
- // leveldb
- leveldb::DB* db;//创建leveldb类型的指针
- leveldb::Options options;
- //感觉这个options应该是打开leveldb文件的方式,类似这种“存在就打开,不存在就创建”的文件打开方式
- options.error_if_exists = true;// 存在就报错
- options.create_if_missing = true;// 不存在就创建
- options.write_buffer_size = 268435456; //256M
- leveldb::WriteBatch* batch = NULL;//创建leveldb类型的“实体数据”
4.3 写入硬盘
Leveldb类型
4.3.1打开(创建)数据库文件
[cpp] view plaincopy
- 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();
//通过leveldb::DB::Open()函数以options的方式,在db_path路径下创建或者打开lmdb类型文件
4.3.2创建数据“转移”的中间变量
[cpp] view plaincopy
- // Storing to db
- char label;
- char* pixels = new char[rows * cols];//定义char指针,指向字符串数组,字符串数组的容量为一个图片的大小
- int count = 0;
- const int kMaxKeyLength = 10; //最大的键值长度
- char key_cstr[kMaxKeyLength];
[cpp] view plaincopy
- <span style="font-family: ‘Microsoft YaHei‘;">string value; //用来获取“键”的内容</span>
//定义C类型的字符串,实际上是字符数组,因为二进制数据必须存储在固定长度的内存块里面,而C++中的string类型时没有固定的内存尺寸的;而变成字符数组后就有固定长度了
4.3.3创建“转换”数据对象datum
[cpp] view plaincopy
- //设置datum数据对象的结构,其结构和源图像结构相同
- Datum datum;
- datum.set_channels(1);
- datum.set_height(rows);
- datum.set_width(cols);
4.3.4读取源数据值并“赋值”给datum
[cpp] view plaincopy
- image_file.read(pixels, rows * cols); //从数据中读取rows * cols个字节,图像中一个像素值(应该是int8类型)用一个字节表示即可
- label_file.read(&label, 1);//读取标签
- datum.set_data(pixels, rows*cols);//setdata函数把源图像值放入,datum对象
- datum.set_label(label);//set_label函数把标签值放入datum
- //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);
4.3.5将数据写入db数据对象batch中
batch->Put(keystr, value);//通过batch中的子方法Put,把数据写入datum中(此时在内存中)
4.3.6把db数据写入硬盘
代码选择1000个样本放入一个batch中,通过batch以批量的方式把数据写入硬盘;写入硬盘通过db.write()函数来实现。
[cpp] view plaincopy
- if (++count % 1000 == 0) {//每个batch为1000个样本
- // Commit txn
- if (db_backend == "leveldb") { // leveldb
- db->Write(leveldb::WriteOptions(), batch);
- delete batch;
- batch = new leveldb::WriteBatch();
//把batch写入到db中,然后删除batch并重新创建,这里为什么要删除重建有些不理解;删除可能是为了清理变量,减少内存占用吧,之后又重建了。
4.3.7写入最后一个batch
[cpp] view plaincopy
- if (count % 1000 != 0) {
- if (db_backend == "leveldb") { // leveldb
- db->Write(leveldb::WriteOptions(), batch);
- delete batch;
- delete db;//删除临时变量,清理内存占用
Lmdb类型
变量和函数说明
MDB_dbi :在数据库环境中的一个独立的数据句柄
MDB_env:数据库环境的“不透明结构”,不透明类型是一种灵活的类型,他的大小是未知的
MDB_val:用于从数据库输入输出的通用结构
MDB_txn:不透明结构的处理句柄,所有的数据库操作都需要处理句柄,处理句柄可指定为只读或读写
mdb_env_create(MDB_env ** env):
创建一个lmdb环境句柄,此函数给mdb_env结构分配内存;释放内存或者关闭句柄可以通过mdb_env_close()函数来操作。在使用meb_env_create()句柄前,必须使用ndb_env_open()函数打开。
参数:env 新句柄的存储地址
mdb_env_open(MDB_env * env,const char * path,unsigned int flags,mdb_mode_t mode )
打开环境句柄,
参数:1 env,是mdb_env_create()函数返回的环境句柄
2 path,数据库文件隶属的文件夹,文件夹必须存在而且是可读的。
mdb_env_set_mapsize (MDB_env *env , size_t size )
设置当前环境的内存映射(内存地图)的尺寸。
int mdb_txn_begin (MDB_env * env, MDB_txn * parent, unsigned int flags, MDB_txn ** txn )
在环境内创建一个用来使用的“处理”transaction句柄
参数:1,env,环境
4,MDB_txn** txn 新txn句柄存储的地址
mdb_open
通过宏定义的方式,把mdb_open()函数用msb_dbi_open()函数替代
#define mdb_open(txn, name, flags,dbi ) mdb_dbi_open(txn,name,flags,dbi)
mdb_dbi_open(txn,name,flags,dbi)
在环境中打开一个数据库
参数:
1,txn mdn_txn_begin()函数返回的处理句柄
2,name 要打开的数据库名称, 如果环境中只需要一个单独的数据库,这个值为null
3,flags 指定当前数据库的操作选项
4,dbi 新的mdb_dbi句柄存储的地址
int mdb_put (MDB_txn * txn,MDB_dbi dbi,MDB_val* key,MDB_val * data,unsigned int flags )
把数据条目保存到数据库;函数把key/data(键值对)保存到数据库
参数:
1,txn mdb_txn_begin()函数返回的transaction处理句柄
2,dbi mdb_dbi_open() 函数返回的数据库句柄
3,key 4,data
int mdb_txn_commit ( MDB_txn * txn )
提交所有transaction操作到数据库中;交易句柄必须是“自由的”freed;在本次调用之后,他和它本身的“光标(指针)”不能够被在此使用;需要再一次指定txn
5.3.1创建lmdb操作环境(输入输出环境)
1)创建lmdb操作环境,
2)设置环境参数,
3)在存储位置“打开”lmdb环境,
4)在环境内创建一个用来使用的“处理”transaction句柄
5)打开lmdb类型文件
[cpp] view plaincopy
- 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";//感觉是创建lmdb类型数据的操作环境,并检查
- CHECK_EQ(mdb_env_set_mapsize(mdb_env, 1099511627776), MDB_SUCCESS)
- // 1TB,感觉是设置lmdb类型操作环境参数
- <<"mdb_env_set_mapsize failed";
- CHECK_EQ(mdb_env_open(mdb_env, db_path, 0, 0664), MDB_SUCCESS)
- //感觉是在db_path处打开上面创建的操作环境
- <<"mdb_env_open failed";
- CHECK_EQ(mdb_txn_begin(mdb_env, NULL, 0, &mdb_txn), MDB_SUCCESS)
- //提交所有transaction操作到数据库中
- <<"mdb_txn_begin failed";
- CHECK_EQ(mdb_open(mdb_txn, NULL, 0, &mdb_dbi), MDB_SUCCESS)
- //<span style="font-family: Arial, Helvetica, sans-serif;">在环境中打开一个数据库</span>
- <<"mdb_open failed. Does the lmdb already exist? ";
5.3.2创建数据“转移”的中间变量
5.3.3创建“转换”数据对象datum
5.3.4读取源数据值并“赋值”给datum
见4.3.2,4.3.3,4.3.4
5.3.5把数据放入lmdb数据类型对象mdb_data(MDB_val类型)
[cpp] view plaincopy
- { // lmdb
- //mv感觉应该是move value,应该是和write()和read()函数文件读写的方式一样,以固定的字节长度按照地址进行读写操作
- mdb_data.mv_size = value.size();//获取value的字节长度,类似sizeof()函数
- mdb_data.mv_data = reinterpret_cast<void*>(&value[0]);//把value的首个字符地址传换成空类型的指针
- mdb_key.mv_size = keystr.size();
- mdb_key.mv_data = reinterpret_cast<void*>(&keystr[0]);
- //通过mdb_put函数把mdb_key和mdb_data所指向的数据,写入到mdb_dbi(mdb_dbi个人理解,这个貌似有问题)
5.3.6 lmdb数据类型对象写入mdb_txn中
[cpp] view plaincopy
- CHECK_EQ(mdb_put(mdb_txn, mdb_dbi, &mdb_key, &mdb_data, 0), MDB_SUCCESS)<<"mdb_put failed";
5.3.7lmdb写入到硬盘
[cpp] view plaincopy
- 感觉是通过mdb_txn_commit函数把mdb_txn中的数据写入到硬盘
- CHECK_EQ(mdb_txn_commit(mdb_txn), MDB_SUCCESS)<<"mdb_txn_commit failed";
[cpp] view plaincopy
- CHECK_EQ(mdb_txn_begin(mdb_env, NULL, 0, &mdb_txn), MDB_SUCCESS)<<"mdb_txn_begin failed";
[cpp] view plaincopy
- //重新设置mdb_txn的写入位置,类似文件写入时的app方式,就是追加(继续)写入
5.3.8写入最后一个batch
[cpp] view plaincopy
- CHECK_EQ(mdb_txn_commit(mdb_txn), MDB_SUCCESS) <<"mdb_txn_commit failed";
- mdb_close(mdb_env, mdb_dbi);//关闭mdb数据对象变量
- mdb_env_close(mdb_env);//关闭mdb操作环境变量
四:大端小端转换
CPU处理器对多字节数据的存储方式,对二进制文件的可移植性有着决定性的影响;二进制文件里数据的排列顺序与他们在计算机内存的存储顺序完全一样。大端字节的计算机,数据的最高位存储在最前面;小端字节的计算机上数据的最低位存储在最前面;大端字节计算机上存储的二进制文件无法在小端计算机上正确读取,反之亦然。感觉mnist的数据集在制作存储的时候官方采用的CPU的存储方式可能和我们的CPU不一样,所以低于mnist需要进行大端小端的转换。
详细介绍参考:http://www.cnblogs.com/passingcloudss/archive/2011/05/03/2035273.html
//convert big endian to little endian in C ;http://stackoverflow.com/questions/2182002/convert-big-endian-to-little-endian-in-c-without-using-provided-funcuint32_t
//大端小端转换(大端小端为一种字节顺序存储的方式,不同的CPU有不同的存储方式)
[cpp] view plaincopy
- uint32_t swap_endian(uint32_t val)
- {//<<为位操作符,“<<”左移一位,实际数值乘以2,整形数字4,对应二进制为:……010,4<<2 ……01000,左移两位后,变成16
- val = ((val << 8) & 0xFF00FF00) | ((val >> 8) & 0xFF00FF); //变量之间的“&”为按照“位”,进行与操作,二进制数:1010 & 0110 =0010
- return (val << 16) | (val >> 16);// 变量之间的“|”操作符为按照“位”进行或操作,二进制数:1010 & 0110 =1110
- }
五:以上代码注释为个人理解,如有遗漏,错误还望大家多多交流,指正,以便共同学习,进步!!