Skip to content

Instantly share code, notes, and snippets.

@rezoo
Last active November 4, 2021 15:28
Show Gist options
  • Save rezoo/a1c8d1459b222fc5658f to your computer and use it in GitHub Desktop.
Save rezoo/a1c8d1459b222fc5658f to your computer and use it in GitHub Desktop.

Caffe tutorial

この文章ではCNN実装であるCaffeを用いて,特徴ベクトルの抽出やパラメータの学習を行うための方法について説明する.

Caffeでサポートされている機能

以下の作業を行いたいのであれば,Caffeを用いることが望ましい.

  • CNNを利用した画像の多クラス分類
  • CNNによる特徴ベクトルの抽出
  • CNNの転移学習
  • Stacked Auto Encoder

!重要!

Caffeは(例えmasterブランチだろうが)頻繁に仕様が変わるので前動いたやつが今は動かないなんてことがしばしばある.この文章も恐らく数カ月後には動かない箇所が出てくると思われる :(

Installation

Anacondaのインストール

基本的なインストール方法は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パッケージをインストールしていないだけなのでインストールする.

Quickstart

Extracting features as an alternative to SIFT

まずはじめに,単純にSIFT/BoFと同様に画像から特徴ベクトルを抽出したい場合のCaffeの使用方法を説明する.特徴量の取り出しはおおまかに2つの方法がある.1つがcaffe/build/tools/extract_features.binを呼び出す方法で,もう1つがPython経由で特徴ベクトルを取り出す方法.楽なのは後者.今回はネット上に公開されているImagenetの学習パラメータを使用して,Python経由で特徴ベクトルを取り出す方法について説明する.

Imagenetの学習パラメータのダウンロード

caffe/examples/imagenet/に移動し,

$ ./get_caffe_reference_imagenet_model.sh

を実行.Dropboxから200MB位の学習パラメータがダウンロードされる(caffe_reference_imagenet_model).もう一度get_caffe_reference_imagenet_model.shを実行するとダウンロードファイルが正しいかどうか確認するためのチェックサムの計算が行われるが,何故か全然合わないことがある.その場合はチェックサム合っているバージョンを送りますので,連絡よろしくお願いいたします.

Python上での特徴ベクトル取り出し

次に,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つのモードTRAINTESTをもつ.特徴ベクトルの取り出しだけであれば,特に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ファイルを保存するのが良い.

predict()oversampleオプションの取り扱いについて

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())

Learning CNN with many images

次に,大量の画像を利用して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の機能の一つ).引数の意味は次の通り:

  1. 画像ファイルの場所(後ろの"/"を忘れないこと)
  2. テキストファイルの場所
  3. LevelDBの出力先
  4. 格納する画像ファイルの順序をシャッフルするか否か(テキストファイルの正解IDがソートされている場合は1を指定.擬似乱数のシードの関係で,シャッフルされる順序は 常に固定 ).
  5. データベースのバックエンドに何を使用するか.leveldbの他にもlmdb(LevelDBよりも高機能なKVS)を指定することも可能.
    1. リサイズする画像の縦と横のサイズ(アスペクト比は無視.すべて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に分けて記述する.

レイヤの繋がりを表すパラメータがbottomtopである.これはどちらかと言うとinputoutputで考えたほうが理解しやすい.ソルバーはファイルに記述された順番で,各レイヤに対しForward()関数を実行する(学習の場合は逆順序でBackward()を実行する).bottomtopは複数あっても良い.例えば,複数のレイヤを繋げるCONCATレイヤは複数個のbottomを持つ.bottomtopに指定する名前はnameと異なっていても良い.例えば,DATAレイヤはtopに,nameには無い新しい名前labelを指定している.

CNNは2つのモードTRAINTESTを持つ.includeを指定した場合,対象のモードになった場合にだけそのレイヤがCNN内に含まれる.

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

CNN学習の詳細

はじめに,Caffeはデータベースから対象のファイルをバッチ数分だけ読み込む.TRAINモードの場合,Caffeは画像から平均画像を引いた後に,crop_sizeで指定した大きさの画像をランダムに切り抜く.また,mirror=trueの場合50%の確率で左右反転する.[0, 1]に正規化するなどの処理は行なっていない.TESTモードの場合,Caffeは中央部分だけを切り抜き反転は行わない.

Transfer learning

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.prototxtbase_lr0.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

等のような形で呼び出してやればよい.

Question and Answer

最後に,やってはいないもののできるかもしれない機能について言及する.

一方向だけでなく木構造のようなレイヤ構造を持つことは可能か?

CONCATレイヤの存在があるので,入力(DATAレイヤ)を2つ以上持たせることはできる.ただ,その場合Pythonのネイティブサポートはないので,適宜頑張る必要がある.

セグメンテーション/回帰/複数の推定解の同時出力は可能か?

できる.入力と出力のデータセットを2つ用意し,それぞれDATAレイヤで読み込ませ,LOSSレイヤのbottomのうち一つを出力用のDATAレイヤに指定する.

複数のGPUを利用した高速演算は可能か?

微妙.ILSVRC2014では魔改造でMultiple GPUに対応したグループもあるらしいが,少なくとも現状のCaffeではサポートされていない.

@kaeton
Copy link

kaeton commented Apr 15, 2016

参考にさせていただいております。
自分は画像からの特徴量抽出をしようと思い、imagenetの学習パラメーターをダウンロードしようとしたのですが、指定のディレクトリで_caffe_reference_imagenet_model.shが見つからず困っています。何かご存知だったりしませんでしょうか?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment