やってみよう!

プログラミングとかでぃーぷらーにんぐとかVRとか気になったものをやってみる予定

VAEで画像分類をしてみたい1

前回の続き blog.ascreit.com

前回、画像のクラスらリング手法を検討してみた結果、

  1. VAEで特徴量を2次元に落とし込む
  2. 何らかの方法で線を引き、分類する

というやり方がいいのかなという結論に至ったので、今回はVAEを試してみようと思います。

VAEってなに?

ざっくりいうと、VAEはオートエンコーダの1種で、

オートエンコーダは入力データXを圧縮してデータを小さしたZ(今回だと2次元データに圧縮)を作ります。

次に圧縮データZから元のデータX'を復元します。

入力データXと出力データX'が同じであればうまく圧縮できているということなので、圧縮データZには意味のある特徴量が抽出されているよね。ということみたいです。

VAEはこのAEがつくるZを正規分布となることを仮定して平均と分散を出力させるということみたいです。

qiita.com

こちらの記事が見やすくまとめてありました。

とりあえずVAEを動かしたい

本当は論文を理解して実装するべきなのでしょうが、私はとにかく動かしてみないと頭に入らないタイプなので、とりあえずサンプルを動かしてみます。

github.com

ここからKerasのVAEをMNISTで動かすサンプルコードが手に入るのでこれを動かしてみます。

GeForce GTX 1080 Ti, pci bus id: 0000:01:00.0, compute capability: 6.1)
60000/60000 [==============================] - 2s 31us/step - loss: 196.7333 - val_loss: 171.9113
Epoch 2/50
60000/60000 [==============================] - 1s 20us/step - loss: 168.9939 - val_loss: 166.4852
Epoch 3/50
60000/60000 [==============================] - 1s 21us/step - loss: 164.9095 - val_loss: 163.9469
Epoch 4/50
60000/60000 [==============================] - 2s 27us/step - loss: 162.4408 - val_loss: 161.7431
Epoch 5/50

f:id:ascreit:20180921033824p:plainf:id:ascreit:20180921033829p:plain

おー!きれいに分かれてますね!

自前データセットで試してみる

では次に以前試した人物自動検出マッシーン(YOLOV3)

blog.ascreit.com

で抽出したデータでやってみます。

アノテーションから人物を切り抜くスクリプト

YOLOv3のアノテーションファイルは

[画像パス],x_min,y_min,x_max,y_max

となっていたので、これを元に人物を切り抜きます。

import sys,os
import argparse
import cv2,glob
import numpy as np
from PIL import Image, ImageFont, ImageDraw
from distutils.util import strtobool


def resize(img, base_w, base_h):
    import cv2
    base_ratio = base_w / base_h   #リサイズ画像サイズ縦横比
    img_h, img_w = img.shape[:2]   #画像サイズ
    img_ratio = img_w / img_h      #画像サイズ縦横比

    white_img = np.zeros((base_h, base_w, 3), np.uint8) #白塗り画像のベース作成
    white_img[:,:] = [255, 255, 255]                    #白塗り

    ###画像リサイズ, 白塗りにオーバーレイ
    if img_ratio > base_ratio:
        h = int(base_w/img_ratio)           #横から縦を計算
        w = base_w                          #横を合わせる
        resize_img = cv2.resize(img, (w,h)) #リサイズ
    else:
        h = base_h                          #縦を合わせる
        w = int(base_h*img_ratio)           #縦から横を計算
        resize_img = cv2.resize(img, (w,h)) #リサイズ

    white_img[int(base_h/2-h/2):int(base_h/2+h/2),int(base_w/2-w/2):int(base_w/2+w/2)] = resize_img #オーバーレイ
    resize_img = white_img #上書き

    return resize_img
if __name__ == '__main__':

    try:
        annotation_path = 'train.txt'
        w,h = 416,416
        save = 1
        output_dir = './croped_data'
        parser = argparse.ArgumentParser(argument_default=argparse.SUPPRESS)
        '''
        Command line options
        '''
        parser.add_argument(
            '--annotation', type=str,
            help='path to annotation file, default train.txt'
        )
        parser.add_argument(
            '--w', type=int,
            help='resize width, default 416'
        )
        parser.add_argument(
            '--h', type=int,
            help='resize height, default 416'
        )
        parser.add_argument(
            '--save', type=strtobool,
            help='save image file, default true'
        )
        parser.add_argument(
            '--output_dir', type=str,
            help='output directory, default ./croped_data'
        )
        FLAGS = parser.parse_args()

        if "annotation" in FLAGS:
            annotation_path = FLAGS.annotation
        if "save" in FLAGS:
            save = FLAGS.save
        if "output_dir" in FLAGS:
            output_dir = FLAGS.output_dir


        with open(annotation_path) as f:
            lines = f.readlines()

        for line in lines:
            data = line.split()
            # image = Image.open(data[0])
            image = cv2.imread(data[0])
            box = np.array([np.array(list(map(int,box.split(',')))) for box in data[1:]])

            # draw = ImageDraw.Draw(image)
            i = 0;
            for b in box:
                i+=1
                left,top,right,bottom,class_id = list(b)
                # image=image.crop([left,top,right,bottom])
                dst = image[top:bottom,left:right]
                # h,w,c = dst.shape
                dst = resize(dst,h,w)
                if save is 1:
                    dirname = os.path.dirname(data[0]).split('/')[-1]
                    filename = os.path.splitext(os.path.basename(data[0]))[0]
                    filepath = '%s/%s_%s_%s.jpg'%(output_dir,dirname,filename,i)
                    if not os.path.exists(output_dir):
                        os.makedirs(output_dir)
                    cv2.imwrite(filepath,dst)
                else:
                    cv2.imshow("result", dst)
                    key = cv2.waitKey(0)

                    # ESCで終了
                    if key==27:
                        print("exit")
                        cv2.destroyAllWindows()
                        exit()
    except KeyboardInterrupt:
        print("exit")

こんなの作って、

python3 crop_image.py --w 28 --h 28 --annotation yolov3_train/train.txt

こうすると

f:id:ascreit:20180921034735p:plain

こんなのが作られるので、これを使います。

動かしてみる

ネットワークとかはそのままで、画像の読み込み部分だけを変更しみ試してみます。

'''Example of VAE on MNIST dataset using MLP
The VAE has a modular design. The encoder, decoder and VAE
are 3 models that share weights. After training the VAE model,
the encoder can be used to  generate latent vectors.
The decoder can be used to generate MNIST digits by sampling the
latent vector from a Gaussian distribution with mean=0 and std=1.
# Reference
[1] Kingma, Diederik P., and Max Welling.
"Auto-encoding variational bayes."
https://arxiv.org/abs/1312.6114
'''

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from keras.layers import Lambda, Input, Dense
from keras.models import Model
from keras.datasets import mnist
from keras.losses import mse, binary_crossentropy
from keras.utils import plot_model,np_utils
from keras.utils import np_utils
from keras.preprocessing.image import array_to_img, img_to_array,  load_img
from sklearn.model_selection import train_test_split
from keras import backend as K

import numpy as np
import matplotlib.pyplot as plt
import argparse
import os,glob
import cv2

# reparameterization trick
# instead of sampling from Q(z|X), sample eps = N(0,I)
# z = z_mean + sqrt(var)*eps
def sampling(args):
    """Reparameterization trick by sampling fr an isotropic unit Gaussian.
    # Arguments:
        args (tensor): mean and log of variance of Q(z|X)
    # Returns:
        z (tensor): sampled latent vector
    """

    z_mean, z_log_var = args
    batch = K.shape(z_mean)[0]
    dim = K.int_shape(z_mean)[1]
    # by default, random_normal has mean=0 and std=1.0
    epsilon = K.random_normal(shape=(batch, dim))
    return z_mean + K.exp(0.5 * z_log_var) * epsilon


def plot_results(models,
                 data,
                 batch_size=128,
                 model_name="vae"):
    """Plots labels and MNIST digits as function of 2-dim latent vector
    # Arguments:
        models (tuple): encoder and decoder models
        data (tuple): test data and label
        batch_size (int): prediction batch size
        model_name (string): which model is using this function
    """

    encoder, decoder = models
    x_test, y_test = data
    os.makedirs(model_name, exist_ok=True)

    filename = os.path.join(model_name, "vae_mean.png")
    # display a 2D plot of the digit classes in the latent space
    z_mean, _, _ = encoder.predict(x_test,
                                   batch_size=batch_size)
    plt.figure(figsize=(12, 10))
    plt.scatter(z_mean[:, 0], z_mean[:, 1], c=y_test)
    plt.colorbar()
    plt.xlabel("z[0]")
    plt.ylabel("z[1]")
    plt.savefig(filename)
    plt.show()

    filename = os.path.join(model_name, "digits_over_latent.png")
    # display a 30x30 2D manifold of digits
    n = 30
    digit_size = 28
    figure = np.zeros((digit_size * n, digit_size * n,3))
    # linearly spaced coordinates corresponding to the 2D plot
    # of digit classes in the latent space
    grid_x = np.linspace(-4, 4, n)
    grid_y = np.linspace(-4, 4, n)[::-1]

    for i, yi in enumerate(grid_y):
        for j, xi in enumerate(grid_x):
            z_sample = np.array([[xi, yi]])
            x_decoded = decoder.predict(z_sample)
            digit = x_decoded[0].reshape(digit_size, digit_size,3)
            figure[i * digit_size: (i + 1) * digit_size,
                   j * digit_size: (j + 1) * digit_size] = digit

    plt.figure(figsize=(10, 10))
    start_range = digit_size // 2
    end_range = n * digit_size + start_range + 1
    pixel_range = np.arange(start_range, end_range, digit_size)
    sample_range_x = np.round(grid_x, 1)
    sample_range_y = np.round(grid_y, 1)
    plt.xticks(pixel_range, sample_range_x)
    plt.yticks(pixel_range, sample_range_y)
    plt.xlabel("z[0]")
    plt.ylabel("z[1]")
    plt.imshow(figure, cmap='Greys_r')
    plt.savefig(filename)
    plt.show()


# MNIST dataset
# (x_train, y_train), (x_test, y_test) = mnist.load_data()


filenames = glob.glob("croped_data/*")
X = []
Y = []
for filename in filenames:
    img = img_to_array(load_img(
    filename
    # , color_mode = "grayscale"
    , target_size=(28,28)))
    X.append(img)
    Y.append(0)

X = np.asarray(X)
Y = np.asarray(Y)

# 画素値を0から1の範囲に変換
X = X.astype('float32')
X = X / 255.0

# クラスの形式を変換
# Y = np_utils.to_categorical(Y, 1)
# 学習用データとテストデータ
x_train, x_test, y_train, y_test = train_test_split(X, Y, test_size=0.33)
cv2.imshow("result", x_train[0])
key = cv2.waitKey(0)

image_size = x_train.shape[1]
original_dim = image_size * image_size *3
print(x_train)
x_train = np.reshape(x_train, [-1, original_dim])
x_test = np.reshape(x_test, [-1, original_dim])
# x_train = x_train.astype('float32') / 255
# x_test = x_test.astype('float32') / 255

print(x_train)
# network parameters
input_shape = (original_dim, )
intermediate_dim = 512
batch_size = 128
latent_dim = 2
epochs = 50

# VAE model = encoder + decoder
# build encoder model
inputs = Input(shape=input_shape, name='encoder_input')
x = Dense(intermediate_dim, activation='relu')(inputs)
z_mean = Dense(latent_dim, name='z_mean')(x)
z_log_var = Dense(latent_dim, name='z_log_var')(x)

# use reparameterization trick to push the sampling out as input
# note that "output_shape" isn't necessary with the TensorFlow backend
z = Lambda(sampling, output_shape=(latent_dim,), name='z')([z_mean, z_log_var])

# instantiate encoder model
encoder = Model(inputs, [z_mean, z_log_var, z], name='encoder')
encoder.summary()
plot_model(encoder, to_file='vae_mlp_encoder.png', show_shapes=True)

# build decoder model
latent_inputs = Input(shape=(latent_dim,), name='z_sampling')
x = Dense(intermediate_dim, activation='relu')(latent_inputs)
outputs = Dense(original_dim, activation='sigmoid')(x)

# instantiate decoder model
decoder = Model(latent_inputs, outputs, name='decoder')
decoder.summary()
plot_model(decoder, to_file='vae_mlp_decoder.png', show_shapes=True)

# instantiate VAE model
outputs = decoder(encoder(inputs)[2])
vae = Model(inputs, outputs, name='vae_mlp')

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    help_ = "Load h5 model trained weights"
    parser.add_argument("-w", "--weights", help=help_)
    help_ = "Use mse loss instead of binary cross entropy (default)"
    parser.add_argument("-m",
                        "--mse",
                        help=help_, action='store_true')
    args = parser.parse_args()
    models = (encoder, decoder)
    data = (x_test, y_test)

    # VAE loss = mse_loss or xent_loss + kl_loss
    if args.mse:
        reconstruction_loss = mse(inputs, outputs)
    else:
        reconstruction_loss = binary_crossentropy(inputs,
                                                  outputs)

    reconstruction_loss *= original_dim
    kl_loss = 1 + z_log_var - K.square(z_mean) - K.exp(z_log_var)
    kl_loss = K.sum(kl_loss, axis=-1)
    kl_loss *= -0.5
    vae_loss = K.mean(reconstruction_loss + kl_loss)
    vae.add_loss(vae_loss)
    vae.compile(optimizer='adam')
    vae.summary()
    plot_model(vae,
               to_file='vae_mlp.png',
               show_shapes=True)

    if args.weights:
        vae.load_weights(args.weights)
    else:
        # train the autoencoder
        vae.fit(x_train,
                epochs=epochs,
                batch_size=batch_size,
                validation_data=(x_test, None))
        vae.save_weights('vae_mlp_mnist.h5')

    print(data)
    plot_results(models,
                 data,
                 batch_size=batch_size,
                 model_name="vae_mlp")

mnistの読み込みをコメントアウトして、代わりに自前データセットを28 x 28のカラーで読み込んで使ってます。

python3 vae.py

f:id:ascreit:20180921054113p:plainf:id:ascreit:20180921054127p:plain

Generaterの画像の方は今回使わないのであんま関係ないです。

分布を見る限り、なんかうまく分けれそうな感じしますね!

さて、あとはどうやって線をひこうか...