研究開発部の画像解析担当のレシェックです。techlife を書くのは初めてです。よろしくお願いいたします。
最先端の機械学習を使うためには、常に自分のスキルアップが必要です。そのために、毎日論文を読んだり、新しいオープンソースのコードを試してみたり、クックパッドのデータで実験しています。これはちょっと料理の練習と似ています。新しいモデルを学習させるのは料理をオーブンに入れるのと同じ気持ちです。オーブンの温度は学習率と同じで、低すぎだとよく焼けず、高すぎだと焦げてしまいます。しかし、ちゃんと他のリサーチャーの論文やブログの中のレシピを見ながら自分のデータでモデルを学習させると、失敗せずに済むかもしれません。
このエントリでは、そういった機械学習のレシピの一例を紹介します。
このブログで使っているテスト画像はPixabayから取得した、Creative Commonsのライセンスの写真です。
概要
クックパッドは料理/非料理のモデルを開発しています。ここでは、このモデルのミニチュア版のレシピを紹介します。カテゴリは「料理」と「非料理」の代わりに、「ホットドッグ」と「非ホットドッグ」にします。そして、パッチ化した画像に対する認識モデルを使って、画像の中でホットドッグがどこにあるかを検出します。
調理器具
- python
- Keras
- numpy
- pillow (PIL)
- jupyter notebook(お好みでお使い下さい。)
KerasはTensorflow、CNTKやTheano上で動く高水準のライブラリーです。Keras は特に画像データに対して、単なる学習以外にも前処理などでも様々な機能があります。
材料
KaggleからHot Dog - Not Hot Dogのデーターセットをダウンロードしてください。なお、ダウンロードするには Kaggle の登録が必要です。
ダウンロードした後、seefood.zip
をunzip
してください。
アーカイブの中に、2つのディレクトリtrain
とtest
があります。
seefood/train/not_hot_dog seefood/train/hot_dog seefood/test/not_hot_dog seefood/test/hot_dog
hot_dog
ディレクトリの中にホットドッグの画像が入っており、not_hot_dog
の中にそれ以外の画像が入っています。新しい機械学習のレシピを開発する時はテストデータを分けるべきです。しかし、今回は画像が少ないので、テストデータも学習に使いましょう。
mkdir seefood/all cp -r seefood/test/* seefood/train/* seefood/all
以降では、seefood/all
のディレクトリを使います。
データ拡張
Keras のモバイルネットは(224px・224px)のフィックスサイズの画像しか認識できないので、これから学習や認識用にサイズを変換します。
IMG_SIZE=[224, 224]
テストデータを学習に使っても、このデータセットはまだ小さいので、データ拡張を使いましょう。
KerasのImageDataGenerator
は学習時に画像を一つずつ変換します。
import keras.preprocessing.image image_generator = keras.preprocessing.image.ImageDataGenerator( rescale=1./255, shear_range=0.0, width_shift_range=0.1, height_shift_range=0.1, rotation_range=10, fill_mode="wrap", vertical_flip=True, horizontal_flip=True )
上のimage_generator
を"seefood/all"
のディレクトリで動かします。
train_generator = image_generator.flow_from_directory( "seefood/all", target_size=IMG_SIZE, batch_size=32, class_mode="categorical", classes=["not_hot_dog", "hot_dog"] )
モデルの作り方
以下のレシピでは、3 個のモデルを 3 層のスポンジケーキのように積み重ねています。
base_model
はMobileNetです。転移学習のために使います。- その上の
patch_model
は画像のパッチごとに分類できます。 - さらにその上の
classifier
は「ホットドッグ」と「非ホットドッグ」の二値分類器です。
まずkeras
をimport
します:
import keras
ベースとして、Googleで開発されたMobileNetというモデルを使います。
weights="imagenet"
は、ILSVRCのコンペティションのデータセットで学習されたパラメタを使って、転移学習することを意味しています。
base_model = keras.applications.mobilenet.MobileNet( input_shape=IMG_SIZE + [3], weights="imagenet", include_top=False )
ベースモデルの一番上のフィーチャサイズは1024です。パッチレイヤが学習できるようにちょっと下げましょう。
drop1 = keras.layers.SpatialDropout2D(0.3)(base_model.output) conv_filter = keras.layers.convolutional.Conv2D( 4, (1,1), activation="relu", use_bias=True, kernel_regularizer=keras.regularizers.l2(0.001) )(drop1)
パッチレイヤもConv2Dのタイプのレイヤです。この場合、softmax
を使えば、パッチごとに分類できるようになります。
drop2 = keras.layers.SpatialDropout2D(0.3)(conv_filter) patch = keras.layers.convolutional.Conv2D( 2, (3, 3), name="patch", activation="softmax", use_bias=True, padding="same", kernel_regularizer=keras.regularizers.l2(0.001) )(drop2)
これでパッチモデルができました。
patch_model = keras.models.Model(
inputs=base_model.input,
outputs=patch
)
パッチモデルをベースにして、最後の出力レイヤを追加して分類モデルを作ります。
pool = keras.layers.GlobalAveragePooling2D()(patch) logits = keras.layers.Activation("softmax")(pool) classifier = keras.models.Model( inputs=base_model.input, outputs=logits )
学習
ベースモデルは学習させません。
for layer in base_model.layers: layer.trainable = False
そして全体のモデルをcompile
します。
classifier.compile(optimizer="rmsprop", loss="categorical_crossentropy", metrics=["accuracy"])
では、学習を始めましょう!
いくつか実験をした結果、以下のようにnot_hot_dog
のクラスのclass_weight
を高くするほうが良いことが分かりました。
%%time classifier.fit_generator( train_generator, class_weight={0: .75, 1: .25}, epochs=10 )
Epoch 1/10 32/32 [==============================] - 148s 5s/step - loss: 0.3157 - acc: 0.5051 Epoch 2/10 32/32 [==============================] - 121s 4s/step - loss: 0.3017 - acc: 0.5051 Epoch 3/10 32/32 [==============================] - 122s 4s/step - loss: 0.2961 - acc: 0.5010 Epoch 4/10 32/32 [==============================] - 121s 4s/step - loss: 0.2791 - acc: 0.5862 Epoch 5/10 32/32 [==============================] - 122s 4s/step - loss: 0.2681 - acc: 0.6380 Epoch 6/10 32/32 [==============================] - 123s 4s/step - loss: 0.2615 - acc: 0.6876 Epoch 7/10 32/32 [==============================] - 121s 4s/step - loss: 0.2547 - acc: 0.6790 Epoch 8/10 32/32 [==============================] - 122s 4s/step - loss: 0.2522 - acc: 0.7052 Epoch 9/10 32/32 [==============================] - 123s 4s/step - loss: 0.2522 - acc: 0.7045 Epoch 10/10 32/32 [==============================] - 145s 5s/step - loss: 0.2486 - acc: 0.7164 CPU times: user 1h 4min 20s, sys: 2min 35s, total: 1h 6min 56s Wall time: 21min 8s
このデータセットの場合、10エポックぐらいが良さそうです。パッチベースを使っているので、精度は100%にならないほうがいいです。70%ぐらいがちょうどいいです。
私の MacBook Pro では10エポックで20分ぐらいかかりました。
確認作業
画像とデータの変換のために、PIL
とnumpy
を使います。
import numpy as np from PIL import Image
画像をインファレンスする前に、numpy
のデータに変換します。
def patch_infer(img): data = np.array(img.resize(IMG_SIZE))/255.0 patches = patch_model.predict(data[np.newaxis]) return patches
そして、元の画像とインファレンス結果をビジュアライズします。
def overlay(img, patches, threshold=0.99): # transposeはパッチをクラスごとに分けます。 patches = patches[0].transpose(2, 0, 1) # hot_dogパッチ - not_hot_dogパッチ patches = patches[1] - patches[0] # 微妙なパッチをなくして patches = np.clip(patches, threshold, 1.0) patches = 255.0 * (patches - threshold) / (1.0 - threshold) # 数字を画像にして patches = Image.fromarray(patches.astype(np.uint8)).resize(img.size, Image.BICUBIC) # もとの画像を白黒に grayscale = img.convert("L").convert("RGB").point(lambda p: p * 0.5) # パッチをマスクに使って、元の画像と白黒の画像をあわせて composite = Image.composite(img, grayscale, patches) return composite
まとめて、インファレンスとビジュアライズを一つのファンクションにすると、
def process_image(path, border=8): img = Image.open(path) patches = patch_infer(img) result = overlay(img, patches) # 元の画像と変換された画像をカンバスに並べます canvas = Image.new( mode="RGB", size=(img.width * 2 + border, img.height), color="white") canvas.paste(img, (0,0)) canvas.paste(result, (img.width + border, 0)) return canvas
では、結果を見てみましょう!
きれいですね!
ホットドッグの色はちょっと隣のコーヒーに移りましたが、ほとんど大丈夫です。
フォーカスが足りないところは認識にならなかったみたいです。なぜでしょう?学習データにフォーカスが当たらないホットドッグがなかったからです。
こちらも、左側のホットドッグはフォーカスが当たっておらず、モデルはホットドッグを認識できませんでした。
ホットドッグではない画像は?
ホットドッグではない画像には、パッチはゼロやゼロに近い値になります。
まとめ
転移学習を使えば、データが少なくても、それなりの識別器が作れますね!
パッチごとの分類を使えば、画像の中の認識したいフィーチャーを可視化できます。
モバイルネット(MobileNet)のおかげで、CPU でもモデルを学習できます。
いかがでしたでしょうか。 クックパッドでは、機械学習を用いて新たなサービスを創り出していける方を募集しています。 興味のある方はぜひ話を聞きに遊びに来て下さい。