この文章ではCNN実装であるCaffeを用いて,特徴ベクトルの抽出やパラメータの学習を行うための方法について説明する.
以下の作業を行いたいのであれば,Caffeを用いることが望ましい.
- CNNを利用した画像の多クラス分類
- CNNによる特徴ベクトルの抽出
- CNNの転移学習
- Stacked Auto Encoder
Caffeは(例えmaster
ブランチだろうが)頻繁に仕様が変わるので前動いたやつが今は動かないなんてことがしばしばある.この文章も恐らく数カ月後には動かない箇所が出てくると思われる :(
基本的なインストール方法はInstallationを参照すればよいが,それだけでは微妙に躓きそうな箇所について簡単に記載する.
まず,CaffeをPython上で動かすためにPythonの科学研究関連のパッケージを一式揃える必要がある.そのインストール作業を一から行うと面倒だが,Anacondaと呼ばれる科学研究系パッケージが全部揃っているパッケージを使うと便利.アンインストールもディレクトリを削除するだけでいいので楽.
Download Anacondaからanacondaをダウンロード.実行権限を与えてシェルを実行.後は流れに任せて適当に答えるだけでよい.最後に~/.bashrc
の末尾に,
export PATH=$HOME/anaconda/bin:$PATH
export LD_LIBRARY_PATH=$HOME/anaconda/lib:$LD_LIBRARY_PATH
を追加し,パスを通す(多分最初のPATH
に関しては既にAnaconda側がよろしくやってくれている筈).ipython
を実行し,
$ ipython
Python 2.7.8 |Anaconda 2.0.1 (64-bit)| (default, Jul 2 2014, 18:08:02)
Type "copyright", "credits" or "license" for more information.
IPython 2.1.0 -- An enhanced Interactive Python.
Anaconda is brought to you by Continuum Analytics.
Please check out: http://continuum.io/thanks and https://binstar.org
? -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help -> Python's own help system.
object? -> Details about 'object', use 'object??' for extra details.
In [1]:
とAnaconda側のipythonが呼び出されていることを確認.
次に,caffeで使用するOpenCVのパッケージをAnaconda経由でインストールする(OpenCVのパッケージもすべてAnaconda側が用意してくれている).apt/yumと同様に,anacondaのパッケージ管理ソフトはconda
を通して実行する.
$ conda update opencv
でOpenCVをインストール.次に,Caffeで使用するProtocol BufferのPythonバインディングをpip
経由でインストールする(OpenCV等のインストールが面倒なパッケージは大体Anacondaが用意してくれているのでconda
でインストールするが,そうでない場合はPythonのパッケージ管理システムであるpip
経由でインストールする).
$ pip install protobuf
でProtocol Bufferをインストールできる.
後はLevelDBやBoostなど,CaffeのC++用ライブラリをapt
経由でインストールするが,そこら辺はInstallation見ると大体分かるはず.コンパイル方法も同様.Anacondaの場合はAnaconda用にMakefile.config
を書き換えれば良い.
Ubuntu 12.04の場合は基本的にその流れでいけるが,Ubuntu 14.04の場合はgcc-4.6
を呼び出すように明示的に書き換える必要がある.そこら辺は確かGithubに同じ質問内容が流れていたはず.
$ make runtest
を実行して,特にエラーが出なければ正常にビルドが完了している.
次に,Python用のCaffeパッケージをビルドする.そのためには$ make pycaffe
を呼び出してmakeさせる必要あり.また,PYTHONPATH
環境変数にcaffeのPythonディレクトリを追加することも必要.Python上で
>>> import caffe
と打ってエラーでなければ大丈夫.Protocol buffer関係のエラー出た場合は多分protobuf
パッケージをインストールしていないだけなのでインストールする.
まずはじめに,単純にSIFT/BoFと同様に画像から特徴ベクトルを抽出したい場合のCaffeの使用方法を説明する.特徴量の取り出しはおおまかに2つの方法がある.1つがcaffe/build/tools/extract_features.bin
を呼び出す方法で,もう1つがPython経由で特徴ベクトルを取り出す方法.楽なのは後者.今回はネット上に公開されているImagenetの学習パラメータを使用して,Python経由で特徴ベクトルを取り出す方法について説明する.
caffe/examples/imagenet/
に移動し,
$ ./get_caffe_reference_imagenet_model.sh
を実行.Dropboxから200MB位の学習パラメータがダウンロードされる(caffe_reference_imagenet_model
).もう一度get_caffe_reference_imagenet_model.sh
を実行するとダウンロードファイルが正しいかどうか確認するためのチェックサムの計算が行われるが,何故か全然合わないことがある.その場合はチェックサム合っているバージョンを送りますので,連絡よろしくお願いいたします.
次に,Python上からCaffeを呼び出して特徴ベクトルを取り出す方法について説明する.Caffeの分類器の作成には,3つのファイルを用意する必要がある.一つはCNNがどのように構成されているのかを表す.prototxt
ファイル.もう一つはパラメータの実態が含まれるmodel
ファイル.最後に平均画像を含む.npy
ファイル(Numpyの行列ファイルは,一般に拡張子.npy
をつけて保存することが一般的).分類器のインスタンスは次のような形で生成する.
import caffe
import numpy as np
net_path = "caffe/examples/imagenet/imagenet_deploy.prototxt"
model_path = "caffe/examples/imagenet/caffe_reference_imagenet_model"
mean_path = "caffe/python/caffe/imagenet/ilsvrc_2012_mean.npy"
net = caffe.Classifier(
net_path, model_path, mean=np.load(mean_path),
channel_swap=(2, 1, 0), raw_scale=255,
image_dims=(256, 256), gpu=True)
ILSVRC2012の学習パラメータは入力から平均画像を引いているので,そのための平均画像を読み込ませる必要あり.channel_swap
はどのような順番で色情報をスワップさせるのかについての指定(Caffeのreference modelは色情報をRGBでなくBGRオーダーで扱うため.何故BGRかというと,BGRオーダーで画像を扱うOpenCVを内部で使っているから).raw_scale
は読み込まれた入力画像をどれ位スケールするかについての指定(詳細は後述).image_dims
は読み込まれた入力画像をどの解像度でリサイズするかの指定,gpu
オプションはGPUを使って計算を高速化させるか否か(10倍位遅くなるが,使いたくない場合はFalse
を指定).
なお,Caffeの分類器は2つのモードTRAIN
とTEST
をもつ.特徴ベクトルの取り出しだけであれば,特にTRAIN
モードを考慮する必要はない.また,caffe.Classifier
のコンストラクタ内部で自動的にTEST
モードに設定されるので,明示的にnet.set_phase_test()
を呼び出す必要もない.
次に,画像の読み込みと識別,特徴ベクトルの抽出に移る.識別にはnet.predict()
を使用する.画像は複数枚入れることができる.返り値にはそれぞれのクラスの確信度を表すベクトルが画像枚数分格納された行列が返される.すなわち(n_images, n_classes).また,特徴ベクトルの抽出をしたい場合は特に理由がなければoversample=False
を指定するのが望ましい(理由は後述).画像の読み込みにはcaffe.io.load_image()
を使用する.すなわち,
image = caffe.io.load_image("cat.jpg")
pred = net.predict([image], oversample=False)
である.画像の実態はRGBオーダーの3階のテンソル(width, height, 3)が含まれるnumpy.ndarray
である.画像の各チャネルは[0, 255]
でなく[0, 1]
に正規化されていることに注意.これでCNN内のすべてのレイヤにそれぞれの特徴量が格納された.
次に,上層のレイヤfc7
から4096次元の特徴ベクトルを取り出す.fc7
の特徴ベクトルの取り出しは次のようにして行う.
feature = net.blobs['fc7'].data
feature = feature.reshape(10, 4096)[0, :]
data
の実態は4階のテンソル(n_batches, n_channels, height, width)である.今回の場合だとバッチ容量は10個,fc7
は4096個のユニットを持っているので(10, 4096, 1, 1)である.バッチ数はpredict
で処理する画像のバッチ容量を表す.多いと一気に処理できる画像の枚数が増えるがその分GPUメモリを食う.また1枚1枚predict()
を呼び出したい場合には不利.imagenet_deploy.prototxt
では10に設定されている.これを(10, 4096)の行列に直し一番初めの特徴ベクトルだけ取り出すことで,4096次元のベクトルを得る.以降の処理をMatlabで行いたい場合はscipy.io.savemat()
を呼び出し,.matファイルを保存するのが良い.
Classifier.predict()
のoversample
は識別結果を向上させるためのオプションである(デフォルトはTrue
).結論を端的に述べると,これはClassifier
内のimage_dims
を明示的に指定しない限り計算が重くなるだけでほとんど性能向上には貢献しない.また特徴ベクトルだけ取り出したいだけの場合は無駄なのでFalse
を指定すべきである.
predict()
の詳細な挙動を次に示す.まず,predict()
は対象の画像をClassifier.image_dims
の大きさにリサイズする.指定されていない場合は.prototxt
で指定したcrop_size
の値が使用される.次に,oversample
が指定された場合,Classifier
は対象の画像の上下左右4箇所と,中央の1箇所を切り取る.切り取る画像の大きさはすべて(crop_size, crop_size)である.加えて,これらを左右反転させた画像を作り,同様に5箇所切り抜く.結果として10枚の(crop_size, crop_size)の画像が生成される.oversample
が指定されない場合は中央の1箇所だけが使用される.後はそれをスケール倍(raw_scale
)したり平均画像引いたりした後にCNNのネットワークに送り,出力結果を得る.oversample
を指定した場合,これらの結果を平均したベクトルが返される.
Caffeの平均画像の取り扱いは結構適当.まず,平均画像は読み込まれた後,(crop_size, crop_size)の大きさにリサイズされて格納される.predict()
はその平均画像を,(crop_size, crop_size)で切り取られた画像に対して引き,CNNのネットワークに送る.元々の学習過程(後述)を考えると結構大雑把なことをやっている.また,extract_features.bin
はまた違う形で平均画像を取り扱っているので,Pythonで抽出した特徴ベクトルとextract_features.bin
で取り出した特徴ベクトルの値は異なる(識別性能も微妙に変わる).
例として,Imagenetディレクトリに含まれる画像すべての特徴ベクトルを抽出し,それをLevelDBと呼ばれるKVSに格納するコードを示す(何故そのようなDBを使う必要があるのかというと,Imagenetのような数百万枚すべての特徴ベクトルをメモリに格納することは困難なため).現在,Imagenetのディレクトリ/path/to/imagenet/train
内にはカテゴリ毎のディレクトリ1,000個が存在し,それぞれのディレクトリには画像1,300枚が格納されている.下のサンプルプログラムはそれらを読み込み,4096次元のfloat型の特徴ベクトルを抽出し,画像のファイル名をKey,その特徴ベクトルをValueとしてLevelDB内に順次格納していく.Caffeの応用例の一つとして参考にされたい.ちなみに,GTX Titan Blackで試したところ実行速度は約20-30 images/sec,大体16時間位を要した.
# -*- coding: utf-8 -*-
import sys
import os
import logging
import numpy as np
import leveldb
import caffe
PROJ_ROOT = "/path/to/proj"
CAFFE_ROOT = "/path/to/caffe"
DATA_ROOT = "/path/to/imagenet/train"
LEVELDB_PATH = PROJ_ROOT + "/data/imagenet"
CAFFE_PROTO = CAFFE_ROOT + "/examples/imagenet/imagenet_deploy.prototxt"
CAFFE_MODEL = CAFFE_ROOT + "/examples/imagenet/caffe_reference_imagenet_model"
CAFFE_MEAN = CAFFE_ROOT + "/python/caffe/imagenet/ilsvrc_2012_mean.npy"
LAYERNAME = "fc7"
logging.basicConfig(
format="%(asctime)s [%(levelname)s] %(message)s",
level=logging.DEBUG)
logger = logging.getLogger(__name__)
def main():
def filter_by(db, filename):
ext = os.path.splitext(filename)[1].lower()
if ext not in [".jpeg", ".jpg", ".png"]:
return False
try:
db.Get(filename)
except KeyError:
return True
else:
return False
logger.info("load fundamental components...")
net = caffe.Classifier(
CAFFE_PROTO, CAFFE_MODEL,
mean=np.load(CAFFE_MEAN),
channel_swap=(2, 1, 0), raw_scale=255,
image_dims=(256, 256), gpu=True)
db = leveldb.LevelDB(LEVELDB_PATH)
n_batches = net.blobs[LAYERNAME].data.shape[0]
categories = os.listdir(DATA_ROOT)
logger.info("# of categories: {0}".format(len(categories)))
for i, category in enumerate(categories):
logger.info("processing category {0}/{1}...({2:.2%})".format(
i, len(categories), float(i)/len(categories)))
category_path = os.path.join(DATA_ROOT, category)
filenames = [f for f in os.listdir(category_path) if filter_by(db, f)]
for s_begin in xrange(0, len(filenames), n_batches):
s_end = s_begin + n_batches
queue_filename = filenames[s_begin:s_end]
queue_data = [caffe.io.load_image(
os.path.join(category_path, f)) for f in queue_filename]
net.predict(queue_data, oversample=False)
blobs = net.blobs[LAYERNAME].data
n_queues = len(queue_data)
n_dim = np.prod(blobs.shape[1:])
features = blobs.reshape(n_batches, n_dim)
batch = leveldb.WriteBatch()
for j in xrange(n_queues):
batch.Put(queue_filename[j], features[j, :].tostring())
db.Write(batch, sync=True)
return 0
if __name__ == "__main__":
sys.exit(main())
次に,大量の画像を利用してCNNを学習するための方法について述べる.Caffeを利用した学習は次の手順で行う: (1)画像ファイル名とカテゴリIDのペアからLevelDBのデータセットを作成する (2)平均画像を作成する (3)LevelDBのデータセットを用いてCNNを学習する
データセットの作成は次のようにして行う.初めに,対象の画像とそのカテゴリIDのペアが含まれたテキストファイルを作成する.例を次に示す.
42813a4366850c30e2dcdccf7f2c634e2f44d947.jpg 4
76a62e47bf96d0695f123885442ebf543fecfa3c.jpg 127
85c35b0935d370cea801ad9fdcbe55f0d1b33dee.jpg 115
...
テキストファイルはtrain, validation, testの三種類を用意する.trainは学習,validationは学習結果の検証,testは識別性能のテストに用いる.学習用のディレクトリを作り,その中にそれぞれtrain.txt
, val.txt
, test.txt
として保存しておくことが望ましい.
次に,/caffe/build/tools/convert_imageset.bin
を呼び出し,画像をLevelDBのデータベース内に格納していく.呼び出し例を次に示す:
GLOG_logtostderr=1 $HOME/caffe/build/tools/convert_imageset.bin \
/path/to/image/dir/ \
/path/to/proj/train.txt \
train_leveldb 1 leveldb
256 256
環境変数GLOG_logtostderr
に1を指定することで,ログをすべてstderr
に出すことができる(GLOG
の機能の一つ).引数の意味は次の通り:
- 画像ファイルの場所(後ろの"/"を忘れないこと)
- テキストファイルの場所
- LevelDBの出力先
- 格納する画像ファイルの順序をシャッフルするか否か(テキストファイルの正解IDがソートされている場合は1を指定.擬似乱数のシードの関係で,シャッフルされる順序は 常に固定 ).
- データベースのバックエンドに何を使用するか.
leveldb
の他にもlmdb
(LevelDBよりも高機能なKVS)を指定することも可能. -
- リサイズする画像の縦と横のサイズ(アスペクト比は無視.すべて0を指定した場合,すべての画像が予め同じ解像度にリサイズされていると判断しリサイズを行わない.縦と横のサイズは同じでなくても構わないが,Caffeの仕様上,すべての画像の解像度は同じでなければならないことに注意).
詳細な例は/caffe/examples/imagenet/create_imagenet.sh
を参照(実は若干古いので,サンプルそのままでは動かない!).
また,LevelDBに格納されるデータの実態を示す.key
は番号の接頭辞が着いたファイル名,value
は/caffe/src/caffe/proto/caffe.proto
で指定された構造体Datum
をProtocol Bufferでシリアライズ化した文字列(言い換えると,画像データとラベルのペアを含む文字列).もし自分でLevelDBのデータセットを作りたい場合には,keyとvalueをそのように指定してLevelDBに突っ込んであげるだけでよい.
次に,trainデータセットから学習を行うための平均画像を作成する.平均画像は/caffe/build/tools/compute_image_mean.bin
から作ることができる.呼び出し例を次に示す.
$HOME/caffe/build/tools/compute_image_mean.bin /path/to/train_leveldb /path/to/meanfile.binaryproto
第一引数にtrainデータセットの場所,第二引数に平均画像の出力先を指定する(拡張子は慣習的に.binaryproto
を用いる)..binaryproto
の実態は/caffe/src/caffe/proto/caffe.proto
で指定された構造体BlobProto
をシリアライズ化したもの.すなわち,.binaryproto
をPython上から読み出すようにするためには,次の手順で変換すれば良い.
import caffe
from caffe.io import blobproto_to_array
from caffe.proto import caffe_pb2
blob = caffe_pb2.BlobProto()
with open("/path/to/meanfile.binaryproto", "rb") as fp:
blob.ParseFromString(fp.read())
numpy_array = blobproto_to_array(blob)
次に,train及びvalidationに使う,CNNのレイヤ構成を表したパラメータファイルを作る.下地は/caffe/examples/imagenet/imagenet_train_val.prototxt
にあるので,それをプロジェクトディレクトリにコピーするのが良い.変えなければならないのは次の箇所:
data/data_param/source
: 生成したデータセットの場所を指定data/data_param/mean_file
: 生成した平均画像の場所を指定data/data_param/batch_size
: バッチサイズを指定.デフォルトの256は大きすぎて多分メモリ不足で落ちる.128や96あたりを指定すること.fc8/inner_product_param/num_output
: 現在解きたい問題のカテゴリ数を指定
次に,/caffe/examples/imagenet/imagenet_solver.prototxt
を同様にプロジェクトディレクトリにコピーし,次の箇所を適当に変更する:
net
: CNNのレイヤ構成を表したパラメータファイルの場所.同ディレクトリにあって名前変えてなければそのままで良いsnapshot_prefix
: スナップショットファイルの接頭辞.特に気にならなければそのままで良い
パラメータファイルには,CNNの各レイヤを表すlayers
を羅列していく.layers
は固有の名前name
とその具体的な種類type
を持つ.Caffeのlayers
は論文中で書かれるレイヤとは少々異なり,むしろ 処理単位 といった意味合いが強い.例えば,内積(あるいは畳込み)を取った後にReLUで非線形処理を行う場合,Caffeは2つのレイヤINNER_PRODUCT
(or CONVOLUTION
)とRELU
に分けて記述する.
レイヤの繋がりを表すパラメータがbottom
とtop
である.これはどちらかと言うとinput
とoutput
で考えたほうが理解しやすい.ソルバーはファイルに記述された順番で,各レイヤに対しForward()
関数を実行する(学習の場合は逆順序でBackward()
を実行する).bottom
とtop
は複数あっても良い.例えば,複数のレイヤを繋げるCONCAT
レイヤは複数個のbottom
を持つ.bottom
とtop
に指定する名前はname
と異なっていても良い.例えば,DATA
レイヤはtop
に,name
には無い新しい名前label
を指定している.
CNNは2つのモードTRAIN
とTEST
を持つ.include
を指定した場合,対象のモードになった場合にだけそのレイヤがCNN内に含まれる.
次に,パラメータファイルとデータセットからCNNを学習する.CNNの学習にはcaffe/build/tools/caffe
を使う.具体的には,以下のコマンドを用いて学習を行う.
$HOME/caffe/build/tools/caffe train --solver=/path/to/imagenet_solver.prototxt
CaffeはTRAIN
モードとTEST
モードを交互に行う.TRAIN
は画像からパラメータを更新するモード.TEST
はvalidationデータセットから現在の大まかな精度を判定するモードで,平均損失と精度が表示される.
GTX Titanを使った場合の学習は大体3-5日を要する.指定された反復回数ごと(デフォルトは10,000回)にCaffeはスナップショットを取る.スナップショットには2種類存在し,.solverstate
がついているファイルとついていないファイルの両方が生成される.前者は学習率などの状態が含まれるファイルで,学習を再開する場合に用いる.後者はCNNのパラメータだけが含まれるファイルで,Python経由の呼び出しの際に用いる.再開の方法は前述のコマンドに,--snapshot
オプションを含めるだけでよい:
$HOME/caffe/build/tools/caffe train --solver=/path/to/imagenet_solver.prototxt \
--snapshot=/path/to/caffe_imagenet_train_10000.solverstate
はじめに,Caffeはデータベースから対象のファイルをバッチ数分だけ読み込む.TRAIN
モードの場合,Caffeは画像から平均画像を引いた後に,crop_size
で指定した大きさの画像をランダムに切り抜く.また,mirror=true
の場合50%の確率で左右反転する.[0, 1]
に正規化するなどの処理は行なっていない.TEST
モードの場合,Caffeは中央部分だけを切り抜き反転は行わない.
CNNの学習は非常に時間のかかるものでありかつ大量の画像を必要としたが,既存のパラメータを別のタスクでも使いまわす転移学習を行うことで,効率的かつ高速に学習を行える.本節ではILSVRC2012で予め学習されたパラメータを利用したCNNの転移学習について説明する.
Caffeのデータセットの作成は前述を参照.平均画像に関しては既存のimagenet_mean.binaryproto
を使えばいいので省略.
パラメータファイルに関しては前述の学習の変更箇所に加え,より多くの箇所を変更する必要がある.caffe
はまずdeployに使用するモデルファイルとCNNのパラメータファイルの両方を読み込み,すべてのレイヤとモデルファイル内のレイヤを照らし合わせ,レイヤ名が同一であればパラメータを転移させる処理を行う.すなわち,転移させたくない上位のパラメータの名前を変更することで,転移を防ぐ必要がある.初めに,一から学習させたいレイヤの名前を変更する.これはimagenet_train_val.prototxt
内のレイヤ名を何か別の名前に変更する(e.g. fc7
-> fc7mod
)だけでよい.
次に,転移させたレイヤ内のパラメータは変更させないよう調整する.これは学習率を0におくだけでよい.すなわち,転移レイヤのblobs_lr
ならびにweight_decay
を0に変更する.
転移学習の場合,そのままの学習率で学習を行った場合パラメータが発散してしまう.そのため,imagenet_solver.prototxt
のbase_lr
を0.01
から0.001
に変更する.もしこれでも発散する場合は1/2ずつ下げてやればよい.
最後に,caffe
を呼び出してCNNの転移学習を開始する.パラメータの転移は--weights
オプションを付与する.すなわち:
$HOME/caffe/build/tools/caffe train --solver=/path/to/imagenet_solver.prototxt \
--weights=$HOME/caffe/examples/caffe_reference_imagenet_model
等のような形で呼び出してやればよい.
最後に,やってはいないもののできるかもしれない機能について言及する.
CONCAT
レイヤの存在があるので,入力(DATA
レイヤ)を2つ以上持たせることはできる.ただ,その場合Pythonのネイティブサポートはないので,適宜頑張る必要がある.
できる.入力と出力のデータセットを2つ用意し,それぞれDATA
レイヤで読み込ませ,LOSS
レイヤのbottom
のうち一つを出力用のDATA
レイヤに指定する.
微妙.ILSVRC2014では魔改造でMultiple GPUに対応したグループもあるらしいが,少なくとも現状のCaffeではサポートされていない.
参考にさせていただいております。
自分は画像からの特徴量抽出をしようと思い、imagenetの学習パラメーターをダウンロードしようとしたのですが、指定のディレクトリで_caffe_reference_imagenet_model.shが見つからず困っています。何かご存知だったりしませんでしょうか?