【DL】第 12 章: 生成式深度学习

        🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎

📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃

🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​

📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】  深度学习【DL】

 🖍foreword

✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。

如果你对这个系列感兴趣的话,可以关注订阅哟👋

文章目录

12.1 文本生成

12.1.1 用于序列生成的生成式深度学习简史

12.1.2 如何生成序列数据?

12.1.3 抽样策略的重要性

12.1.4 使用 Keras 实现文本生成

12.1.5 具有可变温度采样的文本生成回调

12.1.6 总结

12.2 DeepDream

12.2.1 在 Keras 中实现 DeepDream

12.2.2 总结

12.3 神经风格迁移

12.3.1 内容丢失

12.3.2 风格损失

12.3.3 Keras 中的神经风格迁移

12.3.4 总结

12.4 用变分自编码器生成图像

12.4.1 从图像的潜在空间中采样

12.4.2 图像编辑的概念向量

12.4.3 变分自编码器

12.4.4 使用 Keras 实现 VAE

12.4.5 总结

12.5 生成对抗网络简介

12.5.1 GAN 实现示意图

12.5.2  A bag of tricks

12.5.3 掌握 CelebA 数据集

12.5.4 鉴别器

12.5.5 生成器

12.5.6 对抗网络

12.5.7 总结

概括


本章涵盖

  • 文本生成
  • DeepDream
  • 神经风格迁移
  • 变分自编码器
  • 生成对抗网络

人工智能模拟人类思维过程的潜力超越了物体识别等被动任务和驾驶汽车等大多数反应性任务。它很好地扩展到创意活动。当我第一次声称在不远的将来,我们消费的大部分文化内容将在人工智能的大力帮助下创造出来时,我完全不相信,即使是长期的机器学习从业者。那是在 2014 年。快进了几年,这种怀疑以令人难以置信的速度消退了。2015 年夏天,谷歌的 DeepDream 算法将图像变成了狗眼和幻觉伪影的迷幻混乱,这让我们很开心;2016年,我们开始使用智能手机应用程序将照片变成各种风格的画作。2016年夏天,一部实验短片,Sunspring是使用由长期短期记忆编写的脚本指导的。也许您最近听过由神经网络暂时生成的音乐。

诚然,到目前为止,我们从人工智能中看到的艺术作品质量相当低。人工智能在任何地方都无法与人类编剧、画家和作曲家相媲美。但取代人类总是题外话:人工智能并不是要用其他东西取代我们自己的智能,而是要为我们的生活和工作带来更多的智能——一种不同的智能。在许多领域,尤其是在创造性领域,人工智能将被人类用作增强自身能力的工具:增强智能人工智能更多。

艺术创作的很大一部分是简单的模式识别和技术技能。而这正是许多人认为不那么有吸引力甚至可有可无的过程的一部分。这就是人工智能的用武之地。我们的感知方式、我们的语言和我们的艺术作品都有统计结构。学习这种结构是深度学习算法擅长的。机器学习模型可以学习图像、音乐和故事的统计潜在空间,然后它们可以采样从这个空间中,创造出与模型在其训练数据中看到的特征相似的新艺术品。自然,这种采样本身几乎不是一种艺术创作行为。这只是一种数学运算:算法没有人类生活、人类情感或我们对世界的经验的基础;相反,它从与我们几乎没有共同之处的经验中学习。只有我们作为人类观众的解释才能赋予模型生成的内容以意义。但在熟练的艺术家手中,算法生成可以被引导变得有意义和美丽。潜在空间采样可以成为赋予艺术家权力的画笔,增强我们的创造力,并扩展我们可以想象的空间。更重要的是,

Iannis Xenakis 是一位富有远见的电子音乐和算法音乐先驱,他在 1960 年代在自动化技术应用于音乐创作的背景下精美地表达了同样的想法:1

从繁琐的计算中解脱出来,作曲家能够专注于新音乐形式所带来的一般问题,并在修改输入数据的值的同时探索这种形式的角落和缝隙。例如,他可能会测试从独奏家、室内乐团到大型管弦乐队的所有乐器组合。在电子计算机的帮助下,作曲家变成了某种飞行员:他按下按钮,输入坐标,并监督一艘宇宙飞船在声音空间中航行,穿越声波星座和星系,他以前只能作为一名飞行员瞥见。遥远的梦

1 Iannis Xenakis,“形式音乐:音乐创作的新形式原则”,音乐剧歌舞剧特刊第 1 期。253–254 (1963)。

在本章中,我们将从各个角度探讨深度学习在增强艺术创作方面的潜力。我们将回顾序列数据生成(可用于生成文本或音乐)、DeepDream 以及使用变分自动编码器和生成对抗网络的图像生成。我们将让您的计算机构想出前所未有的内容;也许我们也会让你梦想着技术和艺术交汇处的奇妙可能性。让我们开始吧。

12.1 文本生成

在本节中,我们将探讨如何使用递归神经网络生成序列数据。我们将使用文本生成作为示例,但完全相同的技术可以推广到任何类型的序列数据:您可以将其应用于音符序列以生成新音乐,应用于笔触数据的时间序列(可能在一位艺术家在 iPad 上绘画)以逐笔生成绘画,依此类推。

序列数据生成绝不限于艺术内容生成。它已成功应用于语音合成和聊天机器人的对话生成。谷歌在 2016 年发布的智能回复功能,能够自动生成对电子邮件或短信的快速回复,由类似技术提供支持。

12.1.1 用于序列生成的生成式深度学习简史

2014 年底,即使在机器学习社区,也很少有人见过 LSTM 的缩写。使用循环网络生成序列数据的成功应用直到 2016 年才开始出现在主流中。但这些技术具有相当长的历史,从 1997 年 LSTM 算法的发展开始(在第 10 章中讨论)。这种新算法很早就被用于逐个字符地生成文本。

2002 年,当时在瑞士 Schmidhuber 实验室的 Douglas Eck 首次将 LSTM 应用于音乐生成,并取得了可喜的成果。Eck 现在是 Google Brain 的一名研究员,2016 年,他在那里成立了一个名为 Magenta 的新研究小组,专注于应用现代深度学习技术来制作引人入胜的音乐。有时好的想法需要 15 年才能开始。

在 2000 年代末和 2010 年代初,Alex Graves 在使用循环网络生成序列数据方面做了重要的开创性工作。特别是,他在 2013 年应用循环混合密度网络使用笔位置的时间序列生成类人笔迹的工作被一些人视为一个转折点。2神经网络在那个特定时刻的这种特定应用为我捕捉到了做梦的机器的概念,并且在我开始开发 Keras 时是一个重要的灵感。Graves 在 2013 年上传到预印本服务器 arXiv 的 LaTeX 文件中留下了类似的注释掉的评论:“生成顺序数据是最接近梦想的计算机。” 几年后,我们认为很多这些发展是理所当然的,但当时很难看到格雷夫斯的示威,而不是对这些可能性感到敬畏而走开。2015 年至 2017 年间,循环神经网络成功地用于文本和对话生成、音乐生成和语音合成。

2 Alex Graves,“使用递归神经网络生成序列”,arXiv (2013),https://arxiv.org/abs/1308.0850

然后在 2017-2018 年左右,Transformer 架构开始接管循环神经网络,不仅用于有监督的自然语言处理任务,还用于生成序列模型——特别是语言建模(单词级文本生成)。生成式 Transformer 最著名的例子是 GPT-3,这是一个 1750 亿参数的文本生成模型,由初创公司 OpenAI 在一个惊人的大型文本语料库上训练,包括大多数数字书籍、维基百科和大部分爬虫整个互联网的。GPT-3 在 2020 年成为头条新闻,因为它能够在几乎任何主题上生成听起来合理的文本段落,这一能力助长了短暂的炒作浪潮,值得最火热的 AI 夏天。

12.1.2 如何生成序列数据?

在深度学习中生成序列数据的通用方法是训练一个模型(通常是 Transformer 或 RNN)以使用先前的标记作为输入来预测序列中的下一个标记或接下来的几个标记。例如,给定输入“猫在上面”,模型被训练来预测目标“垫子”,即下一个词。像往常一样处理文本数据时,标记通常是单词或字符,任何可以在给定先前标记的情况下对下一个标记的概率进行建模的网络称为语言模型。语言模型捕获语言的潜在空间:它的统计结构。

一旦你有了这样一个训练有素的语言模型,你就可以从中采样(生成新的序列):你给它一个初始的文本字符串(称为条件数据),让它生成下一个字符或下一个单词(你甚至可以生成一次几个令牌),将生成的输出添加回输入数据,并重复该过程多次(见图 12.1)。此循环允许您生成任意长度的序列,这些序列反映了训练模型的数据结构:看起来几乎像人类编写的句子的序列。

图 12.1 使用语言模型逐字生成文本的过程

12.1.3 抽样策略的重要性

生成文本时,选择下一个标记的方式至关重要。一种简单的方法是贪婪抽样,包括总是选择最有可能的下一个角色。但是这种方法会导致重复的、可预测的字符串看起来不像连贯的语言。一种更有趣的方法做出了更令人惊讶的选择:它通过从下一个字符的概率分布中采样来在采样过程中引入随机性。这是称为随机抽样(回想一下,随机性是我们在该领域中所说的随机性)。在这样的设置中,根据模型,如果一个单词在句子中出现下一个单词的概率为 0.3,那么您将选择它的概率为 30%。请注意,贪心抽样也可以作为概率分布的抽样:其中某个单词的概率为 1,而所有其他单词的概率为 0。

从模型的 softmax 输出中进行概率采样非常简洁:它允许在某些时候甚至对不太可能的单词进行采样,生成看起来更有趣的句子,有时通过提出新的、听起来真实的句子来展示创造力在训练数据中。但是这种策略有一个问题:它没有提供一种方法来控制采样过程中的随机性

为什么你想要更多或更少的随机性?考虑一个极端情况:纯随机抽样,从均匀概率分布中抽取下一个单词,并且每个单词的概率均等。该方案具有最大的随机性;换句话说,这个概率分布具有最大熵。自然,它不会产生任何有趣的东西。在另一个极端,贪婪采样也不会产生任何有趣的东西,也没有随机性:相应的概率分布具有最小熵。从“真实”概率分布(模型的 softmax 函数输出的分布)中采样构成了这两个极端之间的中间点。但是,您可能还想探索许多其他更高或更低熵的中间点。更少的熵将使生成的序列具有更可预测的结构(因此它们可能看起来更逼真),而更多的熵将导致更令人惊讶和创造性的序列。从生成模型中采样时,在生成过程中探索不同数量的随机性总是好的。因为我们——人类——是生成数据有趣程度的最终评判者,所以有趣程度是高度主观的,并且无法提前知道最佳熵的点在哪里。

为了控制采样过程中的随机性数量,我们将引入一个称为softmax 温度的参数,它表征用于采样的概率分布的熵:它表征了下一个词的选择将是多么令人惊讶或可预测。给定一个temperature值,通过以下方式对其重新加权,从原始概率分布(模型的 softmax 输出)计算出一个新的概率分布。

清单 12.1 将概率分布重新加权到不同的温度

import numpy as np 
def reweight_distribution(original_distribution, temperature=0.5):   ❶
    distribution = np.log(original_distribution) / temperature
    distribution = np.exp(distribution)
    return distribution / np.sum(distribution)                       ❷

 original_distribution 是一个 1D NumPy 概率值数组,其总和必须为 1。温度是量化输出分布熵的一个因素。

返回原始分布的重新加权版本。分布的总和可能不再为 1,因此您将其除以它的总和以获得新的分布。

较高的温度会导致较高熵的采样分布,这将生成更多令人惊讶和非结构化的生成数据,而较低的温度将导致更少的随机性和更可预测的生成数据(见图 12.2)。

图 12.2 一种概率分布的不同重新加权。低温=更具确定性,高温=更随机。

12.1.4 使用 Keras 实现文本生成

让我们在 Keras 实现中将这些想法付诸实践。您需要的第一件事是大量可用于学习语言模型的文本数据。您可以使用任何足够大的文本文件或一组文本文件——维基百科、指环王等。

在这个例子中,我们将继续使用上一章的 IMDB 电影评论数据集,我们将学习生成从未读过的电影评论。因此,我们的语言模型将是这些电影评论的风格和主题的模型,而不是英语语言的一般模型。

准备数据

就像上一章一样,让我们​​下载并解压缩 IMDB 电影评论数据集。

清单 12.2 下载和解压缩 IMDB 电影评论数据集

!wget https:/ /ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz
!tar -xf aclImdb_v1.tar.gz

您已经熟悉数据的结构:我们得到一个名为 aclImdb 的文件夹,其中包含两个子文件夹,一个用于负面情绪的电影评论,一个用于正面情绪的评论。每个有一个文本文件审查。我们将调用text_dataset_ from_directorywithlabel_mode=None创建一个数据集,该数据集从这些文件中读取并生成每个文件的文本内容。

清单 12.3 从文本文件创建数据集(一个文件 = 一个样本)

import tensorflow as tf 
from tensorflow import keras
dataset = keras.utils.text_dataset_from_directory(
    directory="aclImdb", label_mode=None, batch_size=256)
dataset = dataset.map(lambda x: tf.strings.regex_replace(x, "<br />", " "))❶

❶ 去掉许多评论中出现的 <br /> HTML 标签。这对于文本分类来说并不重要,但我们不想在这个例子中生成 <br /> 标签!

现在让我们使用一个TextVectorization图层计算我们将使用的词汇表。我们将只使用sequence_length每条评论的第一个词:我们的TextVectorization层将在矢量化文本时切断除此之外的任何内容。

清单 12.4 准备一个TextVectorization

from tensorflow.keras.layers import TextVectorization
  
sequence_length = 100 
vocab_size = 15000                            ❶
text_vectorization = TextVectorization(
    max_tokens=vocab_size,                
    output_mode="int",                        ❷
    output_sequence_length=sequence_length,   ❸
)
text_vectorization.adapt(dataset)

我们只会考虑前 15,000 个最常用的词——其他任何词都将被视为词汇表外标记“[UNK]”。

我们要返回整数词索引序列。

我们将使用长度为 100 的输入和目标(但由于我们将目标偏移 1,因此模型实际上将看到长度为 99 的序列)。

让我们使用该层创建一个语言建模数据集,其中输入样本是矢量化文本,对应的目标是偏移一个单词的相同文本。

清单 12.5 设置语言建模数据集

def prepare_lm_dataset(text_batch):
    vectorized_sequences = text_vectorization(text_batch)    ❶
    x = vectorized_sequences[:, :-1]                         ❷
    y = vectorized_sequences[:, 1:]                          ❸
    return x, y
  
lm_dataset = dataset.map(prepare_lm_dataset, num_parallel_calls=4)

将一批文本(字符串)转换为一批整数序列。

通过切断序列的最后一个单词来创建输入。

通过将序列偏移 1 来创建目标。

基于 TRANSFORMER 的序列到序列模型

给定一些初始单词,我们将训练一个模型来预测句子中下一个单词的概率分布。当模型被训练时,我们将给它一个提示,对下一个单词进行采样,将该单词添加回提示中,然后重复,直到我们生成一个简短的段落。

就像我们在第 10 章中对温度预测所做的那样,我们可以训练一个模型,该模型将N个单词的序列作为输入,并简单地预测单词N +1。但是,在序列生成的上下文中,此设置存在几个问题。

首先,模型只会在N个单词可用时学习生成预测,但是能够从少于N个单词开始预测会很有用。否则,我们将只能使用相对较长的提示(在我们的实现中,N = 100 个单词)。我们在第 10 章没有这个需要。

其次,我们的许多训练序列大多是重叠的。考虑N = 4。文本“一个完整的句子必须至少包含三件事:主语、动词和宾语”将用于生成以下训练序列:

  • “一个完整的句子必须”

  • “完整的句子必须有”

  • “句子必须在”

  • 依此类推,直到“动词和宾语”

将每个这样的序列视为独立样本的模型将不得不做大量冗余工作,重新编码以前在很大程度上见过的多次子序列。在第 10 章中,这不是什么大问题,因为我们一开始没有那么多训练样本,我们需要对密集和卷积模型进行基准测试,而每次重做工作是唯一的选择。我们可以尝试通过使用步幅对我们的序列进行采样来缓解这种冗余问题——在两个连续样本之间跳过几个单词。但这会减少我们的训练样本数量,同时只提供部分解决方案。

为了解决这两个问题,我们将使用序列到序列模型:我们将提供N个单词的序列(从 0 到N索引)到我们的模型中,我们将预测序列偏移量为 1(从 1 到N+1)。我们将使用因果掩蔽来确保,对于任何i,模型将只使用从 0 到i的单词来预测单词i + 1。这意味着我们同时训练模型来解决N个大部分重叠但不同的问题:在给定一系列1 <= i <= N先前单词的情况下预测下一个单词(见图 12.3)。在生成时,即使你只用一个词提示模型,它也能够为你提供下一个可能的词的概率分布。

图 12.3 与普通的下一个词预测相比,序列到序列建模同时优化了多个预测问题。

请注意,我们可以在第 10 章中对我们的温度预测问题使用类似的序列到序列设置:给定 120 个每小时数据点的序列,学习生成未来 24 小时偏移的 120 个温度序列。1 <= i < 120给定之前的每小时数据点,您不仅要解决最初的问题,还要解决 24 小时内预测温度的 119 个相关问题。如果你尝试在序列到序列的设置中重新训练第 10 章中的 RNN,你会发现你得到了相似但越来越差的结果,因为使用相同模型解决这些额外的 119 个相关问题的约束会稍微干扰我们真正关心的任务。

在上一章中,您了解了在一般情况下可用于序列到序列学习的设置:将源序列输入编码器,然后将编码序列和目标序列输入解码器,解码器尝试一步预测相同的目标序列偏移量。当您进行文本生成时,没有源序列:您只是试图在给定过去标记的情况下预测目标序列中的下一个标记,我们只使用解码器就可以做到这一点。并且由于因果填充,解码器将只查看单词0...N来预测单词N+1

让我们实现我们的模型——我们将重用我们在第 11 章中创建的构建块:PositionalEmbeddingTransformerDecoder.

清单 12.6 一个简单的基于 Transformer 的语言模型

from tensorflow.keras import layers
embed_dim = 256 
latent_dim = 2048 
num_heads = 2 
  
inputs = keras.Input(shape=(None,), dtype="int64")
x = PositionalEmbedding(sequence_length, vocab_size, embed_dim)(inputs)
x = TransformerDecoder(embed_dim, latent_dim, num_heads)(x, x)
outputs = layers.Dense(vocab_size, activation="softmax")(x)       ❶
model = keras.Model(inputs, outputs)
model.compile(loss="sparse_categorical_crossentropy", optimizer="rmsprop")

对每个输出序列时间步计算的可能词汇的 Softmax。

12.1.5 具有可变温度采样的文本生成回调

我们将使用回调在每个 epoch 之后使用一系列不同的温度生成文本。这使您可以看到生成的文本如何随着模型开始收敛而演变,以及温度对采样策略的影响。为了生成文本,我们将使用提示“这部电影”:我们生成的所有文本都以此开头。

清单 12.7 文本生成回调

import numpy as np
  
tokens_index = dict(enumerate(text_vectorization.get_vocabulary()))    ❶
  
def sample_next(predictions, temperature=1.0):                         ❷
    predictions = np.asarray(predictions).astype("float64")
    predictions = np.log(predictions) / temperature
    exp_preds = np.exp(predictions)
    predictions = exp_preds / np.sum(exp_preds)
    probas = np.random.multinomial(1, predictions, 1)
    return np.argmax(probas)
  
class TextGenerator(keras.callbacks.Callback):
    def __init__(self,
                 prompt,                                               ❸
                 generate_length,                                      ❹
                 model_input_length,
                 temperatures=(1.,),                                   ❺
                 print_freq=1):
        self.prompt = prompt
        self.generate_length = generate_length
        self.model_input_length = model_input_length
        self.temperatures = temperatures
        self.print_freq = print_freq
  
    def on_epoch_end(self, epoch, logs=None):
        if (epoch + 1) % self.print_freq != 0:
            return
        for temperature in self.temperatures:
            print("== Generating with temperature", temperature)
            sentence = self.prompt                                     ❻
            for i in range(self.generate_length):
                tokenized_sentence = text_vectorization([sentence])    ❼
                predictions = self.model(tokenized_sentence)           ❼
                next_token = sample_next(predictions[0, i, :])         ❽
                sampled_token = tokens_index[next_token]               ❽
                sentence += " " + sampled_token                        ❾
            print(sentence)
  
prompt = "This movie" 
text_gen_callback = TextGenerator(
    prompt,
    generate_length=50,
    model_input_length=sequence_length,
    temperatures=(0.2, 0.5, 0.7, 1., 1.5))    

将单词索引映射回字符串的字典,用于文本解码

从概率分布中实现可变温度采样

我们用来种子文本生成的提示

生成多少字

采样温度范围

生成文本时,我们从提示开始。

将当前序列输入我们的模型。

检索最后一个时间步的预测,并使用它们来采样一个新单词。

将新单词附加到当前序列并重复。

我们将使用不同范围的温度对文本进行采样,以展示温度对文本生成的影响。

让我们fit()这件事。

清单 12.8 拟合语言模型

model.fit(lm_dataset, epochs=200, callbacks=[text_gen_callback])

以下是我们在 200 轮训练后能够生成的一些精选示例。请注意,标点符号不是我们词汇的一部分,因此我们生成的文本都没有任何标点符号:

  • temperature=0.2

    • “这部电影是原版电影的[UNK],电影的前半小时很不错,但这是一部非常好的电影,在那个时期是一部好电影”

    • “这部电影是电影中的 [UNK] 它是一部非常糟糕的电影,它是一部 [UNK] 电影 这是一部非常糟糕的电影,它让你同时又笑又哭我认为我从未看过的电影”

  • temperature=0.5

    • “这部电影是有史以来最好的类型电影中的 [UNK],它不是一部好电影,这是我第一次看到这部电影的唯一好处,我仍然记得它是 [UNK]我看了很多年的电影”

    • “这部电影是在浪费时间和金钱我不得不说这部电影完全是在浪费时间我很惊讶地看到这部电影是由一部好电影组成的,而且这部电影不是很好,但这是一种浪费时间和”

  • temperature=0.7

    • “这部电影很有趣,看到所有角色都非常搞笑,而且猫有点像 [UNK] [UNK] 和帽子 [UNK] 电影的规则可以用另一个来讲述场景使它免于在后面”

    • “这部电影是关于 [UNK] 和几个年轻人在一条小船上在偏僻的地方,他们可能会发现自己接触了 [UNK] 牙医,他们被 [UNK] 杀死了书,我还没有看过原版,所以它”

  • temperature=1.0

    • “这部电影很有趣,我觉得情节线响亮而感人,但在整个观看过程中,与原版的艺术形成鲜明对比,我们观看了原版的英格兰,然而弧线有点太普通了 [UNK]是现在的父母[UNK]”

    • “这部电影是远离故事情节的杰作,但这部电影简直令人兴奋和沮丧,它真的让这样的朋友开心,这部电影中的演员试图直接从那个形象出发,他们使它成为一部非常好的电视节目”

  • temperature=1.5

    • “这部电影可能是关于 80 位女性的最糟糕的电影,它像巴克电影一样奇怪而有见地的演员,但在伟大的伙伴中,是的,即使 [UNK] 陆地恐龙拉尔夫安也没有装饰过的盾牌,必须在误播 [UNK] 巴赫之后发生戏剧性事件真的不是摔角狂热,山姆根本不存在”

    • “这部电影可能是如此令人难以置信,卢卡斯本人给我们的国家带来了非常有趣的事情,因为花哨的严肃和强大的表演,科林写得更详细,但在之前和那些烧毁了爱国主义的图像齿轮,我们你期望 dyan 老板的奉献精神必须做你自己职责和另一个”

如您所见,低温值会导致文本非常无聊和重复,有时会导致生成过程陷入循环。随着温度的升高,生成的文本会变得更有趣、更令人惊讶,甚至更有创意。在非常高的温度下,局部结构开始分解,输出看起来很大程度上是随机的。在这里,一个好的生成温度似乎是 0.7 左右。始终尝试多种采样策略!学习结构和随机性之间的巧妙平衡使生成变得有趣。

请注意,通过在更多数据上训练一个更大、更长、更长的模型,您可以获得比这个看起来更加连贯和真实的生成样本——像 GPT-3 这样的模型的输出是语言可以完成的一个很好的例子模型(GPT-3 实际上与我们在此示例中训练的相同,但具有大量的 Transformer 解码器和更大的训练语料库)。但是不要期望生成任何有意义的文本,除了通过随机机会和您自己的解释的魔力:您所做的只是从统计模型中采样数据,即哪些词在哪些词之后。语言模型都是形式,没有实质。

自然语言有很多东西:一种沟通渠道,一种对世界采取行动的方式,一种社交润滑剂,一种制定、存储和检索自己思想的方式。. . 语言的这些用途是其意义的起源。一个深度学习的“语言模型”,尽管它的名字,实际上并没有捕捉到语言的这些基本方面。它不能交流(它没有什么可交流的,也没有人可以交流),它不能作用于世界(它没有代理和意图),它不能是社交的,它没有任何思想要处理文字的帮助。语言是思维的操作系统,因此,要使语言有意义,就需要思维来利用它。

语言模型所做的是捕获我们在使用语言生活​​时生成的可观察工件的统计结构——书籍、在线电影评论、推文。这些人工制品完全具有统计结构这一事实是人类如何实现语言的副作用。这是一个思想实验:如果我们的语言在压缩通信方面做得更好,就像计算机对大多数数字通信所做的那样,会怎样?语言将同样有意义并且仍然可以实现它的许多目的,但它会缺乏任何内在的统计结构,因此无法像你刚才那样进行建模。

12.1.6 总结

  • 您可以通过训练模型来生成离散序列数据,以在给定先前标记的情况下预测下一个标记。

  • 在文本的情况下,这样的模型称为语言模型。它可以基于单词或字符。

  • 对下一个令牌进行采样需要在遵守模型判断的可能内容和引入随机性之间取得平衡。

  • 处理这个问题的一种方法是 softmax 温度的概念。始终尝试不同的温度以找到合适的温度。

12.2 DeepDream

DeepDream是一种艺术图像修改技术,它使用卷积神经网络学习的表示。它于 2015 年夏天由 Google 首次发布,作为使用 Caffe 深度学习库编写的实现(这是 TensorFlow 首次公开发布的几个月前)。3由于它可以生成令人迷惑的图片(例如,参见图 12.4),它很快在互联网上引起轰动,其中充满了算法性幻觉伪影、鸟羽和狗眼——这是 DeepDream 卷积网络经过训练的副产品ImageNet,其中犬种和鸟类的数量大大增加。

3 Alexander Mordvintsev、Christopher Olah 和 Mike Tyka,“DeepDream:用于可视化神经网络的代码示例”,Google 研究博客,2015 年 7 月 1 日,http ://mng.bz/xXlM 。

图 12.4 DeepDream 输出图像示例

DeepDream 算法与第 9 章介绍的卷积神经网络过滤器可视化技术几乎相同,包括反向运行卷积神经网络:对卷积神经网络的输入进行梯度上升,以最大限度地激活神经网​​络上层的特定过滤器。卷积网络。DeepDream 使用了相同的想法,但有一些简单的区别:

  • 使用 DeepDream,您尝试最大化整个层的激活而不是特定过滤器的激活,从而一次将大量特征的可视化混合在一起。

  • 您不是从空白的、略带噪声的输入开始,而是从现有的图像开始——因此,产生的效果会锁定到预先存在的视觉模式上,以某种艺术的方式扭曲图像的元素。

  • 输入图像以不同的比例(称为octaves)进行处理,这提高了可视化的质量。

让我们做一些 DeepDreams。

12.2.1 在 Keras 中实现 DeepDream

让我们从检索一个梦想的测试图像开始。我们将使用冬季崎岖的北加利福尼亚海岸的视图(图 12.5)。

清单 12.9 获取测试图像

from tensorflow import keras 
import matplotlib.pyplot as plt
  
base_image_path = keras.utils.get_file(
    "coast.jpg", origin="https:/ /img-datasets.s3.amazonaws.com/coast.jpg")
  
plt.axis("off")
plt.imshow(keras.utils.load_img(base_image_path))

图 12.5 我们的测试图像

接下来,我们需要一个预训练的卷积网络。在 Keras 中,有许多这样的卷积网络可用:VGG16、VGG19、Xception、ResNet50 等,所有这些都可以在 ImageNet 上使用预训练的权重。您可以使用其中任何一个实现 DeepDream,但您选择的基本模型自然会影响您的可视化,因为不同的架构会导致不同的学习特征。最初的 DeepDream 版本中使用的卷积网络是一个 Inception 模型,在实践中,众所周知,Inception 可以生成漂亮的 DeepDreams,因此我们将使用 Keras 附带的 Inception V3 模型。

清单 12.10 实例化一个预训练InceptionV3模型

from tensorflow.keras.applications import inception_v3
model = inception_v3.InceptionV3(weights="imagenet", include_top=False)

我们将使用我们预训练的卷积网络创建一个特征精确器模型,该模型返回各个中间层的激活,如以下代码所示。对于每一层,我们选择一个标量分数,该分数加权该层对我们将在梯度上升过程中寻求最大化的损失的贡献。如果您想要一个完整的图层名称列表,您可以使用这些名称来选择要播放的新图层与,只需使用model.summary().

清单 12.11 配置每一层对 DeepDream 损失的贡献

layer_settings = {                                                         ❶
    "mixed4": 1.0,
    "mixed5": 1.5,
    "mixed6": 2.0,
    "mixed7": 2.5,
}
outputs_dict = dict(                                                       ❷
    [
        (layer.name, layer.output)
        for layer in [model.get_layer(name)
                     for name in layer_settings.keys()]
    ]
)
feature_extractor = keras.Model(inputs=model.inputs, outputs=outputs_dict) ❸

我们尝试最大化激活的层,以及它们在总损失中的权重。您可以调整这些设置以获得新的视觉效果。

每一层的符号输出

返回每个目标层的激活值的模型(作为字典)

接下来,我们将计算损失:在每个处理规模的梯度上升过程中,我们将寻求最大化的数量。在第 9 章中,对于过滤器可视化,我们尝试最大化特定层中特定过滤器的值。在这里,我们将同时最大化多个层中所​​有过滤器的激活。具体来说,我们将最大化一组高级层激活的 L2 范数的加权平均值。我们选择的确切层集(以及它们对最终损失的贡献)对我们能够产生的视觉效果有重大影响,因此我们希望使这些参数易于配置。较低的层会产生几何图案,而较高的层会产生视觉效果,您可以在其中识别 ImageNet 中的某些类(例如,鸟或狗)。

清单 12.12 DeepDream 损失

def compute_loss(input_image):
    features = feature_extractor(input_image)                                  ❶
    loss = tf.zeros(shape=())                                                  ❷
    for name in features.keys():
        coeff = layer_settings[name]
        activation = features[name]
        loss += coeff * tf.reduce_mean(tf.square(activation[:, 2:-2, 2:-2, :]))❸
    return loss

提取激活。

将损失初始化为 0。

我们通过仅在损失中涉及非边界像素来避免边界伪影。

现在让我们设置我们将在每个八度音程上运行的梯度上升过程。你会发现它和第 9 章中的过滤可视化技术是一回事!DeepDream 算法只是过滤器可视化的一种多尺度形式。

DeepDream 梯度上升过程

import tensorflow as tf
  
@tf.function                                                               ❶
def gradient_ascent_step(image, learning_rate):
    with tf.GradientTape() as tape:                                        ❷
        tape.watch(image)                                                  ❷
        loss = compute_loss(image)                                         ❷
    grads = tape.gradient(loss, image)                                     ❷
    grads = tf.math.l2_normalize(grads)                                    ❸
    image += learning_rate * grads
    return loss, image
  
  
def gradient_ascent_loop(image, iterations, learning_rate, max_loss=None): ❹
    for i in range(iterations):                                            ❺
        loss, image = gradient_ascent_step(image, learning_rate)           ❺
        if max_loss is not None and loss > max_loss:                       ❻
            break                                                          ❻
        print(f"... Loss value at step {i}: {loss:.2f}")
    return image

我们通过将其编译为 tf.function 来加快训练步骤。

计算 DeepDream 损失相对于当前图像的梯度。

标准化梯度(与我们在第 9 章中使用的技巧相同)。

这会针对给定的图像比例(八度音阶)运行梯度上升。

以增加 DeepDream 损失的方式重复更新图像。

如果损失超过某个阈值,则突破(过度优化会产生不需要的图像伪影)​​。

最后是 DeepDream 算法的外循环。首先,我们将定义一个处理图像的尺度列表(也称为octaves)。我们将在三个不同的“八度音阶”上处理我们的图像。对于每个连续的八度音程,从最小到最大,我们将运行 20 个梯度上升步骤,gradient_ascent_loop()以最大化我们之前定义的损失。在每个倍频程之间,我们将图像放大 40%(1.4 倍):我们将从处理一个小图像开始,然后逐渐放大它(见图 12.6)。

图 12.6 DeepDream 过程:空间处理的连续尺度(八度音阶)和放大后的细节重新注入

我们在下面的代码中定义了这个过程的参数。调整这些参数将使您获得新的效果!

step = 20.           ❶
num_octave = 3       ❷
octave_scale = 1.4   ❸
iterations = 30      ❹
max_loss = 15.       ❺

梯度上升步长

运行梯度上升的尺度数

连续刻度之间的尺寸比

每个刻度的梯度上升步数

如果损失高于这个值,我们将停止一个尺度的梯度上升过程。

我们还需要几个实用函数来加载和保存图像。

清单 12.14 图像处理实用程序

import numpy as np
  
def preprocess_image(image_path):                ❶
    img = keras.utils.load_img(image_path)
    img = keras.utils.img_to_array(img)
    img = np.expand_dims(img, axis=0)
    img = keras.applications.inception_v3.preprocess_input(img)
    return img
  
def deprocess_image(img):                        ❷
    img = img.reshape((img.shape[1], img.shape[2], 3))
    img /= 2.0                                   ❸
    img += 0.5                                   ❸
    img *= 255.                                  ❸
    img = np.clip(img, 0, 255).astype("uint8")   ❹
    return img

 Util 函数可打开、调整大小并将图片格式化为适当的数组

将 NumPy 数组转换为有效图像的 Util 函数

撤消 inception v3 预处理。

转换为 uint8 并剪辑到有效范围 [0, 255]。

这是外循环。为了避免在每次连续放大后丢失大量图像细节(导致图像越来越模糊或像素化),我们可以使用一个简单的技巧:每次放大后,我们会将丢失的细节重新注入图像中,这是可能的,因为我们知道原始图像在更大的比例下应该是什么样子。给定一个小的图像大小S和一个更大的图像大小L,我们可以计算大小调整为L的原始图像与大小调整为S的原始图像之间的差异——这个差异量化了从SL时丢失的细节。

清单 12.15 在多个连续的“八度音阶”上运行梯度上升

original_img = preprocess_image(base_image_path)                              ❶
original_shape = original_img.shape[1:3]
  
successive_shapes = [original_shape]                                          ❷
for i in range(1, num_octave):                                                ❷
    shape = tuple([int(dim / (octave_scale ** i)) for dim in original_shape]) ❷
    successive_shapes.append(shape)                                           ❷
successive_shapes = successive_shapes[::-1]                                   ❷
  
shrunk_original_img = tf.image.resize(original_img, successive_shapes[0])
  
img = tf.identity(original_img)                                               ❸
for i, shape in enumerate(successive_shapes):                                 ❹
    print(f"Processing octave {i} with shape {shape}")
    img = tf.image.resize(img, shape)                                         ❺
    img = gradient_ascent_loop(                                               ❻
        img, iterations=iterations, learning_rate=step, max_loss=max_loss
    )
    upscaled_shrunk_original_img = tf.image.resize(shrunk_original_img, shape)❼
    same_size_original = tf.image.resize(original_img, shape)                 ❽
    lost_detail = same_size_original - upscaled_shrunk_original_img           ❾
    img += lost_detail                                                        ❿
    shrunk_original_img = tf.image.resize(original_img, shape)
  
keras.utils.save_img("dream.png", deprocess_image(img.numpy()))               ⓫

加载测试图像。

计算不同倍频程图像的目标形状。

复制图像(我们需要保留原件)。

迭代不同的八度音阶。

放大梦想形象。

跑梯度上升,改变梦想。

放大原始图像的较小版本:它将被像素化。

计算这个尺寸的原始图像的高质量版本。

两者的区别在于放大时丢失的细节。

将丢失的细节重新注入梦境。

保存最终结果。

注意因为最初的 Inception V3 网络经过训练以识别大小为 299 × 299 的图像中的概念,并且考虑到该过程涉及将图像缩小一个合理的因子,所以 DeepDream 实现在 300 × 之间的图像上产生了更好的结果300 和 400 × 400。无论如何,您可以在任何大小和任何比例的图像上运行相同的代码。

在 GPU 上,运行整个程序只需要几秒钟。图 12.7 显示了我们在测试图像上的梦想配置结果。

图 12.7 在测试图像上运行 DeepDream 代码

我强烈建议您通过调整在损失中使用的层来探索可以做什么。网络中较低的层包含更多局部、更少抽象的表示,并导致看起来更几何的梦想模式。基于 ImageNet 中最常见的对象(如狗眼、鸟羽等),更高的层会产生更易识别的视觉模式。您可以使用layer_settings字典中参数的随机生成来快速探索许多不同的层组合。图 12.8 显示了使用不同层配置在美味自制糕点的图像上获得的一系列结果。

图 12.8 在示例图像上尝试一系列 DeepDream 配置

12.2.2 总结

  • DeepDream 包括反向运行一个卷积网络,以根据网络学习的表示生成输入。

  • 产生的结果很有趣,并且有点类似于通过迷幻剂破坏视觉皮层在人类中引起的视觉伪影。

  • 请注意,该过程并非特定于图像模型甚至是卷积网络。它可以用于语音、音乐等。

12.3 神经风格迁移

除了 DeepDream,深度学习驱动的图像修改的另一个主要发展是神经风格迁移,由 Leon Gatys 等人介绍。在 2015 年夏天。4自最初推出以来,神经风格迁移算法经历了许多改进并产生了许多变化,并已进入许多智能手机照片应用程序。为简单起见,本节重点介绍原始论文中描述的公式。

4 Leon A. Gatys、Alexander S. Ecker 和 Matthias Bethge,“艺术风格的神经算法”,arXiv(2015 年),https: //arxiv.org/abs/1508.06576 。

神经风格迁移包括将参考图像的风格应用于目标图像,同时保留目标图像的内容。图 12.9 显示了一个示例。

图 12.9 风格迁移示例

在这种情况下,风格本质上是指图像中的纹理、颜色和视觉图案,在各种空间尺度上,内容是图像的更高层次的宏观结构。例如,蓝黄色的圆形笔触被认为是图 12.9 中的风格(使用文森特梵高的星夜),图宾根照片中的建筑物被认为是内容。

在 2015 年神经风格迁移发展之前,与纹理生成紧密相关的风格迁移概念在图像处理社区中已有很长的历史。但事实证明,基于深度学习的风格迁移的实现提供了以前经典计算机视觉技术所无法比拟的结果,它们引发了计算机视觉创造性应用的惊人复兴。

实现风格迁移背后的关键概念与所有深度学习算法的核心思想相同:您定义一个损失函数来指定您想要实现的目标,并将这种损失最小化。我们知道我们想要实现什么:在采用参考图像的风格的同时保留原始图像的内容。如果我们能够在数学上定义contentstyle,那么最小化的适当损失函数如下:

loss = (distance(style(reference_image) - style(combination_image)) +
        distance(content(original_image) - content(combination_image)))

在这里,distance是一个规范L2范数等函数content是一个获取图像并计算其内容表示的函数,并且style获取图像并计算其样式表示的函数。最小化这种损失导致style(combination_image)接近style(reference_image),并且content(combination_image)接近于content(original_image),从而实现我们定义的风格迁移。

Gatys 等人的基本观察。是深度卷积神经网络提供了一种数学定义stylecontent函数的方法。让我们看看如何。

12.3.1 内容丢失

如您所知,来自网络中较早层的激活包含有关图像的本地信息,而来自较高层的激活包含越来越多的全局抽象信息。以不同的方式表示,卷积网络不同层的激活提供了在不同空间尺度上对图像内容的分解。因此,您希望图像的内容更加全局和抽象,可以通过卷积网络中上层的表示来捕获。

因此,内容损失的一个很好的候选者是在目标图像上计算的预训练卷积网络中上层的激活与在生成的图像上计算的同一层的激活之间的 L2 范数。这保证了,从上层看,生成的图像看起来与原始目标图像相似。假设卷积网络的上层所看到的实际上是其输入图像的内容,这可以作为一种保留图像内容的方式。

12.3.2 风格损失

内容损失仅使用单个上层,但 Gatys 等人定义的样式损失。使用多层卷积网络:您尝试在卷积网络提取的所有空间尺度上捕获样式参考图像的外观,而不仅仅是单个尺度。对于风格损失,Gatys 等人。使用a的Gram 矩阵层的激活:给定层的特征图的内积。这个内积可以理解为表示层的特征之间的相关性的映射。这些特征相关性捕获特定空间尺度模式的统计数据,这些数据在经验上对应于在该尺度上发现的纹理的外观。

因此,风格损失旨在在风格参考图像和生成图像的不同层的激活中保持相似的内部相关性。反过来,这保证了在不同空间尺度上发现的纹理在样式参考图像和生成的图像中看起来相似。

简而言之,您可以使用预训练的 convnet 来定义将执行以下操作的损失:

  • 通过在原始图像和生成的图像之间保持类似的高级层激活来保留内容。卷积网络应该“看到”原始图像和生成的图像包含相同的东西。

  • 通过在激活中保持相似的相关性来保持风格对于低层和高层。特征相关性捕获纹理:生成的图像和风格参考图像应该在不同的空间尺度上共享相同的纹理。

现在让我们看一下 2015 年原始神经风格迁移算法的 Keras 实现。如您所见,它与我们在上一节中开发的 DeepDream 实现有许多相似之处。

12.3.3 Keras 中的神经风格迁移

可以使用任何预训练的卷积网络来实现神经风格迁移。在这里,我们将使用 Gatys 等人使用的 VGG19 网络。VGG19 是第 5 章介绍的 VGG16 网络的简单变体,多了三个卷积层。

这是一般过程:

  • 建立一个网络,同时为样式参考图像、基础图像和生成的图像计算 VGG19 层激活。

  • 使用在这三个图像上计算的层激活来定义前面描述的损失函数,我们将最小化它以实现风格转移。

  • 设置梯度下降过程以最小化此损失函数。

让我们从定义样式参考图像和基础图像的路径开始。为了确保处理后的图像大小相似(大小差异很大,样式转换更加困难),我们稍后会将它们全部调整为 400 像素的共享高度。

清单 12.16 获取样式和内容图像

from tensorflow import keras
  
base_image_path = keras.utils.get_file(                                 ❶
    "sf.jpg", origin="https:/ /img-datasets.s3.amazonaws.com/sf.jpg")
style_reference_image_path = keras.utils.get_file(                      ❷
    "starry_night.jpg", 
    origin="https:/ /img-datasets.s3.amazonaws.com/starry_night.jpg")
  
original_width, original_height = keras.utils.load_img(base_image_path).size
img_height = 400                                                       ❸
img_width = round(original_width * img_height / original_height)       ❸

我们要转换的图像的路径

样式图片的路径

生成图片的尺寸

我们的内容图像如图 12.10 所示,图 12.11 显示了我们的风格图像。

我们还需要一些辅助函数来加载、预处理和后处理进出 VGG19 卷积网络的图像。

图 12.10 内容图片:来自 Nob Hill 的旧金山

图 12.11 风格图:梵高的《星夜》

清单 12.17 辅助函数

import numpy as np
  
def preprocess_image(image_path):                   ❶
    img = keras.utils.load_img(
        image_path, target_size=(img_height, img_width))
    img = keras.utils.img_to_array(img)
    img = np.expand_dims(img, axis=0)
    img = keras.applications.vgg19.preprocess_input(img)
    return img
  
def deprocess_image(img):                           ❷
    img = img.reshape((img_height, img_width, 3))
    img[:, :, 0] += 103.939                         ❸
    img[:, :, 1] += 116.779                         ❸
    img[:, :, 2] += 123.68                          ❸
    img = img[:, :, ::-1]                           ❹
    img = np.clip(img, 0, 255).astype("uint8")
    return img

 Util 函数可打开、调整大小并将图片格式化为适当的数组

将 NumPy 数组转换为有效图像的 Util 函数

通过从 ImageNet 中删除平均像素值来进行零中心化。这反转了由 vgg19.preprocess_input 完成的转换。

将图像从“BGR”转换为“RGB”。这也是 vgg19.preprocess_input 反转的一部分。

让我们建立 VGG19 网络。就像在 DeepDream 示例中一样,我们将使用预训练的卷积网络创建一个特征精确器模型,该模型返回中间层的激活——这次是模型中的所有层。

清单 12.18 使用预训练的 VGG19 模型创建特征提取器

model = keras.applications.vgg19.VGG19(weights="imagenet", include_top=False)❶
  
outputs_dict = dict([(layer.name, layer.output) for layer in model.layers])
feature_extractor = keras.Model(inputs=model.inputs, outputs=outputs_dict)   ❷

构建一个加载了预训练 ImageNet 权重的 VGG19 模型。

返回每个目标层的激活值的模型(作为字典)

让我们定义内容损失,这将确保 VGG19 卷积网络的顶层具有风格图像和组合图像的相似视图。

清单 12.19 内容丢失

def content_loss(base_img, combination_img):
    return tf.reduce_sum(tf.square(combination_img - base_img))

接下来是风格损失。它使用辅助函数来计算输入矩阵的 Gram 矩阵:在原始特征矩阵中找到的相关映射。

清单 12.20 样式丢失

def gram_matrix(x):
    x = tf.transpose(x, (2, 0, 1))
    features = tf.reshape(x, (tf.shape(x)[0], -1))
    gram = tf.matmul(features, tf.transpose(features))
    return gram
  
def style_loss(style_img, combination_img):
    S = gram_matrix(style_img)
    C = gram_matrix(combination_img)
    channels = 3
    size = img_height * img_width
    return tf.reduce_sum(tf.square(S - C)) / (4.0 * (channels ** 2) * (size ** 2))

在这两个损失成分中,您添加了第三个:总变化损失,它运行在生成的组合图像的像素上。它鼓励生成图像的空间连续性,从而避免过度像素化的结果。您可以将其解释为正则化损失。

清单 12.21 总变化损失

def total_variation_loss(x):
    a = tf.square(
        x[:, : img_height - 1, : img_width - 1, :] - x[:, 1:, : img_width - 1, :]
    )
    b = tf.square(
        x[:, : img_height - 1, : img_width - 1, :] - x[:, : img_height - 1, 1:, :]
    )
    return tf.reduce_sum(tf.pow(a + b, 1.25))

您最小化的损失是这三个损失的加权平均值。要计算内容损失,您只使用一个上层——即block5_conv2层——而对于样式损失,您使用跨越低级和高级层的层列表。最后添加总变化损失。

根据您使用的样式参考图像和内容图像,您可能需要调整content_weight系数(内容损失对总损失的贡献)。越高content_weight意味着目标内容将在生成的图像中更容易识别。

清单 12.22 定义要最小化的最终损失

style_layer_names = [                                                       ❶
    "block1_conv1",
    "block2_conv1",
    "block3_conv1",
    "block4_conv1",
    "block5_conv1",
]
content_layer_name = "block5_conv2"                                         ❷
total_variation_weight = 1e-6                                               ❸
style_weight = 1e-6                                                         ❹
content_weight = 2.5e-8                                                     ❺
  
def compute_loss(combination_image, base_image, style_reference_image):
    input_tensor = tf.concat(
        [base_image, style_reference_image, combination_image], axis=0)
    features = feature_extractor(input_tensor)
    loss = tf.zeros(shape=())                                               ❻
    layer_features = features[content_layer_name]                           ❼
    base_image_features = layer_features[0, :, :, :]                        ❼
    combination_features = layer_features[2, :, :, :]                       ❼
    loss = loss + content_weight * content_loss(                            ❼
        base_image_features, combination_features                           ❼
    )
    for layer_name in style_layer_names:                                    ❽
        layer_features = features[layer_name]                               ❽
        style_reference_features = layer_features[1, :, :, :]               ❽
        combination_features = layer_features[2, :, :, :]                   ❽
        style_loss_value = style_loss(                                      ❽
            style_reference_features, combination_features)                 ❽
        loss += (style_weight / len(style_layer_names)) * style_loss_value  ❽
  
    loss += total_variation_weight * total_variation_loss(combination_image)❾
    return loss

用于样式损失的层列表

用于内容丢失的层

总变异损失的贡献权重

 style loss的贡献权重

内容损失的贡献权重

将损失初始化为 0。

添加内容丢失。

添加样式损失。

添加总变化损失。

最后,让我们设置梯度下降过程。在最初的 Gatys 等人中。在论文中,优化是使用 L-BFGS 算法执行的,但这在 TensorFlow 中不可用,所以我们将只使用小批量梯度下降而是优化器SGD。我们将利用您以前从未见过的优化器功能:学习率计划。我们将使用它逐渐将学习率从一个非常高的值(100)降低到一个小得多的最终值(大约 20)。这样,我们将在训练的早期阶段取得快速进展,然后在接近损失最小值时更加谨慎地进行。

清单 12.23 设置梯度下降过程

import tensorflow as tf
  
@tf.function                                                           ❶
def compute_loss_and_grads(
    combination_image, base_image, style_reference_image):
    with tf.GradientTape() as tape:
        loss = compute_loss(
            combination_image, base_image, style_reference_image)
    grads = tape.gradient(loss, combination_image)
    return loss, grads
  
optimizer = keras.optimizers.SGD(
    keras.optimizers.schedules.ExponentialDecay(                       ❷
        initial_learning_rate=100.0, decay_steps=100, decay_rate=0.96 
    )
)
  
base_image = preprocess_image(base_image_path)
style_reference_image = preprocess_image(style_reference_image_path)
combination_image = tf.Variable(preprocess_image(base_image_path))     ❸
  
iterations = 4000 
for i in range(1, iterations + 1): 
    loss, grads = compute_loss_and_grads(
        combination_image, base_image, style_reference_image
    )
    optimizer.apply_gradients([(grads, combination_image)])            ❹
    if i % 100 == 0:
        print(f"Iteration {i}: loss={loss:.2f}")
        img = deprocess_image(combination_image.numpy())
        fname = f"combination_image_at_iteration_{i}.png" 
        keras.utils.save_img(fname, img)                     

我们通过将其编译为 tf.function 来加快训练步骤。

我们将从 100 的学习率开始,每 100 步降低 4%。

使用变量来存储组合图像,因为我们将在训练期间对其进行更新。

向减少风格迁移损失的方向更新组合图像。

定期保存组合图像。

图 12.12 显示了你得到的结果。请记住,这种技术所实现的仅仅是图像重新纹理化或纹理转移的一种形式。它最适用于具有强烈纹理和高度自相似性的样式参考图像,以及不需要高水平细节即可识别的内容目标。它通常无法实现相当抽象的壮举,例如将一幅肖像的风格转移到另一幅肖像。该算法更接近于经典信号处理而不是人工智能,所以不要指望它会像魔术一样工作!

图 12.12 风格转换结果

此外,请注意,这种风格转移算法运行速度很慢。但是该设置操作的转换非常简单,只要您有适当的可用训练数据,它也可以通过一个小型、快速的前馈卷积网络来学习。因此,可以通过首先花费大量计算周期来使用此处概述的方法为固定的风格参考图像生成输入-输出训练示例,然后训练一个简单的卷积网络来学习这种特定于风格的转换,来实现快速风格转移。一旦完成,对给定图像进行样式化是即时的:它只是这个小型卷积网络的前向传递。

12.3.4 总结

  • 风格转移包括创建一个新图像,该图像保留目标图像的内容,同时捕获参考图像的风格。

  • 内容可以通过卷积网络的高级激活来捕获。

  • 风格可以通过卷积网络不同层激活的内部相关性来捕捉。

  • 因此,深度学习允许将样式迁移制定为使用预训练卷积网络定义的损失的优化过程。

  • 从这个基本思想开始,许多变体和改进都是可能的。

12.4 用变分自编码器生成图像

当今最流行和最成功的创意 AI 应用是图像生成:学习潜在的视觉空间并从中采样,以创建从真实图像插值的全新图片——想象中的人、想象中的地方、想象中的猫和狗等的图片。

在本节和下一节中,我们将回顾一些与图像生成有关的高级概念,以及与该领域的两种主要技术相关的实现细节:变分自动编码器(VAE) 和生成对抗网络(GAN)。注意我将在这里介绍的技术并不特定于图像——你可以使用 GAN 和 VAE 开发声音、音乐甚至文本的潜在空间——但在实践中,最有趣的结果是通过图片获得的,这就是我们将重点介绍这里。

12.4.1 从图像的潜在空间中采样

图像生成的关键思想是开发一个表示的低维潜在空间(与深度学习中的其他一切一样,它是一个向量空间),其中任何点都可以映射到“有效”图像:看起来喜欢真实的东西。能够实现这种映射的模块,将潜在点作为输入并输出图像(像素网格),称为生成器(在 GAN 的情况下)或解码器(在 VAE 的情况下)。一旦学习了这样的潜在空间,您就可以从中采样点,并将它们映射回图像空间,生成以前从未见过的图像(见图 12.13)。这些新图像是训练图像的中间部分。

图 12.13 学习图像的潜在向量空间并使用它来采样新图像

GAN 和 VAE 是学习这种图像表示的潜在空间的两种不同策略,每种策略都有自己的特点。VAE 非常适合学习结构良好的潜在空间,其中特定方向编码数据中有意义的变化轴(见图 12.14)。GAN 生成的图像可能非常逼真,但它们来自的潜在空间可能没有那么多的结构和连续性。

图 12.14 Tom White 使用 VAE 生成的连续人脸空间

12.4.2 图像编辑的概念向量

当我们在第 11 章讨论词嵌入时,我们已经暗示了概念向量的想法。这个想法仍然是一样的:给定一个潜在的表示空间,或一个嵌入空间,空间中的某些方向可能会编码有趣的变化轴原始数据。例如,在人脸图像的潜在空间中,可能有一个微笑向量,这样如果潜在点z是某个人脸的嵌入表示,那么潜在点z + s是同一张脸的嵌入表示,微笑。一旦确定了这样的向量,就可以通过将图像投影到潜在空间中来编辑图像,以有意义的方式移动它们的表示,然后将它们解码回图像空间。对于图像空间中基本上任何独立维度的变化都有概念向量——在人脸的情况下,您可能会发现用于在脸上添加太阳镜、移除眼镜、将男性脸变成女性脸等的向量。图 12.15 是一个微笑向量的示例,它是由新西兰维多利亚大学设计学院的 Tom White 发现的概念向量,使用在名人面孔数据集(CelebA 数据集)上训练的 VAE。

图 12.15 微笑矢量

12.4.3 变分自编码器

变分自动编码器,由 Kingma 和 Welling 在 2013 年 12 月同时发现5和 Rezende、Mohamed 和 Wierstra,2014 年 1 月,6是一种生成模型,特别适用于通过概念向量进行图像编辑的任务。它们是对自动编码器(一种旨在将输入编码到低维潜在空间然后将其解码回来的网络)的现代诠释,它将来自深度学习的想法与贝叶斯推理相结合。

5 Diederik P. Kingma 和 Max Welling,“自动编码变分贝叶斯”,arXiv(2013 年),https://arxiv.org/abs/1312.6114

6 Danilo Jimenez Rezende、Shakir Mohamed 和 Daan Wierstra,“深度生成模型中的随机反向传播和近似推理”,arXiv(2014 年),https: //arxiv.org/abs/1401.4082 。

经典的图像自动编码器获取图像,通过编码器模块将其映射到潜在向量空间,然后通过解码器模块将其解码回与原始图像具有相同尺寸的输出(见图 12.16)。然后通过使用与输入图像相同的图像作为目标数据对其进行训练,这意味着自动编码器学习重建原始输入。通过对代码(编码器的输出)施加各种约束,您可以让自动编码器学习或多或少有趣的数据潜在表示。最常见的是,您会将代码限制为低维和稀疏的(主要是零),在这种情况下,编码器充当将输入数据压缩为更少信息位的一种方式。

图 12.16 自编码器将输入x映射到压缩表示,然后将其解码为x '

在实践中,这种经典的自动编码器不会导致特别有用或结构良好的潜在空间。他们也不擅长压缩。由于这些原因,它们在很大程度上已经过时了。然而,VAE 用一点点统计魔法来增强自动编码器,迫使它们学习连续的、高度结构化的潜在空间。事实证明,它们是图像生成的强大工具。

VAE 不是将其输入图像压缩为潜在空间中的固定代码,而是将图像转换为统计分布的参数:均值和方差。本质上,这意味着我们假设输入图像是通过统计过程生成的,并且在编码和解码过程中应该考虑到这个过程的随机性。然后,VAE 使用均值和方差参数对分布的一个元素进行随机抽样,并将该元素解码回原始输入(见图 12.17)。这个过程的随机性提高了鲁棒性并迫使潜在空间在任何地方编码有意义的表示:在潜在空间中采样的每个点都被解码为有效的输出。

图 12.17 VAE 将图像映射到两个向量z_meanz_log_sigma,它们定义了潜在空间上的概率分布,用于对潜在点进行采样以进行解码。

用技术术语来说,VAE 的工作原理如下:

  1. 编码器模块将输入样本 ,input_img转换为表示的潜在空间中的两个参数,z_meanz_log_variance

  2. z您从假设生成输入图像的潜在正态分布中随机采样一个点,通过z = z_mean + exp(z_log_variance) * epsilon,其中epsilon是小值的随机张量。

  3. 解码器模块将潜在空间中的这一点映射回原始输入图像。

因为epsilon是随机的,所以该过程确保靠近您编码input_imgz-mean) 的潜在位置的每个点都可以解码为类似于 的东西input_img,从而迫使潜在空间持续有意义。潜在空间中的任何两个接近点都将解码为高度相似的图像。连续性与潜在空间的低维相结合,迫使潜在空间中的每个方向都对数据变化的有意义轴进行编码,使潜在空间非常结构化,因此非常适合通过概念向量进行操作。

VAE 的参数通过两个损失函数进行训练解码样本以匹配初始输入,以及有助于学习全面的潜在分布并减少对训练数据的过度拟合。从示意图上看,该过程如下所示:

z_mean, z_log_variance = encoder(input_img)   ❶
z = z_mean + exp(z_log_variance) * epsilon    ❷
reconstructed_img = decoder(z)                ❸
model = Model(input_img, reconstructed_img)   ❹

将输入编码为均值和方差参数

使用小的随机 epsilon 绘制潜在点

将 z 解码回图像

实例化自动编码器模型,将输入图像映射到其重建

然后,您可以使用重建损失和正则化损失来训练模型。对于正则化损失,我们通常使用一个表达式(Kullback-Leibler 散度),旨在将编码器输出的分布推向以 0 为中心的全面正态分布。这为编码器提供了一个关于结构的合理假设它正在建模的潜在空间。

现在让我们看看在实践中实现 VAE 是什么样子的!

12.4.4 使用 Keras 实现 VAE

我们将实现一个可以生成 MNIST 数字的 VAE。它将分为三个部分:

  • 将真实图像转换为潜在空间中的均值和方差的编码器网络

  • 一个采样层,它采用这样的均值和方差,并使用它们从潜在空间中采样一个随机点

  • 将潜在空间中的点转换回图像的解码器网络

下面的清单显示了我们将使用的编码器网络,将图像映射到潜在空间上的概率分布参数。这是一个简单的卷积网络,将输入图像映射x到两个向量,z_mean并且z_log_var. 一个重要的细节是我们使用 strides 来对特征图进行下采样,而不是最大池化。我们最后一次这样做是在第 9 章的图像分割示例中。回想一下,一般来说,对于任何关心信息位置的模型来说,strides 比 max pooling 更可取——也就是说也就是说,图像中的东西在哪里——而这个确实如此,因为它必须产生一种可用于重建有效图像的图像编码。

清单 12.24 VAE 编码器网络

from tensorflow import keras 
from tensorflow.keras import layers
  
latent_dim = 2                                              ❶
  
encoder_inputs = keras.Input(shape=(28, 28, 1))
x = layers.Conv2D(
    32, 3, activation="relu", strides=2, padding="same")(encoder_inputs)
x = layers.Conv2D(64, 3, activation="relu", strides=2, padding="same")(x)
x = layers.Flatten()(x)
x = layers.Dense(16, activation="relu")(x)
z_mean = layers.Dense(latent_dim, name="z_mean")(x)         ❷
z_log_var = layers.Dense(latent_dim, name="z_log_var")(x)   ❷
encoder = keras.Model(encoder_inputs, [z_mean, z_log_var], name="encoder")

潜在空间的维度:二维平面

输入图像最终被编码为这两个参数。

它的摘要如下所示:

>>> encoder.summary()
Model: "encoder" 
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to
==================================================================================================
input_1 (InputLayer)            [(None, 28, 28, 1)]  0 
__________________________________________________________________________________________________
conv2d (Conv2D)                 (None, 14, 14, 32)   320         input_1[0][0]
__________________________________________________________________________________________________
conv2d_1 (Conv2D)               (None, 7, 7, 64)     18496       conv2d[0][0]
__________________________________________________________________________________________________
flatten (Flatten)               (None, 3136)         0           conv2d_1[0][0]
__________________________________________________________________________________________________
dense (Dense)                   (None, 16)           50192       flatten[0][0]
__________________________________________________________________________________________________
z_mean (Dense)                  (None, 2)            34          dense[0][0]
__________________________________________________________________________________________________
z_log_var (Dense)               (None, 2)            34          dense[0][0]
==================================================================================================
Total params: 69,076 
Trainable params: 69,076 
Non-trainable params: 0 
__________________________________________________________________________________________________

接下来是使用z_mean和的代码z_log_var,假设已经产生了统计分布的参数input_img,以生成潜在空间点z

清单 12.25 潜在空间采样层

import tensorflow as tf
  
class Sampler(layers.Layer):
    def call(self, z_mean, z_log_var):
        batch_size = tf.shape(z_mean)[0]
        z_size = tf.shape(z_mean)[1]
        epsilon = tf.random.normal(shape=(batch_size, z_size))  ❶
        return z_mean + tf.exp(0.5 * z_log_var) * epsilon       ❷

绘制一批随机法向量。

应用 VAE 采样公式。

下面的清单显示了解码器的实现。我们将向量重塑为z图像的尺寸,然后使用一些卷积层来获得与原始尺寸相同的最终图像输出input_img

清单 12.26 VAE 解码器网络,将潜在空间点映射到图像

latent_inputs = keras.Input(shape=(latent_dim,))                                  ❶
x = layers.Dense(7 * 7 * 64, activation="relu")(latent_inputs)                    ❷
x = layers.Reshape((7, 7, 64))(x)                                                 ❸
x = layers.Conv2DTranspose(64, 3, activation="relu", strides=2, padding="same")(x)❹
x = layers.Conv2DTranspose(32, 3, activation="relu", strides=2, padding="same")(x)❹
decoder_outputs = layers.Conv2D(1, 3, activation="sigmoid", padding="same")(x)    ❺
decoder = keras.Model(latent_inputs, decoder_outputs, name="decoder")

输入我们将在哪里喂 z

产生与我们在编码器的 Flatten 层级别相同数量的系数。

恢复编码器的 Flatten 层。

还原编码器的 Conv2D 层。

输出以形状 (28, 28, 1) 结束。

它的摘要如下所示:

>>> decoder.summary()
Model: "decoder" 
_________________________________________________________________
Layer (type)                 Output Shape              Param # 
=================================================================
input_2 (InputLayer)         [(None, 2)]               0 
_________________________________________________________________
dense_1 (Dense)              (None, 3136)              9408 
_________________________________________________________________
reshape (Reshape)            (None, 7, 7, 64)          0 
_________________________________________________________________
conv2d_transpose (Conv2DTran (None, 14, 14, 64)        36928 
_________________________________________________________________
conv2d_transpose_1 (Conv2DTr (None, 28, 28, 32)        18464 
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 28, 28, 1)         289 
=================================================================
Total params: 65,089 
Trainable params: 65,089 
Non-trainable params: 0 
_________________________________________________________________

现在让我们自己创建 VAE 模型。这是您第一个不进行监督学习的模型示例(自动编码器是自监督学习的示例,因为它使用其输入作为目标)。每当您离开经典的监督学习时,通常都会对类进行子Model类化并实现一个自定义train_ step()来指定新的训练逻辑,你在第 7 章中学到的工作流程。这就是我们在这里要做的。

清单 12.27 带有自定义的 VAE 模型train_step()

class VAE(keras.Model):
    def __init__(self, encoder, decoder, **kwargs):
        super().__init__(**kwargs)
        self.encoder = encoder
        self.decoder = decoder
        self.sampler = Sampler()
        self.total_loss_tracker = keras.metrics.Mean(name="total_loss") ❶
        self.reconstruction_loss_tracker = keras.metrics.Mean(          ❶
            name="reconstruction_loss")
        self.kl_loss_tracker = keras.metrics.Mean(name="kl_loss")       ❶
  
    @property
    def metrics(self):                                                  ❷
        return [self.total_loss_tracker,
                self.reconstruction_loss_tracker,
                self.kl_loss_tracker]
  
    def train_step(self, data):
        with tf.GradientTape() as tape:
            z_mean, z_log_var = self.encoder(data)
            z = self.sampler(z_mean, z_log_var)
            reconstruction = decoder(z)
            reconstruction_loss = tf.reduce_mean(                       ❸
                tf.reduce_sum(                      
                    keras.losses.binary_crossentropy(data, reconstruction),
                    axis=(1, 2)
                )
            )
            kl_loss = -0.5 * (1 + z_log_var - tf.square(z_mean) -       ❹ tf.exp(z_log_var))                                       ❹
            total_loss = reconstruction_loss + tf.reduce_mean(kl_loss)  ❹
        grads = tape.gradient(total_loss, self.trainable_weights)
        self.optimizer.apply_gradients(zip(grads, self.trainable_weights))
        self.total_loss_tracker.update_state(total_loss)
        self.reconstruction_loss_tracker.update_state(reconstruction_loss)
        self.kl_loss_tracker.update_state(kl_loss)
        return {
            "total_loss": self.total_loss_tracker.result(),
            "reconstruction_loss": self.reconstruction_loss_tracker.result(),
            "kl_loss": self.kl_loss_tracker.result(),
        }

我们使用这些指标来跟踪每个时期的损失平均值。

我们在 metrics 属性中列出指标,以使模型能够在每个 epoch 之后(或在多次调用 fit()/evaluate() 之间)重置它们。

我们在空间维度(轴 1 和轴 2)上对重建损失求和,并在批次维度上取其平均值。

添加正则化项(Kullback-Leibler 散度)。

最后,我们准备在 MNIST 数字上实例化和训练模型。因为损失是在自定义层中处理的,所以我们没有在编译时指定外部损失(loss=None),这反过来意味着我们不会在训练期间传递目标数据(如您所见,我们只传递x_train给中的模型fit())。

清单 12.28 训练 VAE

import numpy as np
  
(x_train, _), (x_test, _) = keras.datasets.mnist.load_data()
mnist_digits = np.concatenate([x_train, x_test], axis=0)               ❶
mnist_digits = np.expand_dims(mnist_digits, -1).astype("float32") / 255 
  
vae = VAE(encoder, decoder)
vae.compile(optimizer=keras.optimizers.Adam(), run_eagerly=True)       ❷
vae.fit(mnist_digits, epochs=30, batch_size=128)                       ❸

我们在所有 MNIST 数字上进行训练,因此我们将训练和测试样本连接起来。

请注意,我们没有在 compile() 中传递损失参数,因为损失已经是 train_step() 的一部分。

请注意,我们不会在 fit() 中传递目标,因为 train_step() 不期望任何目标。

模型训练好后,我们可以使用decoder网络将任意潜在空间向量转换为图像。

清单 12.29 从 2D 潜在空间中对图像网格进行采样

import matplotlib.pyplot as plt
  
n = 30                                                        ❶
digit_size = 28 
figure = np.zeros((digit_size * n, digit_size * n))
  
grid_x = np.linspace(-1, 1, n)                                ❷
grid_y = np.linspace(-1, 1, n)[::-1]                          ❷
  
for i, yi in enumerate(grid_y):                               ❸
    for j, xi in enumerate(grid_x):                           ❸
        z_sample = np.array([[xi, yi]])                       ❹
        x_decoded = vae.decoder.predict(z_sample)             ❹
        digit = x_decoded[0].reshape(digit_size, digit_size)  ❹
        figure[
            i * digit_size : (i + 1) * digit_size,
            j * digit_size : (j + 1) * digit_size,
        ] = digit
  
plt.figure(figsize=(15, 15))
start_range = digit_size // 2 
end_range = n * digit_size + start_range
pixel_range = np.arange(start_range, end_range, digit_size)
sample_range_x = np.round(grid_x, 1)
sample_range_y = np.round(grid_y, 1)
plt.xticks(pixel_range, sample_range_x)
plt.yticks(pixel_range, sample_range_y)
plt.xlabel("z[0]")
plt.ylabel("z[1]")
plt.axis("off")
plt.imshow(figure, cmap="Greys_r")

我们将显示一个 30 × 30 位的网格(总共 900 位)。

在 2D 网格上线性采样点。

遍历网格位置。

对于每个位置,采样一个数字并将其添加到我们的图形中。

采样数字的网格(见图 12.18)显示了不同数字类别的完全连续分布,当您沿着一条穿过潜在空间的路径时,一个数字会变形为另一个数字。这个空间中的特定方向是有含义的:例如,有“五”、“一”等方向。

图 12.18 从潜在空间解码的数字网格

在下一节中,我们将详细介绍生成人工图像的其他主要工具:生成对抗网络 (GAN)。

12.4.5 总结

  • 深度学习的图像生成是通过学习捕获有关图像数据集的统计信息的潜在空间来完成的。通过从潜在空间中采样和解码点,您可以生成前所未见的图像。有两个主要工具可以做到这一点:VAE 和 GAN。

  • VAE 产生高度结构化、连续的潜在表示。出于这个原因,它们非常适合在潜在空间中进行各种图像编辑:换脸、将皱眉的脸变成笑脸等等。它们还可以很好地用于制作基于潜在空间的动画,例如动画沿着潜在空间的横截面行走或显示起始图像以连续方式缓慢变形为不同图像。

  • GAN 能够生成逼真的单帧图像,但可能不会产生具有坚实结构和高连续性的潜在空间。

我见过的大多数成功的图像实际应用都依赖于 VAE,但 GAN 在学术研究领域一直很受欢迎。您将在下一节中了解它们的工作原理以及如何实现它们。

12.5 生成对抗网络简介

生成对抗网络 (GAN),由 Goodfellow 等人于 2014 年引入,图7是用于学习图像潜在空间的 VAE 的替代方案。它们通过强制生成的图像在统计上与真实图像几乎没有区别,从而能够生成相当逼真的合成图像。

7 Ian Goodfellow 等人,“生成对抗网络”,arXiv (2014),https://arxiv.org/abs/1406.2661

理解 GAN 的一种直观方法是想象一个伪造者试图创造一幅伪造的毕加索画作。起初,伪造者不擅长这项任务。他将他的一些赝品与真正的毕加索混合在一起,然后将它们全部展示给艺术品经销商。艺术品经销商对每幅画作进行真实性评估,并向伪造者反馈是什么让毕加索看起来像毕加索。伪造者回到他的工作室准备一些新的假货。随着时间的推移,伪造者在模仿毕加索的风格方面变得越来越有能力,而艺术品经销商在发现赝品方面也变得越来越熟练。最后,他们手上有一些优秀的假毕加索。

这就是 GAN 的本质:一个伪造网络和一个专家网络,每个网络都被训练成最好的另一个。因此,GAN 由两部分组成:

  • 发电机网络——作为输入一个随机向量(潜在空间中的一个随机点),并将其解码为合成图像

  • 鉴别器网络(或对手) ——作为输入图像(真实的或合成的),并预测图像是来自训练集还是由生成器网络创建

生成器网络经过训练能够欺骗鉴别器网络,因此随着训练的进行,它会朝着生成越来越逼真的图像发展:看起来与真实图像无法区分的人造图像,以至于鉴别器网络不可能告诉两个分开(见图 12.19)。同时,判别器不断适应生成器逐渐提高的能力,为生成的图像设定了很高的真实感。一旦训练结束,生成器就能够将其输入空间中的任何点变成可信的图像。与 VAE 不同,这个潜在空间对有意义结构的明确保证较少;特别是,它不是连续的。

图 12.19 生成器将随机潜在向量转换为图像,鉴别器试图将真实图像与生成的图像区分开来。生成器被训练来欺骗鉴别器。

值得注意的是,GAN 是一个优化最小值不固定的系统,这与您在本书中遇到的任何其他训练设置不同。通常,梯度下降包括在静态损失景观中滚下山坡。但有了 GAN,下山的每一步都会稍微改变整个景观。这是一个动态系统,优化过程不是寻求最小值,而是寻求两种力之间的平衡。出于这个原因,GAN 是出了名的难以训练——让 GAN 工作需要对模型架构和训练参数进行大量仔细的调整。

12.5.1 GAN 实现示意图

在本节中,我们将解释如何以最简单的形式在 Keras 中实现 GAN。GAN 是先进的,因此深入研究生成图 12.20 中图像的 StyleGAN2 等架构的技术细节超出了本书的范围。我们将在本次演示中使用的具体实现深度卷积 GAN (DCGAN):一个非常基本的 GAN,其中生成器和判别器是深度卷积网络。

图 12.20 潜在的太空居民。https://thispersondoesnotexist.com使用 StyleGAN2 模型生成的图像。(图片来源:Phillip Wang 是网站作者。使用的模型是来自 Karras 等人的 StyleGAN2 模型,https: //arxiv.org/abs/1912.04958 。)

我们将在大型 CelebFaces 属性数据集(称为 CelebA)的图像上训练我们的 GAN,该数据集包含 200,000 张名人面孔(http://mmlab.ie.cuhk.edu.hk/projects/CelebA.html)为了加快训练速度,我们将图像大小调整为 64 × 64,因此我们将学习生成 64 × 64 的人脸图像。

从示意图上看,GAN 如下所示:

  • 网络generator地图形状向量到形状(latent_dim,)图像(64, 64, 3)

  • 网络discriminator将形状图像映射(64, 64, 3)到估计图像真实概率的二进制分数。

  • 网络gan将生成器和鉴别器链接在一起gan(x) = discriminator(generator(x)):因此,该gan网络将潜在空间向量映射到鉴别器对生成器解码的这些潜在向量的真实性的评估。

  • 我们使用真实和虚假图像的示例以及“真实”/“虚假”标签来训练鉴别器,就像我们训练任何常规图像分类模型一样。

  • 为了训练生成器,我们使用生成器权重关于损失的梯度gan模型。这意味着在每一步中,我们都会将生成器的权重移动到一个方向,使鉴别器更有可能将生成器解码的图像分类为“真实”图像。换句话说,我们训练生成器来欺骗判别器。

12.5.2  A bag of tricks

众所周知,训练 GAN 和调整 GAN 实现的过程非常困难。您应该记住许多已知的技巧。就像深度学习中的大多数事情一样,它更像是炼金术而不是科学:这些技巧是启发式的,而不是理论支持的指导方针。它们得到了对手头现象的一定程度的直观理解的支持,并且众所周知,它们在经验上工作得很好,尽管不一定在所有情况下都如此。

以下是本节中实现 GAN 生成器和判别器时使用的一些技巧。这不是 GAN 相关技巧的详尽列表。您会在 GAN 文献中找到更多内容:

  • 我们在鉴别器中使用 strides 而不是池化来对特征图进行下采样,就像我们在 VAE 编码器中所做的那样。

  • 我们使用正态分布(高斯分布)而不是均匀分布从潜在空间中采样点。

  • 随机性有利于诱导鲁棒性。因为 GAN 训练会导致动态平衡,所以 GAN 很可能会以各种方式陷入困境。在训练期间引入随机性有助于防止这种情况。我们通过向鉴别器的标签添加随机噪声来引入随机性。

  • 稀疏梯度会阻碍 GAN 训练。在深度学习中,稀疏性通常是一个理想的属性,但在 GAN 中则不然。有两件事会导致梯度稀疏:最大池化操作relu激活。我们建议不要使用最大池化,而是使用跨步卷积进行下采样,并且我们建议使用LeakyReLU层来代替relu激活。它类似于relu,但它通过允许小的负激活值来放松稀疏约束。

  • 在生成的图像中,经常会看到由生成器中像素空间覆盖不均引起的棋盘伪影(见图 12.21)。为了解决这个问题,每当我们使用跨步Conv2DTransposeConv2D在生成器和判别器中使用时,我们都会使用一个可以被跨步大小整除的内核大小。

图 12.21 步幅和内核大小不匹配导致的棋盘伪影,导致像素空间覆盖不均:GAN 的众多陷阱之一

12.5.3 掌握 CelebA 数据集

您可以从网站手动下载数据集:http: //mmlab.ie.cuhk.edu.hk/projects/CelebA.html。如果您使用的是 Colab,您可以运行以下命令从 Google Drive 下载数据并解压缩。

清单 12.30 获取 CelebA 数据

!mkdir celeba_gan                                                      ❶
!gdown --id 1O7m1010EJjLE5QxLZiM9Fpjs7Oj6e684 -O celeba_gan/data.zip   ❷
!unzip -qq celeba_gan/data.zip -d celeba_gan  

创建工作目录。

使用 gdown 下载压缩数据(在 Colab 中默认可用;否则安装它)。

解压缩数据。

在目录中获得未压缩的图像后,您可以image_dataset_from_directory将其转换为数据集。由于我们只需要图像——没有标签——我们将指定label_mode=None.

清单 12.31 从图像目录创建数据集

from tensorflow import keras
dataset = keras.utils.image_dataset_from_directory(
    "celeba_gan",
    label_mode=None,      ❶
    image_size=(64, 64),
    batch_size=32,
    smart_resize=True)    ❷

只返回图像——没有标签。

我们将使用裁剪和调整大小的智能组合将图像大小调整为 64 × 64,以保持纵横比。我们不希望面部比例失真!

最后,让我们将图像重新缩放到[0-1]范围。

清单 12.32 重新缩放图像

dataset = dataset.map(lambda x: x / 255.)

您可以使用以下代码显示示例图像。

清单 12.33 显示第一张图片

import matplotlib.pyplot as plt 
for x in dataset:
    plt.axis("off")
    plt.imshow((x.numpy() * 255).astype("int32")[0])
    break

12.5.4 鉴别器

首先,我们将开发一个discriminator模型,该模型将候选图像(真实或合成)作为输入,并将其分类为两个类别之一:“生成的图像”或“来自训练集的真实图像”。GAN 经常出现的众多问题之一是生成器卡在生成的看起来像噪声的图像上。一种可能的解决方案是在鉴别器中使用 dropout,这就是我们将在这里做的。

清单 12.34 GAN 鉴别器网络

from tensorflow.keras import layers
  
discriminator = keras.Sequential(
    [
        keras.Input(shape=(64, 64, 3)),
        layers.Conv2D(64, kernel_size=4, strides=2, padding="same"),
        layers.LeakyReLU(alpha=0.2),
        layers.Conv2D(128, kernel_size=4, strides=2, padding="same"),
        layers.LeakyReLU(alpha=0.2),
        layers.Conv2D(128, kernel_size=4, strides=2, padding="same"),
        layers.LeakyReLU(alpha=0.2),
        layers.Flatten(),
        layers.Dropout(0.2),                   ❶
        layers.Dense(1, activation="sigmoid"),
    ],
    name="discriminator",
)

一个 dropout 层:一个重要的技巧!

这是判别器模型摘要:

>>> discriminator.summary()
Model: "discriminator" 
_________________________________________________________________
Layer (type)                 Output Shape              Param # 
=================================================================
conv2d (Conv2D)              (None, 32, 32, 64)        3136 
_________________________________________________________________
leaky_re_lu (LeakyReLU)      (None, 32, 32, 64)        0 
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 16, 16, 128)       131200 
_________________________________________________________________
leaky_re_lu_1 (LeakyReLU)    (None, 16, 16, 128)       0 
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 8, 8, 128)         262272 
_________________________________________________________________
leaky_re_lu_2 (LeakyReLU)    (None, 8, 8, 128)         0 
_________________________________________________________________
flatten (Flatten)            (None, 8192)              0 
_________________________________________________________________
dropout (Dropout)            (None, 8192)              0 
_________________________________________________________________
dense (Dense)                (None, 1)                 8193 
=================================================================
Total params: 404,801 
Trainable params: 404,801 
Non-trainable params: 0 
_________________________________________________________________

12.5.5 生成器

接下来,让我们开发一个generator模型,将向量(来自潜在空间——在训练期间将随机采样)转换为候选图像。

清单 12.35 GAN 生成器网络

latent_dim = 128                                                              ❶
  
generator = keras.Sequential(
    [
        keras.Input(shape=(latent_dim,)),
        layers.Dense(8 * 8 * 128),                                            ❷
        layers.Reshape((8, 8, 128)),                                          ❸
        layers.Conv2DTranspose(128, kernel_size=4, strides=2, padding="same"),❹
        layers.LeakyReLU(alpha=0.2),                                          ❺
        layers.Conv2DTranspose(256, kernel_size=4, strides=2, padding="same"),❹
        layers.LeakyReLU(alpha=0.2),                                          ❺
        layers.Conv2DTranspose(512, kernel_size=4, strides=2, padding="same"),❹
        layers.LeakyReLU(alpha=0.2),                                          ❺
        layers.Conv2D(3, kernel_size=5, padding="same", activation="sigmoid"),❻
    ],
    name="generator",
)

潜在空间将由 128 维向量组成。

产生与编码器 Flatten 层相同数量的系数。

恢复编码器的 Flatten 层。

还原编码器的 Conv2D 层。

我们使用 LeakyReLU 作为我们的激活。

输出以形状 (28, 28, 1) 结束。

这是生成器模型摘要:

>>> generator.summary()
Model: "generator" 
_________________________________________________________________
Layer (type)                 Output Shape              Param # 
=================================================================
dense_1 (Dense)              (None, 8192)              1056768 
_________________________________________________________________
reshape (Reshape)            (None, 8, 8, 128)         0 
_________________________________________________________________
conv2d_transpose (Conv2DTran (None, 16, 16, 128)       262272 
_________________________________________________________________
leaky_re_lu_3 (LeakyReLU)    (None, 16, 16, 128)       0 
_________________________________________________________________
conv2d_transpose_1 (Conv2DTr (None, 32, 32, 256)       524544 
_________________________________________________________________
leaky_re_lu_4 (LeakyReLU)    (None, 32, 32, 256)       0 
_________________________________________________________________
conv2d_transpose_2 (Conv2DTr (None, 64, 64, 512)       2097664 
_________________________________________________________________
leaky_re_lu_5 (LeakyReLU)    (None, 64, 64, 512)       0 
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 64, 64, 3)         38403 
=================================================================
Total params: 3,979,651 
Trainable params: 3,979,651 
Non-trainable params: 0 
_________________________________________________________________

12.5.6 对抗网络

最后,我们将设置 GAN,它将生成器和判别器链接起来。训练后,该模型将使生成器朝一个方向移动,以提高其欺骗鉴别器的能力。该模型将潜在空间点转换为分类决策——“假”或“真实”——它的目的是使用始终为“这些是真实图像”的标签进行训练。因此,训练将以一种在查看虚假图像时更有可能预测“真实”的方式gan更新权重。generatordiscriminator

概括地说,这就是训练循环的示意图。对于每个时期,您执行以下操作:

  1. 在潜在空间中绘制随机点(随机噪声)。

  2. generator使用此随机噪声生成图像。

  3. 将生成的图像与真实图像混合。

  4. discriminator使用这些混合图像和相应的目标进行训练:“真实”(用于真实图像)或“假”(用于生成的图像)​​。

  5. 在潜在空间中绘制新的随机点。

  6. 使用这些随机向量进行训练generator,目标都说“这些是真实图像”。这会更新生成器的权重,以使它们使鉴别器预测生成图像的“这些是真实图像”:这训练生成器来欺骗鉴别器。

让我们实现它。就像在我们的 VAE 示例中一样,我们将使用一个Model子类一个习惯train_step()。请注意,我们将使用两个优化器(一个用于生成器,一个用于鉴别器),因此我们还将覆盖compile()以允许用于传递两个优化器。

清单 12.36 GANModel

import tensorflow as tf
class GAN(keras.Model):
    def __init__(self, discriminator, generator, latent_dim):
        super().__init__()
        self.discriminator = discriminator
        self.generator = generator
        self.latent_dim = latent_dim
        self.d_loss_metric = keras.metrics.Mean(name="d_loss")              ❶
        self.g_loss_metric = keras.metrics.Mean(name="g_loss")              ❶
 
    def compile(self, d_optimizer, g_optimizer, loss_fn):
        super(GAN, self).compile()
        self.d_optimizer = d_optimizer
        self.g_optimizer = g_optimizer
        self.loss_fn = loss_fn
  
    @property
    def metrics(self):                                                      ❶
        return [self.d_loss_metric, self.g_loss_metric]
    def train_step(self, real_images):
        batch_size = tf.shape(real_images)[0]                               ❷
        random_latent_vectors = tf.random.normal(                           ❷
            shape=(batch_size, self.latent_dim))                            ❷
        generated_images = self.generator(random_latent_vectors)            ❸
        combined_images = tf.concat([generated_images, real_images], axis=0)❹
        labels = tf.concat(                                                 ❺
            [tf.ones((batch_size, 1)), tf.zeros((batch_size, 1))],          ❺
            axis=0                                                          ❺
        )
        labels += 0.05 * tf.random.uniform(tf.shape(labels))                ❻
 
        with tf.GradientTape() as tape:                                     ❼
            predictions = self.discriminator(combined_images)               ❼
            d_loss = self.loss_fn(labels, predictions)                      ❼
        grads = tape.gradient(d_loss, self.discriminator.trainable_weights) ❼
        self.d_optimizer.apply_gradients(                                   ❼
            zip(grads, self.discriminator.trainable_weights)                ❼
        )
  
        random_latent_vectors = tf.random.normal(
            shape=(batch_size, self.latent_dim))                            ❽
  
        misleading_labels = tf.zeros((batch_size, 1))                       ❾
  
        with tf.GradientTape() as tape:                                     ❿
            predictions = self.discriminator(                               ❿
                self.generator(random_latent_vectors))                      ❿
            g_loss = self.loss_fn(misleading_labels, predictions)           ❿
        grads = tape.gradient(g_loss, self.generator.trainable_weights)     ❿
        self.g_optimizer.apply_gradients(                                   ❿
            zip(grads, self.generator.trainable_weights))                   ❿
  
        self.d_loss_metric.update_state(d_loss)
        self.g_loss_metric.update_state(g_loss)
        return {"d_loss": self.d_loss_metric.result(),
                "g_loss": self.g_loss_metric.result()}

设置指标来跟踪每个训练时期的两个损失

对潜在空间中的随机点进行采样

将它们解码为假图像

将它们与真实图像相结合

组装标签,区分真假图像

给标签添加随机噪声——一个重要的技巧!

训练判别器

对潜在空间中的随机点进行采样

组装说“这些都是真实图像”的标签(这是一个谎言!)

训练生成器

在我们开始训练之前,我们还要设置一个回调来监控我们的结果:它将使用生成器在每个 epoch 结束时创建并保存一些假图像。

清单 12.37 在训练期间对生成的图像进行采样的回调

class GANMonitor(keras.callbacks.Callback):
    def __init__(self, num_img=3, latent_dim=128):
        self.num_img = num_img
        self.latent_dim = latent_dim
  
    def on_epoch_end(self, epoch, logs=None):
        random_latent_vectors = tf.random.normal(
            shape=(self.num_img, self.latent_dim))
        generated_images = self.model.generator(random_latent_vectors)
        generated_images *= 255 
        generated_images.numpy()
        for i in range(self.num_img):
            img = keras.utils.array_to_img(generated_images[i])
            img.save(f"generated_img_{epoch:03d}_{i}.png")
Finally, we can start training.

最后,我们可以开始训练了。

清单 12.38 编译和训练 GAN

epochs = 100       ❶
  
gan = GAN(discriminator=discriminator, generator=generator,
          latent_dim=latent_dim)
gan.compile(
    d_optimizer=keras.optimizers.Adam(learning_rate=0.0001),
    g_optimizer=keras.optimizers.Adam(learning_rate=0.0001),
    loss_fn=keras.losses.BinaryCrossentropy(),
)
  
gan.fit(
    dataset, epochs=epochs,
    callbacks=[GANMonitor(num_img=10, latent_dim=latent_dim)]
)

 epoch 20 之后你会开始得到有趣的结果。

训练时,您可能会看到对抗性损失开始显着增加,而判别性损失趋于零——判别器可能最终控制了生成器。如果是这种情况,请尝试降低鉴别器的学习率,并提高鉴别器的辍学率。

图 12.22 显示了我们的 GAN 在 30 轮训练后能够生成的结果。

图 12.22 epoch 30 左右的一些生成图像

12.5.7 总结

  • GAN 由一个生成器网络和一个鉴别器网络组成。鉴别器被训练来区分生成器的输出和来自训练数据集的真实图像,并且生成器被训练来欺骗鉴别器。值得注意的是,生成器从不直接看到训练集中的图像。它拥有的关于数据的信息来自鉴别器。

  • GAN 很难训练,因为训练 GAN 是一个动态过程,而不是具有固定损失情况的简单梯度下降过程。让 GAN 正确训练需要使用许多启发式技巧以及广泛的调整。

  • GAN 可能会产生高度逼真的图像。但与 VAE 不同的是,他们学习的潜在空间不具有整洁的连续结构,因此可能不适合某些实际应用,例如通过潜在空间概念向量进行图像编辑。

这些少数技术仅涵盖了这个快速扩展领域的基础知识。那里还有很多东西要发现——生成式深度学习值得一整本书。

概括

  • 您可以使用序列到序列模型一次生成序列数据。这适用于文本生成,也适用于逐个音符的音乐生成或任何其他类型的时间序列数据。

  • DeepDream 通过输入空间中的梯度上升来最大化卷积层激活。

  • 在风格转移算法中,内容图像和风格图像通过梯度下降组合在一起,生成具有内容图像高级特征和风格图像局部特征的图像。

  • VAE 和 GAN 是学习图像潜在空间的模型,然后可以通过从潜在空间中采样来构想全新的图像。潜在空间中的概念向量甚至可以用于图像编辑。

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sonhhxg_柒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值