本教程创建一个小的神经网络用于手写字符的识别。我们使用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_net
和net
,这两个网络分别记录着初始化网络和主网络。为了模块化,我们将模型分割成多个不同的部分。
- 数据输入(AddInput 函数)
- 主要的计算部分(AddLeNetModel 函数)
- 训练部分-梯度操作,参数更新等等 (AddTrainingOperators函数)
- 记录数据部分,比如需要展示训练过程的相关数据(AddBookkeepingOperators 函数)
- 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.
- 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.
- 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.
- 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")
- 定义网络
现在让我们将真正创建模型。前面写的函数将真正被执行。回忆我们四步。
-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.RunNet
200次,需要传入的参数是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')
现我们可以进行抽取数据和预测了
#数据可视化
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')
还记得我们创建的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())
译者注:这里译者不是很明白,test_model是如何从train_model获取参数的?有明白的小伙伴希望能在评论区分享一下。
MNIST教程就此结束。希望本教程能向你展示一些Caffe2的特征。