アプリとサービスのすすめ

アプリやIT系のサービスを中心に書いていきます。たまに副業やビジネス関係の情報なども気ままにつづります

Unet(Auto encoder)でスニーカー画像からロゴを生成してみた【機械学習】

よく元画像から別の画像を生成したりするのに使うautoencoderの亜種「Unet」を使ってみた。
今回やるのはadidasのスニーカーを入力して、ロゴを出力するように学習させるタスク。

autoencoderを使うのは初めてなので、作業過程などをまとめてく。

目次
1.概要
2.コード部分
3.生成画像
・まとめ

1.概要


autoencoderは簡単に言うと画像とか文章を入力して、別のものに変換して出力するモデル。

例は、画像→画像、文章→文章、画像→文章、みたいにたくさんある。

f:id:trafalbad:20190703072233j:plain


Unetはautoencoderの一種。

U-Netが強力なのはEncoderとDecoderとの間に「Contracting path(スキップコネクション)」があるからで、Residual Network(ResNet)と同じ効果を発揮するらしい。

f:id:trafalbad:20190703072149j:plain


ResNetを多様するDeep Unetもあるらしいが、今回は普通のUnetで試した。

今回試すことは、「adidasのスニーカー画像を入力→adidasのロゴ画像を出力する」タスクをやってみた。

adidasのスニーカー】
f:id:trafalbad:20190703072311p:plain




adidasのロゴ
f:id:trafalbad:20190703072335p:plain




2.コード部分

Unet

主に変えた部分は、

・層を二つ追加した
・BatchNormalizationを全層に加えた

の2点。

INPUT_SIZE =(256,256,3)
def unet(input_size = INPUT_SIZE):
    inputs = Input(input_size)

    conv1_1 = Conv2D(32, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(inputs)
    conv1_1 = Conv2D(32, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv1_1)
    norm1_1 =  BatchNormalization()(conv1_1)
    pool1_1 = MaxPooling2D(pool_size=(2, 2))(norm1_1)

    conv1 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool1_1)
    conv1 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv1)
    norm1 =  BatchNormalization()(conv1)
    pool1 = MaxPooling2D(pool_size=(2, 2))(norm1)
    conv2 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool1)
    conv2 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv2)
    norm2 = BatchNormalization()(conv2)
    pool2 = MaxPooling2D(pool_size=(2, 2))(norm2)
    conv3 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool2)
    conv3 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv3)
    norm3 = BatchNormalization()(conv3)
    pool3 = MaxPooling2D(pool_size=(2, 2))(norm3)
    conv4 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool3)
    conv4 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv4)
    norm4 = BatchNormalization()(conv4)
    pool4 = MaxPooling2D(pool_size=(2, 2))(norm4)

    conv5 = Conv2D(1024, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool4)
    conv5 = Conv2D(1024, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv5)
    norm5 = BatchNormalization()(conv5)

    up6 = Conv2D(512, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(norm5))
    merge6 = concatenate([norm4,up6], axis = 3)
    conv6 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge6)
    conv6 = Conv2D(512, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv6)
    norm6 = BatchNormalization()(conv6)

    up7 = Conv2D(256, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(norm6))
    merge7 = concatenate([norm3,up7], axis = 3)
    conv7 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge7)
    conv7 = Conv2D(256, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv7)
    norm7 = BatchNormalization()(conv7)

    up8 = Conv2D(128, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(norm7))
    merge8 = concatenate([norm2,up8], axis = 3)
    conv8 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge8)
    conv8 = Conv2D(128, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv8)
    norm8 = BatchNormalization()(conv8)

    up9 = Conv2D(64, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(norm8))
    merge9 = concatenate([norm1,up9], axis = 3)
    conv9 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge9)
    conv9 = Conv2D(64, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv9)
    conv9 = Conv2D(2, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv9)
    norm9 = BatchNormalization()(conv9)

    up9_1 = Conv2D(32, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(norm9))
    merge9_1 = concatenate([norm1_1, up9_1], axis = 3)
    conv9_1 = Conv2D(32, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge9_1)
    conv9_1 = Conv2D(32, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv9_1)
    conv9_1 = Conv2D(2, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv9_1)
    norm9_1 = BatchNormalization()(conv9_1)

    conv10 = Conv2D(3, 1, activation = 'sigmoid')(conv9_1)
    return Model(input = inputs, output = conv10)


Contracting pathがあるので、層を厚くしても特徴抽出の効果が増した気がする。





画像入力



今回は画像は増幅できないので、kerasのImagegeneratorメソッドは使わず、generatorを自作。

class trainGenerator(object):
    def __init__(self):
        self.img_generator=[]
        self.mask_generator=[]
        self.reset()
   
    def reset(self):
        self.img_generator=[]
        self.mask_generator=[]
      
    def flow_from_dir(self, img_path, mask_path, batch_size=10):
        while True:
            for imgs, mask in zip(os.listdir(img_path), os.listdir(mask_path)):
                imgs=cv2.imread(img_path+'/'+imgs)
                mask=cv2.imread(mask_path+'/'+mask)
                if imgs is not None:
                  self.img_generator.append(imgs/ 255)
                if mask is not None:
                  self.mask_generator.append(mask/ 255)
                    
                  if len(self.img_generator)==batch_size:
                      input_img=np.array(self.img_generator, dtype=np.float32)
                  if len(self.mask_generator)==batch_size:
                      input_mask=np.array(self.mask_generator, dtype=np.float32)
                      self.reset()
                      yield input_img, input_mask

class valGenerator(trainGenerator):
    def __init__(self):
        self.img_generator=[]
        self.mask_generator=[]
        self.reset()

flow_from_dirのimg_pathが入力、mask_pathが出力画像の教師画像になってる。

validationデータ用のvalGeneratorクラスはtrainGeneratorクラスを継承してる。
あと、指定したバッチサイズに切り出されるようにした。

これでmodel.fitgenerator()メソッドに入れればOK。




訓練部分


lossは、Unetで頻繁に使われるDice loss(dice_coef_loss)を使った。optimizerは代表的なAdam。

import keras.backend as K
import tensorflow as tf
import numpy as np

# loss function
def dice_coef_loss(y_true, y_pred):
    return 1.0 - dice_coef(y_true, y_pred)


def dice_coef(y_true, y_pred):
    y_true = K.flatten(y_true)
    y_pred = K.flatten(y_pred)
    intersection = K.sum(y_true * y_pred)
    return 2.0 * intersection / (K.sum(y_true) + K.sum(y_pred) + 1)

あと、keras.callbackに自作のクラスを入れて、5エポックごとにloss曲線のpng画像が保存されるように工夫した。

from dice_coefficient_loss import dice_coef_loss, dice_coef
from HistoryCallbackLoss import HistoryCheckpoint

callback=[]
callback.append(HistoryCheckpoint(filepath='tb/LearningCurve_{history}.png', verbose=1, period=10))
callback.append(TensorBoard(log_dir='tb/'))
callback.append(ModelCheckpoint('logss/{epoch:02d}_unet.hdf5', monitor='loss', verbose=1))

model = unet()
# model.load_weights("10_unet.hdf5")
model.compile(loss=dice_coef_loss, optimizer=Adam(lr=1e-4), metrics=[dice_coef])           
model.summary()

model.fit_generator(training, steps_per_epoch=2000, 
                    epochs=10,
                    callbacks=callback,
                    validation_data=validations, 
                    validation_steps=200)


工夫した点



今回はよく論文に登場する精度の高い「〜ネットワーク」みたいな、飛び道具的なアルゴリズムを使わずに、ニューラルネットワーク自体を変えて、精度改善の工夫をした。


・Unetの層を増やす

・Dice lossを使う

・validation dataを使う

・callbackメソッドを使い、損失関数を自動調節する関数を作成

・正規化→L1, L2正規化、画像を255で割る

の5点を主に工夫した。派手なことせず、地道なテクで、かなり精度が上がった。

ニューラルネット系は人気で精度が高い「飛び道具」的なアルゴリズムでも、実際に実装してみると、全然使えないこともある。

なので、臨機応変に使いこなしたり、ニューラルネットワーク自体をいじって、堅実に精度向上の工夫ができるテクニックは必須。




3.生成画像

生成した画像

f:id:trafalbad:20190703072405p:plain


出来が少し荒い反省点としては、


・真っ黒の画像をいくつか訓練データに含んでしまった(訓練データが荒い)

・訓練枚数(140枚)少ない、訓練回数が少ない

とかが原因。

画像枚数1万枚以上とか、ガチでやってる人のUnetの結果はすごいのが多い。

なので、逆に140枚でこれだけお手軽に結構な精度の画像が生成できるのはすごい。




まとめ

今回はautoencoderのUnetを試してみた。

ガチでニューラルネットワーク自体を変えて精度向上の工夫したのは初めだった。

Variational Autoencoder(VAE)を使った記事も機会があれば書こうと思う。