#概要
初投稿です。kosakae256と申すものです。
kerasでのDCGAN実装にあたっての失敗談を簡潔にまとめてみました。
本投稿ではGenerator内でresnetを使用しています。keras、DCGAN、resnetの実装がこれまでなかったので、ぜひ参考にしてください。
わかりにくい解説ですが、何卒よろしくお願いします。
本実装に関しては@taku-buntu さんの記事が非常に参考になりました。なんなら私のサイトなんていらないくらいわかりやすい解説です。ぜひこちらもご覧ください。https://qiita.com/taku-buntu/items/0093a68bfae0b0ff879d
#前提
- GANについてかじったことがある程度の知識
- DCGANによるMNISTの手書き文字生成をやったことがある
- kerasによるFunctionalAPIの用法及び、CNNの発展知識
- googlecolabの使い方を知っている
- 感覚でクッソ読みづらいコードを読み進めれる人
- resnetが目当てであれば、resnetの基本知識
#環境
- GoogleColaboratoryの実行環境
#画像生成のGANとは
敵対的生成ネットワークと呼ばれる、教師なし学習です。
名前の通り、二つのネットワークが競い合って成長していくネットワークのことを指します。
画像を生成する__Generator__、Generatorが作り出した画像と本物の画像判別する__Discriminator__の二つのネットワークを競い合わせます。
__Generator__は、__Discriminator__をだますように成長していき、やがてDiscriminatorが判別できないほど本物の画像に近い画像を生成できるようになります。
この一連の動作は、「偽札を作る犯罪者」と「偽札を見破る警察」にたとえられます。
ノイズzをうまく操作することで、Generatorは__本物の画像を複数個合成した画像__を作ることも可能です。今回の実装ではノイズzの操作をしていないため省略します。
(正直に言うと、完全に理解していないためです)
#実装
ノイズzからアニメ絵を生成する実装例を見ていきましょう。
アニメ絵のデータセットは以下から拝借しました。計12万枚となります
https://www.kaggle.com/soumikrakshit/anime-faces
https://www.kaggle.com/scribbless/another-anime-face-dataset
予め、128x128の画像に整形しています。
まずは下準備から
Colabで実装しているため、システム上にGoogleDriveをマウントするためのコード
from google.colab import drive
drive.mount('/content/drive')
コピーとかなんやかんややる
!cp "/content/drive/My Drive/animeface.zip" .
!unzip "animeface.zip"
X_trainに128x128のアニメキャラクターの画像データ12万枚を入れ込む。メモリ不足に注意すべし。
import cv2
import numpy as np
#アニメキャラの画像をX_trainに入れる
X_train = []
for i in range(1,120000):
print(i)
img = cv2.imread(f'./animeface/animeface.{str(i)}.jpg')
X_train.append(img)
X_train = np.array(X_train)
メインコード
Generatorのネットワークには劣化版resnetを用いている。何故劣化版を用いているかは後述する。
#128*128*3の、アニメキャラクターの顔の画像を生成する例
from __future__ import print_function, division
from keras.datasets import mnist
from keras.layers import Input, Dense, Reshape, Flatten, Dropout,Reshape,Add
from keras.layers import BatchNormalization, Activation, ZeroPadding2D,MaxPooling2D,GlobalAveragePooling2D
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.convolutional import UpSampling2D, Conv2D
from keras.models import Sequential, Model,model_from_json
from keras.optimizers import Adam
import cv2
import matplotlib.pyplot as plt
import sys
import numpy as np
import random
from keras.models import Sequential, Model,model_from_json
from tensorflow.keras.utils import plot_model
class DCGAN():
def __init__(self):
# Input shape
self.input_img_rows = 128
self.input_img_cols = 128
self.input_channels = 3
self.input_img_shape = (self.input_img_rows, self.input_img_cols, self.input_channels,)
# チューニング対象
self.latent_dim = 100
dis_optimizer = Adam(0.0002, 0.5)
gen_optimizer = Adam(0.0002, 0.5)
# 2値分類のための識別モデルを作成
self.discriminator = self.build_discriminator()
self.discriminator.compile(loss='binary_crossentropy',
optimizer=dis_optimizer,
metrics=['accuracy'])
# Build the generator
self.generator = self.build_generator()
plot_model(
self.generator,
show_shapes=True,
)
z = Input(shape = self.latent_dim)
img = self.generator(z)
# discriminatorのパラメータ固定
self.discriminator.trainable = False
valid = self.discriminator(img)
self.combined = Model(z, valid)#並列結合
self.combined.compile(loss='binary_crossentropy', optimizer=gen_optimizer)
self.value=0
def build_generator(self):
k=0.5 #成長率(笑)
self.filter=256
# ノイズ
input = Input(shape=self.latent_dim)
# 排出画像 128 * 128 * 3
x = Dense(self.filter * 32 * 32)(input)
x = Reshape((32 , 32 , self.filter))(x)
x = Conv2D(self.filter,(3,3),padding="same")(x)
x = BatchNormalization(momentum=0.8)(x)
x = UpSampling2D((2,2))(x) # 32x32 → 64x64
self.filter*=k
x = Conv2D(self.filter,(3,3),padding="same")(x)
x = self.resblock(x)
x = Activation("relu")(x)
x = BatchNormalization(momentum=0.8)(x)
x = UpSampling2D((2,2))(x) # 64x64 → 128x128
self.filter*=k
x = Conv2D(self.filter,(3,3),padding="same")(x)
x = self.resblock(x)
x = Activation("relu")(x)
x = BatchNormalization()(x)
x = Conv2D(self.filter , kernel_size=3, padding="same")(x)
x = Activation("relu")(x)
x = BatchNormalization()(x)
x = Conv2D(self.input_channels, kernel_size=3, padding="same")(x) # 128x128x3になる
output = Activation('tanh')(x) #-1.0 ~ 1.0
model = Model(input,output)
model.summary()
return model
def resblock(self,x):
x2 = Conv2D(self.filter,(3,3),padding="same")(x)
x2 = Activation("relu")(x2)
x2 = BatchNormalization(momentum=0.8)(x2)
x = Conv2D(self.filter,(1,1))(x)
x = Add()([x2,x])
return x
def build_discriminator(self):
model = Sequential()
model.add(Conv2D(256, kernel_size=3, strides=2, input_shape=self.input_img_shape, padding="same"))
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Conv2D(128, kernel_size=3, strides=2,padding="same"))
model.add(ZeroPadding2D(padding=((0, 1), (0, 1))))
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))
model.summary()
img = Input(shape=self.input_img_shape)
validity = model(img)
return Model(img, validity)
def train(self, epochs, batch_size=128, save_interval=5000):
print("実行1")
batch_size = int(batch_size/2)
valid = np.ones((batch_size, 1))
fake = np.zeros((batch_size, 1))
for epoch in range(self.value,epochs + self.value):
# 本物の画像をbatchの数だけランダムで持ってきます
idx = np.random.randint(0, len(X_train), batch_size)
true_imgs = (X_train[idx].astype(np.float16) - 127.5) / 127.5
# batchの数だけ平均0,分散1の正規分布のノイズを生成し、generatorに画像を生成させる
noise = np.random.normal(0, 1, (batch_size, self.latent_dim))
gen_imgs = self.generator.predict(noise)
# 本物の画像とフェイク画像を識別機に学習させます。
d_loss_real = self.discriminator.train_on_batch(true_imgs, valid)
d_loss_fake = self.discriminator.train_on_batch(gen_imgs, fake)
d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)
# 誤差を伝搬させて、generatorを学習させます
g_loss = self.combined.train_on_batch(noise, valid)
noise = np.random.normal(0, 1, (batch_size, self.latent_dim))
# エポック数表示
print ("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" % (epoch, d_loss[0], 100*d_loss[1], g_loss))
if epoch % 50 == 0:
showjpg = np.array((gen_imgs[0] * 0.5 + 0.5) * 255, np.int32) #0~255(人間に見やすく)
cv2.imwrite(f'/content/drive/MyDrive/GANstudy/animegenerate/resimages9/image_{str(epoch)}.jpg', showjpg)
# モデルと学習結果を保存する
if epoch % save_interval == save_interval-1:
print("save_model")
save_path = "/content/drive/MyDrive/GANstudy/animegenerate/resmodels9"
model_json_str = self.combined.to_json()
open(save_path + f'/combined{epoch+1}.json', 'w').write(model_json_str)
self.combined.save_weights(save_path + f'/combined{epoch+1}_weights.h5')
save_path = "/content/drive/MyDrive/GANstudy/animegenerate/resmodels9"
model_json_str = self.generator.to_json()
open(save_path + f'/generator{epoch+1}.json', 'w').write(model_json_str)
self.generator.save_weights(save_path + f'/generator{epoch+1}_weights.h5')
save_path = "/content/drive/MyDrive/GANstudy/animegenerate/resmodels9"
model_json_str = self.discriminator.to_json()
open(save_path + f'/discriminator{epoch+1}.json', 'w').write(model_json_str)
self.discriminator.save_weights(save_path + f'/discriminator{epoch+1}_weights.h5')
if __name__ == '__main__':
dcgan = DCGAN()
dcgan.train(epochs=1000000, batch_size=32, save_interval=5000)
####各関数の説明
init
パラメータの調整をする
generatorとdiscriminatorのモデルを生成
build_generator
劣化版resnetを用いたモデルを生成する
build_discriminator
discriminatorのモデルを生成する
train
実行
#工夫
- Generatorにはreluを、DiscriminatorにはLeakyreluを用いること
- generatorに与えるノイズには正規分布を用いる。generatorの表現力が上がるらしい。
# batchの数だけ平均0,分散1の正規分布のノイズを生成し、generatorに画像を生成させる
noise = np.random.normal(0, 1, (batch_size, self.latent_dim))
gen_imgs = self.generator.predict(noise)
- モデルを定期的に保存、とりあえず保存しとけばいろいろできる。
if epoch % save_interval == save_interval-1:
print("save_model")
save_path = "/content/drive/MyDrive/GANstudy/animegenerate/resmodels9"
model_json_str = self.combined.to_json()
open(save_path + f'/combined{epoch+1}.json', 'w').write(model_json_str)
self.combined.save_weights(save_path + f'/combined{epoch+1}_weights.h5')
#下に続く...
#結果
学習時のデータを適当にとってきた
左から順に250, 2000, 10000, 20000, 170000 epochとなっている
epochが増えるにつれて高精度な画像が生成されて行っているのが見て取れる。
ただ、5万epochを超えたころから目が3つになる現象が多発しはじめる。
そこから進化はせず、学習終了ということになった。
##何故こうなったのか
結論から言うと、シンプルなDCGANでは様々な問題によって、128x128の画像を作り出すことはできないみたいです。先人がこれまで挑戦してきたらしいですが、64x64が限界なんだとか。人にギリギリ見えるレベルの画像をつくれただけだいぶ奮闘できているらしい。
原因は、Generator側の力不足
DCGANの学習には、GeneratorとDiscriminatorのパワーバランスを保つ必要があります。二つのネットワークはだましだまされを繰り返して強くなるので、片方が強すぎると学習が進まなくなります。
基本的にはDiscriminatorのほうが強くなります。そのため、Discriminatorは手加減してあげないといけません。
ですが、Generatorは強化しすぎると学習速度が遅くなりすぎるため、シンプルなGANを強くするすべはないでしょう。
#劣化版resnetについての解説
resnetについてよくわからない人は飛ばしてください。
GANの学習において重要なのは、Generatorの精度と成長速度です。
シンプルなresnetは学習速度が遅すぎます。かといってresnetを使用しないモデルは精度が低すぎます。そこで、私が提案したのが劣化版resnetというわけです。
def resblock(self,x):
x2 = Conv2D(self.filter,(3,3),padding="same")(x)
x2 = Activation("relu")(x2)
x2 = BatchNormalization(momentum=0.8)(x2)
x = Conv2D(self.filter,(1,1))(x)
x = Add()([x2,x])
return x
速度、精度を天秤にかけた結果、このような形になりました。
#まとめ
シンプルなDCGANには限界があります。
ですが、一度128x128xRGBの画像生成に挑戦してみるのはいいかもしれません。
勉強にはなります。また、なぜ無理なのかも感覚で分かってきます。
MNIST手書き文字で満足せず、本当のGANに挑戦してほしいと願っています。
ちなみに、シンプルなDCGANには限界がありますが、PGGANなら1024x1024の画像生成も可能ですよ。ぜひ調べてみてください。