(20/11/09 追記)
Tensorflowv2以降だと動作しなかったので、v2で動作するように修正しました。
https://github.com/sey323/tf-gan
概要
KerasでGANを構築してあるケースは多々見かけるが,TensorflowのみでGANを構築しているケースがあまりないのでここで解説しながら作成を行う.
GANの構成はだいたいこんな感じ.Generatorでノイズから画像を生成し,Discriminatorで正解画像と生成された画像を識別する.この2つのネットワークがお互いのネットワークの性能を超える様に敵対して学習するため,敵対的学習と呼ばれている.
GANの詳しい仕組みは以下のものがとてもわかりやすいので参考に
今さら聞けないGAN(1) 基本構造の理解 - Qiita
ソースコードの全体像は以下のリンクから
https://github.com/sey323/tf-gan/tree/master
実行環境
OS : Ubuntu 16.04
CPU : Intel(R) Core(TM) i9-7980XE CPU @ 2.60GHz
GPU : GEFORCE RTX 2080Ti
python :3.6
tensorflow :1.15.1
GANのクラスの作成
GANの計算グラフの定義と学習のプロセスを記述するGAN
クラスを作成する.
def __init__(self ,
input_size,
channel= 3,
layers = [ 64 , 128 , 256],
filter_size = [ 5 , 5 ],
drop_prob = 0.5,
zdim = 100,
batch_num = 64,
learn_rate = 2e-4,
max_epoch = 100,
gpu_config = tf.GPUOptions(per_process_gpu_memory_fraction=0.1) ,
save_folder = 'results/GAN' ):
self.input_size = input_size
self.channel = channel
self.layers = layers
self.filter_size = filter_size
self.drop_prob = drop_prob
self.zdim = zdim
self.batch_num = batch_num
self.learn_rate = learn_rate
self.max_epoch = max_epoch
インスタンス変数 | 役割 |
---|---|
input_size | ネットワークに入力する画像のサイズ |
channel | ネットワークに入力する画像カラーチャネル(濃淡画像:1,カラー画像:3) |
layers | ネットワークの出力層の次元数 |
filter_size | ネットワークの層のフィルタサイズ |
drop_prob | ネットワークのドロップアウトの確率 |
zdim | 入力するノイズの次元数 |
batch_num | 学習するバッチ数 |
learn_rate | 学習率 |
max_epoch | 最大学習回数 |
gpu_config | GPUの設定 |
save_folder | 学習モデルを保存するディレクトリ |
GANはクラス内で自身のネットワークの構築や学習のプロセスを行う.
-
Generator()
GANの画像を生成するGeneratorのネットワークが定義されているクラス. -
Discriminator()
GANのGeneratorで生成された画像と正解画像の識別を行う,Discriminatorのネットワークが定義されているクラス. -
build_modle()
GANのネットワークの構築や損失関数,最適化手法を定義し,学習する計算グラフを作成するメソッド. -
train()
build_modle()
で作成された計算グラフに,実際に学習画像を入力し学習を行うメソッド.
ネットワーク
GANでは画像を生成するGeneratorと,GANの生成された画像が本物か偽物か識別するDiscriminatorの2つネットワークを用いている.それぞれのネットワークは別々に学習が行われるため,それぞれ別のメソッドで作成する.
Discriminatorの構築
Discriminatorは,生成された画像と正解画像の入力から,どちらの画像が生成された画像か識別する2値分類を行うCNNである.これは従来のニューラルネットワークと同様に,畳み込み層の積み重ねによりネットワークを構築し,全結合層においてクラス分類を行う.
def Discriminator(self, input, channel=3, reuse=False, name=""):
"""
Discriminator
Args
input (tensor):
本物か偽物か識別したい画像のTensor配列
channel (int):
入力するカラーチャネル
reuse (Boolean):
同じネットワークが呼び出された時に再定義し直すかどうか
"""
logging.info("[NETWORK]\tDeep Convolutional Discriminator")
with tf.compat.v1.variable_scope("Discriminator" + name, reuse=reuse) as scope:
if reuse:
scope.reuse_variables()
for i, output_shape in enumerate(self.layers, 1):
if i == 1: # 1層目の時だけ
before_output = input
# conv
with tf.compat.v1.variable_scope("conv_layer{0}".format(i)) as scope:
conv = layer.conv2d(
input=before_output,
stride=2,
filter_size=[self.filter_size[0], self.filter_size[1]],
output_dim=output_shape,
batch_norm=True,
name="Conv_{}".format(i),
)
conv = layer.leakyReLU(conv)
before_output = conv
with tf.compat.v1.variable_scope("Discriminator_Flatten", reuse=reuse) as scope:
# FC層
flatten_1 = layer.flatten(before_output, "Flatten")
output = layer.fc(flatten_1, 1, "Out", batch_norm=False)
return output
層の深さは,self.layer
に記述されているネットワークの出力層の次元数だけ繰り返す.最後の全結合層の出力は,入力された画像が生成された画像か正解画像かの2値分類なので1とする.
ここで重要なのはtf.variable_scope
でネットワークに名前をつけていること.これを用いることで後述のパラメータの更新の際に,更新するネットワーク(GeneratorかDiscriminator)を指定可能となり,別々の指標でネットワークの学習を進めることが可能となる.
Generatorの構築
GANのGeneratorは,1次元のランダムノイズzの入力を,学習画像のドメインに近い画像へ変換を行うネットワークである.Generatorでは,畳み込み層と逆の処理を行う逆畳み込み層(Deconvolution)を用いて,ノイズから画像を生成する.
def Generator(self, input, channel=3, reuse=False):
"""
Generator
Args
input (tensor):
生成の基となるランダムノイズ
channel (int):
出力するカラーチャネル
reuse (Boolean):
同じネットワークが呼び出された時に再定義し直すかどうか
"""
logging.info("[NETWORK]\tDeep Convolutional Generator")
with tf.compat.v1.variable_scope("Generator", reuse=reuse) as scope:
if reuse:
scope.reuse_variables()
# 逆FC
with tf.compat.v1.variable_scope("Fc{0}".format("")) as scope:
dim_h, dim_w = imutil.calcImageSize(
self.input_size[0],
self.input_size[1],
stride=2,
num=len(self.layers),
)
# 1層目
defc_1 = layer.defc(
input,
output_shape=[dim_h, dim_w,],
output_dim=self.layers[-1],
name="defc",
)
before_output = defc_1
# Deconv層
for i, input_shape in enumerate(reversed(self.layers)):
# 初期情報
layer_no = len(self.layers) - i
output_dim = self.layers[layer_no - 1]
output_h, output_w = imutil.calcImageSize(
self.input_size[0], self.input_size[1], stride=2, num=layer_no
)
logging.debug(
"[OUTPUT]\t(batch_size, output_height:{0}, output_width:{1}, output_dim:{2})".format(
output_h, output_w, output_dim
)
)
# deconv
with tf.compat.v1.variable_scope(
"deconv_layer{0}".format(layer_no)
) as scope:
deconv = layer.deconv2d(
before_output,
stride=2,
filter_size=[self.filter_size[0], self.filter_size[1]],
output_shape=[output_h, output_w],
output_dim=output_dim,
batch_norm=True,
name="Deconv_{}".format(layer_no),
)
before_output = layer.ReLU(deconv)
# 最後の層で画像に復元
with tf.compat.v1.variable_scope("image_reconstract") as scope:
deconv_out = layer.deconv2d(
before_output,
stride=2,
filter_size=[self.filter_size[0], self.filter_size[1]],
output_shape=[self.input_size[0], self.input_size[1]],
output_dim=channel,
batch_norm=False,
name="Deconv_Output",
)
output = layer.tanh(deconv_out)
return output
GeneratorもDiscriminatorと同様に,self.layerに記述されているネットワークの出力層の次元数だけ繰り返す.最後の畳み込み層の出力は,学習画像の次元に合わせてchannel
の値とする.
損失関数と最適化関数の定義
学習に用いるネットワークと最適化関数や損失関数,実験に用いるセッションを構築する.
def build_model(self):
'''
ネットワークの全体を作成する
'''
'''変数の定義'''
self.z = tf.placeholder( tf.float32, [None, self.zdim],name="z")
self.y_real = tf.placeholder( tf.float32, [None, self.input_size[0], self.input_size[1], 3],name="image")
'''Generatorのネットワークの構築'''
print('[BUILDING]\tGenerator')
self.y_fake = self.Generator(self.z,self.channel)
self.y_sample = self.Generator(self.z,self.channel,reuse=True)
'''Discrimnatorのネットワークの構築'''
print('[BUILDING]\tDiscriminator')
self.d_real = self.Discriminator(self.y_real)
self.d_fake = self.Discriminator(self.y_fake,reuse=True)
'''損失関数の定義'''
print('[BUILDING]\tLoss Function')
self.g_loss = loss_function.cross_entropy( x=self.d_fake,labels=tf.ones_like (self.d_fake), batch_num = self.batch_num , name = "g_loss_fake")
self.d_loss_real = loss_function.cross_entropy( x=self.d_real,labels=tf.ones_like (self.d_real), batch_num = self.batch_num , name = "d_loss_real")
self.d_loss_fake = loss_function.cross_entropy( x=self.d_fake,labels=tf.zeros_like(self.d_fake), batch_num = self.batch_num , name = "d_loss_fake")
self.d_loss = self.d_loss_real + self.d_loss_fake
'''最適化関数の定義'''
print('[BUILDING]\tOptimizer')
self.g_optimizer = tf.train.AdamOptimizer(self.learn_rate,beta1=0.5).minimize(self.g_loss, var_list=[x for x in tf.trainable_variables() if "Generator" in x.name])
self.d_optimizer = tf.train.AdamOptimizer(self.learn_rate,beta1=0.5).minimize(self.d_loss, var_list=[x for x in tf.trainable_variables() if "Discriminator" in x.name])
'''Tensorboadに保存する設定'''
print('[BUILDING]\tSAVE Node')
tf.summary.scalar( "d_loss_real" , self.d_loss_real)
tf.summary.scalar( "d_loss_fake" , self.d_loss_fake)
tf.summary.scalar( "d_loss" , self.d_loss)
tf.summary.scalar( "g_loss" , self.g_loss)
'''Sessionの定義'''
self.sess = tf.Session(config=self.gpu_config)
### saver
self.saver = tf.train.Saver()
self.summary = tf.summary.merge_all()
if self.save_folder: self.writer = tf.summary.FileWriter(self.save_folder, self.sess.graph)
損失関数
Discriminatorの損失関数は交差誤差を用いて偽物の画像を偽物と識別した際の損失と,本物の画像を本物と識別した際の損失の合計値を用いる.
Generatorの損失関数は,交差誤差を用いてDiscriminatorが偽物の画像を正解の画像と誤って識別した際の損失を用いる.
最適化関数
'''最適化関数の定義'''
print('[BUILDING]\tOptimizer')
self.g_optimizer = tf.train.AdamOptimizer(self.learn_rate,beta1=0.5).minimize(self.g_loss, var_list=[x for x in tf.trainable_variables() if "Generator" in x.name])
self.d_optimizer = tf.train.AdamOptimizer(self.learn_rate,beta1=0.5).minimize(self.d_loss, var_list=[x for x in tf.trainable_variables() if "Discriminator" in x.name])
最適化関数の部分では,Tensorflowのname_scopeの機能を用いて行う.tf.trainable_variables()
には定義したネットワークの計算グラフとその名称が保存されている.train.AdamOptimizer.minimize()
メゾットでは,更新の対象となる層を指定することが可能であるため,tf.trainable_variables()
を用いて計算グラフの名称の一覧を取得し,その名前にDiscriminator
またはGeneraotr
が含まれる計算グラフのみをパラメータ更新の対象として指定する.
学習
学習ではutil/batchgen.py
のbatch
クラスを用いて,ミニバッチ学習を行う.batch
クラスについて簡単に説明すると,画像の教師ラベルと画像データがbatch
クラスに格納されており,それらをbatch.getBatch({バッチ枚数})
を用いることで取得できる.そして取得した画像を,ネットワークとして定義したDiscriminator
の計算グラフに代入し,学習を進める.
batch.getEpoch()
で現在のバッチのループ回数を取得し,初期化の時に指定したmax_epoch
回になるまで学習を進める.学習回数が10回ごとにsummary
の出力,100回ごとに途中結果の出力を行う.
def train(self, batch_o):
self.build_model()
initOP = tf.global_variables_initializer()
self.sess.run(initOP)
step = -1
epoch = 0
time_history=[]
start = time.time()
while batch_o.getEpoch()<self.max_epoch:
step += 1
# ランダムにバッチと画像を取得
batch_images,batch_labels = batch_o.getBatch(self.batch_num)
batch_z = np.random.uniform(-1.,+1.,[self.batch_num,self.zdim]).astype(np.float32)
# Update Discrimnator
_,d_loss,y_fake,y_real,summary = self.sess.run([self.d_optimizer,self.d_loss,self.y_fake,self.y_real,self.summary],feed_dict={self.z:batch_z, self.y_real:batch_images})
# Update Generator
_,g_loss = self.sess.run([ self.g_optimizer , self.g_loss ] , feed_dict={ self.z:batch_z })
if step>0 and step%10==0:
self.writer.add_summary(summary , step )
if epoch != batch_o.getEpoch():
# 実行時間と損失関数の出力
train_time = time.time()-start
print("epoch: %6, loss(D)=%.4e, loss(G)=%.4e; time/step = %.2f sec"%(batch_o.getEpoch(),d_loss,g_loss,train_time))
self.dumper.add(batch_o.getEpoch(),step,d_loss,g_loss,train_time)
# ノイズの作成.
l0 = np.array([ x%10 for x in range( self.batch_num )] , dtype = np.int32)
z1 = np.random.uniform(-1,+1,[ self.batch_num , self.zdim ])
z2 = np.random.uniform(-1,+1,[ self.zdim ])
z2 = np.expand_dims( z2 , axis = 0 )
z2 = np.repeat( z2 , repeats = self.batch_num , axis = 0 )
# 画像を作成して保存
g_image1 = self.sess.run(self.y_sample,feed_dict={self.z:z1})
g_image2 = self.sess.run(self.y_sample,feed_dict={self.z:z2})
cv2.imwrite(os.path.join(self.save_folder,"images","img_%d_real.png"%step),imutil.tileImage( y_real ) * 255.+128.)
self.create(z1 ,os.path.join(self.save_folder,"images","img_%d_fake1.png"%step))
self.create(z2 ,os.path.join(self.save_folder,"images","img_%d_fake2.png"%step))
self.saver.save(self.sess,os.path.join(self.save_folder,"model.ckpt"),step)
epoch = batch_o.getEpoch()
# 時間の計測の再開
start = time.time()
self.dumper.save()
実行ファイル
最後に作成したGANクラスを実行するプログラムを作成する.画像読み込みなどに使うモジュールの詳細は割愛する.tensorflowのFLAGS
機能を使ってコンソール入力を受け取る.layers = [64 , 128 , 256]
としてあるが,今回は64-128-256の3層のGANを構築した.
import os,sys
sys.path.append('./util')
import imload
from batchgen import *
# Ganモデルの読み込み
sys.path.append('./models')
from gan import *
def main(FLAGS):
'''
メイン関数
'''
# パラメータの取得
img_path = os.getenv("DATASET_FOLDER", "dataset")
save_path = os.getenv("SAVE_FOLDER", "results")
'''設定ファイルからパラメータの読み込み'''
print('[LOADING]\tmodel parameters loding')
# ファイルのパラメータ
folder = FLAGS.folder
resize = [ FLAGS.resize , FLAGS.resize ]
file_num = FLAGS.file_num
gray = FLAGS.gray
channel = 1 if gray else 3
# GANのクラスに関するパラメータ
layers = [64 , 128 , 256]
max_epoch = FLAGS.max_epoch
batch_num = FLAGS.batch_size
save_folder = FLAGS.save_folder
save_path = save_path + '/' + save_folder
'''画像の読み込み'''
train_image ,train_label = imload.make( folder , gray = gray , train_num = file_num , img_size = resize[0] )
'''バッチの作成'''
batch = batchgen( train_image , train_label)
'''モデルの作成'''
print("[LOADING]\tGAN")
gan = GAN( input_size=resize,
channel = channel,
layers = layers,
batch_num = batch_num ,
max_epoch = max_epoch ,
save_folder = save_path)
# 学習の開始
gan.train( batch )
if __name__=="__main__":
flags = tf.app.flags
FLAGS = flags.FLAGS
# 実行するGANモデルの指定.
flags.DEFINE_string('type', 'gan', 'Choice GAN type.')
# 読み込む画像周り
flags.DEFINE_string('folder', '', 'Directory to put the training data.')
flags.DEFINE_integer('resize', 64, 'Size of Image.')
flags.DEFINE_integer('file_num', 0, 'Loading Images Num.')
flags.DEFINE_boolean('gray', False, 'Convert Gray Scale?')
# GANの学習パラメータ
flags.DEFINE_float('learning_rate', 0.001, 'Initial learning rate.')
flags.DEFINE_integer('max_epoch', 100, 'Number of steps to run trainer.')
flags.DEFINE_integer('batch_size', 25, 'Batch size. ''Must divide evenly into the dataset sizes.')
# 保存フォルダの決定
flags.DEFINE_string('save_folder', '', 'Data save folder')
main(FLAGS)
実験
データセットの準備
データセットは海外のセレブの画像のデータセットを集めた,FaceScrubを用いて行う.
画像の収集はこちらのリンクの手順で集める.
https://qiita.com/sey323/items/fc3cac3e9632c91ddd3f
集めた画像のうち,手っ取り早くA-Z順に上から10人適当に取ってきて,プログラムのディレクトリに移動する.教師データとなる正解画像は以下の様なものになる.
face --- Aaron_Eckhart
|- Adam_Brody
|- Adam_Mckay
・
・
|- Alec_Baldwin
学習の実行
学習の実行は以下の通り.
$ python train.py --folder=face --batch_size=25
[LOADING] model parameters loding
[LOADING] Label0 Name:Alan_Alda Pictures exit. Unit On 111
[LOADING] Label1 Name:Alan_Rickman Pictures exit. Unit On 119
[LOADING] Label2 Name:Adam_McKay Pictures exit. Unit On 45
[LOADING] Label3 Name:Al_Pacino Pictures exit. Unit On 102
[LOADING] Label4 Name:Adrien_Brody Pictures exit. Unit On 113
[LOADING] Label5 Name:Adam_Sandler Pictures exit. Unit On 95
[LOADING] Label6 Name:Alec_Baldwin Pictures exit. Unit On 125
[LOADING] Label7 Name:Aaron_Eckhart Pictures exit. Unit On 117
[LOADING] Label8 Name:Alan_Arkin Pictures exit. Unit On 94
[LOADING] Label9 Name:Adam_Brody Pictures exit. Unit On 107
[LOADING] GAN
[BUILDING] Generator
[NETWORK]
Deep Convolutional Generator
[NETWORK]
Deep Convolutional Generator
[BUILDING] Discriminator
[NETWORK]
Deep Convolutional Discriminator
[NETWORK]
Deep Convolutional Discriminator
[BUILDING] Loss Function
[BUILDING] Optimizer
[BUILDING] SAVE Node
2019-06-10 22:22:31.394602: I tensorflow/core/platform/cpu_feature_guard.cc:140] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 AVX512F FMA
2019-06-10 22:22:31.584971: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:898] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2019-06-10 22:22:31.586126: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1356] Found device 0 with properties:
name: GeForce GTX 1080 Ti major: 6 minor: 1 memoryClockRate(GHz): 1.582
pciBusID: 0000:17:00.0
totalMemory: 10.92GiB freeMemory: 10.76GiB
2019-06-10 22:22:31.741673: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:898] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2019-06-10 22:22:31.742130: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1356] Found device 1 with properties:
name: GeForce GTX 1080 Ti major: 6 minor: 1 memoryClockRate(GHz): 1.582
pciBusID: 0000:65:00.0
totalMemory: 10.91GiB freeMemory: 10.76GiB
2019-06-10 22:22:31.742985: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1435] Adding visible gpu devices: 0, 1
2019-06-10 22:22:32.114452: I tensorflow/core/common_runtime/gpu/gpu_device.cc:923] Device interconnect StreamExecutor with strength 1 edge matrix:
2019-06-10 22:22:32.114491: I tensorflow/core/common_runtime/gpu/gpu_device.cc:929] 0 1
2019-06-10 22:22:32.114497: I tensorflow/core/common_runtime/gpu/gpu_device.cc:942] 0: N Y
2019-06-10 22:22:32.114501: I tensorflow/core/common_runtime/gpu/gpu_device.cc:942] 1: Y N
2019-06-10 22:22:32.114772: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1053] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with 11178 MB memory) -> physical GPU (device: 0, name: GeForce GTX 1080 Ti, pci bus id: 0000:17:00.0, compute capability: 6.1)
2019-06-10 22:22:32.217003: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1053] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:1 with 11176 MB memory) -> physical GPU (device: 1, name: GeForce GTX 1080 Ti, pci bus id: 0000:65:00.0, compute capability: 6.1)
0: loss(D)=1.4175e+00, loss(G)=2.0379e+00; time/step = 2.53 sec
100: loss(D)=2.7534e-01, loss(G)=7.1763e+00; time/step = 17.97 sec
200: loss(D)=1.4626e-01, loss(G)=8.5171e+00; time/step = 18.54 sec
・
・
結果
生成された画像
学習回数が向上するにつれて,ノイズが少ない品質の高い画像が生成されていることが確認できうる.
損失関数の推移
tensorflowだと指定したパラメータをtensorboadで視覚化することができるので,tensorboadで学習結果を見て見る.
$ tensorboad --logdir={実行結果のパス}
g_lossは徐々にDiscriminatorを誤認識させる確率が向上しており,d_lossも徐々に識別性能が向上しており,うまく敵対的に学習して両方のネットワークの性能を高めていることが確認できる.
終わりに
今回は単純なDCGANを構築して画像を生成した.単純なGANでもかなり品質の高い画像が生成できている.次はcGANを作成し,生成する人物を指定した画像の生成をしたい.
このプログラム,読み込む画像のファイル構成を今回の実験と同じ様な形式で保存してもらえれば顔以外のデータセットでも学習可能なので,適当に試してみてください.