Caffe2 手写字符识别(MNIST - Create a CNN from Scratch)[8]

本教程创建一个小的神经网络用于手写字符的识别。我们使用MNIST数据集进行训练和测试。这个数据集的训练集包含60000张来自500个人的手写字符的图像,测试集包含10000张独立于训练集的测试图像。你可以参看本教程的Ipython notebook。

本节中,我们使用CNN的模型助手来创建网络并初始化参数。首先import所需要的依赖库。

%matplotlib inline
from matplotlib import pyplot
import numpy as np
import os
import shutil
from caffe2.python import core, cnn, net_drawer, workspace, visualize
# 如果你想更加详细的了解初始化的过程,那么你可以把caffe2_log_level=0 改为-1
core.GlobalInit(['caffe2', '--caffe2_log_level=0'])
caffe2_root = "~/caffe2"
print("Necessities imported!")
数据准备

我们会跟踪训练过程的数据,并保存到一个本地的文件夹。我们需要先设置一个数据文件和根文件夹。在数据文件夹里,放置用于训练和测试的MNIST数据集。如果没有数据集,那么你可以到这里下载MNIST Dataset,然后解压数据集和标签。

./make_mnist_db --channel_first --db leveldb --image_file ~/Downloads/train-images-idx3-ubyte --label_file ~/Downloads/train-labels-idx1-ubyte --output_file ~/caffe2/caffe2/python/tutorials/tutorial_data/mnist/mnist-train-nchw-leveldb

./make_mnist_db --channel_first --db leveldb --image_file ~/Downloads/t10k-images-idx3-ubyte --label_file ~/Downloads/t10k-labels-idx1-ubyte --output_file ~/caffe2/caffe2/python/tutorials/tutorial_data/mnist/mnist-test-nchw-leveldb

这段代码实现和上面的一样的功能

# 这部分将你的图像转换成leveldb
current_folder = os.getcwd()
data_folder = os.path.join(current_folder, 'tutorial_data', 'mnist')
root_folder = os.path.join(current_folder, 'tutorial_files', 'tutorial_mnist')
image_file_train = os.path.join(data_folder, "train-images-idx3-ubyte")
label_file_train = os.path.join(data_folder, "train-labels-idx1-ubyte")
image_file_test = os.path.join(data_folder, "t10k-images-idx3-ubyte")
label_file_test = os.path.join(data_folder, "t10k-labels-idx1-ubyte")

def DownloadDataset(url, path):
    import requests, zipfile, StringIO
    print "Downloading... ", url, " to ", path
    r = requests.get(url, stream=True)
    z = zipfile.ZipFile(StringIO.StringIO(r.content))
    z.extractall(path)
if not os.path.exists(data_folder):
    os.makedirs(data_folder)
if not os.path.exists(label_file_train):
    DownloadDataset("https://s3.amazonaws.com/caffe2/datasets/mnist/mnist.zip", data_folder)

def GenerateDB(image, label, name):
    name = os.path.join(data_folder, name)
    print 'DB name: ', name
    syscall = "/usr/local/binaries/make_mnist_db --channel_first --db leveldb --image_file " + image + " --label_file " + label + " --output_file " + name
    print "Creating database with: ", syscall
    os.system(syscall)

# 生成leveldb
GenerateDB(image_file_train, label_file_train, "mnist-train-nchw-leveldb")
GenerateDB(image_file_test, label_file_test, "mnist-test-nchw-leveldb")

if os.path.exists(root_folder):
    print("Looks like you ran this before, so we need to cleanup those old workspace files...")
    shutil.rmtree(root_folder)

os.makedirs(root_folder)
workspace.ResetWorkspace(root_folder)

print("training data folder:"+data_folder)
print("workspace root folder:"+root_folder)
模型创建

CNNModelHelper封装了很多函数,它能将参数初始化和真实的计算分成两个网络中实现。底层实现是,CNNModelHelper有两个网络param_init_netnet,这两个网络分别记录着初始化网络和主网络。为了模块化,我们将模型分割成多个不同的部分。
- 数据输入(AddInput 函数)
- 主要的计算部分(AddLeNetModel 函数)
- 训练部分-梯度操作,参数更新等等 (AddTrainingOperators函数)
- 记录数据部分,比如需要展示训练过程的相关数据(AddBookkeepingOperators 函数)

  1. AddInput会从一个DB中载入数据。我们将MNIST保存为像素值,并且我们用浮点数进行计算,所以我们的数据也必须是Float类型。为了数值稳定性,我们将图像数据归一化到[0,1]而不是[0,255]。注意,我们做的事in-place操作,会覆盖原来的数据,因为我们不需要归一化前的数据。准备数据这个操作,在后向传播时,不需要进行梯度计算。所以我们使用StopGradient来告诉梯度生成器:“不用将梯度传递给我。”
def AddInput(model, batch_size, db, db_type):
    # 载入数据和标签
    data_uint8, label = model.TensorProtosDBInput(
        [], ["data_uint8", "label"], batch_size=batch_size,
        db=db, db_type=db_type)
    # 转化为 float
    data = model.Cast(data_uint8, "data", to=core.DataType.FLOAT)
    #归一化到 [0,1]
    data = model.Scale(data, data, scale=float(1./256))
    # 后向传播不需要梯度
    data = model.StopGradient(data, data)
    return data, label
print("Input function created.")

输出

Input function created.
  1. AddLeNetModel输出softmax.
def AddLeNetModel(model, data):
    conv1 = model.Conv(data, 'conv1', 1, 20, 5)
    pool1 = model.MaxPool(conv1, 'pool1', kernel=2, stride=2)
    conv2 = model.Conv(pool1, 'conv2', 20, 50, 5)
    pool2 = model.MaxPool(conv2, 'pool2', kernel=2, stride=2)
    fc3 = model.FC(pool2, 'fc3', 50 * 4 * 4, 500)
    fc3 = model.Relu(fc3, fc3)
    pred = model.FC(fc3, 'pred', 500, 10)
    softmax = model.Softmax(pred, 'softmax')
    return softmax
print("Model function created.")
Model function created.
  1. AddTrainingOperators函数函数用于添加训练操作。
    AddAccuracy函数输出模型的准确率,我们会在下一个函数使用它来跟踪准确率。
def AddAccuracy(model, softmax, label):
    accuracy = model.Accuracy([softmax, label], "accuracy")
    return accuracy
print("Accuracy function created.")
Accuracy function created.

首先添加一个op:LabelCrossEntropy,用于计算输入和lebel的交叉熵。这个操作在得到softmax后和计算loss前。输入是[softmax, label],输出交叉熵用xent表示。

xent = model.LabelCrossEntropy([softmax, label], 'xent')

AveragedLoss将交叉熵作为输入,并计算出平均损失loss

loss = model.AveragedLoss(xent, "loss")

AddAccuracy为了记录训练过程,我们使用AddAccuracy 函数来计算。

AddAccuracy(model, softmax, label)

接下来这步至关重要:我们把所有梯度计算添加到模型上。梯度是根据我们前面的loss计算得到的。

model.AddGradientOperators([loss])

然后进入迭代

ITER = model.Iter("iter")

更新学习率使用策略是lr = base_lr * (t ^ gamma),注意我们是在最小化,所以基础学率是负数,这样我们才能向山下走。

LR = model.LearningRate(ITER, "LR", base_lr=-0.1, policy="step", stepsize=1, gamma=0.999 ) 
#ONE是一个在梯度更新阶段用的常量。只需要创建一次,并放在param_init_net中。
ONE = model.param_init_net.ConstantFill([], "ONE", shape=[1], value=1.0)

现在对于每一和参数,我们做梯度更新。注意我们如何获取每个参数的梯度——CNNModelHelper保持跟踪这些信息。更新的方式很简单,是简单的相加:param = param + param_grad * LR

for param in model.params:
    param_grad = model.param_to_grad[param]
    model.WeightedSum([param, ONE, param_grad, LR], param)   

我们需要每隔一段时间检查参数。这可以通过Checkpoint 操作。这个操作有一个参数every表示每多少次迭代进行一次这个操作,防止太频繁去检查。这里,我们每20次迭代进行一次检查。

model.Checkpoint([ITER] + model.params, [],
               db="mnist_lenet_checkpoint_%05d.leveldb",
               db_type="leveldb", every=20)

然后我们得到整个AddTrainingOperators函数如下:

def AddTrainingOperators(model, softmax, label):
    # 计算交叉熵
    xent = model.LabelCrossEntropy([softmax, label], 'xent')
    # 计算loss
    loss = model.AveragedLoss(xent, "loss")
    #跟踪模型的准确率
    AddAccuracy(model, softmax, label)
    #添加梯度操作
    model.AddGradientOperators([loss])
    # 梯度下降
    ITER = model.Iter("iter")
    # 学习率
    LR = model.LearningRate(
        ITER, "LR", base_lr=-0.1, policy="step", stepsize=1, gamma=0.999 )
    ONE = model.param_init_net.ConstantFill([], "ONE", shape=[1], value=1.0)
    # 梯度更新
    for param in model.params:
        param_grad = model.param_to_grad[param]
        model.WeightedSum([param, ONE, param_grad, LR], param)
    # 每迭代20次检查一次
    # you may need to delete tutorial_files/tutorial-mnist to re-run the tutorial
    model.Checkpoint([ITER] + model.params, [],
                   db="mnist_lenet_checkpoint_%05d.leveldb",
                   db_type="leveldb", every=20)
print("Training function created.")
Training function created.
  1. **AddBookkeepingOperators **添加一些记录操作,这些操作不会影响训练过程。他们只是收集数据和打印出来或者写到log里面去。
def AddBookkeepingOperators(model):
    # 输出 blob的内容. to_file=1 表示输出到文件,文件保存的路径是 root_folder/[blob name]
    model.Print('accuracy', [], to_file=1)
    model.Print('loss', [], to_file=1)
    # Summarizes 给出一些参数比如均值,方差,最大值,最小值
    for param in model.params:
        model.Summarize(param, [], to_file=1)
        model.Summarize(model.param_to_grad[param], [], to_file=1)
print("Bookkeeping function created")
  1. 定义网络
    现在让我们将真正创建模型。前面写的函数将真正被执行。回忆我们四步。
-data input  
-main computation
-training
-bookkeeping

在我们读进数据前,我们需要定义我们训练模型。我们将使用到前面定义的所有东西。我们将在MNIST数据集上使用NCHW的储存顺序。

train_model = cnn.CNNModelHelper(order="NCHW", name="mnist_train")
data, label = AddInput(train_model, batch_size=64,
              db=os.path.join(data_folder, 'mnist-train-nchw-leveldb'), db_type='leveldb')
softmax = AddLeNetModel(train_model, data)
AddTrainingOperators(train_model, softmax, label)
AddBookkeepingOperators(train_model)
# Testing model. 我们设置batch=100,这样迭代100次就能覆盖10000张测试图像
# 对于测试模型,我们需要数据输入 ,LeNetModel,和准确率三部分
#注意到init_params 设置为False,是因为我们从训练网络获取参数。
test_model = cnn.CNNModelHelper(order="NCHW", name="mnist_test", init_params=False)
data, label = AddInput(test_model, batch_size=100,
    db=os.path.join(data_folder, 'mnist-test-nchw-leveldb'), db_type='leveldb')
softmax = AddLeNetModel(test_model, data)
AddAccuracy(test_model, softmax, label)

# Deployment model. 我们仅需要LeNetModel 部分
deploy_model = cnn.CNNModelHelper(order="NCHW", name="mnist_deploy", init_params=False)
AddLeNetModel(deploy_model, "data")
#你可能好奇deploy_model的param_init_net 发生了什么,在这节中,我们没有使用它,
#因为在deployment 阶段,我们不会随机初始化参数,而是从本地载入。
print('Created training and deploy models.')

现在让我们用caffe2的可视化工具看看Training和Deploy模型是什么样子的。如果下面的命令运行失败,那可能是因为你的机器没有安装graphviz。可以用如下命令安装:

sudo yum install graphviz #ubuntu 用户sudo apt-get install graphviz 

图看起来可能很小,右键点击在新的窗口打开就能看清。

from IPython import display
graph = net_drawer.GetPydotGraph(train_model.net.Proto().op, "mnist", rankdir="LR")
display.Image(graph.create_png(), width=800)

现在上图展示了训练阶段的一切东西。白色的节点是blobs,绿色的矩形节点是operators.你可能留意到大规模的像火车轨道一样的平行线。这些依赖关系从前前向传播的blobs指向到后向传播的操作。
让我们仅仅展示必要的依赖关系和操作。如果你细心看,你会发现,左半图式前向传播,右半图式后向传播,在最右边是一系列参数更新操作和summarization .

graph = net_drawer.GetPydotGraphMinimal(
    train_model.net.Proto().op, "mnist", rankdir="LR", minimal_dependency=True)
display.Image(graph.create_png(), width=800)

现在我们可以通过Python来跑起网络,记住,当我们跑起网络时,我们随时可以从网络中拿出blob数据,下面先来展示下如何进行这个操作。

我们重申一下,CNNModelHelper 类目前没有执行任何东西。他目前做的仅仅是声明网络,只是简单的创建了protocol buffers.例如我们可以展示网络一部分序列化的protobuf。

print(str(train_model.param_init_net.Proto())[:400] + '\n...')

当然,我们也可以把protobuf写到本地磁盘中去,这样可以方便的查看。你会发现这些protobuf和以前的Caffe网络定义很相似。

with open(os.path.join(root_folder, "train_net.pbtxt"), 'w') as fid:
    fid.write(str(train_model.net.Proto()))
with open(os.path.join(root_folder, "train_init_net.pbtxt"), 'w') as fid:
    fid.write(str(train_model.param_init_net.Proto()))
with open(os.path.join(root_folder, "test_net.pbtxt"), 'w') as fid:
    fid.write(str(test_model.net.Proto()))
with open(os.path.join(root_folder, "test_init_net.pbtxt"), 'w') as fid:
    fid.write(str(test_model.param_init_net.Proto()))
with open(os.path.join(root_folder, "deploy_net.pbtxt"), 'w') as fid:
    fid.write(str(deploy_model.net.Proto()))
print("Protocol buffers files have been created in your root folder: "+root_folder)

现在,让我们进入训练过程。我们使用Python来训练。当然也可以使用C++接口来训练。这留在另一个教程讨论。

训练网络

首先,初始化网络是必须的

workspace.RunNetOnce(train_model.param_init_net)

接着我们创建训练网络并,加载到workspace中去。

workspace.CreateNet(train_model.net)

然后设置迭代200次,并把准确率和loss保存到两个np矩阵中去

total_iters = 200
accuracy = np.zeros(total_iters)
loss = np.zeros(total_iters)

网络和跟踪准确loss都配置好后,我们循环调用workspace.RunNet200次,需要传入的参数是train_model.net.Proto().name.每一次迭代,我们计算准确率和loss。

for i in range(total_iters):
    workspace.RunNet(train_model.net.Proto().name)
    accuracy[i] = workspace.FetchBlob('accuracy')
    loss[i] = workspace.FetchBlob('loss')

最后我们可以用pyplot画出结果。

# 参数初始化只需跑一次
workspace.RunNetOnce(train_model.param_init_net)
# 创建网络
workspace.CreateNet(train_model.net)
#设置迭代数和跟踪accuracy & loss
total_iters = 200
accuracy = np.zeros(total_iters)
loss = np.zeros(total_iters)
# 我们迭代200次
for i in range(total_iters):
    workspace.RunNet(train_model.net.Proto().name)
    accuracy[i] = workspace.FetchBlob('accuracy')
    loss[i] = workspace.FetchBlob('loss')
# 迭代完画出结果
pyplot.plot(loss, 'b')
pyplot.plot(accuracy, 'r')
pyplot.legend(('Loss', 'Accuracy'), loc='upper right')
Caffe2 手写字符识别(MNIST - Create a CNN from Scratch)[8]_第1张图片

现我们可以进行抽取数据和预测了

#数据可视化
pyplot.figure()
data = workspace.FetchBlob('data')
_ = visualize.NCHW.ShowMultiple(data)
pyplot.figure()
softmax = workspace.FetchBlob('softmax')
_ = pyplot.plot(softmax[0], 'ro')
pyplot.title('Prediction for the first image')
Caffe2 手写字符识别(MNIST - Create a CNN from Scratch)[8]_第2张图片

Caffe2 手写字符识别(MNIST - Create a CNN from Scratch)[8]_第3张图片

还记得我们创建的test net吗?我们将跑一遍test net测试准确率。**注意,虽然test_model的参数来自train_model,但是仍然需要初始化test_model.param_init_net **。这次,我们只需要追踪准确率,并且只迭代100次。

workspace.RunNetOnce(test_model.param_init_net)
workspace.CreateNet(test_model.net)
test_accuracy = np.zeros(100)
for i in range(100):
    workspace.RunNet(test_model.net.Proto().name)
    test_accuracy[i] = workspace.FetchBlob('accuracy')
pyplot.plot(test_accuracy, 'r')
pyplot.title('Acuracy over test batches.')
print('test_accuracy: %f' % test_accuracy.mean())

Caffe2 手写字符识别(MNIST - Create a CNN from Scratch)[8]_第4张图片

译者注:这里译者不是很明白,test_model是如何从train_model获取参数的?有明白的小伙伴希望能在评论区分享一下。

MNIST教程就此结束。希望本教程能向你展示一些Caffe2的特征。

转载请注明出处:http://www.jianshu.com/c/cf07b31bb5f2

你可能感兴趣的:(Caffe2 手写字符识别(MNIST - Create a CNN from Scratch)[8])