技术 | 生成对抗网络(GANs)的前沿话题(三)

推荐理由:

1.本文对GANs的原理进行了简明扼要的阐述,并结合其众多主流的前沿应用。

2.对于GANs训练中的常见问题进行了细致归类总结,并给出了针对性的改进方法。

3.结合实例,提供了详细的代码实例。有利于读者加深巩固对GANs的认知。


 

Building an Image GAN

 

正如我们已经多次讨论过的那样,培训GAN可能会令人沮丧且耗费时间。 我们将在Keras中介绍一个干净简练地例子。 结果仅在概念证明层面上增强理解。 在代码示例中,如果不仔细调整参数,则不会超过图像生成的这个级别(见下文):

 

 

网络拍摄图像H,W,C并输出M的矢量,分类(分类)或单个分数量化照片写实。 可以是任何图像分类网络,例如 ResNet或DenseNet。 我们使用简约的自定义架构。

 

 

采用噪声矢量N并输出维度为H,W,C的图像。 网络必须执行综合。 同样,我们使用非常简单的自定义架构。

 


 

在Keras中正确定义模型非常重要,这样可以在正确的时间固定各个模型的权重。

1、定义鉴别器模型,并进行编译。

2、定义生成器模型,无需编译。

3、定义由这两者组成的整体模型,在编译之前将鉴别器设置为不可训练


 


 

训练循环必须手动执行:

1、从训练集中选择R真实图像。

2、通过对大小为N的随机向量进行采样,并使用生成器从它们预测图像来生成F伪图像。

3、使用trainonbatch训练鉴别器:分别为R真实图像批次和F伪图像调用它,其实际结果应分别为1和0。

4、采样大小为N的新随机向量。

5、重复第一步用trainonbatch训练新模型的使之完整。之后这将更新生成器。


 

编码实践

 

现在,我们将使用著名的CelebA数据集上的Keras框架以代码格式进行上述简单的实践。 如果您对代码的结构方式感到困惑,可能需要参考上述过程,尽管我会尽力指导您完成此操作。

 

在我的相应GitHub存储库中可以免费获得这个由3部分组成的教程的完整代码实现。
 

请注意,在这段代码中我没有以任何方式优化它,我忽略了几条经验规则。 之后,我将在第3部分中更多地讨论这些内容,这将涉及对GAN代码的更深入的讨论。

 

全局参数

 

在我看来,最好在开始时就指定这些参数以避免以后混淆,因为这些网络可能会变得相当混乱和复杂。
 

SPATIAL_DIM = 64 # Spatial dimensions of the images.

LATENT_DIM = 100 # Dimensionality of the noise vector.

BATCH_SIZE = 32 # Batchsize to use for training.

DISC_UPDATES = 1  # Number of discriminator updates per training iteration.

GEN_UPDATES = 1 # Nmber of generator updates per training iteration.

 

FILTER_SIZE = 5 # Filter size to be applied throughout all convolutional layers.

NUM_LOAD = 10000 # Number of images to load from CelebA. Fit also according to the available memory on your machine.

NET_CAPACITY = 16 # General factor to globally change the number of convolutional filters.

 

PROGRESS_INTERVAL = 80 # Number of iterations after which current samples will be plotted.

ROOT_DIR = 'visualization' # Directory where generated samples should be saved to.

 

if not os.path.isdir(ROOT_DIR):

    os.mkdir(ROOT_DIR)


 

数据准备
 

我们现在进行一些图像预处理以标准化图像,并绘制图像以确保我们的实现正常工作。

 

def plot_image(x):

    plt.imshow(x * 0.5 + 0.5)

X = []

# Reference to CelebA dataset here. I recommend downloading from the Harvard 2019 ComputeFest GitHub page (there is also some good coding tutorials here)

faces = glob.glob('../Harvard/ComputeFest 2019/celeba/img_align_celeba/*.jpg')

 

for i, f in enumerate(faces):

    img = cv2.imread(f)

    img = cv2.resize(img, (SPATIAL_DIM, SPATIAL_DIM))

    img = np.flip(img, axis=2)

    img = img.astype(np.float32) / 127.5 - 1.0

    X.append(img)

    if i >= NUM_LOAD - 1:

        break

X = np.array(X)

plot_image(X[4])

X.shape, X.min(), X.max()

 

定义结构

 

Keras格式的架构非常简单。所以我们最好用块编写代码以保持尽可能地简洁。

 

首先,我们添加编码器块部分。 请注意,我们使用“相同”填充,以便输入和输出尺寸相同,以及批量标准化和泄漏ReLU。 Stride主要是可选的,因为泄漏的ReLU参数的大小也是如此。 我放在这里的值没有优化,但确实给了我一个合理的结果。
 

def add_encoder_block(x, filters, filter_size):

    x = Conv2D(filters, filter_size, padding='same')(x)

    x = BatchNormalization()(x)

    x = Conv2D(filters, filter_size, padding='same', strides=2)(x)

    x = BatchNormalization()(x)

    x = LeakyReLU(0.3)(x)

    return x

 

接下来是鉴别器本身 - 注意我们如何回收编码器块段并逐渐增加过滤器大小以解决我们之前讨论的用于(大)图像训练的问题(对所有图像执行此操作的最佳实践)。

 

def build_discriminator(start_filters, spatial_dim, filter_size):

    inp = Input(shape=(spatial_dim, spatial_dim, 3))

    

    # Encoding blocks downsample the image.

    x = add_encoder_block(inp, start_filters, filter_size)

    x = add_encoder_block(x, start_filters * 2, filter_size)

    x = add_encoder_block(x, start_filters * 4, filter_size)

    x = add_encoder_block(x, start_filters * 8, filter_size)

    

    x = GlobalAveragePooling2D()(x)

    x = Dense(1, activation='sigmoid')(x)

    return keras.Model(inputs=inp, outputs=x)

 

现在我们进入到解码器块段。 这次我们正在执行卷积层的反面,即反卷积。 请注意,为了便于实现,步幅和填充是相同的,我们再次使用批量标准化和泄漏ReLU。

 

def add_decoder_block(x, filters, filter_size):

    x = Deconvolution2D(filters, filter_size, strides=2, padding='same')(x)

    x = BatchNormalization()(x)

    x = LeakyReLU(0.3)(x)

    return x

 

现在构建生成器,注意这次我们使用解码器块并逐渐减小过滤器大小。
 

def build_generator(start_filters, filter_size, latent_dim):

    inp = Input(shape=(latent_dim,))

    

    # Projection.

    x = Dense(4 * 4 * (start_filters * 8), input_dim=latent_dim)file:///.file/id=6571367.8606697666/(inp)

    x = BatchNormalization()(x)

    x = Reshape(target_shape=(4, 4, start_filters * 8))(x)

    

    # Decoding blocks upsample the image.

    x = add_decoder_block(x, start_filters * 4, filter_size)

    x = add_decoder_block(x, start_filters * 2, filter_size)

    x = add_decoder_block(x, start_filters, filter_size)

    x = add_decoder_block(x, start_filters, filter_size)    

    

    x = Conv2D(3, kernel_size=5, padding='same', activation='tanh')(x)

    return keras.Model(inputs=inp, outputs=x)

 

训练
 

现在我们已经建立了网络架构,我们可以概述培训过程,这可能是人们容易混淆的地方。这可能是因为在更多功能中的函数内部有函数。

 

def construct_models(verbose=False):

    # 1. Build discriminator.

    discriminator = build_discriminator(NET_CAPACITY, SPATIAL_DIM, FILTER_SIZE)

    discriminator.compile(loss='binary_crossentropy', optimizer=keras.optimizers.Adam(lr=0.0002), metrics=['mae'])

 

    # 2. Build generator.

    generator = build_generator(NET_CAPACITY, FILTER_SIZE, LATENT_DIM)

 

    # 3. Build full GAN setup by stacking generator and discriminator.

    gan = keras.Sequential()

    gan.add(generator)

    gan.add(discriminator)

    discriminator.trainable = False # Fix the discriminator part in the full setup.

    gan.compile(loss='binary_crossentropy', optimizer=keras.optimizers.Adam(lr=0.0002), metrics=['mae'])

 

    if verbose: # Print model summaries for debugging purposes.

        generator.summary()

        discriminator.summary()

gan.summary()

    return generator, discriminator, gan


 

基本上,我们上面所做的是设计一个基于我们的全局参数创建GAN模型的函数。请注意,我们编译鉴别器,但不编译生成器,但我们最后编译GAN作为一个整体
 

另外,请注意我们已经将鉴别器设置为不可训练,这是人们在构建GAN时常常忘记的事情!

 

这是如此重要的原因是你不能同时训练两个网络,就像试图校正多个变量变化的东西,你会得到零星的结果。在训练任何单个网络时,您需要对模型的其余部分进行固定设置。
 

现在构建了完整的模型,我们可以继续进行训练。
 

def run_training(start_it=0, num_epochs=1000):

# Save configuration file with global parameters

    config_name = 'gan_cap' + str(NET_CAPACITY) + '_batch' + str(BATCH_SIZE) + '_filt' + str(FILTER_SIZE) + '_disc' + str(DISC_UPDATES) + '_gen' + str(GEN_UPDATES)

    folder = os.path.join(ROOT_DIR, config_name)

 

    if not os.path.isdir(folder):

        os.mkdir(folder)

# Initiate loop variables

    avg_loss_discriminator = []

    avg_loss_generator = []

    total_it = start_it

    # Start of training loop

    for epoch in range(num_epochs):

        loss_discriminator = []

        loss_generator = []

        for it in range(200):

 

            # Update discriminator.

            for i in range(DISC_UPDATES):

                # Fetch real examples (you could sample unique entries, too).

                imgs_real = X[np.random.randint(0, X.shape[0], size=BATCH_SIZE)]

 

                # Generate fake examples.

                noise = np.random.randn(BATCH_SIZE, LATENT_DIM)

                imgs_fake = generator.predict(noise)

 

                d_loss_real = discriminator.train_on_batch(imgs_real, np.ones([BATCH_SIZE]))[1]

                d_loss_fake = discriminator.train_on_batch(imgs_fake, np.zeros([BATCH_SIZE]))[1]

            

            # Progress visualizations.

            if total_it % PROGRESS_INTERVAL == 0:

                plt.figure(figsize=(5,2))

                # We sample separate images.

                num_vis = min(BATCH_SIZE, 8)

                imgs_real = X[np.random.randint(0, X.shape[0], size=num_vis)]

                noise = np.random.randn(num_vis, LATENT_DIM)

                imgs_fake = generator.predict(noise)

                for obj_plot in [imgs_fake, imgs_real]:

                    plt.figure(figsize=(num_vis * 3, 3))

                    for b in range(num_vis):

                        disc_score = float(discriminator.predict(np.expand_dims(obj_plot[b], axis=0))[0])

                        plt.subplot(1, num_vis, b + 1)

                        plt.title(str(round(disc_score, 3)))

                        plot_image(obj_plot[b])

                    if obj_plot is imgs_fake:

                        plt.savefig(os.path.join(folder, str(total_it).zfill(10) + '.jpg'), format='jpg', bbox_inches='tight')

                    plt.show()  

 

            # Update generator.

            loss = 0

            y = np.ones([BATCH_SIZE, 1])

            for j in range(GEN_UPDATES):

                noise = np.random.randn(BATCH_SIZE, LATENT_DIM)

                loss += gan.train_on_batch(noise, y)[1]

 

            loss_discriminator.append((d_loss_real + d_loss_fake) / 2.)        

            loss_generator.append(loss / GEN_UPDATES)

            total_it += 1

 

        # Progress visualization.

        clear_output(True)

        print('Epoch', epoch)

        avg_loss_discriminator.append(np.mean(loss_discriminator))

        avg_loss_generator.append(np.mean(loss_generator))

        plt.plot(range(len(avg_loss_discriminator)), avg_loss_discriminator)

        plt.plot(range(len(avg_loss_generator)), avg_loss_generator)

        plt.legend(['discriminator loss', 'generator loss'])

        plt.show()


 

上面的代码可能看起来很混乱。上面的代码中有几个项目只对管理GAN的运行有帮助。例如,第一部分设置配置文件并保存,因此您可以在将来引用它并确切知道网络的体系结构和超参数是什么。

 

还有进度可视化步骤,可实时打印笔记本的输出,以便您可以访问GAN的当前性能。
 

如果忽略配置文件和进度可视化则代码相对简单。首先,我们更新鉴别器,然后更新生成器,然后我们在这两个场景之间进行迭代。
 

现在,由于我们巧妙地使用了功能,我们可以将模型分为两行。

 

generator, discriminator, gan = construct_models(verbose=True)

run_training()
 

剩下要做的就是等待(可能是数小时或数天),然后测试网络的输出。

 

Sample from Trained GAN

 

现在,在等待网络完成培训后,我们可以从网络中获取大量样本。

 

NUM_SAMPLES = 7

plt.figure(figsize=(NUM_SAMPLES * 3, 3))

 

for i in range(NUM_SAMPLES):

    noise = np.random.randn(1, LATENT_DIM)

    pred_raw = generator.predict(noise)[0]

    pred = pred_raw * 0.5 + 0.5

    plt.subplot(1, NUM_SAMPLES, i + 1)

    plt.imshow(pred)

plt.show()


 

瞧!这是我们第一次实现基本的GAN。这可能是构建功能齐全的GAN的最简单方法。


 

结语

 

在本教程中我已经介绍了GAN的高级主题,它们的架构和当前的应用程序以及简单的GAN的编码实现。在本教程的最后部分,我们将比较VAE,GAN的性能以及VAE-GAN的实现,以生成动画图像。