Skip to content

Latest commit

 

History

History
750 lines (566 loc) · 42.4 KB

File metadata and controls

750 lines (566 loc) · 42.4 KB

六、基于 GANs 的时尚产业风格迁移

风格转移的概念是指将一个产品的风格渲染成另一个产品的过程。想象一下,你的时尚狂朋友买了一个蓝色印花的包,想要一双相似印花的鞋子来搭配它。在 2016 年之前,这可能是不可能的,除非他们是一位时装设计师的朋友,这位设计师必须先设计鞋子,然后才能获准生产。然而,随着生成对抗网络的最新进展,这种设计过程很容易实现。

生成性对抗网络是通过在生成器网络和鉴别器网络之间玩零和游戏来学习的网络。假设一个时装设计师想要设计一个特定结构的手提包,并且正在探索不同的印花。设计师可能会画出包的结构草图,然后将草图图像输入到一个生成性对抗网络中,为手提包设计出几种可能的最终图案。这种风格转移的过程可以让顾客自己绘制产品设计和图案,而不需要设计师的大量投入,从而对时尚行业产生巨大的影响。时装公司也可以通过推荐类似设计和风格的产品来补充顾客已经拥有的产品而受益。

在这个项目中,我们将构建一个智能人工智能系统,该系统将生成与给定手提包风格相似的鞋子,反之亦然。我们之前讨论的普通 GAN 不足以实现这个项目;我们需要的是 GAN 的定制版本,比如 DiscoGAN 和 CycleGAN。

在本章中,我们将涵盖以下主题:

  • 我们将讨论 DiscoGAN 背后的工作原理和数学基础
  • 我们将比较 DiscoGAN 和 CycleGAN,它们在架构和工作原理上非常相似
  • 我们将训练一个 DiscoGAN,学会从一些给定的包的草图中生成包的图像
  • 最后,我们将讨论与培训 DiscoGAN 相关的复杂性

技术要求

读者应该具备 Python 3 和人工智能的基本知识,才能完成本章中的项目。

本章代码文件可在 GitHub: https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 04上找到

查看以下视频,查看正在运行的代码: http://bit.ly/2CUZLQb

不安

A DiscoGAN 是一个生成性对抗网络,在给定域 a 中的图像的情况下,生成域 B 中产品的图像。下图所示为 DiscoGAN 网络的体系结构图:

Figure 4.1: Architectural diagram of a DiscoGAN

在域 B 中生成的图像在样式和图案上都类似于域 A 中的图像。无需在训练期间显式配对来自两个域的图像,就可以学习这种关系。鉴于物品配对是一项耗时的任务,这是一项相当强大的功能。在高层次上,它试图以神经网络的形式学习两个生成器函数GABT3】和GBAT7】,使得图像xAT11】在通过生成器GABT15】馈入时,产生在域 b 中看起来逼真的图像xABT19】此外, 当这个图像 x AB 通过另一个发电机网络 G BA 馈入时,它应该产生一个图像xABAT31】,理想情况下应该与原始图像xAT35】相同。 关于生成器函数,以下关系应该成立:******

然而,在实践中,发生器函数 G ABG BA 不可能是彼此的逆函数,因此我们通过选择 L1 或 L2 范数损失来尽可能地最小化重建图像和原始图像之间的损失。L1 赋范损失基本上是每个数据点的绝对误差之和,而 L2 赋范损失代表每个数据点的平方损失之和。我们可以将单个图像的 L2 范数损失表示如下:

仅仅最小化前面的损失是不够的。我们必须确保创建的图像 x B 在域 B 中看起来是真实的。例如,如果我们将域 A 中的衣服映射到域 B 中的鞋子,我们必须确保xBT7】类似于鞋子。域 B 侧的鉴别器 D B 将检测到 x B 为假的,如果图像作为鞋不够逼真,因此也必须考虑与此相关的损失。一般来说,在训练过程中,鉴别器既被馈送生成的图像xAB= GAB(xA)又被馈送域 B 中的原始图像,我们在这里选择用 y B 来表示,这样它就学会了将真实图像与虚假图像进行分类。你可能还记得,在一个 GAN 中,生成器和鉴别器互相玩一个零和极小极大游戏*,以不断变好,直到达到平衡。如果假图像看起来不够逼真,鉴别器会对其进行处罚,这意味着生成器必须学习在给定输入图像xAT37】的情况下生成更好的图像xABT33】。考虑到所有这些,我们可以将我们想要最小化的发生器的损耗公式化为重构损耗和关于鉴别器识别 x AB 为假的损耗。第二个损失将试图使生成器在域 B 中产生逼真的图像。将域 A 中的图像 x A 映射到域 B 中的图像的生成器损失可以表示如下:***

L2 准则下的重建损失可表示如下:

由于我们处理的是图像,我们可以假设 x A 是所有像素的展平向量,以符合 L2 范数术语。如果我们假设 x A 是一个矩阵,那么最好将称为弗罗贝纽斯范数。然而,这些只是数学术语,本质上我们只是取原始图像和重建图像之间像素值差异的平方和。

让我们考虑一下发生器在追求使变换后的图像 x AB 在鉴别器看来是真实的过程中试图最小化的成本。鉴别器会试图将图像标记为假图像,因此生成器 G AB 应该产生 x AB ,使得其作为假图像的对数损失尽可能小。如果域 B 中的鉴别器DBT18】将真实图像标记为1,将虚假图像标记为0,则图像真实的概率由 D B (。),那么发电机就要让 x AB 在鉴频器网络下极有可能,让DB(xB)= DB(GAB(xA)尽量靠近1。日志丢失方面,生成器要尽量减少前面概率的负日志,基本上给我们CD(AB)T42】,如下图:**

结合*(3)**(4)*,我们可以得到将一幅图像从域 A 映射到域 B 的总生成器开销 C_G AB ,如下图:

最大的问题是,我们就此打住吗?由于我们有来自两个域的图像,为了获得更好的映射,我们也可以从域 B 获取图像,并通过生成器 G BA 将其映射到域 A。如果我们在域 B 中取一个图像 x B ,通过生成器GBAT15】将其转换为图像xBA,域 A 的鉴别器由DAT19】给出,那么与这样的转换相关联的代价函数由下式给出:**

如果我们对这两个域中的全部图像进行求和,发电机损耗将由 (5) 和*(6)*的总和给出,如下所示:

现在,让我们建立鉴别器试图最小化的成本函数,以建立零和最小/最大游戏。每个域中的鉴别器将尝试区分真实图像和虚假图像,因此鉴别器GBT3】将尝试最小化成本C _ DBT7】,如下所示:**

类似地,鉴别器DAT3】会尝试最小化成本C _ DAT7】,如下所示:**

结合*(8)**(9)*整体鉴频器成本由 C D 给出,如下:

如果我们将GABT3】、GBAT7】、DAT11】和DBT15】的参数表示为,那么网络的优化参数可以表示如下:**

对代价函数进行随机梯度下降,如 Adam,以达到最优解。请注意,如前所述,生成性对抗网络的解决方案是关于被优化的成本函数的鞍点。

-你好

A CycleGAN 基本上类似于 DiscoGAN,只是做了一个小小的修改。在 CycleGAN 中,我们可以灵活地确定重建损失相对于 GAN 损失或鉴别器损失的权重。此参数有助于根据手头的问题以正确的比例平衡损失,以帮助网络在训练时更快地收敛。CycleGAN 的其余实现与 DiscoGAN 相同。

学习从草图轮廓生成自然手袋

在这一章中,我们将从草图轮廓生成手提包,而不使用使用 DiscoGAN 的显式配对。我们将草图图像表示为属于域 A,而将自然手提包图像表示为属于域 B。将有两个生成器:一个获取域 A 的图像并将它们映射到在域 B 下看起来真实的图像,另一个则相反:一个将手提包图像从域 B 映射到在域 A 下看起来真实的图像。鉴别器将尝试从每个域中的真实图像中识别生成器生成的假图像。生成器和鉴别器会互相进行一个极大极小零和游戏。

为了训练这个网络,我们将需要两套图像,草图,或手袋的轮廓和手袋的自然图像。图片可从以下链接下载:https://people . eecs . Berkeley . edu/~ ting huiz/projects/pix2pix/dataset/edge S2 手袋. tar.gz

在接下来的几节中,我们将介绍在 TensorFlow 中定义 DiscoGAN 网络的过程,然后训练它使用手提包草图生成逼真的手提包图像,这些草图充当图像的边缘。我们将从定义发电机网络的架构开始。

预处理图像

edges2handbags数据集文件夹中的每个图像都包含同一图像中bag的图片和bag edges的图片。为了训练网络,我们需要将它们分离为属于两个域 A 和 B 的图像,我们已经在 DiscoGAN 的体系结构中讨论过这两个域。通过使用以下代码(image_split.py),可以将映像分为域 A 和域 B 映像:

# -*- coding: utf-8 -*-
"""
Created on Fri Apr 13 00:10:12 2018

@author: santanu
"""

import numpy as np
import os
from scipy.misc import imread
from scipy.misc import imsave
import fire
from elapsedtimer import ElapsedTimer
from pathlib import Path
import shutil 
'''
Process the images in Domain A and Domain and resize appropriately
Inputs contain the Domain A and Domain B image in the same image
This program will break them up and store them in their respecective folder

'''

def process_data(path,_dir_):
    os.chdir(path)
    try: 
        os.makedirs('trainA')
    except:
        print(f'Folder trainA already present, cleaning up and recreating empty folder trainA')
        try:
            os.rmdir('trainA')
        except:
            shutil.rmtree('trainA')

        os.makedirs('trainA')

    try: 
        os.makedirs('trainB')
    except:
        print(f'Folder trainA already present, cleaning up and recreating empty folder trainB')
        try:
            os.rmdir('trainB')
        except:
            shutil.rmtree('trainB')
        os.makedirs('trainB')
    path = Path(path) 
    files = os.listdir(path /_dir_)
    print('Images to process:', len(files))
    i = 0
    for f in files:
        i+=1 
        img = imread(path / _dir_ / str(f))
        w,h,d = img.shape
        h_ = int(h/2)
        img_A = img[:,:h_]
        img_B = img[:,h_:]
        imsave(f'{path}/trainA/{str(f)}_A.jpg',img_A)
        imsave(f'{path}/trainB/{str(f)}_B.jpg',img_A)
        if ((i % 10000) == 0 & (i >= 10000)):
            print(f'the number of input images processed : {i}')
    files_A = os.listdir(path / 'trainA')
    files_B = os.listdir(path / 'trainB')
    print(f'No of images written to {path}/trainA is {len(files_A)}')
    print(f'No of images written to {path}/trainA is {len(files_B)}')

with ElapsedTimer('process Domain A and Domain B Images'):
    fire.Fire(process_data)

image_split.py代码可以如下调用:

python image_split.py --path /media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/edges2handbags/ --_dir_ train

输出日志如下:

Folder trainA already present, cleaning up and recreating empty folder trainA
Folder trainA already present, cleaning up and recreating empty folder trainB
Images to process: 138569 the number of input images processed : 10000
the number of input images processed : 20000
the number of input images processed : 30000

.....

迪厅的发电机

DiscoGAN 的生成器是前馈卷积神经网络,其中输入和输出是图像。在网络的第一部分,图像在空间维度上按比例缩小,而输出要素图的数量随着图层的进展而增加。在网络的第二部分,图像沿空间维度按比例放大,而输出要素图的数量逐层减少。在最终输出图层中,将生成与输入具有相同空间维度的图像。如果将图像xAT3】转换为xABT7】从域 A 转换为域 B 的生成器用GABT15】表示,那么我们就有了**

这里展示的是build_generator函数,我们可以用它来构建 DiscoGAN 网络的生成器:

def build_generator(self,image,reuse=False,name='generator'):
    with tf.variable_scope(name):
        if reuse:
            tf.get_variable_scope().reuse_variables()
        else:
            assert tf.get_variable_scope().reuse is False

            """U-Net generator"""
        def lrelu(x, alpha,name='lrelu'):
            with tf.variable_scope(name):
                return tf.nn.relu(x) - alpha * tf.nn.relu(-x)

    """Layers used during downsampling"""
        def common_conv2d(layer_input,filters,f_size=4,
                          stride=2,padding='SAME',norm=True,
                          name='common_conv2d'):
            with tf.variable_scope(name):
                if reuse:
                    tf.get_variable_scope().reuse_variables()

                else:
                    assert tf.get_variable_scope().reuse is False

                d = 
               tf.contrib.layers.conv2d(layer_input,filters,
                                        kernel_size=f_size,
                                        stride=stride,padding=padding)
                if norm:
                    d = tf.contrib.layers.batch_norm(d)

                d = lrelu(d,alpha=0.2)
                return d

         """Layers used during upsampling"""

       def common_deconv2d(layer_input,filters,f_size=4,
                           stride=2,padding='SAME',dropout_rate=0,
                           name='common_deconv2d'):
            with tf.variable_scope(name):
                if reuse:
                    tf.get_variable_scope().reuse_variables()

                else:
                    assert tf.get_variable_scope().reuse is False

                u = 
                tf.contrib.layers.conv2d_transpose(layer_input,
                                                   filters,f_size,
                                                   stride=stride,
                                                   padding=padding)
                if dropout_rate:
                    u = tf.contrib.layers.dropout(u,keep_prob=dropout_rate)

                u = tf.contrib.layers.batch_norm(u)
                u = tf.nn.relu(u)
                return u 

        # Downsampling
        #  64x64 -> 32x32
        dwn1 = common_conv2d(image,self.gf,stride=2,norm=False,name='dwn1') 
        #  32x32 -> 16x16
       dwn2 = common_conv2d(dwn1,self.gf*2,stride=2,name='dwn2')           
        #  16x16   -> 8x8
       dwn3 = common_conv2d(dwn2,self.gf*4,stride=2,name='dwn3')           
        #  8x8   -> 4x4 
       dwn4 = common_conv2d(dwn3,self.gf*8,stride=2,name='dwn4')            
        #  4x4   -> 1x1 
       dwn5 = common_conv2d(dwn4,100,stride=1,padding='valid',name='dwn5') 

        # Upsampling
        #  4x4    -> 4x4
        up1 = 
       common_deconv2d(dwn5,self.gf*8,stride=1,
                       padding='valid',name='up1')      
        #  4x4    -> 8x8
        up2 = common_deconv2d(up1,self.gf*4,name='up2')                  
        #  8x8    -> 16x16
        up3 = common_deconv2d(up2,self.gf*2,name='up3')                  
        #  16x16    -> 32x32 
        up4 = common_deconv2d(up3,self.gf,name='up4')                    
       out_img = tf.contrib.layers.conv2d_transpose(up4,self.channels,
                                                    kernel_size=4,stride=2,                                                                                                                                      
                                                    padding='SAME',
                                                    activation_fn=tf.nn.tanh) 
       # 32x32 -> 64x64
        return out_img

在生成器函数中,我们定义了一个泄漏的 ReLU 激活函数,并使用了一个泄漏因子0.2。我们还定义了卷积层生成函数common_conv2d,用于对图像进行下采样,以及common_deconv2d,用于将下采样图像上采样到其原始空间维度。

我们使用tf.get_variable_scope().reuse_variables()reuse选项定义生成器函数。当同一生成器函数被多次调用时,重用选项确保我们重用特定生成器使用的相同变量。当我们删除重用选项时,我们为生成器创建了一组新的变量。

例如,我们可以使用生成器函数来创建两个生成器网络,因此在第一次创建这些网络时,我们不会使用reuse选项。如果再次引用该生成器函数,我们使用reuse选项。卷积(下采样)和反卷积(上采样)过程中的激活函数是泄漏 ReLU,在此之前进行批处理归一化,以实现稳定和快速的收敛。

网络中不同图层的输出要素图数量为self.gf 或其倍数。对于我们的 DiscoGAN 网络,我们选择self.gf作为64

发生器中需要注意的一点是输出层的tanh激活功能。这确保了由发生器产生的图像的像素值将在[-1, +1]的范围内。这使得输入图像具有在[-1, +1]范围内的像素强度变得重要,这可以通过对像素强度进行简单的逐元素变换来完成,如下所示:

同样,要将图像转换为可显示的 0-255 像素强度格式,我们需要应用逆变换,如下所示:

不适者的歧视性

DiscoGAN 的辨别者将学会在特定领域区分真实图像和虚假图像。我们将有两个鉴别器:一个用于域 A,一个用于域 b。鉴别器也是可以执行二进制分类的卷积网络。与传统的基于分类的卷积网络不同,鉴别器没有任何完全连接的层。使用步长为 2 的卷积对输入图像进行下采样,直到最后一层,输出为 1 x 1。同样,我们使用泄漏 ReLU 作为激活函数,并使用批处理标准化来实现稳定快速的收敛。以下代码显示了 TensorFlow 中的鉴别器构建函数实现:

def build_discriminator(self,image,reuse=False,name='discriminator'):
    with tf.variable_scope(name):
        if reuse:
            tf.get_variable_scope().reuse_variables()
        else:
            assert tf.get_variable_scope().reuse is False

        def lrelu(x, alpha,name='lrelu'):

            with tf.variable_scope(name):
                if reuse:
                    tf.get_variable_scope().reuse_variables()
                else:
                    assert tf.get_variable_scope().reuse is False

            return tf.nn.relu(x) - alpha * tf.nn.relu(-x)

                """Discriminator layer"""

        def d_layer(layer_input,filters,f_size=4,stride=2,norm=True,
                    name='d_layer'):
            with tf.variable_scope(name):
                if reuse:
                    tf.get_variable_scope().reuse_variables()
                else:
                    assert tf.get_variable_scope().reuse is False

                d = 
                tf.contrib.layers.conv2d(layer_input,
                                         filters,kernel_size=f_size,
                                         stride=2, padding='SAME')
                if norm:
                    d = tf.contrib.layers.batch_norm(d)

                d = lrelu(d,alpha=0.2)
                return d

        #64x64 -> 32x32        
        down1 = d_layer(image,self.df, norm=False,name='down1')  
        #32x32 -> 16x16
        down2 = d_layer(down1,self.df*2,name='down2')         
        #16x16 -> 8x8
        down3 = d_layer(down2,self.df*4,name='down3')         
        #8x8 -> 4x4
        down4 = d_layer(down3,self.df*8,name='down4')        
        #4x4 -> 1x1
        down5  = 
       tf.contrib.layers.conv2d(down4,1,kernel_size=4,stride=1,
                                padding='valid')

        return down5

鉴别器网络的不同层中的输出特征图的数量或者是self.df或者是它的倍数。为了我们的网络,我们把self.df当成了64

构建网络并定义成本函数

在本节中,我们将使用生成器和鉴别器函数构建整个网络,并定义在训练过程中要优化的成本函数。张量流代码如下:

def build_network(self):
    def squared_loss(y_pred,labels):
        return tf.reduce_mean((y_pred - labels)**2)
   def abs_loss(y_pred,labels):
        return tf.reduce_mean(tf.abs(y_pred - labels))  
   def binary_cross_entropy_loss(logits,labels):
        return tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(
                                        labels=labels,logits=logits))
     self.images_real = tf.placeholder(tf.float32,[None,self.image_size,self.image_size,self.input_dim + self.output_dim])

    self.image_real_A = self.images_real[:,:,:,:self.input_dim]
    self.image_real_B = 
    self.images_real[:,:,:,self.input_dim:self.input_dim + self.output_dim]
    self.images_fake_B = 
    self.build_generator(self.image_real_A,
                         reuse=False,name='generator_AB')
    self.images_fake_A = 
    self.build_generator(self.images_fake_B,
                         reuse=False,name='generator_BA')
    self.images_fake_A_ = 
    self.build_generator(self.image_real_B,
                         reuse=True,name='generator_BA')
    self.images_fake_B_ = 
    self.build_generator(self.images_fake_A_,
                         reuse=True,name='generator_AB')

    self.D_B_fake = 
    self.build_discriminator(self.images_fake_B ,
                             reuse=False, name="discriminatorB")
    self.D_A_fake = 
    self.build_discriminator(self.images_fake_A_,
                             reuse=False, name="discriminatorA") 

    self.D_B_real = 
    self.build_discriminator(self.image_real_B,
                             reuse=True, name="discriminatorB")
    self.D_A_real = 
    self.build_discriminator(self.image_real_A,
                             reuse=True, name="discriminatorA")
    self.loss_GABA = 
    self.lambda_l2*squared_loss(self.images_fake_A,self.image_real_A) +
    binary_cross_entropy_loss(labels=tf.ones_like(self.D_B_fake),
    logits=self.D_B_fake)
    self.loss_GBAB = 
    self.lambda_l2*squared_loss(self.images_fake_B_,
    self.image_real_B) + 
    binary_cross_entropy_loss(labels=tf.ones_like(self.D_A_fake),
    logits=self.D_A_fake)
    self.generator_loss = self.loss_GABA + self.loss_GBAB

    self.D_B_loss_real =     
    binary_cross_entropy_loss(tf.ones_like(self.D_B_real),self.D_B_real)
    self.D_B_loss_fake = 
    binary_cross_entropy_loss(tf.zeros_like(self.D_B_fake),self.D_B_fake)
    self.D_B_loss = (self.D_B_loss_real + self.D_B_loss_fake) / 2.0

    self.D_A_loss_real = 
    binary_cross_entropy_loss(tf.ones_like(self.D_A_real),self.D_A_real)
    self.D_A_loss_fake = 
    binary_cross_entropy_loss(tf.zeros_like(self.D_A_fake),self.D_A_fake)
    self.D_A_loss = (self.D_A_loss_real + self.D_A_loss_fake) / 2.0

    self.discriminator_loss = self.D_B_loss + self.D_A_loss
    self.loss_GABA_sum = tf.summary.scalar("g_loss_a2b", self.loss_GABA)
    self.loss_GBAB_sum = tf.summary.scalar("g_loss_b2a", self.loss_GBAB)
    self.g_total_loss_sum = tf.summary.scalar("g_loss", self.generator_loss)
    self.g_sum = tf.summary.merge([self.loss_GABA_sum,
                                   self.loss_GBAB_sum,self.g_total_loss_sum])
    self.loss_db_sum = tf.summary.scalar("db_loss", self.D_B_loss)
    self.loss_da_sum = tf.summary.scalar("da_loss", self.D_A_loss)
    self.loss_d_sum = tf.summary.scalar("d_loss",self.discriminator_loss)

    self.db_loss_real_sum = tf.summary.scalar("db_loss_real", self.D_B_loss_real)
    self.db_loss_fake_sum = tf.summary.scalar("db_loss_fake", self.D_B_loss_fake)
    self.da_loss_real_sum = tf.summary.scalar("da_loss_real", self.D_A_loss_real)
    self.da_loss_fake_sum = tf.summary.scalar("da_loss_fake", self.D_A_loss_fake)
    self.d_sum = tf.summary.merge(
            [self.loss_da_sum, self.da_loss_real_sum, self.da_loss_fake_sum,
             self.loss_db_sum, self.db_loss_real_sum, self.db_loss_fake_sum,
             self.loss_d_sum]
        )

    trainable_variables = tf.trainable_variables()
    self.d_variables = 
    [var for var in trainable_variables if 'discriminator' in var.name]
    self.g_variables =
    [var for var in trainable_variables if 'generator' in var.name]

    print ('Variable printing start :'  )
    for var in self.d_variables: 
        print(var.name)

    self.test_image_A = 
    tf.placeholder(tf.float32,[None, self.image_size,
                   self.image_size,self.input_dim], name='test_A')
    self.test_image_B =
    tf.placeholder(tf.float32,[None, self.image_size,
                   self.image_size,self.output_c_dim], name='test_B')
    self.saver = tf.train.Saver()

在构建网络中,我们首先定义了 L2 赋范误差和二元交叉熵误差的代价函数。L2 赋范误差将用作重构损失,而二进制交叉熵将用作鉴别器损失。然后,我们使用生成器函数为两个域中的图像定义占位符,并为每个域中的假图像定义张量流运算。我们还通过传递特定于该域的假图像和真实图像来定义鉴别器输出的 ops。除此之外,我们还为这两个域中的重建图像定义了张量流运算。

一旦定义了 ops,我们使用它们来计算损失函数,考虑图像的重建损失和归因于鉴别器的损失。请注意,我们使用了相同的生成器函数来定义从域 A 到域 B 的生成器,以及从域 B 到域 A 的生成器。我们唯一不同的做法是为两个网络提供不同的名称:generator_ABgenerator_BA。由于变量范围被定义为name,这两个生成器将有不同的以提供的名称为前缀的权重集。

下表显示了我们需要跟踪的不同损失变量。相对于发生器或鉴别器的参数,所有这些损失都需要最小化:

| 不同损失的变量 | 描述 | | self.D_B_loss_real | 鉴别器DBT3】对 B 域真实图像进行分类时的二值交叉熵损失(This loss is to be minimized with respect to the parameters of the discriminator DB.) | | self.D_B_loss_fake | 鉴别器DBT3】二值交叉熵损失在 B 域伪图像分类中的应用(This loss is to be minimized with respect to the parameters of the discriminator DB.) | | self.D_A_loss_real | 鉴别器DAT3】在对域 A 中的真实图像进行分类时的二值交叉熵损失(This loss is to be minimized with respect to the parameters of the discriminator DA.) | | self.D_A_loss_fake | 鉴别器DAT3】二值交叉熵损失在分类域 A 中的伪图像(This loss is to be minimized with respect to the parameters of the discriminator DA.) | | self.loss_GABA | 通过两个生成器 G ABG BA 将域 A 中的图像映射到域 B 再映射回域 A 的重建损失)加上域 B 中鉴别器标记为真实图像的伪图像*GAB(xA)的二值交叉熵(This loss is to be minimized with respect to the parameters of the generators GAB and GBA.) | | self.loss_GBAB | 通过两个生成器 G BAG AB 加上伪图像GBA(xB)*的二值交叉熵将域 B 中的图像映射到 A 再映射回 B 的重构损失被域 A 中的鉴别器标记为真实图像(This loss is to be minimized with respect to the parameters of the generators GAB and GBA.) |

前四个损耗组成鉴别器损耗,相对于鉴别器DAT3】和DBT7】的参数需要最小化。最后两个损耗组成发电机损耗,相对于发电机 G ABG BA 的参数需要最小化。**

损失变量通过tf.summary.scaler 与 TensorBoard 关联,以便在训练过程中监控这些损失,确保损失以期望的方式减少。稍后,我们将在训练过程中看到这些损失痕迹在 TensorBoard 中的样子。

构建培训流程

train_network函数中,我们首先为发生器和鉴别器损失函数定义优化器。我们将 Adam 优化器用于生成器和鉴别器,因为这是随机梯度下降优化器的高级版本,在训练 GANs 时非常有效。Adam 使用梯度的衰减平均值,很像稳定梯度的动量,以及平方梯度的衰减平均值,提供了关于成本函数曲率的信息。与tf.summary定义的不同损耗相关的变量被写入日志文件,因此可以通过 TensorBoard 进行监控。以下是train功能的详细代码:

def train_network(self):

        self.learning_rate = tf.placeholder(tf.float32)
        self.d_optimizer = tf.train.AdamOptimizer(self.learning_rate,beta1=self.beta1,beta2=self.beta2).minimize(self.discriminator_loss,var_list=self.d_variables)
        self.g_optimizer = tf.train.AdamOptimizer(self.learning_rate,beta1=self.beta1,beta2=self.beta2).minimize(self.generator_loss,var_list=self.g_variables) 

        self.init_op = tf.global_variables_initializer()
        self.sess = tf.Session()
        self.sess.run(self.init_op)
        #self.dataset_dir = '/home/santanu/Downloads/DiscoGAN/edges2handbags/train/'
        self.writer = tf.summary.FileWriter("./logs", self.sess.graph)
        count = 1
        start_time = time.time()

        for epoch in range(self.epoch):
            data_A = os.listdir(self.dataset_dir + 'trainA/')
            data_B = os.listdir(self.dataset_dir + 'trainB/')
            data_A = [ (self.dataset_dir + 'trainA/' + str(file_name)) for file_name in data_A ] 

            data_B = [ (self.dataset_dir + 'trainB/' + str(file_name)) for file_name in data_B ] 
            np.random.shuffle(data_A)
            np.random.shuffle(data_B)
            batch_ids = min(min(len(data_A), len(data_B)), self.train_size) // self.batch_size
            lr = self.l_r if epoch < self.epoch_step else self.l_r*(self.epoch-epoch)/(self.epoch-self.epoch_step)

            for id_ in range(0, batch_ids):
                batch_files = list(zip(data_A[id_ * self.batch_size:(id_ + 1) * self.batch_size],
                                      data_B[id_ * self.batch_size:(id_ + 1) * self.batch_size]))
                batch_images = [load_train_data(batch_file, self.load_size, self.fine_size) for batch_file in batch_files]
                batch_images = np.array(batch_images).astype(np.float32)

                    # Update G network and record fake outputs
                fake_A, fake_B, _, summary_str = self.sess.run(
                        [self.images_fake_A_,self.images_fake_B,self.g_optimizer,self.g_sum],
                        feed_dict={self.images_real: batch_images, self.learning_rate:lr})
                self.writer.add_summary(summary_str, count)
                [fake_A,fake_B] = self.pool([fake_A, fake_B])

                    # Update D network
                _, summary_str = self.sess.run(
                        [self.d_optimizer,self.d_sum],
                        feed_dict={self.images_real: batch_images,
                               # self.fake_A_sample: fake_A,
                               # self.fake_B_sample: fake_B,
                                   self.learning_rate: lr})
                self.writer.add_summary(summary_str, count)

                count += 1
                print(("Epoch: [%2d] [%4d/%4d] time: %4.4f" % (
                        epoch, id_, batch_ids, time.time() - start_time)))

                if count % self.print_freq == 1:
                    self.sample_model(self.sample_dir, epoch, id_)

                if count % self.save_freq == 2:
                    self.save_model(self.checkpoint_dir, count)

正如我们在代码末尾看到的那样,sample_model函数在训练过程中不时被调用,以检查基于来自另一个域的输入图像在一个域中生成的图像的质量。基于save_freq,模型也会定期保存。

我们在前面的代码中提到的sample_model功能和save_model功能在这里举例说明,以供参考:

def sample_model(self, sample_dir, epoch, id_):
    if not os.path.exists(sample_dir):
        os.makedirs(sample_dir)
    data_A = os.listdir(self.dataset_dir + 'trainA/')
    data_B = os.listdir(self.dataset_dir + 'trainB/') 
    data_A = [ (self.dataset_dir + 'trainA/' + str(file_name)) for 
              file_name in data_A ]
    data_B = [ (self.dataset_dir + 'trainB/' + str(file_name)) for 
              file_name in data_B ]
   np.random.shuffle(data_A)
    np.random.shuffle(data_B)
    batch_files = 
    list(zip(data_A[:self.batch_size], data_B[:self.batch_size]))
    sample_images = 
    [load_train_data(batch_file, is_testing=True) for 
     batch_file in batch_files]
    sample_images = np.array(sample_images).astype(np.float32)

    fake_A, fake_B = self.sess.run(
            [self.images_fake_A_,self.images_fake_B],
            feed_dict={self.images_real: sample_images}
        )
    save_images(fake_A, [self.batch_size, 1],
                    './{}/A_{:02d}_{:04d}.jpg'.format(sample_dir, epoch, id_))
    save_images(fake_B, [self.batch_size, 1],
                    './{}/B_{:02d}_{:04d}.jpg'.format(sample_dir, epoch, id_))

在这个sample_model功能中,从域 A 中随机选择的图像被拍摄并馈送到生成器 G AB 以产生域 B 中的图像。类似地,从域 B 中随机选择的图像被馈送到生成器GBAT8】以产生域 A 中的图像。这些输出图像由两个生成器在不同的时期生成,并且批次被保存在样本文件夹中,以查看生成器在训练过程中是否随着时间的推移而改进以产生更好的图像质量。

使用 TensorFlow 保存功能保存模型的save_model功能如下所示:

def save_model(self,checkpoint_dir,step):
    model_name = "discogan.model"
    model_dir = "%s_%s" % (self.dataset_dir, self.image_size)
    checkpoint_dir = os.path.join(checkpoint_dir, model_dir)

    if not os.path.exists(checkpoint_dir):
        os.makedirs(checkpoint_dir)

    self.t(self.sess,
                    os.path.join(checkpoint_dir, model_name),
                    global_step=step)

GAN 训练的重要参数值

在本节中,我们将讨论用于训练 DiscoGAN 的不同参数值。下表列出了这些内容:

| 参数名称 | 变量名和值集 | 原理 | | 亚当优化器的学习速率 | self.l_r = 2e-4 | 我们应该始终训练一个学习率低的 GAN 网络,以获得更好的稳定性,DiscoGAN 也不例外。 | | 亚当优化器的衰减率 | self.beta1 = 0.5 self.beta2 = 0.99 | 参数beta1定义梯度的衰减平均值,而参数beta2定义梯度平方的衰减平均值。 | | 世 | self.epoch = 200 | 200在这个实现中,对于 DiscoGAN 网络的收敛来说,时代已经足够好了。 | | 批量 | self.batch_size = 64 | 64的批量对于这个实现很有效。然而,由于资源限制,我们可能不得不选择较小的批量。 | | 学习率线性下降的时期 | epoch_step = 10 | 在epoch_step指定的时代数后,学习率线性下降,由以下方案确定:lr = self.l_r if epoch < self.epoch_step else self.l_r*(self.epoch-epoch)/(self.epoch-self.epoch_step) |

调用培训

我们之前展示的所有函数都是在一个DiscoGAN()类中创建的,该类具有在__init__ 函数中声明的重要参数值,如下面的代码块所示。训练网络时唯一需要通过的两个参数是dataset_dir和需要进行训练的epochs的数量

   def __init__(self,dataset_dir,epochs=200):
        # Input shape
        self.dataset_dir = dataset_dir
        self.lambda_l2 = 1.0
        self.image_size = 64
        self.input_dim = 3
        self.output_dim = 3
        self.batch_size = 64 
        self.df = 64
        self.gf = 64
        self.channels = 3
        self.output_c_dim = 3
        self.l_r = 2e-4
        self.beta1 = 0.5
        self.beta2 = 0.99
        self.weight_decay = 0.00001
        self.epoch = epochs
        self.train_size = 10000
        self.epoch_step = 10
        self.load_size = 64
        self.fine_size = 64 
        self.checkpoint_dir = 'checkpoint'
        self.sample_dir = 'sample'
        self.print_freq = 5
        self.save_freq = 10 
        self.pool = ImagePool()

        return None

现在我们已经定义了训练模型所需的所有内容,我们可以通过process_main 函数调用训练,如下所示:

   def process_main(self):
        self.build_network()
        self.train_network()

我们之前为培训演示的端到端代码在脚本cycledGAN_edges_to_bags.py中。我们可以通过运行 python 脚本cycledGAN_edges_to_bags.py来训练模型,如下所示:

python cycledGAN_edges_to_bags.py process_main  --dataset_dir /media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/edges2handbags/ epochs 100

脚本cycledGAN_edges_to_bags.py执行的输出日志如下:

Epoch: [ 0] [ 0/ 156] time: 3.0835
Epoch: [ 0] [ 1/ 156] time: 3.9093
Epoch: [ 0] [ 2/ 156] time: 4.3661
Epoch: [ 0] [ 3/ 156] time: 4.8208
Epoch: [ 0] [ 4/ 156] time: 5.2821
Epoch: [ 0] [ 5/ 156] time: 6.2380
Epoch: [ 0] [ 6/ 156] time: 6.6960
Epoch: [ 0] [ 7/ 156] time: 7.1528
Epoch: [ 0] [ 8/ 156] time: 7.6138
Epoch: [ 0] [ 9/ 156] time: 8.0732
Epoch: [ 0] [ 10/ 156] time: 8.8163
Epoch: [ 0] [ 11/ 156] time: 9.6669
Epoch: [ 0] [ 12/ 156] time: 10.1256
Epoch: [ 0] [ 13/ 156] time: 10.5846
Epoch: [ 0] [ 14/ 156] time: 11.0427
Epoch: [ 0] [ 15/ 156] time: 11.9135
Epoch: [ 0] [ 16/ 156] time: 12.3712
Epoch: [ 0] [ 17/ 156] time: 12.8290
Epoch: [ 0] [ 18/ 156] time: 13.2899
Epoch: [ 0] [ 19/ 156] time: 13.7525
.......

监控发生器和鉴别器损耗

损失可以在 TensorBoard 仪表板中监控。可以按如下方式调用 TensorBoard 仪表板:

  1. 从终端运行以下命令:
 tensorboard --logdir=./logs

./logs是存储程序专用的张量板日志的目的地,应在程序中定义如下:

      self.writer = tf.summary.FileWriter("./logs", self.sess.graph)
  1. 一旦执行了步骤 1 中的命令,导航到 TensorBoard 的localhost:6006站点:

下面的截图显示了在项目中实施的 DiscoGAN 培训期间,TensorBoard 中看到的发生器和鉴别器损耗的一些痕迹:

Figure 4.2: Tensorboard Scalars section containing the traces for different losses

以下屏幕截图显示了随着培训的进行,域 A 中鉴别器的损耗成分:

Figure 4.3: Losses of discriminator in domain A

从前面的截图中,我们可以看到不同批次的域 A 中鉴别器的损失。da_lossda_loss_realda_loss_fake 损失的总和*。*da_loss_real稳定下降,因为鉴别器很容易学会在域 A 中识别真实图像,而伪图像的损失稳定在 0.69 左右,这是当二进制分类器输出 1/2 概率的类时可以预期的logloss。发生这种情况是因为生成器也在同时学习使假图像看起来真实,因此使得鉴别器很难容易地将生成器图像分类为假图像。在域 B 的鉴别器的损失概况看起来类似于在域 a 的上一个截图中说明的那些

现在让我们看看发电机的损耗曲线,如下所示:

Figure 4.4: Loss profiles for the generators of the DiscoGAN

g_loss_a2b 是从域 A 到域 B 再到域 B 重建图像的组合生成器损失,以及与使变换后的图像在域 B 中看起来逼真相关联的二进制交叉熵损失。类似地, g_loss_b2a是从域 B 到域 A 再到域 B 重建图像的组合发生器损失,也是与使转换后的图像在域 A 中看起来真实相关的二进制交叉熵损失。正如我们从上一个截图中的张量板视觉效果中看到的,这两个损失分布以及它们的总和g_loss随着批次的进行而稳步减少。

因为训练生成性对抗网络通常是相当棘手的,所以监控它们的损失情况的进展以了解训练是否按预期进行是有意义的。

DiscoGAN 生成的样本图像

当我们到达这一章的结尾时,让我们来看看由 DiscoGAN 在这两个域中生成的一些图像:

Figure 4.5: Handbag images generated given the sketches 

以下截图包含生成的手提袋草图图像(域 A) :

Figure 4.6: Sketches generated given the handbag images 

我们可以看到,DiscoGAN 在将任何一个领域的图像转换为另一个领域的高质量逼真图像方面做得很好。

摘要

我们现在已经到了这一章的结尾。现在,您应该非常熟悉 DiscoGAN 的技术细节和实现复杂性。我们在这一章中探索的概念可以用来实现各种各样的生成性对抗网络,这些网络具有适合当前问题的微妙变化。这个 DiscoGAN 网络的端到端实现位于 GitHub 存储库中,位于https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 04

第五章视频字幕应用中,我们将会看到视频到文本的翻译应用,它们属于人工智能中专家系统的范畴。