【C++ Caffe】Ubuntu下 手写体数字识别

MNIST数据集是机器学习领域中非常经典的一个数据集,由60000个训练样本和10000个测试样本组成,每个样本都是一张28 * 28像素的灰度手写数字图片 。
1.下载MNIST数据集
MNIST数据集可以在Caffe源码框架的data/mnist下用.sh下载
cd data/mnist/./get_mnist.sh
上述过程下载的时间可能有点长,我将文件放在我的百度网盘中了,密码: ln4o
下载后将文件解压放到data/mnist/目录下
在data/mnist/目录下使用tree命令查看目录结构
接下来,我们来看一下的内容
#!/usr/bin/env sh# 这个脚本用于下载并解压 mnist 文件DIR="$( cd "$(dirname "$0")" ; pwd -P )"cd "$DIR"# printfecho "Downloading..."# for循环 设置要下载文件的名称,这里一共4个文件for fname in train-images-idx3-ubyte train-labels-idx1-ubyte t10k-images-idx3-ubyte t10k-labels-idx1-ubytedoif [ ! -e $fname ]; then# 进行下载文件wget --no-check-certificate http://yann.lecun.com/exdb/mnist/${fname}.gz# 进行解压文件gunzip ${fname}.gzfidone
从上述的脚本我们可以看出来,MNIST原始数据为4个文件,如下表所示:
文件名说明
train--idx3-ubyte
训练集,图片
train--idx1-ubyte
训练集,标签
t10k--idx3-ubyte
测试集,图片
t10k--idx1-ubyte
测试集,标签
2.转换格式
但是问题出现了,caffe 并不支持 二进制文件,只支持LMDB 或者,所以我们需要将数据进行转换.进行数据转换的脚本在 " caffe路径//mnist/.sh"
进入到 " caffe路径 ",进行执行 “.//mnist/.sh”,显示如下:
lichunlin@ThinkPad-T420:~/caffe$ ./examples/mnist/create_mnist.sh Creating lmdb...I0226 18:49:42.627514 18492 db_lmdb.cpp:35] Opened lmdb examples/mnist/mnist_train_lmdbI0226 18:49:42.627812 18492 convert_mnist_data.cpp:88] A total of 60000 items.I0226 18:49:42.627828 18492 convert_mnist_data.cpp:89] Rows: 28 Cols: 28I0226 18:49:43.350973 18492 convert_mnist_data.cpp:108] Processed 60000 files.I0226 18:49:43.377131 18496 db_lmdb.cpp:35] Opened lmdb examples/mnist/mnist_test_lmdbI0226 18:49:43.377393 18496 convert_mnist_data.cpp:88] A total of 10000 items.I0226 18:49:43.377408 18496 convert_mnist_data.cpp:89] Rows: 28 Cols: 28I0226 18:49:43.497009 18496 convert_mnist_data.cpp:108] Processed 10000 files.Done.
这就是说明格式转换成功了
在转换成功之前,曾经报过一个错误,显示如下,导致格式转换不成功
lichunlin@ThinkPad-T420:~/caffe$ ./examples/mnist/create_mnist.sh Creating lmdb...F0226 14:33:14.406914 32482 convert_mnist_data.cpp:59] Check failed: magic == 2051 (529205256 vs. 2051) Incorrect image file magic.*** Check failure stack trace: ***@ 0x7f26f3ceb0cd google::LogMessage::Fail()@ 0x7f26f3cecf33 google::LogMessage::SendToLog()@ 0x7f26f3ceac28 google::LogMessage::Flush()@ 0x7f26f3ced999 google::LogMessageFatal::~LogMessageFatal()@ 0x564d964ea6e0 (unknown)@ 0x564d964e947a (unknown)@ 0x7f26f2cf0b97 __libc_start_main@ 0x564d964e94ca (unknown)Aborted (core dumped)
上述问题我是通过删除了MNIST原始数据(第一节中提到的4个文件),然后用指令 ./.sh 重新下载(a fewlater)然后重新执行 .//mnist/.sh 指令重新进行格式转换
到此,数据准备已经完成了!
接下来分析一下 “.sh” 文件
#!/usr/bin/env sh# This script converts the mnist data into lmdb/leveldb format,# depending on the value assigned to $BACKEND.set -e# 定义一些变量EXAMPLE=examples/mnistDATA=http://www.kingceram.com/post/data/mnistBUILD=build/examples/mnistBACKEND="lmdb"# printfecho "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."
最重要的内容就是
$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}
但是 " .bin " 是编译后的二进制文件,并没有任何信息,所以我们应该去了解 " caffe目录//mnist/.cpp " 这个文件的内容.
C 语言从 main 函数开始阅读.
/* 主函数 包含 额外参数(int argc, char** argv)使用方法,convert_mnist_data.bin 图片文件路径 标签文件路径 生成数据库文件路径 --backend=使用数据库类型例如convert_mnist_data.bin t10k-images-ubyte t10k-labels-ubyte mnist_test_lmdb --backend=lmdb*/int main(int argc, char** argv) {// 条件编译 实际中我们定义了 #include #ifndef GFLAGS_GFLAGS_H_namespace gflags = google;#endifFLAGS_alsologtostderr = 1;// SetUsageMessage: 设置命令行帮助信息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 data/mnist/get_mnist.sh\n");/* ParseCommandLineFlags: 解析命令行参数,* 与上文定义的 * DEFINE_string(backend, "lmdb", "The backend for storing the result");* 相对应* backend 变为 FLAGS_backend 变量*/gflags::ParseCommandLineFlags(&argc, &argv, true);const string& db_backend = FLAGS_backend;if (argc != 4) {// 输出错误信息函数,第一个参数必须是 argv[0]gflags::ShowUsageWithFlagsRestrict(argv[0],"examples/mnist/convert_mnist_data");} else {// 初始化 gloggoogle::InitGoogleLogging(argv[0]);// 进行数据转换(实际操作)convert_dataset(argv[1], argv[2], argv[3], db_backend);}return 0;}
主函数中用到了大量的和 glogs 的函数,都是用来对命令行与 日志相关,具体的真实干活的是函数.
// convert_dataset 实际中 将 二进制文件内容 存储到 数据库 中的函数void convert_dataset(const char* image_filename, const char* label_filename,const char* db_path, const string& db_backend) {/* 以输入的方式 image_file 和 label_file* std::ifstream image_file 指 image_file 为一个类,并指定了文件路径和打开方法*/std::ifstream image_file(image_filename, std::ios::in | std::ios::binary);std::ifstream label_file(label_filename, std::ios::in | std::ios::binary);// glog 中的 宏函数 如果 状态不正常 则输出后面内容CHECK(image_file) << "Unable to open file " << image_filename;CHECK(label_file) << "Unable to open file " << label_filename;//与二进制文件内容相关/* magic魔数,类似于校验信息* num_items条目(图片)的个数* num_labels标签的个数* rows一行元素中像素点个数* cols一列元素中像素点个数*/uint32_t magic;uint32_t num_items;uint32_t num_labels;uint32_t rows;uint32_t cols;// 使用 read 方法读取 image_file 前 (4*8)32 位元素的内容,即为魔数image_file.read(reinterpret_cast(&magic), 4);magic = swap_endian(magic);// 校验魔数是否一致CHECK_EQ(magic, 2051) << "Incorrect image file magic.";// 同理校验 label_file 的正确性label_file.read(reinterpret_cast(&magic), 4);magic = swap_endian(magic);CHECK_EQ(magic, 2049) << "Incorrect label file magic.";// 读取 条目的个数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);/* 来自* #include "boost/scoped_ptr.hpp"* 不用再分 levelDB 与LMDB*/// 创建新 数据库DBscoped_ptr db(db::GetDB(db_backend));// 打开数据库,并指定数据库的路径db->Open(db_path, db::NEW);// 创建对数据库的 操作句柄(txn) Transaction 表示 事务,数据库内容的知识scoped_ptr txn(db->NewTransaction());// Storing to dbchar label;char* pixels = new char[rows * cols];int count = 0;string value;// 定义图片 对象Datum datum;// 图片的通道数datum.set_channels(1);// 图片的高datum.set_height(rows);// 图片的宽datum.set_width(cols);LOG(INFO) << "A total of " << num_items << " items.";LOG(INFO) << "Rows: " << rows << " Cols: " << cols;// 对 二进制文件中所有的 条目(图片) 进行处理for (int item_id = 0; item_id < num_items; ++item_id) {// 读取像素点image_file.read(pixels, rows * cols);// 读取标签label_file.read(&label, 1);datum.set_data(pixels, rows*cols);datum.set_label(label);string key_str = caffe::format_int(item_id, 8);// 对图片进行字符串序列化datum.SerializeToString(&value);// 通过事务 将 序列化后的图片信息 和 编号进行加入到数据库中txn->Put(key_str, value);// 并且 每 1000 个 提交一次if (++count % 1000 == 0) {txn->Commit();}}// 防止出现不能被1000整除的情况,对最后一批进行处理if (count % 1000 != 0) {txn->Commit();}LOG(INFO) << "Processed " << count << " files.";//删除 new 的内容delete[] pixels;// 关闭数据库db->Close();}
利用对 二进制文件的了解,对二进制文件进行解析,最后将其以数据库的形式保存.其中运用了一些数据库相关的知识,并且将数据库变成了面向对象的编程.
解决最后的问题,数据的大小端问题.
/* MNIST原始数据为大端存储,即数据的高字节保存在地址的低地址中,而数据的低字节保存在内存的高地址中* C/C++数据为小端存储,和MNIST正好相反,* 所以需要定义一个函数进行转换*/uint32_t swap_endian(uint32_t val) {val = ((val << 8) & 0xFF00FF00) | ((val >> 8) & 0xFF00FF);return (val << 16) | (val >> 16);}
3. 训练数据
开始训练生成的数据,训练脚本在"caffe路径//mnist/.sh"
其中调用了真正执行的语句
#!/usr/bin/env shset -e./build/tools/caffe train --solver=examples/mnist/lenet_solver.prototxt
".//mnist/.sh"运行后,可能你会看见这样的显示:
lichunlin@ThinkPad-T420:~/caffe$ ./build/tools/caffe train --solver=examples/mnist/lenet_solver.prototxt I0226 16:01:17.276763 16197 caffe.cpp:204] Using GPUs 0F0226 16:01:17.277010 16197 common.cpp:66] Cannot use GPU in CPU-only Caffe: check mode.*** Check failure stack trace: ***@0x7f32a8e065cdgoogle::LogMessage::Fail()@0x7f32a8e08433google::LogMessage::SendToLog()@0x7f32a8e0615bgoogle::LogMessage::Flush()@0x7f32a8e08e1egoogle::LogMessageFatal::~LogMessageFatal()@0x7f32a9373780caffe::Caffe::SetDevice()@0x40a45btrain()@0x406f70main@0x7f32a7f83b17(unknown)@0x40779a_start@(nil)(unknown)已放弃 (core dumped)
这是因为默认在 " --=/mnist/. " 中写着:
# caffe 求解模式: CPU or GPUsolver_mode: GPU将其改为solver_mode: CPU
就能正常运行
运行中显示的内容提示:
lichunlin@ThinkPad-T420:~/caffe$ ./examples/mnist/train_lenet.shI0226 16:32:44.089705 18561 caffe.cpp:197] Use CPU.使用CPUI0226 16:32:44.089968 18561 solver.cpp:45] 初始化超参数以下内容来自: "caffe/example/mnist/lenet_solver.prototxt(训练超参数文件) "test_iter: 100test_interval: 500.....snapshot: 5000snapshot_prefix: "examples/mnist/lenet"solver_mode: CPUnet: "examples/mnist/lenet_train_test.prototxt"train_state {level: 0stage: ""}I0226 16:32:44.090420 18561 solver.cpp:102] Creating training net from net file: examples/mnist/lenet_train_test.prototxt 按照" examples/mnist/lenet_train_test.prototxt " 文件建立网络I0226 16:32:44.090696 18561 net.cpp:296] The NetState phase (0) differed from the phase (1) specified by a rule in layer mnistI0226 16:32:44.090768 18561 net.cpp:296] The NetState phase (0) differed from the phase (1) specified by a rule in layer accuracyI0226 16:32:44.090971 18561 net.cpp:53] Initializing net from parameters: 初始化网络参数以下内容来自 " examples/mnist/lenet_train_test.prototxt "name: "LeNet"state {# 创建训练网络phase: TRAINlevel: 0stage: ""}layer {name: "mnist"type: "Data"top: "data"top: "label"include {phase: TRAIN}transform_param {scale: 0.00390625}data_param {source: "examples/mnist/mnist_train_lmdb"# 使用到的数据库路径batch_size: 64backend: LMDB}}......layer {name: "loss"type: "SoftmaxWithLoss"bottom: "ip2"bottom: "label"top: "loss"}I0226 16:32:44.097754 18561 layer_factory.hpp:77] Creating layer mnistI0226 16:32:44.097884 18561 db_lmdb.cpp:35] Opened lmdb examples/mnist/mnist_train_lmdb创建 各种训练 层I0226 16:32:44.097921 18561 net.cpp:86] Creating Layer mnist......# 显示当前内存占有I0226 16:32:44.098275 18561 net.cpp:139] Memory required for data: 200960......进行反馈回路I0226 16:32:44.104403 18561 layer_factory.hpp:77] Creating layer lossI0226 16:32:44.104418 18561 net.cpp:86] Creating Layer lossI0226 16:32:44.104429 18561 net.cpp:408] loss <- ip2I0226 16:32:44.104440 18561 net.cpp:408] loss <- labelI0226 16:32:44.104460 18561 net.cpp:382] loss -> lossI0226 16:32:44.104496 18561 layer_factory.hpp:77] Creating layer lossI0226 16:32:44.104522 18561 net.cpp:124] Setting up lossI0226 16:32:44.104537 18561 net.cpp:131] Top shape: (1)I0226 16:32:44.104553 18561 net.cpp:134]with loss weight 1I0226 16:32:44.104581 18561 net.cpp:139] Memory required for data: 5169924I0226 16:32:44.104593 18561 net.cpp:200] loss needs backward computation......I0226 16:32:44.104709 18561 net.cpp:244] This network produces output lossI0226 16:32:44.104732 18561 net.cpp:257] Network initialization done.创建 测试 网络(同上)......开始迭代(每100次显示一次,每500次保存一次)I0226 16:32:47.178556 18561 solver.cpp:239] Iteration 0 (-4.34403e-44 iter/s, 3.065s/100 iters), loss = 2.3588loss值I0226 16:32:47.178653 18561 solver.cpp:258]Train net output #0: loss = 2.3588 (* 1 = 2.3588 loss)......
上面的指令中 重要的部分是 " --=/mnist/. "
/mnist/. 是使用编写的训练超参数文件.文件中指定的超参数如下:
# 训练与测试 网络的 prototxt 文件所在位置net: "examples/mnist/lenet_train_test.prototxt"# test_iter specifies how many forward passes the test should carry out.# In the case of MNIST, we have test batch size 100 and 100 test iterations,# covering the full 10,000 testing images.# 测试截断迭代次数test_iter: 100# 训练时每迭代500次进行一次预测test_interval: 500# 网络的基础学习速率,冲量,权衰量base_lr: 0.01momentum: 0.9weight_decay: 0.0005# 学习速率的衰减策略lr_policy: "inv"gamma: 0.0001power: 0.75# 每100次迭代,屏幕打印一次display: 100# 最大的迭代次数max_iter: 10000# 每5000次迭代打印一次快照snapshot: 5000#快照保存位置snapshot_prefix: "examples/mnist/lenet"# 求解模式选择(CPU 或者GPU)solver_mode: CPU
同样的 在超参数文档中指定了网络结构 net: “/mnist/.” 也是使用了. 也是格式
4. 训练网络分析
上文提到了,训练网络位于 “/mnist/.” 也是使用格式.
那这就来看看 " . " 中的内容
# 网络名称为 LeNetname: "LeNet"layer {# 层的名称name: "mnist"# 层的类型为:数据层type: "Data"# top 表示输出# 输出 data 和labeltop: "data"top: "label"include {# 该层只有训练阶段有效phase: TRAIN}transform_param {# 数据变换使用的缩放因子scale: 0.00390625}# 数据来源参数data_param {source: "examples/mnist/mnist_train_lmdb"batch_size: 64backend: LMDB}}layer {name: "mnist"type: "Data"top: "data"top: "label"include {# 同上,该层只有测试阶段有效phase: TEST}transform_param {scale: 0.00390625}data_param {source: "examples/mnist/mnist_test_lmdb"batch_size: 100backend: LMDB}}# 卷积层layer {name: "conv1"# 层的类型为:卷积层type: "Convolution"# bottom 表示输入bottom: "data"# top 表示输出top: "conv1"# 第一个 pram 表示权值学习速率倍乘因子param {lr_mult: 1}# 第二个 pram 表示bias(偏移量)学习速率倍乘因子param {lr_mult: 2}# 卷积计算参数convolution_param {# 输出 的 通道数num_output: 20# 卷积核大小kernel_size: 5# 卷积的步长stride: 1# 权值使用 xavier 填充器weight_filler {type: "xavier"}# 偏移量使用 常数填充器,默认为0bias_filler {type: "constant"}}}#池化层layer {name: "pool1"# 层的类型为:池化层type: "Pooling"# 输入输出bottom: "conv1"top: "pool1"# 池化层参数pooling_param {# 使用最大池化pool: MAX# 池化核大小kernel_size: 2# 池化核移动步长stride: 2}}......# 全连接层layer {name: "ip1"# 层的类型为:全连接层type: "InnerProduct"bottom: "pool2"top: "ip1"param {lr_mult: 1}param {lr_mult: 2}# 全连接层参数inner_product_param {# 输出通道数num_output: 500weight_filler {type: "xavier"}bias_filler {type: "constant"}}}# 非线性层layer {name: "relu1"# 层的类型为:非线性层type: "ReLU"# 输入输出信息bottom: "ip1"top: "ip1"}......# 准确率计算层layer {name: "accuracy"# 层的类型为:准确率计算层type: "Accuracy"bottom: "ip2"bottom: "label"top: "accuracy"include {# 只有 测试阶段 有效phase: TEST}}# 损失层layer {name: "loss"# 层的类型为:softmaxloss 损失值计算层type: "SoftmaxWithLoss"bottom: "ip2"bottom: "label"top: "loss"}
【【C++ Caffe】Ubuntu下 手写体数字识别】训练结果可以看我的这个博客