よく元画像から別の画像を生成したりするのに使うautoencoderの亜種「Unet」を使ってみた。
今回やるのはadidasのスニーカーを入力して、ロゴを出力するように学習させるタスク。
autoencoderを使うのは初めてなので、作業過程などをまとめてく。
目次
1.概要
2.コード部分
3.生成画像
・まとめ
1.概要
autoencoderは簡単に言うと画像とか文章を入力して、別のものに変換して出力するモデル。
例は、画像→画像、文章→文章、画像→文章、みたいにたくさんある。
Unetはautoencoderの一種。
U-Netが強力なのはEncoderとDecoderとの間に「Contracting path(スキップコネクション)」があるからで、Residual Network(ResNet)と同じ効果を発揮するらしい。
ResNetを多様するDeep Unetもあるらしいが、今回は普通のUnetで試した。
今回試すことは、「adidasのスニーカー画像を入力→adidasのロゴ画像を出力する」タスクをやってみた。
【adidasのスニーカー】
【adidasのロゴ】
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)
工夫した点
今回はよく論文に登場する精度の高い「〜ネットワーク」みたいな、飛び道具的なアルゴリズムを使わずに、ニューラルネットワーク自体を変えて、精度改善の工夫をした。
・Dice lossを使う
・validation dataを使う
・callbackメソッドを使い、損失関数を自動調節する関数を作成
・正規化→L1, L2正規化、画像を255で割る
の5点を主に工夫した。派手なことせず、地道なテクで、かなり精度が上がった。
ニューラルネット系は人気で精度が高い「飛び道具」的なアルゴリズムでも、実際に実装してみると、全然使えないこともある。
なので、臨機応変に使いこなしたり、ニューラルネットワーク自体をいじって、堅実に精度向上の工夫ができるテクニックは必須。
3.生成画像
生成した画像出来が少し荒い反省点としては、
・真っ黒の画像をいくつか訓練データに含んでしまった(訓練データが荒い)
・訓練枚数(140枚)少ない、訓練回数が少ない
とかが原因。
画像枚数1万枚以上とか、ガチでやってる人のUnetの結果はすごいのが多い。
なので、逆に140枚でこれだけお手軽に結構な精度の画像が生成できるのはすごい。
まとめ
今回はautoencoderのUnetを試してみた。
ガチでニューラルネットワーク自体を変えて精度向上の工夫したのは初めだった。
Variational Autoencoder(VAE)を使った記事も機会があれば書こうと思う。