hsn-ai-iot-merge-1

60 阅读25分钟

物联网的人工智能实用指南(二)

原文:annas-archive.org/md5/0095b5f3f49dd33558a193990b8e61a8

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:遗传算法在物联网中的应用

在上一章中,我们研究了不同的基于深度学习的算法;这些算法在识别、检测、重建甚至在视觉、语音和文本数据生成领域取得了成功。目前,深度学习DL)在应用和就业方面处于领先地位,但它与进化算法有着激烈的竞争。这些算法受自然进化过程的启发,是世界上最好的优化器。是的,连我们自己也是多年遗传进化的结果。在这一章中,你将进入迷人的进化算法世界,并更详细地了解一种特定类型的进化算法——遗传算法。在这一章中,你将学习到以下内容:

  • 什么是优化

  • 解决优化问题的不同方法

  • 理解遗传算法背后的直觉

  • 遗传算法的优点

  • 理解并实现交叉、变异和适应度函数选择的过程

  • 使用遗传算法找回丢失的密码

  • 遗传算法在优化模型中的各种应用

  • Python 遗传算法库中的分布式进化算法

优化

优化(Optimization)不是一个新词汇;我们之前已经在机器学习和深度学习(DL)算法中使用过它,当时我们使用了 TensorFlow 自动微分器,通过一种梯度下降算法找到最佳的模型权重和偏差。在本节中,我们将深入了解优化、优化问题以及执行优化的不同技术。

从最基本的角度来看,优化是使某物变得更好的过程。其核心思想是找到最佳的解决方案,显然,当我们谈论最佳解决方案时,意味着存在不止一个解决方案。在优化中,我们尝试调整我们的变量参数/过程/输入,以便找到最小或最大的输出。通常,这些变量构成输入,我们有一个称为目标函数损失函数适应度函数的函数,作为输出我们期望的是成本/损失或适应度。成本或损失应该最小化,如果我们定义适应度,那么它应该最大化。在这里,我们通过改变输入(变量)来实现所需的(优化的)输出。

我希望你能理解,称其为损失/成本或适应度只是一个选择问题,计算成本并需要最小化的函数,如果我们给它加上一个负号,那么我们期望修改后的函数能够最大化。例如,最小化2 - x²在区间*-2 < x < 2上,和在相同区间内最大化x*² - 2 是一样的。

我们的日常生活中充满了许多这样的优化任务。到办公室的最佳路线应该是怎样的?我应该先做哪个项目?为面试做准备,应该阅读哪些主题以最大化面试成功率?下图展示了输入变量需要优化的函数输出/成本之间的基本关系:

输入、需要优化的函数和输出之间的关系

目标是最小化成本,使得函数指定的约束条件通过输入变量得到满足。成本函数、约束条件和输入变量之间的数学关系决定了优化问题的复杂性。一个关键问题是成本函数和约束条件是凸的还是非凸的。如果成本函数和约束条件是凸的,我们可以确信确实存在可行解,并且如果我们在一个足够大的领域内进行搜索,我们一定能找到一个。下图展示了一个凸成本函数的示例:

一个凸成本函数。左边是表面图,右边显示的是同一成本函数的等高线图。图像中最深的红色点对应于最优解点。

另一方面,如果成本函数或约束条件是非凸的,优化问题会变得更加困难,我们无法确定是否确实存在解决方案,或者我们是否能够找到一个。

在数学和计算机编程中,有多种方法可以解决优化问题。接下来让我们了解一下它们每一种方法。

确定性和解析方法

当目标函数是平滑的并且具有连续的二阶导数时,根据微积分的知识,我们知道在局部最小值处,以下条件成立:

  • 在最小值处,目标函数的梯度是 0,即f'(x) = 0

  • 二阶导数(Hessian H(x) = ∇²f(x))是正定的

在这种情况下,对于某些问题,可以通过确定梯度的零点并验证 Hessian 矩阵在零点处的正定性来解析地找到解决方案。因此,在这些情况下,我们可以通过迭代地探索搜索空间来找到目标函数的最小值。有多种搜索方法;让我们来看一看。

梯度下降法

我们在前面的章节中学习了梯度下降及其工作原理,看到搜索方向是梯度下降的方向,-∇f(x)。这也叫做柯西方法,因为它是由柯西于 1847 年提出的,从那时起就非常流行。我们从目标函数表面上的一个任意点开始,沿着梯度方向改变变量(在前面的章节中,这些是权重和偏置)。数学上,它表示为:

这里,α[n]是迭代n时的步长(变化/学习率)。梯度下降算法在训练深度学习模型时效果良好,但也有一些严重的缺点:

  • 所使用的优化器性能在很大程度上取决于学习率和其他常数。如果稍微改变它们,网络很可能不会收敛。正因为如此,研究人员有时将训练模型称为一门艺术,或炼金术。

  • 由于这些方法基于导数,它们不适用于离散数据。

  • 当目标函数是非凸时,我们无法可靠地应用该方法,这在许多深度学习网络中是常见的(尤其是使用非线性激活函数的模型)。许多隐藏层的存在可能导致多个局部最小值,模型很有可能陷入局部最小值中。这里,你可以看到一个具有多个局部最小值的目标函数示例:

具有多个局部最小值的成本函数。左侧是表面图,右侧是相同成本函数的等高线图。图中的深红色点对应于最小值。

梯度下降法有许多变种,其中最流行的几种可以在 TensorFlow 优化器中找到,包括以下几种:

  • 随机梯度优化器

  • Adam 优化器

  • Adagrad 优化器

  • RMSProp 优化器

你可以通过 TensorFlow 文档中的www.tensorflow.org/api_guides/python/train了解更多有关 TensorFlow 中不同优化器的信息。

一个不错的来源是 Sebastian Ruder 的博客(ruder.io/optimizing-gradient-descent/index.html#gradientdescentoptimizationalgorithms),基于他在 arXiv 上的论文arxiv.org/abs/1609.04747

牛顿-拉夫森方法

该方法基于目标函数f(x)在点*x^*附近的二阶泰勒级数展开:

这里,x**是泰勒级数展开的点,x是靠近x**的点,超脚本T表示转置,H是 Hessian 矩阵,其元素如下所示:

通过对泰勒级数展开式求梯度并使其等于0,我们得到:

假设初始猜测为x[0],下一个点x[n+1]可以通过以下公式从前一个点x[n]计算得到:

该方法同时使用目标函数的一阶和二阶偏导数来寻找最小值。在第k次迭代时,它通过围绕x(k)的二次函数来逼近目标函数,并朝向最小值移动。

由于计算 Hessian 矩阵在计算上很昂贵且通常未知,因此有许多算法致力于逼近 Hessian 矩阵;这些技术被称为拟牛顿方法。它们可以表示如下:

α[n]是第n次迭代中的步长(变化/学习率),A[n]是第n次迭代中的 Hessian 矩阵逼近值。我们构造一个 Hessian 的逼近序列,使得以下条件成立:

两种流行的拟牛顿方法如下:

  • Davidon-Fletcher-Powell 算法

  • Broyden-Fletcher-Goldfarb-Shanno 算法

当 Hessian 的逼近*A[n]*是单位矩阵时,牛顿法变成了梯度下降法。

牛顿法的主要缺点是它无法扩展到具有高维输入特征空间的问题。

自然优化方法

自然优化方法受到一些自然过程的启发,即在自然界中存在的一些在优化某些自然现象上非常成功的过程。这些算法不需要求取目标函数的导数,因此即使是离散变量和非连续目标函数也能使用。

模拟退火

模拟退火是一种随机方法。它受到物理退火过程的启发,在该过程中,一个固体首先被加热到足够高的温度使其融化,然后温度慢慢降低;这使得固体的粒子能够以最低的能量状态重新排列,从而产生一个高度结构化的晶格。

我们从为每个变量分配一些随机值开始,这表示初始状态。在每一步中,我们随机选择一个变量(或一组变量),然后选择一个随机值。如果将该值分配给变量后,目标函数有所改善,则算法接受该赋值,形成新的当前赋值,系统状态发生变化。否则,它以一定的概率P接受该赋值,P的值依赖于温度T以及当前状态和新状态下目标函数值的差异。如果不接受变化,则当前状态保持不变。从状态i到状态j的转变概率P如下所示:

这里,T表示一个与物理系统中温度类似的变量。当温度趋近于0时,模拟退火算法就变成了梯度下降算法。

粒子群优化

粒子群优化PSO)由爱德华和肯尼迪于 1995 年提出。它基于动物的社会行为,例如鸟群。你一定注意到,在天空中,鸟群飞行时呈 V 字形。研究鸟类行为的人告诉我们,当鸟群寻找食物或更好的栖息地时,它们会以这种方式飞行,队伍前面的鸟最接近目标源。

现在,当它们飞行时,领头的鸟并不固定不变;相反,随着它们的移动,领头鸟会发生变化。看到食物的鸟会发出声音信号,其他鸟会围绕着它以 V 字形聚集。这是一个连续重复的过程,已经让鸟类受益了数百万年。

PSO 从这种鸟类行为中汲取灵感,并利用它来解决优化问题。在 PSO 中,每个解决方案都是搜索空间中的一只鸟(称为粒子)。每个粒子都有一个适应度值,这个值通过待优化的适应度函数来评估;它们还有速度,决定了粒子的飞行方向。粒子通过跟随当前最优粒子飞行,通过问题的搜索空间。

粒子在搜索空间中根据两个最佳适应度值进行移动,一个是它们自己已知的最佳位置(pbest:粒子最佳),另一个是整个群体已知的最佳适应度值(gbest:全局最佳)。随着改进位置的发现,这些位置被用来指导粒子群体的运动。这个过程不断重复,期望最终能够找到一个最优解。

遗传算法

当我们环顾四周,看到不同的物种时,一个问题自然会浮现:为什么这些特征组合能稳定存在,而其他特征不能?为什么大多数动物有两条腿或四条腿,而不是三条腿?我们今天看到的这个世界,是否是经过多次迭代优化算法的结果?

设想有一个成本函数来衡量生存能力,并且该生存能力应该是最大化的。自然界生物的特性适应于一个拓扑景观。生存能力的水平(通过适应性来衡量)代表景观的高度。最高的点对应于最适合的条件,而限制条件则由环境以及不同物种之间的相互作用提供。

那么,进化过程可以看作是一个庞大的优化算法,它选择哪些特征能产生适应生存的物种。景观的顶峰由生物体占据。有些顶峰非常广阔,容纳了多种特征,涵盖了许多物种,而另一些顶峰则非常狭窄,只允许具有非常特定特征的物种存在。

我们可以将这一类比扩展到包括分隔不同物种的山谷。我们可以认为人类可能处于这个景观的全局最优峰值,因为我们拥有智慧和改变环境的能力,并能确保在极端环境下的更好生存能力。

因此,可以将这个拥有不同生命形式的世界视作一个巨大的搜索空间,而不同的物种则是许多次迭代优化算法的结果。这一思想构成了遗传算法的基础。

本章的主题是遗传算法,让我们深入探讨一下它们。

遗传算法简介

根据著名生物学家查尔斯·达尔文的研究,我们今天看到的动物和植物物种是经过数百万年的进化而形成的。进化过程遵循适者生存的原则,选择那些拥有更好生存机会的生物。我们今天看到的植物和动物是数百万年适应环境约束的结果。在任何时候,许多不同的生物可能会共存并争夺相同的环境资源。

那些最具资源获取能力和繁殖能力的生物,它们的后代将拥有更多的生存机会。另一方面,能力较弱的生物往往会有很少或没有后代。随着时间的推移,整个种群会发生演化,平均而言,新一代的生物会比前几代更具适应性。

是什么使得这一切成为可能?是什么决定了一个人会很高,而一棵植物会有特定形状的叶子?这一切都被像一套规则一样编码在生命蓝图中的程序里——基因。地球上的每一个生物都拥有这套规则,它们描述了该生物是如何被设计(创造)的。基因存在于染色体中。每个生物都有不同数量的染色体,这些染色体包含数千个基因。例如,我们人类有 46 条染色体,这些染色体包含约 20,000 个基因。每个基因代表一个特定的规则:比如一个人会有蓝色的眼睛,棕色的头发,是女性,等等。这些基因通过繁殖过程从父母传递给后代。

基因从父母传递给后代的方式有两种:

  • 无性繁殖:在这种情况下,子代是父代的完全复制。它发生在一个叫做有丝分裂的生物过程里;如细菌和真菌等低等生物通过有丝分裂繁殖。此过程中只需要一个父母:

有丝分裂过程:父母的染色体首先翻倍,然后细胞分裂成两个子细胞

  • 有性生殖:这一过程通过一种叫做减数分裂的生物学过程完成。在这一过程中,最初涉及两个父母;每个父母的细胞经历交叉过程,其中一条染色体的一部分与另一条染色体的一部分交换位置。这样改变了遗传序列,细胞随后分裂为两部分,但每部分只包含一半的染色体数。来自两个父母的单倍体细胞最终结合,形成受精卵,后通过有丝分裂和细胞分化,最终产生一个与父母相似但又有所不同的后代

细胞分裂过程:父母的细胞染色体发生交叉,一部分染色体与另一部分染色体交换位置。然后,细胞分裂为两部分,每个分裂的细胞只包含一条染色体(单倍体)。来自两个父母的两个单倍体细胞随后结合,完成染色体的总数。

自然选择和进化过程中另一个有趣的现象是突变现象。在这里,基因发生突然变化,产生一个完全新的基因,这个基因在父母双方中都没有出现过。这个现象进一步增加了多样性。

通过世代间的有性生殖,应该带来进化,并确保具有最适应特征的生物拥有更多的后代。

遗传算法

现在让我们来了解如何实现遗传算法。这一方法是由约翰·霍兰德于 1975 年提出的。他的学生戈德堡展示了这一方法可以用来解决优化问题,并且使用遗传算法来控制天然气管道的传输。此后,遗传算法一直广受欢迎,并启发了其他各种进化程序的研究。

为了将遗传算法应用于计算机优化问题的求解,第一步我们需要将问题变量编码为基因。这些基因可以是实数的字符串或二进制位串(0 和 1 的序列)。这代表了一个潜在的解决方案(个体),而多个这样的解决方案一起构成了时间点t时的人口。例如,考虑一个需要找到两个变量 a 和 b 的问题,其中这两个变量的范围是(0, 255)。对于二进制基因表示,这两个变量可以通过一个 16 位的染色体表示,其中高 8 位代表基因 a,低 8 位代表基因 b。编码之后需要解码才能获得变量 a 和 b 的实际值。

遗传算法的第二个重要要求是定义一个合适的适应度函数,该函数计算任何潜在解的适应度分数(在前面的示例中,它应该计算编码染色体的适应度值)。这是我们希望通过寻找系统或问题的最优参数集来优化的函数。适应度函数是与问题相关的。例如,在自然进化过程中,适应度函数代表生物体在其环境中生存和运作的能力。

一旦我们决定了问题解决方案在基因中的编码方式,并确定了适应度函数,遗传算法将遵循以下步骤:

  1. 种群初始化:我们需要创建一个初始种群,其中所有染色体(通常)是随机生成的,以产生整个可能解的范围(搜索空间)。偶尔,解决方案可能会在最有可能找到最佳解的区域中进行初始化。种群的大小取决于问题的性质,但通常包含数百个潜在的解决方案,这些解决方案被编码成染色体。

  2. 父代选择:对于每一代,根据适应度函数(或随机选择),我们接下来选择现有种群中的一定比例。这些被选中的种群将通过繁殖形成新一代。这个过程是通过竞赛选择法来完成的:随机选择固定数量的个体(竞赛大小),然后选择适应度分数最好的个体作为父母之一。

  3. 繁殖:接下来,我们通过在步骤 2 中选择的个体,通过遗传算子如交叉和变异来生成后代。最终,这些遗传算子会产生一个与初代不同但又在许多方面继承了父母特征的后代染色体种群。

  4. 评估:生成的后代将通过适应度函数进行评估,并且它们将替换种群中最不适应的个体,以保持种群大小不变。

  5. 终止:在评估步骤中,如果任何后代达到了目标适应度分数或达到最大代数,遗传算法过程将终止。否则,步骤24将重复进行,以产生下一代。

两个对遗传算法成功至关重要的算子是交叉和变异。

交叉

为了执行交叉操作,我们在两个父母的染色体上选择一个随机位置,然后基于这个点交换它们的遗传信息,交叉概率为P[x]。这将产生两个新的后代。当交叉发生在一个随机点时,称为单点交叉(或Single Point Crossover):

单点交叉:随机选择父代染色体中的一个点,并交换相应的基因位。

我们也可以在多个位置交换父代的基因;这称为多点交叉

多点交叉:在多个位置交换父代的基因。这是双点交叉的一个例子。

存在许多不同的交叉方式,例如,均匀交叉、基于顺序的交叉和循环交叉。

变异

虽然交叉操作确保了多样性并且有助于加速搜索,但它并不产生新的解。这是变异操作符的工作,变异操作符帮助保持和引入种群中的多样性。变异操作符以概率*P[m]*应用于子代染色体的某些基因(位)。

我们可以进行位翻转变异;如果我们考虑之前的例子,那么在 16 位染色体中,位翻转变异将导致一个位的状态发生变化(从0变为1,或者从1变为0)。

我们有可能将基因设置为所有可能值中的一个随机值,这称为随机重置

概率*P[m]起着重要作用;如果我们给P[m]分配一个非常低的值,它可能导致遗传漂移,但另一方面,过高的P[m]*可能会导致丧失好的解。我们选择一个变异概率,使得算法学会牺牲短期适应度来获得长期适应度。

优缺点

遗传算法听起来很酷,对吧!现在,在我们尝试围绕它们构建代码之前,让我们先指出遗传算法的一些优缺点。

优势

遗传算法提供了一些令人着迷的优势,并且在传统的基于梯度的方法失败时也能产生结果。

  • 它们可以用于优化连续变量或离散变量。

  • 与梯度下降不同,我们不需要导数信息,这也意味着适应度函数不必是连续可微的。

  • 它可以从广泛的成本表面进行同时搜索。

  • 我们可以处理大量变量,而不会显著增加计算时间。

  • 种群的生成及其适应度值的计算可以并行进行,因此遗传算法非常适合并行计算机。

  • 它们即使在拓扑表面极其复杂时也能正常工作,因为交叉和变异操作符帮助它们跳出局部最小值。

  • 它们可以提供多个最优解。

  • 我们可以将它们应用于数值生成的数据、实验数据,甚至是分析函数。它们特别适用于大规模优化问题。

劣势

尽管之前提到的优势存在,我们仍然不认为遗传算法是所有优化问题的普适解决方案。原因如下:

  • 如果优化函数是一个良性凸函数,那么基于梯度的方法将实现更快的收敛速度

  • 大量的解集帮助遗传算法更广泛地覆盖搜索空间,但也导致了收敛速度变慢

  • 设计一个适应度函数可能是一项艰巨的任务

使用 Python 中的分布式进化算法编码遗传算法

现在我们理解了遗传算法的工作原理,接下来我们可以尝试用它们解决一些问题。它们已经被用来解决 NP 难题,例如旅行推销员问题。为了简化生成种群、执行交叉操作和进行变异操作的任务,我们将使用分布式进化算法在 Python 中DEAP)。它支持多进程,我们也可以用它来进行其他进化算法的应用。你可以通过 PyPi 直接下载 DEAP,方法如下:

pip install deap

它与 Python 3 兼容。

要了解更多关于 DEAP 的信息,可以参考其 GitHub 仓库(github.com/DEAP/deap)和用户指南(deap.readthedocs.io/en/master/)。

猜测单词

在这个程序中,我们使用遗传算法猜测一个单词。遗传算法知道单词中有多少个字母,并会一直猜这些字母,直到找到正确的答案。我们决定将基因表示为单个字母数字字符;这些字符的字符串构成了染色体。我们的适应度函数是个体与正确单词中匹配的字符数之和:

  1. 作为第一步,我们导入所需的模块。我们使用string模块和random模块来生成随机字符(a—z、A—Z 以及 0—9)。从 DEAP 模块中,我们使用creatorbasetools
import string
import random

from deap import base, creator, tools
  1. 在 DEAP 中,我们首先创建一个继承自deep.base模块的类。我们需要告诉它我们是进行函数的最小化还是最大化;这通过权重参数来实现。+1表示我们在进行最大化(如果是最小化,则值为-1.0)。以下代码行将创建一个类FitnessMax,它将最大化该函数:
creator.create("FitnessMax", base.Fitness, weights=(1.0,))  
  1. 我们还定义了一个Individual类,该类将继承列表类,并告诉 DEAP 创作者模块将FitnessMax分配为其fitness属性:
creator.create("Individual", list, fitness=creator.FitnessMax)
  1. 现在,定义了Individual类后,我们使用 DEAP 在基础模块中定义的toolbox。我们将用它来创建种群并定义基因池。从现在开始,我们所需的所有对象——个体、种群、函数、算子和参数——都存储在一个叫做toolbox的容器中。我们可以使用register()unregister()方法向toolbox容器中添加或删除内容:
toolbox = base.Toolbox()
# Gene Pool
toolbox.register("attr_string", random.choice, \
               string.ascii_letters + string.digits )
  1. 现在我们已经定义了如何创建基因池,我们通过反复使用Individual类来创建个体和种群。我们将类传递给负责创建N参数的工具箱,告诉它需要生成多少个基因:
#Number of characters in word
# The word to be guessed
word = list('hello')
N = len(word)
# Initialize population
toolbox.register("individual", tools.initRepeat, \
         creator.Individual, toolbox.attr_string, N )
toolbox.register("population",tools.initRepeat, list,\
         toolbox.individual)
  1. 我们定义了fitness函数。注意返回语句中的逗号。这是因为 DEAP 中的适应度函数以元组的形式返回,以支持多目标的fitness函数:
def evalWord(individual, word):
    return sum(individual[i] == word[i] for i in\
            range(len(individual))),    
  1. 将适应度函数添加到容器中。同时,添加交叉算子、变异算子和父代选择算子。可以看到,为此我们使用了register函数。在第一行中,我们注册了已定义的适应度函数,并传入它将接受的额外参数。下一行注册了交叉操作;它指定我们这里使用的是双点交叉(cxTwoPoint)。接下来,我们注册了变异算子;我们选择了mutShuffleIndexes选项,它会以indpb=0.05的概率打乱输入个体的属性。最后,我们定义了父代选择的方式;在这里,我们定义了采用比赛选择的方法,比赛大小为3
toolbox.register("evaluate", evalWord, word)
toolbox.register("mate", tools.cxTwoPoint)
toolbox.register("mutate", tools.mutShuffleIndexes, indpb=0.05)
toolbox.register("select", tools.selTournament, tournsize=3)
  1. 现在我们已经有了所有的组成部分,接下来我们将编写遗传算法的代码,它将按我们之前提到的步骤反复执行:
def main():
    random.seed(64)
    # create an initial population of 300 individuals 
    pop = toolbox.population(n=300)
    # CXPB is the crossover probability 
    # MUTPB is the probability for mutating an individual
    CXPB, MUTPB = 0.5, 0.2

    print("Start of evolution")

    # Evaluate the entire population
    fitnesses = list(map(toolbox.evaluate, pop))
    for ind, fit in zip(pop, fitnesses):
        ind.fitness.values = fit

    print(" Evaluated %i individuals" % len(pop))

    # Extracting all the fitnesses of individuals in a list
    fits = [ind.fitness.values[0] for ind in pop]
    # Variable keeping track of the number of generations
    g = 0

    # Begin the evolution
    while max(fits) < 5 and g < 1000:
        # A new generation
        g += 1
        print("-- Generation %i --" % g)

        # Select the next generation individuals
        offspring = toolbox.select(pop, len(pop))
        # Clone the selected individuals
        offspring = list(map(toolbox.clone, offspring))

        # Apply crossover and mutation on the offspring
        for child1, child2 in zip(offspring[::2], offspring[1::2]):
            # cross two individuals with probability CXPB
            if random.random() < CXPB:    
            toolbox.mate(child1, child2)
            # fitness values of the children
            # must be recalculated later
            del child1.fitness.values
            del child2.fitness.values
        for mutant in offspring:
            # mutate an individual with probability MUTPB
            if random.random() < MUTPB:
                toolbox.mutate(mutant)
                del mutant.fitness.values

        # Evaluate the individuals with an invalid fitness
        invalid_ind = [ind for ind in offspring if not ind.fitness.valid]
        fitnesses = map(toolbox.evaluate, invalid_ind)
        for ind, fit in zip(invalid_ind, fitnesses):
        ind.fitness.values = fit

        print(" Evaluated %i individuals" % len(invalid_ind))

        # The population is entirely replaced by the offspring
        pop[:] = offspring

        # Gather all the fitnesses in one list and print the stats
        fits = [ind.fitness.values[0] for ind in pop]

        length = len(pop)
        mean = sum(fits) / length
        sum2 = sum(x*x for x in fits)
        std = abs(sum2 / length - mean**2)**0.5

        print(" Min %s" % min(fits))
        print(" Max %s" % max(fits))
        print(" Avg %s" % mean)
        print(" Std %s" % std)

    print("-- End of (successful) evolution --")

    best_ind = tools.selBest(pop, 1)[0]
    print("Best individual is %s, %s" % (''.join(best_ind),\
             best_ind.fitness.values))
  1. 在这里,你可以看到这个遗传算法的结果。在七代内,我们找到了正确的词:

DEAP 提供了选择各种交叉工具、不同变异算子,甚至如何进行比赛选择的选项。DEAP 提供的所有进化工具及其说明的完整列表可在deap.readthedocs.io/en/master/api/tools.html.查看。

CNN 架构的遗传算法

在第四章《物联网中的深度学习》中,我们了解了不同的深度学习模型,如 MLP、CNN、RNN 等。现在,我们将看到如何将遗传算法应用于这些深度学习模型。遗传算法可以用来找到优化的权重和偏差,已经有人尝试过了。但在深度学习模型中,遗传算法最常见的应用是寻找最优超参数。

在这里,我们使用遗传算法来寻找最优的 CNN 架构。该方案基于 Lingxi Xie 和 Alan Yuille 发表的论文 Genetic CNNarxiv.org/abs/1703.01513)。第一步是找到问题的正确表示方法。作者提出了网络架构的二进制串表示。网络的家族被编码成固定长度的二进制串。网络由 S 个阶段组成,其中第 s 阶段 s = 1, 2,....S,包含 K[s] 个节点,表示为 ,这里 k[s] = 1, 2,..., K[s][. 每个阶段的节点是有序的,并且为了正确表示,只允许从较低编号的节点连接到较高编号的节点。每个节点表示一个卷积层操作,随后是批量归一化和 ReLU 激活。位串的每一位表示一个卷积层(节点)与另一个卷积层之间连接的存在或不存在,位的顺序如下:第一位表示 (v[s,1],v[s,2]) 之间的连接,接下来的两位表示 (v[s,1],v[s,3]) 和 (v[s,2],v[s,3]) 之间的连接,接下来的三位表示 (v[s,1],v[s,3]),(v[s,1],v[s,4]) 和 (v[s,2],v[s,4]) 之间的连接,依此类推。

为了更好地理解它,我们考虑一个两阶段的网络(每个阶段将具有相同数量的滤波器和滤波器大小)。假设阶段 S[1] 包含四个节点(即 K[s] = 4),因此需要编码的位数总共为 (4×3×½ =) 6。阶段 1 中的卷积滤波器数量是 3*2;同时我们确保卷积操作不会改变图像的空间维度(例如,填充保持一致)。下图显示了相应的位串编码及对应的卷积层连接。红色连接是默认连接,不在位串中编码。第一位编码了 (a1a2) 之间的连接,接下来的两位编码了 (a1a3) 和 (a2a3) 之间的连接,最后三位编码了 (a1a4),(a2a4) 和 (a3a4) 之间的连接:

位串编码及对应的卷积层连接

阶段 1 接受一个 32 × 32 × 3 的输入;该阶段的所有卷积节点都有 32 个滤波器。红色连接表示默认连接,不在位串中编码。绿色连接表示根据编码的位串 1-00-111 所表示的连接。阶段 1 的输出将传递到池化层,并在空间维度上减半。

第二阶段有五个节点,因此需要(5×4×½ =) 10 位。它将从第一阶段1接收输入,维度为16 × 16 × 32。现在,如果我们将第二阶段2中的卷积滤波器数量设为64,那么池化后的输出将是 8 × 8 × 64。

这里呈现的代码来自github.com/aqibsaeed/Genetic-CNN。由于我们需要表示图结构,因此网络是使用有向无环图DAG)构建的。为了表示 DAG,我们定义了一个类 DAG,其中定义了添加新节点、删除现有节点、在两个节点之间添加边(连接)和删除两个节点之间的边的方法。除此之外,还定义了查找节点前驱节点、连接到该节点的节点以及图的叶子节点列表的方法。完整代码位于dag.py中,可以通过 GitHub 链接访问。

主要代码在Genetic_CNN.ipynb Jupyter 笔记本中给出。我们使用 DEAP 库来运行遗传算法,并使用 TensorFlow 根据遗传算法构建的图来构建 CNN。适应度函数是准确度。代码旨在找到在 MNIST 数据集上能够给出最高准确度的 CNN(我们在第四章《物联网深度学习》中使用了手写数字,这里我们直接从 TensorFlow 库中获取它们):

  1. 第一步是导入模块。这里,我们将需要 DEAP 和 TensorFlow,还将导入我们在dag.py中创建的 DAG 类,以及标准的 Numpy 和 Random 模块:
import random
import numpy as np

from deap import base, creator, tools, algorithms
from scipy.stats import bernoulli
from dag import DAG, DAGValidationError

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
  1. 我们直接从 TensorFlow 示例库中读取数据:
mnist = input_data.read_data_sets("mnist_data/", one_hot=True)
train_imgs   = mnist.train.images
train_labels = mnist.train.labels
test_imgs    = mnist.test.images
test_labels  = mnist.test.labels

train_imgs = np.reshape(train_imgs,[-1,28,28,1])
test_imgs = np.reshape(test_imgs,[-1,28,28,1])
  1. 现在,我们构建将保存网络信息的比特数据结构。我们设计的网络是一个三阶段网络,第一个阶段有三个节点(3 位),第二个阶段有四个节点(6 位),第三个阶段有五个节点(10 位)。因此,一个个体将由一个二进制字符串表示,长度为3 + 6 + 10 = 19 位:
STAGES = np.array(["s1","s2","s3"]) # S
NUM_NODES = np.array([3,4,5]) # K

L =  0 # genome length
BITS_INDICES, l_bpi = np.empty((0,2),dtype = np.int32), 0 # to keep track of bits for each stage S
for nn in NUM_NODES:
    t = nn * (nn - 1)
    BITS_INDICES = np.vstack([BITS_INDICES,[l_bpi, l_bpi + int(0.5 * t)]])
    l_bpi = int(0.5 * t)
    L += t
L = int(0.5 * L)

TRAINING_EPOCHS = 20
BATCH_SIZE = 20
TOTAL_BATCHES = train_imgs.shape[0] // BATCH_SIZE
  1. 现在是根据编码后的比特串构建图的部分。这将有助于为遗传算法构建种群。首先,我们定义构建 CNN 所需的函数(weight_variable:为卷积节点创建权重变量;bias_variable:为卷积节点创建偏置变量;apply_convolution:执行卷积操作的函数;apply_pool:在每个阶段之后执行池化操作的函数;最后,使用linear_layer函数构建最后的全连接层):
def weight_variable(weight_name, weight_shape):
    return tf.Variable(tf.truncated_normal(weight_shape, stddev = 0.1),name = ''.join(["weight_", weight_name]))

def bias_variable(bias_name,bias_shape):
    return tf.Variable(tf.constant(0.01, shape = bias_shape),name = ''.join(["bias_", bias_name]))

def linear_layer(x,n_hidden_units,layer_name):
    n_input = int(x.get_shape()[1])
    weights = weight_variable(layer_name,[n_input, n_hidden_units])
    biases = bias_variable(layer_name,[n_hidden_units])
    return tf.add(tf.matmul(x,weights),biases)

def apply_convolution(x,kernel_height,kernel_width,num_channels,depth,layer_name):
    weights = weight_variable(layer_name,[kernel_height, kernel_width, num_channels, depth])
    biases = bias_variable(layer_name,[depth])
    return tf.nn.relu(tf.add(tf.nn.conv2d(x, weights,[1,2,2,1],padding = "SAME"),biases)) 

def apply_pool(x,kernel_height,kernel_width,stride_size):
    return tf.nn.max_pool(x, ksize=[1, kernel_height, kernel_width, 1], 
            strides=[1, 1, stride_size, 1], padding = "SAME")
  1. 现在,我们可以基于编码后的比特串构建网络。所以,我们使用generate_dag函数生成 DAG:
def generate_dag(optimal_indvidual,stage_name,num_nodes):
    # create nodes for the graph
    nodes = np.empty((0), dtype = np.str)
    for n in range(1,(num_nodes + 1)):
        nodes = np.append(nodes,''.join([stage_name,"_",str(n)]))

    # initialize directed asyclic graph (DAG) and add nodes to it
    dag = DAG()
    for n in nodes:
        dag.add_node(n)

    # split best indvidual found via genetic algorithm to identify vertices connections and connect them in DAG 
    edges = np.split(optimal_indvidual,np.cumsum(range(num_nodes - 1)))[1:]
    v2 = 2
    for e in edges:
        v1 = 1
        for i in e:
            if i:
                dag.add_edge(''.join([stage_name,"_",str(v1)]),''.join([stage_name,"_",str(v2)])) 
            v1 += 1
        v2 += 1

    # delete nodes not connected to anyother node from DAG
    for n in nodes:
        if len(dag.predecessors(n)) == 0 and len(dag.downstream(n)) == 0:
            dag.delete_node(n)
            nodes = np.delete(nodes, np.where(nodes == n)[0][0])

    return dag, nodes
  1. 生成的图用于使用generate_tensorflow_graph函数构建 TensorFlow 图。该函数利用add_node函数添加卷积层,使用sum_tensors函数将多个卷积层的输入合并:
def generate_tensorflow_graph(individual,stages,num_nodes,bits_indices):
    activation_function_pattern = "/Relu:0"

    tf.reset_default_graph()
    X = tf.placeholder(tf.float32, shape = [None,28,28,1], name = "X")
    Y = tf.placeholder(tf.float32,[None,10],name = "Y")

    d_node = X
    for stage_name,num_node,bpi in zip(stages,num_nodes,bits_indices):
        indv = individual[bpi[0]:bpi[1]]

        add_node(''.join([stage_name,"_input"]),d_node.name)
        pooling_layer_name = ''.join([stage_name,"_input",activation_function_pattern])

        if not has_same_elements(indv):
            # ------------------- Temporary DAG to hold all connections implied by genetic algorithm solution ------------- #  

            # get DAG and nodes in the graph
            dag, nodes = generate_dag(indv,stage_name,num_node) 
            # get nodes without any predecessor, these will be connected to input node
            without_predecessors = dag.ind_nodes() 
            # get nodes without any successor, these will be connected to output node
            without_successors = dag.all_leaves()

            # ----------------------------------------------------------------------------------------------- #

            # --------------------------- Initialize tensforflow graph based on DAG ------------------------- #

            for wop in without_predecessors:
                add_node(wop,''.join([stage_name,"_input",activation_function_pattern]))

            for n in nodes:
                predecessors = dag.predecessors(n)
                if len(predecessors) == 0:
                    continue
                elif len(predecessors) > 1:
                    first_predecessor = predecessors[0]
                    for prd in range(1,len(predecessors)):
                        t = sum_tensors(first_predecessor,predecessors[prd],activation_function_pattern)
                        first_predecessor = t.name
                    add_node(n,first_predecessor)
                elif predecessors:
                    add_node(n,''.join([predecessors[0],activation_function_pattern]))

            if len(without_successors) > 1:
                first_successor = without_successors[0]
                for suc in range(1,len(without_successors)):
                    t = sum_tensors(first_successor,without_successors[suc],activation_function_pattern)
                    first_successor = t.name
                add_node(''.join([stage_name,"_output"]),first_successor) 
            else:
                add_node(''.join([stage_name,"_output"]),''.join([without_successors[0],activation_function_pattern])) 

            pooling_layer_name = ''.join([stage_name,"_output",activation_function_pattern])
            # ------------------------------------------------------------------------------------------ #

        d_node =  apply_pool(tf.get_default_graph().get_tensor_by_name(pooling_layer_name), 
                                 kernel_height = 16, kernel_width = 16,stride_size = 2)

    shape = d_node.get_shape().as_list()
    flat = tf.reshape(d_node, [-1, shape[1] * shape[2] * shape[3]])
    logits = linear_layer(flat,10,"logits")

    xentropy =  tf.nn.softmax_cross_entropy_with_logits(logits = logits, labels = Y)
    loss_function = tf.reduce_mean(xentropy)
    optimizer = tf.train.AdamOptimizer().minimize(loss_function) 
    accuracy = tf.reduce_mean(tf.cast( tf.equal(tf.argmax(tf.nn.softmax(logits),1), tf.argmax(Y,1)), tf.float32))

    return  X, Y, optimizer, loss_function, accuracy

# Function to add nodes
def add_node(node_name, connector_node_name, h = 5, w = 5, nc = 1, d = 1):
    with tf.name_scope(node_name) as scope:
        conv = apply_convolution(tf.get_default_graph().get_tensor_by_name(connector_node_name), 
                   kernel_height = h, kernel_width = w, num_channels = nc , depth = d, 
                   layer_name = ''.join(["conv_",node_name]))

def sum_tensors(tensor_a,tensor_b,activation_function_pattern):
    if not tensor_a.startswith("Add"):
        tensor_a = ''.join([tensor_a,activation_function_pattern])

    return tf.add(tf.get_default_graph().get_tensor_by_name(tensor_a),
                 tf.get_default_graph().get_tensor_by_name(''.join([tensor_b,activation_function_pattern])))

def has_same_elements(x):
    return len(set(x)) <= 1
  1. 适应度函数评估生成的 CNN 架构的准确性:
def evaluateModel(individual):
    score = 0.0
    X, Y, optimizer, loss_function, accuracy = generate_tensorflow_graph(individual,STAGES,NUM_NODES,BITS_INDICES)
    with tf.Session() as session:
        tf.global_variables_initializer().run()
        for epoch in range(TRAINING_EPOCHS):
            for b in range(TOTAL_BATCHES):
                offset = (epoch * BATCH_SIZE) % (train_labels.shape[0] - BATCH_SIZE)
                batch_x = train_imgs[offset:(offset + BATCH_SIZE), :, :, :]
                batch_y = train_labels[offset:(offset + BATCH_SIZE), :]
                _, c = session.run([optimizer, loss_function],feed_dict={X: batch_x, Y : batch_y})

        score = session.run(accuracy, feed_dict={X: test_imgs, Y: test_labels})
        #print('Accuracy: ',score)
    return score,
  1. 所以,现在我们准备实现遗传算法:我们的适应度函数将是一个最大值函数(weights=(1.0,)),我们使用伯努利分布(bernoulli.rvs)初始化二进制字符串,个体的长度为 L= 19,种群由 20 个个体组成。这一次,我们选择了有序交叉,其中从第一个父代选择一个子串并将其复制到子代的相同位置;剩余位置由第二个父代填充,确保子串中的节点不重复。我们保留了之前的变异操作符 mutShuffleIndexes;锦标赛选择方法为 selRoulette,它通过轮盘选择方法进行选择(我们选择 k 个个体,并从中选择适应度最高的个体)。这一次,我们没有自己编码遗传算法,而是使用了 DEAP 的 eaSimple 算法,这是基本的遗传算法:
population_size = 20
num_generations = 3
creator.create("FitnessMax", base.Fitness, weights = (1.0,))
creator.create("Individual", list , fitness = creator.FitnessMax)
toolbox = base.Toolbox()
toolbox.register("binary", bernoulli.rvs, 0.5)
toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.binary, n = L)
toolbox.register("population", tools.initRepeat, list , toolbox.individual)
toolbox.register("mate", tools.cxOrdered)
toolbox.register("mutate", tools.mutShuffleIndexes, indpb = 0.8)
toolbox.register("select", tools.selRoulette)
toolbox.register("evaluate", evaluateModel)
popl = toolbox.population(n = population_size)

import time
t = time.time()
result = algorithms.eaSimple(popl, toolbox, cxpb = 0.4, mutpb = 0.05, ngen = num_generations, verbose = True)
t1 = time.time() - t
print(t1)
  1. 算法将需要一些时间;在 i7 配备 NVIDIA 1070 GTX GPU 的机器上,大约需要 1.5 小时。最好的三个解决方案如下:
best_individuals = tools.selBest(popl, k = 3)
for bi in best_individuals:
    print(bi)

LSTM 优化的遗传算法

在遗传 CNN 中,我们使用遗传算法来估计最佳的 CNN 架构;在遗传 RNN 中,我们将使用遗传算法来寻找 RNN 的最佳超参数、窗口大小和隐藏单元数。我们将找到能够减少 均方根误差 (RMSE) 的参数。

超参数窗口大小和单元数再次被编码为二进制字符串,窗口大小使用 6 位,单元数使用 4 位。因此,完整编码的染色体将是 10 位。LSTM 使用 Keras 实现。

我们实现的代码来自 github.com/aqibsaeed/Genetic-Algorithm-RNN

  1. 必要的模块已导入。这一次,我们使用 Keras 来实现 LSTM 模型:
import numpy as np
import pandas as pd
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split as split

from keras.layers import LSTM, Input, Dense
from keras.models import Model

from deap import base, creator, tools, algorithms
from scipy.stats import bernoulli
from bitstring import BitArray

np.random.seed(1120)
  1. 我们需要的 LSTM 数据集必须是时间序列数据;我们使用来自 Kaggle 的风力发电预测数据 (www.kaggle.com/c/GEF2012-wind-forecasting/data):
data = pd.read_csv('train.csv')
data = np.reshape(np.array(data['wp1']),(len(data['wp1']),1))

train_data = data[0:17257]
test_data = data[17257:]
  1. 定义一个函数,根据选定的 window_size 准备数据集:
def prepare_dataset(data, window_size):
    X, Y = np.empty((0,window_size)), np.empty((0))
    for i in range(len(data)-window_size-1):
        X = np.vstack([X,data[i:(i + window_size),0]])
        Y = np.append(Y,data[i + window_size,0])   
    X = np.reshape(X,(len(X),window_size,1))
    Y = np.reshape(Y,(len(Y),1))
    return X, Y
  1. train_evaluate 函数为给定个体创建 LSTM 网络并返回其 RMSE 值(适应度函数):
def train_evaluate(ga_individual_solution):   
    # Decode genetic algorithm solution to integer for window_size and num_units
    window_size_bits = BitArray(ga_individual_solution[0:6])
    num_units_bits = BitArray(ga_individual_solution[6:]) 
    window_size = window_size_bits.uint
    num_units = num_units_bits.uint
    print('\nWindow Size: ', window_size, ', Num of Units: ', num_units)

    # Return fitness score of 100 if window_size or num_unit is zero
    if window_size == 0 or num_units == 0:
        return 100, 

    # Segment the train_data based on new window_size; split into train and validation (80/20)
    X,Y = prepare_dataset(train_data,window_size)
    X_train, X_val, y_train, y_val = split(X, Y, test_size = 0.20, random_state = 1120)

    # Train LSTM model and predict on validation set
    inputs = Input(shape=(window_size,1))
    x = LSTM(num_units, input_shape=(window_size,1))(inputs)
    predictions = Dense(1, activation='linear')(x)
    model = Model(inputs=inputs, outputs=predictions)
    model.compile(optimizer='adam',loss='mean_squared_error')
    model.fit(X_train, y_train, epochs=5, batch_size=10,shuffle=True)
    y_pred = model.predict(X_val)

    # Calculate the RMSE score as fitness score for GA
    rmse = np.sqrt(mean_squared_error(y_val, y_pred))
    print('Validation RMSE: ', rmse,'\n')

    return rmse,
  1. 接下来,我们使用 DEAP 工具定义个体(由于染色体是通过二进制编码的字符串(10 位)表示的,所以我们使用伯努利分布),创建种群,使用有序交叉,使用 mutShuffleIndexes 变异,并使用轮盘选择法来选择父代:
population_size = 4
num_generations = 4
gene_length = 10

# As we are trying to minimize the RMSE score, that's why using -1.0\. 
# In case, when you want to maximize accuracy for instance, use 1.0
creator.create('FitnessMax', base.Fitness, weights = (-1.0,))
creator.create('Individual', list , fitness = creator.FitnessMax)

toolbox = base.Toolbox()
toolbox.register('binary', bernoulli.rvs, 0.5)
toolbox.register('individual', tools.initRepeat, creator.Individual, toolbox.binary, n = gene_length)
toolbox.register('population', tools.initRepeat, list , toolbox.individual)

toolbox.register('mate', tools.cxOrdered)
toolbox.register('mutate', tools.mutShuffleIndexes, indpb = 0.6)
toolbox.register('select', tools.selRoulette)
toolbox.register('evaluate', train_evaluate)

population = toolbox.population(n = population_size)
r = algorithms.eaSimple(population, toolbox, cxpb = 0.4, mutpb = 0.1, ngen = num_generations, verbose = False)
  1. 我们得到最佳解决方案,如下所示:
best_individuals = tools.selBest(population,k = 1)
best_window_size = None
best_num_units = None

for bi in best_individuals:
    window_size_bits = BitArray(bi[0:6])
    num_units_bits = BitArray(bi[6:]) 
    best_window_size = window_size_bits.uint
    best_num_units = num_units_bits.uint
    print('\nWindow Size: ', best_window_size, ', Num of Units: ', best_num_units)
  1. 最后,我们实现了最佳 LSTM 解决方案:
X_train,y_train = prepare_dataset(train_data,best_window_size)
X_test, y_test = prepare_dataset(test_data,best_window_size)

inputs = Input(shape=(best_window_size,1))
x = LSTM(best_num_units, input_shape=(best_window_size,1))(inputs)
predictions = Dense(1, activation='linear')(x)
model = Model(inputs = inputs, outputs = predictions)
model.compile(optimizer='adam',loss='mean_squared_error')
model.fit(X_train, y_train, epochs=5, batch_size=10,shuffle=True)
y_pred = model.predict(X_test)

rmse = np.sqrt(mean_squared_error(y_test, y_pred))
print('Test RMSE: ', rmse)

耶!现在,你已经拥有了最好的 LSTM 网络来预测风能。

总结

本章介绍了一种有趣的自然启发算法家族:遗传算法。我们涵盖了各种标准优化算法,涵盖了从确定性模型到基于梯度的算法,再到进化算法。我们讨论了自然选择的生物学过程。接着,我们学习了如何将优化问题转换为适合遗传算法的形式。遗传算法中的两个关键操作——交叉和变异也得到了阐述。虽然无法广泛涵盖所有交叉和变异方法,但我们学习了其中一些流行的方式。

我们将所学应用于三种非常不同的优化问题。我们用它来猜一个单词。例子是一个五个字母的单词;如果我们使用简单的暴力搜索,搜索空间会是61⁵。我们使用遗传算法优化了 CNN 架构;同样注意,假设有19个可能的位,搜索空间是2¹⁹。然后,我们用它来寻找 LSTM 网络的最优超参数。

在下一章中,我们将讨论另一个引人入胜的学习范式:强化学习。这是另一种自然的学习范式,因为在自然界中,我们通常没有监督学习;相反,我们通过与环境的互动来学习。同样,在这里,智能体除了从环境中获得的奖励和惩罚外,什么也不被告知。

第六章:物联网的强化学习

强化学习RL)与监督学习和无监督学习有很大不同。它是大多数生物学习的方式——与环境互动。在本章中,我们将研究强化学习中使用的不同算法。随着章节的进展,你将完成以下内容:

  • 了解什么是强化学习,以及它与监督学习和无监督学习的不同

  • 学习强化学习的不同元素

  • 了解强化学习在现实世界中的一些迷人应用

  • 了解用于训练强化学习(RL)智能体的 OpenAI 接口

  • 了解 Q 学习并用它来训练一个强化学习智能体

  • 了解深度 Q 网络并用它来训练一个智能体玩 Atari 游戏

  • 了解策略梯度算法并运用它

介绍

你是否曾观察过婴儿如何学会翻身、坐起、爬行,甚至站立?你有没有看过小鸟如何学会飞翔——父母把它们从巢里扔出去,它们拍打一段时间,慢慢学会飞行。所有这些学习都包含了以下的一个组成部分:

  • 试错法:婴儿尝试不同的方法,许多次未能成功,最终才能做到。

  • 目标导向:所有的努力都指向一个特定的目标。人类婴儿的目标可以是爬行,幼鸟的目标可以是飞翔。

  • 与环境的互动:它们得到的唯一反馈来自环境。

这个 YouTube 视频是一个美丽的视频,展示了一个孩子学习爬行以及中间的各个阶段:www.youtube.com/watch?v=f3xWaOkXCSQ

人类婴儿学习爬行或幼鸟学习飞行都是自然界中强化学习的例子。

强化学习(在人工智能中)可以定义为一种从与环境互动中进行目标导向学习和决策的计算方法,在某些理想化条件下进行。让我们详细说明这一点,因为我们将使用各种计算机算法来执行学习——这是一种计算方法。在我们考虑的所有例子中,智能体(学习者)都有一个具体的目标,试图实现——这是一个目标导向的方法。强化学习中的智能体没有被给予任何明确的指示,它只从与环境的互动中学习。正如下面的图所示,与环境的互动是一个循环过程。智能体可以感知环境的状态,智能体可以对环境执行特定的、定义明确的动作;这会导致两件事:首先,环境状态发生变化,其次,生成奖励(在理想条件下)。这个循环继续进行:

智能体与环境之间的互动

与监督学习不同,智能体并没有被提供任何示例。智能体不知道正确的动作是什么。与无监督学习不同,智能体的目标不是在输入数据中寻找某种固有的结构(尽管学习过程可能会发现某些结构,但这不是目标);相反,它的目标是最大化奖励(从长远来看)。

RL 术语

在学习不同的算法之前,让我们先了解一下 RL 术语。为了举例说明,我们可以考虑两个例子:一个智能体在迷宫中找到路线,另一个智能体驾驶自动驾驶汽车SDC)。这两个例子在下图中进行了说明:

两个示例 RL 场景

在深入之前,让我们熟悉一些常见的 RL 术语:

  • 状态 s:状态可以看作是一个表示所有可能环境状态的集合(或表示符)。状态可以是连续的,也可以是离散的。例如,在一个智能体寻找迷宫路径的案例中,状态可以由一个 4 × 4 的数组表示,其中0代表空白块,1代表被智能体占据的块,X代表不能占据的状态;这里的状态是离散的。对于一个驾驶方向盘的智能体来说,状态就是 SDC 前方的视图。图像包含连续值的像素。

  • 动作 a(s):动作是智能体在某一特定状态下可以执行的所有可能操作的集合。可能的动作集合,a,取决于当前的状态,s。动作可能导致状态的变化,也可能不会。动作可以是离散的,也可以是连续的。在迷宫中,智能体可以执行五个离散动作:[上, 下, 左, 右, 不变]。另一方面,SDC 智能体可以在一个连续的角度范围内旋转方向盘。

  • 奖励 r(s, a, s'):当智能体选择一个动作时,环境返回的标量值。它定义了目标;如果动作将智能体带到目标附近,智能体会得到更高的奖励,否则会得到较低(甚至负)的奖励。我们如何定义奖励完全取决于我们——以迷宫为例,我们可以将奖励定义为智能体当前位置与目标之间的欧几里得距离。SDC 智能体的奖励可以是汽车在道路上(正奖励)或在道路外(负奖励)。

  • 策略 π(s):它定义了每个状态与在该状态下采取的动作之间的映射。策略可以是确定性的——即对于每个状态都有一个明确的策略。例如,对于迷宫代理,策略可以是如果上方的格子是空的,则向上移动。策略也可以是随机的——即某个动作是由某个概率决定的。它可以通过简单的查找表来实现,或者可以是一个依赖于当前状态的函数。策略是强化学习代理的核心。在本章中,我们将学习帮助代理学习策略的不同算法。

  • 价值函数 V(s):它定义了一个状态在长期中的好坏。它可以被看作是代理从状态s开始,未来能够积累的奖励的总量。你可以将其视为长期的好处,而不是奖励的即时好处。你认为哪个更重要,最大化奖励还是最大化价值函数?是的,你猜对了:就像下棋时,我们有时会牺牲一个兵去换取几步后赢得比赛一样,代理应该尝试最大化价值函数。价值函数通常有两种考虑方式:

    • 价值函数 V^π(s):它是跟随策略π时状态的好坏。数学上,在状态s时,它是从跟随策略π中预期的累积奖励:

  • 价值-状态函数(或 Q 函数) Q^π(s, a):它是状态 s 下采取动作 a,然后跟随策略 π 时的好坏。数学上,我们可以说,对于一个状态-动作对 (s, a),它是从状态 s 中采取动作 a,然后跟随策略 π 所得到的预期累积奖励:

γ是折扣因子,它的值决定了我们在比较即时奖励和后续奖励时对即时奖励赋予多大的重要性。折扣因子的高值决定了代理能看到多远的未来。许多成功的强化学习算法中,γ的理想选择值通常为0.97

  • 环境模型:它是一个可选元素,模拟环境的行为,并包含环境的物理规律;换句话说,它定义了环境将如何表现。环境模型由转移到下一个状态的概率定义。

强化学习问题在数学上被公式化为马尔可夫决策过程MDP),并且遵循马尔可夫性质——即当前状态完全表征世界的状态

深度强化学习

强化学习算法可以根据它们所迭代/逼近的内容分为两类:

  • 基于价值的方法:在这些方法中,算法选择最大化价值函数的动作。这里,智能体学习预测一个给定状态或动作的好坏。因此,目标是找到最优的价值。一个基于价值的方法的例子是 Q 学习。例如,考虑我们的强化学习智能体在迷宫中的情况:假设每个状态的值是从该方格到达目标所需步数的负数,那么,在每个时间步,智能体将选择一个动作,带它到达具有最优值的状态,如下图所示。所以,从值为**-6的状态开始,它将移动到-5**、-4-3-2-1,最终到达值为0的目标:

具有每个方格值的迷宫世界

  • 基于策略的方法:在这些方法中,算法预测最大化价值函数的最佳策略。目标是找到最优策略。一个基于策略的方法的例子是策略梯度。在这里,我们近似策略函数,从而将每个状态映射到最佳的对应动作。

我们可以使用神经网络作为函数逼近器来获取策略或价值的近似值。当我们使用深度神经网络作为策略逼近器或价值逼近器时,我们称之为深度强化学习DRL)。在最近的研究中,DRL 取得了非常成功的结果,因此,在本章中,我们将重点讨论 DRL。

一些成功的应用

在过去几年中,强化学习在各种任务中取得了成功,尤其是在游戏和机器人领域。在学习其算法之前,让我们先了解一些强化学习的成功案例:

  • AlphaGo Zero:由谷歌的 DeepMind 团队开发的 AlphaGo Zero,通过完全没有人类知识的方式掌握围棋,从一个完全空白的起点开始(tabula rasa)。AlphaGo Zero 使用一个神经网络来同时近似走棋概率和价值。这个神经网络以原始的棋盘表示为输入,使用由神经网络引导的蒙特卡洛树搜索来选择走棋。强化学习算法将前瞻性搜索整合到训练循环中。它使用 40 个区块的残差 CNN 训练了 40 天,在此过程中,它进行了大约 2900 万场比赛(一个非常庞大的数字!)。该神经网络在谷歌云上使用 TensorFlow 进行了优化,配备了 64 个 GPU 工作节点和 19 个 CPU 参数服务器。你可以在这里访问论文:www.nature.com/articles/nature24270

  • AI 控制的滑翔机:微软开发了一种控制系统,可以在多种不同的自动驾驶硬件平台上运行,如 Pixhawk 和 Raspberry Pi 3。它可以通过自动寻找并搭乘自然发生的上升气流,使滑翔机在空中飞行而不使用发动机。该控制器帮助滑翔机自行操作;它检测并利用上升气流在没有发动机或人员帮助的情况下飞行。他们将其实现为部分可观察的 MDP(马尔可夫决策过程)。他们采用贝叶斯强化学习,并使用蒙特卡罗树搜索来寻找最佳动作。他们将整个系统分为两个级别的规划者——一个基于经验做出决策的高级规划者和一个利用贝叶斯强化学习实时检测并捕捉上升气流的低级规划者。您可以在微软新闻上查看滑翔机的操作:news.microsoft.com/features/science-mimics-nature-microsoft-researchers-test-ai-controlled-soaring-machine/

  • 运动行为:在论文*《丰富环境中运动行为的出现》*(arxiv.org/pdf/1707.02286.pdf)中,DeepMind 的研究人员为代理提供了丰富且多样的环境。这些环境提供了不同难度等级的挑战。代理面临的困难按顺序递增,这促使代理在没有执行任何奖励工程的情况下学会了复杂的运动技能。

模拟环境

由于强化学习(RL)涉及试错过程,因此在模拟环境中首先训练我们的 RL 代理是很有意义的。虽然有大量的应用可以用于创建环境,但一些流行的应用包括以下内容:

  • OpenAI gym:它包含了一系列我们可以用来训练 RL 代理的环境。在本章中,我们将使用 OpenAI gym 接口。

  • Unity ML-Agents SDK:它允许开发者通过易于使用的 Python API,将使用 Unity 编辑器创建的游戏和模拟转换为可以通过深度强化学习(DRL)、进化策略或其他机器学习方法训练智能代理的环境。它与 TensorFlow 兼容,并提供训练二维/三维以及虚拟现实(VR)/增强现实(AR)游戏中智能代理的能力。您可以在此了解更多:github.com/Unity-Technologies/ml-agents

  • Gazebo:在 Gazebo 中,我们可以构建具有基于物理模拟的三维世界。Gazebo 与机器人操作系统ROS)和 OpenAI Gym 接口一起使用,称为 gym-gazebo,可以用来训练 RL 代理。有关更多信息,您可以参考白皮书:erlerobotics.com/whitepaper/robot_gym.pdf

  • Blender学习环境:这是 Blender 游戏引擎的 Python 接口,它也可以在 OpenAI gym 上使用。它以 Blender 为基础。Blender 是一个免费的三维建模软件,集成了游戏引擎,提供了一套易于使用、功能强大的工具,用于创建游戏。它提供了一个 Blender 游戏引擎的接口,而游戏本身是在 Blender 中设计的。我们可以创建自定义虚拟环境,以便在特定问题上训练强化学习(RL)智能体(github.com/LouisFoucard/gym-blender)。

OpenAI gym

OpenAI gym 是一个开源工具包,用于开发和比较 RL 算法。它包含多种模拟环境,可用于训练智能体并开发新的 RL 算法。首先,你需要安装gym。对于 Python 3.5 及以上版本,可以使用pip安装gym

pip install gym

OpenAI gym 支持多种环境,从简单的基于文本的到三维环境。最新版本中支持的环境可以分为以下几类:

  • 算法:它包含涉及执行计算任务的环境,如加法运算。尽管我们可以轻松地在计算机上进行计算,但作为一个 RL 问题,这些问题的有趣之处在于智能体仅通过示例学习这些任务。

  • Atari:此环境提供各种经典的 Atari/街机游戏。

  • Box2D:它包含二维机器人任务,如赛车代理或双足机器人行走。

  • 经典控制:包含经典的控制理论问题,如平衡推车摆杆。

  • MuJoCo:这是一个专有的物理引擎(你可以获得一个为期一个月的免费试用)。它支持多种机器人模拟任务。该环境包含物理引擎,因此用于训练机器人任务。

  • 机器人学:这个环境也使用 MuJoCo 的物理引擎。它模拟基于目标的任务,如取物和影子手机器人任务。

  • 玩具文本:这是一个基于文本的简单环境,非常适合初学者。

要获取这些组下所有环境的完整列表,你可以访问:gym.openai.com/envs/#atari。OpenAI 接口的最棒之处在于,所有环境都可以通过相同的最小接口进行访问。要获取你安装中所有可用环境的列表,你可以使用以下代码:

from gym import envs
print(envs.registry.all())

这将提供所有已安装环境的列表及其环境 ID,ID 为字符串类型。你还可以在gym注册表中添加你自己的环境。要创建一个环境,我们使用make命令,并将环境名称作为字符串传递。例如,要创建一个使用 Pong 环境的游戏,我们需要的字符串是Pong-v0make命令创建环境,而reset命令用于激活该环境。reset命令将环境恢复到初始状态。该状态以数组形式表示:

import gym
env = gym.make('Pong-v0')
obs = env.reset()
env.render()

Pong-v0的状态空间由一个 210×160×3 的数组表示,这实际上代表了 Pong 游戏的原始像素值。另一方面,如果你创建一个Go9×9-v0环境,状态则由一个 3×9×9 的数组定义。我们可以使用render命令来可视化环境。下图展示了Pong-v0Go9x9-v0环境在初始状态下的渲染环境:

Pong-v0Go9x9-v0的渲染环境

render命令会弹出一个窗口。如果你想在内联显示环境,可以使用 Matplotlib 的内联模式,并将render命令更改为plt.imshow(env.render(mode='rgb_array'))。这将在 Jupyter Notebook 中内联显示环境。

环境包含action_space变量,它决定了环境中可能的动作。我们可以使用sample()函数随机选择一个动作。选择的动作可以通过step函数影响环境。step函数在环境上执行所选动作;它返回改变后的状态、奖励、一个布尔值表示游戏是否结束,以及一些有助于调试但在与强化学习智能体互动时不会用到的环境信息。以下代码展示了一个 Pong 游戏,其中智能体执行一个随机动作。我们在每个时间步将状态存储在一个数组frames中,以便稍后查看游戏:

frames = [] # array to store state space at each step
for _ in range(300):
    frames.append(env.render(mode='rgb_array'))
    obs,reward,done, _ = env.render(env.action_space.sample())
    if done:
        break

这些帧可以借助 Matplotlib 和 IPython 中的动画函数,在 Jupyter Notebook 中以持续播放的 GIF 风格图像显示:

import matplotlib.animation as animation
from JSAnimation.Ipython_display import display_animation
from IPython.display import display

patch = plt.imshow(frames[0])
plt.axis('off')

def animate(i)
    patch.set_data(frames[i])

anim = animation.FuncAnimation(plt.gcf(), animate, \
        frames=len(frames), interval=100)

display(display_animation(anim, default_mode='loop')

通常情况下,训练一个智能体需要大量的步骤,因此在每个步骤保存状态空间是不可行的。我们可以选择在前述算法中的每 500 步(或任何其他你希望的步数)后进行存储。相反,我们可以使用 OpenAI gym 的包装器将游戏保存为视频。为此,我们首先需要导入包装器,然后创建环境,最后使用 Monitor。默认情况下,它将存储 1、8、27、64 等的每个视频,以及每 1,000^(次)的回合(回合数为完美的立方数);每次训练默认保存在一个文件夹中。实现此功能的代码如下:

import gym
from gym import wrappers
env = gym.make('Pong-v0')
env = wrappers.Monitor(env, '/save-mov', force=True)
# Follow it with the code above where env is rendered and agent
# selects a random action

如果你想在下次训练中使用相同的文件夹,可以在Monitor方法调用中选择force=True选项。最后,我们应该使用close函数关闭环境:

env.close()

前述代码可以在OpenAI_practice.ipynb Jupyter Notebook 中找到,位于 GitHub 的第六章,物联网强化学习文件夹内。

Q-learning

在他的博士论文《从延迟奖励中学习》中,Watkins 在 1989 年提出了 Q 学习的概念。Q 学习的目标是学习一个最优的动作选择策略。给定一个特定的状态,s,并采取一个特定的动作,a,Q 学习试图学习状态s的值。在最简单的版本中,Q 学习可以通过查找表来实现。我们维护一个表,记录环境中每个状态(行)和动作(列)的值。算法试图学习这个值——即在给定状态下采取特定动作的好坏。

我们首先将 Q 表中的所有条目初始化为0;这确保了所有状态都有相同的(因此是平等的)价值。后来,我们观察采取特定动作所获得的奖励,并根据奖励更新 Q 表。Q 值的更新是动态进行的,通过贝尔曼方程来帮助实现,方程如下:

这里,α是学习率。以下是基本的 Q 学习算法:

简单的 Q 学习算法

如果你有兴趣,你可以在这里阅读 Watkins 的 240 页博士论文:www.cs.rhul.ac.uk/~chrisw/new_thesis.pdf

学习结束时,我们将拥有一个好的 Q 表,并且有最优策略。这里有一个重要问题:我们如何选择第二步的动作?有两种选择;首先,我们随机选择一个动作。这使得我们的智能体能够以相等的概率探索所有可能的动作,但同时忽略了它已经学到的信息。第二种方式是我们选择具有最大值的动作;最初,所有动作的 Q 值相同,但随着智能体的学习,一些动作将获得高值,另一些则获得低值。在这种情况下,智能体正在利用它已经学到的知识。那么,哪个更好呢:探索还是利用?这就是所谓的探索-利用权衡。解决这个问题的一种自然方式是依赖于智能体已经学到的知识,但有时也需要进行探索。这是通过使用ε-贪婪算法实现的。基本的想法是,智能体以概率ε随机选择动作,而以概率(1-ε)利用在之前的回合中学到的信息。该算法大多数时候(1-ε)选择最好的选项(贪婪),但有时(ε)会做出随机选择。现在让我们尝试在一个简单的问题中实现我们学到的东西。

使用 Q 表的出租车下车

简单的 Q 学习算法涉及维护一个大小为 m×n 的表,其中 m 为状态总数,n 为可能的动作总数。因此,我们从玩具文本组中选择了一个问题,因为它们的 state 空间和 action 空间都很小。为了便于说明,我们选择了 Taxi-v2 环境。我们的智能体目标是选择一个位置的乘客并将其送到另一个位置。智能体成功送客后会获得 +20 分,每走一步会失去 1 分。如果进行非法的接送操作,还会扣除 10 分。状态空间中有墙壁(用 | 表示)和四个位置标记,分别是 RGYB。出租车用框表示:接送位置可以是这四个标记中的任何一个。接客点用蓝色表示,送客点用紫色表示。Taxi-v2 环境的状态空间大小为 500,动作空间大小为 6,因此 Q 表的大小为 500×6=3000 个条目:

出租车接送环境

在出租车接送环境中,出租车用黄色框表示。位置标记 R 是接客点,G 是送客点:

  1. 我们首先导入必要的模块并创建我们的环境。由于这里只需要创建查找表,因此使用 TensorFlow 并不是必需的。如前所述,Taxi-v2 环境有 500 种可能的状态和 6 种可能的动作:
import gym
import numpy as np
env = gym.make('Taxi-v2')
obs = env.reset()
env.render()
  1. 我们将大小为 (300×6) 的 Q 表初始化为全零,并定义超参数:γ 为折扣因子,α 为学习率。我们还设定了最大轮次(一个轮次意味着从重置到完成=True 的一次完整运行)和智能体将在每一轮中学习的最大步数:
m = env.observation_space.n # size of the state space
n = env.action_space.n # size of action space
print("The Q-table will have {} rows and {} columns, resulting in \
     total {} entries".format(m,n,m*n))

# Intialize the Q-table and hyperparameters
Q = np.zeros([m,n])
gamma = 0.97
max_episode = 1000
max_steps = 100
alpha = 0.7
epsilon = 0.3
  1. 现在,对于每一轮,我们选择具有最高值的动作,执行该动作,并根据收到的奖励和未来的状态,使用贝尔曼方程更新 Q 表:
for i in range(max_episode):
    # Start with new environment
    s = env.reset()
    done = False
    for _ in range(max_steps):
        # Choose an action based on epsilon greedy algorithm
        p = np.random.rand()
        if p > epsilon or (not np.any(Q[s,:])):
            a = env.action_space.sample() #explore
        else:
            a = np.argmax(Q[s,:]) # exploit
        s_new, r, done, _ = env.step(a) 
        # Update Q-table
        Q[s,a] = (1-alpha)*Q[s,a] + alpha*(r + gamma*np.max(Q[s_new,:]))
        #print(Q[s,a],r)
        s = s_new
        if done:
            break
  1. 现在我们来看一下学习型智能体是如何工作的:
s = env.reset()
done = False
env.render()
# Test the learned Agent
for i in range(max_steps):
 a = np.argmax(Q[s,:])
 s, _, done, _ = env.step(a)
 env.render()
 if done:
 break 

下图展示了在一个特定示例中智能体的行为。空车用黄色框表示,载有乘客的车用绿色框表示。从图中可以看出,在给定的情况下,智能体在 11 步内完成接送乘客,目标位置标记为 (B),目的地标记为 (R):

使用学习到的 Q 表,智能体接送乘客

酷吧?完整的代码可以在 GitHub 上找到,文件名为 Taxi_drop-off.ipynb

Q 网络

简单的 Q 学习算法涉及维持一个大小为 m×n 的表格,其中 m 是状态的总数,n 是可能动作的总数。这意味着我们不能将其用于大规模的状态空间和动作空间。一个替代方法是用神经网络替换表格,作为一个函数逼近器,逼近每个可能动作的 Q 函数。在这种情况下,神经网络的权重存储着 Q 表格的信息(它们将给定状态与相应的动作及其 Q 值匹配)。当我们用深度神经网络来逼近 Q 函数时,我们称其为 深度 Q 网络 (DQN)。

神经网络以状态为输入,计算所有可能动作的 Q 值。

使用 Q-网络进行出租车下车

如果我们考虑前面的 出租车下车 示例,我们的神经网络将由 500 个输入神经元组成(状态由 1×500 的 one-hot 向量表示),以及 6 个输出神经元,每个神经元代表给定状态下某一特定动作的 Q 值。神经网络将在此处逼近每个动作的 Q 值。因此,网络应该经过训练,使得其逼近的 Q 值与目标 Q 值相同。目标 Q 值由贝尔曼方程给出,如下所示:

我们训练神经网络,使得目标 Q 和预测 Q 之间的平方误差最小化——也就是说,神经网络最小化以下损失函数:

目标是学习未知的 Q[target] 函数。通过反向传播更新 QNetwork 的权重,以使损失最小化。我们使神经网络 QNetwork 来逼近 Q 值。它是一个非常简单的单层神经网络,具有提供动作及其 Q 值(get_action)、训练网络(learnQ)以及获取预测 Q 值(Qnew)的方法:

class QNetwork:
    def __init__(self,m,n,alpha):
        self.s = tf.placeholder(shape=[1,m], dtype=tf.float32)
        W = tf.Variable(tf.random_normal([m,n], stddev=2))
        bias = tf.Variable(tf.random_normal([1, n]))
        self.Q = tf.matmul(self.s,W) + bias
        self.a = tf.argmax(self.Q,1)

        self.Q_hat = tf.placeholder(shape=[1,n],dtype=tf.float32)
        loss = tf.reduce_sum(tf.square(self.Q_hat-self.Q))
        optimizer = tf.train.GradientDescentOptimizer(learning_rate=alpha)
        self.train = optimizer.minimize(loss)
        init = tf.global_variables_initializer()

        self.sess = tf.Session()
        self.sess.run(init)

    def get_action(self,s):
        return self.sess.run([self.a,self.Q], feed_dict={self.s:s})

    def learnQ(self,s,Q_hat):
        self.sess.run(self.train, feed_dict= {self.s:s, self.Q_hat:Q_hat})

    def Qnew(self,s):
        return self.sess.run(self.Q, feed_dict={self.s:s})

我们现在将这个神经网络集成到之前训练 RL 代理解决 出租车下车 问题的代码中。我们需要进行一些更改;首先,OpenAI 步骤和重置函数返回的状态只是状态的数字标识符,所以我们需要将其转换为一个 one-hot 向量。此外,我们不再使用 Q 表格更新,而是从 QNetwork 获取新的 Q 预测值,找到目标 Q 值,并训练网络以最小化损失。代码如下:

QNN = QNetwork(m,n, alpha)
rewards = []
for i in range(max_episode):
 # Start with new environment
 s = env.reset()
 S = np.identity(m)[s:s+1]
 done = False
 counter = 0
 rtot = 0
 for _ in range(max_steps):
 # Choose an action using epsilon greedy policy
 a, Q_hat = QNN.get_action(S) 
 p = np.random.rand()
 if p > epsilon:
 a[0] = env.action_space.sample() #explore

 s_new, r, done, _ = env.step(a[0])
 rtot += r
 # Update Q-table
 S_new = np.identity(m)[s_new:s_new+1]
 Q_new = QNN.Qnew(S_new) 
 maxQ = np.max(Q_new)
 Q_hat[0,a[0]] = r + gamma*maxQ
 QNN.learnQ(S,Q_hat)
 S = S_new
 #print(Q_hat[0,a[0]],r)
 if done:
 break
 rewards.append(rtot)
print ("Total reward per episode is: " + str(sum(rewards)/max_episode))

这本应该做得很好,但正如你所看到的,即使训练了1,000个回合,网络的奖励依然很低,如果你查看网络的表现,似乎它只是随便走动。是的,我们的网络什么都没学到;表现比 Q 表还差。这也可以从训练过程中的奖励图表验证——理想情况下,随着代理的学习,奖励应该增加,但这里并没有发生这种情况;奖励像是围绕平均值的随机漫步(该程序的完整代码可以在Taxi_drop-off_NN.ipynb文件中找到,文件在 GitHub 上可用):

代理在学习过程中每个回合获得的总奖励

发生了什么?为什么神经网络没有学到东西,我们能改进它吗?

假设出租车需要往西走去接客,而代理随机选择了西行;代理获得了奖励,网络会学习到,在当前状态下(通过一个独热编码表示),西行是有利的。接下来,考虑另一个与此相似的状态(相关状态空间):代理再次做出西行动作,但这次却获得了负奖励,所以现在代理会忘记之前学到的东西。因此,相似的状态-动作对但目标不同会混淆学习过程。这被称为灾难性遗忘。问题的产生是因为连续状态高度相关,因此,如果代理按顺序学习(如本例),这个高度相关的输入状态空间会妨碍代理的学习。

我们可以打破输入数据与网络之间的关联吗?可以:我们可以构建一个回放缓冲区,在这里我们首先存储每个状态、其对应的动作、连续奖励和结果状态(状态、动作、奖励、新状态)。在这种情况下,动作是完全随机选择的,从而确保了动作和结果状态的多样性。回放缓冲区最终将由这些元组(SARS')组成一个大列表。接下来,我们将这些元组随机地输入到网络中(而不是按顺序输入);这种随机性将打破连续输入状态之间的关联。这被称为经验回放。它不仅解决了输入状态空间中的关联问题,还使我们能够多次从相同的元组中学习,回顾稀有事件,并且通常能更好地利用经验。从某种意义上说,通过使用回放缓冲区,我们已经减少了监督学习中的问题(回放缓冲区作为输入输出数据集),其中输入的随机采样确保了网络能够进行泛化。

我们的方法的另一个问题是,我们立即更新目标 Q。这也会导致有害的相关性。请记住,在 Q 学习中,我们试图最小化Q[target]与当前预测的Q之间的差异。这个差异被称为时序差分TD)误差(因此 Q 学习是一种TD 学习)。目前,我们立即更新我们的Q[target],因此目标与我们正在更改的参数之间(通过Q[pred]进行的权重)存在相关性。这几乎就像在追逐一个移动的目标,因此不会给出一个通用的方向。我们可以通过使用固定的 Q 目标来解决这个问题——即使用两个网络,一个用于预测Q,另一个用于目标Q。这两个网络在架构上完全相同,预测 Q 网络在每一步中都会改变权重,而目标 Q 网络的权重会在固定的学习步骤后更新。这提供了一个更加稳定的学习环境。

最后,我们做一个小小的改变:目前,我们的 epsilon 在整个学习过程中都有一个固定值。但在现实生活中并非如此。最初,当我们一无所知时,我们会进行大量探索,但随着我们变得熟悉,我们倾向于采取已学到的路径。在我们的 epsilon 贪婪算法中也可以做到这一点,通过随着网络在每个回合中学习,逐步改变 epsilon 的值,使得 epsilon 随时间减少。

配备了这些技巧后,现在我们来构建一个 DQN 来玩 Atari 游戏。

DQN 来玩 Atari 游戏

我们将在这里学习的 DQN 基于 DeepMind 的论文(web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf)。DQN 的核心是一个深度卷积神经网络,它以游戏环境的原始像素为输入(就像任何人类玩家看到的一样),每次捕捉一屏幕,并将每个可能动作的值作为输出。值最大的动作就是选择的动作:

  1. 第一步是获取我们所需的所有模块:

import gym
import sys
import random
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from datetime import datetime
from scipy.misc import imresize
  1. 我们选择了 OpenAI Atari 游戏列表中的 Breakout 游戏——你可以尝试其他 Atari 游戏的代码;你可能唯一需要更改的地方是预处理步骤。Breakout 的输入空间——即我们的输入空间——由 210×160 个像素组成,每个像素有 128 种可能的颜色。这是一个非常庞大的输入空间。为了减少复杂性,我们将选择图像中的一个感兴趣区域,将其转换为灰度图像,并将其调整为大小为80×80的图像。我们通过preprocess函数来实现这一点:
def preprocess(img):
    img_temp = img[31:195] # Choose the important area of the image
    img_temp = img_temp.mean(axis=2) # Convert to Grayscale#
    # Downsample image using nearest neighbour interpolation
    img_temp = imresize(img_temp, size=(IM_SIZE, IM_SIZE), interp='nearest')
    return img_temp

以下截图展示了预处理前后的环境:

原始环境的大小为 210×160(彩色图像),处理后的环境大小为 80×80(灰度图像)。

  1. 从前面的图示中可以看到,无法判断球是下落还是上升。为了解决这个问题,我们将四个连续的状态(由于四个独特的动作)结合为一个输入。我们定义了一个函数update_state,它将当前环境的观察结果追加到前一个状态数组中:
def update_state(state, obs):
    obs_small = preprocess(obs)
    return np.append(state[1:], np.expand_dims(obs_small, 0), axis=0)

该函数将处理过的新状态追加到切片的状态中,确保网络的最终输入由四个帧组成。在以下截图中,你可以看到这四个连续的帧。这是我们 DQN 的输入:

DQN 的输入是四个连续的游戏状态(帧)

  1. 我们创建了一个在class DQN中定义的 DQN;它由三层卷积层组成,最后一层卷积层的输出被展平,然后是两个全连接层。该网络和之前的情况一样,试图最小化*Q[target]Q[predicted]*之间的差异。在代码中,我们使用的是 RMSProp 优化器,但你也可以尝试其他优化器:
def __init__(self, K, scope, save_path= 'models/atari.ckpt'):
    self.K = K
    self.scope = scope
    self.save_path = save_path
    with tf.variable_scope(scope):
        # inputs and targets
        self.X = tf.placeholder(tf.float32, shape=(None, 4, IM_SIZE, IM_SIZE), name='X')
        # tensorflow convolution needs the order to be:
        # (num_samples, height, width, "color")
        # so we need to tranpose later
        self.Q_target = tf.placeholder(tf.float32, shape=(None,), name='G')
        self.actions = tf.placeholder(tf.int32, shape=(None,), name='actions')
        # calculate output and cost
        # convolutional layers
        Z = self.X / 255.0
        Z = tf.transpose(Z, [0, 2, 3, 1])
        cnn1 = tf.contrib.layers.conv2d(Z, 32, 8, 4, activation_fn=tf.nn.relu)
        cnn2 = tf.contrib.layers.conv2d(cnn1, 64, 4, 2, activation_fn=tf.nn.relu)
        cnn3 = tf.contrib.layers.conv2d(cnn2, 64, 3, 1, activation_fn=tf.nn.relu)
        # fully connected layers
        fc0 = tf.contrib.layers.flatten(cnn3)
        fc1 = tf.contrib.layers.fully_connected(fc0, 512)
        # final output layer
        self.predict_op = tf.contrib.layers.fully_connected(fc1, K)
        Qpredicted = tf.reduce_sum(self.predict_op * tf.one_hot(self.actions, K),
     reduction_indices=[1])
        self.cost = tf.reduce_mean(tf.square(self.Q_target - Qpredicted))
        self.train_op = tf.train.RMSPropOptimizer(0.00025, 0.99, 0.0, 1e-6).minimize(self.cost)

我们在接下来的步骤中讨论了该类所需的必要方法:

  1. 我们添加了一个方法来返回预测的 Q 值:
def predict(self, states):
    return self.session.run(self.predict_op, feed_dict={self.X: states})
  1. 我们需要一个方法来确定具有最大值的动作。在这个方法中,我们还实现了 epsilon-贪婪策略,且 epsilon 的值在主代码中会发生变化:
def sample_action(self, x, eps):
    """Implements epsilon greedy algorithm"""
    if np.random.random() < eps:
        return np.random.choice(self.K)
    else:
        return np.argmax(self.predict([x])[0])
  1. 我们需要一个方法来更新网络的权重,以最小化损失。该函数可以定义如下:
 def update(self, states, actions, targets):
     c, _ = self.session.run(
         [self.cost, self.train_op],
         feed_dict={
         self.X: states,
         self.Q_target: targets,
         self.actions: actions
         })
     return c
  1. 将模型权重复制到固定的 Q 网络中:
def copy_from(self, other):
    mine = [t for t in tf.trainable_variables() if t.name.startswith(self.scope)]
    mine = sorted(mine, key=lambda v: v.name)
    theirs = [t for t in tf.trainable_variables() if t.name.startswith(other.scope)]
    theirs = sorted(theirs, key=lambda v: v.name)
    ops = []
    for p, q in zip(mine, theirs):
        actual = self.session.run(q)
        op = p.assign(actual)
        ops.append(op)
    self.session.run(ops)
  1. 除了这些方法,我们还需要一些辅助函数来保存学习到的网络、加载保存的网络,并设置 TensorFlow 会话:
def load(self):
    self.saver = tf.train.Saver(tf.global_variables())
    load_was_success = True
    try:
        save_dir = '/'.join(self.save_path.split('/')[:-1])
        ckpt = tf.train.get_checkpoint_state(save_dir)
        load_path = ckpt.model_checkpoint_path
        self.saver.restore(self.session, load_path)
    except:
        print("no saved model to load. starting new session")
        load_was_success = False
    else:
        print("loaded model: {}".format(load_path))
        saver = tf.train.Saver(tf.global_variables())
        episode_number = int(load_path.split('-')[-1])

def save(self, n):
    self.saver.save(self.session, self.save_path, global_step=n)
    print("SAVED MODEL #{}".format(n))

def set_session(self, session):
    self.session = session
    self.session.run(tf.global_variables_initializer())
    self.saver = tf.train.Saver()
  1. 为了实现 DQN 算法,我们使用一个learn函数;它从经验重放缓冲区中随机选择一个样本,并使用目标 Q 网络中的目标 Q 来更新 Q 网络:
def learn(model, target_model, experience_replay_buffer, gamma, batch_size):
    # Sample experiences
    samples = random.sample(experience_replay_buffer, batch_size)
    states, actions, rewards, next_states, dones = map(np.array, zip(*samples))
    # Calculate targets
     next_Qs = target_model.predict(next_states)
     next_Q = np.amax(next_Qs, axis=1)
     targets = rewards +     np.invert(dones).astype(np.float32) * gamma * next_Q
    # Update model
     loss = model.update(states, actions, targets)
     return loss
  1. 好了,所有的元素都准备好了,现在让我们决定 DQN 的超参数并创建我们的环境:
# Some Global parameters
MAX_EXPERIENCES = 500000
MIN_EXPERIENCES = 50000
TARGET_UPDATE_PERIOD = 10000
IM_SIZE = 80
K = 4 # env.action_space.n

# hyperparameters etc
gamma = 0.97
batch_sz = 64
num_episodes = 2700
total_t = 0
experience_replay_buffer = []
episode_rewards = np.zeros(num_episodes)
last_100_avgs = []
# epsilon for Epsilon Greedy Algorithm
epsilon = 1.0
epsilon_min = 0.1
epsilon_change = (epsilon - epsilon_min) / 700000

# Create Atari Environment
env = gym.envs.make("Breakout-v0")

# Create original and target Networks
model = DQN(K=K, scope="model")
target_model = DQN(K=K, scope="target_model")
  1. 最后,以下是调用并填充经验重放缓冲区、逐步执行游戏并在每一步训练模型网络以及每四步训练target_model的代码:
with tf.Session() as sess:
    model.set_session(sess)
    target_model.set_session(sess)
    sess.run(tf.global_variables_initializer())
    model.load()
    print("Filling experience replay buffer...")
    obs = env.reset()
    obs_small = preprocess(obs)
    state = np.stack([obs_small] * 4, axis=0)
    # Fill experience replay buffer
    for i in range(MIN_EXPERIENCES):
        action = np.random.randint(0,K)
        obs, reward, done, _ = env.step(action)
        next_state = update_state(state, obs)
        experience_replay_buffer.append((state, action, reward, next_state, done))
        if done:
            obs = env.reset()
            obs_small = preprocess(obs)
            state = np.stack([obs_small] * 4, axis=0)
        else:
            state = next_state
        # Play a number of episodes and learn
        for i in range(num_episodes):
            t0 = datetime.now()
            # Reset the environment
            obs = env.reset()
            obs_small = preprocess(obs)
            state = np.stack([obs_small] * 4, axis=0)
            assert (state.shape == (4, 80, 80))
            loss = None
            total_time_training = 0
            num_steps_in_episode = 0
            episode_reward = 0
            done = False
            while not done:
                # Update target network
                if total_t % TARGET_UPDATE_PERIOD == 0:
                    target_model.copy_from(model)
                    print("Copied model parameters to target network. total_t = %s, period = %s" % (total_t, TARGET_UPDATE_PERIOD))
                # Take action
                action = model.sample_action(state, epsilon)
                obs, reward, done, _ = env.step(action)
                obs_small = preprocess(obs)
                next_state = np.append(state[1:], np.expand_dims(obs_small, 0), axis=0)
                episode_reward += reward
                # Remove oldest experience if replay buffer is full
                if len(experience_replay_buffer) == MAX_EXPERIENCES:
                    experience_replay_buffer.pop(0)
                    # Save the recent experience
                    experience_replay_buffer.append((state, action, reward, next_state, done))

                # Train the model and keep measure of time
                t0_2 = datetime.now()
                loss = learn(model, target_model, experience_replay_buffer, gamma, batch_sz)
                dt = datetime.now() - t0_2
                total_time_training += dt.total_seconds()
                num_steps_in_episode += 1
                state = next_state
                total_t += 1
                epsilon = max(epsilon - epsilon_change, epsilon_min)
                duration = datetime.now() - t0
                episode_rewards[i] = episode_reward
                time_per_step = total_time_training / num_steps_in_episode
                last_100_avg = episode_rewards[max(0, i - 100):i + 1].mean()
                last_100_avgs.append(last_100_avg)
                print("Episode:", i,"Duration:", duration, "Num steps:", num_steps_in_episode, "Reward:", episode_reward, "Training time per step:", "%.3f" % time_per_step, "Avg Reward (Last 100):", "%.3f" % last_100_avg,"Epsilon:", "%.3f" % epsilon)
                if i % 50 == 0:
                    model.save(i)
                sys.stdout.flush()

#Plots
plt.plot(last_100_avgs)
plt.xlabel('episodes')
plt.ylabel('Average Rewards')
plt.show()
env.close()

我们可以看到,现在奖励随着回合数增加,最终的平均奖励是20,尽管它可能更高,但我们仅仅学习了几千个回合,甚至我们的重放缓冲区的大小在(50,000 到 5,000,000)之间:

智能体学习过程中的平均奖励

  1. 让我们看看我们的智能体在学习了大约 2,700 个回合后是如何表现的:
env = gym.envs.make("Breakout-v0")
frames = []
with tf.Session() as sess:
    model.set_session(sess)
    target_model.set_session(sess)
    sess.run(tf.global_variables_initializer())
    model.load()
    obs = env.reset()
    obs_small = preprocess(obs)
    state = np.stack([obs_small] * 4, axis=0)
    done = False
    while not done:
        action = model.sample_action(state, epsilon)
        obs, reward, done, _ = env.step(action)
        frames.append(env.render(mode='rgb_array'))
        next_state = update_state(state, obs)
        state = next_state

你可以在这里看到学习过的智能体的视频:www.youtube.com/watch?v=rPy-3NodgCE

很酷,对吧?没有告诉它任何信息,它仅仅通过 2,700 个回合学会了如何玩一款不错的游戏。

有一些方法可以帮助你更好地训练智能体:

  • 由于训练需要大量时间,除非你有强大的计算资源,否则最好保存模型并重新启动保存的模型。

  • 在代码中,我们使用了Breakout-v0和 OpenAI gym,在这种情况下,环境中会对连续(随机选择的1234)帧重复相同的步骤。你也可以选择BreakoutDeterministic-v4,这是 DeepMind 团队使用的版本;在这里,步骤会恰好在连续的四帧中重复。因此,智能体在每第四帧后看到并选择动作。

双重 DQN

现在,回想一下,我们使用最大值操作符来选择一个动作并评估这个动作。这可能导致一个可能并非最理想的动作被高估。我们可以通过将选择和评估解耦来解决这个问题。通过 Double DQN,我们有两个权重不同的 Q 网络;这两个网络都通过随机经验进行学习,但一个用于通过 epsilon-greedy 策略来确定动作,另一个用于确定其值(因此,计算目标 Q 值)。

为了更清楚地说明,让我们先看一下 DQN 的情况。选择具有最大 Q 值的动作;设W为 DQN 的权重,那么我们正在做的是:

上标W表示用于近似 Q 值的权重。在 Double DQN 中,方程变为如下:

请注意变化:现在,动作是通过使用权重为W的 Q 网络选择的,并且最大 Q 值是通过使用权重为W'的 Q 网络预测的。这减少了过估计,有助于我们更快且更可靠地训练智能体。你可以在这里访问Deep Reinforcement Learning with Double Q-Learning论文:www.aaai.org/ocs/index.php/AAAI/AAAI16/paper/download/12389/11847

对战 DQN

对战 DQN 将 Q 函数解耦为价值函数和优势函数。价值函数和之前讨论的相同;它表示状态的价值,与动作无关。另一方面,优势函数提供了动作a在状态s中的相对效用(优势/好处)的度量:

在 Dueling DQN 中,使用相同的卷积层来提取特征,但在后期阶段,它被分成两个独立的网络,一个提供价值,另一个提供优势。随后,两个阶段通过聚合层重新组合,以估计 Q 值。这确保了网络为价值函数和优势函数生成独立的估计值。这种价值和优势的解耦直觉是,对于许多状态,估计每个动作选择的价值并非必要。例如,在赛车中,如果前方没有车,那么选择“向左转”或“向右转”就没有必要,因此在给定状态下无需估计这些动作的价值。这使得网络可以学习哪些状态是有价值的,而不必为每个状态确定每个动作的效果。

在聚合层中,价值和优势被结合在一起,使得可以从给定的Q中唯一地恢复出VA。这是通过强制要求优势函数估计器在所选动作下的优势为零来实现的:

这里,θ是通用卷积特征提取器的参数,αβ是优势和值估计器网络的参数。Dueling DQN 也是由谷歌 DeepMind 团队提出的。你可以在arXiv上阅读完整的论文:arxiv.org/abs/1511.06581。作者发现,用平均操作替换先前的max操作可以提高网络的稳定性。在这种情况下,优势的变化速度仅与均值变化速度相同。因此,在他们的结果中,使用了以下给出的聚合层:

以下截图展示了 Dueling DQN 的基本架构:

Dueling DQN 的基本架构

策略梯度

在基于 Q 学习的方法中,我们在估计价值/Q 函数之后生成策略。在基于策略的方法中,如策略梯度方法,我们直接逼近策略。

按照之前的方法,我们在这里使用神经网络来逼近策略。在最简单的形式下,神经网络通过使用最陡梯度上升法调整权重来学习选择最大化奖励的动作策略,因此得名“策略梯度”。

在策略梯度中,策略由一个神经网络表示,其输入是状态的表示,输出是动作选择的概率。该网络的权重是我们需要学习的策略参数。自然会产生一个问题:我们应该如何更新这个网络的权重?由于我们的目标是最大化奖励,因此可以理解,我们的网络试图最大化每个回合的期望奖励:

在这里,我们使用了一个参数化的随机策略 π——也就是说,策略决定了在给定状态 s 的情况下选择动作 a 的概率,神经网络的参数是 θR 代表一个回合中所有奖励的总和。然后,使用梯度上升法更新网络参数:

这里,η 是学习率。通过策略梯度定理,我们得到如下公式:

因此,替代最大化期望回报,我们可以使用损失函数作为对数损失(将期望动作和预测动作作为标签和 logits),并将折扣奖励作为权重来训练网络。为了增加稳定性,研究发现添加基线有助于减少方差。最常见的基线形式是折扣奖励的总和,结果如下所示:

基线 b(s[t]) 如下所示:

这里,γ 是折扣因子。

为什么选择策略梯度?

首先,政策梯度方法像其他基于策略的方法一样,直接估计最优策略,无需存储额外的数据(经验回放缓冲区)。因此,它的实现非常简单。其次,我们可以训练它来学习真正的随机策略。最后,它非常适合连续动作空间。

使用策略梯度玩 Pong 游戏

让我们尝试使用策略梯度来玩 Pong 游戏。这里的实现灵感来自 Andrej Karpathy 的博客文章,文章地址:karpathy.github.io/2016/05/31/rl/。回想一下,在 Breakout 游戏中,我们使用了四个游戏帧堆叠在一起作为输入,从而使代理能够了解游戏的动态;而在这里,我们使用连续两帧游戏图像的差异作为输入。因此,我们的代理同时拥有当前状态和前一个状态的信息:

  1. 首先,和往常一样,我们需要导入必要的模块。我们导入了 TensorFlow、Numpy、Matplotlib 和 gym 作为环境:
import numpy as np
import gym
import matplotlib.pyplot as plt
import tensorflow as tf
from gym import wrappers
%matplotlib inline
  1. 我们构建了我们的神经网络,即 PolicyNetwork;它以游戏状态作为输入,并输出动作选择概率。在这里,我们构建了一个简单的两层感知器,且没有偏置。weights 使用 Xavier 初始化方法随机初始化。隐藏层使用 ReLU 激活函数,输出层使用 softmax 激活函数。我们使用稍后定义的 tf_discount_rewards 方法来计算基线。最后,我们使用 TensorFlow 的 tf.losses.log_loss 来计算预测的动作概率,并选择一个热编码的动作向量作为标签,同时将折扣奖励和方差校正后的奖励作为权重:
class PolicyNetwork(object):
    def __init__(self, N_SIZE, h=200, gamma=0.99, eta=1e-3, decay=0.99, save_path = 'models1/pong.ckpt' ):
        self.gamma = gamma
        self.save_path = save_path
        # Placeholders for passing state....
        self.tf_x = tf.placeholder(dtype=tf.float32, shape=[None, N_SIZE * N_SIZE], name="tf_x")
        self.tf_y = tf.placeholder(dtype=tf.float32, shape=[None, n_actions], name="tf_y")
        self.tf_epr = tf.placeholder(dtype=tf.float32, shape=[None, 1], name="tf_epr")

        # Weights
        xavier_l1 = tf.truncated_normal_initializer(mean=0, stddev=1\. / N_SIZE, dtype=tf.float32)
        self.W1 = tf.get_variable("W1", [N_SIZE * N_SIZE, h], initializer=xavier_l1)
        xavier_l2 = tf.truncated_normal_initializer(mean=0, stddev=1\. / np.sqrt(h), dtype=tf.float32)
        self.W2 = tf.get_variable("W2", [h, n_actions], initializer=xavier_l2)

        #Build Computation
        # tf reward processing (need tf_discounted_epr for policy gradient wizardry)
        tf_discounted_epr = self.tf_discount_rewards(self.tf_epr)
        tf_mean, tf_variance = tf.nn.moments(tf_discounted_epr, [0], shift=None, name="reward_moments")
        tf_discounted_epr -= tf_mean
        tf_discounted_epr /= tf.sqrt(tf_variance + 1e-6)

        #Define Optimizer, compute and apply gradients
        self.tf_aprob = self.tf_policy_forward(self.tf_x)
        loss = tf.losses.log_loss(labels = self.tf_y,
        predictions = self.tf_aprob,
        weights = tf_discounted_epr)
        optimizer = tf.train.AdamOptimizer()
        self.train_op = optimizer.minimize(loss)
  1. 该类有方法来计算动作概率(tf_policy_forwardpredict_UP),使用 tf_discount_rewards 计算基线,更新网络权重(update),并最终设置会话(set_session),然后加载和保存模型:
def set_session(self, session):
    self.session = session
    self.session.run(tf.global_variables_initializer())
    self.saver = tf.train.Saver()

def tf_discount_rewards(self, tf_r): # tf_r ~ [game_steps,1]
    discount_f = lambda a, v: a * self.gamma + v;
    tf_r_reverse = tf.scan(discount_f, tf.reverse(tf_r, [0]))
    tf_discounted_r = tf.reverse(tf_r_reverse, [0])
    return tf_discounted_r

def tf_policy_forward(self, x): #x ~ [1,D]
    h = tf.matmul(x, self.W1)
    h = tf.nn.relu(h)
    logp = tf.matmul(h, self.W2)
    p = tf.nn.softmax(logp)
    return p

def update(self, feed):
    return self.session.run(self.train_op, feed)

def load(self):
    self.saver = tf.train.Saver(tf.global_variables())
    load_was_success = True 
    try:
        save_dir = '/'.join(self.save_path.split('/')[:-1])
        ckpt = tf.train.get_checkpoint_state(save_dir)
        load_path = ckpt.model_checkpoint_path
        print(load_path)
        self.saver.restore(self.session, load_path)
    except:
        print("no saved model to load. starting new session")
        load_was_success = False
    else:
        print("loaded model: {}".format(load_path))
        saver = tf.train.Saver(tf.global_variables())
        episode_number = int(load_path.split('-')[-1])

def save(self):
    self.saver.save(self.session, self.save_path, global_step=n)
    print("SAVED MODEL #{}".format(n))

def predict_UP(self,x):
    feed = {self.tf_x: np.reshape(x, (1, -1))}
    aprob = self.session.run(self.tf_aprob, feed);
    return aprob
  1. 现在,PolicyNetwork已经创建,我们为游戏状态创建了一个preprocess函数;我们不会处理完整的 210×160 状态空间——而是将其缩减为 80×80 的二值状态空间,最后将其展平:
# downsampling
def preprocess(I):
    """ 
    prepro 210x160x3 uint8 frame into 6400 (80x80) 1D float vector 
    """
    I = I[35:195] # crop
    I = I[::2,::2,0] # downsample by factor of 2
    I[I == 144] = 0 # erase background (background type 1)
    I[I == 109] = 0 # erase background (background type 2)
    I[I != 0] = 1 # everything else (paddles, ball) just set to 1
    return I.astype(np.float).ravel()
  1. 让我们定义一些变量,来保存状态、标签、奖励和动作空间大小。我们初始化游戏状态并实例化策略网络:
# Create Game Environment
env_name = "Pong-v0"
env = gym.make(env_name)
env = wrappers.Monitor(env, '/tmp/pong', force=True)
n_actions = env.action_space.n # Number of possible actions
# Initializing Game and State(t-1), action, reward, state(t)
states, rewards, labels = [], [], []
obs = env.reset()
prev_state = None

running_reward = None
running_rewards = []
reward_sum = 0
n = 0
done = False
n_size = 80
num_episodes = 2500

#Create Agent
agent = PolicyNetwork(n_size)
  1. 现在我们开始实施策略梯度算法。对于每一集,智能体首先进行游戏,存储状态、奖励和选择的动作。一旦游戏结束,它就会使用所有存储的数据来进行训练(就像监督学习一样)。然后它会重复这一过程,直到达到你想要的集数:
with tf.Session() as sess:
    agent.set_session(sess)
    sess.run(tf.global_variables_initializer())
    agent.load()
    # training loop
    done = False
    while not done and n< num_episodes:
        # Preprocess the observation
        cur_state = preprocess(obs)
        diff_state = cur_state - prev_state if prev_state isn't None else np.zeros(n_size*n_size)
        prev_state = cur_state

        #Predict the action
        aprob = agent.predict_UP(diff_state) ; aprob = aprob[0,:]
        action = np.random.choice(n_actions, p=aprob)
        #print(action)
        label = np.zeros_like(aprob) ; label[action] = 1

        # Step the environment and get new measurements
        obs, reward, done, info = env.step(action)
        env.render()
        reward_sum += reward

        # record game history
        states.append(diff_state) ; labels.append(label) ; rewards.append(reward)

        if done:
            # update running reward
            running_reward = reward_sum if running_reward is None else         running_reward * 0.99 + reward_sum * 0.01    
            running_rewards.append(running_reward)
            #print(np.vstack(rs).shape)
            feed = {agent.tf_x: np.vstack(states), agent.tf_epr: np.vstack(rewards), agent.tf_y: np.vstack(labels)}
            agent.update(feed)
            # print progress console
            if n % 10 == 0:
                print ('ep {}: reward: {}, mean reward: {:3f}'.format(n, reward_sum, running_reward))
            else:
                print ('\tep {}: reward: {}'.format(n, reward_sum))

            # Start next episode and save model
            states, rewards, labels = [], [], []
            obs = env.reset()
            n += 1 # the Next Episode

            reward_sum = 0
            if n % 50 == 0:
                agent.save()
            done = False

plt.plot(running_rewards)
plt.xlabel('episodes')
plt.ylabel('Running Averge')
plt.show()
env.close()
  1. 在训练了 7,500 集后,智能体开始赢得一些游戏。在训练了 1,200 集后,胜率有所提高,达到了 50%。经过 20,000 集训练后,智能体已经能够赢得大部分游戏。完整代码可在 GitHub 的 Policy gradients.ipynb 文件中找到。你可以在这里查看智能体经过 20,000 集学习后进行的游戏:youtu.be/hZo7kAco8is。请注意,这个智能体学会了在自己的位置附近振荡;它还学会了将自己运动产生的力量传递给球,并且知道只有通过进攻性击球才能击败对手。

演员-评论家算法

在策略梯度方法中,我们引入了基线来减少方差,但依然是动作和基线(仔细看:方差是预期奖励的总和,换句话说,它是状态的好坏或其价值函数)同时变化。是不是应该将策略评估和价值评估分开呢?这正是演员-评论家方法的思想。它由两个神经网络组成,一个用于近似策略,称为演员网络,另一个用于近似价值,称为评论家网络。我们在策略评估和策略改进步骤之间交替进行,从而实现更稳定的学习。评论家使用状态和动作值来估计价值函数,接着用它来更新演员的策略网络参数,使得整体性能得以提升。下图展示了演员-评论家网络的基本架构:

演员-评论家架构

总结

在本章中,我们学习了强化学习(RL)以及它与监督学习和无监督学习的区别。本章的重点是深度强化学习(DRL),在该方法中,深度神经网络用于近似策略函数或价值函数,甚至两者兼而有之。本章介绍了 OpenAI Gym,这是一个提供大量环境来训练 RL 代理的库。我们学习了基于价值的方法,如 Q-learning,并利用它训练一个代理来接载和放下出租车中的乘客。我们还使用了 DQN 来训练一个代理玩 Atari 游戏。接着,本章介绍了基于策略的方法,特别是策略梯度。我们讨论了策略梯度背后的直觉,并使用该算法训练一个 RL 代理来玩 Pong 游戏。

在下一章中,我们将探索生成模型,并学习生成对抗网络背后的秘密。

第七章:物联网的生成模型

机器学习ML)和人工智能AI)几乎触及了所有与人类相关的领域。农业、音乐、健康、国防——你找不到一个没有 AI 印记的领域。AI/ML 的巨大成功,除了计算能力的存在,还依赖于大量数据的生成。大多数生成的数据是未标注的,因此理解数据的内在分布是一个重要的机器学习任务。正是在这一点上,生成模型发挥了作用。

在过去的几年中,深度生成模型在理解数据分布方面取得了巨大成功,并已应用于各种领域。最受欢迎的两种生成模型是 变分自编码器VAEs)和 生成对抗网络GANs)。

在本章中,我们将学习 VAEs 和 GANs,并使用它们生成图像。阅读完本章后,你将掌握以下内容:

  • 了解生成网络与判别网络之间的区别

  • 了解 VAEs

  • 理解 GANs 的直观功能

  • 实现一个基本的 GAN,并用它生成手写数字

  • 了解 GAN 最受欢迎的变种——深度卷积 GAN

  • 在 TensorFlow 中实现深度卷积 GAN,并用它生成人脸

  • 了解 GANs 的进一步修改和应用

介绍

生成模型是深度学习模型中的一个令人兴奋的新分支,它通过无监督学习进行学习。其主要思想是生成具有与给定训练数据相同分布的新样本;例如,一个在手写数字上训练的网络可以生成新的数字,这些数字不在数据集中,但与其相似。从形式上讲,我们可以说,如果训练数据遵循分布 Pdata,那么生成模型的目标是估计概率密度函数 Pmodel,该函数与 Pdata 相似。

生成模型可以分为两种类型:

  • 显式生成模型:在这些模型中,概率密度函数 Pmodel 被显式定义并求解。密度函数可能是可处理的,像 PixelRNN/CNN 这种情况,或者是密度函数的近似,像 VAE 这种情况。

  • 隐式生成模型:在这些模型中,网络学习从 Pmodel 中生成一个样本,而无需显式地定义它。GANs 就是这种类型的生成模型的一个例子。

在本章中,我们将探讨 VAE(一种显式生成模型)和 GAN(一种隐式生成模型)。生成模型可以有效地生成逼真的样本,并可用于执行超分辨率、上色等任务。对于时间序列数据,我们甚至可以用它们进行模拟和规划。最后,生成模型还可以帮助我们理解数据的潜在表示。

使用 VAEs 生成图像

在第四章《物联网深度学习》中,你应该对自编码器及其功能有所了解。VAE 是一种自编码器;在这里,我们保留了(训练过的)解码器部分,可以通过输入随机的潜在特征z来生成类似于训练数据的样本。现在,如果你还记得,在自编码器中,编码器的作用是生成低维特征,z

自编码器的架构

VAE 关注的是从潜在特征z中找到似然函数 p(x):

这是一个难以处理的密度函数,无法直接优化;相反,我们通过使用简单的高斯先验 p(z) 并使编码器解码器网络具有概率性质来获得下界:

VAE 的架构

这使我们能够定义一个可处理的对数似然的下界,如下所示:

在前述的公式中,θ表示解码器网络的参数,φ表示编码器网络的参数。通过最大化这个下界,网络得以训练:

下界中的第一个项负责输入数据的重构,第二个项则用于使近似后验分布接近先验分布。训练完成后,编码器网络作为识别或推理网络,而解码器网络则作为生成器。

你可以参考 Diederik P Kingma 和 Max Welling 在 2014 年 ICLR 会议上发表的论文《Auto-Encoding Variational Bayes》(arxiv.org/abs/1312.6114)中的详细推导。

TensorFlow 中的 VAE

现在让我们来看一下 VAE 的实际应用。在这个示例代码中,我们将使用标准的 MNIST 数据集,并训练一个 VAE 来生成手写数字。由于 MNIST 数据集较为简单,编码器和解码器网络将仅由全连接层组成;这将帮助我们集中精力于 VAE 架构。如果你计划生成复杂的图像(例如 CIFAR-10),你需要将编码器和解码器网络修改为卷积和反卷积网络:

  1. 与之前的所有情况一样,第一步是导入所有必要的模块。在这里,我们将使用 TensorFlow 的高级 API,tf.contrib,来构建全连接层。注意,这样我们就避免了单独声明每一层的权重和偏置的麻烦:
import numpy as np
import tensorflow as tf

import matplotlib.pyplot as plt
%matplotlib inline

from tensorflow.contrib.layers import fully_connected
  1. 我们读取数据。MNIST 数据集可以在 TensorFlow 教程中找到,所以我们直接从那里获取:
# Load MNIST data in a format suited for tensorflow.
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
n_samples = mnist.train.num_examples
n_input = mnist.train.images[0].shape[0]
  1. 我们定义了VariationalAutoencoder类;这个类是核心代码。它包含用于定义编码器和解码器网络的方法。编码器生成潜在特征z的均值和方差,分别称为z_muz_sigma。利用这些,取一个样本Z。然后,潜在特征z被传递给解码器网络生成x_hat。网络通过 Adam 优化器最小化重建损失和潜在损失的总和。该类还定义了重建、生成、转换(到潜在空间)和单步训练的方法:
class VariationalAutoencoder(object):
    def __init__(self,n_input, n_z,
        learning_rate=0.001, batch_size=100):
        self.batch_size = batch_size
        self.n_input = n_input
        self.n_z = n_z

        # Place holder for the input 
        self.x = tf.placeholder(tf.float32, shape = [None, n_input])

        # Use Encoder Network to determine mean and 
        # (log) variance of Gaussian distribution in the latent space
        self.z_mean, self.z_log_sigma_sq = self._encoder_network()
        # Draw a sample z from Gaussian distribution
        eps = tf.random_normal((self.batch_size, n_z), 0, 1, dtype=tf.float32)
        # z = mu + sigma*epsilon
        self.z = tf.add(self.z_mean,tf.multiply(tf.sqrt(tf.exp(self.z_log_sigma_sq)), eps))
        # Use Decoder network to determine mean of
        # Bernoulli distribution of reconstructed input
        self.x_hat = self._decoder_network()

        # Define loss function based variational upper-bound and 
        # corresponding optimizer
        # define generation loss
        reconstruction_loss = \
            -tf.reduce_sum(self.x * tf.log(1e-10 + self.x_hat)
            + (1-self.x) * tf.log(1e-10 + 1 - self.x_hat), 1)
        self.reconstruction_loss = tf.reduce_mean(reconstruction_loss)

        latent_loss = -0.5 * tf.reduce_sum(1 + self.z_log_sigma_sq 
            - tf.square(self.z_mean)- tf.exp(self.z_log_sigma_sq), 1)
        self.latent_loss = tf.reduce_mean(latent_loss)
        self.cost = tf.reduce_mean(reconstruction_loss + latent_loss) 
        # average over batch
        # Define the optimizer
        self.optimizer = tf.train.AdamOptimizer(learning_rate).minimize(self.cost)

        # Initializing the tensor flow variables
        init = tf.global_variables_initializer()
        # Launch the session
        self.sess = tf.InteractiveSession()
        self.sess.run(init)

    # Create encoder network
    def _encoder_network(self):
        # Generate probabilistic encoder (inference network), which
        # maps inputs onto a normal distribution in latent space.
        layer_1 = fully_connected(self.x,500,activation_fn=tf.nn.softplus) 
        layer_2 = fully_connected(layer_1, 500, activation_fn=tf.nn.softplus) 
        z_mean = fully_connected(layer_2,self.n_z, activation_fn=None)
        z_log_sigma_sq = fully_connected(layer_2, self.n_z, activation_fn=None)
        return (z_mean, z_log_sigma_sq)

    # Create decoder network
    def _decoder_network(self):
        # Generate probabilistic decoder (generator network), which
        # maps points in the latent space onto a Bernoulli distribution in the data space.
        layer_1 = fully_connected(self.z,500,activation_fn=tf.nn.softplus) 
        layer_2 = fully_connected(layer_1, 500, activation_fn=tf.nn.softplus) 
        x_hat = fully_connected(layer_2, self.n_input, activation_fn=tf.nn.sigmoid)

        return x_hat

    def single_step_train(self, X):
        _,cost,recon_loss,latent_loss = self.sess.run([self.optimizer,         self.cost,self.reconstruction_loss,self.latent_loss],feed_dict={self.x: X})
        return cost, recon_loss, latent_loss

    def transform(self, X):
        """Transform data by mapping it into the latent space."""
        # Note: This maps to mean of distribution, we could alternatively
        # sample from Gaussian distribution
        return self.sess.run(self.z_mean, feed_dict={self.x: X})

    def generate(self, z_mu=None):
        """ Generate data by sampling from latent space.

        If z_mu isn't None, data for this point in latent space is
        generated. Otherwise, z_mu is drawn from prior in latent 
        space. 
        """
        if z_mu is None:
            z_mu = np.random.normal(size=n_z)
            # Note: This maps to mean of distribution, we could alternatively    
            # sample from Gaussian distribution
        return self.sess.run(self.x_hat,feed_dict={self.z: z_mu})

    def reconstruct(self, X):
        """ Use VAE to reconstruct given data. """
        return self.sess.run(self.x_hat, feed_dict={self.x: X})
  1. 在所有成分准备好之后,我们开始训练我们的 VAE。我们通过train函数来完成这项任务:
def train(n_input,n_z, learning_rate=0.001,
    batch_size=100, training_epochs=10, display_step=5):
    vae = VariationalAutoencoder(n_input,n_z, 
        learning_rate=learning_rate, 
        batch_size=batch_size)
    # Training cycle
    for epoch in range(training_epochs):
        avg_cost, avg_r_loss, avg_l_loss = 0., 0., 0.
        total_batch = int(n_samples / batch_size)
        # Loop over all batches
        for i in range(total_batch):
            batch_xs, _ = mnist.train.next_batch(batch_size)
            # Fit training using batch data
            cost,r_loss, l_loss = vae.single_step_train(batch_xs)
            # Compute average loss
            avg_cost += cost / n_samples * batch_size
            avg_r_loss += r_loss / n_samples * batch_size
            avg_l_loss += l_loss / n_samples * batch_size
        # Display logs per epoch step
        if epoch % display_step == 0:
            print("Epoch: {:4d} cost={:.4f} Reconstruction loss = {:.4f} Latent Loss = {:.4f}".format(epoch,avg_cost,avg_r_loss,avg_l_loss))
     return vae
  1. 在以下截图中,您可以看到潜在空间大小为 10 的 VAE 的重建数字(左)和生成的手写数字(右):

  1. 正如之前讨论的,编码器网络将输入空间的维度降低。为了更清楚地说明这一点,我们将潜在空间的维度降为 2。以下是二维 z 空间中每个标签的分离情况:

  1. 来自潜在空间维度为 2 的 VAE 的重建和生成数字如下所示:

从前面的截图(右)可以看到有趣的一点是,改变二维z的值会导致不同的笔画和不同的数字。完整的代码可以在 GitHub 上的Chapter 07文件夹中的VariationalAutoEncoders_MNIST.ipynb文件里找到:

tf.contrib.layers.fully_connected(
    inputs,
    num_outputs,
    activation_fn=tf.nn.relu,
    normalizer_fn=None,
    normalizer_params=None,
    weights_initializer=intializers.xavier_intializer(),
    weights_regularizer= None, 
    biases_initializer=tf.zeros_intializer(),
    biases_regularizer=None,
    reuse=None,
    variables_collections=None,
    outputs_collections=None,
    trainable=True,
    scope=None
)

contrib层是 TensorFlow 中包含的一个更高级的包。它提供了构建神经网络层、正则化器、总结等操作。在前面的代码中,我们使用了tf.contrib.layers.fully_connected()操作,定义在tensorflow/contrib/layers/python/layers/layers.py中,它添加了一个全连接层。默认情况下,它创建代表全连接互连矩阵的权重,默认使用 Xavier 初始化。同时,它也创建了初始化为零的偏置。它还提供了选择归一化和激活函数的选项。

GANs

GANs 是隐式生成网络。在 Quora 的一个会议中,Facebook 人工智能研究总监兼纽约大学教授 Yann LeCun 将 GANs 描述为过去 10 年中机器学习领域最有趣的想法。目前,关于 GAN 的研究非常活跃。过去几年的主要 AI/ML 会议上,报道了大多数与 GAN 相关的论文。

GAN 由 Ian J. Goodfellow 和 Yoshua Bengio 在 2014 年的论文《生成对抗网络》(arxiv.org/abs/1406.2661)中提出。它们的灵感来自于两人对抗的游戏场景。就像游戏中的两个玩家一样,GAN 中有两个网络——一个称为判别网络,另一个称为生成网络——彼此对抗。生成网络试图生成与输入数据相似的数据,而判别网络则必须识别它所看到的数据是来自真实数据还是伪造数据(即由生成器生成)。每次判别器发现真实输入和伪造数据之间的分布差异时,生成器都会调整其权重以减少这种差异。总结来说,判别网络试图学习伪造数据和真实数据之间的边界,而生成网络则试图学习训练数据的分布。随着训练的结束,生成器学会生成与输入数据分布完全相似的图像,判别器则无法再区分二者。GAN 的总体架构如下:

GAN 的架构

现在让我们深入探讨 GAN 是如何学习的。判别器和生成器轮流进行学习。学习过程可以分为两个步骤:

  1. 在这里,判别器D(x),进行学习。生成器G(z),用于从随机噪声z(它遵循某种先验分布P(z))生成假图像生成器生成的假图像和来自训练数据集的真实图像都被输入到判别器,然后判别器执行监督学习,试图将假图像与真实图像区分开。如果Pdata 是训练数据集的分布,那么判别器网络试图最大化其目标,使得当输入数据为真实数据时,D(x)接近 1,当输入数据为假数据时,接近 0。这可以通过对以下目标函数执行梯度上升来实现:

  1. 在下一步,生成器网络进行学习。它的目标是欺骗判别器网络,让它认为生成的G(z)是真实的,也就是说,迫使D(G(z))接近 1。为了实现这一目标,生成器网络最小化以下目标:

这两个步骤会依次重复进行。一旦训练结束,判别器将无法再区分真实数据和伪造数据,生成器也变得非常擅长生成与训练数据非常相似的数据。嗯,说起来容易做起来难:当你尝试使用 GAN 时,你会发现训练并不是很稳定。这是一个开放的研究问题,已经提出了许多 GAN 的变种来解决这一问题。

在 TensorFlow 中实现一个基础的 GAN

在这一部分,我们将编写一个 TensorFlow 代码来实现一个 GAN,正如我们在上一节所学的那样。我们将为判别器和生成器使用简单的 MLP 网络。为了简便起见,我们将使用 MNIST 数据集:

  1. 像往常一样,第一步是添加所有必要的模块。由于我们需要交替访问和训练生成器和判别器的参数,我们将为了清晰起见在当前代码中定义我们的权重和偏置。使用 Xavier 初始化权重并将偏置初始化为零总是更好的做法。因此,我们还从 TensorFlow 导入执行 Xavier 初始化的方法:from tensorflow.contrib.layers import xavier_initializer
# import the necessaey modules
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import os
from tensorflow.contrib.layers import xavier_initializer
%matplotlib inline
  1. 让我们读取数据并定义超参数:
# Load data
from tensorflow.examples.tutorials.mnist import input_data
data = input_data.read_data_sets('MNIST_data', one_hot=True)

# define hyperparameters
batch_size = 128
Z_dim = 100
im_size = 28
h_size=128
learning_rate_D = .0005
learning_rate_G = .0006
  1. 我们定义生成器和判别器的训练参数,并为输入X和潜在变量Z定义占位符:
#Create Placeholder for input X and random noise Z
X = tf.placeholder(tf.float32, shape=[None, im_size*im_size])
Z = tf.placeholder(tf.float32, shape=[None, Z_dim])
initializer=xavier_initializer()

# Define Discriminator and Generator training variables
#Discriminiator
D_W1 = tf.Variable(initializer([im_size*im_size, h_size]))
D_b1 = tf.Variable(tf.zeros(shape=[h_size]))

D_W2 = tf.Variable(initializer([h_size, 1]))
D_b2 = tf.Variable(tf.zeros(shape=[1]))

theta_D = [D_W1, D_W2, D_b1, D_b2]

#Generator
G_W1 = tf.Variable(initializer([Z_dim, h_size]))
G_b1 = tf.Variable(tf.zeros(shape=[h_size]))

G_W2 = tf.Variable(initializer([h_size, im_size*im_size]))
G_b2 = tf.Variable(tf.zeros(shape=[im_size*im_size]))

theta_G = [G_W1, G_W2, G_b1, G_b2]
  1. 既然我们已经设置了占位符和权重,我们定义一个函数来生成来自Z的随机噪声。在这里,我们使用均匀分布来生成噪声;有些人也尝试过使用高斯噪声——要做到这一点,你只需要将随机函数从uniform改为normal
def sample_Z(m, n):
    return np.random.uniform(-1., 1., size=[m, n])
  1. 我们构建判别器和生成器网络:
def generator(z):
    """ Two layer Generator Network Z=>128=>784 """
    G_h1 = tf.nn.relu(tf.matmul(z, G_W1) + G_b1)
    G_log_prob = tf.matmul(G_h1, G_W2) + G_b2
    G_prob = tf.nn.sigmoid(G_log_prob)
    return G_prob

def discriminator(x):
    """ Two layer Discriminator Network X=>128=>1 """
    D_h1 = tf.nn.relu(tf.matmul(x, D_W1) + D_b1)
    D_logit = tf.matmul(D_h1, D_W2) + D_b2
    D_prob = tf.nn.sigmoid(D_logit)
    return D_prob, D_logit
  1. 我们还需要一个辅助函数来绘制生成的手写数字。以下函数会将生成的 25 个样本以 5×5 的网格展示:
def plot(samples):
    """function to plot generated samples"""
    fig = plt.figure(figsize=(10, 10))
    gs = gridspec.GridSpec(5, 5)
    gs.update(wspace=0.05, hspace=0.05)
    for i, sample in enumerate(samples):
        ax = plt.subplot(gs[i])
        plt.axis('off')
        ax.set_xticklabels([])
        ax.set_yticklabels([])
        ax.set_aspect('equal')
        plt.imshow(sample.reshape(28, 28), cmap='gray')
    return fig
  1. 现在,我们定义 TensorFlow 操作来生成生成器的样本,并从判别器获取对假输入和真实输入数据的预测:
G_sample = generator(Z)
D_real, D_logit_real = discriminator(X)
D_fake, D_logit_fake = discriminator(G_sample)
  1. 接下来,我们为生成器和判别器网络定义交叉熵损失,并交替最小化它们,同时保持其他权重参数冻结:
D_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logit_real, labels=tf.ones_like(D_logit_real)))
D_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logit_fake, labels=tf.zeros_like(D_logit_fake)))
D_loss = D_loss_real + D_loss_fake
G_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logit_fake, labels=tf.ones_like(D_logit_fake)))

D_solver = tf.train.AdamOptimizer(learning_rate=learning_rate_D).minimize(D_loss, var_list=theta_D)
G_solver = tf.train.AdamOptimizer(learning_rate=learning_rate_G).minimize(G_loss, var_list=theta_G)
  1. 最后,让我们在 TensorFlow 会话中执行训练:
sess = tf.Session()
sess.run(tf.global_variables_initializer())
GLoss = []
DLoss = []
if not os.path.exists('out/'):
    os.makedirs('out/')

for it in range(100000):
    if it % 100 == 0:
        samples = sess.run(G_sample, feed_dict={Z: sample_Z(25, Z_dim)})
        fig = plot(samples)
        plt.savefig('out/{}.png'.format(str(it).zfill(3)), bbox_inches='tight')
        plt.close(fig)
    X_mb, _ = data.train.next_batch(batch_size)
    _, D_loss_curr = sess.run([D_solver, D_loss], feed_dict={X: X_mb, Z: sample_Z(batch_size, Z_dim)})
    _, G_loss_curr = sess.run([G_solver, G_loss], feed_dict={Z: sample_Z(batch_size, Z_dim)})
    GLoss.append(G_loss_curr)
    DLoss.append(D_loss_curr)
    if it % 100 == 0:
        print('Iter: {} D loss: {:.4} G_loss: {:.4}'.format(it,D_loss_curr, G_loss_curr))

print('Done')
  1. 在以下屏幕截图中,你可以看到生成和判别网络的损失是如何变化的:

生成和判别网络的损失

  1. 让我们还看看在不同的训练轮次中生成的手写数字:

手写数字

尽管手写数字已经足够好,但我们可以看到仍有很多改进的空间。研究人员用来稳定性能的一些方法如下:

  • 将输入图像从(0,1)标准化到(-1,1)。而且,生成器最终输出的激活函数不再使用 sigmoid,而是使用双曲正切激活函数。

  • 我们可以通过最大化损失log D来替代最小化生成器损失log 1-D;这可以通过在训练生成器时反转标签来实现,例如(将真实标签转为假标签,假标签转为真实标签)。

  • 另一种方法是存储以前生成的图像,并通过从中随机选择来训练判别器。(没错,你猜对了——这类似于我们在第六章《物联网的强化学习》中学到的经验回放缓冲区。)

  • 人们还尝试过只有当生成器或判别器的损失超过某个阈值时才更新它们。

  • 在判别器和生成器的隐藏层中,使用 Leaky ReLU 激活函数,而不是 ReLU。

深度卷积生成对抗网络

2016 年,Alec Radford 等人 提出了 GAN 的一种变体,叫做 深度卷积生成对抗网络 (DCGAN)。 (完整论文链接:arxiv.org/abs/1511.06434。)他们将 MLP 层替换为卷积层,并在生成器和判别器网络中都加入了批量归一化。我们将在这里使用名人图像数据集实现 DCGAN。你可以从 mmlab.ie.cuhk.edu.hk/projects/CelebA.html 下载 ZIP 文件 img_align_celeba.zip。我们将利用我们在第二章《物联网的数据访问与分布式处理》中创建的 loader_celebA.py 文件来解压并读取图像:

  1. 我们将导入所有需要的模块的语句:
import loader
import os
from glob import glob
import numpy as np
from matplotlib import pyplot
import tensorflow as tf
%matplotlib inline
  1. 我们使用 loader_celebA.py 解压 img_align_celeba.zip。由于图像数量非常庞大,我们使用该文件中定义的 get_batches 函数来生成用于训练网络的批次:
loader.download_celeb_a()

# Let's explore the images
data_dir = os.getcwd()
test_images = loader.get_batch(glob(os.path.join(data_dir, 'celebA/*.jpg'))[:10], 56, 56)
pyplot.imshow(loader.plot_images(test_images))

在接下来的内容中,您可以看到数据集的图像:

  1. 我们定义了判别器网络。它由三个卷积层组成,分别使用 64128256 个 5×5 大小的滤波器。前两个层使用 2 的步幅,第三个卷积层使用 1 的步幅。所有三个卷积层都使用 leakyReLU 作为激活函数。每个卷积层后面都跟有一个批量归一化层。第三个卷积层的结果会被拉平,并传递到最后一个全连接(密集)层,该层使用 sigmoid 激活函数:
def discriminator(images, reuse=False):
    """
    Create the discriminator network
    """
    alpha = 0.2

    with tf.variable_scope('discriminator', reuse=reuse):
        # using 4 layer network as in DCGAN Paper

        # First convolution layer
        conv1 = tf.layers.conv2d(images, 64, 5, 2, 'SAME')
        lrelu1 = tf.maximum(alpha * conv1, conv1)

        # Second convolution layer
        conv2 = tf.layers.conv2d(lrelu1, 128, 5, 2, 'SAME')
        batch_norm2 = tf.layers.batch_normalization(conv2, training=True)
        lrelu2 = tf.maximum(alpha * batch_norm2, batch_norm2)

        # Third convolution layer
        conv3 = tf.layers.conv2d(lrelu2, 256, 5, 1, 'SAME')
        batch_norm3 = tf.layers.batch_normalization(conv3, training=True)
        lrelu3 = tf.maximum(alpha * batch_norm3, batch_norm3)

        # Flatten layer
        flat = tf.reshape(lrelu3, (-1, 4*4*256))

        # Logits
        logits = tf.layers.dense(flat, 1)

        # Output
        out = tf.sigmoid(logits)

        return out, logits
  1. 生成器网络是判别器的反向;生成器的输入首先会传递给一个包含 2×2×512 单元的全连接层。全连接层的输出会被重塑,以便我们将其传递给卷积堆栈。我们使用 tf.layers.conv2d_transpose() 方法来获取转置卷积的输出。生成器有三个转置卷积层。除了最后一个卷积层外,所有层都使用 leakyReLU 作为激活函数。最后一个转置卷积层使用双曲正切激活函数,以确保输出位于 (-11) 的范围内:
def generator(z, out_channel_dim, is_train=True):
    """
    Create the generator network
    """
    alpha = 0.2

    with tf.variable_scope('generator', reuse=False if is_train==True else True):
        # First fully connected layer
        x_1 = tf.layers.dense(z, 2*2*512)

        # Reshape it to start the convolutional stack
        deconv_2 = tf.reshape(x_1, (-1, 2, 2, 512))
        batch_norm2 = tf.layers.batch_normalization(deconv_2, training=is_train)
        lrelu2 = tf.maximum(alpha * batch_norm2, batch_norm2)

        # Deconv 1
        deconv3 = tf.layers.conv2d_transpose(lrelu2, 256, 5, 2, padding='VALID')
        batch_norm3 = tf.layers.batch_normalization(deconv3, training=is_train)
        lrelu3 = tf.maximum(alpha * batch_norm3, batch_norm3)

        # Deconv 2
        deconv4 = tf.layers.conv2d_transpose(lrelu3, 128, 5, 2, padding='SAME')
        batch_norm4 = tf.layers.batch_normalization(deconv4, training=is_train)
        lrelu4 = tf.maximum(alpha * batch_norm4, batch_norm4)

        # Output layer
        logits = tf.layers.conv2d_transpose(lrelu4, out_channel_dim, 5, 2, padding='SAME')

        out = tf.tanh(logits)

        return out
  1. 我们定义了计算模型损失的函数,该函数定义了生成器和判别器的损失,并返回它们:
def model_loss(input_real, input_z, out_channel_dim):
    """
    Get the loss for the discriminator and generator
    """

    label_smoothing = 0.9

    g_model = generator(input_z, out_channel_dim)
    d_model_real, d_logits_real = discriminator(input_real)
    d_model_fake, d_logits_fake = discriminator(g_model, reuse=True)

    d_loss_real = tf.reduce_mean(
        tf.nn.sigmoid_cross_entropy_with_logits(logits=d_logits_real,
                                                labels=tf.ones_like(d_model_real) * label_smoothing))
    d_loss_fake = tf.reduce_mean(
        tf.nn.sigmoid_cross_entropy_with_logits(logits=d_logits_fake,
                                                labels=tf.zeros_like(d_model_fake)))

    d_loss = d_loss_real + d_loss_fake

    g_loss = tf.reduce_mean(
        tf.nn.sigmoid_cross_entropy_with_logits(logits=d_logits_fake,
                                                labels=tf.ones_like(d_model_fake) * label_smoothing))

    return d_loss, g_loss
  1. 接下来,我们需要定义优化器,以便判别器和生成器能够顺序学习。为此,我们利用tf.trainable_variables()获取所有训练变量的列表,然后首先优化仅判别器的训练变量,再优化生成器的训练变量:
def model_opt(d_loss, g_loss, learning_rate, beta1):
    """
    Get optimization operations
    """
    t_vars = tf.trainable_variables()
    d_vars = [var for var in t_vars if var.name.startswith('discriminator')]
    g_vars = [var for var in t_vars if var.name.startswith('generator')]

    # Optimize
    with tf.control_dependencies(tf.get_collection(tf.GraphKeys.UPDATE_OPS)): 
        d_train_opt = tf.train.AdamOptimizer(learning_rate, beta1=beta1).minimize(d_loss, var_list=d_vars)
        g_train_opt = tf.train.AdamOptimizer(learning_rate, beta1=beta1).minimize(g_loss, var_list=g_vars)

    return d_train_opt, g_train_opt
  1. 现在,我们已经具备了训练 DCGAN 所需的所有要素。为了更好地观察生成器的学习过程,我们定义了一个辅助函数,来显示生成器网络在学习过程中生成的图像:
def generator_output(sess, n_images, input_z, out_channel_dim):
    """
    Show example output for the generator
    """
    z_dim = input_z.get_shape().as_list()[-1]
    example_z = np.random.uniform(-1, 1, size=[n_images, z_dim])

    samples = sess.run(
        generator(input_z, out_channel_dim, False),
        feed_dict={input_z: example_z})

    pyplot.imshow(loader.plot_images(samples))
    pyplot.show()
  1. 最后,进入训练部分。在这里,我们使用之前定义的ops来训练 DCGAN,并将图像按批次输入到网络中:
def train(epoch_count, batch_size, z_dim, learning_rate, beta1, get_batches, data_shape, data_files):
    """
    Train the GAN
    """
    w, h, num_ch = data_shape[1], data_shape[2], data_shape[3]
    X = tf.placeholder(tf.float32, shape=(None, w, h, num_ch), name='input_real') 
    Z = tf.placeholder(tf.float32, (None, z_dim), name='input_z')
    #model_inputs(data_shape[1], data_shape[2], data_shape[3], z_dim)
    D_loss, G_loss = model_loss(X, Z, data_shape[3])
    D_solve, G_solve = model_opt(D_loss, G_loss, learning_rate, beta1)

    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        train_loss_d = []
        train_loss_g = []
        for epoch_i in range(epoch_count):
            num_batch = 0
            lossD, lossG = 0,0
            for batch_images in get_batches(batch_size, data_shape, data_files):

                # values range from -0.5 to 0.5 so we scale to range -1, 1
                batch_images = batch_images * 2
                num_batch += 1

                batch_z = np.random.uniform(-1, 1, size=(batch_size, z_dim))

                _,d_loss = sess.run([D_solve,D_loss], feed_dict={X: batch_images, Z: batch_z})
                _,g_loss = sess.run([G_solve,G_loss], feed_dict={X: batch_images, Z: batch_z})

                lossD += (d_loss/batch_size)
                lossG += (g_loss/batch_size)
                if num_batch % 500 == 0:
                    # After every 500 batches
                    print("Epoch {}/{} For Batch {} Discriminator Loss: {:.4f} Generator Loss: {:.4f}".
                          format(epoch_i+1, epochs, num_batch, lossD/num_batch, lossG/num_batch))

                    generator_output(sess, 9, Z, data_shape[3])
            train_loss_d.append(lossD/num_batch)
            train_loss_g.append(lossG/num_batch)

    return train_loss_d, train_loss_g
  1. 现在我们来定义数据的参数并进行训练:
# Data Parameters
IMAGE_HEIGHT = 28
IMAGE_WIDTH = 28
data_files = glob(os.path.join(data_dir, 'celebA/*.jpg'))

#Hyper parameters
batch_size = 16
z_dim = 100
learning_rate = 0.0002
beta1 = 0.5
epochs = 2
shape = len(data_files), IMAGE_WIDTH, IMAGE_HEIGHT, 3
with tf.Graph().as_default():
    Loss_D, Loss_G = train(epochs, batch_size, z_dim, learning_rate, beta1, loader.get_batches, shape, data_files)

每处理一个批次,你可以看到生成器的输出在不断改善:

DCGAN 生成器输出随着学习的进展而改善

GAN 的变体及其酷炫应用

在过去几年里,已经提出了大量的 GAN 变体。你可以通过github.com/hindupuravinash/the-gan-zoo访问完整的 GAN 变体列表。在这一部分,我们将列出一些更受欢迎和成功的变体。

Cycle GAN

2018 年初,伯克利 AI 研究实验室发表了一篇名为《使用循环一致对抗网络进行未配对图像到图像的转换》的论文(arXiv 链接:arxiv.org/pdf/1703.10593.pdf)。这篇论文的特别之处不仅在于它提出了一种新的架构——CycleGAN,并且具有更好的稳定性,还因为它展示了这种架构可以用于复杂的图像转换。以下图示展示了 CycleGAN 的架构,两个部分分别突出显示了在计算两个对抗损失时起作用的生成器判别器

CycleGAN 的架构

CycleGAN 由两个 GAN 组成。它们分别在两个不同的数据集上进行训练,xPdata 和yPdata。生成器被训练来执行映射,即G[A]: x→yG[B]: y→x。每个判别器的训练目标是能够区分图像x和变换后的图像GB,从而得出两个变换的对抗损失函数,定义如下:

第二个步骤如下:

两个 GAN 的生成器以循环方式连接在一起,这样如果一个生成器的输出被输入到另一个生成器中,并且将对应的输出反馈到第一个生成器,我们就会得到相同的数据。我们通过一个例子来解释这个过程;假设生成器 AG[A])输入一张图像 x,那么输出就是转换后的图像 GA。这个转换后的图像现在输入到生成器 BG[B]),GB)≈x,结果应该是原始图像 x。类似地,我们也会有 GA≈y。这一过程通过引入循环损失项得以实现:

因此,网络的目标函数如下:

在这里,λ 控制着两个目标之间的相对重要性。它们还在经验缓冲区中保留了先前的图像,用于训练判别器。在下面的截图中,你可以看到论文中报告的从 CycleGANs 获得的一些结果:

CycleGAN 的结果(来自原始论文)

作者展示了 CycleGANs 可以用于以下几种情况:

  • 图像转换:例如将马变成斑马,反之亦然。

  • 分辨率增强:当 CycleGAN 用包含低分辨率和超分辨率图像的数据集进行训练时,能够在输入低分辨率图像时执行超分辨率。

  • 风格迁移:给定一张图像,可以将其转换为不同的绘画风格

GANs 的应用

GANs 确实是很有趣的网络;除了你已经看到的应用,GANs 还在许多其他令人兴奋的应用中得到了探索。接下来,我们列举了一些:

  • 音乐生成:MIDINet 是一种卷积 GAN,已被证明可以生成旋律。你可以参考这篇论文:arxiv.org/pdf/1703.10847.pdf

  • 医学异常检测:AnoGAN 是一种由 Thomas Schlegl 等人展示的 DCGAN,用于学习正常解剖变异性的流形。他们成功地训练了网络,能够标记视网膜光学相干断层扫描图像中的异常。如果你对这项工作感兴趣,可以在 arXiv 上阅读相关论文:arxiv.org/pdf/1703.05921.pdf

  • 使用 GANs 进行人脸向量运算:在 Indico Research 和 Facebook 共同研究的论文中,他们展示了使用 GANs 进行图像运算的可能性。例如,戴眼镜的男人不戴眼镜的男人 + 不戴眼镜的女人 = 戴眼镜的女人。这是一篇有趣的论文,你可以在 Arxiv 上阅读更多内容:arxiv.org/pdf/1511.06434.pdf

  • 文本到图像合成:生成对抗网络(GAN)已被证明可以根据人类书写的文本描述生成鸟类和花卉的图像。该模型使用了 DCGAN,并结合了混合字符级卷积递归网络。该工作的详细信息可以在论文《生成对抗文本到图像合成》中找到。论文的链接是arxiv.org/pdf/1605.05396.pdf

总结

这是一个有趣的章节,希望你在阅读时能像我写这篇文章时一样享受其中。目前这是研究的热点话题。本章介绍了生成模型及其分类,即隐式生成模型和显式生成模型。首先介绍的生成模型是变分自编码器(VAE);它们是显式生成模型,旨在估计密度函数的下界。我们在 TensorFlow 中实现了 VAE,并用其生成了手写数字。

本章随后转向了一个更为流行的显式生成模型:生成对抗网络(GAN)。详细解释了 GAN 架构,特别是判别器网络和生成器网络如何相互竞争。我们使用 TensorFlow 实现了一个 GAN,用于生成手写数字。本章还介绍了 GAN 的成功变种:DCGAN,并实现了一个 DCGAN,用于生成名人图像。本章还介绍了最近提出的 GAN 变种——CycleGAN 的架构细节,以及它的一些酷应用。

本章标志着本书第一部分的结束。到目前为止,我们集中讨论了不同的机器学习(ML)和深度学习(DL)模型,这些模型是我们理解数据并用于预测/分类以及其他任务所需的。从下一章开始,我们将更多地讨论数据本身,以及在当前物联网驱动的环境中,我们如何处理这些数据。

在下一章,我们将探讨分布式处理,这是处理大量数据时的必需技术,并介绍两种提供分布式处理的平台。