Caffe : Notebook Example : LeNet を利用して Python で解く

Caffe : Notebook Example : LeNet を利用して Python で解く (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
日時 : 04/04/2017

* 本ページは、jupyter サンプル: examples/01-learning-lenet.ipynb – Solving in Python with LeNet を
実行・翻訳した上で適宜、補足説明したものです:
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

古典的な LeNet を Python I/F で定義し、訓練し、そしてテストします。

このサンプルでは fully-exposed Solver I/F を使用して、Python でCaffe による学習を探ります。

 

1. セットアップ

  • Python 環境をセットアップします : numpy と plot inline のために pylab import を使用します。
from pylab import *
%matplotlib inline
  • caffe を import します、必要ならば sys.path に追加します。pycaffe をビルドしたことを確認してください。
caffe_root = '../'  # this file should be run from {caffe_root}/examples (otherwise change this line)
​
import sys
sys.path.insert(0, caffe_root + 'python')
import caffe
  • 提供されている LeNet サンプル・データとネットワークを使用します(データをダウンロードしてデータベースを作成したことを、以下のように確認してください)。
# run scripts from caffe root
import os
os.chdir(caffe_root)
# Download data
!data/mnist/get_mnist.sh
# Prepare data
!examples/mnist/create_mnist.sh
# back to examples
os.chdir('examples')
Downloading...
Creating lmdb...
I0404 22:24:02.548912  6525 db_lmdb.cpp:35] Opened lmdb examples/mnist/mnist_train_lmdb
I0404 22:24:02.549057  6525 convert_mnist_data.cpp:88] A total of 60000 items.
I0404 22:24:02.549067  6525 convert_mnist_data.cpp:89] Rows: 28 Cols: 28
I0404 22:24:07.663831  6525 convert_mnist_data.cpp:108] Processed 60000 files.
I0404 22:24:07.672586  6527 db_lmdb.cpp:35] Opened lmdb examples/mnist/mnist_test_lmdb
I0404 22:24:07.695653  6527 convert_mnist_data.cpp:88] A total of 10000 items.
I0404 22:24:07.695670  6527 convert_mnist_data.cpp:89] Rows: 28 Cols: 28
I0404 22:24:08.837038  6527 convert_mnist_data.cpp:108] Processed 10000 files.
Done.

 

2. net を作成する

さて LeNet の変形を作成しましょう、古典的な 1989 convnet アーキテクチャです。

手助けのために2つの外部ファイルが必要です :

  • net prototxt、アーキテクチャを定義して train/test データを指し示します。
  • solver prototxt、学習パラメータを定義します。

net を作成することから始めます。net を簡潔で自然な方法で Python コードとして書きます、これは Caffe の protobuf モデル・フォーマットにシリアライズされます。

このネットワークは事前生成した LMDB から読むことを期待していますが、MemoryDataLayer を使用して ndarray から直接読むこともまた可能です。

from caffe import layers as L, params as P

def lenet(lmdb, batch_size):
    # LeNet の私たちのバージョンです : 線形と単純な非線形変換の連続な列です。
    n = caffe.NetSpec()
    
    n.data, n.label = L.Data(batch_size=batch_size, backend=P.Data.LMDB, source=lmdb,
                             transform_param=dict(scale=1./255), ntop=2)
    
    n.conv1 = L.Convolution(n.data, kernel_size=5, num_output=20, weight_filler=dict(type='xavier'))
    n.pool1 = L.Pooling(n.conv1, kernel_size=2, stride=2, pool=P.Pooling.MAX)
    n.conv2 = L.Convolution(n.pool1, kernel_size=5, num_output=50, weight_filler=dict(type='xavier'))
    n.pool2 = L.Pooling(n.conv2, kernel_size=2, stride=2, pool=P.Pooling.MAX)
    n.fc1 =   L.InnerProduct(n.pool2, num_output=500, weight_filler=dict(type='xavier'))
    n.relu1 = L.ReLU(n.fc1, in_place=True)
    n.score = L.InnerProduct(n.relu1, num_output=10, weight_filler=dict(type='xavier'))
    n.loss =  L.SoftmaxWithLoss(n.score, n.label)
    
    return n.to_proto()
    
with open('mnist/lenet_auto_train.prototxt', 'w') as f:
    f.write(str(lenet('mnist/mnist_train_lmdb', 64)))
    
with open('mnist/lenet_auto_test.prototxt', 'w') as f:
    f.write(str(lenet('mnist/mnist_test_lmdb', 100)))

Google の protobuf ライブラリを使用して net はより冗長ながら human-readable なシリアライゼーション・フォーマットでディスクに書かれました。この記述を直接読み、書き、そして変更することができます。train net を見てみましょう。

!cat mnist/lenet_auto_train.prototxt
layer {
  name: "data"
  type: "Data"
  top: "data"
  top: "label"
  transform_param {
    scale: 0.00392156885937
  }
  data_param {
    source: "mnist/mnist_train_lmdb"
    batch_size: 64
    backend: LMDB
  }
}
layer {
  name: "conv1"
  type: "Convolution"
  bottom: "data"
  top: "conv1"
  convolution_param {
    num_output: 20
    kernel_size: 5
    weight_filler {
      type: "xavier"
    }
  }
}
layer {
  name: "pool1"
  type: "Pooling"
  bottom: "conv1"
  top: "pool1"
  pooling_param {
    pool: MAX
    kernel_size: 2
    stride: 2
  }
}
layer {
  name: "conv2"
  type: "Convolution"
  bottom: "pool1"
  top: "conv2"
  convolution_param {
    num_output: 50
    kernel_size: 5
    weight_filler {
      type: "xavier"
    }
  }
}
layer {
  name: "pool2"
  type: "Pooling"
  bottom: "conv2"
  top: "pool2"
  pooling_param {
    pool: MAX
    kernel_size: 2
    stride: 2
  }
}
layer {
  name: "fc1"
  type: "InnerProduct"
  bottom: "pool2"
  top: "fc1"
  inner_product_param {
    num_output: 500
    weight_filler {
      type: "xavier"
    }
  }
}
layer {
  name: "relu1"
  type: "ReLU"
  bottom: "fc1"
  top: "fc1"
}
layer {
  name: "score"
  type: "InnerProduct"
  bottom: "fc1"
  top: "score"
  inner_product_param {
    num_output: 10
    weight_filler {
      type: "xavier"
    }
  }
}
layer {
  name: "loss"
  type: "SoftmaxWithLoss"
  bottom: "score"
  bottom: "label"
  top: "loss"
}

さて学習パラメータを見てみましょう、これもまた prototxt ファイルとして書かれます (既にディスクに提供されています)。momentum、重み減衰、そして特定の学習率スケジュールで SGD を使用します。

!cat mnist/lenet_auto_solver.prototxt
# The train/test net protocol buffer definition
train_net: "mnist/lenet_auto_train.prototxt"
test_net: "mnist/lenet_auto_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
# Carry out testing every 500 training iterations.
test_interval: 500
# The base learning rate, momentum and the weight decay of the network.
base_lr: 0.01
momentum: 0.9
weight_decay: 0.0005
# The learning rate policy
lr_policy: "inv"
gamma: 0.0001
power: 0.75
# Display every 100 iterations
display: 100
# The maximum number of iterations
max_iter: 10000
# snapshot intermediate results
snapshot: 5000
snapshot_prefix: "mnist/lenet"

 

3. solver をロードしてチェックする

デバイスを選択して solver をロードしましょう。SGD (with momentum) を使用しますが、他のメソッド (Adagrad と Nesterov’s accelerated gradient のような) もまた利用可能です。

caffe.set_device(0)
caffe.set_mode_gpu()
​
### solver をロードして train と test net を作成します。
solver = None  # ignore this workaround for lmdb data (can't instantiate two solvers on the same data)
solver = caffe.SGDSolver('mnist/lenet_auto_solver.prototxt')
  • net のアーキテクチャの考えを得るために、中間的な特徴 (blobs) とパラメータの次元をチェックできます (これらはまたデータ層を操作する時に参照すると有用です)。
# 各出力は (バッチサイズ, 特徴次元, 空間次元) = (batch size, feature dim, spatial dim)
[(k, v.data.shape) for k, v in solver.net.blobs.items()]
[('data', (64, 1, 28, 28)),
 ('label', (64,)),
 ('conv1', (64, 20, 24, 24)),
 ('pool1', (64, 20, 12, 12)),
 ('conv2', (64, 50, 8, 8)),
 ('pool2', (64, 50, 4, 4)),
 ('fc1', (64, 500)),
 ('score', (64, 10)),
 ('loss', ())]
# 重みサイズを単に出力します (バイアスは省きます)

[(k, v[0].data.shape) for k, v in solver.net.params.items()]
[('conv1', (20, 1, 5, 5)),
 ('conv2', (50, 20, 5, 5)),
 ('fc1', (500, 800)),
 ('score', (10, 500))]
  • take off する前に、全てが期待どおりにロードされているかを確認しましょう。train と test net 上で forward pass を実行してデータを含むかをチェックします。
solver.net.forward()  # train net
solver.test_nets[0].forward()  # test net (there can be more than one)
{'loss': array(2.2321081161499023, dtype=float32)}
# we use a little trick to tile the first eight images
imshow(solver.net.blobs['data'].data[:8, 0].transpose(1, 0, 2).reshape(28, 8*28), cmap='gray'); axis('off')
print 'train labels:', solver.net.blobs['label'].data[:8]
train labels: [ 5.  0.  4.  1.  9.  2.  1.  3.]

imshow(solver.test_nets[0].blobs['data'].data[:8, 0].transpose(1, 0, 2).reshape(28, 8*28), cmap='gray'); axis('off')
print 'test labels:', solver.test_nets[0].blobs['label'].data[:8]
test labels: [ 7.  2.  1.  0.  4.  1.  4.  9.]

 

4. solver のステップを取る

train と test net の両者ともデータをロードして、正しいラベルを持っているようです。

  • (minibatch) SGD の 1 ステップを取って何が起きるか見ましょう。
solver.step(1)

フィルタ経由で勾配を伝播させたでしょうか?最初の層への更新を見てみましょう、ここでは 5x 5 フィルタの 4 x 5 グリッドとして示されます。

imshow(solver.net.params['conv1'][0].diff[:, 0].reshape(4, 5, 5, 5)
       .transpose(0, 2, 1, 3).reshape(4*5, 5*5), cmap='gray'); axis('off')
(-0.5, 24.5, 19.5, -0.5)

 

5. カスタム訓練ループを書く

何かが起きています。しばらく net を実行して、それが進むままに 2,3 の点を追跡してみましょう。このプロセスは caffe バイナリを通して訓練するのと同じであることに注意してください。特に :

  • ロギングは通常のように起き続けます
  • スナップショットは solver prototxt (ここでは、5000 反復毎) で指定された間隔で取られます
  • テスティングは指定された間隔で起きます (ここでは 500 反復毎)

Python でのループの制御を持つので、下で見るように、進むにつれて自由に追加のことを計算できます。他のたくさんのことも同様にできます、例えば :

  • カスタム停止基準 (stopping criterion) を書く
  • ループのネットを更新することで solving プロセスを変更する
%%time
niter = 200
test_interval = 25
# losses will also be stored in the log
train_loss = zeros(niter)
test_acc = zeros(int(np.ceil(niter / test_interval)))
output = zeros((niter, 8, 10))

# メイン solver ループ
for it in range(niter):
    solver.step(1)  # Caffe による SGD
 
    # 訓練損失をストアする
    train_loss[it] = solver.net.blobs['loss'].data
    
    # 最初のテストバッチ上の出力をストアする
    # (新しいデータをロードするのを回避するために conv1 で forward パスを開始する)
    solver.test_nets[0].forward(start='conv1')
    output[it] = solver.test_nets[0].blobs['score'].data[:8]
    
    # run a full test every so often
    # (Caffe can also do this for us and write to a log, but we show here
    #  how to do it directly in Python, where more complicated things are easier.)
    if it % test_interval == 0:
        print 'Iteration', it, 'testing...'
        correct = 0
        for test_it in range(100):
            solver.test_nets[0].forward()
            correct += sum(solver.test_nets[0].blobs['score'].data.argmax(1)
                           == solver.test_nets[0].blobs['label'].data)
        test_acc[it // test_interval] = correct / 1e4
Iteration 0 testing...
Iteration 25 testing...
Iteration 50 testing...
Iteration 75 testing...
Iteration 100 testing...
Iteration 125 testing...
Iteration 150 testing...
Iteration 175 testing...
CPU times: user 52.1 s, sys: 12 ms, total: 52.1 s
Wall time: 51.6 s
  • 訓練損失とテスト精度をプロットしてみましょう。
_, ax1 = subplots()
ax2 = ax1.twinx()
ax1.plot(arange(niter), train_loss)
ax2.plot(test_interval * arange(len(test_acc)), test_acc, 'r')
ax1.set_xlabel('iteration')
ax1.set_ylabel('train loss')
ax2.set_ylabel('test accuracy')
ax2.set_title('Test Accuracy: {:.2f}'.format(test_acc[-1]))

損失は迅速に落ちて収束するように見えます (確率性は別にして)、精度はそれに応じて上がりました。Hooray!

  • 最初のテストバッチ上の結果を保存したので、予測スコアがどう進化したか見ることができます。x 軸上に時間を y 上に各々可能性のあるラベルをプロットします、明るさで信頼度を示します。
for i in range(8):
    figure(figsize=(2, 2))
    imshow(solver.test_nets[0].blobs['data'].data[i, 0], cmap='gray')
    figure(figsize=(10, 2))
    imshow(output[:50, i].T, interpolation='nearest', cmap='gray')
    xlabel('iteration')
    ylabel('label')


これらの任意の数字について小さなアイデアで開始して各々について正しい分類で終了しました。沿って追随してきたのであれば、最後の数字が最も難しいことが分かるでしょう、傾斜した “9” は (理解できますが) “4” ともっとも混同されます。

  • これらは “生の” 出力スコアで softmax 計算された確率ベクトルではない点に注意してください。後者は、下で示されるように、net の信頼度を見ることをより簡単にします (しかしありそうにない数字に対するスコアは見るのがより難しいです)。
for i in range(8):
    figure(figsize=(2, 2))
    imshow(solver.test_nets[0].blobs['data'].data[i, 0], cmap='gray')
    figure(figsize=(10, 2))
    imshow(exp(output[:50, i].T) / exp(output[:50, i].T).sum(0), interpolation='nearest', cmap='gray')
    xlabel('iteration')
    ylabel('label')


 

6. アーキテクチャと最適化で実験

LeNet を定義して、訓練して、そしてテストした今、多くの可能性のある次のステップがあります :

  • 比較のために新しいアーキテクチャを定義する
  • base_lr と同様なものを設定するか単に訓練をより長くして最適化を調整します
  • SGD から AdaDelta や Adam のような適応性のあるメソッドに solver type をスイッチする

続いての all-in-one サンプルを編集することでこれらの方向性を自由に探ってください。提案された選択ポイントは “EDIT HERE” コメントを探してください。デフォルトではこれはベースラインとして単純な線形分類器を定義しています。貴方のコーヒーがまだ kick in されておらずにインスピレーションを得たいのであれば、試してみましょう

  1. 非線形を ReLU から ELU あるいは Sigmoid のような saturing 非線形にスイッチする
  2. 完全結合と非線形層を更に積み重ねる
  3. 学習率を一度に 10x にして探ってみます (0.1 と 0.001 を試します)
  4. solver type を Adam にスイッチします (この adaptive solver type はハイパーパラメータに敏感さが少ない (less sensitive) はずですが、保証はありません… )
  5. 訓練の違いをより示すために niter をより高く (例えば 500 または 1,000 に) 設定することでより長く解きます
train_net_path = 'mnist/custom_auto_train.prototxt'
test_net_path = 'mnist/custom_auto_test.prototxt'
solver_config_path = 'mnist/custom_auto_solver.prototxt'

### define net
def custom_net(lmdb, batch_size):
    # define your own net!
    n = caffe.NetSpec()
    
    # keep this data layer for all networks
    n.data, n.label = L.Data(batch_size=batch_size, backend=P.Data.LMDB, source=lmdb,
                             transform_param=dict(scale=1./255), ntop=2)
    
    # EDIT HERE to try different networks
    # this single layer defines a simple linear classifier
    # (in particular this defines a multiway logistic regression)
    n.score =   L.InnerProduct(n.data, num_output=10, weight_filler=dict(type='xavier'))
    
    # EDIT HERE this is the LeNet variant we have already tried
    # n.conv1 = L.Convolution(n.data, kernel_size=5, num_output=20, weight_filler=dict(type='xavier'))
    # n.pool1 = L.Pooling(n.conv1, kernel_size=2, stride=2, pool=P.Pooling.MAX)
    # n.conv2 = L.Convolution(n.pool1, kernel_size=5, num_output=50, weight_filler=dict(type='xavier'))
    # n.pool2 = L.Pooling(n.conv2, kernel_size=2, stride=2, pool=P.Pooling.MAX)
    # n.fc1 =   L.InnerProduct(n.pool2, num_output=500, weight_filler=dict(type='xavier'))
    # EDIT HERE consider L.ELU or L.Sigmoid for the nonlinearity
    # n.relu1 = L.ReLU(n.fc1, in_place=True)
    # n.score =   L.InnerProduct(n.fc1, num_output=10, weight_filler=dict(type='xavier'))
    
    # keep this loss layer for all networks
    n.loss =  L.SoftmaxWithLoss(n.score, n.label)
    
    return n.to_proto()

with open(train_net_path, 'w') as f:
    f.write(str(custom_net('mnist/mnist_train_lmdb', 64)))    
with open(test_net_path, 'w') as f:
    f.write(str(custom_net('mnist/mnist_test_lmdb', 100)))

### define solver
from caffe.proto import caffe_pb2
s = caffe_pb2.SolverParameter()

# Set a seed for reproducible experiments:
# this controls for randomization in training.
s.random_seed = 0xCAFFE

# Specify locations of the train and (maybe) test networks.
s.train_net = train_net_path
s.test_net.append(test_net_path)
s.test_interval = 500  # Test after every 500 training iterations.
s.test_iter.append(100) # Test on 100 batches each time we test.

s.max_iter = 10000     # no. of times to update the net (training iterations)
 
# EDIT HERE to try different solvers
# solver types include "SGD", "Adam", and "Nesterov" among others.
s.type = "SGD"

# Set the initial learning rate for SGD.
s.base_lr = 0.01  # EDIT HERE to try different learning rates
# Set momentum to accelerate learning by
# taking weighted average of current and previous updates.
s.momentum = 0.9
# Set weight decay to regularize and prevent overfitting
s.weight_decay = 5e-4

# Set `lr_policy` to define how the learning rate changes during training.
# This is the same policy as our default LeNet.
s.lr_policy = 'inv'
s.gamma = 0.0001
s.power = 0.75
# EDIT HERE to try the fixed rate (and compare with adaptive solvers)
# `fixed` is the simplest policy that keeps the learning rate constant.
# s.lr_policy = 'fixed'

# Display the current training loss and accuracy every 1000 iterations.
s.display = 1000

# Snapshots are files used to store networks we've trained.
# We'll snapshot every 5K iterations -- twice during training.
s.snapshot = 5000
s.snapshot_prefix = 'mnist/custom_net'

# Train on the GPU
s.solver_mode = caffe_pb2.SolverParameter.GPU

# Write the solver to a temporary file and return its filename.
with open(solver_config_path, 'w') as f:
    f.write(str(s))

### load the solver and create train and test nets
solver = None  # ignore this workaround for lmdb data (can't instantiate two solvers on the same data)
solver = caffe.get_solver(solver_config_path)

### solve
niter = 250  # EDIT HERE increase to train for longer
test_interval = niter / 10
# losses will also be stored in the log
train_loss = zeros(niter)
test_acc = zeros(int(np.ceil(niter / test_interval)))

# the main solver loop
for it in range(niter):
    solver.step(1)  # SGD by Caffe
    
    # store the train loss
    train_loss[it] = solver.net.blobs['loss'].data
    
    # run a full test every so often
    # (Caffe can also do this for us and write to a log, but we show here
    #  how to do it directly in Python, where more complicated things are easier.)
    if it % test_interval == 0:
        print 'Iteration', it, 'testing...'
        correct = 0
        for test_it in range(100):
            solver.test_nets[0].forward()
            correct += sum(solver.test_nets[0].blobs['score'].data.argmax(1)
                           == solver.test_nets[0].blobs['label'].data)
        test_acc[it // test_interval] = correct / 1e4

_, ax1 = subplots()
ax2 = ax1.twinx()
ax1.plot(arange(niter), train_loss)
ax2.plot(test_interval * arange(len(test_acc)), test_acc, 'r')
ax1.set_xlabel('iteration')
ax1.set_ylabel('train loss')
ax2.set_ylabel('test accuracy')
ax2.set_title('Custom Test Accuracy: {:.2f}'.format(test_acc[-1]))
Iteration 0 testing...
Iteration 25 testing...
Iteration 50 testing...
Iteration 75 testing...
Iteration 100 testing...
Iteration 125 testing...
Iteration 150 testing...
Iteration 175 testing...
Iteration 200 testing...
Iteration 225 testing...

 

以上