読者です 読者をやめる 読者になる 読者になる

人工知能に関する断創録

人工知能、認知科学、心理学、ロボティクス、生物学などに興味を持っています。このブログでは人工知能のさまざまな分野について調査したことをまとめています。最近は、機械学習、Deep Learning、Kerasに関する記事が多いです。



VGG16のFine-tuningによる犬猫認識 (2)

Deep Learning Keras コンピュータビジョン

VGG16のFine-tuningによる犬猫認識 (1) (2017/1/8)のつづき。

前回、予告したように下の3つのニューラルネットワークを動かして犬・猫の2クラス分類の精度を比較したい。

  1. 小さな畳み込みニューラルネットをスクラッチから学習する
  2. VGG16が抽出した特徴を使って多層パーセプトロンを学習する
  3. VGG16をFine-tuningする

リポジトリ:dogs_vs_cats

1. 小さな畳み込みニューラルネットをスクラッチから学習する

ベースラインとしてVGG16は使わずに小規模な畳み込みニューラルネットワークをスクラッチから学習する。学習データは、犬クラス1000枚、猫クラス1000枚と小規模なデータを使うのであまり大規模なネットワークは学習できない。そこで、畳込みが3層のLeNet相当の小さなモデルを構成した。

f:id:aidiary:20170110195058p:plain:h400

横の矢印はそのレイヤでの出力の4Dテンソルのサイズ (samples, rows, cols, channels) を表している。入力は、150x150ピクセルで3チャンネルのカラー画像とした。青で囲った部分が畳込み層(Convolution2D)+プーリング層(MaxPooling2D)で緑で囲った部分がフル結合層(FC)である。犬猫分類では、出力ユニット数は1であり、0が猫で1が犬とする。

(補足)犬クラスと猫クラスの割り当てはImageDataGeneratorが勝手に決める。どっちが0でどっちが1に割り当てられたかはclass_indicesでわかる。

> print(train_generator.class_indices)
{'dogs': 1, 'cats': 0}

今回は猫が0で犬が1なのでNNの出力は犬である確率(出力が1に近いほど犬で0に近いほど猫)と解釈できる。0.5を閾値として0.5未満なら猫、0.5以上なら犬と判定すればよい。どのディレクトリがどのクラスに割り当てられるかは表示してみないとわからないみたい。いつもアルファベット順ではなさそうだ。

さっそく、Kerasで実装してみよう。

スクリプト: smallcnn.py

    # モデルを構築
    model = Sequential()
    model.add(Convolution2D(32, 3, 3, input_shape=(150, 150, 3)))
    model.add(Activation('relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))

    model.add(Convolution2D(32, 3, 3))
    model.add(Activation('relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))

    model.add(Convolution2D(64, 3, 3))
    model.add(Activation('relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))

    model.add(Flatten())
    model.add(Dense(64))
    model.add(Activation('relu'))
    model.add(Dropout(0.5))
    model.add(Dense(1))
    model.add(Activation('sigmoid'))

    model.compile(loss='binary_crossentropy',
                  optimizer='adam',
                  metrics=['accuracy'])

    # 訓練データとバリデーションデータを生成するジェネレータを作成
    train_datagen = ImageDataGenerator(
        rescale=1.0 / 255,
        shear_range=0.2,
        zoom_range=0.2,
        horizontal_flip=True)

    test_datagen = ImageDataGenerator(rescale=1.0 / 255)

    train_generator = train_datagen.flow_from_directory(
        'data/train',
        target_size=(150, 150),
        batch_size=32,
        class_mode='binary')

    validation_generator = test_datagen.flow_from_directory(
        'data/validation',
        target_size=(150, 150),
        batch_size=32,
        class_mode='binary')

    # 訓練
    history = model.fit_generator(
        train_generator,
        samples_per_epoch=2000,
        nb_epoch=nb_epoch,
        validation_data=validation_generator,
        nb_val_samples=800)

    # 結果を保存
    model.save_weights(os.path.join(result_dir, 'smallcnn.h5'))
    save_history(history, os.path.join(result_dir, 'history_smallcnn.txt'))

出力層は0から1なので出力層の活性化関数にはsigmoidを指定。また、2クラス分類なので損失関数にbinary_crossentropyを指定。最適化アルゴリズムはadamを使った。dataにある画像を直接ロードするためにImageDataGeneratorflow_from_directory()を使った。ここら辺は前回(2017/1/8)まとめた。学習履歴はあとでグラフ化したいのでsave_history()という自作関数でファイルに保存するようにした。

結果はあとで3つまとめて考察したいので次へいこう。

2. VGG16が抽出した特徴を使って多層パーセプトロンを学習する

f:id:aidiary:20170110200655p:plain:h500

2つめの方法はVGG16を特徴抽出器として使うアプローチである。VGG16は左の図のように畳み込み層のブロックが5つ続いたあとにフル結合層のブロックがくっつく構成になっている。VGG16のフル結合層はImageNetの1000クラスを分類するようになっているためそのままでは2クラスの犬・猫分類には使えない。

そこで、この使えないフル結合層(FC)を捨ててしまって直前の畳み込みブロックまでを使うのがこのアプローチである。

f:id:aidiary:20170110201742j:plain:w250
Fine-tuningのためにいつも捨てられる全結合層のイラスト(from @yu4u)

直前の畳込みブロックの出力は、(None, 4, 4, 512) でフラット化すると4x4x512=8192次元ベクトルになる。つまり、入力画像 (None, 150, 150, 3) の150x150x3=67500次元ベクトルを8192次元ベクトルに圧縮した特徴量(ボトルネック特徴量)を抽出する。そして、この8192次元ベクトルのボトルネック特徴量を入力として2クラスを出力とする多層パーセプトロン(2016/11/3)を新たに学習する。

f:id:aidiary:20170110202247p:plain:w300

この程度の小さな多層パーセプトロンの学習なら小さなデータでも十分だろう。

以下の実装では、訓練データとバリデーションデータの画像からボトルネック特徴量を抽出するsave_bottleneck_features()と抽出したボトルネック特徴量を使って多層パーセプトロンを学習するtrain_top_model()に分けている。VGG16include_topオプションをFalseにするとFC層を除外したネットワークがロードできる。

スクリプト: extractor.py

def save_bottleneck_features():
    """VGG16にDog vs Catの訓練画像、バリデーション画像を入力し、
    ボトルネック特徴量(FC層の直前の出力)をファイルに保存する"""

    # VGG16モデルと学習済み重みをロード
    # Fully-connected層(FC)はいらないのでinclude_top=False)
    model = VGG16(include_top=False, weights='imagenet')
    model.summary()

    # ジェネレータの設定
    datagen = ImageDataGenerator(rescale=1.0 / 255)

    # Dog vs Catのトレーニングセットを生成するジェネレータを作成
    generator = datagen.flow_from_directory(
        train_data_dir,
        target_size=(img_width, img_height),
        batch_size=32,
        class_mode=None,
        shuffle=False)

    # ジェネレータから生成される画像を入力し、VGG16の出力をファイルに保存
    bottleneck_features_train = model.predict_generator(generator, nb_train_samples)
    np.save(os.path.join(result_dir, 'bottleneck_features_train.npy'),
            bottleneck_features_train)

    # Dog vs Catのバリデーションセットを生成するジェネレータを作成
    generator = datagen.flow_from_directory(
        validation_data_dir,
        target_size=(img_width, img_height),
        batch_size=32,
        class_mode=None,
        shuffle=False)

    # ジェネレータから生成される画像を入力し、VGG16の出力をファイルに保存
    bottleneck_features_validation = model.predict_generator(generator, nb_validation_samples)
    np.save(os.path.join(result_dir, 'bottleneck_features_validation.npy'),
            bottleneck_features_validation)


def train_top_model():
    """VGGのボトルネック特徴量を入力とし、Dog vs Catの正解を出力とするFCネットワークを訓練"""
    # 訓練データをロード
    # ジェネレータではshuffle=Falseなので最初の1000枚がcats、次の1000枚がdogs
    train_data = np.load(os.path.join(result_dir, 'bottleneck_features_train.npy'))
    train_labels = np.array([0] * int(nb_train_samples / 2) + [1] * int(nb_train_samples / 2))

    # (2000, 4, 4, 512)
    print(train_data.shape)

    # バリデーションデータをロード
    validation_data = np.load(os.path.join(result_dir, 'bottleneck_features_validation.npy'))
    validation_labels = np.array([0] * int(nb_validation_samples / 2) + [1] * int(nb_validation_samples / 2))

    # (800, 4, 4, 512)
    print(validation_data.shape)

    # FCネットワークを構築
    model = Sequential()
    model.add(Flatten(input_shape=train_data.shape[1:]))
    model.add(Dense(256, activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(1, activation='sigmoid'))

    model.compile(loss='binary_crossentropy',
                  optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
                  metrics=['accuracy'])

    history = model.fit(train_data, train_labels,
                        nb_epoch=nb_epoch,
                        batch_size=32,
                        validation_data=(validation_data, validation_labels))

    model.save_weights(os.path.join(result_dir, 'bottleneck_fc_model.h5'))
    save_history(history, os.path.join(result_dir, 'history_extractor.txt'))


if __name__ == '__main__':
    # 訓練データとバリデーションデータのボトルネック特徴量の抽出
    save_bottleneck_features()

    # ボトルネット特徴量でFCネットワークを学習
    train_top_model()

ここで、save_bottleneck_features()では一切学習を行っていない点に注意。VGG16の学習した重みをそのまま使ってpredict_generator()で出力を求めている(フィードフォワードのみ)。一方、train_top_model()ではランダムな初期値から多層パーセプトロンの重みをfit()で学習している。ここで、optimizeradamではなく、学習率を小さくしたSGDを使った。実際は、adamでもうまくいくのだが、次のFine-tuningと比較するためにSGDにしている。

この方法では、大規模データで学習したVGG16を特徴抽出器として使っている。ニューラルネットのこのような使い方は知ってしまえば当たり前だが初めて知ったときは衝撃を受けた。Deep Learningの強みは青色部分の多段の特徴抽出にあって分類を行うフル結合層はおまけみたいなものなのだ。実際は、多層パーセプトロンを使わずにSVM(Support Vector Machine)を使うケースもあるようだ。

3. VGG16をFine-tuningする

f:id:aidiary:20170110205228p:plain:h500

2番目の方法では、実質的に重みを学習したのはフル結合層の多層パーセプトロンの部分のみだった。これに対してFine-tuningではフル結合層だけでなく、その1つ前の畳み込み層の重みも学習する。畳込みニューラルネットでは浅い層ほどエッジやブロブなど汎用的な特徴が抽出されているのに対し、深い層ほど学習データに特化した特徴が抽出される傾向がある。そこで、浅い層の汎用的な特徴抽出器はそのまま固定(frozen)し、深い層の重みのみ今回の犬・猫分類に合うように再調整(fine-tuning)してしまう

KerasでFine-tuningをするには下のようなコードを書く。

スクリプト: finetuning.py

if __name__ == '__main__':
    # VGG16モデルと学習済み重みをロード
    # Fully-connected層(FC)はいらないのでinclude_top=False)
    # input_tensorを指定しておかないとoutput_shapeがNoneになってエラーになるので注意
    # https://keras.io/applications/#inceptionv3
    input_tensor = Input(shape=(150, 150, 3))
    vgg16_model = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)
    # vgg16_model.summary()

    # FC層を構築
    # Flattenへの入力指定はバッチ数を除く
    top_model = Sequential()
    top_model.add(Flatten(input_shape=vgg16_model.output_shape[1:]))
    top_model.add(Dense(256, activation='relu'))
    top_model.add(Dropout(0.5))
    top_model.add(Dense(1, activation='sigmoid'))

    # 学習済みのFC層の重みをロード
    top_model.load_weights(os.path.join(result_dir, 'bottleneck_fc_model.h5'))

    # vgg16_modelはkeras.engine.training.Model
    # top_modelはSequentialとなっている
    # ModelはSequentialでないためadd()がない
    # そのためFunctional APIで二つのモデルを結合する
    # https://github.com/fchollet/keras/issues/4040
    model = Model(input=vgg16_model.input, output=top_model(vgg16_model.output))
    print('vgg16_model:', vgg16_model)
    print('top_model:', top_model)
    print('model:', model)

    # Total params: 16,812,353
    # Trainable params: 16,812,353
    # Non-trainable params: 0
    model.summary()

    # layerを表示
    for i in range(len(model.layers)):
        print(i, model.layers[i])

    # 最後のconv層の直前までの層をfreeze
    for layer in model.layers[:15]:
        layer.trainable = False

    # Total params: 16,812,353
    # Trainable params: 9,177,089
    # Non-trainable params: 7,635,264
    model.summary()

    # ここでAdamを使うとうまくいかない
    # Fine-tuningのときは学習率を小さくしたSGDの方がよい?
    model.compile(loss='binary_crossentropy',
                  optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
                  metrics=['accuracy'])

(1) VGG16と多層パーセプトロンの接続

2つめの実装と異なり、今回はFC層を除いたVGG16とフル結合層をKerasのFunctional APIを使って結合している。vgg16_modelがフル結合層を除いたVGG16(図の青色と黄色部分)でtop_modelが多層パーセプトロン(図の緑色部分)である。この2つのネットワークを結合する必要がある。

vgg16_model.add(top_model)とできれば理想的なのだができなかった。よくよく調べるとtop_modelSequentialクラスであるのに対し、vgg16_modelkeras.engine.training.Modelという独自クラスでなぜかこのクラスにはadd()がない!そこで、少し面倒だがこの2つの入出力をつなげる下のような新しいモデルを定義する。

model = Model(input=vgg16_model.input, output=top_model(vgg16_model.output))

この式は、新しいモデルへの入力(input)はvgg16_modelの入力と同じで、出力(output)はtop_modelvgg16_modelの出力を入力した値という意味。なんというややこしさ(^^; ここら辺はベースのTheanoやTensorFlowの書き方に少し近いと思った。

(2) フル結合層の初期値

今回は、フル結合層(top_model)の重みとして2番目の方法で学習した重みを初期値として用いた。実際はあとで結果に示すようにランダムな初期値から初めても最終的に同じ精度に収束するのでこの行は省いてもよさそう。

    # 学習済みのFC層の重みをロード
    top_model.load_weights(os.path.join(result_dir, 'bottleneck_fc_model.h5'))

同じように黄色部分の畳込み層の重みの初期値はVGG16のものだけどこれをランダム初期値から学習するとどうなるか気になった。結局、時間がかかっても同じ精度まで収束するのかな?ニューラルネットは初期値がかなり重要なファクターだからなあ。

(3) 重みの固定

VGG16の図の青色の部分は重みを固定(frozen)したい。Kerasではlayer.trainableFalseにすると重みの学習が行われなくなる。layersInputLayerlayers[0]でパラメータがないMaxPooling2Dlayersに含むのでlayers[:15]はちょうど図の青色の部分にあたる。

    # 最後のconv層の直前までの層をfreeze
    for layer in model.layers[:15]:
        layer.trainable = False

(4) 最適化には学習率の小さいSGDを使う

最初、最適化にAdamを使っていたのだがうまく学習できなかった。Fine-tuningはあくまで重みの再調整なので学習率を小さくしたSGDを使う方がよさそうだ。

実験結果

以上の3つの学習履歴をプロットすると下のようになった。

f:id:aidiary:20170110212330p:plain f:id:aidiary:20170110212333p:plain

ほぼ予想通りの結果で1つめの小さな畳み込みニューラルネット(smallcnn)では75%くらいの精度しか出ていないが、VGG16をFine-tuning(finetuning)すると94%くらいまで精度が跳ね上がる。VGG16を特徴抽出器(extractor)として用いた場合はその中間くらい。

先の(2)で述べたようにFine-tuningでフル結合層をランダムな初期値から始めても(finetuning random)最終的には学習済み初期値を使った場合と同様に94%近くまで上がることもわかった。

Kaggleのスコアを見ると精度94%はトップ100位以内には入っているのでけっこうよい順位だろう。しかも、今回は訓練に25000枚のうちたった2000枚しか使っていない。VGG16の学習済みモデルがいかに強力かわかる。

あとでテストデータで評価してKaggleにsubmitしてみよう!

犬と猫の分類

訓練データに含まれないテスト画像をいくつか入力して本当に分類できているか確認しておいた。train.zipの画像から適当に選んだけど各クラス最初の1000枚しか学習に使っていないので以下の画像は訓練外データである。出力が0.5未満だと猫、0.5以上だと犬と判定すればよい。

スクリプト: predict.py

f:id:aidiary:20170110215210j:plain:w300

input: train/cat.6341.jpg
[ 0.00011273] => 猫!

f:id:aidiary:20170110215214j:plain:w300

input: train/cat.6342.jpg
[ 0.01214203] => 猫!

f:id:aidiary:20170110215220j:plain:w300

input: train/dog.3877.jpg
[ 1.] => 犬!

f:id:aidiary:20170110215234j:plain:w300

input: train/dog.4143.jpg
[ 0.99994457] => 犬!

うちの愛犬を忘れるところだった(笑)

f:id:aidiary:20170104210733j:plain:w300

input: IMG_1550.jpg
[ 0.99496752] => 犬!

f:id:aidiary:20170104210738j:plain:w300

input: IMG_0041.JPG
[ 0.98027724] => 犬!

犬だってよ。よかったね。コロちゃん、くうちゃん(^^)v

実際は、Fine-tuningも限界がある。実は、VGG16を学習したImageNetに犬や猫の画像をたくさんあり、犬猫を分類するのに適した特徴量が抽出できていたため今回の犬猫分類がうまくいったのだ。つまり、ImageNetの画像とまったく異なる種類の画像ではうまく特徴抽出ができずFine-tuningもうまくいかなかったと予測される。

次回はVGG16のFine-tuningの別の例として花の品種の分類を試してみたい。

参考

VGG16のFine-tuningによる犬猫認識 (1)

Deep Learning Keras コンピュータビジョン

VGG16はILSVRCのコンペ用に学習されたニューラルネットなのでImageNetの1000クラスを認識できる。しかし、前の記事(2017/1/4)で実験したように「ひまわり」のようなImageNetに存在しないクラスはそのままでは認識できない。

この問題を解決するためVGG16の高い認識能力を継承しつつ、新しい独自のクラス(今回は犬か猫かの2クラス)を認識できるように少量のデータでニューラルネットの重みを再調整することをFine-tuningという*1。「少量のデータで」というところがすごく重要。もし大量データでないとダメだったらAWSの利用料で破産するのでこの記事は書けない(^^;;

今回は、Keras Blogの - Building powerful image classification models using very little dat を参考に犬と猫の2クラス認識を例としてVGGのFine-tuningについて実験した。このKeras Blogの記事はKeras 1.2.0の公開より前に書かれており、keras.applications.vgg16が使われていない。この記事ではKeras 1.2.0に合わせて一部を書き直した。

リポジトリ:dogs_vs_cats

セットアップ

タスクは犬と猫の分類。犬か猫の画像をニューラルネットに入力して犬または猫を出力する2クラス分類のタスク。ILSVRCの1000クラス分類に比べたらずっと簡単だけどVGG16は1000クラス分類のニューラルネットなので犬と猫の2クラス分類はそのままではできない。

f:id:aidiary:20170107203445j:plain
Dogs vs. Cats Redux: Kernels Edition | Kaggle

このデータセットはKaggleのDogs vs. Catsで提供されているためダウンロードするにはKaggleに登録する必要がある*2

訓練データのtrain.zipを解凍すると犬の画像が12500枚、猫の画像が12500枚含まれている。解凍してしばらく癒されてた(笑)

今回は、小規模データを使ったFine-tuningの実験なのでここから犬1000枚、猫1000枚を訓練データ、犬400枚、猫400枚をバリデーションデータとして用いた。テストデータのtest.zipは正解ラベルが付いていなかったので今回は使わない。正解ラベルがついてたらコンテストにならないからだと思われる。

train.zipを解凍するとtrainディレクトリができる。その後にsetup.pyを実行すると下のようにデータを振り分けられる。

data
├── train
│   ├── cats   cat0001.jpg - cat1000.jpg
│   └── dogs   dog0001.jpg - dog1000.jpg
└── validation
    ├── cats   cat0001.jpg - cat0400.jpg
    └── dogs   dog0001.jpg - dog0400.jpg

分類クラスごとにサブディレクトリ(ここではcatsdogs)を作るのが重要。KerasのImageDataGeneratorが自動的にcats/dogsをクラス名と認識してくれるのだ。

Kerasで画像ファイルを直接ロード

これまでMNIST(2016/11/9)やCIFAR-10(2016/11/27)のようにPythonのNumPy array形式のデータを直接ダウンロードできる例ばかり扱ってきた。今回は、NumPy arrayのデータは提供されておらず、画像ファイルしか提供されていない。このようなケースでは、先にデータ拡張(2016/12/12)で使ったImageDataGeneratorが非常に役立つ。このクラスにはディレクトリを指定するとそこの画像ファイルを読み込む関数が提供されている。

下のような感じで使う。

    # 訓練データとバリデーションデータを生成するジェネレータを作成
    train_datagen = ImageDataGenerator(
        rescale=1.0 / 255,
        shear_range=0.2,
        zoom_range=0.2,
        horizontal_flip=True)

    test_datagen = ImageDataGenerator(rescale=1.0 / 255)

    train_generator = train_datagen.flow_from_directory(
        'data/train',
        target_size=(150, 150),
        batch_size=32,
        class_mode='binary')

    validation_generator = test_datagen.flow_from_directory(
        'data/validation',
        target_size=(150, 150),
        batch_size=32,
        class_mode='binary')
  • ImageDataGeneratorでどのような画像拡張をするかオプションで指定できる。
  • flow_from_directory()に画像が含まれるディレクトリ名を指定してジェネレータを作成。このディレクトリに含まれる画像が「種画像」になる。
  • 指定したディレクトリの下にクラスごとにディレクトリ(dogs/cats)を作っておくとクラスも認識する。
  • 画像ファイルのサイズが異なっていてもtarget_sizeで指定した大きさにリサイズする。
  • 今回はcats/dogsの2クラス分類なのでclass_modeにはbinaryを指定する。あとで別の例を紹介するが多クラス分類の場合はcategoricalを指定する。

あとはこのジェネレータが4Dテンソル形式(samples, rows, cols, channels)に変換した画像データをじゃんじゃん生成してくれるのだ!実際には自分で生成する必要さえなく、下のように訓練用の関数fit_generator()にジェネレータを渡すだけでよい。これは楽できていいね。画像の読み込みとか地味に面倒だから。

    # 訓練
    history = model.fit_generator(
        train_generator,
        samples_per_epoch=2000,
        nb_epoch=nb_epoch,
        validation_data=validation_generator,
        nb_val_samples=800)

三種類の方法を比較

前置きが長かったけどここからが本題。次の3つのニューラルネットを学習して精度を比較してみたい。

  1. 小さな畳み込みニューラルネットをスクラッチから学習する
  2. VGG16が抽出した特徴を使って多層パーセプトロンを学習する
  3. VGG16をFine-tuningする

ちょっと長くなりそうなので次回へ。

参考

*1:Fine-tuningは転移学習(transfer learning)とも呼ばれる。音声分野だと似たような技術は適応(adaptation)と呼ぶけど関連あるのかしら?今回の話は平均声と話者適応の関係に似ていると思った。

*2:このタスクでKaggleに興味が出てきたので初めて登録した

KerasでVGG16を使う

Deep Learning Keras コンピュータビジョン

今回は、Deep Learningの画像応用において代表的なモデルであるVGG16をKerasから使ってみた。この学習済みのVGG16モデルは画像に関するいろいろな面白い実験をする際の基礎になるためKerasで取り扱う方法をちゃんと理解しておきたい。

ソースコード: test_vgg16

VGG16の概要

VGG16*1は2014年のILSVRC(ImageNet Large Scale Visual Recognition Challenge)で提案された畳み込み13層とフル結合3層の計16層から成る畳み込みニューラルネットワーク。層の数が多いだけで一般的な畳み込みニューラルネットと大きな違いはなく、同時期に提案されたGoogLeNetに比べるとシンプルでわかりやすい。ImageNetと呼ばれる大規模な画像データセットを使って訓練したモデルが公開されている。

VGG16の出力層は1000ユニットあり、1000クラスを分類するニューラルネットである。1000クラスのリストは1000 synsets for Task 2にある。あとでこの1000クラスの画像をクローリングする方法もまとめたい。

KerasのVGG16モデル

KerasではVGG16モデルがkeras.applications.vgg16モジュールに実装されているため簡単に使える。これはImageNetの大規模画像セットで学習済みのモデルなので自分で画像を集めて学習する必要がない

(注)少し古いバージョンのKerasだと自分でモデル構造を書いて、.h5ファイル形式の重みをダウンロードする必要があった(参考: VGG16 model for Keras)が最新の1.2.0では不要になっている。バックエンドに合わせて変換された重みファイルを自動ダウンロードしてくれる。keras.applications.vgg16が実装される前に書かれた記事も多いので要注意。

from keras.applications.vgg16 import VGG16
model = VGG16(include_top=True, weights='imagenet', input_tensor=None, input_shape=None)

VGG16クラスは4つの引数を取る。

  • include_topはVGG16のトップにある1000クラス分類するフル結合層(FC)を含むか含まないかを指定する。今回は画像分類を行いたいためFCを含んだ状態で使う。FCを捨ててVGG16を特徴抽出器として使うことでいろいろ面白いことができるがまた今度取り上げたい。
  • weightsはVGG16の重みの種類を指定する。VGG16は単にモデル構造であるため必ずしもImageNetを使って学習しなければいけないわけではない。しかし、現状ではImageNetで学習した重みしか提供されていない。Noneにするとランダム重みになる。自分で集めた画像で学習する猛者はこちらか?
  • input_tensorは自分でモデルに画像を入力したいときに使うが今回は未使用。あとでVGG16のFine-tuningをする際に使う。
  • input_shapeは入力画像の形状を指定する。include_top=Trueにして画像分類器として使う場合は (224, 224, 3) で固定なのでNoneでOK。何か中途半端な解像度だけどこれがImageNetの標準サイズのようだ。

読み込んだモデルをちょっと調べてみよう。

% print(model)
<keras.engine.training.Model at 0x2b05220aa978>

どうやらVGG16はKerasで一般的なSequentialモデルではなく、別のクラスのようだ。dir(model)をするとわかるが、Sequentialモデルで層を積み重ねるのによく使っていたadd()がないので注意。たとえば、VGG16に新たに層を付け加えるときにちょっとした工夫がいる。これもあとで詳しく取り上げたい。

summary()するとモデル構造が見られる。

% model.summary()
____________________________________________________________________________________________________
Layer (type)                     Output Shape          Param #     Connected to
====================================================================================================
input_26 (InputLayer)            (None, 224, 224, 3)   0
____________________________________________________________________________________________________
block1_conv1 (Convolution2D)     (None, 224, 224, 64)  1792        input_26[0][0]
____________________________________________________________________________________________________
block1_conv2 (Convolution2D)     (None, 224, 224, 64)  36928       block1_conv1[0][0]
____________________________________________________________________________________________________
block1_pool (MaxPooling2D)       (None, 112, 112, 64)  0           block1_conv2[0][0]
____________________________________________________________________________________________________
block2_conv1 (Convolution2D)     (None, 112, 112, 128) 73856       block1_pool[0][0]
____________________________________________________________________________________________________
block2_conv2 (Convolution2D)     (None, 112, 112, 128) 147584      block2_conv1[0][0]
____________________________________________________________________________________________________
block2_pool (MaxPooling2D)       (None, 56, 56, 128)   0           block2_conv2[0][0]
____________________________________________________________________________________________________
block3_conv1 (Convolution2D)     (None, 56, 56, 256)   295168      block2_pool[0][0]
____________________________________________________________________________________________________
block3_conv2 (Convolution2D)     (None, 56, 56, 256)   590080      block3_conv1[0][0]
____________________________________________________________________________________________________
block3_conv3 (Convolution2D)     (None, 56, 56, 256)   590080      block3_conv2[0][0]
____________________________________________________________________________________________________
block3_pool (MaxPooling2D)       (None, 28, 28, 256)   0           block3_conv3[0][0]
____________________________________________________________________________________________________
block4_conv1 (Convolution2D)     (None, 28, 28, 512)   1180160     block3_pool[0][0]
____________________________________________________________________________________________________
block4_conv2 (Convolution2D)     (None, 28, 28, 512)   2359808     block4_conv1[0][0]
____________________________________________________________________________________________________
block4_conv3 (Convolution2D)     (None, 28, 28, 512)   2359808     block4_conv2[0][0]
____________________________________________________________________________________________________
block4_pool (MaxPooling2D)       (None, 14, 14, 512)   0           block4_conv3[0][0]
____________________________________________________________________________________________________
block5_conv1 (Convolution2D)     (None, 14, 14, 512)   2359808     block4_pool[0][0]
____________________________________________________________________________________________________
block5_conv2 (Convolution2D)     (None, 14, 14, 512)   2359808     block5_conv1[0][0]
____________________________________________________________________________________________________
block5_conv3 (Convolution2D)     (None, 14, 14, 512)   2359808     block5_conv2[0][0]
____________________________________________________________________________________________________
block5_pool (MaxPooling2D)       (None, 7, 7, 512)     0           block5_conv3[0][0]
____________________________________________________________________________________________________
flatten (Flatten)                (None, 25088)         0           block5_pool[0][0]
____________________________________________________________________________________________________
fc1 (Dense)                      (None, 4096)          102764544   flatten[0][0]
____________________________________________________________________________________________________
fc2 (Dense)                      (None, 4096)          16781312    fc1[0][0]
____________________________________________________________________________________________________
predictions (Dense)              (None, 1000)          4097000     fc2[0][0]
====================================================================================================
Total params: 138,357,544
Trainable params: 138,357,544
Non-trainable params: 0
____________________________________________________________________________________________________

重み(#Param)がある層を数えていくと全部で16個あることがわかる。今回は、include_top=Trueなのでfc1fc2predictionsという層が追加されているのが確認できる。また、最後のpredictions層の形状が (None, 1000) で1000クラスの分類であることもわかる。Noneはサイズが決まっていないことを意味し、ここでは入力サンプル数(入力バッチ数)を意味する。

VGG16で一般物体認識

VGG16モデルが読み込めたのでさっそく画像を入力して分類するプログラムを書いてみよう。今回はコマンドラインから分類したい画像ファイル名を引数として入力するようにした。実際は、VGG16のロードに時間がかかるので起動後にプロンプトでファイル名を入力できるようにした方がよさそう。

from keras.applications.vgg16 import VGG16, preprocess_input, decode_predictions
from keras.preprocessing import image
import numpy as np
import sys

"""
ImageNetで学習済みのVGG16モデルを使って入力画像のクラスを予測する
"""

if len(sys.argv) != 2:
    print("usage: python test_vgg16.py [image file]")
    sys.exit(1)

filename = sys.argv[1]

# 学習済みのVGG16をロード
# 構造とともに学習済みの重みも読み込まれる
model = VGG16(weights='imagenet')
# model.summary()

# 引数で指定した画像ファイルを読み込む
# サイズはVGG16のデフォルトである224x224にリサイズされる
img = image.load_img(filename, target_size=(224, 224))

# 読み込んだPIL形式の画像をarrayに変換
x = image.img_to_array(img)

# 3次元テンソル(rows, cols, channels) を
# 4次元テンソル (samples, rows, cols, channels) に変換
# 入力画像は1枚なのでsamples=1でよい
x = np.expand_dims(x, axis=0)

# Top-5のクラスを予測する
# VGG16の1000クラスはdecode_predictions()で文字列に変換される
preds = model.predict(preprocess_input(x))
results = decode_predictions(preds, top=5)[0]
for result in results:
    print(result)

今回は、複数の画像をまとめて入力せずに1枚だけ入力するようにした(実際はバッチ単位で1000枚入力してまとめて予測も可)。

画像の入力はkeras.preprocessing.imageモジュールを使うといろいろ便利。load_img()で指定したサイズにリサイズして画像がロードできる。また、img_to_array()でPIL形式の画像をNumPy array形式に変換できる。

load_img()でロードした画像は (rows, cols, channels) の3Dテンソルなのでこれにサンプル数 samples を追加した4Dテンソルに変換する必要がある。

クラスの予測はpredict()で行う。VGG16用の平均を引く前処理 preprocess_input() を通した4Dテンソルを入力とする。predict()の戻り値はNNの出力であり1000クラスの確率値である。このままではどのクラスが何なのか非常にわかりづらい。VGG16用のdecode_predictions()を使うと確率値が高い順にクラス名を出力してくれる。

いくつかImageNetからクローリングした適当な画像を入力して認識結果を見てみよう。実際のところVGG16の訓練データがどれかわからない。なのでImageNetから適当に拾ってきた下の画像がたまたま訓練内データに含まれていた可能性もあるので注意。

f:id:aidiary:20170104210653j:plain:w300

('n02328150', 'Angora', 0.98844689)★
('n02326432', 'hare', 0.0081565334)
('n02325366', 'wood_rabbit', 0.0029539457)
('n02342885', 'hamster', 0.00032567442)
('n02364673', 'guinea_pig', 7.7807999e-05)

結果は認識結果のTop-5を出力している。各結果は (WordNet ID, クラス名, 確率) の3つ組からなる。確率が高い順にTop-5が出力されているのがわかると思う。この結果は、写真がAngora(アンゴラ)である確率が98.8%という意味。実際、入力画像はアンゴラ(ウサギの一種)なので正解!これはすごい。正解のクラスに★を付けておく。

f:id:aidiary:20170104210228g:plain:w300

('n02009912', 'American_egret', 0.9786104)★
('n02012849', 'crane', 0.020076046)
('n02009229', 'little_blue_heron', 0.0011492056)
('n02007558', 'flamingo', 6.9171525e-05)
('n02006656', 'spoonbill', 4.3949272e-05)

あっている。

f:id:aidiary:20170104210658j:plain:w300

('n04147183', 'schooner', 0.99256611)★
('n04612504', 'yawl', 0.006880702)
('n03947888', 'pirate', 0.00047176969)
('n02981792', 'catamaran', 2.9590283e-05)
('n04483307', 'trimaran', 1.5395544e-05)

正解!他の4個も船に関連したクラスが出てくるのがすごい。

f:id:aidiary:20170104210702j:plain:w300

('n04208210', 'shovel', 0.98540133)★
('n04367480', 'swab', 0.0067446162)
('n02906734', 'broom', 0.0027940609)
('n03498962', 'hatchet', 0.0025928672)
('n03481172', 'hammer', 0.0012616612)

正解。

f:id:aidiary:20170104210705j:plain:w300

('n03028079', 'church', 0.76123959)
('n02699494', 'altar', 0.21932022)★
('n04523525', 'vault', 0.010518107)
('n03854065', 'organ', 0.004585206)
('n03781244', 'monastery', 0.00092995056)

これはおしい。正解はaltarなのだがchurchでもいいだろう。

f:id:aidiary:20170104211944j:plain:w300

('n02109961', 'Eskimo_dog', 0.58899087)
('n02110185', 'Siberian_husky', 0.37142584)★
('n02110063', 'malamute', 0.032857068)
('n03218198', 'dogsled', 0.0035322146)
('n02105412', 'kelpie', 0.00089457975)

これもおしい。犬の種類を間違えているけどシベリアンハスキーは2つ目に出てくる。

次にImageNetにないデータを使ってみよう。つまり、正解のクラスがわからない。

f:id:aidiary:20170104210223j:plain:w300

('n02504013', 'Indian_elephant', 0.63950682)
('n02504458', 'African_elephant', 0.31736749)
('n01871265', 'tusker', 0.034719132)
('n02437312', 'Arabian_camel', 0.004969846)
('n02410509', 'bison', 0.00089000992)

実際の種類はよくわからないけど1位、2位ともに象の種類なのであっていそう。

f:id:aidiary:20170104210714j:plain:w300

('n03085013', 'computer_keyboard', 0.78958303)
('n04264628', 'space_bar', 0.13960978)
('n04505470', 'typewriter_keyboard', 0.050729375)
('n03793489', 'mouse', 0.0087937126)
('n04074963', 'remote_control', 0.0026325041)

この前買ったHHKの写真だけどキーボードなので正解。

f:id:aidiary:20170104210733j:plain:w300

('n02090622', 'borzoi', 0.21255279)
('n02111889', 'Samoyed', 0.16779339)
('n02109961', 'Eskimo_dog', 0.04057616)
('n02104029', 'kuvasz', 0.035596009)
('n03026506', 'Christmas_stocking', 0.031400401)

うちのコロちゃんの写真。ボルゾイ、サモエド、エスキモー犬、クーバースと犬の名前を出してきたので「犬」ということは認識できているようだ。雑種なんだけどさ(笑)

f:id:aidiary:20170104210738j:plain:w300

('n02134084', 'ice_bear', 0.35386214)
('n02114548', 'white_wolf', 0.20906797)
('n02104029', 'kuvasz', 0.13921976)
('n02437616', 'llama', 0.08272367)
('n02109961', 'Eskimo_dog', 0.048978001)

うちのくうちゃんの写真。1位はシロクマ・・・一体なぜと思ったけどよくみたらまあわからなくもない(笑)。2位は白狼で3位はクーバースなのでなかなかよい線。

まあこんな感じでなかなかうまくいっているようにみえるが実は限界も多い。VGG16を学習した際に選ばれた1000クラス以外はどうやっても認識できないのだ。たとえば、認識がすごく簡単そうなひまわりの画像を入れてみよう。

f:id:aidiary:20170104212004j:plain:w300

('n11939491', 'daisy', 0.97069114)
('n02206856', 'bee', 0.011910294)
('n01944390', 'snail', 0.0033144613)
('n02219486', 'ant', 0.0021784289)
('n02281406', 'sulphur_butterfly', 0.0020090577)

VGG16の1000クラスにはsunflowerがないのでどうやっても認識できない。まあdaisyと一番近そうな花の名前を出してきたのはすごいけどね。

実はVGG16を利用してImageNetの1000クラスに含まれていない画像もちゃんと認識するFine-tuningという技術がある。次回取り上げたい。

参考

*1:VGGが何の略か結局わからない・・・Visual Geometry Groupという研究グループ名だとTwitterで教えていただきました。ありがとうございます!