複数画像をKerasのVGG16で特徴抽出してk-means++でクラスタリング

VGG16, VGG19, ResNet50, InceptionV3など、 ImageNetで学習済みのモデルがKerasで使える。 物体認識だけでなく特徴抽出にも使えるので、 複数画像をVGG16で特徴抽出して、これをk-means++でクラスタリングしてみた。 なお複数画像は、ハワイで撮影したフラダンスの動画をフレーム分割して用意した。 以下に説明するコードは、ここ に置いておく。

ディレクトリー構成

ディレクトリー構成は以下の通り。 src/image_clustering.py が実行ファイルとなる。 data/video/クラスタリングしたい動画を置く。 data/images/targrt/ に動画をフレーム分割した画像が保存される。 data/images/clustered/ に分類済みの画像が保存される。 なお、動画でなくても、クラスタリングしたい画像を data/images/targrt/ に置くこともできる。 なお、ここ には src/image_clustering.py しかないので、 data/video/data/images/ フォルダーを追加して使ってください。

image_clustering
├─data
│  ├─images
│  │  ├─clustered
│  │  └─target
│  └─video
└─src
    └─image_clustering.py

初期設定

グローバル変数は固定。 __init__()の引数に、動画の場合は video_file にファイル名を指定する。 画像の場合は、input_video をFalseに変更する。

DATA_DIR = '../data/'
VIDEOS_DIR = '../data/video/'                        # The place to put the video
TARGET_IMAGES_DIR = '../data/images/target/'         # The place to put the images which you want to execute clustering
CLUSTERED_IMAGES_DIR = '../data/images/clustered/'   # The place to put the images which are clustered
IMAGE_LABEL_FILE ='image_label.csv'                  # Image name and its label

class Image_Clustering:
    def __init__(self, n_clusters=50, video_file='IMG_2140.MOV', image_file_temp='img_%s.png', input_video=True):
        self.n_clusters = n_clusters            # The number of cluster
        self.video_file = video_file            # Input video file name
        self.image_file_temp = image_file_temp  # Image file name template
        self.input_video = input_video          # If input data is a video

メイン関数

以下の順で処理を行う。1.は動画の場合のみ。

  1. 動画のフレーム分割
  2. 画像のラベリング(特徴抽出とクラスタリング
  3. 画像の分類(ラベルごとにディレクトリーにまとめる)
def main(self):
    if self.input_video == True:
        self.video_2_frames()
    self.label_images()
    self.classify_images()

動画のフレーム分割

OpenCVでフレーム分割して、data/images/targrt/ に保存する。

def video_2_frames(self):
    print('Video to frames...')
    cap = cv2.VideoCapture(VIDEOS_DIR+self.video_file)

    # Remove and make a directory.
    if os.path.exists(TARGET_IMAGES_DIR):
        shutil.rmtree(TARGET_IMAGES_DIR)  # Delete an entire directory tree
    if not os.path.exists(TARGET_IMAGES_DIR):
        os.makedirs(TARGET_IMAGES_DIR)  # Make a directory

    i = 0
    while(cap.isOpened()):
        flag, frame = cap.read()  # Capture frame-by-frame
        if flag == False:
            break  # A frame is not left
        cv2.imwrite(TARGET_IMAGES_DIR+self.image_file_temp % str(i).zfill(6), frame)  # Save a frame
        i += 1
        print('Save', TARGET_IMAGES_DIR+self.image_file_temp % str(i).zfill(6))

    cap.release()  # When everything done, release the capture
    print('')

実行すると、次々と画像が保存されていく。 フラダンスの動画は17秒だが、フレーム数は530枚となっている。

> python .\image_clustering.py
Video to frames...
Save ../data/images/target/img_000001.png
Save ../data/images/target/img_000002.png
Save ../data/images/target/img_000003.png
...
Save ../data/images/target/img_000530.png

画像のラベリング(特徴抽出とクラスタリング

モデルにはVGG16を使用。 include_top=Falseとすることで、特徴抽出が可能になる。 動画を分割したフレームはpngで保存しているが、画像はjpgも対象。 jpegとかも対象にしたい場合は、この辺を書き換えてください。 後述する__feature_extraction()にVGG16のモデルと画像を渡して特徴を取得する。 全画像の特徴をk-means++でクラスタリングを行い、ラベルを取得して画像とセットにして、pandasでCSVに保存する。

def label_images(self):
    print('Label images...')

    # Load a model
    model = VGG16(weights='imagenet', include_top=False)

    # Get images
    images = [f for f in os.listdir(TARGET_IMAGES_DIR) if f[-4:] in ['.png', '.jpg']]
    assert(len(images)>0)

    X = []
    pb = ProgressBar(max_value=len(images))
    for i in range(len(images)):
        # Extract image features
        feat = self.__feature_extraction(model, TARGET_IMAGES_DIR+images[i])
        X.append(feat)
        pb.update(i)  # Update progressbar

    # Clutering images by k-means++
    X = np.array(X)
    kmeans = KMeans(n_clusters=self.n_clusters, random_state=0).fit(X)
    print('')
    print('labels:')
    print(kmeans.labels_)
    print('')

    # Merge images and labels
    df = pd.DataFrame({'image': images, 'label': kmeans.labels_})
    df.to_csv(DATA_DIR+IMAGE_LABEL_FILE, index=False)

__feature_extraction() の中身は、 Applications - Keras Documentation に書かれてある通り。 リサイズして、4次元データにして、zero-centeringしてから特徴を取得。 ただし、特徴は (1, 7, 7, 512) の4次元で返ってくるので、 flatten()(25088,) の1次元に変換する。

def __feature_extraction(self, model, img_path):
    img = image.load_img(img_path, target_size=(224, 224))  # resize
    x = image.img_to_array(img)
    x = np.expand_dims(x, axis=0)  # add a dimention of samples
    x = preprocess_input(x)  # RGB 2 BGR and zero-centering by mean pixel based on the position of channels

    feat = model.predict(x)  # Get image features
    feat = feat.flatten()  # Convert 3-dimentional matrix to (1, n) array

    return feat

実行した結果、画像530枚でも、VGG16による特徴抽出は24秒で終わった。なお、GPUGeForce 1080 x1。 k-means++で50クラスタに分類しているが、だいたい連続するフレームが同じラベルになっていることが分かる。 しかし、ラベル7などフレームが飛んでいても同じラベルになっているものもある。

Label images...
 99% (527 of 530) |################################## | Elapsed Time: 0:00:24 ETA: 0:00:00
labels:
[28 28 28 28 28 28 28 28 28  3  3  3  3  3  3  3  3  3  3  3  3  3  3  3 16
 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 26 26 26 26 26 26 26 26 26
 26 26 26 26 26 26 26 26 26 11 11 11 11 11 11 11 11 11 32 32 32 32 32 32 32
 32 32 32 32 27 27 27 27  9  9  9  9  9  9  9  9  9  9  9  9  9  9  9  9  9
  9  9  9  9  9 37 37 37 37 37 37 37 37 37 37 37 37 37 37 21 21 21 21 21 21
 21 21 21 21 21 21 21 21 21 21 21 29 29 29 29 29 29 29 29 29 29 29 29 29  1
  1  1  1  1 42 42 42 42 42 42 45 45 45 45 45 45 45 45 45 45 45  4  4  4  4
  4  4  4  4  4  4  4  4  4  4  4  4  4 33 33 33 33 33 33 33 33 33 33 33 33
 33 33 33 35 35 35 35 35 35 35 35 35 35 35 35 19 19 19 19 19 19 41 41 41 41
 41 41 41 49 49 49 49 49 49 49 13 13 13 13 13 13 44 44 44 44 44 44 44 44 44
 44 44  5  5 17 17 17 17 17 17 17 17 17 17 13 13  5  5 34 34 34 34 34 34 34
 34 34 34  5  5  5  5  5  5 25 25 25 25 25 25 25 25 25 15 15 15 15 15 15 31
 31 31 31 31 48 48 48 48 48 48 40 40 40 40 40 40 40 20 20 20 20 20 20 20 39
 39 39 39 39 39 39 39  2  2  2  2  2  2  2  2  2  2  2  2  2  2 18 18 18 18
 18 18 18 18 18 18 18 18 18 46 46 46 46 46 46 46 46 46 47 47 47 47 47 47 47
 47 47 22 22 22 22 22 22  6  6  6  6  6  6  6  6  6  6 43 43 43 43 43 43 43
 43 43 43 30 30 30 30 30 30 30 30 30 30  7  7  7  7  7  7  7  7  7  7  7  7
  7  7  7 38 38 38 38 38 38 38 38 38 38 38 38 38 38 38  7  7  7 10 10 10 10
 10 10 10 10 10 24 24 24 24 24 24 24 24 24 14 14  0  0  0  0  0  0  0  0 14
 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 36 36 36 36 36 36 36
 36 36 36 36 36 12 12 12 12 12 12 12 12  8  8  8  8  8  8  8  8 23 23 23 23
 23 23 23 23 23]

CSVに保存したデータは以下の通り。

image,label
img_000000.png,28
img_000001.png,28
img_000002.png,28
...
img_000009.png,3
img_000010.png,3
img_000011.png,3
...
img_000024.png,16
img_000025.png,16
img_000026.png,16
...

画像の分類(ラベルごとにディレクトリーにまとめる)

CSVファイルを読み込んで、ラベルの数だけ data/images/clustered/ にラベル名のディレクトリーを作成し、 data/images/target/ から画像をラベルごとにコピペする。

def classify_images(self):
    print('Classify images...')

    # Get labels and images
    df = pd.read_csv(DATA_DIR+IMAGE_LABEL_FILE)
    labels = list(set(df['label'].values))

    # Delete images which were clustered before
    if os.path.exists(CLUSTERED_IMAGES_DIR):
        shutil.rmtree(CLUSTERED_IMAGES_DIR)

    for label in labels:
        print('Copy and paste label %s images.' % label)

        # Make directories named each label
        new_dir = CLUSTERED_IMAGES_DIR + str(label) + '/'
        if not os.path.exists(new_dir):
            os.makedirs(new_dir)

        # Copy images to the directories
        clustered_images = df[df['label']==label]['image'].values
        for ci in clustered_images:
            src = TARGET_IMAGES_DIR + ci
            dst = CLUSTERED_IMAGES_DIR + str(label) + '/' + ci
            shutil.copyfile(src, dst)

以下が実行ログ。

Classify images...
Copy and paste label 0 images.
Copy and paste label 1 images.
Copy and paste label 2 images.
...
Copy and paste label 49 images.

分類結果

先ほどのラベル7の画像を見てみると、左手を頭より上に、右手を胸の高さに上げている画像がまとめられている。 画像のindexは427から443に富んでいるが、どちらも同じポーズをしている。

f:id:Shoto:20170722193736p:plain

またラベル45の画像を見てみると、両手を胸の高さに上げて、海の方を向いている画像がまとめられている。

f:id:Shoto:20170722193749p:plain

感想

今回、動画からフレーム分割した画像ということもあって、特徴がほとんど変わらない画像が対象となってはいるが、 上手くクラスタリングできている。今後はなるべく似ていない画像で試してみようと思う。 あとできれば、k-means++を使わずにCNNに閉じた方法があれば試してみたい。

参考文献

listやarrayからPandas DataFrameを作成

カラム名をkey、listやarrayをvalueにして辞書を作成して、DataFrameに突っ込むとできる。

import numpy as np
import pandas as pd

def make_df_from_list_and_array():
    col1 = [0,1,2,3,4]
    col2 = np.array([5,6,7,8,9])

    df = pd.DataFrame({'col_list':col1, 'col_array':col2})
    print(df)

コードはここ

>>> import test
>>> test.make_df_from_list_and_array()
   col_array  col_list
0          5         0
1          6         1
2          7         2
3          8         3
4          9         4

Pythonでディレクトリー内から特定の拡張子のファイルを取得

listdirを使ってディレクトリー内のファイルを取得し、リスト内包表記を使って条件付きでファイルを読み込む。 if文を変えれば拡張子に限定されない。 例えば、接頭語を指定するとか。

import os

def get_target_files(dir_path='./image_dir/'):
    files = [f for f in os.listdir(dir_path) if f[-4:]=='.png']

    return files

コードは一応、ここに置いておきます。

>>> import test
>>> test.get_target_files()
['img_000000.png', 'img_000001.png', 'img_000002.png', ...]

Python-OpenCVで動画をフレーム分割して画像として保存

動画をフレームに分割して画像として指定したディレクトリーに保存する操作をまとめた。 動画を読み込んで、フレームごとに存在を確認して、確認できれば画像として保存する、という手順。 引数は適宜変えてください。

import os
import shutil
import cv2

def video_2_frames(video_file='./IMG_2140.MOV', image_dir='./image_dir/', image_file='img_%s.png'):
    # Delete the entire directory tree if it exists.
    if os.path.exists(image_dir):
        shutil.rmtree(image_dir)  

    # Make the directory if it doesn't exist.
    if not os.path.exists(image_dir):
        os.makedirs(image_dir)

    # Video to frames
    i = 0
    cap = cv2.VideoCapture(video_file)
    while(cap.isOpened()):
        flag, frame = cap.read()  # Capture frame-by-frame
        if flag == False:  # Is a frame left?
            break
        cv2.imwrite(image_dir+image_file % str(i).zfill(6), frame)  # Save a frame
        print('Save', image_dir+image_file % str(i).zfill(6))
        i += 1

    cap.release()  # When everything done, release the capture

githubに置いたコードを 以下のように実行すると、指定したディレクトリーに動画をフレーム分割した画像が保存される。

>>> import test
>>> test.video_2_frames()
Save ./image_dir/img_000000.png
Save ./image_dir/img_000001.png
Save ./image_dir/img_000002.png
...

参考文献

Pythonでディレクトリーの削除と作成

処理中、一時的にディレクトリーを作ることが多いので、以下に操作をまとめた。 引数にディレクトリーパスを渡して、ある時は削除して、ない時は作るという仕様。

# coding: utf-8

import os
import shutil

def delete_and_make_directory(dir_path='./image_dir/'):
    # Delete the entire directory tree if it exists.
    if os.path.exists(dir_path):
        shutil.rmtree(dir_path)  
    
    # Make the directory if it doesn't exist.
    if not os.path.exists(dir_path):
        os.makedirs(dir_path)

githubに置いたコードを 以下のように実行すると、test.pyと同じ階層に新しいディレクトリ-が作成される。

>>> import test
>>> test.delete_and_make_directory()

参考文献

TensorFlowからKerasに乗り換えてみた

モデル設計などの際に、TensorFlowのコードが長くなるので自分でラッパーを書いていたのだが、 ざっとKerasを調べてみたら、ラッパーが必要ないくらいシンプルに書けるし、 前処理などモデル設計以外のツールも充実しているようだったので、 KerasでCIFAR10のモデルを訓練するコードを書いてみた。 なおKerasについては、KerasでCIFAR-10の一般物体認識 - 人工知能に関する断創録 が非常に分かりやすかった。そのため以下に示すコードは、こちらの記事をベースに色々とカスタマイズしている。 なおKeras v1.2.2, tensorflow-gpu v1.0.0, opencv-python v3.2.0を使用しており、 全コードはここに公開している。

パラメーター

IDG_PARAMはData Augmentationのパラメーター。 今回は、ZCA Whiteningと、ランダムで上下左右の移動と左右反転を行う。 これだけのData Augmentationを、独自に実装することなく、パラメーター操作だけで色々と試せるのがKerasの良いところ。 Kerasによるデータ拡張 - 人工知能に関する断創録 に各パラメーターを可視化した様子が掲載されている。
モデルなどの訓練結果はDIRに保存する。最後にFILEが付いてるパラメーターがそれらのファイル。

N_CLASS = 10
N_EPOCH = 50  # 100
BATCH_SIZE = 128
INPUT_DIM = (32, 32, 3)
DATA_AUGMENTATION = True
IDG_PARAM = {'featurewise_center': False,
             'samplewise_center': False,
             'featurewise_std_normalization': False,
             'samplewise_std_normalization': False,
             'zca_whitening': True,  # False
             'rotation_range': 0.,
             'width_shift_range': 0.1, # 0.,
             'height_shift_range': 0.1, # 0.,
             'shear_range': 0.,
             'zoom_range': 0.,
             'channel_shift_range': 0.,
             'fill_mode': 'nearest',
             'cval': 0.,
             'horizontal_flip': True,
             'vertical_flip': False,
             'rescale': None,
             'preprocessing_function': None
}

DIR = '../result/'
MODEL_FILE = 'model.json'
WEIGHT_FILE = 'weights.h5'
HISTORY_DATA_FILE = 'history.csv'
HISTORY_IMAGE_FILE = 'history.jpg'
PARAM_EVAL_FILE = 'param_eval.csv'

メイン

今回実装したのは、データ取得、モデル設計、モデル訓練、結果保存、モデル試験の5つ。 これらをメソッドに分けて、すべてTestクラスのmainメソッドから呼び出す。

class Test:
    def __init__(self):
        pass


    def main(self):
        # Training
        start = time.clock()

        data = self.get_data()  # データ取得
        model = self.design_model(data[0])  # モデル設計
        result = self.train_model(data, model)  # モデル訓練
        self.save(result)  # 結果保存

        print('Training Time: %s min' % round((time.clock()-start)/60., 1))
        print('')

        # Test
        self.test_model(data)  # モデル試験

データ取得

cifar10.load_data()を実行するだけで、CIFAR10を訓練/試験、データ/ラベルに分割して取得可能。 ただし、normalizeとonehot化は自分で行う必要があるが、後者もまたnp_utils.to_categorical()を実行するだけでOK。

def get_data(self):
    # Load CIFAR-10
    (X_train, y_train), (X_test, y_test) = cifar10.load_data()
    self.__draw_sample_images(X_train, y_train)

    # Normalize data
    X_train = X_train.astype('float32')
    X_test = X_test.astype('float32')
    X_train /= 255.0
    X_test /= 255.0

    # Onehot label
    Y_train = np_utils.to_categorical(y_train, N_CLASS)
    Y_test = np_utils.to_categorical(y_test, N_CLASS)

    print('X_train.shape:', X_train.shape, 'Y_train.shape:', Y_train.shape)
    print('X_test.shape:', X_test.shape, 'Y_test.shape:', Y_test.shape)

    return X_train, Y_train, X_test, Y_test

なお、self.__draw_sample_images(X_train, y_train)でCIFAR10の一部を可視化している。

f:id:Shoto:20170625191743j:plain

モデル設計

VGGっぽいモデルを設計。 Convolution2Dではborder_mode='same'にしてゼロパディング。 個人的にKerasの気に入っているところはActivation('relu')の部分。 活性化関数を文字列で指定できるところがいい。 TensorFlowでは活性化関数別にメソッドが用意されてたので、独自にラッパーを書いてた。 このモデルは、model.summay()により標準出力できる。

def design_model(self, X_train):
    # Initialize
    model = Sequential()

    # (Conv -> Relu) * 2 -> Pool -> Dropout
    model.add(Convolution2D(32, 3, 3, border_mode='same', input_shape=X_train.shape[1:]))
    model.add(Activation('relu'))
    model.add(Convolution2D(32, 3, 3, border_mode='same'))
    model.add(Activation('relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))

    # (Conv -> Relu) * 2 -> Pool -> Dropout
    model.add(Convolution2D(64, 3, 3, border_mode='same'))
    model.add(Activation('relu'))
    model.add(Convolution2D(64, 3, 3, border_mode='same'))
    model.add(Activation('relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))

    # Flatten
    model.add(Flatten())  # 6*6*64

    # FC -> Relu -> Dropout
    model.add(Dense(512))
    model.add(Activation('relu'))
    model.add(Dropout(0.5))

    # FC -> Softmax
    model.add(Dense(N_CLASS))
    model.add(Activation('softmax'))

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

    # Output model summary
    model.summary()

    return model

以下がモデルの標準出力の様子。

____________________________________________________________________________________________________
Layer (type)                     Output Shape          Param #     Connected to
====================================================================================================
convolution2d_1 (Convolution2D)  (None, 32, 32, 32)    896         convolution2d_input_1[0][0]
____________________________________________________________________________________________________
activation_1 (Activation)        (None, 32, 32, 32)    0           convolution2d_1[0][0]
____________________________________________________________________________________________________
convolution2d_2 (Convolution2D)  (None, 32, 32, 32)    9248        activation_1[0][0]
____________________________________________________________________________________________________
activation_2 (Activation)        (None, 32, 32, 32)    0           convolution2d_2[0][0]
____________________________________________________________________________________________________
maxpooling2d_1 (MaxPooling2D)    (None, 16, 16, 32)    0           activation_2[0][0]
____________________________________________________________________________________________________
dropout_1 (Dropout)              (None, 16, 16, 32)    0           maxpooling2d_1[0][0]
____________________________________________________________________________________________________
convolution2d_3 (Convolution2D)  (None, 16, 16, 64)    18496       dropout_1[0][0]
____________________________________________________________________________________________________
activation_3 (Activation)        (None, 16, 16, 64)    0           convolution2d_3[0][0]
____________________________________________________________________________________________________
convolution2d_4 (Convolution2D)  (None, 16, 16, 64)    36928       activation_3[0][0]
____________________________________________________________________________________________________
activation_4 (Activation)        (None, 16, 16, 64)    0           convolution2d_4[0][0]
____________________________________________________________________________________________________
maxpooling2d_2 (MaxPooling2D)    (None, 8, 8, 64)      0           activation_4[0][0]
____________________________________________________________________________________________________
dropout_2 (Dropout)              (None, 8, 8, 64)      0           maxpooling2d_2[0][0]
____________________________________________________________________________________________________
flatten_1 (Flatten)              (None, 4096)          0           dropout_2[0][0]
____________________________________________________________________________________________________
dense_1 (Dense)                  (None, 512)           2097664     flatten_1[0][0]
____________________________________________________________________________________________________
activation_5 (Activation)        (None, 512)           0           dense_1[0][0]
____________________________________________________________________________________________________
dropout_3 (Dropout)              (None, 512)           0           activation_5[0][0]
____________________________________________________________________________________________________
dense_2 (Dense)                  (None, 10)            5130        dropout_3[0][0]
____________________________________________________________________________________________________
activation_6 (Activation)        (None, 10)            0           dense_2[0][0]
====================================================================================================
Total params: 2,168,362
Trainable params: 2,168,362
Non-trainable params: 0
____________________________________________________________________________________________________

モデル訓練

Data augmentationを行うか否かで場合分け。 行う場合は、上記で設定したIDG_PARAMがセットされる。 ZCA Whiteningは訓練/試験共に実施される。

def train_model(self, data, model):
    X_train, Y_train, X_test, Y_test = data

    if not DATA_AUGMENTATION:
        print('Not using data augmentation')

        # Train the model
        history = model.fit(X_train, Y_train,
                            batch_size=BATCH_SIZE,
                            nb_epoch=N_EPOCH,
                            verbose=1,
                            validation_data=(X_test, Y_test),
                            shuffle=True)
    else:
        print('Using real-time data augmentation')

        # Make a generator for training data
        train_datagen = ImageDataGenerator(featurewise_center=IDG_PARAM['featurewise_center'],
                                           samplewise_center=IDG_PARAM['samplewise_center'],
                                           featurewise_std_normalization=IDG_PARAM['featurewise_std_normalization'],
                                           samplewise_std_normalization=IDG_PARAM['samplewise_std_normalization'],
                                           zca_whitening=IDG_PARAM['zca_whitening'],
                                           rotation_range=IDG_PARAM['rotation_range'],
                                           width_shift_range=IDG_PARAM['width_shift_range'],
                                           height_shift_range=IDG_PARAM['height_shift_range'],
                                           shear_range=IDG_PARAM['shear_range'],
                                           zoom_range=IDG_PARAM['zoom_range'],
                                           channel_shift_range=IDG_PARAM['channel_shift_range'],
                                           fill_mode=IDG_PARAM['fill_mode'],
                                           cval=IDG_PARAM['cval'],
                                           horizontal_flip=IDG_PARAM['horizontal_flip'],
                                           vertical_flip=IDG_PARAM['vertical_flip'],
                                           rescale=IDG_PARAM['rescale'],
                                           preprocessing_function=IDG_PARAM['preprocessing_function'])
        train_datagen.fit(X_train)
        train_generator = train_datagen.flow(X_train, Y_train, batch_size=BATCH_SIZE)

        # Make a generator for test data
        test_datagen = ImageDataGenerator(zca_whitening=IDG_PARAM['zca_whitening'])
        test_datagen.fit(X_test)
        test_generator = test_datagen.flow(X_test, Y_test)

        # Train the model
        history = model.fit_generator(train_generator,
                                      samples_per_epoch=X_train.shape[0],
                                      nb_epoch=N_EPOCH,
                                      validation_data=test_generator,
                                      nb_val_samples=X_test.shape[0])

        # Evaluate the model
        if not DATA_AUGMENTATION:
            loss, acc = model.evaluate(X_test, Y_test, verbose=0)
        else:
            loss, acc = model.evaluate_generator(test_generator, val_samples=X_test.shape[0])

        print('Test loss: %s, Test acc: %s' % (loss, acc))

    result = {'model': model, 'history': history, 'loss': loss, 'acc': acc}

    return result

なお訓練中は、以下のように学習状況を標準出力してくれる。 プログレスバーに加えて、訓練/検証別にエラー率と精度が出力されるので、 適切に学習できているかが分かる。

Using real-time data augmentation
Epoch 1/50
50000/50000 [==============================] - 106s - loss: 1.6846 - acc: 0.3863 - val_loss: 1.1716 - val_acc: 0.5766
Epoch 2/50
50000/50000 [==============================] - 108s - loss: 1.1772 - acc: 0.5828 - val_loss: 0.9773 - val_acc: 0.6528
Epoch 3/50
50000/50000 [==============================] - 107s - loss: 1.0237 - acc: 0.6394 - val_loss: 0.8878 - val_acc: 0.6886
...
Epoch 48/50
50000/50000 [==============================] - 107s - loss: 0.4872 - acc: 0.8319 - val_loss: 0.4522 - val_acc: 0.8453
Epoch 49/50
50000/50000 [==============================] - 105s - loss: 0.4835 - acc: 0.8315 - val_loss: 0.5183 - val_acc: 0.8318
Epoch 50/50
50000/50000 [==============================] - 104s - loss: 0.4835 - acc: 0.8322 - val_loss: 0.4671 - val_acc: 0.8403
Test loss: 0.456553607655, Test acc: 0.8476

結果保存

モデル、重み、訓練/検証別のエラー率と精度、Data augmentationなどパラメーターと試験結果を保存する。 最後のはパラメーターによって試験結果がどのように変化するかを調査するためのログとなる。

def save(self, result):
    """
  Save model, weight, history, parameter and evaluation
  """
    model = result['model']
    history = result['history']
    loss = result['loss']
    acc = result['acc']

    # Model
    model_json = model.to_json()

    # Weight
    with open(os.path.join(DIR, MODEL_FILE), 'w') as json_file:
        json_file.write(model_json)
    model.save_weights(os.path.join(DIR, WEIGHT_FILE))

    # History
    self.__save_history(history)
    self.__plot_history(history)

    # Param and evaluation
    dic = IDG_PARAM
    dic.update({'n_epoch': N_EPOCH, 'batch_size': BATCH_SIZE, 'loss': loss, 'acc': acc})
    if os.path.exists(DIR+PARAM_EVAL_FILE):
        df = pd.read_csv(DIR+PARAM_EVAL_FILE)
        df = pd.concat([df, pd.DataFrame([dic])])
    else:
        df = pd.DataFrame([dic])
    df.to_csv(DIR+PARAM_EVAL_FILE, index=False)

以下は保存したhistory.jpg

f:id:Shoto:20170625191810j:plain

モデル試験

上記で保存したモデルとその重みを読み込んで、テストデータにかける。

def test_model(self, data):
    X_train, Y_train, X_test, Y_test = data

    model_file = os.path.join(DIR, MODEL_FILE)
    weight_file = os.path.join(DIR, WEIGHT_FILE)

    with open(model_file, 'r') as fp:
        model = model_from_json(fp.read())
    model.load_weights(weight_file)
    model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])

    if not DATA_AUGMENTATION:
        loss, acc = model.evaluate(X_test, Y_test, verbose=0)
    else:
        # Make a generator for test data
        test_datagen = ImageDataGenerator(zca_whitening=True)
        test_datagen.fit(X_test)
        test_generator = test_datagen.flow(X_test, Y_test)

        loss, acc = model.evaluate_generator(test_generator, val_samples=X_test.shape[0])

    print('Test loss: %s, Test acc: %s' % (loss, acc))
    print('')

結果はモデル訓練の時とほぼ一致している。

Test loss: 0.460079106236, Test acc: 0.8441

最後に

Kerasのツールの便利さが身に染みた。 他のKerasで実装されたコードを読んでると、 最も精度が高かった重みを上書きしない設定など、 他にも色々な便利ツールがあるっぽいので、 必要な処理を自前で書く前にKerasから探すのもいいと思う。

参考文献

PythonでData Augmentation

Pythonで画像の左右反転、回転、拡大を行ってみた。 Data Augmentationに使えるかなと。

f:id:Shoto:20170602001453p:plain

左右反転

scikit-imageだけで実現したかったのだが、APIを見つけられなかったのでOpenCVで実装。 でも3つの処理の中で最も簡単に書けた。 ちなみに第2引数を1ではなく0にすると上下反転になる。

# flip
img_fliped = cv2.flip(img, 1)

回転

angleには角度(°)を指定。 resizeがFalseの場合は、そのまま回転。Trueにすると元画像の画像の角が隠れないようになる。 centerをNoneにすると、中央を原点にして回転。

# rotatate
img_rotated = skimage.transform.rotate(img, angle=10, resize=False, center=None)

拡大

目的の処理が行えるシンプルなAPIがなかったので、AffineTransform()を使用。 rateがプラスの倍率。0.2の場合は20%拡大。scaleには1-rateを指定する形となる。 拡大は左上を原点にして行われるので、元画像の中央が拡大後も中央になるようにtranslationで修正。 具体的には拡大した大きさの半分だけ左上に平行移動している。

# expand
rate = 0.2
size = img.shape[0]
matrix_expanded = skimage.transform.AffineTransform(scale=(1-rate, 1-rate), translation=(size*rate/2, size*rate/2))
img_expanded = skimage.transform.warp(img, matrix_expanded)

実行

記事のトップ画像を表示するためのコードは以下の通り。

# coding: utf-8

import numpy as np
import cv2
import matplotlib.pyplot as plt
import skimage
from skimage import io
from skimage import transform

DIR = '../data/'

class Data:
    def test(self):
        # read Lenna image
        img_raw = skimage.io.imread(DIR+'lenna.jpg')

        # resize
        #img = skimage.transform.resize(img_raw, (32, 32))
        img = img_raw

        # flip
        img_fliped = cv2.flip(img, 1)

        # rotatate
        img_rotated = skimage.transform.rotate(img, angle=10, resize=False, center=None)

        # expand
        rate = 0.2
        size = img.shape[0]
        matrix_expanded = skimage.transform.AffineTransform(scale=(1-rate, 1-rate), translation=(size*rate/2, size*rate/2))
        img_expanded = skimage.transform.warp(img, matrix_expanded)

        # white background
        fig = plt.figure()
        fig.patch.set_facecolor('white')

        # display images
        plt.subplot(141)
        plt.title('raw')
        plt.imshow(img)

        plt.subplot(142)
        plt.title('flip')
        plt.imshow(img_fliped)

        plt.subplot(143)
        plt.title('rotate')
        plt.imshow(img_rotated)

        plt.subplot(144)
        plt.title('expand')
        plt.imshow(img_expanded)

        plt.show()

参考文献