这篇博文的目的,是回顾一下已经学过的GAN、cycleGAN、starGAN,重新看一遍它们的网络架构和loss的训练步骤,以求在原理上做更清晰的理解。

我们首先看GAN

GAN的框架很清晰,有两个模块,一个是生成器G,一个是判别器D。G要生成生成数据(虚假数据)以让D判别不出来;D要判别D的输入数据,看这个数据是真实的还是生成的(虚假的)。双方不断博弈,以至于达到平衡,即D的判别概率为50%。在这里,我们需要解决一个疑惑,那就是生成器G和判别器D到底是个什么玩意。答案是,G和D都是神经网络我们把随机噪声(比如随机生成服从正态分布的噪声)输入生成器G的神经网络,经过权重和偏置,输出生成的数据我们把这些生成的数据作为判别器D的输入,输入进判别器D的神经网络,经过权重和偏置,输出判断的结果(如同mnist手写数据集那样)既然是神经网络,那么所谓的训练生成器G和判别器D,实际上也就是训练神经网络,实际上也就是进行BP罢了所以在编写代码的时候,需要单独设置生成器G,它的内容是一个神经网络;需要单独设置判别器D,它的内容也是一个神经网络

以上就是GAN的网络架构。GAN的网络架构比较简单,我们重点理解loss的设置。

# 第四步:设置判别器网络结构,先输入大师作画,返回判断真的概率,再输入造假分子的画,同样返回判为真概率
with tf.variable_scope('Discriminator'):
    """判别器与生成器不同,生成器只需要输入随机噪声就行,它无法接触到专家的画,
    如果能输入专家的画,那就不用学习了,直接导入到判别器就是0.5的概率,换句话说,
    生成器只能通过生成器的误差反馈来调节权重,使得逐渐生成逼真的画。"""
    # 输入大师的画
    real_art = tf.placeholder(tf.float32, [None, ART_COMPONENTS], name='real_in')
    # 将大师的画输入到判别器,判别器判断这副画来自于大师的概率
    D_l0 = tf.layers.dense(real_art, 128, tf.nn.relu, name='Discri')
    prob_artist0 = tf.layers.dense(D_l0, 1, tf.nn.sigmoid, name='out')
    # 之后输入造假分子的画,G_out代入到判别器中。
    D_l1 = tf.layers.dense(G_out, 128, tf.nn.relu, name='Discri', reuse=True)
    # 代入生成的画,判别器判断这副画来自于大师的概率
    prob_artist1 = tf.layers.dense(D_l1, 1, tf.nn.sigmoid, name='out', reuse=True)
    """注意到,判别器中当输入造假分子的画时,这层是可以重复利用的,
       通过动态调整这次的权重来完成判别器的loss最小,关键一步。"""

# 第五步:定义误差loss
"""对于D_loss先固定G(生成器),先让判别器学习一下大师的画,
对大师的画有了“印象”之后再去接受造假分子的画,再对D(判别器)上求V的最大值化以此来得到此时的最优解D。
由于tensorflow只支持minimize(),所以这里添加“-”号,来转化为求最大值。
对于G_loss先固定D,等同于把logD(X)的期望当作常数,所以只需要最小化后面那一部分即可"""
# 判别器Dloss,此时需同时优化两部分的概率
D_loss = -tf.reduce_mean(tf.log(prob_artist0) + tf.log(1 - prob_artist1))
# 对于生成器G的loss,此时prob_artist0是固定的,可以看到生成器并没有输入大师的画,
# 所以tf.log(prob_artist0)是一个常数,故在这里不用考虑。
G_loss = tf.reduce_mean(tf.log(1 - prob_artist1))

# 第六步:定义Train_D和Train_G
train_D = tf.train.AdamOptimizer(LR_D).minimize(
    D_loss, var_list=tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope='Discriminator'))
train_G = tf.train.AdamOptimizer(LR_G).minimize(
    G_loss, var_list=tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope='Generator'))

这是定义的判别器和loss,我们可以看到判别器的设置就是按照框图来的:设置一个输入为真实图像的loss,再设置一个输入为虚假图像的loss,然后让这个判别器的神经网络reuse=true

重点是这个loss,我们的loss严格按照公式来,我说的这个公式不一定就是论文里给的公式,像在这里,我们用的公式就是这个GAN+cycleGAN+starGAN网络结构与训练loss的个人理解-LMLPHP,但我要告诉你的是loss一定要按公式来。而且一定要G的loss和D的loss分开写,训练也是要分开训练,所以你看到上面的代码有D_loss和G_loss,有train_D和train_G。

另一个重点是这个var_list=tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope='Discriminator'),意思是:获取指定scope的tensor。也就是说获取Discriminator这个scope的tensor,毕竟用到的变量是在Discriminator这个变量空间里的。这行代码可以在训练时更新其中的变量,tf.get_collection 获取一个集合里面的所有资源,tf.GraphKeys.TRAINABLE_VARIABLES就是指定变量空间里可学习的变量(一般指神经网络中的参数)。你可能会诧异,为什么会在minimize中有var_list?其实在我们以前的代码中,只有minimize(loss),这样默认的是训练全网络的所有参数。而有的时候并不是网络中所有参数都要被训练,抑或是这一个变量代表我只想训练这半部分的参数,那一个变量代表我只想训练那部分参数。所以我们需要在minimize( )中说好,我们是对全部的参数都训练呢,还是只训练指定的参数,一旦要训练指定的参数,那么我们就必须有var_list,一个字都不能差。就比如这里的train_D,如果不写清楚var_list,就会对全部参数进行优化,这样D_loss涉及到的G_out就会被优化,而这并不是train_D的目的。

以下是在run的时候我们如何去run:

for step in range(5000):
    artist_paintings = artist_works()  # 专家的画
    G_ideas = np.random.randn(BATCH_SIZE, N_IDEAS)  # 伪造的画,输入是一堆噪声,shape=(64,15)
    G_paintings, pa0, Dl = sess.run([G_out, prob_artist0, D_loss, train_D, train_G],
                                    {G_in: G_ideas, real_art: artist_paintings})[:3]#训练和获取结果
    """最后那个[:3]的意思是G_paintings, pa0, Dl分别是G_out, prob_artist0, D_loss的运行结果"""

可以看出对于GAN,原理上G和D分开训练的,而在代码里,分开训练的代码就是把他们同时放进run里,如果对于G和D分别写run,则会在代码执行的时候出现重复交叠的地方,这样训练就会出错。所以对于GAN网络,我们一般都是用上面的这种方法。

我们接下来看cycleGAN

cycleGAN的框架很清晰,有四个模块,一个是从X类到Y类的生成器G,一个是从Y类到X类的生成器F,一个是Y类的判别器Dy,一个是X类的判别器Dx。G和F要生成生成数据(虚假数据)以让Dx、Dy判别不出来;Dx、Dy要判别Dx、Dy的输入数据,看这个数据是真实的还是生成的(虚假的)。双方不断博弈,以至于达到平衡,即Dx、Dy的判别概率为50%。在这里,我们需要解决一个疑惑,那就是生成器G、F和判别器Dx、Dy到底是个什么玩意。答案是和GAN的答案一样一样的,G、F和Dx、Dy都是神经网络我们把源图片x输入生成器G的神经网络,经过权重和偏置,输出生成图片s我们把这生成的图片s作为判别器Dy的输入,输入进判别器Dy的神经网络,经过权重和偏置,输出判断的结果(如同mnist手写数据集那样)既然是神经网络,那么所谓的训练生成器G、F和判别器Dx、Dy,实际上也就是训练神经网络,实际上也就是进行BP罢了所以在编写代码的时候,需要单独设置生成器G、F,它的内容是一个神经网络;需要单独设置判别器Dx、Dy,它的内容也是一个神经网络

cycleGAN的文件构成是这样的:

train.py 训练的主控程序

train_image_reader.py 训练数据读取接口

net.py 定义网络结构

evaluate.py 测试的主控程序

test_image_reader.py 测试数据读取接口

其中,训练时使用到的文件是train.py、train_image_reader.py、net.py,测试时使用到的文件时net.py、evaluate.py、test_image_reader.py。

和之前的GAN一样,我们不去想目录是啥,怎么提取图像,怎么预处理,我们只看网络架构和loss:

对于网络架构,我们看:

# 定义生成器
def generator(image, gf_dim=64, reuse=False, name="generator"):
    # 生成器输入尺度: 1*256*256*3,因为我们设定的是手头上的图片是256*256*3的RGB彩图,我们要把这些图片输入进生成器G
    input_dim = image.get_shape()[-1]
    with tf.variable_scope(name):
        if reuse:
            tf.get_variable_scope().reuse_variables()
        else:
            assert tf.get_variable_scope().reuse is False
        # 第1个卷积模块,输出尺度: 1*256*256*64
        c0 = relu(
            batch_norm(conv2d(input_=image, output_dim=gf_dim, kernel_size=7, stride=1, name='g_e0_c'), name='g_e0_bn'))
        # 第2个卷积模块,输出尺度: 1*128*128*128
        c1 = relu(batch_norm(conv2d(input_=c0, output_dim=gf_dim * 2, kernel_size=3, stride=2, name='g_e1_c'),
                             name='g_e1_bn'))
        # 第3个卷积模块,输出尺度: 1*64*64*256
        c2 = relu(batch_norm(conv2d(input_=c1, output_dim=gf_dim * 4, kernel_size=3, stride=2, name='g_e2_c'),
                             name='g_e2_bn'))

        # 9个残差块:
        #cycleGAN的主要目的之一是保留原始输入的特性,如对象的大小和形状。残差网络非常适合这些类型的变换
        r1 = residule_block_33(input_=c2, output_dim=gf_dim * 4, atrous=False, name='g_r1')
        r2 = residule_block_33(input_=r1, output_dim=gf_dim * 4, atrous=False, name='g_r2')
        r3 = residule_block_33(input_=r2, output_dim=gf_dim * 4, atrous=False, name='g_r3')
        r4 = residule_block_33(input_=r3, output_dim=gf_dim * 4, atrous=False, name='g_r4')
        r5 = residule_block_33(input_=r4, output_dim=gf_dim * 4, atrous=False, name='g_r5')
        r6 = residule_block_33(input_=r5, output_dim=gf_dim * 4, atrous=False, name='g_r6')
        r7 = residule_block_33(input_=r6, output_dim=gf_dim * 4, atrous=False, name='g_r7')
        r8 = residule_block_33(input_=r7, output_dim=gf_dim * 4, atrous=False, name='g_r8')
        r9 = residule_block_33(input_=r8, output_dim=gf_dim * 4, atrous=False, name='g_r9')
        # 第9个残差块的输出尺度: 1*64*64*256

        # 第1个反卷积模块,输出尺度: 1*128*128*128
        d1 = relu(batch_norm(deconv2d(input_=r9, output_dim=gf_dim * 2, kernel_size=3, stride=2, name='g_d1_dc'),
                             name='g_d1_bn'))
        # 第2个反卷积模块,输出尺度: 1*256*256*64
        d2 = relu(
            batch_norm(deconv2d(input_=d1, output_dim=gf_dim, kernel_size=3, stride=2, name='g_d2_dc'), name='g_d2_bn'))
        # 最后一个卷积模块,输出尺度: 1*256*256*3
        d3 = conv2d(input_=d2, output_dim=input_dim, kernel_size=7, stride=1, name='g_d3_c')
        # 经过tanh函数激活得到生成的输出
        output = tf.nn.tanh(d3)
        return output

就像我们说的那样,后续的GAN网络,生成器基本都是先卷积→反卷积→输出,如果网络比较深,就在卷积和反卷积之间加上几个残差网络。

# 定义判别器
def discriminator(image, df_dim=64, 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
        # 第1个卷积模块,输出尺度: 1*128*128*64
        h0 = lrelu(conv2d(input_=image, output_dim=df_dim, kernel_size=4, stride=2, name='d_h0_conv'))
        # 第2个卷积模块,输出尺度: 1*64*64*128
        h1 = lrelu(
            batch_norm(conv2d(input_=h0, output_dim=df_dim * 2, kernel_size=4, stride=2, name='d_h1_conv'), 'd_bn1'))
        # 第3个卷积模块,输出尺度: 1*32*32*256
        h2 = lrelu(
            batch_norm(conv2d(input_=h1, output_dim=df_dim * 4, kernel_size=4, stride=2, name='d_h2_conv'), 'd_bn2'))
        # 第4个卷积模块,输出尺度: 1*32*32*512
        h3 = lrelu(
            batch_norm(conv2d(input_=h2, output_dim=df_dim * 8, kernel_size=4, stride=1, name='d_h3_conv'), 'd_bn3'))
        # 最后一个卷积模块,输出尺度: 1*32*32*1
        output = conv2d(input_=h3, output_dim=1, kernel_size=4, stride=1, name='d_h4_conv')
        return output

我们看到判别器的结构就是几层卷积层

下面看loss:

    fake_y = generator(image=x_img, reuse=False, name='generator_x2y')  # 生成的y域图像
    fake_x_ = generator(image=fake_y, reuse=False, name='generator_y2x')  # 重建的x域图像
    fake_x = generator(image=y_img, reuse=True, name='generator_y2x')  # 生成的x域图像
    fake_y_ = generator(image=fake_x, reuse=True, name='generator_x2y')  # 重建的y域图像

    dy_fake = discriminator(image=fake_y, reuse=False, name='discriminator_y')  # 判别器返回的对生成的y域图像的判别结果
    dx_fake = discriminator(image=fake_x, reuse=False, name='discriminator_x')  # 判别器返回的对生成的x域图像的判别结果
    dy_real = discriminator(image=y_img, reuse=True, name='discriminator_y')  # 判别器返回的对真实的y域图像的判别结果
    dx_real = discriminator(image=x_img, reuse=True, name='discriminator_x')  # 判别器返回的对真实的x域图像的判别结果
    """这一部分就是按照原理图来的,判别器和生成器完全按照原理图的流程工作!"""
gen_loss = gan_loss(dy_fake, tf.ones_like(dy_fake)) + gan_loss(dx_fake,
                                                                   tf.ones_like(dx_fake)) + args.lamda * l1_loss(x_img,
                                                                                                                 fake_x_) + args.lamda * l1_loss(
        y_img, fake_y_)  # 计算生成器的全部loss

    dy_loss_real = gan_loss(dy_real, tf.ones_like(dy_real))  # 计算判别器判别的真实的y域图像的loss
    dy_loss_fake = gan_loss(dy_fake, tf.zeros_like(dy_fake))  # 计算判别器判别的生成的y域图像的loss
    dy_loss = (dy_loss_real + dy_loss_fake) / 2  # 计算判别器判别的y域图像的loss

    dx_loss_real = gan_loss(dx_real, tf.ones_like(dx_real))  # 计算判别器判别的真实的x域图像的loss
    dx_loss_fake = gan_loss(dx_fake, tf.zeros_like(dx_fake))  # 计算判别器判别的生成的x域图像的loss
    dx_loss = (dx_loss_real + dx_loss_fake) / 2  # 计算判别器判别的x域图像的loss

    dis_loss = dy_loss + dx_loss  # 计算判别器的全部loss

我们看出,以上内容都是按照公式来的,有了loss,我们train的时候只要minimize这个loss就可以。

gen_loss_value, dis_loss_value, _ = sess.run([gen_loss, dis_loss, train_op],
                                                         feed_dict=feed_dict)  # 得到每个step中的生成器和判别器loss

在代码中,我们通过这个来训练,因为可以看到run里面有train_op,与此同时输出了gen_loss_value和dis_loss_value。有这俩也无妨,反正我已经运行了train_op,我就已经训练网络了!OK!

最后看starGAN:

我们首先要说明的是,从原理图出发,我们可以看到starGAN和cycleGAN的不同之处:

GAN+cycleGAN+starGAN网络结构与训练loss的个人理解-LMLPHP

↑cycleGAN                                                          starGAN↓

GAN+cycleGAN+starGAN网络结构与训练loss的个人理解-LMLPHP

发现没有,starGAN的生成器G的输入不仅有input image,还有target domain,这有点像CGAN。而cycleGAN的生成器G输入只有input image。判别器输出也不是只输出判别real/fake的结果,还有domain的判别结果。

另外,starGAN的流程是一个双线程操作:

首先向生成器G输入目标domain输入图片,然后G输出的生成图片兵分两路,一方面会传送给判别器D,判别器D会判别这张图片是由生成器生成的虚假图片还是真实图片,以及domain分类是什么;另一方面这个生成图片会再次传回给这个生成器G,不过此时我们将这张生成图和这张图的原始domain作为输入,并希望输出的图片能和最初的输入图片尽可能相似。

其实整套训练流程与CycleGAN是非常相似的,不同之处在于CycleGAN使用了两个生成器G做风格的来回变换,而在StarGAN中仅使用了一个生成器G实现这一变换。

理解了这般,我们就可以在cycleGAN的基础上做starGAN的代码了。

具体的代码我们这里就先不放了,如果你搞不懂,可以按照上面讲的两个GAN网络去看starGAN的代码。

另外,我想说的是,通过原理的讲述,可以看出starGAN有CGAN的影子,主要体现在输入target domain上。其实无妨,我们只要把input image和target domain拼接在一起,放进网络就可以,你觉得别扭,那不打紧,反正根据上面的讲解,你应该能够理解神经网络的神奇,只要我把loss设立好,然后train就可以达到我们优化网络权重的目的,至于为什么input image和target domain拼接在一起就进行定向生成呢?这就是发明者的发现了,我们完全可以用拿来主义,接受这个设定,毕竟CGAN是这样做的,那么starGAN也可以这样做。ps.starGAN添加target domain的方法更像CGAN生成mnist手写数据的方法,这部分可以看下代码。

写在后面

你可能感觉fake和real的放入在代码和论文上给你的感觉不一样,但是没关系,我要告诉你的是,论文只是理论,它会为了描述清楚而做一些排版或者是格式上的处理,让他看起来更好看更容易理解,就像在讲GANs的时候我们总是要放一个框图。然而你也知道,我们在代码里面总来没有框图这个东西,只有一行行代码。所以,我想表达的是,我们真正操作起来是使用代码来让网络工作的,所以我们更重要的是从代码理解网络搭建的模型。所以你看到,我们有些建构并没有像论文框图里写的那样,而是依据公式。对,这就是我要说的重点思想:我们对于网络的架构和流程可以根据框图,但也一定要按照公式来,尤其是涉及loss、train等关键点。、

这就是我要告诉你的,你不理解生成器如何把生成的图像变得越来越像真实图像,而上面那句话就已经告诉你了,答案就是根据公式,根据loss的公式,根据train的优化器和minimize,我们就可以让生成图片越来越像真实图片。如果你还不理解,那就记住我说的话。

最终,我要说一句,如果你总是怀疑,不如就这样接受,在日后的学习中你会慢慢的理解。千万别着急,一切都会好。

07-12 19:00