本教程創建一個小的神經網絡用於手寫字符的識別。我們使用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的特征。