DeepLearning

DCGANをNumpyでゼロから作ったので解説します。コードあり

はじめに

大人気のディープラーニング学習本「ゼロから作るDeepLearning」では、TensorFlowやPytorchディープラーニングのフレームワークを使うことなく「ゼロから作る」ことでより深く理解することを目的とされています。この本は非常にわかりやすく評判もとても良いです。私も本書からディープラーニングの理論と実装について多くのことを学びました。シリーズ化もされていますね。

無印ではディープラーニングの基本と画像認識、②では自然言語処理や時系列処理(RNN)について取り扱っています。そして現在執筆中の③ではディープラーニングのフレームワークについて記載されるようです。

しかしながら、今のところディープラーニングの生成モデルについては詳しく取り上げられてはいません。
このブログでは勝手ながら、 「ゼロから作る」を意識した生成モデルの紹介をしていこうと思います。

今回はDCGANについて解説していきます。

本記事のDCGAN全実装はGitHubのpometa0507/DCGAN-Numpyに載せてあります。綺麗なコードではありませんが、是非コードを追いながら実行して理解を深めていただければと思います。

前提知識

本記事では、「ゼロから作るDeepLearning」の内容を理解していることを前提に解説をしていきます。

GANとは

まずはGANの概要について説明します。

GAN(Generative Adversarial Networks 、敵対的生成ネットワーク)は生成モデルに対するアプローチのひとつでイアン・グッドフェロー氏が考案したモデルです。ヤン・ルカン氏が「機械学習分野において、この10年で最も面白いアイデア」と発言したことでも有名です。

GANは教師なし学習に分類されます。GANは訓練データから特徴を学習することで、実在しないデータを生成することができます!

GANの実例として、猫の生成画像を見てみます。

上の図では、GANで白猫ちゃんとトラ猫ちゃんを生成しています。 (白猫ちゃんはオッドアイでかわいいですね!)
そして、白猫ちゃんからトラ猫ちゃんの中間の画像も生成させています。
これらの猫ちゃんは全て実在しない猫です。つまり、GANで実在しない猫を生成できたということです。

実在しない画像を生成できると何がうれしいのでしょうか。
ここで一例をあげておきます。ディープラーニングでは非常に大量の教師データが必要です。もちろん、画像認識においても学習には多くの画像を要します。しかしながら、大量の画像を準備するにはそれなりに苦労します。そんなときに役立つのがGANです。GANを使うことで実在しない教師画像を大量に生成することができます!

実例では、東芝の「学習画像をAIで自動生成、高精度な送電線保守点検へ」などがあります。

GANの仕組み

それではGANの仕組みについて説明していきます。

GANはGenerator(生成器)とDiscriminator(識別器)の2つのネットワークがあります。

Generatorは潜在変数zを入力として偽物画像を生成します。GeneratorはDiscriminatorを騙すように本物に近い画像が生成されるように学習します。

Discriminatorは入力された画像が偽物か本物かを識別します。Discriminatorは本物を本物と、偽物を偽物と正しく識別できるように学習します。

GeneratorとDiscriminatorを競わせるようにお互いの学習を交互に繰り返していきます。

GeneratorとDiscriminatorはお互いが敵対的な関係となっていることです。簡単に言うといたちごっこですね。偽札の偽造者と警察官の事例で例えられたりもします。

学習が進むにつれてGeneratorは本物に近い画像を生成できるようになるわけです。すんばらしいですね!

DCGANの概要

DCGAN(Deep Convolutional GAN)は、GANのGeneratorとDiscriminatorのニューラルネットワーク構造にCNN(畳み込み層)を適用したものです。

GeneratorとDiscriminatorが敵対的な関係で学習するという基本的な構造はGANと同じですが、CNNを導入することでより画像の生成と認識ともに精度の向上を図っています。

論文  Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks で記載されているDCGANのアプローチを以下にあげます。

  • Pooling層は使用しない。その代わり、Dicriminatorでは畳み込み層(Convolution)でダウンサイズし、Generatorでは転置畳み込み層(TransConvolution)でアップサイズを行う。
  • バッチ正規化をGeneratorとDiscriminatorの両方に使う。
  • 全結合層は使わない。
  • Generatorでは、出力層以外の活性化関数にReLUを使う。出力層ではTanhを使う。
  • Dicriminatorでは、すべての層で活性化関数にLeakyReLUを使う。

ネットワーク構成

DCGANをディープラーニングのフレームワークを使わずにNumpyを使って実装したコードの要点を解説していきます。 (DCGANの学習は非常に計算量が多いです。Numpyだと途方もない時間がかかるため、高速化のためオプションとしてCupyでも実装しています。)

※DCGANのネットワーク構造は必ずしもこの通りといわけではありません。これはDCGANの一例です。

DCGANの構造を、Generatorのネットワーク構成、Discriminatorのネットワーク構成、そしてこれらを組み合わせたGCGANの3つのクラスにわけて実装しています。

Generatorクラスのネットワーク構造と順伝播

Generatorは潜在変数から画像を生成します。

Generatorの入力は潜在変数zです。 この潜在変数から段階的にアップサイズさせて偽物画像を生成します。

Generatorで扱うデータの形状(shape)は(バッチサイズ、チャンネルサイズ、画像の縦サイズ、画像の横サイズ)としています。(これはDiscriminatorも同じです。)

Generator-Network

それではGeneratorの各層の働きをみていきます。

入力層では100次元の潜在変数zを入力とします。潜在変数zの形状は(BatchSize, 100, 1, 1)となっています。入力層の処理は隠れ層と同じで「転置畳み込み層+バッチ正規化+ReLU」を行います。

隠れ層では「転置畳み込み層+バッチ正規化+ReLU」 をひとつのセットとして、これを4セット分繰り返すことでアップサイズしていきます。

出力層では「転置畳み込み層+Tanh」を用いています。 出力層の活性化関数にTanhを使用することで-1~+1の範囲で出力しています。出力層を通ることで、出力される画像データの形状は(BatchSize, 3, 64, 64)となります。

ここでひとつ注意点があります。それは画像データのピクセル値を「-1~+1」として扱っていることです。画像のピクセル値は「0~1」、または、「0~255」を使うのが普通ですが、DCGANではピクセル値を -1~+1 に正規化することで誤差逆伝播による学習をスムーズに行っているようです。

Generatorのネットワークを順伝播することで、潜在変数の形状(BatchSize, 100, 1, 1)を画像データの形状(BatchSize, 3, 64, 64)までアップサイズさせています。(ここで画像データのチャンネル数3はRGBを表しています、)

それでは、Generatorクラスの初期化と順伝播部分のコードをみていきます。

class Generator:
    def __init__(self, latent_dim=100):

        self.latent_dim = latent_dim   #潜在空間

        # レイヤの生成
        self.layers = OrderedDict()
        self.layers['ConvTrans1'] = ConvTrans2D(in_channels=latent_dim, out_channels=256, kernel_size=4, stride=1, pad=0, bias=False)
        self.layers['BN1'] = BatchNormalization(np.ones(256),np.zeros(256))
        self.layers['Relu1'] = Relu()

        self.layers['ConvTrans2'] = ConvTrans2D(in_channels=256, out_channels=128, kernel_size=4, stride=2, pad=1, bias=False)
        self.layers['BN2'] = BatchNormalization(np.ones(128),np.zeros(128))
        self.layers['Relu2'] = Relu()
        
        self.layers['ConvTrans3'] = ConvTrans2D(in_channels=128, out_channels=64, kernel_size=4, stride=2, pad=1, bias=False)
        self.layers['BN3'] = BatchNormalization(np.ones(64),np.zeros(64))
        self.layers['Relu3'] = Relu()
        
        self.layers['ConvTrans4'] = ConvTrans2D(in_channels=64, out_channels=32, kernel_size=4, stride=2, pad=1, bias=False)
        self.layers['BN4'] = BatchNormalization(np.ones(32),np.zeros(32))
        self.layers['Relu4'] = Relu()
        
        self.layers['ConvTrans5'] = ConvTrans2D(in_channels=32, out_channels=3, kernel_size=4, stride=2, pad=1, bias=False)
        self.layers['Tanh'] = Tanh()
        
        (中略)

    def forward(self, x, train_flag=True):
        x = self.layers['ConvTrans1'].forward(x)
        x = self.layers['BN1'].forward(x, train_flag)
        x = self.layers['Relu1'].forward(x)
        x = self.layers['ConvTrans2'].forward(x)
        x = self.layers['BN2'].forward(x, train_flag)
        x = self.layers['Relu2'].forward(x)
        x = self.layers['ConvTrans3'].forward(x)
        x = self.layers['BN3'].forward(x, train_flag)
        x = self.layers['Relu3'].forward(x)
        x = self.layers['ConvTrans4'].forward(x)
        x = self.layers['BN4'].forward(x, train_flag)
        x = self.layers['Relu4'].forward(x)
        x = self.layers['ConvTrans5'].forward(x)
        x = self.layers['Tanh'].forward(x)
        return x

まずは、__init__関数です。

latent_dimは潜在変数の次元数を示しています。ここでは100次元としています。

レイヤの生成部分では、「転置畳み込み層+バッチ正規化(BatchNorm)+ReLU」 を4セット繰り返したのち、出力層で「転置畳み込み層+Tanh」を定義しています。

これにより、潜在変数の形状(BatchSize, 100, 1, 1)を画像(偽物)の形状( BatchSize, 3, 64, 64)にアップサイズしています。

次に、forward関数です。

潜在変数を入力xとして、先ほど定義した層で順伝播させます。forwardの返り値returnは画像データ(偽物)なので、データの形状は (BatchSize, 3, 64, 64)です。

train_flagについて補足します。隠れ層の中でバッチ正規化を使っているので、train_flagを引数としています。訓練時はtrain_flag=Trueとして、訓練データのバッチから平均と分散を算出して正規化しています。推論(潜在変数から画像を生成)するときは、 train_flag=Falseとして訓練時に算出された平均と分散を用いて正規化を行っているわけです。

(backward(誤差逆伝播の処理)については後述。)

Generatorの逆伝播

Generatorの逆伝播 (backward) についてです。 (DCGANの学習フロー詳細についてはDiscriminatorと合わせて後述します。)

Generatorの逆伝播は、Discriminatorからの勾配を受け取ったのちGeneratorの各層を定義した逆順に辿って勾配を計算していきます。

それでは、Generatorの誤差逆伝播のコードをみていきます。

class Generator:

    (中略)

    def backward(self, dout_d):
        """勾配を求める(誤差逆伝播法)
        Parameters
        ----------
        dout_d : Discriminatorから逆伝播された勾配
        """

        # backward
        self.dout = dout_d           #Discriminatorからの勾配
        
        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            self.dout = layer.backward(self.dout)
            self.dout = layer.backward(self.dout)
        
        # 設定
        grads = {}
        grads['W1']= self.layers['ConvTrans1'].dW
        grads['gamma1'], grads['beta1'] = self.layers['BN1'].dgamma, self.layers['BN1'].dbeta
        grads['W2']= self.layers['ConvTrans2'].dW
        grads['gamma2'], grads['beta2'] = self.layers['BN2'].dgamma, self.layers['BN2'].dbeta
        grads['W3']= self.layers['ConvTrans3'].dW
        grads['gamma3'], grads['beta3'] = self.layers['BN3'].dgamma, self.layers['BN3'].dbeta
        grads['W4']= self.layers['ConvTrans4'].dW
        grads['gamma4'], grads['beta4'] = self.layers['BN4'].dgamma, self.layers['BN4'].dbeta
        grads['W5']= self.layers['ConvTrans5'].dW

        return grads

backward関数では引数にdout_dをとります。 dout_dはDiscriminatorから逆伝播してきた勾配を与えます。その後、dout_dをGeneratorの逆伝播させて各層のパラメータの勾配を求めてgradsに格納してreturnします。

最後に損失を求めるloss関数についてです。

Generatorクラスでは損失を算出しないため、loss関数はpassとして処理を行っていません。(というよりloss関数を使っていないです。)損失を算出するのはDiscriminatorの役割になります。

    def loss(self, z, t, skip_forward=False):
        pass

Discriminatorクラスのネットワーク構造と順伝播

Discriminatorは画像から偽物か本物かを識別します。

Discriminatorの入力は画像データです。 この画像が偽物か本物かを識別します。

Discriminatorrで扱うデータの形状(shape)は(バッチサイズ、チャンネルサイズ、画像の縦サイズ、画像の横サイズ)としています。(これはGeneratorも同じでしたね。)

Discriminator-Network

それでは、Discriminatorの各層の働きをみていきます。

入力層では画像データを入力とします。画像データの形状は(BatchSize, 3, 64, 64)となっています。 入力層ではバッチ正規化せず「畳み込み層+LeakyReLU」となります。

隠れ層では「畳み込み層+バッチ正規化(BatchNorm)+LeakyReLU」 をひとつのセットとして、これを3セット分繰り返すことでダウンサイズさせていきます。

出力層では「畳み込み層+ Sigmoid を用いています。出力は0か1の2クラス分類のため、出力層の活性化関数にSigmoidを使用することで0~+1の範囲で出力しています。出力層を通ることで、出力されるデータの形状は(BatchSize, 1, 1, 1)となります。Discriminatorの役割は識別でした。そのため出力データは画像が偽物と判定したとき0に、本物と判定した1に近い値を出力します。

Discriminatorのネットワークを順伝播することで、画像データのサイズ(3x64x64)を識別値のサイズ(1x1x1)までダウンサイズさせます。

それでは、Discriminatorクラスの初期化と順伝播部分のコードをみていきます。

class Discriminator:
    def __init__(self):

        # レイヤの生成
        self.layers = OrderedDict()
        self.layers['Conv1'] = Convolution(3,32,4,4,stride=2, pad=1, bias=False)
        self.layers['LRelu1'] = LRelu(0.2)

        self.layers['Conv2'] = Convolution(32,64,4,4,stride=2, pad=1, bias=False)
        self.layers['BN1'] = BatchNormalization(np.ones(64),np.zeros(64))
        self.layers['LRelu2'] = LRelu(0.2)
        
        self.layers['Conv3'] = Convolution(64,128,4,4,stride=2, pad=1, bias=False)
        self.layers['BN2'] = BatchNormalization(np.ones(128),np.zeros(128))
        self.layers['LRelu3'] = LRelu(0.2)
        
        self.layers['Conv4'] = Convolution(128,256,4,4,stride=2, pad=1, bias=False)
        self.layers['BN3'] = BatchNormalization(np.ones(256),np.zeros(256))
        self.layers['LRelu4'] = LRelu(0.2)
        
        self.layers['Conv5'] = Convolution(256,1,4,4,stride=1, pad=0, bias=False)

        #Binary Cross Entropy With Sigmoid
        self.last_layer = SigmoidWithLoss()
        
        self.dout = None
                
        (中着)

    def forward(self, x, train_flag=True):
        x = self.layers['Conv1'].forward(x)
        x = self.layers['LRelu1'].forward(x)
        x = self.layers['Conv2'].forward(x)
        x = self.layers['BN1'].forward(x, train_flag)
        x = self.layers['LRelu2'].forward(x)
        x = self.layers['Conv3'].forward(x)
        x = self.layers['BN2'].forward(x, train_flag)
        x = self.layers['LRelu3'].forward(x)
        x = self.layers['Conv4'].forward(x)
        x = self.layers['BN3'].forward(x, train_flag)
        x = self.layers['LRelu4'].forward(x)
        x = self.layers['Conv5'].forward(x)       
        return x

まずは、__init__関数についてです。

レイヤの生成部分では、入力層に 「畳み込み層+LeakyReLU」 、隠れ層 に「畳み込み層+バッチ正規化+LeakyReLU」 を3セット、そして出力層に「畳み込み層+Sigmoid」を定義しています。

これにより、画像の形状( BatchSize, 3, 64, 64)から識別値の形状(BatchSie, 1, 1, 1)にダウンサイズしています。

self.last_layerではSigmoid関数と損失関数のBinaryCrossEntropyを合わせたSigmoidWithLoss()を定義しています。

次に、forward関数です。

画像データを入力xとして、先ほど定義した層で順伝播させます。forwardのreturnは入力された画像が偽物(0)か本物(1)かを判定するテンソルとなります。出力データの形状は (BatchSize, 1, 1, 1)です。

DiscriminatorについてもGenerator同様にバッチ正規化を用いているため、引数にtrain_flagをとっています。

Discriminatorの逆伝播と損失

Discriminatorの逆伝播 (backward) についてです。 (DCGANの学習フロー詳細についてはGeneratorと合わせて後述します。)

Discriminatorの逆伝播は、通常のニューラルネットワークと同様の処理となり、Discriminatorの各層を定義した逆順に辿って勾配を計算していくだけです。

それでは、Discriminatorの誤差逆伝播のコードをみていきます。

class Discriminator:

    (中略)

    def backward(self, z, t):
        """勾配を求める(誤差逆伝播法)
        Parameters
        ----------
        z : 入力データ
        t : 教師ラベル
        """
        # forward
        self.loss_D = self.loss(z, t)

        # backward
        self.dout = 1
        self.dout = self.last_layer.backward(self.dout)   #勾配

        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            self.dout = layer.backward(self.dout)
        
        # 設定
        grads = {}
        grads['W1']= self.layers['Conv1'].dW
        grads['W2']= self.layers['Conv2'].dW
        grads['gamma1'], grads['beta1'] = self.layers['BN1'].dgamma, self.layers['BN1'].dbeta
        grads['W3']= self.layers['Conv3'].dW
        grads['gamma2'], grads['beta2'] = self.layers['BN2'].dgamma, self.layers['BN2'].dbeta
        grads['W4']= self.layers['Conv4'].dW
        grads['gamma3'], grads['beta3'] = self.layers['BN3'].dgamma, self.layers['BN3'].dbeta
        grads['W5']= self.layers['Conv5'].dW

        return grads

まず最初にself.loss_D = self.loss(z, t)でforward処理を行います。

つぎに、self.dout = self.last_layer.backward(self.dout)で、損失関数+Sigmoidの逆伝播を行います。その後、残りの層の逆伝播を行い、各層のパラメータの勾配を求めてgradsに格納しreturnします。

最後に損失を求めるloss関数についてです。 入力データzと教師データtから損失を求めます。

class Discriminator:

    (中略)
    def loss(self, z, t):
        """損失関数を求める
        Parameters
        ----------
        z : 入力データ
        t : 教師ラベル
        """
        y = self.forward(z)
        return self.last_layer.forward(y, t)

学習

GANの学習はGeneratorとDiscriminatorの学習を交互に行います。DCGANの学習手順もGANと基本的に同じです。異なるのはネットワークがCNNかどうかだけで、学習の手順は同じになるわけです。

それではGeneratorとDiscriminatorの各々の学習についてみていきます。

Generatorの学習

Generatorの学習について概要を説明します。

Generatorは生成された偽物画像をDiscriminatorで本物と判定させたいのです。そのため、Discriminatorで判定された画像の正解ラベルを「本物」として、損失を算出し誤差逆伝播にて勾配を更新します。生成画像を本物に近づけるためにこうやって教師データを本物として定義しているのですね。

つまり、Generatorの学習はDicriminatorを騙せるように学習します。

Generatorの学習では、Dicriminatorを通して勾配をGeneratorに逆伝播させていきます。このとき、Dicriminatorでは勾配の更新を行いません。Discriminatorは誤差逆伝播により求めた勾配をGeneratorに逆伝播させるだけです。

Generator-Training

Discriminatorの学習

Discriminatorの学習について概要を解説します。

DiscriminatorはGeneratorから生成された偽物画像を偽物と判定し、かつ、訓練データの本物画像を本物と判定したいのです。

そこで、Discriminatorの学習は偽物と本物を分けて行います。

①Generatorが生成した画像を偽物と識別できるように学習(図中の緑線)
 Generatorの学習時では正解ラベルを本物(1)としましたが、ここでは偽物と判定させたいので、正解ラベルは偽物(0)として学習させます。

②本物画像を本物と識別できるように学習(図中の青線)
 正解ラベルは本物(1)として学習させます。

Discriminator-Training

DCGANクラス

最後に、GeneratorとDiscriminatorを合わせたDCGANクラスについて解説します。

先にコードを示します。

class DCGAN:
    """Convolutional GAN"""
    def __init__(self, latent_dim=100):
        #潜在変数の次元
        self.latent_dim = latent_dim
        
        #Model
        self.model_G = Generator(self.latent_dim)
        self.model_D = Discriminator()

        #Optimizer
        self.optimizer_G = Adam(lr=0.0002, beta1=0.5, beta2=0.999)
        self.optimizer_D = Adam(lr=0.0002, beta1=0.5, beta2=0.999)

    def train(self, x_train, savedir, epochs, batch_size=64):
        """学習"""

        print("epocks={} , batch_size={}".format(epochs,batch_size))

        loss_G_array = np.array([])
        loss_D_real_array = np.array([])
        loss_D_fake_array = np.array([])
        
        #訓練データ シャッフル
        np.random.shuffle(x_train)

        for ep in range(epochs):

            #イテレーション処理(tqdmでプログレスバーを表示)
            itr = np.arange(0, len(x_train), batch_size)
            with tqdm(itr) as pbar:
                for idx in pbar:
                    #tqdm プログレスバー表示
                    pbar.set_description("[Epoch %d]" % (ep))
                    
                    #訓練データ(batch)
                    x = x_train[idx:idx+batch_size]
                    
                    #実際のbatchサイズ
                    batch = x.shape[0]
                    
                    #教師データ 0 or 1
                    t_ones = np.ones((batch,1,1,1))
                    t_zeros = np.zeros((batch,1,1,1))
                    
                    ########################################
                    ####   Generator  backpropagation   ####
                    ########################################

                    z = np.random.randn(batch, self.latent_dim, 1, 1)        #潜在変数 z
                    fake_img = self.model_G.forward(z)                       #Generatorが生成したfake_img

                    self.model_D.backward(fake_img, t_ones)                  #Dは勾配のみ求める。このとき教師は1とする
                    Loss_G = self.model_D.loss_D                             #Dの損失関数(ログ用)

                    #Discriminatorの勾配 model_D.dout
                    dout_D = self.model_D.dout
                    grads_G = self.model_G.backward(dout_D)                  #Gの勾配
                    self.optimizer_G.update(self.model_G.params, grads_G)
                    

                    ########################################
                    #### Discriminator  backpropagation ####
                    ########################################

                    real_img = x                                             #Real_img

                    #それぞれの勾配を計算
                    grads_D_real = self.model_D.backward(real_img, t_ones)   #Real_imgからDの勾配を求める。このとき教師は1とする
                    Loss_D_real = self.model_D.loss_D                        #Dの損失関数(ログ用)
                    
                    grads_D_fake = self.model_D.backward(fake_img, t_zeros)  #Fake_imgからDの勾配を求める。このとき教師は0とする
                    Loss_D_fake = self.model_D.loss_D                        #Dの損失関数(ログ用)

                    #勾配を合算して最適化を実行
                    grads_D = {}
                    for key in grads_D_real.keys():       
                        grads_D[key] = grads_D_real[key] + grads_D_fake[key]

                    self.optimizer_D.update(self.model_D.params, grads_D)


                    #損失関数のログを格納
                    loss_G_array = np.concatenate((loss_G_array, np.atleast_1d(Loss_G)))
                    loss_D_real_array = np.concatenate((loss_D_real_array, np.atleast_1d(Loss_D_real)))
                    loss_D_fake_array = np.concatenate((loss_D_fake_array, np.atleast_1d(Loss_D_fake)))

                    #tqdm プログレスバー表示
                    pbar.set_postfix(OrderedDict(Loss_G=Loss_G, Loss_D_real=Loss_D_real, Loss_D_fake=Loss_D_fake))
                

            #10epochsごとに64枚(8*8)の画像を保存
            if ep % 10 == 0:
                z = np.random.randn(64, self.latent_dim, 1, 1)               #潜在変数 z
                fake_img = self.model_G.forward(z, train_flag=False)         #train_flagはFalse          
                savename = os.path.join(savedir, 'Generate_{}.png'.format(str(ep)))
                self.save_generate_imgs(fake_img, savename)
        
        history = [loss_G_array, loss_D_real_array, loss_D_fake_array]
        return history

    def save_generate_imgs(self, imgs, savename):        
        genarate_imgs = imgs.transpose(0,2,3,1) #N,H,W,C
        genarate_imgs = (genarate_imgs + 1) / 2            #0~1に正規化
        
        #cupy対応
        if np.__name__=='cupy':        #cupyの場合はnumpyのndarrayに変換
            genarate_imgs = np.asnumpy(genarate_imgs)

        #64枚をひとつのイメージに変換
        genarate_imgs = genarate_imgs.reshape(-1, 64, 3)   # 縦64枚に並べる
        for i in range(8):                                 # 縦8枚 横8枚に並べる
            if i == 0:
                img_tile = genarate_imgs[0:64*8]
            else:
                img_tile = npy.concatenate( [img_tile, genarate_imgs[64*8*i:64*8*(i+1)] ], axis=1 )

        plt.imshow(img_tile)
        plt.tick_params(labelbottom=False, labelleft=False, labelright=False, labeltop=False) #目盛非表示
        plt.savefig(savename)

まずは、__init__関数についてです。

self.model_GでGeneratorのクラス、self.model_DでDiscriminatorのクラスをインスタンス化しています。Generatorの潜在変数の次元は100としています。

#Model
self.model_G = Generator(self.latent_dim)
self.model_D = Discriminator()

OptimizerにはAdamを使っています。Adamのパラメータは、学習率(lr)が0.002、beta1=0.5、beta2=0.999としています。これは論文の実験で使わていたパラメータをそのまま適用しています。

#Optimizer
self.optimizer_G = Adam(lr=0.0002, beta1=0.5, beta2=0.999)
self.optimizer_D = Adam(lr=0.0002, beta1=0.5, beta2=0.999)

次に学習を行うtrain関数です。

引数は、訓練データx_train、生成画像を保存するディレクトリのパスsavedir、エポック数epochs、バッチサイズbatch_sizeです。

def train(self, x_train, savedir, epochs, batch_size=64):

次のコードは損失をログとして配列に格納しています。

loss_G_array = np.array([])
loss_D_real_array = np.array([])
loss_D_fake_array = np.array([])

ここからひとつのバッチでの学習処理をみていきます。

まずはバッチサイズ分batchの訓練データxを作ります。

そして、教師データとして、形状が(batch,1,1,1)のテンソルにすべての値が1のものt_onesと0のものt_zerosを用意します。

#訓練データ(batch)
x = x_train[idx:idx+batch_size]

#実際のbatchサイズ
batch = x.shape[0]

#教師データ 0 or 1
t_ones = np.ones((batch,1,1,1))
t_zeros = np.zeros((batch,1,1,1))

次にGeneratorの学習です。

########################################
####   Generator  backpropagation   ####
########################################

z = np.random.randn(batch, self.latent_dim, 1, 1)        #潜在変数 z
fake_img = self.model_G.forward(z)   #Generatorが生成したfake_img

self.model_D.backward(fake_img, t_ones)                  #Dは勾配のみ求める。このとき教師は1とする
Loss_G = self.model_D.loss_D         #Dの損失関数(ログ用)

#Discriminatorの勾配 model_D.dout
dout_D = self.model_D.dout
grads_G = self.model_G.backward(dout_D)                  #Gの勾配
self.optimizer_G.update(self.model_G.params, grads_G)

潜在変数zをnp.random.randn(batch, self.latent_dim, 1, 1)で生成します。Generatorの順伝播をforward関数で実行し偽物画像fake_imgを生成します。

生成した偽物画像をDiscriminatorに入力させます。この処理はDiscriminatorのbackward関数で行っています。
ここで引数の教師データには t_onesを使います。これはGenerator学習時には本物に近い画像を生成できるように教師データを正解ラベル本物(1)とするためでした。

勾配を求める際には、先にDiscriminatorから逆伝播されてきた勾配dout_Dを求めてから、 dout_DをGeneratorのbackward関数の引数に渡してGeneratorの勾配grads_Gを求めます。

Generatorの勾配grads_Gを使って、optimizerでGeneratorのパラメータを更新します。

ここまでがGeneratorの学習となります。

次にDiscriminatorの学習です。

########################################
#### Discriminator  backpropagation ####
########################################

real_img = x     #Real_img

#それぞれの勾配を計算
grads_D_real = self.model_D.backward(real_img, t_ones)   #Real_imgからDの勾配を求める。このとき教師は1とする
Loss_D_real = self.model_D.loss_D    #Dの損失関数(ログ用)

grads_D_fake = self.model_D.backward(fake_img, t_zeros)  #Fake_imgからDの勾配を求める。このとき教師は0とする
Loss_D_fake = self.model_D.loss_D    #Dの損失関数(ログ用)

#勾配を合算して最適化を実行
grads_D = {}
for key in grads_D_real.keys():       
    grads_D[key] = grads_D_real[key] + grads_D_fake[key]

self.optimizer_D.update(self.model_D.params, grads_D)

Discriminatorの学習では訓練データxを使います。本物画像なので変数real_imgに代入しています。

Discriminatorの学習では、偽物と本物に分けて勾配と損失の算出を行います。

①Generatorが生成した画像に対する勾配と損失

grads_D_real = self.model_D.backward(real_img, t_ones)   #Real_imgからDの勾配を求める。このとき教師は1とする
Loss_D_real = self.model_D.loss_D                        #Dの損失関数(ログ用)

②訓練データ(本物画像)に対する勾配と損失

grads_D_fake = self.model_D.backward(fake_img, t_zeros)  #Fake_imgからDの勾配を求める。このとき教師は0とする
Loss_D_fake = self.model_D.loss_D                        #Dの損失関数(ログ用)

①と②の勾配を合算してoptimizerでパラメータ更新を行います。

#勾配を合算して最適化を実行
grads_D = {}
for key in grads_D_real.keys():       
    grads_D[key] = grads_D_real[key] + grads_D_fake[key]

self.optimizer_D.update(self.model_D.params, grads_D)

ここまでがDiscriminatorの学習となります。

このようにしてGeneratorとDiscriminatorの学習を交互に行っていきます。

生成画像

Generatorの学習過程で生成した画像は次のようになります。学習が進むにつれて猫の顔画像を生成できるようになっていることがわかるかと思います。

0エポック

50エポック

250エポック

-DeepLearning

Copyright© Program as Life , 2023 All Rights Reserved Powered by AFFINGER5.