1. 前言
学习机器学习,一般并不会从 caffe 开始启蒙,一般都是使用 pytorch,tensorflow 等偏向于 python 的工具开始,所以讲解 caffe 的使用,默认大家对其他的 python 类开发工具有所了解.
这次讲解 caffe 的使用,使用的是MNIST 手写数字识别,首先是因为,最容易上手,并且正确率能达到比较高的水平,另外就是 caffe 的example 路径下有 MNIST相关的内容.
需要的预备知识:
C/C++
linux shell 脚本
如果没有相关知识,也可以试着读一下.
2. MNIST 数据文件下载
在路径 "caffe 路径/examples/mnist" 路径下,含有完整的 caffe 的 MNIST 项目.
在路径 "caffe 路径/build/examples/mnist" 路径下,有对 caffe 编译后,产生的对 caffe 的 MNIST 项目编译的结果.
首先来看 下载 MNIST 的脚本: " caffe 路径/data/mnist/get_mnist.sh "
#!/usr/bin/env sh
# 这个脚本用于下载并解压 mnist 文件
DIR="$( cd "$(dirname "$0")" ; pwd -P )"
cd "$DIR"
# printf
echo "Downloading..."
# for循环 设置要下载文件的名称
for fname in train-images-idx3-ubyte train-labels-idx1-ubyte t10k-images-idx3-ubyte t10k-labels-idx1-ubyte
do
if [ ! -e $fname ]; then
# 进行下载文件
wget --no-check-certificate http://yann.lecun.com/exdb/mnist/${fname}.gz
# 进行解压文件
gunzip ${fname}.gz
fi
done
使用 "./get_mnist.sh" 运行该程序.显示如下:
lee@lee:~/Documents/caffe/data/mnist$ ./get_mnist.sh
Downloading...
--2019-02-26 13:39:50-- http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz
Resolving yann.lecun.com (yann.lecun.com)... 216.165.22.6
Connecting to yann.lecun.com (yann.lecun.com)|216.165.22.6|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 9912422 (9.5M) [application/x-gzip]
Saving to: ‘train-images-idx3-ubyte.gz’
train-images-idx3-u 100%[===================>] 9.45M 199KB/s in 49s
2019-02-26 13:40:40 (197 KB/s) - ‘train-images-idx3-ubyte.gz’ saved [9912422/9912422]
......
t10k-labels-idx1-ub 100%[===================>] 4.44K --.-KB/s in 0s
2019-02-26 13:40:52 (152 MB/s) - ‘t10k-labels-idx1-ubyte.gz’ saved [4542/4542]
下载的数据是二进制格式,我们可以使用 vim 以二进制格式打开命令
vim -b t10k-images-idx3-ubyte
并在 vim 下方输入 ":%!xxd" 将文本文件强制以十六进制转换
如下
注:似乎,打开过文件会被破坏,建议删除重新执行一次.
但是问题出现了,caffe 并不支持 二进制文件,只支持LMDB 或者levelDB,所以我们需要将数据进行转换.进行数据转换的脚本在 " caffe路径/examples/mnist/create_mnist.sh"
进入到 " caffe路径 ",进行执行 "./examples/mnist/create_mnist.sh",显示如下:
lee@lee:~/Documents/caffe$ ./examples/mnist/create_mnist.sh
Creating lmdb...
I0226 14:39:08.853714 4944 db_lmdb.cpp:35] Opened lmdb examples/mnist/mnist_train_lmdb
I0226 14:39:08.853905 4944 convert_mnist_data.cpp:88] A total of 60000 items.
I0226 14:39:08.853924 4944 convert_mnist_data.cpp:89] Rows: 28 Cols: 28
I0226 14:39:13.977677 4944 convert_mnist_data.cpp:108] Processed 60000 files.
F0226 14:39:13.994539 4953 convert_mnist_data.cpp:59] Check failed: magic == 2051 (808464432 vs. 2051) Incorrect image file magic.
*** Check failure stack trace: ***
@ 0x7f71094415cd google::LogMessage::Fail()
@ 0x7f7109443433 google::LogMessage::SendToLog()
@ 0x7f710944115b google::LogMessage::Flush()
@ 0x7f7109443e1e google::LogMessageFatal::~LogMessageFatal()
@ 0x4031d3 convert_dataset()
@ 0x40226a main
@ 0x7f71087c2b17 (unknown)
@ 0x4022ba _start
@ (nil) (unknown)
Aborted (core dumped)
接下来分析一下 "create_mnist.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/mnist
DATA=data/mnist
BUILD=build/examples/mnist
BACKEND="lmdb"
# printf
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."
很明显最重要的内容就是
$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}
但是 " convert_mnist_data.bin " 是编译后的二进制文件,并没有任何信息,所以我们应该去了解 " caffe目录/example/mnist/convert_mnist_data.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 <gflags/gflags.h>
#ifndef GFLAGS_GFLAGS_H_
namespace gflags = google;
#endif
FLAGS_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 {
// 初始化 glog
google::InitGoogleLogging(argv[0]);
// 进行数据转换(实际操作)
convert_dataset(argv[1], argv[2], argv[3], db_backend);
}
return 0;
}
主函数中用到了大量的 gflags 和 glogs 的函数,都是用来对命令行与 日志相关,具体的真实干活的是 convert_dataset 函数.
// 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<char*>(&magic), 4);
magic = swap_endian(magic);
// 校验魔数是否一致
CHECK_EQ(magic, 2051) << "Incorrect image file magic.";
// 同理校验 label_file 的正确性
label_file.read(reinterpret_cast<char*>(&magic), 4);
magic = swap_endian(magic);
CHECK_EQ(magic, 2049) << "Incorrect label file magic.";
// 读取 条目的个数
image_file.read(reinterpret_cast<char*>(&num_items), 4);
num_items = swap_endian(num_items);
// 读取标签的个数
label_file.read(reinterpret_cast<char*>(&num_labels), 4);
num_labels = swap_endian(num_labels);
CHECK_EQ(num_items, num_labels);
// 读取 条目(图片)的行像素点个数
image_file.read(reinterpret_cast<char*>(&rows), 4);
rows = swap_endian(rows);
// 读取 条目(图片)的列像素点个数
image_file.read(reinterpret_cast<char*>(&cols), 4);
cols = swap_endian(cols);
/* 来自
* #include "boost/scoped_ptr.hpp"
* 不用再分 levelDB 与LMDB
*/
// 创建新 数据库DB
scoped_ptr<db::DB> db(db::GetDB(db_backend));
// 打开数据库,并指定数据库的路径
db->Open(db_path, db::NEW);
// 创建对数据库的 操作句柄(txn) Transaction 表示 事务,数据库内容的知识
scoped_ptr<db::Transaction> txn(db->NewTransaction());
// Storing to db
char 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();
}
convert_dataset 利用对 二进制文件的了解,对二进制文件进行解析,最后将其以数据库的形式保存.其中运用了一些数据库相关的知识,并且将数据库变成了面向对象的编程.
解决最后的问题,数据的大小端问题.
/* 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路径/example/mnist/train_lenet.sh"
其中调用了真正执行的语句
#!/usr/bin/env sh
set -e
./build/tools/caffe train --solver=examples/mnist/lenet_solver.prototxt
"./example/mnist/train_lenet.sh"运行后,可能你会看见这样的显示:
lee@lee:~/Documents/caffe$ ./build/tools/caffe train --solver=examples/mnist/lenet_solver.prototxt
I0226 16:01:17.276763 16197 caffe.cpp:204] Using GPUs 0
F0226 16:01:17.277010 16197 common.cpp:66] Cannot use GPU in CPU-only Caffe: check mode.
*** Check failure stack trace: ***
@ 0x7f32a8e065cd google::LogMessage::Fail()
@ 0x7f32a8e08433 google::LogMessage::SendToLog()
@ 0x7f32a8e0615b google::LogMessage::Flush()
@ 0x7f32a8e08e1e google::LogMessageFatal::~LogMessageFatal()
@ 0x7f32a9373780 caffe::Caffe::SetDevice()
@ 0x40a45b train()
@ 0x406f70 main
@ 0x7f32a7f83b17 (unknown)
@ 0x40779a _start
@ (nil) (unknown)
已放弃 (core dumped)
这是因为默认在 " --solver=examples/mnist/lenet_solver.prototxt " 中写着:
# caffe 求解模式: CPU or GPU
solver_mode: GPU
将其改为
solver_mode: CPU
就能正常运行
运行中显示的内容提示:
lee@lee:~/Documents/caffe$ ./examples/mnist/train_lenet.sh
I0226 16:32:44.089705 18561 caffe.cpp:197] Use CPU.使用CPU
I0226 16:32:44.089968 18561 solver.cpp:45] 初始化超参数
以下内容来自: "caffe/example/mnist/lenet_solver.prototxt(训练超参数文件) "
test_iter: 100
test_interval: 500
.....
snapshot: 5000
snapshot_prefix: "examples/mnist/lenet"
solver_mode: CPU
net: "examples/mnist/lenet_train_test.prototxt"
train_state {
level: 0
stage: ""
}
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 mnist
I0226 16:32:44.090768 18561 net.cpp:296] The NetState phase (0) differed from the phase (1) specified by a rule in layer accuracy
I0226 16:32:44.090971 18561 net.cpp:53] Initializing net from parameters: 初始化网络参数
以下内容来自 " examples/mnist/lenet_train_test.prototxt "
name: "LeNet"
state { # 创建训练网络
phase: TRAIN
level: 0
stage: ""
}
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: 64
backend: LMDB
}
}
......
layer {
name: "loss"
type: "SoftmaxWithLoss"
bottom: "ip2"
bottom: "label"
top: "loss"
}
I0226 16:32:44.097754 18561 layer_factory.hpp:77] Creating layer mnist
I0226 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 loss
I0226 16:32:44.104418 18561 net.cpp:86] Creating Layer loss
I0226 16:32:44.104429 18561 net.cpp:408] loss <- ip2
I0226 16:32:44.104440 18561 net.cpp:408] loss <- label
I0226 16:32:44.104460 18561 net.cpp:382] loss -> loss
I0226 16:32:44.104496 18561 layer_factory.hpp:77] Creating layer loss
I0226 16:32:44.104522 18561 net.cpp:124] Setting up loss
I0226 16:32:44.104537 18561 net.cpp:131] Top shape: (1)
I0226 16:32:44.104553 18561 net.cpp:134] with loss weight 1
I0226 16:32:44.104581 18561 net.cpp:139] Memory required for data: 5169924
I0226 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 loss
I0226 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.3588
loss值
I0226 16:32:47.178653 18561 solver.cpp:258] Train net output #0: loss = 2.3588 (* 1 = 2.3588 loss)
......
上面的指令中 重要的部分是 " --solver=examples/mnist/lenet_solver.prototxt "
examples/mnist/lenet_solver.prototxt 是使用 protobuffer 编写的训练超参数文件.文件中指定的超参数如下:
# 训练与测试 网络的 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.01
momentum: 0.9
weight_decay: 0.0005
# 学习速率的衰减策略
lr_policy: "inv"
gamma: 0.0001
power: 0.75
# 每100次迭代,屏幕打印一次
display: 100
# 最大的迭代次数
max_iter: 10000
# 每5000次迭代打印一次快照
snapshot: 5000
#快照保存位置
snapshot_prefix: "examples/mnist/lenet"
# 求解模式选择(CPU 或者GPU)
solver_mode: CPU
同样的 在超参数文档中指定了网络结构 net: "examples/mnist/lenet_train_test.prototxt" 也是使用了lenet_train_test.prototxt 也是 prototxt 格式
5. 训练 网络 分析
上文提到了,训练网络位于 "examples/mnist/lenet_train_test.prototxt" 也是使用 protoBuffer 格式.
那这就来看看 " lenet_train_test.prototxt " 中的内容
# 网络名称为 LeNet
name: "LeNet"
layer {
# 层的名称
name: "mnist"
# 层的类型为:数据层
type: "Data"
# top 表示输出
# 输出 data 和label
top: "data"
top: "label"
include {
# 该层只有训练阶段有效
phase: TRAIN
}
transform_param {
# 数据变换使用的缩放因子
scale: 0.00390625
}
# 数据来源参数
data_param {
source: "examples/mnist/mnist_train_lmdb"
batch_size: 64
backend: 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: 100
backend: 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"
}
# 偏移量使用 常数填充器,默认为0
bias_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: 500
weight_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"
}
6. caffe 的 一些 技巧
既然已经学习了一些其他的 机器学习框架,只是在另一个框架进行套用,只是一些名称的不同,所以我们还要搞清楚 caffe 支持哪些层.
在 我们安装的 "caffe 路径/docs/tutorial/layers" 这个路径下就有所有层的介绍
lee@lee:~/Documents/caffe/docs/tutorial/layers$ tree .
.
├── absval.md
├── accuracy.md
├── argmax.md
├── batchnorm.md
├── batchreindex.md
├── bias.md
├── bnll.md
├── clip.md
├── concat.md
├── contrastiveloss.md
├── convolution.md
├── crop.md
├── data.md
├── deconvolution.md
├── dropout.md
├── dummydata.md
├── eltwise.md
├── elu.md
├── embed.md
├── euclideanloss.md
├── exp.md
├── filter.md
├── flatten.md
├── hdf5data.md
├── hdf5output.md
├── hingeloss.md
├── im2col.md
├── imagedata.md
├── infogainloss.md
├── innerproduct.md
├── input.md
├── log.md
├── lrn.md
├── lstm.md
├── memorydata.md
├── multinomiallogisticloss.md
├── mvn.md
├── parameter.md
├── pooling.md
├── power.md
├── prelu.md
├── python.md
├── recurrent.md
├── reduction.md
├── relu.md
├── reshape.md
├── rnn.md
├── scale.md
├── sigmoidcrossentropyloss.md
├── sigmoid.md
├── silence.md
├── slice.md
├── softmax.md
├── softmaxwithloss.md
├── split.md
├── spp.md
├── tanh.md
├── threshold.md
├── tile.md
└── windowdata.md
这也局限了 caffe 比如 SSD 算法的输出并不是固定的,对于 SSD 需要专门的 caffe-SSD 的支持.
同样 prototxt 带来了很多的好处,比如可以优秀的可视化工具,连接如下:
http://ethereon.github.io/netscope/#/editor
出现一个报错,
解决方案:netscope,Can't infer network data shapes