Caffe2: Tutorial: MNIST – CNN をスクラッチから作成する

Caffe2: Tutorial: MNIST – CNN をスクラッチから作成する
翻訳 : (株)クラスキャット セールスインフォメーション
日時 : 04/22/2017

* 本ページは、Caffe2 Tutorials の MNIST を動作確認・翻訳した上で適宜、補足説明したものです:
https://github.com/caffe2/caffe2/blob/master/caffe2/python/tutorials/MNIST.ipynb

 

本文

このチュートリアルでは、小さいとは言え、実際の CNN モデルをどのように訓練するかを示します。旧いけれど良い MNIST データセットと LeNet モデルを使用しますが、sigmoid 活性化を ReLU に置き換えるというわずかな変更を伴っています。

cnn モデル・ヘルパーを使用します – これはパラメータ初期化を自然に処理します。

最初に、必要なものを import しましょう。

%matplotlib inline
from matplotlib import pyplot
import numpy as np
import os
import shutil
from IPython import display

from caffe2.python import core, cnn, net_drawer, workspace, visualize

# If you would like to see some really detailed initializations,
# you can change --caffe2_log_level=0 to --caffe2_log_level=-1
core.GlobalInit(['caffe2', '--caffe2_log_level=0'])
# set this where the root of caffe2 is installed
caffe2_root = "~/caffe2"
print("Necessities imported!")
Necessities imported!

訓練時の統計を追跡してこれらをディスク上のローカルフォルダにストアします。データのためのデータフォルダと統計のためのルート・フォルダをセットアップする必要があります。これらのフォルダは既に保持しているでしょう、そしてデータフォルダでは MNIST データセットは、このチュートリアルのためには訓練セットとテストセットの両者について leveldb データベースとしてセットアップされるべきです。

もしこれらのフォルダがないのであれば、MNIST データセットをダウンロードして、データセットとラベルを g/unzip して、それから /caffe2/build/caffe2/binaries/ か /usr/local/binaries/ でバイナリを見つけて次を実行する必要があります、けれども下のコードブロックはこれを行なうことを試みますので、最初にそれを試してください。

  • ./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
# This section preps your image and test set in a leveldb
current_folder = os.getcwd()

data_folder = os.path.join(current_folder, 'tutorial_data', 'mnist')
root_folder = os.path.join(current_folder, 'tutorial_files', '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")

# Get the dataset if it is missing
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)

def GenerateDB(image, label, name):
    name = os.path.join(data_folder, name)
    print 'DB: ', name
    if not os.path.exists(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)
    else:
        print "Database exists already. Delete the folder if you have issues/corrupted DB, then rerun this."
        if os.path.exists(os.path.join(name, "LOCK")):
            print "Deleting the pre-existing lock file"
            os.remove(os.path.join(name, "LOCK"))

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)
    
if os.path.exists(root_folder):
    print("Looks like you ran this before, so we need to cleanup those old files...")
    shutil.rmtree(root_folder)
    
os.makedirs(root_folder)
workspace.ResetWorkspace(root_folder)

# (Re)generate the levledb database (known to get corrupted...) 
GenerateDB(image_file_train, label_file_train, "mnist-train-nchw-leveldb")
GenerateDB(image_file_test, label_file_test, "mnist-test-nchw-leveldb")

    
print("training data folder:" + data_folder)
print("workspace root folder:" + root_folder)

 
CNNModelHelper を使用します、これはラッパー関数のセットを持ち、自動的にパラメータ初期化と実際の計算を2つのネットワークに分割します。その裏では、CNNModelHelper オブジェクトは2つの基礎となる net、param_init_net と net を持ち、それぞれネットワーク初期化と主となるネットワークを記録し続けます。

モジュール化のために、モデルを複数の異なるパートに分割します :

  • (1) データ入力パート (AddInput 関数)
  • (2) 主要計算パート (AddLeNetModel 関数)
  • (3) 訓練パート – 勾配演算、更新 etc. を追加します。 (AddTrainingOperators 関数)
  • (4) bookkeeping パート、ここでは精査のための統計をプリントアウトするだけです。 (AddBookkeepingOperators 関数)

AddInput は DB からデータをロードします。MNIST データをピクセル値にストアし、バッチ処理後にはこれは以下を与えます :

  • – `(batch_size, num_channels, width, height)` のデータ
    • – この場合はデータ型 *uint8* の `[batch_size, 1, 28, 28]`
  • – データ型 *int* の shape `[batch_size]` のラベル

float 計算を行ないますので、データを float データ型にキャストします。より良い数値安定性のために、データを [0, 255] の範囲で表現する代わりに、それらを [0, 1] にスケールダウンします。この演算子のために in-place 計算を行なうことに注意してください : pre-scale データは必要ありません。さて、backward パスを計算する時、backward パスのための勾配計算は必要ありません。StopGradient はまさにこれを行ないます : forward パスではそれは何もしませんそして backward パスではそれが行なうことの全ては勾配ジェネレータに “勾配は自分を通過する必要はない” と伝えるだけです。

# For the sake of modularity, we will separate the model to multiple different parts:
# (1) The data input part
# (2) The main computation part
# (3) The training part - adding gradient operators, update, etc.
# (4) The bookkeeping part, where we just print out statistics for inspection.

def AddInput(model, batch_size, db, db_type):
    """Adds the data input part."""
    # Load the data from a DB. Now, we store MNIST data in pixel values, so after
    # batching, this will give us data with shape [batch_size, 1, 28, 28] of data
    # type uint8, and label with shape [batch_size,] of data type int.
    data_uint8, label = model.TensorProtosDBInput(
        [], ["data_uint8", "label"], batch_size=batch_size,
        db=db, db_type=db_type)
    # Since we are going to do float computations, what we will do is to cast the
    # data to float.
    data = model.Cast(data_uint8, "data", to=core.DataType.FLOAT)
    # For better numerical stability, instead of representing data in [0, 255] range
    # we will scale them down to [0, 1]. Note that we are doing in-place computation
    # for this operator: we don't need the pre-scale data.
    data = model.Scale(data, data, scale=float(1./256))
    # Now, when computing the backward pass, we will not need the gradient computation
    # for the backward pass. StopGradient does exactly that: in the forward pass it
    # does nothing and in the backward pass all it does is to tell the gradient
    # generator "the gradient does not need to pass through me".
    data = model.StopGradient(data, data)
    return data, label

print("Input function created.")

このポイントで確率に変換するネットワークから出力される予測を見る必要があります。”見ているこの数字が 5 である確率は何か”、あるいは “これは 7 であるか”、等々。結果は 0 と 1 の間の範囲に収まり、1 に近づくほど数字は予測にマッチする可能性が高くなります。これを行なうために使用可能なプロセスは LeNet で利用可能で softmax 予測を与えてくれます。下の AddLeNetModel は softmax を出力します。けれども、この場合、それは softmax 以上のことを行ないます – softmax に加えて、畳み込まれた層とともに計算されたモデルです。

TODO: include image of the model below

def AddLeNetModel(model, data):
    """Adds the main LeNet model.
    
    This part is the standard LeNet model: from data to the softmax prediction.
    
    For each convolutional layer we specify dim_in - number of input channels
    and dim_out - number or output channels. Also each Conv and MaxPool layer change
    image size. For example, kernel of size 5 reduces each side of an image by 4. 
    
    While when we have kernel and stride sizes equal 2 in a MaxPool layer, it devides
    each side in half. 
    """
    # Image size: 28 x 28 -> 24 x 24
    conv1 = model.Conv(data, 'conv1', dim_in=1, dim_out=20, kernel=5)
    # Image size: 24 x 24 -> 12 x 12
    pool1 = model.MaxPool(conv1, 'pool1', kernel=2, stride=2)
    # Image size: 12 x 12 -> 8 x 8
    conv2 = model.Conv(pool1, 'conv2', dim_in=20, dim_out=50, kernel=5)
    # Image size: 8 x 8 -> 4 x 4
    pool2 = model.MaxPool(conv2, 'pool2', kernel=2, stride=2)
    # 50 * 4 * 4 stands for dim_out from previous layer multiplied by the image size
    fc3 = model.FC(pool2, 'fc3', dim_in=50 * 4 * 4, dim_out=500)
    fc3 = model.Relu(fc3, fc3)
    pred = model.FC(fc3, 'pred', 500, 10)
    softmax = model.Softmax(pred, 'softmax')
    return softmax

print("Model function created.")

下の AddAccuracy はモデルに精度演算子を追加します。モデルの精度を追跡するために次の関数で使用します。

def AddAccuracy(model, softmax, label):
    """Adds an accuracy op to the model"""
    accuracy = model.Accuracy([softmax, label], "accuracy")
    return accuracy
print("Accuracy function created.")

次の関数 AddTrainingOperators はモデルに訓練演算子を追加します。最初のステップでは、演算子 LabelCrossEntropy を適用します、これは入力とラベルセットの間の交差エントロピーを計算します。この演算子は殆どいつも softmax を得た後そしてモデルの損失を計算する前に使用されます。それは “Cross Entropy” のための ‘xent’ というラベルで [softmax, label] 配列を取り込みます。

  • xent = model.LabelCrossEntropy([softmax, label], ‘xent’)

AveragedLoss は交差エントロピーを取り込み交差エントロピーで見つかる損失の平均を返します。

  • loss = model.AveragedLoss(xent, “loss”)

bookkeeping 目的では、次のように AddAccuracy 関数を呼び出すことでモデル精度もまた計算します :

  • AddAccuracy(model, softmax, label)

次の行は訓練モデルの主要なパートです : モデルに全ての勾配演算子を追加します。勾配は上で計算した損失に関して計算されます。

  • model.AddGradientOperators([loss])

次の一握りの行は非常に単純な確率的勾配効果をサポートします。— TODO(jiayq): 私たちはこれらの SGD 演算子をよりクリーンな流儀でラップする作業をしており、準備ができたらこれを更新するでしょう。取り敢えずは、私たちが SGD アルゴリズムを基本演算子でどのように 表現しているかを見ることができます。現時点でこのパートを完全に理解する必要はありませんが、とにかくプロセスを通して見ましょう。model.Iter から始めます、これは訓練で実行する反復数のためのカウンターです。

  • ITER = model.Iter(“iter”)

lr = base_lr * (t ^ gamma) である、単純な学習率スケジュールを行ないます。最小化を行なっていることに注意してください、そのため base_lr は負で下り坂 (DOWNHILL) 方向に進んでいます。

  • 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 する必要があります。これは Checkpoint 演算子を通して達成されます。それはまた頻繁過ぎる checkpoint を行なわないようにパラメータ “every” を取り込みます。この場合では、20 反復毎に checkpoint しましょうと述べていて、これは多分妥当です。

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

 

def AddTrainingOperators(model, softmax, label):
    """モデルに訓練演算子を追加する。"""
    xent = model.LabelCrossEntropy([softmax, label], 'xent')
    # 予想される損失を計算する
    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 は定数値で勾配更新で使用されます。
    # それを一度作成することだけが必要で、param_init_net に明示的に置かれます。
    ONE = model.param_init_net.ConstantFill([], "ONE", shape=[1], value=1.0)
    # Now, for each parameter, we do the gradient updates.
    for param in model.params:
        # Note how we get the gradient of each parameter - CNNModelHelper keeps
        # track of that.
        param_grad = model.param_to_grad[param]
        # The update is a simple weighted sum: param = param + param_grad * LR
        model.WeightedSum([param, ONE, param_grad, LR], param)
    # let's checkpoint every 20 iterations, which should probably be fine.
    # 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.")

次の関数 AddBookkeepingOperations は後で検査できる 2, 3 の bookkeeping 演算子を追加します。これらの演算子は訓練手続きには影響しません : それらは統計情報を集めてファイルかログに出力するだけです。

def AddBookkeepingOperators(model):
    """This adds a few bookkeeping operators that we can inspect later.
    
    These operators do not affect the training procedure: they only collect
    statistics and prints them to file or to logs.
    """    
    # Print basically prints out the content of the blob. to_file=1 routes the
    # printed output to a file. The file is going to be stored under
    #     root_folder/[blob name]
    model.Print('accuracy', [], to_file=1)
    model.Print('loss', [], to_file=1)
    # Summarizes the parameters. Different from Print, Summarize gives some
    # statistics of the parameter, such as mean, std, min and max.
    for param in model.params:
        model.Summarize(param, [], to_file=1)
        model.Summarize(model.param_to_grad[param], [], to_file=1)
    # Now, if we really want to be verbose, we can summarize EVERY blob
    # that the model produces; it is probably not a good idea, because that
    # is going to take time - summarization do not come for free. For this
    # demo, we will only show how to summarize the parameters and their
    # gradients.
print("Bookkeeping function created")

さて、実際に訓練とテストのためのモデルを作成しましょう。下で WARNING メッセージを見る場合でも、心配しないでください。先に確立した関数は今実行されます。私たちが行なっている4つのステップを思い出してください :

  • (1) データ入力
  • (2) 主要な計算
  • (3) 訓練する
  • (4) bookkeeping

けれどもデータ入力が可能となる前に訓練モデルを定義する必要があります。基本的には上で定義したコンポーネントの全ての断片が必要となります。この例では、mnist_train データセット上では 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)

# 訓練モデル。テストパスが 100 反復 (全部で 10,000 画像) となるようにバッチサイズを 100 に設定します。
# テストモデルに対しては、データ入力パート、主要な 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) モデル。単純に主要な LeNetModel パートが必要です。
deploy_model = cnn.CNNModelHelper(
    order="NCHW", name="mnist_deploy", init_params=False)
AddLeNetModel(deploy_model, "data")
# You may wonder what happens with the param_init_net part of the deploy_model.
# No, we will not use them, since during deployment time we will not randomly
# initialize the parameters, but load the parameters from the db.

print('Created training and deploy models.')

Caffe2 が持つ単純なグラフ可視化ツールを使用して訓練とデプロイモデルがどのように見えるか見てみましょう。もし次のコマンドが失敗するようであればそれは実行しているマシンに graphviz がインストールされていないからでしょう。通常は以下でインストール可能です :

  • sudo yum install graphviz
graph = net_drawer.GetPydotGraph(train_model.net.Proto().op, "mnist", rankdir="LR")
display.Image(graph.create_png(), width=800)

 

 
(訳注: 以下は最右端部を拡大した画像です。)

 
さて、上のグラフは訓練段階で生じる全てを示します : 白色ノードは blob で、緑色矩形ノードは実行される演算子です。線路のような大きな平行線に気がついたかもしれません : これらは forward パスで生成された blob から backward 演算子への依存です。

必要な依存性だけを表示して演算子だけを表示することによりより最小限な方法でグラフを表示してみましょう。注意深く見れば、グラフの左半分が forward パスで、右半分が backward パスで、最も右にはパラメータ更新と要約演算子のセットがあることが見れるでしょう。

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 buffer を作成することです。例えば、訓練モデルの param init net のためのシリアライズされた protocol buffer の部分を示します。

print(str(train_model.param_init_net.Proto())[:400] + '\n...')
name: "mnist_train_init"
op {
  output: "dbreader_/home/masao/caffe2/caffe2/caffe2/python/tutorials/tutorial_data/mnist/mnist-train-nchw-leveldb"
  name: ""
  type: "CreateDB"
  arg {
    name: "db_type"
    s: "leveldb"
  }
  arg {
    name: "db"
    s: "/home/masao/caffe2/caffe2/caffe2/python/tutorials/tutorial_data/mnist/mnist-train-nchw-leveldb"
  }
}
op {
  output: "conv1_w"
  name: ""
  type
...

全ての protocol buffer をディスクにダンプすればそれらを簡単に検査できます。気がついたかもしれないように、これらの protocol buffer は旧くて良い 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++ で完全に訓練することもできます。そのルートについての議論は他のチュートリアルのために残します。

このプロセスは実行に少し時間がかかることに注意してください。コードブロックがまだ実行中であることを示す、アスタリスク (In [*]) か他の IPython インジケータから目を離さないでください、

最初にネットワークを初期化しなければなりません :

  • workspace.RunNetOnce(train_model.param_init_net)

主要なネットワークを複数回実行しますので、protobuf から生成される実際のネットワークを workspace に置く、ネットワークを最初に作成します。

  • workspace.CreateNet(train_model.net)

ネットワークを 200 まで実行する反復数を設定してそして各反復で精度と損失を記録するために2つの numpy 配列を作成します。

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

ネットワークと、精度と損失の追跡のセットアップで workspace.RunNet を呼び出してネットワークの名前 train_model.net.Proto().name を渡すことで 200 反復ループできます。各反復において workspace.FetchBlob(‘accuracy’) と workspace.FetchBlob(‘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 を使用して結果をプロットできます。

# The parameter initialization network only needs to be run once.
workspace.RunNetOnce(train_model.param_init_net)
# creating the network
workspace.CreateNet(train_model.net)
# set the number of iterations and track the accuracy & loss
total_iters = 200
accuracy = np.zeros(total_iters)
loss = np.zeros(total_iters)
# Now, we will manually run the network for 200 iterations. 
for i in range(total_iters):
    workspace.RunNet(train_model.net.Proto().name)
    accuracy[i] = workspace.FetchBlob('accuracy')
    loss[i] = workspace.FetchBlob('loss')
# After the execution is done, let's plot the values.
pyplot.plot(loss, 'b')
pyplot.plot(accuracy, 'r')
pyplot.legend(('Loss', 'Accuracy'), loc='upper right')

そしてデータの幾つかと予測をサンプリングできます。

# Let's look at some of the data.
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 パスを実行してテスト精度をレポートします。test_model はtrain_model から得たパラメータを使用しますが、依然として test_model.param_init_net は入データを初期化するために実行されなければならない
ことに注意してください。この実行では、精度を追跡する必要があるだけでまた 100 反復を実行するだけです。

# run a test pass on the test net
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')
# After the execution is done, let's plot the values.
pyplot.plot(test_accuracy, 'r')
pyplot.title('Acuracy over test batches.')
print('test_accuracy: %f' % test_accuracy.mean())
test_accuracy: 0.949800
 

以上