JavaScript 深度学习(四)(1)

简介: JavaScript 深度学习(四)

第十一章:生成深度学习

这一章涵盖了

  • 生成深度学习是什么,它的应用以及它与我们迄今看到的深度学习任务有何不同
  • 如何使用 RNN 生成文本
  • 什么是潜在空间以及它如何成为生成新图像的基础,通过变分自编码器示例
  • 生成对抗网络的基础知识

深度神经网络展示了生成看起来或听起来真实的图像、声音和文本的一些令人印象深刻的任务。如今,深度神经网络能够创建高度真实的人脸图像,([1])合成自然音质的语音,([2])以及组织连贯有力的文本,([3])这仅仅是一些成就的名单。这种*生成*模型在许多方面都很有用,包括辅助艺术创作,有条件地修改现有内容,以及增强现有数据集以支持其他深度学习任务。([4])

¹

Tero Karras, Samuli Laine 和 Timo Aila, “一种基于风格的生成对抗网络,” 提交日期:2018 年 12 月 12 日, arxiv.org/abs/1812.04948. 在 thispersondoesnotexist.com/ 查看演示。

²

Aäron van den Oord 和 Sander Dieleman, “WaveNet: 一种用于原始音频的生成模型,” 博客, 2016 年 9 月 8 日, mng.bz/MOrn.

³

“更好的语言模型及其影响”,OpenAI, 2019, openai.com/blog/better-language-models/.

Antreas Antoniou, Amos Storkey 和 Harrison Edwards, “数据增强生成对抗网络,” 提交日期:2017 年 11 月 12 日, arxiv.org/abs/1711.04340.

除了在潜在顾客的自拍照上化妆等实际应用外,生成模型还值得从理论上研究。生成模型和判别模型是机器学习中两种根本不同类型的模型。到目前为止,我们在本书中研究的所有模型都是判别模型。这些模型旨在将输入映射到离散或连续的值,而不关心生成输入的过程。回想一下,我们构建的网络针对钓鱼网站、鸢尾花、MNIST 数字和音频声音的分类器,以及对房价进行回归的模型。相比之下,生成模型旨在数学地模拟不同类别示例生成的过程。但是一旦生成模型学习到这种生成性知识,它也可以执行判别性任务。因此,与判别模型相比,可以说生成模型“更好地理解”数据。

本节介绍了文本和图像的深度生成模型的基础知识。在本章结束时,您应该熟悉基于 RNN 的语言模型、面向图像的自编码器和生成对抗网络的思想。您还应该熟悉这些模型在 TensorFlow.js 中的实现方式,并能够将这些模型应用到您自己的数据集上。

10.1. 使用 LSTM 生成文本

让我们从文本生成开始。为此,我们将使用我们在前一章中介绍的 RNN。虽然您将在这里看到的技术生成文本,但它并不局限于这个特定的输出领域。该技术可以适应生成其他类型的序列,比如音乐——只要能够以合适的方式表示音符,并找到一个足够的训练数据集。[5]类似的思想可以应用于生成素描中的笔画,以便生成漂亮的素描[6],甚至是看起来逼真的汉字[7]。

例如,请参阅 Google 的 Magenta 项目中的 Performance-RNN:magenta.tensorflow.org/performance-rnn

例如,请参阅 David Ha 和 Douglas Eck 的 Sketch-RNN:mng.bz/omyv

David Ha,“Recurrent Net Dreams Up Fake Chinese Characters in Vector Format with TensorFlow”,博客,2015 年 12 月 28 日,mng.bz/nvX4

10.1.1. 下一个字符预测器:生成文本的简单方法

首先,让我们定义文本生成任务。假设我们有一个相当大的文本数据语料库(至少几兆字节)作为训练输入,比如莎士比亚的全部作品(一个非常长的字符串)。我们想要训练一个模型,尽可能地生成看起来训练数据的新文本。这里的关键词当然是“看起来”。现在,让我们满足于不精确地定义“看起来”的含义。在展示方法和结果之后,这个意义将变得更加清晰。

让我们思考如何在深度学习范式中制定这个任务。在前一章节涉及的日期转换示例中,我们看到一个精确格式化的输出序列可以从一个随意格式化的输入序列中生成。那个文本到文本的转换任务有一个明确定义的答案:ISO-8601 格式中的正确日期字符串。然而,这里的文本生成任务似乎不适合这一要求。没有明确的输入序列,并且“正确”的输出并没有明确定义;我们只想生成一些“看起来真实的东西”。我们能做什么呢?

一个解决方案是构建一个模型,预测在一系列字符之后会出现什么字符。这被称为 下一个字符预测。例如,对于在莎士比亚数据集上训练良好的模型,当给定字符串“Love looks not with the eyes, b”作为输入时,应该以高概率预测字符“u”。然而,这只生成一个字符。我们如何使用模型生成一系列字符?为了做到这一点,我们简单地形成一个与之前相同长度的新输入序列,方法是将前一个输入向左移动一个字符,丢弃第一个字符,并将新生成的字符(“u”)粘贴到末尾。在这种情况下,我们的下一个字符预测器的新输入就是“ove looks not with the eyes, bu”。给定这个新的输入序列,模型应该以高概率预测字符“t”。这个过程,如图 10.1 所示,可以重复多次,直到生成所需长度的序列。当然,我们需要一个初始的文本片段作为起点。为此,我们可以从文本语料库中随机抽样。

图 10.1. 用基于 RNN 的下一个字符预测器生成文本序列的示意图,以初始输入文本片段作为种子。在每个步骤中,RNN 使用输入文本预测下一个字符。然后,将输入文本与预测的下一个字符连接起来,丢弃第一个字符。结果形成下一个步骤的输入。在每个步骤中,RNN 输出字符集中所有可能字符的概率分数。为了确定实际的下一个字符,进行随机抽样。

这种表述将序列生成任务转化为基于序列的分类问题。这个问题类似于我们在第九章中看到的 IMDb 情感分析问题,其中从固定长度的输入中预测二进制类别。文本生成模型基本上做了同样的事情,尽管它是一个多类别分类问题,涉及到 N 个可能的类别,其中 N 是字符集的大小——即文本数据集中所有唯一字符的数量。

这种下一个字符预测的表述在自然语言处理和计算机科学中有着悠久的历史。信息论先驱克劳德·香农进行了一项实验,在实验中,被要求的人类参与者在看到一小段英文文本后猜测下一个字母。[8] 通过这个实验,他能够估计出在给定上下文的情况下,典型英文文本中每个字母的平均不确定性。这种不确定性约为 1.3 位的熵,告诉我们每个英文字母所携带的平均信息量。

1951 年的原始论文可在mng.bz/5AzB中获取。

当字母以完全随机的方式出现时,1.3 位的结果比如果 26 个字母完全随机出现所需的位数要少,该数值为 log2 = 4.7 位数。这符合我们的直觉,因为我们知道英语字母并不是随机出现的,而是具有某些模式。在更低的层次上,只有某些字母序列是有效的英语单词。在更高的层次上,只有某些单词的排序满足英语语法。在更高的层次上,只有某些语法上有效的句子实际上是有意义的。

如果你考虑一下,这正是我们的文本生成任务的基础所在:学习所有这些层面的模式。注意,我们的模型基本上是被训练来做 Shannon 实验中的那个志愿者所做的事情——也就是猜测下一个字符。现在,让我们来看一下示例代码以及它是如何工作的。请记住 Shannon 的 1.3 位结果,因为我们稍后会回到它。

10.1.2《LSTM-text-generation》示例

在 tfjs-examples 仓库中的 lstm-text-generation 示例中,我们训练了一个基于 LSTM 的下一个字符预测器,并利用它生成了新的文本。训练和生成都在 JavaScript 中使用 TensorFlow.js 完成。你可以在浏览器中或者使用 Node.js 运行示例。前者提供了更加图形化和交互式的界面,但后者具有更快的训练速度。

要在浏览器中查看此示例的运行情况,请使用以下命令:

git clone https://github.com/tensorflow/tfjs-examples.git
cd tfjs-examples/lstm-text-generation
yarn && yarn watch

在弹出的页面中,你可以选择并加载四个提供的文本数据集中的一个来训练模型。在下面的讨论中,我们将使用莎士比亚的数据集。一旦数据加载完成,你可以点击“创建模型”按钮为它创建一个模型。一个文本框允许你调整创建的 LSTM 将具有的单元数。它默认设置为 128。但你也可以尝试其他值,例如 64。如果你输入由逗号分隔的多个数字(例如 128,128),则创建的模型将包含多个叠放在一起的 LSTM 层。

若要使用 tfjs-node 或 tfjs-node-gpu 在后端执行训练,请使用 yarn train 命令而不是 yarn watch

yarn train shakespeare \
      --lstmLayerSize 128,128 \
      --epochs 120 \
      --savePath ./my-shakespeare-model

如果你已经正确地设置了 CUDA-enabled GPU,可以在命令中添加 --gpu 标志,让训练过程在 GPU 上运行,这将进一步加快训练速度。--lstmLayerSize 标志在浏览器版本的示例中起到了 LSTM-size 文本框的作用。前面的命令将创建并训练一个由两个 LSTM 层组成的模型,每个 LSTM 层都有 128 个单元,叠放在一起。

此处正在训练的模型具有堆叠 LSTM 架构。堆叠 LSTM 层是什么意思?在概念上类似于在 MLP 中堆叠多个密集层,这增加了 MLP 的容量。类似地,堆叠多个 LSTM 允许输入序列在被最终 LSTM 层转换为最终回归或分类输出之前经历多个 seq2seq 表示转换阶段。图 10.2 给出了这种架构的图解。一个重要的事情要注意的是,第一个 LSTM 的returnSequence属性被设置为true,因此生成包括输入序列的每个单个项目的输出序列。这使得可以将第一个 LSTM 的输出馈送到第二个 LSTM 中,因为 LSTM 层期望顺序输入而不是单个项目输入。

图 10.2. 在模型中如何堆叠多个 LSTM 层。在这种情况下,两个 LSTM 层被堆叠在一起。第一个 LSTM 的returnSequence属性被设置为true,因此输出一个项目序列。第一个 LSTM 的序列输出被传递给第二个 LSTM 作为其输入。第二个 LSTM 输出一个单独的项目而不是项目序列。单个项目可以是回归预测或 softmax 概率数组,它形成模型的最终输出。

清单 10.1 包含构建下一个字符预测模型的代码,其架构如图 10.2 所示(摘自 lstm-text-generation/model.js)。请注意,与图表不同,代码包括一个稠密层作为模型的最终输出。密集层具有 softmax 激活。回想一下,softmax 激活将输出归一化,使其值介于 0 和 1 之间,并总和为 1,就像概率分布一样。因此,最终的密集层输出表示唯一字符的预测概率。

createModel() 函数的 lstmLayerSize 参数控制 LSTM 层的数量和每个层的大小。第一个 LSTM 层的输入形状根据 sampleLen(模型一次接收多少个字符)和 charSetSize(文本数据中有多少个唯一字符)进行配置。对于基于浏览器的示例,sampleLen 是硬编码为 40 的;对于基于 Node.js 的训练脚本,可以通过 --sampleLen 标志进行调整。对于莎士比亚数据集,charSetSize 的值为 71。字符集包括大写和小写英文字母、标点符号、空格、换行符和几个其他特殊字符。给定这些参数,清单 10.1 中的函数创建的模型具有输入形状 [40, 71](忽略批处理维度)。该形状对应于 40 个 one-hot 编码字符。模型的输出形状是 [71](同样忽略批处理维度),这是下一个字符的 71 种可能选择的 softmax 概率值。

清单 10.1. 构建一个用于下一个字符预测的多层 LSTM 模型
export function createModel(sampleLen,                    ***1***
                            charSetSize,                  ***2***
                            lstmLayerSizes) {             ***3***
  if (!Array.isArray(lstmLayerSizes)) {
    lstmLayerSizes = [lstmLayerSizes];
  }
  const model = tf.sequential();
  for (let i = 0; i < lstmLayerSizes.length; ++i) {
    const lstmLayerSize = lstmLayerSizes[i];
    model.add(tf.layers.lstm({                            ***4***
      units: lstmLayerSize,
      returnSequences: i < lstmLayerSizes.length - 1,     ***5***
      inputShape: i === 0 ?
          [sampleLen, charSetSize] : undefined            ***6***
    }));
  }
  model.add(
      tf.layers.dense({
        units: charSetSize,
        activation: 'softmax'
  }));                                                    ***7***
  return model;
}
  • 1 模型输入序列的长度
  • 2 所有可能的唯一字符的数量
  • 3 模型的 LSTM 层的大小,可以是单个数字或数字数组
  • 4 模型以一堆 LSTM 层开始。
  • 5 设置 returnSequencestrue 以便可以堆叠多个 LSTM 层
  • 6 第一个 LSTM 层是特殊的,因为它需要指定其输入形状。
  • 7 模型以一个密集层结束,其上有一个 softmax 激活函数,适用于所有可能的字符,反映了下一个字符预测问题的分类特性。

为了准备模型进行训练,我们使用分类交叉熵损失对其进行编译,因为该模型本质上是一个 71 路分类器。对于优化器,我们使用 RMSProp,这是递归模型的常用选择:

const optimizer = tf.train.rmsprop(learningRate);
model.compile({optimizer: optimizer, loss: 'categoricalCrossentropy'});

输入模型训练的数据包括输入文本片段和每个片段后面的字符的对,所有这些都编码为 one-hot 向量(参见图 10.1)。在 lstm-text-generation/data.js 中定义的 TextData 类包含从训练文本语料库生成此类张量数据的逻辑。那里的代码有点乏味,但思想很简单:随机从我们的文本语料库中的非常长的字符串中抽取固定长度的片段,并将它们转换为 one-hot 张量表示。

如果您正在使用基于 Web 的演示,页面的“模型训练”部分允许您调整超参数,例如训练时期的数量、每个时期进入的示例数量、学习率等等。单击“训练模型”按钮启动模型训练过程。对于基于 Node.js 的训练,这些超参数可以通过命令行标志进行调整。有关详细信息,您可以通过输入 yarn train --help 命令获取帮助消息。

根据您指定的训练周期数和模型大小,训练时间可能会在几分钟到几个小时之间不等。基于 Node.js 的训练作业在每个训练周期结束后会自动打印模型生成的一些示例文本片段(见 表格 10.1)。随着训练的进行,您应该看到损失值从初始值约为 3.2 不断降低,并在 1.4–1.5 的范围内收敛。大约经过 120 个周期后,损失减小后,生成的文本质量应该会提高,以至于在训练结束时,文本应该看起来有些像莎士比亚的作品,而验证损失应该接近 1.5 左右——并不远离香农实验中的每字符信息不确定性 1.3 比特。但请注意,考虑到我们的训练范式和模型容量,生成的文本永远不会像实际的莎士比亚的写作。

表格 10.1. 基于 LSTM 的下一字符预测模型生成的文本样本。生成基于种子文本。初始种子文本:" “在每小时的关于你的特定繁荣的议会中,和 lo”。^([a]) 根据种子文本后续的实际文本(用于比较):“爱你不会比你的老父亲梅奈尼乌斯对你更差!…”。

^a

摘自 莎士比亚的《科里奥兰纳斯》,第 5 幕,第 2 场。请注意,示例中包括换行和单词中间的停顿(love)。

训练周期 验证损失 T = 0 T = 0.25 T = 0.5 T = 0.75
5 2.44 "rle the the the the the the the the the the the the the the the the the the the the the the the the the the the the the " “te ans and and and and and warl torle an at an yawl and tand and an an ind an an in thall ang ind an tord and and and wa” “te toll nlatese ant ann, tomdenl, teurteeinlndting fall ald antetetell linde ing thathere taod winld mlinl theens tord y” "p, af ane me pfleh; fove this? Iretltard efidestind ants anl het insethou loellr ard,
25 1.96 "ve tray the stanter an truent to the stanter to the stanter to the stanter to the stanter to the stanter to the stanter " “ve to the enter an truint to the surt an truin to me truent me the will tray mane but a bean to the stanter an trust tra” “ve of marter at it not me shank to an him truece preater the beaty atweath and that marient shall me the manst on hath s” “rd; not an an beilloters An bentest the like have bencest on it love gray to dreath avalace the lien I am sach me, m”
50 1.67 “世界的世界的世界的世界的世界的世界的世界的世界的世界的世界的世界” “他们是他们的英语是世界的世界的立场的证明了他们的弦应该世界我” “他们的愤怒的苦恼的,因为你对于你的设备的现在的将会” “是我的光,我将做 vall twell。斯伯”
100 1.61 “越多的人越多,越奇怪的是,越奇怪的是,越多的人越多” “越多的人越多越多” “越多的人越多。为了这样一个内容,” “和他们的 consent,你将会变成三个。长的和一个心脏和不奇怪的。一位 G”
120 1.49 “打击的打击的打击的打击的打击的打击和打击的打击的打击” “亲爱的打击我的排序的打击,打击打击,亲爱的打击和” “为他的兄弟成为这样的嘲笑。一个模仿的” “这是我的灵魂。Monty 诽谤他你的矫正。这是为了他的兄弟,这是愚蠢的”

表格 10.1 展示了在四个不同 温度值 下采样的一些文本,这是一个控制生成文本随机性的参数。在生成文本的样本中,您可能已经注意到,较低的温度值与更多重复和机械化的文本相关联,而较高的值与不可预测的文本相关联。由 Node.js 的训练脚本演示的最高温度值默认为 0.75,有时会导致看起来像英语但实际上不是英语单词的字符序列(例如表格中的“stratter”和“poins”)。在接下来的部分中,我们将探讨温度是如何工作的,以及为什么它被称为温度。

10.1.3. 温度:生成文本中的可调随机性

列表 10.2 中的函数 sample() 负责根据模型在文本生成过程的每一步的输出概率来确定选择哪个字符。正如您所见,该算法有些复杂:它涉及到三个低级 TensorFlow.js 操作的调用:tf.div()tf.log()tf.multinomial()。为什么我们使用这种复杂的算法而不是简单地选择具有最高概率得分的选项,这将需要一个单独的 argMax() 调用呢?

如果我们这样做,文本生成过程的输出将是确定性的。也就是说,如果你多次运行它,它将给出完全相同的输出。到目前为止,我们所见到的深度神经网络都是确定性的,也就是说,给定一个输入张量,输出张量完全由网络的拓扑结构和其权重值决定。如果需要的话,你可以编写一个单元测试来断言其输出值(见第十二章讨论机器学习算法的测试)。对于我们的文本生成任务来说,这种确定性并不理想。毕竟,写作是一个创造性的过程。即使给出相同的种子文本,生成的文本也更有趣些带有一些随机性。这就是tf.multinomial()操作和温度参数有用的地方。tf.multinomial()是随机性的来源,而温度控制着随机性的程度。

列表 10.2。带有温度参数的随机抽样函数
export function sample(probs, temperature) {
  return tf.tidy(() => {
    const logPreds = tf.div(
        tf.log(probs),                                                     ***1***
        Math.max(temperature, 1e-6));                                      ***2***
    const isNormalized = false;
    return tf.multinomial(logPreds, 1, null, isNormalized).dataSync()[0];  ***3***
  });
}
  • 1 模型的密集层输出归一化的概率分数;我们使用 log()将它们转换为未归一化的 logits,然后再除以温度。
  • 2 我们用一个小的正数来防止除以零的错误。除法的结果是调整了不确定性的 logits。
  • 3 tf.multinomial()是一个随机抽样函数。它就像一个多面的骰子,每个面的概率不相等,由 logPreds——经过温度缩放的 logits 来确定。

在列表 10.2 的sample()函数中最重要的部分是以下行:

const logPreds = tf.div(tf.log(probs),
                        Math.max(temperature, 1e-6));

它获取了probs(模型的概率输出)并将它们转换为logPreds,概率的对数乘以一个因子。对数运算(tf.log())和缩放(tf.div())做了什么?我们将通过一个例子来解释。为了简单起见,假设只有三个选择(字符集中的三个字符)。假设我们的下一个字符预测器在给定某个输入序列时产生了以下三个概率分数:

[0.1, 0.7, 0.2]

让我们看看两个不同的温度值如何改变这些概率。首先,让我们看一个相对较低的温度:0.25。缩放后的 logits 是

log([0.1, 0.7, 0.2]) / 0.25 = [-9.2103, -1.4267, -6.4378]

要理解 logits 的含义,我们通过使用 softmax 方程将它们转换回实际的概率分数,这涉及将 logits 的指数和归一化:

exp([-9.2103, -1.4267, -6.4378]) / sum(exp([-9.2103, -1.4267, -6.4378]))
= [0.0004, 0.9930, 0.0066]

正如你所看到的,当温度为 0.25 时,我们的 logits 对应一个高度集中的概率分布,在这个分布中,第二个选择的概率远高于其他两个选择(见图 10.3 的第二面板)。

图 10.3. 不同温度(T)值缩放后的概率得分。较低的 T 值导致分布更集中(更少随机);较高的 T 值导致分布在类别之间更均等(更多随机)。T 值为 1 对应于原始概率(无变化)。请注意,无论 T 的值如何,三个选择的相对排名始终保持不变。

如果我们使用更高的温度,比如说 0.75,通过重复相同的计算,我们得到

log([0.1, 0.7, 0.2]) / 0.75 = [-3.0701, -0.4756, -2.1459]
exp([-3.0701, -0.4756, -2.1459]) / sum([-3.0701, -0.4756, -2.1459])
= [0.0591, 0.7919 0.1490]

与之前的情况相比,这是一个峰值较低的分布,当温度为 0.25 时(请参阅图 10.3 的第四面板)。但是与原始分布相比,它仍然更尖峭。你可能已经意识到,温度为 1 时,你将得到与原始概率完全相同的结果(图 10.3,第五面板)。大于 1 的温度值会导致选择之间的概率分布更“均等”(图 10.3,第六面板),而选择之间的排名始终保持不变。

这些转换后的概率(或者说它们的对数)然后被馈送到 tf.multinomial() 函数中,该函数的作用类似于一个多面骰子,其面的不等概率由输入参数控制。这给我们了下一个字符的最终选择。

所以,这就是温度参数如何控制生成文本的随机性。术语 temperature 源自热力学,我们知道,温度较高的系统内部混乱程度较高。这个类比在这里是合适的,因为当我们在代码中增加温度值时,生成的文本看起来更加混乱。温度值有一个“甜蜜的中间值”。在此之下,生成的文本看起来太重复和机械化;在此之上,文本看起来太不可预测和古怪。

这结束了我们对文本生成 LSTM 的介绍。请注意,这种方法非常通用,可以应用于许多其他序列,只需进行适当的修改即可。例如,如果在足够大的音乐分数数据集上进行训练,LSTM 可以通过逐步从之前的音符中预测下一个音符来作曲。^([9])

Allen Huang 和 Raymond Wu,“Deep Learning for Music”,2016 年 6 月 15 日提交,arxiv.org/abs/1606.04930

10.2. 变分自动编码器:找到图像的高效和结构化的向量表示

前面的部分为您介绍了如何使用深度学习来生成文本等连续数据。在本章的剩余部分,我们将讨论如何构建神经网络来生成图像。我们将研究两种类型的模型:变分自编码器(VAE)和生成对抗网络(GAN)。与 GAN 相比,VAE 的历史更悠久,结构更简单。因此,它为您进入基于深度学习的图像生成的快速领域提供了很好的入口。

10.2.1. 传统自编码器和 VAE: 基本概念

图 10.4 以示意方式显示了自编码器的整体架构。乍一看,自编码器是一个有趣的模型,因为它的输入和输出模型的图像大小是相同的。在最基本的层面上,自编码器的损失函数是输入和输出之间的均方误差(MSE)。这意味着,如果经过适当训练,自编码器将接受一个图像,并输出一个几乎相同的图像。这种模型到底有什么用呢?

图 10.4. 传统自编码器的架构

实际上,自编码器是一种重要的生成模型,而且绝不是无用的。对于前面的问题答案在于小时钟形状的架构(图 10.4)。自编码器的最细部分是一个与输入和输出图像相比具有更少元素的向量。因此,由自编码器执行的图像转换是非平凡的:它首先将输入图像转变为高压缩形式的表示,然后在不使用任何额外信息的情况下从该表示中重新构建图像。中间的有效表示称为潜在向量,或者z-向量。我们将这两个术语互换使用。这些向量所在的向量空间称为潜在空间,或者z-空间。将输入图像转换为潜在向量的自编码器部分称为编码器;将潜在向量转换回图像的后面部分称为解码器

和图像本身相比,潜在向量可以小几百倍,我们很快会通过一个具体的例子进行展示。因此,经过训练的自编码器的编码器部分是一个非常高效的维度约简器。它对输入图像的总结非常简洁,但包含足够重要的信息,以使得解码器可以忠实地复制输入图像,而不需要使用额外的信息。解码器能够做到这一点,这也是非常了不起的。

我们还可以从信息理论的角度来看待自编码器。假设输入和输出图像各包含N比特的信息。从表面上看,N是每个像素的位深度乘以像素数量。相比之下,自编码器中间的潜在向量由于其小的大小(假设为m比特),只能保存极少量的信息。如果m小于N,那么从潜在向量重构出图像就在理论上不可能。然而,图像中的像素不是完全随机的(完全由随机像素组成的图像看起来像静态噪音)。相反,像素遵循某些模式,比如颜色连续性和所描绘的现实世界对象的特征。这导致N的值比基于像素数量和深度的表面计算要小得多。自编码器的任务是学习这种模式;这也是自编码器能够工作的原因。

在自编码器训练完成后,其解码器部分可以单独使用,给定任何潜在向量,它都可以生成符合训练图像的模式和风格的图像。这很好地符合了生成模型的描述。此外,潜在空间将有望包含一些良好的可解释结构。具体而言,潜在空间的每个维度可能与图像的某个有意义的方面相关联。例如,假设我们在人脸图像上训练了一个自编码器,也许潜在空间的某个维度将与微笑程度相关。当你固定潜在向量中所有其他维度的值,仅变化“微笑维度”的值时,解码器产生的图像将是同一张脸,但微笑程度不同(例如,参见图 10.5)。这将使得有趣的应用成为可能,例如在保持所有其他方面不变的情况下,改变输入人脸图像的微笑程度。可以通过以下步骤来完成此操作。首先,通过应用编码器获取输入的潜在向量。然后,仅修改向量的“微笑维度”即可;最后,通过解码器运行修改后的潜在向量。

图 10.5. “微笑维度”。自编码器所学习的潜在空间中期望的结构的示例。

不幸的是,图 10.4 中所示的 经典自编码器 并不能产生特别有用和良好结构的潜变量空间。它们在压缩方面也不太出色。因此,到 2013 年,它们在很大程度上已经不再流行了。VAE(Variational Autoencoder)则在 2013 年 12 月由 Diederik Kingma 和 Max Welling 几乎同时发现^([10]),而在 2014 年 1 月由 Danilo Rezende、Shakir Mohamed 和 Daan Wiestra 发现^([11]),通过一点统计魔法增加了自编码器的能力,强制模型学习连续且高度结构化的潜变量空间。VAE 已经证明是一种强大的生成式图像模型。

¹⁰

Diederik P. Kingma 和 Max Welling,“Auto-Encoding Variational Bayes”,2013 年 12 月 20 日提交,arxiv.org/abs/1312.6114

¹¹

Danilo Jimenez Rezende,Shakir Mohamed 和 Daan Wierstra,“Stochastic Backpropagation and Approximate Inference in Deep Generative Models”,2014 年 1 月 16 日提交,arxiv.org/abs/1401.4082

VAE 不是将输入图像压缩为潜变量空间中的固定向量,而是将图像转化为统计分布的参数——具体来说是高斯分布的参数。高斯分布有两个参数:均值和方差(或者等效地,标准差)。VAE 将每个输入图像映射到一个均值上。唯一的额外复杂性在于,如果潜变量空间超过 1D,则均值和方差可以是高于一维的,正如我们将在下面的例子中看到的那样。本质上,我们假设图像是通过随机过程生成的,并且在编码和解码过程中应该考虑到这个过程的随机性。然后,VAE 使用均值和方差参数从分布中随机采样一个向量,并使用该随机向量将其解码回原始输入的大小(参见图 10.6)。这种随机性是 VAE 改善鲁棒性、强迫潜变量空间在每个位置都编码有意义表示的关键方式之一:在解码器解码时,潜变量空间中采样的每个点应该是一个有效的图像输出。

图 10.6. 比较经典自编码器(面板 A)和 VAE(面板 B)的工作原理。经典自编码器将输入图像映射到一个固定的潜变量向量上,并使用该向量进行解码。相比之下,VAE 将输入图像映射到一个由均值和方差描述的分布上,从该分布中随机采样一个潜变量向量,并使用该随机向量生成解码后的图像。这个 T 恤图案是来自 Fashion-MNIST 数据集的一个例子。

接下来,我们将通过使用 Fashion-MNIST 数据集展示 VAE 的工作原理。正如其名称所示,Fashion-MNIST^([12]) 受到了 MNIST 手写数字数据集的启发,但包含了服装和时尚物品的图像。与 MNIST 图像一样,Fashion-MNIST 图像是 28 × 28 的灰度图像。有着确切的 10 个服装和时尚物品类别(如 T 恤、套头衫、鞋子和包袋;请参见 图 10.6 作为示例)。然而,与 MNIST 数据集相比,Fashion-MNIST 数据集对机器学习算法来说略微“更难”,当前最先进的测试集准确率约为 96.5%,远低于 MNIST 数据集的 99.75% 最先进准确率。^([13]) 我们将使用 TensorFlow.js 构建一个 VAE 并在 Fashion-MNIST 数据集上对其进行训练。然后,我们将使用 VAE 的解码器从 2D 潜在空间中对样本进行采样,并观察该空间内部的结构。

¹²

Han Xiao、Kashif Rasul 和 Roland Vollgraf,“Fashion-MNIST: 用于机器学习算法基准测试的新型图像数据集”,提交于 2017 年 8 月 25 日,arxiv.org/abs/1708.07747

¹³

来源:“所有机器学习问题的最新技术结果”,GitHub,2019 年,mng.bz/6w0o

10.2.2. VAE 的详细示例:Fashion-MNIST 示例

要查看 fashion-mnist-vae 示例,请使用以下命令:

git clone https://github.com/tensorflow/tfjs-examples.git
cd tfjs-examples/fashion-mnist-vae
yarn
yarn download-data

这个例子由两部分组成:在 Node.js 中训练 VAE 和使用 VAE 解码器在浏览器中生成图像。要开始训练部分,请使用以下命令

yarn train

如果您正确设置了 CUDA 启用的 GPU,则可以使用 --gpu 标志来加速训练:

yarn train --gpu

训练在配备有 CUDA GPU 的合理更新的台式机上大约需要五分钟,没有 GPU 的情况下则需要不到一个小时。训练完成后,使用以下命令构建并启动浏览器前端:

yarn watch

前端将加载 VAE 的解码器,通过使用正则化的 2D 网格的潜在向量生成多个图像,并在页面上显示这些图像。这将让您欣赏到潜在空间的结构。

从技术角度来看,这就是 VAE 的工作原理:

  1. 编码器将输入样本转换为潜在空间中的两个参数:zMeanzLogVar,分别是均值和方差的对数(对数方差)。这两个向量的长度与潜在空间的维度相同。例如,我们的潜在空间将是 2D,因此 zMeanzLogVar 将分别是长度为 2 的向量。为什么我们使用对数方差(zLogVar)而不是方差本身?因为方差必须是非负的,但没有简单的方法来强制该层输出的符号要求。相比之下,对数方差允许具有任何符号。通过使用对数,我们不必担心层输出的符号。对数方差可以通过简单的指数运算(tf.exp())操作轻松地转换为相应的方差。

¹⁴

严格来说,长度为 N 的潜在向量的协方差矩阵是一个 N × N 矩阵。然而,zLogVar 是一个长度为 N 的向量,因为我们将协方差矩阵约束为对角线矩阵——即,潜在向量的两个不同元素之间没有相关性。

  1. VAE 算法通过使用一个称为 epsilon 的向量——与 zMeanzLogVar 的长度相同的随机向量——从潜在正态分布中随机抽样一个潜在向量。在简单的数学方程中,这一步骤在文献中被称为重参数化,看起来像是
z = zMean + exp(zLogVar * 0.5) * epsilon
  1. 乘以 0.5 将方差转换为标准差,这基于标准差是方差的平方根的事实。等效的 JavaScript 代码是
z = zMean.add(zLogVar.mul(0.5).exp().mul(epsilon));
  1. (见 listing 10.3。) 然后,z 将被馈送到 VAE 的解码器部分,以便生成输出图像。

在我们的 VAE 实现中,潜在向量抽样步骤是由一个名为 ZLayer 的自定义层执行的(见 listing 10.3)。我们在 第九章 中简要介绍了一个自定义 TensorFlow.js 层(我们在基于注意力的日期转换器中使用的 GetLastTimestepLayer 层)。我们 VAE 使用的自定义层略微复杂,值得解释一下。

ZLayer 类有两个关键方法:computeOutputShape()call()computeOutputShape() 被 TensorFlow.js 用来推断给定输入形状的 Layer 实例的输出形状。call() 方法包含了实际的数学计算。它包含了先前介绍的方程行。下面的代码摘自 fashion-mnist-vae/model.js。

listing 10.3 抽样潜在空间(z 空间)的代码示例
class ZLayer extends tf.layers.Layer {
  constructor(config) {
    super(config);
  }
  computeOutputShape(inputShape) {
    tf.util.assert(inputShape.length === 2 && Array.isArray(inputShape[0]),
        () => `Expected exactly 2 input shapes. ` +
              `But got: ${inputShape}`);             ***1***
    return inputShape[0];                            ***2***
  }
  call(inputs, kwargs) {
    const [zMean, zLogVar] = inputs;
    const batch = zMean.shape[0];
    const dim = zMean.shape[1];
    const mean = 0;
    const std = 1.0;
    const epsilon = tf.randomNormal(                 ***3***
        [batch, dim], mean, std);                    ***3***
    return zMean.add(                              ***4***
        zLogVar.mul(0.5).exp().mul(epsilon));        ***4***
  }
  static get ClassName() {                           ***5***
    return 'ZLayer';
  }
}
tf.serialization.registerClass(ZLayer);              ***6***
  • 1 检查确保我们只有两个输入:zMean 和 zLogVar
  • 2 输出(z)的形状将与 zMean 的形状相同。
  • 3 从单位高斯分布中获取一个随机批次的 epsilon
  • 4 这是 z 向量抽样发生的地方:zMean + standardDeviation * epsilon。
  • 5 如果要对该层进行序列化,则设置静态的 className 属性。
  • 6 注册类以支持反序列化

如清单 10.4 所示,ZLayer被实例化并被用作编码器的一部分。编码器被编写为一个功能型模型,而不是更简单的顺序模型,因为它具有非线性的内部结构,并且产生三个输出:zMeanzLogVarz(参见图 10.7 中的示意图)。编码器输出z是因为它将被解码器使用,但为什么编码器包括zMeanzLogVar在输出中?这是因为它们将用于计算 VAE 的损失函数,很快你就会看到。

图 10.7。TensorFlow.js 实现 VAE 的示意图,包括编码器和解码器部分的内部细节以及支持 VAE 训练的自定义损失函数和优化器。

除了ZLayer,编码器还包括两个单隐藏层的 MLP。它们用于将扁平化的输入 Fashion-MNIST 图像转换为zMeanzLogVar向量,分别。这两个 MLP 共享相同的隐藏层,但使用单独的输出层。这种分支模型拓扑结构也是由于编码器是一个功能型模型。

清单 10.4。我们 VAE 的编码器部分(摘自 fashion-mnist-vae/model.js)
function encoder(opts) {
  const {originalDim, intermediateDim, latentDim} = opts;
  const inputs = tf.input({shape: [originalDim], name: 'encoder_input'});
  const x = tf.layers.dense({units: intermediateDim, activation: 'relu'})
                .apply(inputs);                                              ***1***
  const zMean = tf.layers.dense({units: latentDim, name: 'z_mean'}).apply(x);***2***
  const zLogVar = tf.layers.dense({                                          ***2***
        units: latentDim,                                                    ***2***
        name: 'z_log_var'                                                    ***2***
      }).apply(x);                                                         ***2*** ***3***
  const z =                                                                  ***3***
      new ZLayer({name: 'z', outputShape: [latentDim]}).apply([zMean,        ***3***
     zLogVar]);                                                              ***3***
  const enc = tf.model({
    inputs: inputs,
    outputs: [zMean, zLogVar, z],
    name: 'encoder',
  })
  return enc;
}
  • 1 编码器底部是一个简单的 MLP,有一个隐藏层。
  • 2 与普通的 MLP 不同,我们在隐藏的密集层之后放置了两个层,分别用于预测 zMean 和 zLogVar。这也是我们使用功能型模型而不是更简单的顺序模型类型的原因。
  • 3 实例化我们自定义的 ZLayer,并使用它来生成遵循由 zMean 和 zLogVar 指定的分布的随机样本

清单 10.5 中的代码构建了解码器。与编码器相比,解码器的拓扑结构更简单。它使用一个 MLP 将输入的 z 向量(即潜在向量)转换为与编码器输入相同形状的图像。请注意,我们的 VAE 处理图像的方式有些简单和不寻常,因为它将图像扁平化为 1D 向量,因此丢弃了空间信息。面向图像的 VAE 通常使用卷积和池化层,但由于我们图像的简单性(其尺寸较小且仅有一个颜色通道),扁平化方法足够简单地处理此示例的目的。

清单 10.5。我们 VAE 的解码器部分(摘自 fashion-mnist-vae/model.js)
function decoder(opts) {
  const {originalDim, intermediateDim, latentDim} = opts;
  const dec = tf.sequential({name: 'decoder'});   ***1***
  dec.add(tf.layers.dense({
    units: intermediateDim,
    activation: 'relu',
    inputShape: [latentDim]
  }));
  dec.add(tf.layers.dense({
    units: originalDim,
    activation: 'sigmoid'                         ***2***
  }));
  return dec;
}
  • 1 解码器是一个简单的 MLP,将潜在(z)向量转换为(扁平化的)图像。
  • 2 Sigmoid 激活是输出层的一个好选择,因为它确保输出图像的像素值被限制在 0 和 1 之间。

将编码器和解码器合并成一个名为 VAE 的单个tf.LayerModel对象时,列表 10.6 中的代码会提取编码器的第三个输出(z 向量)并将其通过解码器运行。然后,组合模型会将解码图像暴露为其输出,同时还有其他三个输出:zMeanzLogVar和 z 向量。这完成了 VAE 模型拓扑结构的定义。为了训练模型,我们需要两个东西:损失函数和优化器。以下列表中的代码摘自 fashion-mnist-vae/model.js。

将编码器和解码器放在一起组成 VAE 时,列表 10.6 中完成。
function vae(encoder, decoder) {
  const inputs = encoder.inputs;                     ***1***
  const encoderOutputs = encoder.apply(inputs);
  const encoded = encoderOutputs[2];                 ***2***
  const decoderOutput = decoder.apply(encoded);
  const v = tf.model({                               ***3***
    inputs: inputs,
    outputs: [decoderOutput, ...encoderOutputs],     ***4***
    name: 'vae_mlp',
  })
  return v;
}
  • 1 VAE 的输入与编码器的输入相同:原始输入图像。
  • 2 在编码器的所有三个输出中,只有最后一个(z)进入解码器。
  • 3 由于模型的非线性拓扑结构,我们使用功能模型 API。
  • 4 VAE 模型对象的输出除了 zMean、zLogVar 和z之外还包括解码图像。

当我们访问第五章中的 simple-object-detection 模型时,我们描述了如何在 TensorFlow.js 中定义自定义损失函数的方式。在这里,需要自定义损失函数来训练 VAE。这是因为损失函数将是两个项的总和:一个量化输入和输出之间的差异,另一个量化潜在空间的统计属性。这让人想起了 simple-object-detection 模型的自定义损失函数,其中一个项用于对象分类,另一个项用于对象定位。

如您从列表 10.7 中的代码中所见(摘自 fashion-mnist-vae/model.js),定义输入输出差异项是直接的。我们简单地计算原始输入和解码器输出之间的均方误差(MSE)。然而,统计项,称为Kullbach-Liebler(KL)散度,数学上更加复杂。我们会免去详细的数学[¹⁵],但从直觉上讲,KL 散度项(代码中的 klLoss)鼓励不同输入图像的分布更均匀地分布在潜在空间的中心周围,这使得解码器更容易在图像之间进行插值。因此,klLoss项可以被视为 VAE 的主要输入输出差异项之上添加的正则化项。

¹⁵

Irhum Shafkat 的这篇博文包含了对 KL 散度背后数学的更深入讨论:mng.bz/vlvr

第 10.7 节列出了 VAE 的损失函数。
function vaeLoss(inputs, outputs) {
  const originalDim = inputs.shape[1];
  const decoderOutput = outputs[0];
  const zMean = outputs[1];
  const zLogVar = outputs[2];
  const reconstructionLoss =                                              ***1***
      tf.losses.meanSquaredError(inputs, decoderOutput).mul(originalDim); ***1***
  let klLoss = zLogVar.add(1).sub(zMean.square()).sub(zLogVar.exp());
  klLoss = klLoss.sum(-1).mul(-0.5);                                      ***2***
  return reconstructionLoss.add(klLoss).mean();                           ***3***
}
  • 1 计算“重构损失”项。最小化此项的目标是使模型输出与输入数据匹配。
  • 2 计算 zLogVar 和 zMean 之间的 KL 散度。最小化此项旨在使潜变量的分布更接近于潜在空间的中心处正态分布。
  • 3 将图像重建损失和 KL-散度损失汇总到最终的 VAE 损失中

我们 VAE 训练的另一个缺失部分是优化器及其使用的训练步骤。优化器的类型是流行的 ADAM 优化器(tf.train .adam())。VAE 的训练步骤与本书中所有其他模型不同,因为它不使用模型对象的fit()fitDataset()方法。相反,它调用优化器的minimize()方法(列表 10.8)。这是因为自定义损失函数的 KL-散度项使用模型的四个输出中的两个,但在 TensorFlow.js 中,只有在模型的每个输出都具有不依赖于任何其他输出的损失函数时,fit()fitDataset()方法才能正常工作。

如列表 10.8 所示,minimize()函数以箭头函数作为唯一参数进行调用。这个箭头函数返回当前批次的扁平化图像的损失(代码中的reshaped),这个损失被函数闭包。minimize()计算损失相对于 VAE 的所有可训练权重的梯度(包括编码器和解码器),根据 ADAM 算法调整它们,然后根据调整后的梯度在权重的相反方向应用更新。这完成了一次训练步骤。这一步骤重复进行,遍历 Fashion-MNIST 数据集中的所有图像,并构成一个训练时期。yarn train 命令执行多个训练周期(默认:5 个周期),在此之后损失值收敛,并且 VAE 的解码器部分被保存到磁盘上。编码器部分不保存的原因是它不会在接下来的基于浏览器的演示步骤中使用。

列表 10.8. VAE 的训练循环(摘自 fashion-mnist-vae/train.js)
for (let i = 0; i < epochs; i++) {
    console.log(`\nEpoch #${i} of ${epochs}\n`)
    for (let j = 0; j < batches.length; j++) {
      const currentBatchSize = batches[j].length
      const batchedImages = batchImages(batches[j]);            ***1***
      const reshaped =
          batchedImages.reshape([currentBatchSize, vaeOpts.originalDim]);
      optimizer.minimize(() => {                                ***2***
        const outputs = vaeModel.apply(reshaped);
        const loss = vaeLoss(reshaped, outputs, vaeOpts);
        process.stdout.write('.');                              ***3***
        if (j % 50 === 0) {
          console.log('\nLoss:', loss.dataSync()[0]);
        }
        return loss;
      });
      tf.dispose([batchedImages, reshaped]);
    }
    console.log('');
    await generate(decoderModel, vaeOpts.latentDim);            ***4***
  }
  • 1 获取一批(扁平化的)Fashion-MNIST 图像
  • 2 VAE 训练的单个步骤:使用 VAE 进行预测,并计算损失,以便 optimizer.minimize 可以调整模型的所有可训练权重
  • 3 由于我们不使用默认的fit()方法,因此不能使用内置的进度条,必须自己打印控制台上的状态更新。
  • 4 在每个训练周期结束时,使用解码器生成一幅图像,并将其打印到控制台以进行预览

yarn watch 命令打开的网页将加载保存的解码器,并使用它生成类似于图 10.8 所示的图像网格。这些图像是从二维潜在空间中的正则网格的潜在向量获得的。每个潜在维度上的上限和下限可以在 UI 中进行调整。

图 10.8. 在训练后对 VAE 的潜在空间进行采样。该图显示了一个 20 × 20 的解码器输出网格。该网格对应于一个 20 × 20 的二维潜在向量的正则间隔网格,其中每个维度位于[-4, 4]的区间内。

图像网格显示了来自 Fashion-MNIST 数据集的完全连续的不同类型的服装,一种服装类型在潜在空间中沿着连续路径逐渐变形为另一种类型(例如,套头衫变成 T 恤,T 恤变成裤子,靴子变成鞋子)。潜在空间的特定方向在潜在空间的子域内具有一定的意义。例如,在潜在空间的顶部区域附近,水平维度似乎代表“靴子特性与鞋子特性;”在潜在空间的右下角附近,水平维度似乎代表“T 恤特性与裤子特性”,依此类推。

在接下来的章节中,我们将介绍另一种生成图像的主要模型类型:GANs。

JavaScript 深度学习(四)(2)https://developer.aliyun.com/article/1516979

相关实践学习
基于阿里云DeepGPU实例,用AI画唯美国风少女
本实验基于阿里云DeepGPU实例,使用aiacctorch加速stable-diffusion-webui,用AI画唯美国风少女,可提升性能至高至原性能的2.6倍。
相关文章
|
23天前
|
机器学习/深度学习 JavaScript 前端开发
JavaScript 深度学习(五)(5)
JavaScript 深度学习(五)
8 0
|
23天前
|
机器学习/深度学习 JavaScript 前端开发
JavaScript 深度学习(五)(4)
JavaScript 深度学习(五)
24 0
|
23天前
|
存储 机器学习/深度学习 JavaScript
JavaScript 深度学习(五)(3)
JavaScript 深度学习(五)
22 0
|
23天前
|
机器学习/深度学习 并行计算 JavaScript
JavaScript 深度学习(五)(2)
JavaScript 深度学习(五)
29 0
|
23天前
|
机器学习/深度学习 人工智能 JavaScript
JavaScript 深度学习(五)(1)
JavaScript 深度学习(五)
25 0
|
23天前
|
机器学习/深度学习 前端开发 JavaScript
JavaScript 深度学习(四)(5)
JavaScript 深度学习(四)
12 1
|
23天前
|
机器学习/深度学习 存储 算法
JavaScript 深度学习(四)(4)
JavaScript 深度学习(四)
22 1
|
23天前
|
机器学习/深度学习 算法 JavaScript
JavaScript 深度学习(四)(3)
JavaScript 深度学习(四)
26 1
|
23天前
|
机器学习/深度学习 编解码 JavaScript
JavaScript 深度学习(四)(2)
JavaScript 深度学习(四)
22 1
|
2天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的校园竞赛管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的校园竞赛管理系统附带文章源码部署视频讲解等
148 63