Caffe : Example : MNIST 上で LeNet を Caffe で訓練する

Caffe : Example : MNIST 上で LeNet を Caffe で訓練する (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
日時 : 03/31/2017

* 本ページは、Caffe の本家サイトの Example – Training LeNet on MNIST with Caffe を翻訳した上で
適宜、補足説明したものです:
    http://caffe.berkeleyvision.org/gathered/examples/mnist.html
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

データセットを用意する

最初に必要なのは MNIST website からダウンロードしてデータ・フォーマットを変換することです。これを行なうためには、単に次のコマンドを実行します :

cd $CAFFE_ROOT
./data/mnist/get_mnist.sh
./examples/mnist/create_mnist.sh

スクリプトを実行した後には2つのデータセット、mnist_train_lmdb と mnist_test_lmdb があるでしょう。

 

LeNet: MNIST 分類モデル

実際に訓練プログラムを実行する前に、何が起きるのかを説明しておきましょう。私たちは LeNet ネットワークを使用します、これは数字分類タスクで上手く動作することが知られています。元の LeNet 実装からは少しだけ異なるバージョンを使用します、ニューロンのための sigmoid 活性を Rectified Linear Unit (ReLU) 活性で置き換えます。

LeNet のデザインは CNN の本質を含みます、これは ImageNet のもののような大きなモデルでも使用されます。一般的に、それは pooling 層が続く畳込み層、pooling 層が続くもう一つの畳込み層、そして従来の多層パーセプトロンに似た2つの完全結合層から成ります。$CAFFE_ROOT/examples/mnist/lenet_train_test.prototxt で層を定義しました。

 

MNIST ネットワークを定義する

このセクションは、MNIST 手書き数字分類のための LeNet モデルを指定する lenet_train_test.prototxt モデル定義を説明します。貴方が Google Protobuf に精通していることを仮定しそして Caffe で使用される protobuf 定義を読んでいることを仮定しています、これは $CAFFE_ROOT/src/caffe/proto/caffe.proto で見つかります。

特に、caffe::NetParameter (あるいは python では、 caffe.proto.caffe_pb2.NetParameter) protobuf を書いていきます。ネットワークに名前を与えることから始めます :

name: "LeNet"

* 訳注: NetParameter 及び関連 message の定義は以下です (deprecated なフィールドが多いですが) :

message NetParameter {
  optional string name = 1; // ネットワーク名を与えることを考える
  // DEPRECATED. InputParameter 参照。ネットワークへの入力 blob。
  repeated string input = 3;
  // DEPRECATED. InputParameter 参照。入力 blob の shape。
  repeated BlobShape input_shape = 8;

  // 4D input dimensions -- deprecated.  代わりに "input_shape" を使用する。
  // If specified, for each input blob there should be four
  // values specifying the num, channels, height and width of the input blob.
  // Thus, there should be a total of (4 * #input) numbers.
  repeated int32 input_dim = 4;

  // ネットワークが全ての層に backward 演算を carry out することを強制するか否か。
  // False を設定する場合、backward を carry out するか否かは
  // ネット構造と学習率に従って自動的に決定されます。
  optional bool force_backward = 5 [default = false];
  // phase, level, そして stage を含む、ネットワークの現在の "state"。
  // Some layers may be included/excluded depending on this state and the states
  // specified in the layers' include and exclude fields.
  optional NetState state = 6;

  // Net::Forward, Net::Backward, そして Net::Update を実行する間の結果についての
  // デバッギング情報を出力する。
  optional bool debug_info = 7 [default = false];
  // ネットを作る層群。connectivity と behavior を含む、それらの configurations の各々は
  //  LayerParameter として指定されます。
  repeated LayerParameter layer = 100;  // ID 100 so layers are printed last.

  // DEPRECATED: 代わりに 'layer' を使用します。
  repeated V1LayerParameter layers = 2;
}

message InputParameter {
  // This layer produces N >= 1 top blob(s) to be assigned manually.
  // Define N shapes to set a shape for each top.
  // Define 1 shape to set the same shape for every top.
  // Define no shape to defer to reshaping manually.
  repeated BlobShape shape = 1;
}

// Blob の shape (次元群) を指定する。
message BlobShape {
  repeated int64 dim = 1 [packed = true];
}

データ層を書く

現在、デモで先に作成した lmdb から MNIST データを読みます。これはデータ層で定義されます :

layer {
  name: "mnist"
  type: "Data"
  transform_param {
    scale: 0.00390625
  }
  data_param {
    source: "mnist_train_lmdb"
    backend: LMDB
    batch_size: 64
  }
  top: "data"
  top: "label"
}

特に、この層は name mnist, type data を持ち、そしてそれは与えられた lmdb ソースからデータを読みます。バッチサイズ 64 を使用し入ってくるピクセルを範囲 [0, 1) におさまるようにスケールします。何故 0.00390625 ?それは 1 を 256 で除算したものです。そして最後に、この層は2つの blob を生成します、一つは data blob で、もう一つは label blob です。

畳込み層を書く

最初の畳込み層を定義しましょう :

layer {
  name: "conv1"
  type: "Convolution"
  param { lr_mult: 1 }
  param { lr_mult: 2 }
  convolution_param {
    num_output: 20
    kernel_size: 5
    stride: 1
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
  bottom: "data"
  top: "conv1"
}

この層は (data 層から提供される) data blobを取り、conv1 層を生成します。それは 20 チャネルの出力を生成します、畳込みカーネルサイズ 5 そしてストライド 1 で実行されます。

filler は重みとバイアスの値をランダムに初期化することを可能にします。weight filler に対しては、xavier アルゴリズムを使用します、これは入力と出力ニューロンの数を基に初期化のスケールを自動的に決定します。bias filler に対しては、デフォルトの filling 値 0 で単に constant として初期化します。

* 訳注: xavier アルゴリズムについては Xavier Initialization が参考になります。

lr_mults は層の学習可能なパラメータのための学習率調整です。この場合、重み学習率はランタイム時に solver により与えられる学習率と同じになるように、バイアス学習率はそれの2倍の大きさに設定します – これは通常より良い収束率に導かれます。

Pooling 層を書く

やれやれ (Phew)。Pooling 層は定義するに実際にははるかに簡単です :

layer {
  name: "pool1"
  type: "Pooling"
  pooling_param {
    kernel_size: 2
    stride: 2
    pool: MAX
  }
  bottom: "conv1"
  top: "pool1"
}

これは、pool カーネル・サイズ 2 とストライド 2 で max pooling を実行することを言っています(従って近接する pooling 領域間にオーバーラップはありません )。

同様に、2番目の畳込みと pooling 層も書き上げることができます。
詳細は $CAFFE_ROOT/examples/mnist/lenet_train_test.prototxt を確認してください。

完全結合層を書く

完全結合層を書くのもまた単純です :

layer {
  name: "ip1"
  type: "InnerProduct"
  param { lr_mult: 1 }
  param { lr_mult: 2 }
  inner_product_param {
    num_output: 500
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
  bottom: "pool2"
  top: "ip1"
}

これは完全結合層 (Caffe では InnerProduct 層として知られています) を 500 出力で定義します。他の全ての行は馴染みがあるはずです、違いますか?

ReLU 層を書く

ReLU 層もまた単純です :

layer {
  name: "relu1"
  type: "ReLU"
  bottom: "ip1"
  top: "ip1"
}

ReLU は element-wise な演算ですから、何某かのメモリをセーブするために in-place 操作を行なうことができます。これは単に bottom と top blob に同じ名前を与えることにより達成されます。もちろん他の層タイプのための重複した blob 名は使用しないでください!

ReLU 層の後、もう一つの innerproduct 層を書きます :

layer {
  name: "ip2"
  type: "InnerProduct"
  param { lr_mult: 1 }
  param { lr_mult: 2 }
  inner_product_param {
    num_output: 10
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
  bottom: "ip1"
  top: "ip2"
}

損失層を書く

最後に、損失を書きます!

layer {
  name: "loss"
  type: "SoftmaxWithLoss"
  bottom: "ip2"
  bottom: "label"
}

softmax_loss 層は softmax と 多項ロジスティック損失の両者を実装しています (時間を節約して数値的安定性を改善します)。それは2つの blob を取ります、最初の一つは予測で2つ目はデータ層で提供される label です (覚えていますか?)。それはどのような出力も生成しません – それが行なう全ては損失関数値を計算し、backpropagation が始まったときにそれをレポートし、そして ip2 に関する勾配を initiate します。これはマジックが始まる場所です。

追加ノート: 層ルールを書く

層定義は、ネットワーク定義においてそれらが含まれるかどうかいつ含まれるかのためのルールを含むことができます、以下のようなものです :

layer {
  // ...layer definition...
  include: { phase: TRAIN }
}

これは、現在のネットワークの状態を基に、層のネットワーク内の包含を制御するルールです。層ルールとモデル・スキーマについて更なる情報は $CAFFE_ROOT/src/caffe/proto/caffe.proto を参照できます。

上の例では、この層は TRAIN 段階 (phase) でのみ含まれます。TRAIN を TEST に変更すれば、この層は test 段階でのみ使用されます。デフォルトでは、つまり層ルールなしでは、層はいつもネットワークに含まれます。こうして、lenet_train_test.prototxt は (異なる batch_size で) 定義された2つの DATA 層を持ちます、一つは訓練段階のためそしてもう一つはテスティング段階のためです。また、Accuracy (精度) 層もあります、これは、lenet_solver.prototxt で定義されたように、モデル精度を 100 反復毎にレポートするために TEST 段階にのみ含まれます。

 

MNIST Solver を定義する

prototxt $CAFFE_ROOT/examples/mnist/lenet_solver.prototxt の各行を説明しているコメントを確認してください :

# 訓練/テスト・ネット protocol buffer 定義
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 訓練反復毎にテスティングを carry out する。
test_interval: 500
# ベース学習率、momentum そしてネットワークの重み減衰。
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
# 中間結果のスナップショット
snapshot: 5000
snapshot_prefix: "examples/mnist/lenet"
# solver モード: CPU または GPU
solver_mode: GPU

 

モデルを訓練してテストする

ネットワーク定義 protobuf と solver protobuf ファイルを書いた後ではモデルを訓練するのは単純です。単に train_lenet.sh、あるいは次のコマンドを直接実行します :

cd $CAFFE_ROOT
./examples/mnist/train_lenet.sh

train_lenet.sh は単純なスクリプトですが、これが素早い説明です : 訓練のための主要なツールは caffe でその入力としてアクション train と solver protobuf テキストファイルを取ります。コードを実行する時、このような沢山のメッセージが飛ぶのを見るでしょう :

I1203 net.cpp:66] Creating Layer conv1
I1203 net.cpp:76] conv1 <- data
I1203 net.cpp:101] conv1 -> conv1
I1203 net.cpp:116] Top shape: 20 24 24
I1203 net.cpp:127] conv1 needs backward computation.

これらのメッセージは各層、その接続そしてその出力 shape についての詳細を教えてくれます、それはデバッギングで手助けとなるでしょう。初期化の後、訓練が始まります :

I1203 net.cpp:142] Network initialization done.
I1203 solver.cpp:36] Solver scaffolding done.
I1203 solver.cpp:44] Solving LeNet

solver 設定を基にして、100 反復毎に訓練損失関数を出力し、そして 500 反復毎にネットワークをテストします。このようなメッセージを見るでしょう :

I1203 solver.cpp:204] Iteration 100, lr = 0.00992565
I1203 solver.cpp:66] Iteration 100, loss = 0.26044
...
I1203 solver.cpp:84] Testing net
I1203 solver.cpp:111] Test score #0: 0.9785
I1203 solver.cpp:111] Test score #1: 0.0606671

各訓練反復について、lr はその反復の学習率で loss は訓練関数です。テスト段階の出力について、score 0 は精度で、score 1 はテスト損失関数です。

そして数分後、完了です!

I1203 solver.cpp:84] Testing net
I1203 solver.cpp:111] Test score #0: 0.9897
I1203 solver.cpp:111] Test score #1: 0.0324599
I1203 solver.cpp:126] Snapshotting to lenet_iter_10000
I1203 solver.cpp:133] Snapshotting solver state to lenet_iter_10000.solverstate
I1203 solver.cpp:78] Optimization Done.

バイナリ protobuf ファイルとしてストアされる、最終的なモデルは以下にストアされます :

lenet_iter_10000

これは貴方のアプリケーションで訓練されたモデルとしてデプロイされます、もし現実世界のアプリケーション・データセット上で訓練するのであれば。

Um… GPU 訓練についてはどうでしょう?

You just did! 全ての訓練は GPU 上で実行されました。事実として、CPU 上で訓練をしたいのであれば、lenet_solver.prototxt の一行を単に変更できます :

# solver mode: CPU or GPU
solver_mode: CPU

そうすれば訓練のために CPU を使用できるでしょう。簡単でしょう?

MNIST は小さなデータセットですから、通信オーバーヘッドにより GPU での訓練は実際にはそれほどの恩恵をもたらしません。より複雑なモデルでより大きなデータセット上では、ImageNet のような、計算速度の違いはより本質的になります。

固定ステップにおいて学習率をどのように減少させるか?

lenet_multistep_solver.prototxt を見てください。

 

以上