Posted at

初めてでもわかる!画像認識向け機械学習ライブラリ Caffe

More than 1 year has passed since last update.


はじめに

画像認識向け機械学習ライブラリ「Caffe」を、初心者でもわかるように説明します。

CaffeはC++で書かれており、OpenCVやOpenBLASといった高機能ライブラリを駆使して

他言語より高速に、画像認識に向けた深層学習を可能にしている機械学習ライブラリです。

インターフェースはPythonやMatlabで、exampleも多く、使いやすいと思われます。

また、新しい方のライブラリで、(GitHubのIssue見れば分かりますが)開発は今もなお大変活発です。

レイヤ定義の幅の広さ等も売りの一つだと思います。

(N次元Poolingができない点は他言語に引けを取っているし、その上開発が2015年から止まっているのはクソですが。)

自身、初めて使った機械学習ライブラリがCaffeだったのですが、

Caffeは日本語レファレンスが乏しく、個人で始めると習得までに大変苦労しました。

Advent Calendarというモチベーションの中で、極力丁寧に説明すること、

また皆様に個人で気軽に機械学習の画像分類を楽しんで頂くことが、この記事の第一目標なので、

他の多くのサイトのまとめや受け売り、あっても和訳程度でしかありません。

分かる方には既視感満載ですが、その点ご了承頂けると幸いです!


深層学習による画像認識のモチベーション

機械学習で有名な弊大学M尾教授の言葉の受け売りであり、その点はご容赦ください(突っ込まれたら消しますね)。

深層学習のお陰で圧倒的な進化が期待されるものの一つは、画像認識, 映像認識であるという言説があります。



(引用元:犬は小型の狼である?〜犬に関する都市言説を検証する)

以前の機械学習では人間が努力して特徴量を定義していましたが、それでは追いつかないような絶妙な判定を強いられることがあり、その一つが画像分類です。

例えば動物の判定は、一つに犬と狼の判定(上画像参照)があり、人間でも難しいものです(私だったら確実に無理です)。

「特徴量を自ら探し出す」という深層学習の他にはない強みは、そういった難点に対抗し得るものです。

その精度は人間の認識率を上回る結果もあります(引用元:人工知能(AI)が人間をどのように追い越してきたのか、追い越そうとしているのかをグラフ化するとこんな感じ - ライブドアニュース)。

眼を持つ生物は持たぬ生物よりも破壊的に強くなれることはカンブリア爆発(今でいう三葉虫に当たる生物が、初めて眼らしいものを持ったことで爆発的に進化を遂げた)が一つの例です。

今世紀では、例えば機械は長らく眼を持たないでいましたが、深層学習によって眼を持つようになることで、工業,医療,農業等多くの分野において大きな技術革新が期待されるのです。


環境構築


環境

Windows10

cmake環境

git環境

今回はCPU環境での構築をします。


Bash on Ubuntu on Windowsでビルド

結論から言うと、Win10環境では『Bash on Ubuntu on Windows』でビルドするのがおすすめです。


  • 元々Ubuntu環境でできているので、困ったときにググるとUbuntuベースで解説してるものが多く、作業しやすくなる

  • ビルドが速いし、手順が煩雑でない

  • Anaconda入れなくていい

Windows branchでのビルドがダメってわけではないですが、

解説に***.shを実行とかあると、Cygwinで環境が整っていることが前提だったりしてつらい。

ちょっとの設定で何でも揃うBash on Ubuntu on Windowsは使いやすくてお勧めです。

注意(未確認) Win向けPython Interfaceでcaffe masterブランチのライブラリを導入しようとするとエラーが出ます。

pycaffeでの細かい作業に拘る人はWindowsブランチのほうがいいかも?


設定方法

こちら(Windows Subsystem for Linuxをインストールしてみよう!)を参考に設定してみてください。

開発者モードの有効化までしてください。

その後こちら(Ubuntu on WindowsにCaffeをインストール)の通りにやればいいだけ。

下の追記/注意を参考に 進めて頂く。本当にこれだけでcaffeが入ります。

追記/注意


  • git cloneの場所は、あとでエクスプローラから覗くことができるよう/mnt/c/直下にフォルダを適当に作って置くのが良いです。

  • Makefile.configの編集にはviあるいはvimが必要なので、同様にsudo apt -y install vim等してください。
    vimの使い方はiでinsert(挿入), Escキーで挿入終了, :wで保存, :qでexit, 保存&exitは:wq, 保存せずに終了は:q!です。

  • LMDBは確かにruntest通りません。TrainとTestで同じdbを使用したい場合、LMDBだと複製する必要がないという程度で、LevelDBで特別事足りなくなることは無いので、LMDB=0にして差支えないでしょう。

  • GPU使いたい人はNVIDIA社のドライバーが入ったPCでないとそもそもダメで、そうでない人はCPUしか使えません(業界の人には周知の事実なんだろうけど、、、)

  • make all で「hdf5.h: そのようなファイルやディレクトリはありません」といったエラーが出たらこちらを参考にし、加えて
    Makefile.config のLIBRARY_DIRSでこう変更

-LIBRARY_DIRS := $(PYTHON_LIB) /usr/local/lib /usr/lib 

+LIBRARY_DIRS := $(PYTHON_LIB) /usr/local/lib /usr/lib /usr/lib/x86_64-linux-gnu/hdf5/serial



  • vi ~/.profileで以下のパスを追加すると、後述のtrain, convert_imageset, parse_log.sh等よく使うコマンドが楽に使えて便利。編集後source ~/.profileを忘れずに。

PATH="$PATH:(CAFFE_ROOT)/caffe/build/tools:(CAFFE_ROOT)/caffe/tools/extra"


画像データ準備

この画像準備は骨の折れる作業です。

機械学習においてデータセットの準備は非常に重要です。

1回で上手く作れるものでもないと思うし、一般にハイパーパラメータがヤバいと言いますがこちらも大変です。

お試しならmnist公式の数字画像を持って来れば良いのですが、

自分も含め、一から準備する人向けにハウツーを紹介します。


データセットの重要性について語る

以下引用サイトで割愛させてください。

機械学習のデータセットの重要性 - Qiita

【ディープラーニング】少ないデータで効率よく学習させる方法:準備編 - ニートの言葉


バッチサイズというCaffeの穴

用意した上下左右を切り取って4倍、それらを左右対称にする、輝度を3段階に変化させる、まですると、データ数は24倍になります。

カサ増しはデータ収集のコストを削減し非常に有用ですが、prototxt概説でもあるように

Caffeは残念ながら1回の学習で使用できるデータサイズに限りがありますので、

データ数が多く、分類クラス数が少なく、画像サイズが大きいほど、1回の学習で使用できる画像枚数が限られ、学習時に使用するデータのラベルが全て同じになってしまう確率が上がります(なんせ相手が画像なので仕方ない)。

そうするとロス関数が爆発し、自分も訳わかんなさで爆発します。

GPUを使用して一回の学習で食わせる画像の枚数を増やしつつ、また精度向上のために画像サイズを縮小しすぎないことは重要ですが、

お家でCPUで頑張りたいよ!といった場合は画像をリサイズし、バッチサイズ確保を優先させてください


用意した画像をtrain, testのフォルダに分ける

お好みの配分で分けてください(1:1でも1:3でも3:1でも)


ラベル付与テキストtrain.txt, test.txtを作成


train.txt

0_0.png 0

0_1.png 0
...
4_32.png 4
...

等と、クラスを連番にしてラベリングしてください。(連番でないとロス関数が爆発する事例もあるそうですが、定かでない)

尚train/, test/内で一部のデータだけ使用したい場合、このtrain.txt, test.txtの内容を該当のデータのみの記述とすれば良いだけ。


コマンド上で使用するデータセットLMDB or LevelDBを作成

Ubuntu on Windowsで(CAFFE_ROOT)/caffe/build/toolsにパスを通しておいて、

convert_imageset train/ train.txt train_leveldb 1 -backend leveldb 28 28

を実行。左から順に、

train/: 画像データ格納場所フォルダ名

train.txt: ラベル付与テキスト

train_leveldb: データセット配置フォルダ名(新規じゃないとダメ)

1: シャッフルする(1),しない(0:デフォルト)

-backend leveldb: LevelDB(デフォルト)にするかLMDBか。

どっちでもいいけど、試験的に同じデータセットを同時に2つ以上のデータレイヤのソースにできないのはleveldb。でも2つleveldbフォルダを生成すればいいだけの話なのでぶっちゃけどちらでも構わない(というかUbuntuOnWinでLMDBは使えないのでleveldb)

28 28: 縦,横の順でリサイズ


prototxt概説

学習のために用意しておくprototxtは主に3つあります(名前はぶっちゃけなんでも良い。呼び出しの際にファイル名の指定を正しくできてさえいれば良い)


solver.prototxt

caffe train コマンドで使用する。

学習の繰り返し回数や学習率から、trainコマンド実行後の経過観察まで、学習の様々な基本要項についてまとめたファイルとなります。

このスクリプトから、ニューラルネットワークについての記述があるtrain_test.prototxtを参照します。


train_test.prototxt

trainコマンド実行時に、solver.prototxtを通して間接的に使用します。

畳み込み層やプーリング層の細かい設定(窓のカーネルサイズやステップ幅等)、各層の順序等を決定します。

この細かなハイパーパラメータの設定が職人技と言われるところ。


deploy.prototxt

実際に画像分類を行う際に使用します。

このコードの中間部はtrain_test.prototxtの中間部と同じ内容でよくて、入力層と出力層をdeploy用に記述します。


solver.prototxt

trainコマンドで、

caffe train --solver=solver.prototxt

などという形で使用します。

以下のスクリプトはcaffe/exampleに内包されているsolver.prototxtの一つで、これを元に概説します。


(CAFFE_ROOT)/caffe/examples/mnist/lenet_solver.prototxt

# The train/test net protocol buffer definition

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
# 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: "examples/mnist/lenet"
# solver mode: CPU or GPU
solver_mode: GPU

net: "train_test.prototxt"

ニューラルネット各層の細かな設定が記述されているこのprototxtをnetworkとして指定します。

test_iter: 100

test_interval: 500

ここの値は、train_test.prototxtの、TRAINとTESTの2種のDataレイヤで記述されているdata_paramのbatch_sizeと関係を持って決定します。

バッチサイズ(batch_size)は、一度の学習/テストで使用する画像枚数です。

test_iterは1回のテストのフェーズで繰り返すテスト回数のことです。

例えば10,000枚テスト画像がありバッチサイズが100なら、100回繰り返せば1回のテストフェーズですべてのテスト画像を使うことができます。

test_intervalはテストを行う間隔のことで、ここでは500回の学習毎に1回のテストフェーズがやってくることになります。

base_lr: 0.01

momentum: 0.9
weight_decay: 0.0005

base_lr: 学習率の初期値。下で紹介するlr_policy(学習のポリシー)によってはここから変動します。

momentum: 損失関数における慣性項の意味で、というのもCaffeが採用しているMomentum SGD(慣性項付き確率的勾配降下法:収束をより慣性的にする手法)で使う、前回の更新量にここで設定した値をかけて加えるというものです。

weight_decay: 毎回の学習で損失関数に加えられる正規化項の重み。

また、正規化項のタイプはオプションで決めることができて、

regularization_type: "L2" # "L1"

という形で書ける(デフォルトはL2)

lr_policy: "inv"

gamma: 0.0001
power: 0.75

lr_policy: 学習率をステップに応じどのように変化させるかを決定。

gamma, power: lr_policyで決められた式で使用される定数


  • fixed: 最後まで同じ学習率で固定

  • step: base_lr * gamma ^ (floor(iter / step))
    後にstepsizeを指定し、この値毎に学習率を上の式で更新

  • exp: base_lr * gamma ^ iter 毎ステップ

  • multistep: stepと基本は同じだが、等間隔ではなく、stepvalueで決められた回数の時に更新。例えば以下のような形

stepvalue: 5000

stepvalue: 7000
stepvalue: 8000
stepvalue: 9000
stepvalue: 9500


  • inv: base_lr * (1 + gamma * iter) ^ (- power). べき減衰

  • poly: base_lr * (1 - iter/max_iter) ^ (power), 名前のとおりべきで、max_iterでゼロになる

  • sigmoid: base_lr ( 1/(1 + exp(-gamma * (iter - stepsize))))

display: 100

学習中、何回の学習毎にコマンドプロンプトに表示するかということです。ここはお好みで。

max_iter: 10000

終了するまで繰り返す回数。10,000とか40,000とか1,000とかお好みで。

snapshot: 5000

snapshot_prefix: "examples/mnist/lenet"

学習の最後だけでなく、途中でmodel(その時点でのNNの各重みが保存されている)やsolverstate(途中から学習を再開させるのに使う)を保存することをsnapshotといい、その回数を指定。

snapshot_prefixではその保存ファイルの接頭辞を指定。複数出力させる場合は別にフォルダを作成して接頭辞とするなど。

solver_mode: GPU

CPUならCPUを指定する。

またCaffeデフォルトソルバーはSGD(確率的勾配降下法)ですが、typeで変更することができます。

物によってmomentum2等設定する必要があるそうで、caffe/examples/mnist/以下の*_solver.prototxtを参考にしてください。

type: "SGD" # DEFAULT

# type: "Adam"
# type: "RMSProp"
# type: "AdaDelta"
# type: "AdaGrad"
# type: "Nesterov"


train_test.prototxt

公式のmnistでの例(lenet_train.test.prototxt)を参考にお話しします。



  • name: "LeNet": 本スクリプトのNNの名前。なくても通ります。

  • 最初のレイヤはDataレイヤ(type: "Data")を2種(TRAIN, TEST)用意します。(l.2~35)

  • 初めはDataレイヤ*2→Conv,Poolingレイヤを交互におく→ReLU→全結合層→Accuracy, Lossをの順においておくところからスタートすればOK

TRAINを例に説明します。

layer {

name: "mnist" # レイヤの名前。なんでも良い
type: "Data" # レイヤのタイプを指定。ここではData。すなわち入力層
top: "data" # topには通常自分自身のレイヤの名前を指定するが、DataLayerに限ってはdataと書く。
top: "label" # dataレイヤにはデータラベルのレイヤも付与する。お決まりとして書いておく
include {
phase: TRAIN # 学習のフェーズで使うと指定。TESTにするとテストのフェーズを指定
}
transform_param {
scale: 0.00390625 #
}
data_param {
source: "examples/mnist/mnist_train_lmdb" # データのソースのフォルダ名を指定。
batch_size: 64 # 1度の学習に使う入力データ数を指定。②
backend: LMDB # データの種類を指定。LMDBかLevelDBか
}
}

特に、どの層でも同じですが、

  include {

phase: TRAIN # TEST
}

を記述すると、学習/テストの場合だけ使うレイヤとして定義することができます。(Accuracy Layerでも記述あり)


  • Convolution Layer: 畳み込み層

最も有名なレイヤことConvolutionレイヤ。num_output数分窓関数を用意し、各々畳み込んだ結果を出します。

layer {

name: "conv1" # 名前。こちらもtopとbottomの指定さえ正しくできていれば何でも良いが、連番が通例
type: "Convolution" # タイプ名
bottom: "data" # 前段階のレイヤの名前を指定。
top: "conv1" # 自身のレイヤの名前を指定。

# 重み付きフィルタに乗算するlearning rateとdecay
param { lr_mult: 1 decay_mult: 1 }
# バイアスフィルタに乗算するlearning rateとdecay
param { lr_mult: 2 decay_mult: 0 }

convolution_param {
num_output: 20 # 直訳すると出力数。具体的には畳み込みの窓関数の種類を言う。

kernel_size: 5 # 窓関数の縦と横の長さ。
# kernel_w: 3 # 窓関数の縦の長さ
# kernel_h: 5 # 窓関数の横の長さ

stride: 1 # 窓関数を何ピクセル毎に適用するか。
# stride_h: 1 # 縦方向のstride
# stride_w: 2 # 横方向のstride

pad: 3 # 窓関数の縦横の余白。デフォルトは0(optional)
# pad_h: 1 # 窓関数の縦の余白。デフォルトは0(optional)
# pad_w: 5 # 窓関数の横の余白。デフォルトは0(optional)

dilation: 3 # 窓関数内部でいくつおきに窓を設けるか。デフォルトは1。(optional)

weight_filler {
type: "xavier" #
}
bias_filler {
type: "constant"
}

force_nd_im2col : false # N次元Convolutionを行うか否か。デフォルトはfalseで、この場合2次元(optional)
}
}

NDConvolution

force_nd_im2col: trueとするとN次元畳み込みができます。

LevelDBやLMDBではN次元データに対応しておらず、HDF5を用いることが必要。

HDF5はビルドからやり直す必要があり、設定含みやや厄介。

またCaffeにはまだN次元Pooling機能が搭載されていない(2017/12/3現在)ため、使い勝手がいいかと言われると微妙です。


  • Pooling Layer: プーリング層

決められた枠内で最大のピクセル値or平均値を割り出してゆき、データサイズを減らしたり、主に畳み込みした後に用いる、より良い特徴量を割り出す層です。

当然ですが入力数と出力数は同じ。

layer {

name: "pool1" # 名前。何でも良いが、連番がおすすめ。
type: "Pooling" # Type名
bottom: "conv1" # 前の層の名前
top: "pool1" # 自分の層の名前
pooling_param {
pool: MAX # 窓の中からの値の算出法を選択。MAX(最大値,デフォルト), AVE(平均), STOCHASTIC(確率的に(というかランダムに)決定)

kernel_size: 5 # 窓関数の縦と横の長さ。
# kernel_w: 3 # 窓関数の縦の長さ
# kernel_h: 5 # 窓関数の横の長さ

stride: 2 # 窓関数を何ピクセル毎に適用するか。
# stride_h: 1 # 縦方向のstride
# stride_w: 2 # 横方向のstride

pad: 3 # 窓関数の縦横の余白。デフォルトは0(optional)
# pad_h: 1 # 窓関数の縦の余白。デフォルトは0(optional)
# pad_w: 5 # 窓関数の横の余白。デフォルトは0(optional)

}
}


  • ReLU Layer: 活性化関数の一つで、ステップ関数。

活性化関数は出力を正規化するのに使い、ReLUの他にシグモイド関数, ソフトマックス関数などがあります。

過去は活性化関数にシグモイド関数を採用する風潮があり(見た目がカッコいいから!?)、お陰で上手くいかない時期が長くなったそうな。

layer {

name: "relu1"
type: "ReLU"
bottom: "conv1"
top: "conv1"

# optional
# xが負の場合何倍の値を出力するかを決めるパラメータで、デフォルトは0。1だと単にy=xとなる
navigate_slope: 1

活性化関数を挟む場合、基本はReLUを使えば良いと思います。


  • InnerProduct Layer: 全結合層

前のレイヤの出力全てを重み付きで入力させ、和を取った値を出力する層です。

必然とnum_outputは重みのバリエーションの数となります。

全結合層は出力レイヤにも使われ、その場合のnum_outputは分類クラス数に一致させます。

layer {

name: "ip2"
type: "InnerProduct"
bottom: "ip1" # 自分の前の層の名前
top: "ip2" # 自分の層の名前
# 重み付きフィルタに乗算するlearning rateとdecay
param { lr_mult: 1 decay_mult: 1 }
# バイアスフィルタに乗算するlearning rateとdecay
param { lr_mult: 2 decay_mult: 0 }
inner_product_param {
num_output: 10 # 出力数。
weight_filler {
type: "xavier"
}
bias_filler {
type: "constant"
}
}
}


  • Accuracy Layer: 認識率

最後の出力層と最初に定義したlabelレイヤをbottomに置いてipで認識率を出力する層。includeを消せばTRAIN時も認識率を吐き出してくれます。

layer {

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


  • Loss Layer: 損失関数
    逆誤差伝播法に基づき損失を計算します。

layer {

name: "loss"
type: "SoftmaxWithLoss"
bottom: "ip2"
bottom: "label"
top: "loss"
}

上記のlayerらで簡単なNNは作れますが、その他Dropoutレイヤ(層の中のいくつかのノードを無効にする)等も組み込むと精度向上によくて、その辺りは公式Caffe | Layer Catalogueなりexampleなりを参考にしてください。


deploy.prototxt

学習後、学習済みモデルを用いて画像分類する際のレイヤを記述します。

上にも書きましたが入力DataLayerと出力レイヤ以外はtrain_test.prototxtに一致させる必要があり、そうでないとエラーが返ります。

具体的には、

train_test.prototxt → deploy.prototxt

2種のデータレイヤ → Inputレイヤ

Accuracy, Lossレイヤ → Probレイヤ

といった形に改変します。

公式のmodels/bvlc_reference_caffenet/deploy.prototxtを参考に概説します。


  • Netの名前

name: "CaffeNet" # なんでも良い


  • Input Layer: 入力層

layer {

name: "data" # なんでも良い
type: "Input" # type
top: "data" # ここは変更しない
input_param { shape: { dim: 10 dim: 3 dim: 227 dim: 227 } }
}

input_paramのdimは順に、

train_test.prototxtのDataレイヤ(Train)のバッチサイズ

チャネル数





です。


  • Prob Layer: 予測値を確率と共に吐き出すレイヤ

layer {

name: "prob"
type: "Softmax"
bottom: "fc8"
top: "prob"
}


画像分類

お待たせしました画像分類です。

5クラス分類の学習で、

data/0_0.png, data/0_1.png, ..., data/0_4.png, data/1_0.png, ..., data/5_4.png

の計30枚のデータを分類したいときのpythonコードが以下。


classifier.py


import numpy as np
import scipy as sp

import caffe
from PIL import Image

caffe.set_mode_cpu() # GPUモードならset_mode_gpu()

model_def = 'prototxt/deploy.prototxt' # deploy.prototxtを指定
model_weights = 'snapshots/snapshot_iter_10000.caffemodel' # 学習で生成されたモデル(連番の最後)を指定
net = caffe.Net(model_def, model_weights, caffe.TEST) # 変数netにcaffe.Netを生成

# transformerを生成
transformer = caffe.io.Transformer({'data': net.blobs['data'].data.shape})
transformer.set_raw_scale('data', 255)
transformer.set_transpose('data', (2, 0, 1))

net.blobs['data'].reshape(16, 3, 120, 150) # 学習のバッチサイズ, チャネル数, 縦, 横の順で

for cl in range(0, 5): # clはクラスの連番
print('\nprint class: ' + str(cl))
for n in range(0, 5):
# (cl)_(n).pngの画像をロード
image = caffe.io.load_image('data/' + str(cl) + '_' + str(n) + '.png')

# データの整形
transformed_image = transformer.preprocess('data', image)
net.blobs['data'].data[...] = transformed_image
output = net.forward()
output_prob = output['prob'][0]

# ラベル名の格納されたテキストファイルをロード
labels_file = 'synset_words.txt'
labels = np.loadtxt(labels_file, str, delimiter='\t')

# 予測ラベルを吐き出す
print('predicted class is ', labels[output_prob.argmax()]) #


synset_words.txtは、各クラス番号の名前が記述されています。例えばこんな書き方でよいです。


synset_words.txt

0 cat

1 dog
2 hamster
3 cow
4 horse


その他, 最後に

modelファイルから中間レイヤを表示できたり、ロスやAccuracyのグラフをプロットできたり、ファインチューニングだったり、他にも色々できることがCaffeにはありますが、それはまた追い追い。

むつかしいものですが1人でも多くの人がCaffeに挑戦しようと思ってくだされば幸いです。