Python
DeepLearning
Caffe

CaffeでCNNしたった

CaffeのチュートリアルはMNISTとかCIFAR-10とか既に学習とテストのデータセットが用意されてる。けど、自分が使いたいデータセットを学習させる方法はちゃんと書かれていない。
ということで、自分で集めた画像でCNNを使う方法。

手順

  1. 作業するディレクトリを作る
  2. LMDBの用意
  3. prototxtの用意
  4. 学習
  5. テスト

0. MNISTのチュートリアル

caffeでは既にLeNetを使ったMNISTのチュートリアルが用意されてます。
まずはcaffeのディレクトリに移動。

$HOME --- caffe

最初は次のコマンドでMNISTのデータを取得します。

./data/mnist/get_mnist.sh

次にそのデータをLMDBに変換します。

./examples/mnist/create_mnist.sh

次に学習します。

./examples/mnist/train_lenet.sh

大体accuracyが0.98とかになればチュートリアル成功です。
たまにCUDNNを使ってるとここでaccuacyが増えないとかの問題になることがあるので、その時はCUDNNをなしにしてcaffeをmakeし直して下さい。

1. 作業するディレクトリを作る

まずは自分がこれから作業するディレクトリを作ります。
caffeのディレクトリ内でやってもいいですが、それだとごちゃごちゃしそうなので、$HOME下に適当なディレクリを作っちゃいましょう!

$HOME --- caffe-work

こんな感じで。

2. LMDBの用意

まずはLMDBを用意します。これは入力データ、テストデータをまとめたものなり、caffeで予め用意されてるプログラムを使うことで、簡単に作れます。

画像データの用意

まずは自分で使いたいデータを集めてください。
用意できたら次のディレクリ構造で画像をおいてください。(拡張子は1つに統一して下さい、じゃないと学習でうまくいかないみたいです)

$HOME --- caffe-work --- Dataset --- Train --- Image --- img1.jpg
                                                      |- img2.jpg
                                                         ...

次にtrain.txtファイルを作ります。このファイルには各行に画像の名前とラベルを書き込みます。中身はこんな感じで。ラベルは属するクラスのインデックスで0から始めて下さい!

train.txt
img1.jpg 0
img2.jpg 1
img3.jpg 0
img4.jpg 2
...

このファイルをTrainの中に置いて下さい。

$HOME --- Caffe-work --- Dataset --- Train --- Image
                                            |- train.txt

LMDBの作成

ここまできたらあとはLMDBにコンバートするだけです。
これにはconvert_imagesetというプログラムを使います。
まずは caffe-workに移動しましょう。

cd ~/caffe-work

でここで次のコマンドでLMDBにコンバートします。ただしcaffeが$HOME下にあるとしています。

../caffe/build/tools/convert_imageset \
  ./Dataset/Train/Image/ \
  ./Dataset/Train/train.txt \
  train_lmdb \
  --shuffle -backend lmdb --resize_height 64 --resize_width 64
  • 1番目の引数: ./Dataset/Train/Image は画像が置いてあるディレクトリの指定
  • 2番目の引数: ./Dataset/Train/train.txt はラベルデータが書いてあるファイルの指定
  • 3番目の引数: train_lmdb は作ったLMDBが置かれるディレクトリの指定
  • 4番目の引数: --shuffle はデータをフャッフルするかの指定。
  • 5番目の引数: -backend lmdb はLMDBを使うかの指定(今回はLMDBなのでこれでlmdb)
  • 6番目の引数: --resize_height 64 は画像の高さを64にリサイズするという指定
  • 7番目の引数: --resize_width 64 は画像の幅を64にリサイズするという指定

これでtrain_lmdbというディレクリができているはずです。

$HOME --- caffe-work --- Dataset
                      |- train_lmdb

同じように学習中に使う評価用データ(validation)も作りましょう。
ここで作ったLMDBはval_lmdbとします。

平均値データの作成

画像をCNNに入力する際、Mean-subtraction、すなわち平均値を引く作業をします。このためには平均値を予め計算する必要がありますが、caffeではこれもプログラムを動かすだけでやってくれます。以下のコマンドを実行するだけです。

../caffe/build/tools/compute_image_mean \
  train_lmdb \
  mean.binaryproto \
  --backend=lmdb
  • 1番目の引数: train_lmdb これは平均値を計算するLMDBの指定です
  • 2番目の引数: mean.binaryproto これは計算した平均値を出力するファイル名
  • 3番目の引数: --backedn=lmdb は1番目の引数で指定するデータセットの形式(今回はLMDBなのでlmdb)

これでmean.binaryprotoファイルができているはずです。

$HOME --- caffe-work --- Dataset
                      |- train_lmdb
                      |- val_lmdb
                      |- mean.binaryproto

3. prototxtの用意

caffeではネットワーク定義やoptimizerの設定はprototxtファイルというものを作って定義します。
全部で3つ作ります。(ちなみにprototxtは正式な拡張子ではないので、なんでもいいですw ここではめんどいので.ptとします)

  • train.pt(学習用のネットワーク定義ファイル)
  • solver.pt(optimizerなどの設定)
  • test.pt (テスト用のネットワーク定義ファイル。ものよっては deploy.ptって名前になってるがどっちでもいい)

まずはprototxtっていうディレクトリを作りましょう! んでその中に3つのprototxtファイルを作ります。

$HOME --- caffe-work --- Image
                      |- ...
                      |- prototxt --- train.pt
                                   |- solver.pt
                                   |- test.pt

作り方はMNISTとかCIFAR-10とかのチュートリアルのを参考にするといいかも。

例えばalextnetを元にしたものはこんな感じ

solver.pt

solver.pt
net: "prototxt/train.pt"
test_iter: 50
test_interval: 100
base_lr: 0.01
lr_policy: "step"
gamma: 0.1
stepsize: 100000
display: 20
max_iter: 10000
momentum: 0.9
weight_decay: 0.0005
snapshot: 10000
snapshot_prefix: "alexnet"
solver_mode: GPU
  • net: これは学習時に使うprototxtファイルを指定します。パスは学習を実行するディレクトリからの相対パスを指定します。学習はcaffe-workで実行するので今回はこんな感じで。
  • test_iter: これは学習の途中で評価する時に何回評価するかの指定。自分の好きな数字でおkです。
  • test_interval: これは何回学習iterationをしたら評価するかのインターバルです。これも好きな数字でおkです。
  • base_lr: 最初の学習率の設定です。だいたいは0.01~0.001とかにすればいいですが、学習中に数値がnanとか発散しちゃう時は0.0001とか下げてみましょう。
  • lr_policy: 学習の途中で学習率を下げたりする時は、これで "step"とかにすることでできます。基本使わなていいですが、研究者などガチな方はけっこう多様すると思います。他には"multi-step"などがあるようです。
  • gammma: lr_policyでstepなどにしている時はここで指定した値を学習率に掛けて、学習率を下げます。ここでは最初にbase_lr×gammmaで0.001になり、次は0.001×gammaで0.0001になっていきます。
  • stepsize: lr_policyでstepなどにしている時はここで指定したiterationの時に学習率を下げます。ここでは100000iterationの時に学習率にgammmaで指定した値を掛けて、学習率を下げます。
  • diplay: これは学習状況を表示するiterationのタイミングです。今回は20iteration毎にlossなどを表示します。
  • max_iter: これは学習するiteration回数を指定します。
  • weight_decay: これは学習時の重み減衰の指定です。
  • snapshot: これは学習中に学習したパラメータを保存するiterationの回数です。今回は10000なので、10000iterationの度に学習したパラメータを保存します。
  • snapshop_prefix: これは学習パラメータを保存するファイル名の最初につける名前です。"alexnet" としているので、alextnet_iter_###.caffemodelとalexnet_iter_###.solverstateの2つができます。
  • solver_mode: これはGPUを使うかCPUを使うかの指定です。

train.pt

学習で使うprototxtファイルです。例えばこんな感じで。

train.pt
name: "AlexNet"
layer {
  name: "data" type: "Data" top: "data" top: "label"
  include { phase: TRAIN }
  transform_param {
    mirror: true
    mean_file: "mean.binaryproto"
  }
  data_param {
    source: "train_lmdb"
    batch_size: 256
    backend: LMDB
  }
}
layer {
  name: "data" type: "Data" top: "data" top: "label"
  include { phase: TEST }
  transform_param {
    mirror: false
    mean_file: "mean.binaryproto"
  }
  data_param {
    source: "val_lmdb"
    batch_size: 20
    backend: LMDB
  }
}
layer {
  name: "conv1" type: "Convolution" bottom: "data" top: "conv1"
  param { lr_mult: 1 decay_mult: 1 }
  param { lr_mult: 2 decay_mult: 0 }
  convolution_param {
    num_output: 96 kernel_size: 11 stride: 4
    weight_filler { type: "gaussian" std: 0.01 }
    bias_filler { type: "constant" value: 0 }
  }
}
layer { name: "relu1" type: "ReLU" bottom: "conv1" top: "conv1" }
layer {
  name: "norm1" type: "LRN" bottom: "conv1" top: "norm1"
  lrn_param { local_size: 5 alpha: 0.0001 beta: 0.75 }
}
layer {
  name: "pool1" type: "Pooling" bottom: "norm1" top: "pool1"
  pooling_param { pool: MAX kernel_size: 3 stride: 2 }
}
layer {
  name: "conv2" type: "Convolution" bottom: "pool1" top: "conv2"
  param { lr_mult: 1 decay_mult: 1 }
  param { lr_mult: 2 decay_mult: 0 }
  convolution_param {
    num_output: 256 pad: 2 kernel_size: 5 group: 2
    weight_filler { type: "gaussian" std: 0.01 }
    bias_filler { type: "constant" value: 0.1 }
  }
}
layer { name: "relu2" type: "ReLU" bottom: "conv2" top: "conv2" }
layer {
  name: "norm2" type: "LRN" bottom: "conv2" top: "norm2"
  lrn_param { local_size: 5 alpha: 0.0001 beta: 0.75 }
}
layer {
  name: "pool2" type: "Pooling" bottom: "norm2" top: "pool2"
  pooling_param { pool: MAX kernel_size: 3 stride: 2 }
}
layer {
  name: "conv3" type: "Convolution" bottom: "pool2" top: "conv3"
  param { lr_mult: 1 decay_mult: 1 }
  param { lr_mult: 2 decay_mult: 0 }
  convolution_param { num_output: 384 pad: 1 kernel_size: 3
    weight_filler { type: "gaussian" std: 0.01 }
    bias_filler { type: "constant" value: 0 }
  }
}
layer { name: "relu3" type: "ReLU" bottom: "conv3" top: "conv3" }
layer {
  name: "conv4" type: "Convolution" bottom: "conv3" top: "conv4"
  param { lr_mult: 1 decay_mult: 1 }
  param { lr_mult: 2 decay_mult: 0 }
  convolution_param { 
    num_output: 384 pad: 1 kernel_size: 3 group: 2
    weight_filler { type: "gaussian" std: 0.01 }
    bias_filler { type: "constant" value: 0.1 }
  }
}
layer { name: "relu4" type: "ReLU" bottom: "conv4" top: "conv4" }
layer {
  name: "conv5" type: "Convolution" bottom: "conv4" top: "conv5"
  param { lr_mult: 1 decay_mult: 1 }
  param { lr_mult: 2 decay_mult: 0 }
  convolution_param {
    num_output: 256 pad: 1 kernel_size: 3 group: 2
    weight_filler { type: "gaussian" std: 0.01 }
    bias_filler { type: "constant" value: 0.1 }
  }
}
layer { name: "relu5" type: "ReLU" bottom: "conv5" top: "conv5" }
layer {
  name: "pool5" type: "Pooling" bottom: "conv5" top: "pool5"
  pooling_param { pool: MAX kernel_size: 3 stride: 2 }
}
layer {
  name: "fc6-ft" type: "InnerProduct" bottom: "pool5" top: "fc6"
  param { lr_mult: 1 decay_mult: 1 }
  param { lr_mult: 2 decay_mult: 0 }
  inner_product_param { num_output: 4096
    weight_filler { type: "gaussian" std: 0.005 }
    bias_filler { type: "constant" value: 0.1 }
  }
}
layer { name: "relu6" type: "ReLU" bottom: "fc6" top: "fc6" }
layer {
  name: "drop6" type: "Dropout" bottom: "fc6" top: "fc6"
  dropout_param { dropout_ratio: 0.5 }
}
layer {
  name: "fc7-ft" type: "InnerProduct" bottom: "fc6" top: "fc7"
  param { lr_mult: 1 decay_mult: 1 }
  param { lr_mult: 2 decay_mult: 0 }
  inner_product_param { num_output: 4096
    weight_filler { type: "gaussian" std: 0.005 }
    bias_filler { type: "constant" value: 0.1 }
  }
}
layer { name: "relu7" type: "ReLU" bottom: "fc7" top: "fc7" }
layer { name: "drop7" type: "Dropout" bottom: "fc7" top: "fc7"
  dropout_param { dropout_ratio: 0.5 }
}
layer {
  name: "fc8-ft" type: "InnerProduct" bottom: "fc7" top: "fc8"
  param { lr_mult: 1 decay_mult: 1 }
  param { lr_mult: 2 decay_mult: 0 }
  inner_product_param { num_output: 6
    weight_filler { type: "gaussian" std: 0.01 }
    bias_filler { type: "constant" value: 0 }
  }
}
layer {
  name: "accuracy" type: "Accuracy" 
  bottom: "fc8" bottom: "label" top: "accuracy"
  include { phase: TEST }
}
layer {
  name: "loss" type: "SoftmaxWithLoss"
  bottom: "fc8" bottom: "label" top: "loss"
}

name: このネットワークの名前です。今回は"AlexNet"と名付けました。

caffeはlayer{}というものを書いてConvolutionとかPooling, InnerProduct(Fully-Connected)などを書いていきます。
caffeは長いのでchainerやkerasを使ってる人にはつらたんかもしれないです。
caffeの層に関してはそのうち一覧表を作ります(`・ω・´)ゞ

Data Layer

画像などのデータを入力する層です。

layer {
  name: "data" type: "Data" top: "data" top: "label"
  include { phase: TRAIN }
  transform_param {
    mirror: true
    mean_file: "mean.binaryproto"
  }
  data_param {
    source: "train_lmdb"
    batch_size: 256
    backend: LMDB
  }
}
layer {
  name: "data" type: "Data" top: "data" top: "label"
  include { phase: TEST }
  transform_param {
    mirror: false
    mean_file: "mean.binaryproto"
  }
  data_param {
    source: "val_lmdb"
    batch_size: 20
    backend: LMDB
  }
}

ここでは2つありますが、include { phase: TRAIN } がある方が学習時のデータを入力する層で、include { phase: TEST } が評価時のデータを入力する層に分かれています。

top:が2つありますが、top: "data"は画像データ、**top: "label"は画像に対する正解ラベルです。

DataLayerではdata_param {}で諸々設定します。
data_param {}の中の
source: は画像データのLMDBまでの相対パスです。
barch_size ミニバッチサイズの数です。
backend: 今回はLMDB を使うのでlmdbにします。

Convolution Layer

Convolution layerとかはlayerにこんな感じで書きます。

layer {
  name: "conv1" type: "Convolution" bottom: "data" top: "conv1"
  param { lr_mult: 1 decay_mult: 1 }
  param { lr_mult: 2 decay_mult: 0 }
  convolution_param {
    num_output: 96 kernel_size: 11 stride: 4
    weight_filler { type: "gaussian" std: 0.01 }
    bias_filler { type: "constant" value: 0 }
  }
}

name: 層の名前です。
type: 畳み込み層にしたいので、"Convolution"にします。
bottom: この層への入力を指定します。ここではDataLayerの出力であるdata(画像データ)を入力します。
top: この層で畳み込み処理を行った結果を"conv1"という名前で出力する、という意味です。
param {} このconvではカーネルのweightとbiasの2つの学習すべきパラメータがあるので、それぞれの学習率と重み減衰を指定します。param{}が2個あるのはそれぞれweight,bias用です。
convolution_param {} ここで畳み込み層のパラメータを指定します。

  • num_output: カーネルの数、つまり出力のチャネル数になります。
  • kernel_size: カーネルの1辺のサイズ、kernel_size:11 とすれば11x11のカーネルになります。
  • stride: カーネルの畳み込みのストライド、つまり動くピクセルになります。
  • weight_filler {} カーネルのweightの初期化になります。type:"gaussian" std: 0.01 は標準偏差0.01のガウス分布でランダム初期化することのになります。
  • bias_filler {} カーネルのbiasの初期化になります。type:"constant" value: 0 は全て0の定数で初期化することになります。

ReLU

活性化関数はこんな感じで導入します。

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

type: ReLUを使うので、"ReLU"で。
bottom: conv1というデータをこの層の入力します。conv1は上のconv1層の出力がconv1だったので、それをこの層に入れます。
top: conv1という名前で出力します。

ここでさっきのconv1層と違うのはbottomとtopの名前が一緒なこと。この層ではconv1という名前の出力を受け取ってReLUに通して、conv1という名前で出力するということになります。
名前が一緒なことに関しては特に問題はなさそうです。

Output

層の出力はクラス数の数のユニットを用意しなければなりません。下のような感じで。

layer {
  name: "fc8-ft" type: "InnerProduct" bottom: "fc7" top: "fc8"
  param { lr_mult: 1 decay_mult: 1 }
  param { lr_mult: 2 decay_mult: 0 }
  inner_product_param { num_output: 6
    weight_filler { type: "gaussian" std: 0.01 }
    bias_filler { type: "constant" value: 0 }
  }
}

type: 最後の層は出力に相当するので、Fully-Connected Layerにしなければなりません。caffeではFC層はInnerProductという名前になります。
inner_product_param: これはInnerProductのパラメータ数を設定します。これは最後の出力なので、num_outputはクラス数に設定します。(この場合は6クラス)

Lossの定義

最後にLoss関数の定義をします。caffeではネットワークのlayerでLossを定義します。

layer {
  name: "loss" type: "SoftmaxWithLoss"
  bottom: "fc8" bottom: "label" top: "loss"
}

type: 今回はSoftmax Cross EntropyをLossとして使うので、"SoftmaxWithLoss"を設定します。
bottom: ネットワークの最後の出力と最初のDataLayerで出力した教師ラベルのlabelを入力したlossを計算します。
top: 計算したlossが出力されます。このときのlossは全ピクセルの和となり、スカラで出力されます。

Accuracyの計算

学習の途中で評価用データでその時点での正解率accuracyが計算されます。caffeではそのためのlayerが用意されてます。

layer {
  name: "accuracy" type: "Accuracy" 
  bottom: "fc8" bottom: "label" top: "accuracy"
  include { phase: TEST }
}

type: これはAccuracyです。
bottom: ここでもネットワークの最後の出力と教師ラベルでaccuracyを入力します。
accuracy: 計算したaccuracyを出力します。ここでは、0.9など0~1のスカラで出力されます。
include { phase: TEST } accuracyの計算は学習時ではく評価時のみで行うので、これをつけます。

test.pt

テスト用に使うprototxtファイルです。ここでいうテストとは学習が全て終わった後に自分で判別したいデータを入力してCNNに分類させる時です。

test.pt
name: "AlexNet"

input: "data"
input_dim: 1 input_dim: 3 input_dim: 64 input_dim: 64

layer {
  name: "conv1" type: "Convolution" bottom: "data" top: "conv1"
  param { lr_mult: 1 decay_mult: 1 }
  param { lr_mult: 2 decay_mult: 0 }
  convolution_param { num_output: 96 kernel_size: 11 stride: 4 }
}
layer { name: "relu1" type: "ReLU" bottom: "conv1" top: "conv1" }
layer {
  name: "norm1" type: "LRN" bottom: "conv1" top: "norm1"
  lrn_param { local_size: 5 alpha: 0.0001 beta: 0.75 }
}
layer {
  name: "pool1" type: "Pooling" bottom: "norm1" top: "pool1"
  pooling_param { pool: MAX kernel_size: 3 stride: 2 }
}
layer {
  name: "conv2" type: "Convolution" bottom: "pool1" top: "conv2"
  param { lr_mult: 1 decay_mult: 1 }
  param { lr_mult: 2 decay_mult: 0 }
  convolution_param { num_output: 256 pad: 2 kernel_size: 5 group: 2 }
}
layer { name: "relu2" type: "ReLU" bottom: "conv2" top: "conv2" }
layer {
  name: "norm2" type: "LRN" bottom: "conv2" top: "norm2"
  lrn_param { local_size: 5 alpha: 0.0001 beta: 0.75 }
}
layer {
  name: "pool2" type: "Pooling" bottom: "norm2" top: "pool2"
  pooling_param { pool: MAX kernel_size: 3 stride: 2 }
}
layer {
  name: "conv3" type: "Convolution" bottom: "pool2" top: "conv3"
  param { lr_mult: 1 decay_mult: 1 }
  param { lr_mult: 2 decay_mult: 0 }
  convolution_param { num_output: 384 pad: 1 kernel_size: 3 }
}
layer { name: "relu3" type: "ReLU" bottom: "conv3" top: "conv3" }
layer {
  name: "conv4" type: "Convolution" bottom: "conv3" top: "conv4"
  param { lr_mult: 1 decay_mult: 1 }
  param { lr_mult: 2 decay_mult: 0 }
  convolution_param { num_output: 384 pad: 1 kernel_size: 3 group: 2 }
}
layer { name: "relu4" type: "ReLU" bottom: "conv4" top: "conv4" }
layer {
  name: "conv5" type: "Convolution" bottom: "conv4" top: "conv5"
  param { lr_mult: 1 decay_mult: 1 }
  param { lr_mult: 2 decay_mult: 0 }
  convolution_param { num_output: 256 pad: 1 kernel_size: 3 group: 2 }
}
layer { name: "relu5" type: "ReLU" bottom: "conv5" top: "conv5" }
layer {
  name: "pool5" type: "Pooling" bottom: "conv5" top: "pool5"
  pooling_param { pool: MAX kernel_size: 3 stride: 2 }
}
layer {
  name: "fc6-ft" type: "InnerProduct" bottom: "pool5" top: "fc6"
  param { lr_mult: 1 decay_mult: 1 }
  param { lr_mult: 2 decay_mult: 0 }
  inner_product_param { num_output: 4096 }
}
layer { name: "relu6" type: "ReLU" bottom: "fc6" top: "fc6" }
layer {
  name: "drop6" type: "Dropout" bottom: "fc6" top: "fc6"
  dropout_param { dropout_ratio: 0.5 }
}
layer {
  name: "fc7-ft" type: "InnerProduct" bottom: "fc6" top: "fc7"
  param { lr_mult: 1 decay_mult: 1 }
  param { lr_mult: 2 decay_mult: 0 }
  inner_product_param { num_output: 4096 }
}
layer { name: "relu7" type: "ReLU" bottom: "fc7" top: "fc7" }
layer {
  name: "drop7" type: "Dropout" bottom: "fc7" top: "fc7"
  dropout_param { dropout_ratio: 0.5 }
}
layer {
  name: "fc8-ft" type: "InnerProduct" bottom: "fc7" top: "fc8"
  param { lr_mult: 1 decay_mult: 1 }
  param { lr_mult: 2 decay_mult: 0 }
  inner_product_param { num_output: 6 }
}
layer { 
  name: "prob" type: "Softmax" bottom: "fc8" top: "prob" 
}

test.ptはほとんどtrain.ptと同じですが、、、

  • 最初が
input: "data"
input_dim: 1 input_dim: 3 input_dim: 64 input_dim: 64

となっています。これは入力の次元を[1,3,64,64]に指定しています。これはそれぞれ[ミニバッチサイズ、入力画像のチャネル、入力画像の高さ、入力画像の幅]になっています。
入力データの名前は"data"となります。

  • 最後が
layer { 
  name: "prob" type: "Softmax" bottom: "fc8" top: "prob" 
}

となります。これは最後のInnerProductに対してSoftmax関数を適用して、クラス確率を出していることになります。出力の名前は"prob"です。

以上でprototxtの準備は終了です。次から学習を始めます!!

4. 学習

準備は終わったのであとは学習させるだけです。自分で学習の関数を書くことができますが、caffeは学習のための関数が用意されています。最初はそちらを使ってみましょう。
今caffe-workディレクトリにいるとします。

$HOME --- caffe
       |- caffe-work

既存の学習プログラムを使う時

この状態で以下のコマンドを実行すれば学習が実行できます。

../caffe/build/tools/caffe train --solver=prototxt/solver.pt

オプションで学習済みモデルによるfine-tuningができます。

../caffe/build/tools/caffe train --solver=prototxt/solver.pt -weights @@@.caffemodel

Fine-tuningに関して

caffeではfine-tuningは簡単にできます。この時、layerのnameが同じものにパラメータがコピーされます。なので、fine-tuningしたいprototxtのlayerのnameを学習済みモデルのものと同じにして下さい。逆に言えばfine-tuningしたくない部分をnameを変える必要があります。最終出力のクラス数などは殆どの場合で学習済みモデルと違うので、nameを変えるのを忘れないで下さい。

[pretrained]       [train.pt]
   conv1  == FT ==>  conv1
   conv2  == FT ==>  conv2
   conv3  == FT x   conv3-ft
   conv4  == FT ==>  conv4
    FC    == FT x    FC-ft

また、train.ptでweight_filler,bias_fillerを設定している場合でもfine-tuningが優先されるので、fillerを記述しても問題ありません。

学習のlossを逆伝搬したくない時

train.pt で param { lr_mult: 1 decay_mult: 1 } がありますが、lr_mult: 0 decay_mult: 0に設定すればそのlayerでlossが適用されません。
lr_multはlossで学習させる時に loss x lr_mult の分だけ学習が行われます。decay_mult も同じです。

ちなみに、前の層全てにlossを逆伝搬させたくない時は、layerにpropagate_down: 0 を設定します(propagate_down: 1 はlossを逆伝搬させます)。例えばこんな感じで。

layer {
  name: "loss" type: "SoftmaxWithLoss"
  bottom: "fc8" bottom: "label" top: "loss"
  propagate_down: 1
  propagate_down: 0
}

propagate_downはbottomの数だけ書いて下さい。それぞれbottomの順に逆伝搬させるかを設定します。上の場合だとfc8に逆伝搬させ、labelにはさせません。

学習のLossが減らない

iterationが進んでもlossが最初の値から少し上下するだけで全く減らない、またはaccuracyが増えない時は、次の順番で試して下さい。

  1. train.ptにweight_filler, bias_fillerがちゃんと設定されているか確認。
  2. solver.ptでbase_lrを変える(大体は下げる)。
  3. caffeのMakefile.configでCUDNN:=1をアンコメントする。

*CUDNNを使ってると、makeはできるけど学習が進まないことがあるようです。

自分で学習プログラムを実装する

caffe/build/tools/caffe train をあえて使わず自分で学習を実装する方法です。いろいろcaffeを改造(魔改造というとか)する人向けです。

まずはcaffeをインポートします。

import caffe

次にCPUかGPUの設定をします。

CPU
caffe.set_mode_cpu()
GPU
caffe.set_mode_gpu()
caffe.set_device(0)

GPUのset_deviceとは使うGPUのIDです。搭載した順に0,1,2,... とIDが振られます。nvidia-smiコマンドで確認して見てください。

次にsolver.ptを読み込みます。これはSGDSolverを使います。

solver_prototxt = 'prototxt/solver.pt'
solver = caffe.SGDSolver(solver_prototxt)

もしfine-tuningする場合はsolver.net.copy_from()を使います。例えばalexnetの学習済みモデルを使う場合はネットからとって、caffe-workに新しくpretrained_modelディレクトリを作り、そこに置きます。

$HOME --- caffe-work --- pretrained_model --- bvlc_alexnet.caffemodel
pretrained_model = 'pretrained_model/bvlc_alexnet.caffemodel'
solver.net.copy_from(pretrained_model)

次はsolver.ptのパラメータを読み込みます。これにはcaffe_pb2を使います。

import caffe.proto import caffe_pb2
import google.protobuf as pb2
from google.protobuf import text_format

solver_param = caffe_pb2.SolverParameter()
with open(solver_prototxt, 'rt') as f:
    pb2.text_format.Merge(f.read(), solver_param)

ここから学習自体の実装になります。
学習はsolver.step(1)で1iteration学習を進めます。これをwhileで最大iterationまで回すという流れです。
まずはsolver.ptのmax_iterを読み込みます。

max_iters = solver_param.max_iter

んで、whileで回します。

while solver.iter < max_iters:
    solver.step(1)

もし途中でlossなどの情報が欲しい時もあると思うので、、、
例えば100iteration毎にlossの情報が欲しければ、次のように書きます。
blobs['loss']は欲しいlayerのtopの名前を書きます(nameではありません)。

while solver.iter < max_iters:
    solver.step(1)

    if solver.iter % 100 == 0:
        loss = solver.net.blobs['loss'].data

学習が終わったらパラメータを保存する必要があります。それはnet.save()を使います。

filename = solver_param.snapshot_prefix + '_iter_{:d}'.format(solver.iter) + '.caffemodel'
net.save(filename)

caffemodelがあるか確認してみましょう。以上で学習は終了です。上の流れを1つのファイルにすると使いやすいと思います。

5. テスト

学習したモデルを使って実際に分類してみましょう。

平均値ファイル作成

その前に学習データの平均値が保存されたファイルをbinaryprotoからnpyに変換します。次のようなファイルを用意します。

convert.py
import caffe
import numpy as np
import sys

blob = caffe.proto.caffe_pb2.BlobProto()
data = open( sys.argv[1] , 'rb' ).read()
blob.ParseFromString(data)
arr = np.array( caffe.io.blobproto_to_array(blob) )
out = arr[0]
np.save( sys.argv[2] , out )

んで、次のコマンドでnpyファイルを作ります。

python convert.py mean.binaryproto mean.npy
  • 1番目の引数: これは最初に作ったbinaryprotoの平均値が書かれたファイルです。
  • 2番目の引数:  変換したnpyの名前です。この名前で保存されます。

以上でmean.npyファイルができます。

テストモデルの用意

次からテストを始めます。
まずはcaffeをインポートします。

import caffe

次にCPUを使うかGPUを使うかを指定します。

CPU
caffe.set_mode_cpu()
GPU
caffe.set_mode_gpu()
caffe.set_device(0)

GPUを使う時のset_deviceはGPUの番号を振ります。大体の場合は1個めのGPUを使うので、0をして大丈夫です。

次に実際にネットワーク定義の学習済みモデルのパラメータを読み込みます。これはcaffe.Net()を使ってnetに読み込みます。

PROTOTXT = 'prototxt/test.pt'
CAFFEMODEL = 'alexnet_iter_10000.caffemodel'
net = caffe.Net(PROTOTXT, CAFFEMODEL, caffe.TEST)

入力画像の前処理

次に平均値を読み込みます。これでRGBの平均値の平均値、つまり全ピクセルの平均値がmuになります。

import numpy as np
mu = np.load('mean.npy')
mu = mu.mean(1).mean(1)

次に入力画像をネットワークに入力できるようにします。これはTransformerというものを使います。

transformer = caffe.io.Transformer({'data':net.blobs['data'].data.shape})

これでtest.ptの最初で定義した[1,3,64,64]というサイズが読み込まれます。
次にtransformerの設定をします。

transformer.set_transpose('data', (2,0,1))
transformer.set_mean('data', mu)
transformer.set_raw_scale('data', 255)
transformer.set_channel_swap('data', (2,1,0))
  • set_transpose() 入力の形の順番を変えます。今回は後で分かりますが、caffe.io.load_imageで読み込みます。これは[幅、高さ、チャネル]の形になりますが、caffeの入力は[ミニバッチサイズ、チャネル、高さ、幅]なので、(2,0,1)を指定して順番を入れ替えます。
  • set_mean() これで入力データから平均値を引きます。
  • set_raw_scale() imreadで読むと画素値が0~255の形になるので、これで0~1に正規化します。
  • set_channel_swap imreadだとBGRの順番に読ま込まれます。なので、RGBの順に並び替えます。

次に入力する画像を読み込みます。

image = caffe.io.load_image(image_file_path)

んで、transformerで画像を変換します。

transformed_image = transformer.preprocess('data', image)

画像を入力して出力を得る

あとはnetの入力にセットします。

net.blobs['data'].data[...] = transformed_image

最後に順伝搬(forward)して、全layerに計算させます。。

net.forward()

次に出力を得ます。

output = net.blobs['prob'].data[0, :].copy()

これで出力がoutputに得られました。ですがこのままだとsoftmaxの出力全部を受け取ってます(つまり、確率分布)。
なので、確率が高いインデックスを得るには

predict_index = output.argmax()

その確率を得るには、

score = output.max()

でそれぞれ取れます。

ここでいうクラスのインデックスはLMDBを作る際に用意したtrain.txtで設定した教師ラベルと同じになります。

中間層の可視化

研究とかだと中間層の出力を見ることも必要になると思うので、その実装です。
以下のvis_inter_layer()を用意します。最初の方では入力のshapeを正規化してます。

def vis_inter_layer(self, data):

    if len(data.shape) == 4:
        batch, channels, h, w = data.shape
        data = data[0]
    elif len(data.shape) == 3:
        channels, h, w = data.shape

    line = math.ceil(math.sqrt(channels))

    plt.figure(figsize=(15,15))
    plt.subplots_adjust(left=0.001, right=0.999, top=0.999, bottom=0.001, hspace=0.05, wspace=0.01)

    for i in range(channels):
        im = data[i]
        plt.subplot(line, line, i+1)
        plt.axis('off')
        plt.imshow(im, cmap='jet')

    plt.show()

んで、forward()した後に、こんな感じで使います。

net.forward()
vis_inter_layer(net.blobs['conv5'].data[0].copy())

以上でcaffeのCNNに関する基本的な流れができます