演化式深度学习-全-
演化式深度学习(全)
原文:Evolutionary Deep Learning
译者:飞龙
前言
前言
当我在 25 多年前开始我的机器学习和人工智能职业生涯时,两种主导技术被认为是下一个大事件。这两种技术都显示出解决复杂问题的潜力,并且它们在计算上等效。这两种技术是进化算法和神经网络(深度学习)。
在接下来的几十年里,我见证了进化算法的急剧下降和深度学习的爆炸性增长。虽然这场战斗是通过计算效率赢得的,但深度学习也展示了众多新颖的应用。另一方面,对于大部分情况,进化算法和遗传算法的知识和使用已经减少到脚注的地位。
我写这本书的目的是展示进化算法和遗传算法为深度学习系统提供的好处。随着深度学习时代成熟到自动化机器学习(AutoML)时代,这些好处尤其相关,在这个时代,能够自动化大规模和广泛范围的模型开发正在成为主流。
我也相信,通过观察进化,我们可以帮助我们的通用人工智能和智能的研究。毕竟,进化是自然界用来形成我们智能的工具,那么为什么不能用它来提升人工智能呢?我的猜测是我们太急躁和自大,认为人类可以独自解决这个问题。
通过写这本书,我希望展示进化方法在深度学习之上的力量,作为一种打破常规的思考方式。我的希望是,它以有趣和创新的途径展示了进化方法的基础,同时也涉足进化的深度学习网络(即 NEAT)和本能学习的高级领域。本能学习是我对我们应该更加关注生物生命如何进化,并在我们寻找更智能的人工网络中反映这些相同特性的看法。
致谢
我想要感谢开源社区,特别是以下项目:
-
分布式进化算法在 Python 中(DEAP)—
github.com/DEAP/deap -
基于 Python 的基因表达编程框架(GEPPY)—
github.com/ShuhuaGao/geppy -
Python 中增强拓扑结构的神经进化(NEAT Python)—
github.com/CodeReclaimers/neat-python -
OpenAI Gym—
github.com/openai/gym -
Keras/TensorFlow—
github.com/tensorflow/tensorflow -
PyTorch—
github.com/pytorch/pytorch
没有其他人不懈地投入工作和时间来开发和维护这些存储库,像这样的书是不可能的。这些也是任何对提高进化算法(EA)或深度学习(DL)技能感兴趣的人的极好资源。
特别感谢我的家人,他们一直支持我的写作、教学和演讲活动。他们总是愿意阅读一段或一节内容,并给我提供意见,无论是好是坏。
非常感谢 Manning 出版社的编辑和生产团队,他们帮助创建了这本书。
感谢所有审稿人:Al Krinker、Alexey Vyskubov、Bhagvan Kommadi、David Paccoud、Dinesh Ghanta、Domingo Salazar、Howard Bandy、Edmund Ronald、Erik Sapper、Guillaume Alleon、Jasmine Alkin、Jesús Antonino Juárez Guerrero、John Williams、Jose San Leandro、Juan J. Durillo、kali kaneko、Maria Ana、Maxim Volgin、Nick Decroos、Ninoslav Čerkez、Oliver Korten、Or Golan、Raj Kumar、Ricardo Di Pasquale、Riccardo Marotti、Sergio Govoni、Sadhana G、Simone Sguazza、Shivakumar Swaminathan、Szymon Harabasz 和 Thomas Heiman。你们的建议帮助使这本书更加完善。
最后,我还要感谢查尔斯·达尔文,他的灵感来源于他写出的开创性作品《物种起源》。作为一个非常虔诚的宗教徒,查尔斯在二十年的时间里内部挣扎,在信仰和观察之间斗争,最终决定出版他的书。最终,他展现了勇气和信任科学,超越了信仰和当时的主流思想。这是我写一本结合进化和深度学习的书时所受到的启发。
关于本书
本书向读者介绍了进化算法和遗传算法,从解决有趣的机器学习问题到将概念与深度学习相结合。本书首先介绍 Python 中的模拟和进化算法的概念。随着内容的深入,重点转向展示价值,包括深度学习的应用。
适合阅读本书的人群
你应该有扎实的 Python 基础,并理解核心机器学习和数据科学概念。对于理解后续章节中的概念,深度学习背景将是必不可少的。
本书组织结构:路线图
本书分为三个部分:入门、优化深度学习和高级应用。在第一部分“入门”中,我们首先介绍模拟、进化以及遗传和其他算法的基础知识。接着,我们转向展示进化算法在深度学习中的应用以及遗传搜索的多种应用。最后,我们通过探讨生成建模、强化学习和通用智能的高级应用来结束本书。以下是各章节的摘要:
-
第一部分:入门
-
第一章:介绍进化深度学习——本章介绍了将进化算法与深度学习相结合的概念。
-
第二章:介绍进化计算——本章提供了计算模拟的基本介绍以及如何利用进化。
-
第三章:使用 DEAP 介绍遗传算法—本章介绍了遗传算法的概念以及如何使用 DEAP 框架。
-
第四章:使用 DEAP 进行更多进化计算—本章探讨了从旅行商问题到生成蒙娜丽莎图像的遗传和进化算法的有趣应用。
-
-
第二部分:优化深度学习
-
第五章:自动化超参数优化—本章展示了使用遗传或进化算法在深度学习系统中优化超参数的几种方法。
-
第六章:神经进化优化—在本章中,我们探讨了使用神经进化对深度学习系统的网络架构进行优化。
-
第七章:进化的卷积神经网络—本章探讨了使用进化优化卷积神经网络架构的高级应用。
-
-
第三部分:高级应用
-
第八章:演化的自编码器—本章介绍了或回顾了使用自编码器的生成模型的基础知识。然后,它展示了进化如何发展演化的自编码器。
-
第九章:生成深度学习和进化—本章在上一章的基础上,介绍了生成对抗网络及其如何通过进化进行优化。
-
第十章:NEAT:拓扑增强的神经进化—本章介绍了 NEAT,并涵盖了它如何应用于各种基线应用。
-
第十一章:使用 NEAT 进行进化学习—本章讨论了强化学习和深度强化学习的基础知识,然后展示了如何使用 NEAT 在 OpenAI Gym 上解决一些难题。
-
第十二章:进化机器学习和未来—最后一章探讨了机器学习进化的未来,以及它如何为通用人工智能提供洞见。
-
虽然这本书旨在从头到尾阅读,但并非所有读者都有时间、背景或兴趣阅读所有内容。以下是一个快速指南,可以帮助你选择想要关注的章节或部分:
-
第一部分:入门—如果你是模拟和进化或遗传计算的初学者,请务必阅读本节。本节也可以作为一个有用的复习,并展示了几个有趣的应用。
-
第二部分:优化深度学习—如果你确实需要优化用于神经进化或超参数调整的深度学习系统,请阅读本节或其中的特定章节。
-
第三部分:高级应用—本部分中的章节分为三个子部分:进化的生成建模(第八章和第九章)、NEAT(第十章和第十一章)和本能学习(第十二章)。这些子部分可以独立处理。
关于代码
本书的所有代码都是使用 Google Colab 笔记本编写的,可以在作者的 GitHub 仓库中找到:github.com/cxbxmxcx/EvolutionaryDeepLearning。要运行代码,您只需在浏览器中导航到 GitHub 仓库,并找到相关的代码示例。所有代码示例都带有章节编号的前缀和示例编号,例如,EDL_2_2_Simulating_Life.ipynb。从那里,只需点击 Google Colab 徽标即可在 Colab 中启动笔记本。所有依赖项要么已在 Colab 中预先安装,要么作为笔记本的一部分安装。
本书包含许多源代码示例,既有编号列表,也有与普通文本混排。在这两种情况下,源代码都使用 fixed-width font like this 这样的固定宽度字体格式化,以将其与普通文本区分开来。有时代码也会用 in bold 加粗来突出显示与章节中先前步骤不同的代码,例如,当新功能添加到现有代码行时。
在许多情况下,原始源代码已被重新格式化;我们添加了换行并重新调整了缩进,以适应书中的可用页面空间。在极少数情况下,即使这样也不够,列表中还包括了行续续标记(➥)。此外,当代码在文本中描述时,源代码中的注释通常也会从列表中删除。代码注释伴随着许多列表,突出显示重要概念。
您可以从本书的 liveBook(在线)版本中获取可执行的代码片段,网址为 livebook.manning.com/book/evolutionary-deep-learning。书中示例的完整代码可以从 Manning 网站 www.manning.com/books/evolutionary-deep-learning 和 GitHub github.com/cxbxmxcx/EvolutionaryDeepLearning 下载。
liveBook 讨论论坛
购买《进化深度学习》包括免费访问 liveBook,Manning 的在线阅读平台。使用 liveBook 的独家讨论功能,您可以在全局或特定章节或段落中添加评论。为自己做笔记、提问和回答技术问题以及从作者和其他用户那里获得帮助都非常简单。要访问论坛,请访问 livebook.manning.com/book/evolutionary-deep-learning/discussion。您还可以在 livebook.manning.com/discussion 了解更多关于 Manning 论坛和行为准则的信息。
Manning 对我们读者的承诺是提供一个场所,在这里个人读者之间以及读者与作者之间可以进行有意义的对话。这不是对作者参与特定数量活动的承诺,作者对论坛的贡献仍然是自愿的(且未付费)。我们建议您尝试向作者提出一些挑战性的问题,以免他的兴趣偏离!只要这本书还在印刷,论坛和先前讨论的存档将可通过出版社的网站访问。
关于作者

Micheal Lanham 是一位经验丰富的软件和技术创新者,拥有 25 年的经验。在这段时间里,他作为研发开发者,在游戏、图形、网络、桌面、工程、人工智能、GIS 以及为各种行业开发机器学习应用等领域开发了广泛的软件应用。在千年之交,Micheal 开始与游戏开发中的神经网络和进化算法合作。他利用这些技能和经验,作为 GIS 和大数据/企业架构师,增强和游戏化了一系列工程和商业应用。自 2016 年底以来,Micheal 一直是一位热心的作者和演讲者,将他的知识回馈给社区。目前,他已经完成了关于增强现实、音效设计、机器学习和人工智能的众多书籍。他在人工智能和软件开发领域享有众多声誉,但目前专注于生成建模、强化学习和机器学习操作。Micheal 与家人居住在加拿大卡尔加里,目前正致力于撰写、教学和演讲关于人工智能、机器学习操作和工程软件开发。
关于封面插图
《进化深度学习》封面上的图像是“Kourilien 人”,或“库页岛人”,取自 Jacques Grasset de Saint-Sauveur 的收藏,1788 年出版。每一幅插图都是手工精心绘制和着色的。
在那些日子里,仅凭人们的服饰就能轻易地识别出他们居住的地方以及他们的职业或社会地位。Manning 通过基于几个世纪前丰富多样的地域文化的书封面,庆祝计算机行业的创新精神和主动性,这些文化通过像这一系列图片这样的收藏品被重新带回生活。
第一部分:入门
进化算法和遗传算法已经存在了几十年。从计算的角度来看,用于机器学习的进化方法在深度学习面前并不那么强大。然而,进化方法可以为我们提供独特的工具,以协助广泛的优化模式,从超参数调整到网络架构。但在我们讨论这些模式之前,我们需要介绍进化算法和遗传算法。
在第一章中,我们介绍了使用进化方法优化深度学习系统的概念。由于本书中涵盖的深度学习优化方法属于自动化机器学习,我们也介绍了带有进化的 AutoML。
第二章接着介绍了康威生命游戏的生存模拟,使用一个简单的场景,该场景随后通过遗传算法进行进化。从那里,第三章介绍了各种形式的遗传算法,使用 Python 中的分布式遗传算法(DEAP)。最后,在第四章中,我们通过介绍其他多种形式的进化方法来结束章节部分。
1 介绍进化深度学习
本章涵盖
-
进化计算是什么以及它如何集成到深度学习系统中
-
进化深度学习应用
-
建立优化深度学习网络的模式
-
自动化机器学习在优化网络中所扮演的角色
-
将进化计算方法应用于增强深度学习开发的应用
深度学习(DL)已经成为与人工智能(AI)和机器学习(ML)爆炸最相关的普遍技术。它已经从被认为是一种伪科学(参见 Terrence J. Sejnowski 的《深度学习革命》,2018 年,麻省理工学院出版社)发展到被用于从诊断乳腺癌到驾驶汽车的各种主流应用。虽然许多人认为它是一种未来的技术,但其他人则采取更务实和实际的方法来应对其日益增长的复杂性和对数据的需求。
随着深度学习的日益复杂,我们不断地给它喂入更多的数据,希望在某一个特定领域有某种伟大的顿悟。不幸的是,这种情况很少发生,我们经常留下糟糕的模型、糟糕的结果和愤怒的老板。这是一个将持续存在,直到我们开发出高效的深度学习系统流程的问题。
构建有效且健壮的深度学习系统的过程与任何其他机器学习(ML)或数据科学(DS)项目的过程相似——或者应该相似。虽然某些阶段在所需资源和复杂性方面可能有所不同,但所有步骤都将保持不变。在相对较新的深度学习世界中,通常缺乏一个工具包可以帮助自动化这些过程中的某些部分。
进入 进化深度学习(EDL)。EDL 是这样一个工具包或一系列模式和惯例,可以帮助自动化深度学习系统的开发。本书中使用的 EDL 术语涵盖了广泛的应用进化计算方法和模式,应用于机器学习过程中深度学习系统的各个方面。
1.1 什么是进化深度学习?
进化深度学习(EDL),这个术语首次在本书中描述,是对一系列结合进化方法和深度学习的技术的一般分类和分组。这些方法可以用来优化深度学习系统,从数据收集到验证。EDL 并非新事物;将进化方法与深度学习结合的工具已经有许多名称,包括深度神经网络进化、进化神经网络自动机器学习、神经进化、进化人工智能等。
EDL 是人工智能(AI)两个独特子领域的融合:进化计算(EC)和将深度学习(DL)应用于自动化和改进模型。进化计算本身是一系列方法,通过模拟生物或自然过程来解决复杂问题。反过来,这些方法可以应用于深度学习之上,以自动化和优化解决方案,同时具有发现新策略和架构的潜力。
我们将在 EDL 下涵盖的广泛方法绝非新颖,它们已经存在了 20 多年。虽然其中许多研究已经证明在自动调整深度学习模型方面是成功的,但它们在人工智能炒作的更前沿、手工制作的例子之后受到了次要的关注。在许多论文中,作者讨论了进行数据或特征工程以及超参数调整一个创新模型所花费的大量时间。
然而,对于许多现在正在拥抱深度学习的开发者来说,构建稳健、高性能模型是一个艰巨的挑战,充满了困难。许多这些挑战需要对其选择的深度学习框架的所有选项和怪癖有深入和复杂的知识,以便理解模型可能只是错误地拟合。在这里,我们提出了 EDL(进化深度学习)作为自动化机器学习(AutoML)的解决方案,以解决实践者——无论是经验丰富的还是新手——将面临的大部分问题。
EDL 的目的是提供更好的机制和工具集,以提供优化和 AutoML,用于构建深度学习解决方案。进化方法是一种优秀且相对简单的机制,可以提供一套广泛的优化工具,这些工具可以应用于深度学习。虽然进化技术有可能自动化更高级人工智能的构建,但这不是 EDL 或本书的当前意图。
相反,我们专注于使用进化技术构建更好的优化网络。在我们这样做之前,我们将介绍操作并讨论使用进化计算(EC)和进化算法(EAs)来深入了解基本概念,下一节将简要介绍进化和进化过程。
1.1.1 介绍进化计算
进化计算是人工智能的一个子领域,它使用生物和自然启发的过程来解决复杂问题。这个词“进化”用来描述这一系列算法,因为许多算法以自然选择理论为基础。
查尔斯·达尔文在其著作《物种起源》(1859 年,约翰·默里出版社)中提出的自然选择理论,定义了地球上生命的进化过程。它描述了最强壮和最适应的生命将不断增长,而弱小或不适应的生命将死亡并灭绝。他从 1837 年左右在南美洲环航期间作为自然学家在“贝格尔号”上的经历中发展了这一理论。作为虔诚的宗教徒,达尔文在出版著名的作品之前,又与他的发现斗争了 22 年。
基于达尔文的学说,进化计算的一个基石是模拟系统中的个体或个体群体以找到最佳的方法。目的是通过允许它们改变,推导或进化出能够在这样的人工环境中生存和繁荣的个体。这种个体变化的机制将因进化计算方法而异,但在所有情况下,我们都需要一个量化个体生存能力的机制。
我们用来量化个体可能存活或繁荣程度的术语称为适应度。这是一个在 EC 中广泛使用的通用术语,它定义了个体在环境中的生存或表现能力。适应度可以通过多种方式来衡量,但在所有情况下,它都是决定个体或个体群体解决问题效率的终极决定因素。
自然选择和适应度的概念已被用作构建计算方法的基石,这些方法旨在复制生物繁殖过程,无论是粗略地还是深入地。其中一些方法甚至模拟了细胞在染色体分裂和 DNA 共享过程中发生的遗传有丝分裂。以下列表是当前一些显著的 EC 算法的总结:
-
人工生命——从康威的生命游戏和冯·诺伊曼的细胞自动机回溯,这些过程通过代理模拟了生命本身的模拟过程。在这个算法中,代理通常根据它们与其他代理或环境的接近程度移动、流动、生存或死亡。虽然代理模拟通常用于模拟现实世界,但它也可以用于优化过程。
-
差分进化——一个将微分计算与进化算法相结合以优化搜索的过程。这种技术通常与其他 EC 方法,如人工生命,结合使用。在这个算法中,代理通过取向量差异并将其重新应用于群体来进化或改变。
-
进化算法——一个更广泛的 EC 方法类别,它将自然选择的形式应用于问题。这些方法通常侧重于模拟个体群体。
-
进化编程——一种使用代码创建算法的专门进化算法形式。在这个算法中,个体由一段代码表示,其相应的适应度通过运行代码产生的某个最优值来衡量。EP 的代码生成有几种实现方式,在许多情况下,我们将依赖于更具体的方法,如基因表达。
-
遗传算法——这个算法使用我们在生物体中看到的低级细胞有丝分裂,允许将遗传特征传递给后代。遗传算法(GA)是通过将个体的特征编码到基因序列中来模拟这个过程,这个任意的基因序列可能只是一个 0s 或 1s 的序列,它评估为某个适应度指标。这个适应度用于模拟生物选择过程和父代个体的交配,以产生新的结合后代。
-
遗传编程—这个算法使用遗传算法(GA)构建编程代码。在 GA 中,个体的特征更为通用,但在遗传编程(GP)中,一个特征或基因可以代表任意数量的函数或其他代码逻辑。GP 是一种专门的技术,允许开发新的算法代码。这种技术的例子已被用于编写能够解决迷宫或创建图片的代理模拟代码。
-
基因表达式编程—这个算法是遗传编程的进一步扩展,用于开发代码或数学函数。在遗传编程(GP)中,代码被抽象为高级函数,而在基因表达式编程(GEP)中,目的是开发特定的数学方程。GEP 与 GP 之间的一个关键区别是使用表达式树来表示函数。在 GP 中,表达式树表示代码,而在 GEP 中,表达式表示一个数学表达式树。其好处是代码将遵循基于位置的运算顺序。
-
粒子群优化—这属于人工生命的一个子集,是对人工和相对智能粒子的模拟。在这个算法中,评估每个粒子的适应性,最好的粒子成为剩余粒子围绕其群聚的焦点。
-
群体智能—这个算法是一种搜索方法,模拟群体昆虫或鸟类的行为以找到优化问题的峰值。它与粒子群优化(PSO)非常相似,但在实现上有所不同,这取决于适应性评估。
图 1.1 显示了本书中用于应用 EDL 的 EC 方法层次结构。还可以使用其他 EC 方法来改进深度学习模型,但作为介绍,我们将涵盖图中的基本方法,重点关注生命和遗传模拟领域。

图 1.1 用于应用 EDL 的 EC 子集
生命模拟是进化计算(EC)的一个特定子集,它采用了一种模拟我们在自然界中观察到的自然过程的方法,例如粒子或鸟类的群聚方式。另一方面,遗传模拟模仿我们在生物生命中观察到的细胞有丝分裂过程。更具体地说,它模拟了基因和染色体通过生物体的进化进行遗传转移。
1.2 进化深度学习的为什么和在哪里
进化深度学习(EDL)既是一个概念,也是一组用于深度学习优化(DL)的工具和技术。从概念上讲,EDL 是使用进化计算(EC)来优化深度学习网络的模式和惯例。然而,它也提供了一套可以叠加在深度学习之上或甚至作为深度学习替代品的工具。
为什么以及在哪里使用 EDL 不仅取决于你在深度学习(DL)方面的专业知识水平,还取决于你推动极限的需求。这并不意味着深度学习的新手不能从使用 EDL 中受益。实际上,这本书探讨了使用 EDL 暴露出的许多神经网络细微差别,这些差别对任何从业者都有益。
EDL 可以使用的答案很简单:任何地方。它可以用于基本的超参数优化、神经权重搜索以解决不连续的解决方案、平衡生成对抗网络中的对抗网络,甚至可以替代深度强化学习。你真的可以将本书中介绍的技术应用到任何深度学习系统中。
回答 EDL 的 为什么 的问题归结为必要性。进化方法为任何深度学习系统提供了进一步优化或改进解决方案的选项。然而,EDL 计算密集,可能不适合简单的系统。但是,对于复杂或新颖的问题,进化为任何深度学习从业者提供了一套新的技巧。
1.3 深度学习优化的必要性
深度学习是一种强大、但相对较新且常被误解的技术,它提供了众多好处以及一些缺点。其中一个缺点是需要理解和优化模型。这是一个可能需要数小时数据标注或模型超参数调整的过程。
在几乎所有情况下,我们无法直接使用现成的模型,我们通常需要优化深度学习系统的各个方面,从调整学习率到选择激活函数。优化网络模型通常成为主要练习,如果手动进行,这可能需要相当大的努力。
优化深度学习网络可以包括广泛的因素。除了通常的超参数调整之外,我们还需要考虑网络架构本身。
1.3.1 优化网络架构
随着网络通过添加层或各种节点类型而变得更加复杂,它直接影响了损失/误差如何通过它反向传播。图 1.2 展示了在增长更复杂和更大的深度学习系统时遇到的最常见问题。

图 1.2 增长深度学习系统时常见的问题
在更大的网络中,损失量需要分成越来越小的组件,最终接近零。当这些损失组件或梯度接近零时,我们称之为 梯度消失问题,这通常与深度网络相关。相反,组件也可能通过逐层传递而变得异常大,这些层放大了输入信号。这导致梯度组件变得很大,或者称为 梯度爆炸。
这两种梯度问题都可以使用各种技术来解决,例如标准化输入数据和,再次通过层。图 1.2 中展示了称为 标准化 和 dropout 的特殊层函数。这些技术增加了网络的计算复杂性和要求,也可能过分平滑数据中的重要和特征性特征。因此,需要更大的和多样化的训练数据集来开发良好的网络性能。
归一化可以解决深层网络的梯度消失和爆炸问题,但随着模型的增长,这些会引发其他担忧。随着模型的增长,模型处理更大输入集和图像的能力增加。然而,这可能导致称为网络记忆的副作用,如果输入训练集太小,就会发生这种情况。这是因为网络如此之大,它可能开始记忆输入块集或甚至整个图像或文本集。
你可能听说过的尖端深度学习模型,如来自 OpenAI 的自然语言处理器 GPT-3,部分受到记忆问题的困扰。即使向这些模型输入了代表多种文本形式的数十亿份文档,这个问题仍然存在。即使拥有如此多样化和庞大的训练集,GPT-3 之类的模型也已被证明会重放记忆中的整段文本。这个问题可能是一个对不适合深度学习模型的数据库来说有效的特征。
已经开发出了一些针对称为dropout的记忆问题的解决方案,这是一个通过在每个训练过程中关闭网络层中一定比例的节点的过程。通过在每个过程中开关节点,创建了一个更通用的网络。然而,这需要网络现在变得 100%到 200%更大。
在这些问题之上,向更深层的网络添加更多层会增加更多权重——这些权重需要在数十亿到数万亿次的迭代中单独训练。训练此类模型需要指数级增长的计算能力,许多顶级、尖端模型现在仅由能够承担这种高成本的组织开发。
许多人认为更宽、更深的网络趋势很快就会达到大多数深度学习实践者的平台期,将未来的尖端发展留给像 Google DeepMind 这样的 AI 巨头。因此,简单的解决方案是考虑其他可以简化如此大型网络开发的替代方法。这就是我们回到应用 EC 于 DL 以优化网络架构、权重或两者都优化的时候了。
幸运的是,EDL 提供了几种潜在的方法,因为它可以自动优化我们将在本书中探讨的各种问题的网络大小和形式。自动优化是 EDL 的基石,并将成为本书中许多练习的重点,这些练习展示了这些技术。
由于进化算法提供了多种优化模式,可以解决众多问题,因此 EDL 可以在机器学习开发过程的各个方面发挥作用。这包括调整模型超参数以适应数据或特征工程、模型验证、模型选择和架构。
1.4 使用自动机器学习自动化优化
EDL 提供了一套工具,以帮助自动化深度学习系统的优化,以创建更稳健的模型。因此,它应被视为一个 AutoML 工具。许多商业 AutoML 平台,如 Google AutoML,使用各种进化方法来开发模型。
在我们继续之前,我们还需要讨论术语自动化机器学习和AutoML的品牌或误命名问题。在这本书中,我们将交替使用AML和AutoML;它们通常被认为是相同的,并且就我们的目的而言,它们是相同的。然而,AML 和 AutoML 可能被认为是不同的,因为前者通常用来描述一个产生优化模型的黑盒系统。
自动化任何 AI/ML 模型的优化和开发被认为是任何研究和发展项目开发过程中的下一步。它是超越研究和开发并正式化模型构建过程的演变,这使得从业者可以将模型带入全面商业化和产品化。
1.4.1 什么是自动化机器学习?
自动化机器学习,或 AutoML,是一套用于自动化和增强 AI/ML 构建的工具或工具集。它不是一个特定的技术,而是一系列方法,其中进化算法或进化优化方法被视为一个子集。它是一个可以在 AI/ML 工作流程的任何阶段使用的工具,如图 1.3 所示。

图 1.3 使用 AutoML 和/或 EDL 开发良好 AI/ML 模型的步骤
AutoML 工具
以下是一个提供 AutoML 的工具和平台列表:
-
DataRobot—被视为 AutoML 的第一个平台和起点,DataRobot 提供了一套用于自动构建模型的多样化工具。
-
Google Cloud AutoML—这是一个由当前人工智能领域的领军企业推出的流行且稳健的平台。该平台处理各种类型的数据,从图像到结构化数据。
-
Amazon SageMaker AutoPilot—这个强大的平台非常适合自动化依赖于结构化数据的模型开发。
-
H2O AutoML—这个工具提供了各种自动化机器学习工作流程的功能。
-
Azure Machine Learning—这个平台提供了对各种形式数据的模型调优的自动化流程。
-
AutoKeras—这个出色的工具提供了自动开发网络架构的功能。
-
AutoTorch—这个工具提供了自动架构搜索的功能。
许多其他工具和平台都可用,但超出了这个列表的范围。
图 1.3 展示了用于构建用于对新数据进行自信推断的良好模型的典型 AI/ML 工作流程。这个工作流程通常由各种 AI/ML 从业者手动执行,但已经尝试自动化所有步骤。以下是对这些步骤的更详细总结,包括它们如何通过 AML 进行自动化:
-
数据准备—为人工智能/机器学习训练准备数据既耗时又昂贵。一般来说,准备数据和自动化这一任务可以显著提高对微调复杂模型至关重要的数据工作流程的性能。AutoML 在线服务通常假设用户已经根据大多数机器学习模型的要求准备和清理了数据。使用进化方法,有几种自动化数据准备的方法,尽管这项任务并非特定于 EDL,我们将在后面的章节中介绍它。
-
特征工程—这是使用先前的领域知识从数据中提取相关特征的过程,专家根据他们的直觉和经验选择相关特征。由于领域专家昂贵且固执己见,自动化这一任务可以降低成本并提高标准化。根据 AutoML 工具的不同,特征工程可能包含在过程中。
-
模型选择—随着人工智能/机器学习的进步,已经创建了数百种可以解决类似问题的模型类型。通常,数据科学家会花费几天或几周的时间来选择一组模型以进行进一步评估。自动化这一过程可以加快模型开发,并帮助数据科学家确认他们正在使用适合工作的正确模型。一个好的 AutoML 工具可以从数十或数百个模型中进行选择,包括深度学习变体或模型集成。
-
模型架构—根据人工智能/机器学习和深度学习领域,定义正确的模型架构通常至关重要。以自动化的方式正确完成这一点可以减轻无数小时的架构调整和模型重新运行的工作。根据实现方式,一些 AutoML 系统在模型架构上有所不同,但这通常仅限于众所周知的变体。
-
超参数优化—微调模型超参数的过程可能耗时且容易出错。为了克服这些问题,许多从业者依赖直觉和以往的经验。虽然这在过去是成功的,但随着模型复杂性的增加,这项任务变得难以承受。通过自动化 HP 调整,我们不仅减轻了构建者的工作负担,还揭示了模型选择或架构中可能存在的潜在缺陷。
-
验证选择—评估模型性能有许多选项,从决定用于训练和测试的数据量到可视化模型的输出性能。自动化模型的验证提供了一种稳健的方法,在数据变化时重新表征模型性能,并使模型长期更具可解释性。对于在线 AutoML 服务,这是关键优势之一,为采用此类工具提供了令人信服的理由。
典型的自动化机器学习(AML)/自动化机器学习(AutoML)工作流程仅尝试解决特征工程步骤及其之后的步骤,这个过程通常是迭代进行的,无论是单个步骤还是多个步骤的组合。一些步骤,如超参数调整,是特定于模型类型的,在深度学习(DL)的情况下,可能需要大量时间来优化模型。
尽管这股新的商业自动化机器学习(AutoML)服务浪潮在处理各种数据类型和形式方面取得了成功,但产生的模型缺乏创新,并且可能相当昂贵。构建调整后的模型需要大量的计算能力来处理自动化机器学习(AutoML)需要执行的所有任务,而这些开发出的模型基本上是前一代基准的重建,并且通常缺乏对优化的任何新颖见解。
那些希望以预算获得更多创新自动化模型的 AI/ML 从业者通常会转向开发他们的自动化机器学习(AutoML)解决方案,进化深度学习(EDL)是一个主要候选者。正如我们将在本书的后续章节中看到的那样,进化方法可以为自动构建和优化深度学习(DL)模型、超参数、特征工程和网络架构提供各种各样的解决方案。
1.5 进化深度学习的应用
既然我们已经理解了为什么需要将进化计算(EC)和深度学习(DL)结合到自动化机器学习(AutoML)解决方案中,我们就可以继续探讨如何实现。也就是说,我们如何将遗传算法(GAs)等方法应用于深度学习(DL)之上以改进现有的 AI 解决方案?可能存在无数种可能性,允许将进化计算(EC)与深度学习(DL)合并,但在这本书中,我们将坚持一些基本的实用策略。
理解这些策略将使您能够修改现有的深度学习(DL)网络或创建您自己的结合进化计算(EC)/深度学习(DL)的模型。这将使您能够在更短的时间内和更少的资源下创建尖端优化的网络,并赋予您选择和选择策略甚至随着经验增长开发新策略的能力。
为了实现如此宏伟的目标,我们将从深度学习(DL)和进化计算(EC)的一个特定子集的基础知识开始探索。我们将构建基本模型来解决这两个子领域的问题,然后在后面的章节中,我们将探讨如何将它们结合起来以实现更好的性能和自动化。
进化计算(EC)可以以几种形式应用于深度学习(DL),以涵盖自动化机器学习(AutoML)中包裹的各种自动化策略。图 1.4 展示了可以应用于深度学习(DL)的进化计算(EC)或进化深度学习(EDL)的各个子集,以及它们在 AI/ML 模型开发工作流程中可能的应用位置。

图 1.4 将进化计算(EDL)应用于深度学习(DL)的 AI/ML 模型开发工作流程
1.5.1 模型选择:权重搜索
如前所述,所选的基础模型和层类型通常由要解决的问题类型决定。在大多数情况下,模型选择优化可以快速手动完成。然而,模型选择不仅仅是选择层类型;它还可以包括优化形式、起始权重以及用于训练模型的损失。
通过优化模型层类型、优化机制甚至损失形式,可以使网络更鲁棒,从而更有效地学习。我们将探讨一些示例,其中初始模型权重、优化类型和损失度量被调整以适应各种问题。
1.5.2 模型架构:架构优化
许多时候,在构建深度学习网络时,我们常常会过度设计模型或模型中的节点和层数量。随着时间的推移,我们会将网络规模缩小,使其更适合问题。在许多情况下,网络过大可能会导致对输入数据的记忆化,从而引起过拟合。相反,如果网络太小,无法学习数据的多样性和数量,通常会导致欠拟合。
为了解决过拟合和欠拟合问题,我们可以应用遗传算法自动修剪网络到其最低形式。这不仅提高了模型性能并限制了过拟合或欠拟合,而且通过减小网络规模减少了训练时间。这是一种在尝试优化更大、更深网络时效果很好的技术。
1.5.3 超参数调整/优化
超参数 调整 是我们在人工智能和机器学习中所进行的过程,需要通过调整定义模型的各个控制变量来优化模型。在深度学习中,参数用于表示模型的权重;我们通过称控制变量为超参数来区分它们。
EC 提供了几种替代措施,以在广泛的模型中添加自动超参数优化,包括深度学习。粒子群优化、微分进化以及遗传算法都已被成功应用。这些方法中的每一种都将被探索,以在各种框架中衡量性能。
1.5.4 验证和损失函数优化
在开发鲁棒的深度学习模型时,我们通常依赖于几种已建立的模式来生成高质量的网络。这可能包括通过迭代地审查训练和测试损失来验证模型的训练和性能。我们希望确保损失的两个度量指标不要相差太远。
在典型的监督学习训练场景中,我们通常会使用与标签比较相一致的标准度量。在更高级的生成式深度学习场景中,优化损失形式甚至验证度量的机会变得可用。
如自动编码器、嵌入层和生成对抗网络等网络架构提供了应用组合损失和模型验证的机会。使用 EC,我们可以使用方法以 AutoML 的方式优化这些网络形式。
1.5.5 增强拓扑的神经进化
增强拓扑的神经进化(NEAT)是一种将超参数和架构优化与权重搜索相结合的技术,可以自动构建新的 DL 模型,这些模型还可以发展它们自己的损失和验证方法。虽然 NEAT 几乎 20 年前就已经开发出来,但直到最近,这项技术才被应用于 DL 和深度强化学习的各种应用中。
1.5.6 目标
在这本书中,我们探讨之前提到的技术集合以及它们如何应用于 DL。我们关注实用的技术,这些技术可以通过实际解决方案应用于各种问题,特别关注如何将各种形式的 AML/AutoML 也应用于优化 DL 系统和评估技术间的性能。我们的关注范围还包括进化方法之外的更广泛的技术。
在接下来的章节中,我们将逐步介绍 AutoML 过程的部分内容,向熟悉 DL 的人介绍关键概念。在介绍 EC 的基础知识之后,我们将展示超参数优化,然后是数据和特征工程、模型选项选择和模型架构。最后,我们将进一步探讨更复杂的示例,旨在改进生成性 DL 和深度强化学习问题。
到这本书结束时,你应该能够舒适地描述和使用 DL 以及 EC 的某些子集,无论是单独使用还是组合使用来优化网络。你将能够构建模型来解决使用这两个子领域的问题,并理解哪些更适合特定类别的问题,包括在 DL 模型上应用 EC 进行各种优化和应用 AutoML 的能力。
摘要
-
DL 是一种强大的技术,能够解决许多 AI 和 ML 任务,但它很复杂;需要大量的数据;并且开发、训练和优化的成本很高。
-
EC 是 AI 和 ML 的一个子领域,其定义基于自然选择理论。它的发展速度没有 DL 快,但仍然提供了解决各种复杂问题的技术。
-
EDL 是一个广泛的概念,包括进化方法和 DL 的结合。神经进化、进化超参数优化和增强拓扑的神经进化是 EDL 的例子。EDL 定义了 EC 方法的一个子集,可用于自动化和改进 ML 工作流程中许多阶段的 DL 模型开发。
-
AML(自动化机器学习)和 AutoML 定义了一套旨在自动化整个 AI 和 ML 模型开发工作流程的工具和技术。许多形式的进化计算已经被使用,并且可以用于自动化模型开发工作流程。谷歌和其他公司已经大量投资于 AutoML 的开发,以帮助消费者根据自身需求构建稳健的模型。尽管这些服务功能强大,但它们通常像黑盒一样工作,限制了新前沿模型更敏捷的定制化。
2 介绍进化计算
本章涵盖
-
使用 Google Colaboratory 探索生命游戏
-
在 Python 中创建简单的细胞生命模拟
-
通过模拟生命来优化单元格属性
-
将进化论理论应用于模拟
-
将遗传学和遗传算法应用于模拟优化
在上一章中,我们介绍了在深度学习之上或作为其优化的基础上应用进化计算的概念。作为一个通用的总称,我们将这个过程称为 进化深度学习(EDL)。在我们开始探索 EDL 的应用之前,我们首先需要了解进化计算或算法是什么。
同样地,“进化计算”是一个总称,涵盖了从多种形式的生命模拟中借鉴的众多方法,其中进化只是其中之一。在本章中,我们将逐步介绍生命模拟,包括它是什么,它能做什么,以及它如何优化问题。
生命模拟是我们可以使用来探索和优化问题的模拟形式之一。还有许多其他形式的模拟,允许我们更好地模拟过程,从火灾到金融市场等等。然而,它们都有一个共同点:它们的起源都来自康威生命游戏的计算机版本。
2.1 在 Google Colaboratory 上实现康威的生命游戏
生命游戏是由约翰·霍顿·康威在 1970 年开发的一种简单的细胞自动化,这个“游戏”被认为是计算机模拟的诞生。尽管模拟的规则很简单,但它能产生的模式和表现是对其优雅性的惊人证明。
下一个练习也有助于我们介绍 Google Colaboratory,或简称 Colab。Colab 是一个执行所有形式机器学习(从进化计算到深度学习)的优秀平台。它基于 Jupyter Notebook,因此对于大多数有笔记本背景的 Python 开发者来说应该很熟悉。此外,它是免费的,并提供我们后来会大量使用的 CPU 和 GPU 资源。
开始练习,请在浏览器中加载 EDL_2_1_Conways_Game_of_Life.ipynb 练习。有关如何从 GitHub 仓库将代码加载到 Colab 中的详细信息,请参阅附录。
在 Colab 中打开笔记本后,您将看到几个文本和代码单元格。请不要担心这个练习中的任何代码——只需关注如何使用 Colab 执行笔记本和探索结果的操作步骤。
接下来,选择笔记本中的第一个代码单元格,然后在左上角点击运行单元格按钮,或者按 Ctrl-Enter 或 Cmd-Enter 来运行单元格。这将运行代码并设置稍后要使用的 show_video 函数。我们使用此函数来展示模拟的实时可视化输出。
Google Colaboratory:Colab 和实时输出
Colab 是一个出色的平台,也是一个令人难以置信的教育工具,可以快速向学生展示代码。虽然它可以用来快速探索各种任务的代码,但 Colab 的一个缺点是它不提供实时图形渲染输出。为了解决这个问题,我们在本书中使用了几个技巧和技术来可视化实时模拟图形输出。
移动到下一个单元格,该单元格实现了生命的简单规则。同样,我们在这里不探索代码,但图 2.1 以图解的形式解释了康威生命游戏的规则。通过按运行按钮或使用键盘快捷键来运行单元格。
康威生命游戏的规则
生命游戏的优雅之处在于其规则的简单性,这些规则用于模拟细胞模拟。使用了四个简单的规则来模拟或模拟细胞的生存:
-
任何拥有少于两个活细胞的活细胞都会死亡,就像因为人口不足一样。
-
任何拥有两个或三个活细胞的活细胞将存活到下一代。
-
任何拥有超过三个活细胞的活细胞都会死亡,就像因为过度拥挤一样。
-
任何拥有恰好三个活细胞的死细胞会变成活细胞,就像通过繁殖一样。

图 2.1 生命游戏规则
运行下一个单元格,并观察输出,如图 2.2 所示。对于这个简单的生命模拟,起始的细胞模式很简单。还有许多其他起始位置可以产生一些令人惊叹的动画和结构。

图 2.2 起始细胞位置被突出显示
由于我们并不感兴趣去探索代码的其他部分,我们可以简单地通过菜单中的“运行”>“运行所有”或通过按 Ctrl-F9,Cmd-F9 来运行整个笔记本。倒数第二个执行模拟的单元格需要几分钟才能运行,但在此过程中,会显示一个进度条。当模拟完成后,我们最初设置的第一个函数 show_video 会在输出中显示一个简短的视频剪辑。

图 2.3 观看模拟视频
在过程完成后播放视频,并观察细胞模拟的运行情况。视频的摘录如图 2.3 所示,突出了细胞网络可以扩展到多么广阔。
生命游戏的简单性和优雅展示了计算机模拟的力量,并催生了许多学科。它展示了如何使用简单的规则来模拟生命,但也可以根据一些非常基本的规则和输入生成新的和未预见的解决方案。
虽然自从路径生命模拟以来看起来显著不同,但我们通常试图坚持康威在这第一次模拟中赞美的简单原则:推导出一套简单的规则,以模拟更复杂的过程,目的是揭示一些未预见的模式或解决方案。这个目标有助于我们在本章和未来的章节中了解进化计算的方法。
2.2 使用 Python 模拟生命
在我们探讨使用进化或其他方法推导更复杂的生命模拟形式之前,查看一个简单的、人为的实现是有帮助的。我们继续探讨模拟细胞生命,但这次,我们只考虑细胞的属性,忽略物理条件。
地理空间生命和代理模拟
使用空间或空间表示的模拟,如生命游戏,仍然用于进行各种建模和预测,从交通到像 COVID-19 这样的病毒传播。这些类型的模拟可以很有趣去探索和运行,但不会是我们在 EDL 中的重点。相反,我们的空间关注点更侧重于数学驱动,这意味着我们更多地关注分析向量或图距离,而不是物理空间。
在下一个练习中,我们将跳转到 Colab 上的 Python 代码,演示一个简单的细胞生命模拟。请记住,这是一个人为的例子,仅用于展示一些基本概念,以及在某种程度上说明不应该做什么。随着我们进入本章,示例将演变成一个完整的进化方法。
在浏览器中打开笔记本 EDL_2_2_Simulating_Life.ipynb。如需帮助,请参阅附录。
通常情况下,笔记本中的前几个单元格会安装或设置任何额外的依赖项,并执行一般导入。运行单元格以执行导入,如下所示。
列表 2.1 EDL_2_2_Simulating_Life.ipynb:使用 import
import random ❶
import time ❷
import matplotlib.pyplot as plt ❸
from IPython.display import clear_output ❹
❶ 用于生成随机数
❷ 用于跟踪时间和等待
❸ 用于显示图表
❹ 用于清除笔记本单元格输出
移动到下一个笔记本单元格。此代码块设置了一个创建新细胞以及根据所需后代数量生成单元格列表或集合的函数。运行此单元格,您将看到单元格的示例列表,如下所示。
列表 2.2 EDL_2_2_Simulating_Life.ipynb:使用 create_cell 和 birth
def create_cell():
return dict(health = random.randint(1, 100)) ❶
def birth(offspring):
return [create_cell() for i in range(offspring)] ❷
cells = birth(10) ❸
print(cells)
❶ 创建一个具有 1-100 之间随机健康值的单元格
❷ 创建一个大小为后代的细胞列表
❸ 使用出生函数构建单元格列表
下面的列表定义了繁殖和死亡代码/规则。与生命游戏不同,此示例使用一个预定义的参数,称为 RPRD_RATE,来定义新细胞被创建的可能性。同样,代码还根据随机评估检查细胞死亡。
列表 2.3 EDL_2_2_Simulating_Life.ipynb:繁殖和死亡
RPRD_RATE = 25 ❶
DEATH_RATE = 25 ❶
def reproduce(cells): ❷
return [create_cell() for cell in cells if random.randint(1, 100) <
➥ RPRD_RATE]
def death(cells): ❸
return [cell for cell in cells if random.randint(1, 100) > DEATH_RATE ]
def run_generation(cells): ❹
cells.extend(reproduce(cells))
return death(cells)
❶ 定义繁殖和死亡的速率
❷ 对于每个细胞,根据速率繁殖新细胞
❸ 对于每个细胞,根据死亡变化让细胞存活
❹ 运行一代细胞的繁殖和死亡
运行最后一个代码单元格以创建繁殖和死亡函数;这设置了基本的生命模拟函数。在这个阶段,由于我们只是在设置函数,所以不会有任何输出。
接下来,跳转到最后一个单元格。这个单元格执行模拟,我们现在的唯一目标是增加单元格人口,如下所示。
列表 2.4 EDL_2_2_Simulating_Life.ipynb:繁殖和死亡
{top code omitted}
cells = birth(initial_offspring) ❶
history = []
for i in range(generations): ❷
cells = run_generation(cells) ❸
history.append(len(cells))
clear_output() ❹
plt.plot(history) ❹
plt.show()
time.sleep(1) ❺
❶ 创建一个新的细胞列表
❷ 遍历代数数量
❸ 在细胞上运行一代(繁殖/死亡)
❹ 清除输出,并绘制人口更新的历史图表
❺ 睡眠一秒钟,以便可以看到图表
运行这个单元格,并观察模拟的运行。如果繁殖和死亡速率设置正确,人口应该会增加。你可以使用 Colab 表单滑块修改驱动模拟的参数,如图 2.4 所示。你可以返回并更改参数,然后再次运行最后一个笔记本单元格以查看更新的模拟结果。

图 2.4 在 Colab 中运行模拟和更改参数
本练习的目标仅仅是设置一个简单的细胞模拟,并尝试让人口增长。我们定义了速率来控制细胞的繁殖和死亡。这个模拟没有太多的优雅性,但它易于理解和使用。使用下一节的学习练习来理解这个基本的生命模拟。
2.2.1 学习练习
每个部分的结尾都有一个练习集,帮助你回顾各个部分的代码和概念。花时间完成这些练习将极大地帮助你理解未来的概念:
-
修改
代数和初始后代参数以查看这对结果的影响。 -
修改出生率和死亡率以查看这对最终
人口的影响。 -
看看你是否能找到一个繁殖和死亡率,它会导致
人口增长下降。
现在我们已经了解了如何轻松地模拟生命,我们继续了解为什么在特定应用中我们想要这样做。
2.3 生命模拟作为优化
在这种情况下,我们使用我们之前的简单例子,并将其提升到对单元格上定义的属性进行优化的水平。我们可能开发模拟以进行各种形式的行为发现、优化或启迪的原因有很多。对于大多数进化算法的应用,我们的最终目标是优化一个过程、参数或结构。
在这个笔记本中,我们将每个单元格的属性从健康扩展到包括一个名为力量的新参数。我们的目标是优化整个人口的细胞力量。力量参数代表任何使生物在其环境中成功的特征。这意味着在我们的简单例子中,我们的目标是最大化整个人口的力量。
在你的浏览器中打开笔记本示例 EDL_2_3_Simulating_Life.ipynb。如果需要帮助,请查看附录。
我们在这个书中使用了一个有用的实时绘图库,称为 LiveLossPlot,来展示几个示例。这个库旨在用于绘制机器学习和深度学习问题的训练损失,因此默认图表中包含我们在深度学习问题中会使用的术语。尽管如此,它非常适合我们的需求。以下列表展示了安装该包并导入PlotLosses类。
列表 2.5 EDL_2_3_Simulating_Life.ipynb:安装PlotLosses
!pip install livelossplot –quiet ❶
from livelossplot import PlotLosses ❷
❶ 在 Colab 中安装 livelossplot 包
❷ 加载 PlotLosses 类以供后续使用
这个例子中的大部分代码与上一个例子共享,因此我们只需关注这里的差异。从第一个代码单元开始,我们可以看到以下列表中定义生命模拟的函数中的一些变化。最大的变化是我们现在使用新的strength参数来推导细胞的health。
列表 2.6 EDL_2_3_Simulating_Life.ipynb:生命函数更新
def create_cell():
return dict(
health = random.randint(1, 100),
strength = random.randint(1, 100) ❶
)
def birth(offspring):
return [create_cell() for i in range(offspring)]
def evaluate(cells): ❷
for cell in cells:
cell["health"] *= cell["strength"]/100 ❸
return cells
❶ 将强度参数添加到细胞中
❷ 新的评估函数计算细胞健康。
❸ 细胞健康成为强度的函数。
同样,繁殖和死亡函数也被修改,不再随机选择细胞进行繁殖或死亡。相反,新的函数根据健康属性确定细胞是否繁殖或死亡。注意以下列表中新增加了两个参数——RPRD_BOUNDS和DEATH_BOUNDS。这些新参数控制细胞在什么健康水平下可以繁殖或何时应该死亡。
列表 2.7 EDL_2_3_Simulating_Life.ipynb:新的繁殖和死亡函数
def reproduce(cells): ❶
return [create_cell() for cell in cells if cell["health"] > RPRD_BOUNDS]
def death(cells): ❷
return [cell for cell in cells if cell["health"] > DEATH_BOUNDS]
def run_generation(cells): ❸
cells = evaluate(cells) ❹
cells.extend(reproduce(cells))
return death(cells)
❶ 繁殖现在将健康与 RPRD_BOUNDS 进行比较。
❷ 死亡比较细胞健康是否超过 DEATH_BOUNDS。
❸ 细胞健康成为强度的函数。
❹ 添加一个新的评估函数,根据强度更新细胞健康
对于这个模拟,我们根据细胞健康制定了细胞死亡或繁殖的显式规则。记住,我们模拟的目标是优化细胞的population strength属性。
跳转到最后一个代码单元;我们对生成输出做了一些额外的更改,但除此之外,模拟代码基本上保持不变。以下列表中的新代码使用PlotLosses类来输出模拟运行时的实时图表。
列表 2.8 EDL_2_3_Simulating_Life.ipynb:绘制结果
cells = birth(initial_offspring)
groups = {'Population': ['population'], 'Attributes' :
➥ ['avg_strength','avg_health']} ❶
liveloss = PlotLosses(groups=groups)
history = {}
for i in range(generations):
cells = run_generation(cells)
history["population"] = len(cells) ❷
history["avg_strength"] = sum([cell["strength"] for cell in
➥ cells])/(len(cells)+1)
history["avg_health"] = sum([cell["health"] for cell in
➥ cells])/(len(cells)+1)
liveloss.update(history)
liveloss.send() ❸
❶ 设置绘图组以生成输出图表
❷ 更新历史字典以跟踪变量。
❸ 将输出发送到图表
您可以通过菜单中的“运行”>“运行所有”或使用 Ctrl-F9,CMD-F9 来运行整个笔记本。图 2.5 显示了运行 25 代模拟的输出。注意左边的属性图,平均强度和健康都呈上升趋势。

图 2.5 模拟运行输出
通过修改我们的生命模拟代码,我们能够展示单个属性strength的大致优化。虽然我们可以看到population的strength和health属性逐渐增加,但结果并不令人印象深刻。事实上,如果我们的生命模拟要复制现实世界,那么我们很可能永远不会进化成现在的我们。
我们生命模拟中缺失的关键是细胞将它们的成功性状传递给后代的能力。查尔斯·达尔文首先观察到,生命通过他称之为进化的过程将成功性状传递给后代。事实证明,这个进化理论不仅是地球上的生命,也是进化计算的基石。
2.3.1 学习练习
使用这些快速练习来帮助提高你对概念的理解:
-
修改死亡率和出生率参数,看看这对结果有什么影响。
-
修改列表 2.6 中的
evaluate函数,以改变返回的健康参数,然后重新运行模拟并看看有什么影响。 -
修改列表 2.6 中的
create_cell函数中health和strength的起始值。
模拟作为一种优化形式,是一个多样化的领域,但我们在下一节以及本书的其余部分将专注于模拟进化以进行优化。
2.4 将进化添加到生命模拟中
将我们的生命模拟提升到下一个层次需要我们模拟进化。虽然这可能听起来很困难,但实现起来相对简单且优雅。在接下来的练习中,我们借鉴了达尔文和其他人的许多观察结果来构建我们的升级版生命模拟。
2.4.1 模拟进化
再次,在这个练习中,我们借鉴了前一个练习的大部分代码,修改它来模拟进化或细胞传递选择性性状的能力。然而,这一次,我们不是使用像力量这样的单一性状,而是分配了三个新的性状,分别标记为a、b和c。此外,我们将health性状替换为一个更通用的术语,称为fitness。
在浏览器中打开笔记本示例 EDL_2_4_Simulating_Evolution.ipynb。如果你需要帮助做这件事,请查阅附录。
这段代码有几个升级,我们将详细检查,首先是更新的create_cell函数。在这里,重要的是要注意,该函数现在接受两个输入细胞或两个细胞来产生一个后代。例如,如果在模拟开始时没有亲本,那么性状将被设置为随机值。如果有亲本,那么每个性状的平均值将成为孩子的新的值,如下面的列表所示。请记住,这种平均机制只是创建新孩子性状值的一种可能性。
列表 2.9 EDL_2_4_Simulating_Evolution.ipynb:更新create_cell
def create_cell(parent1, parent2): ❶
if parent1 is None or parent2 is None:
return dict(
fitness = 0, ❷
a = random.randint(1, 100), ❸
b = random.randint(1, 100),
c = random.randint(1, 100)
)
else:
return dict(
fitness = 0, ❷
a = (parent1[“a”] + parent2[“a”])/2, ❹
b = (parent1[« b »] + parent2[« b »])/2, ❹
c = (parent1[« c »] + parent2[« c »])/2, ❹
)
❶ 现在需要两个亲本细胞来繁殖。
❷ 适应性始终从 0 开始。
❸ 如果没有亲本,性状将初始化为随机值。
❹ 新的性状值是两个亲本的平均值。
接下来,我们查看更新的 reproduce 函数。这里有一些变化。首先,我们按 fitness 对父母细胞进行排序,然后通过一个称为 selection 的过程选择上半部分。其次,我们对剩余的父母进行两次循环(每个父母两个子代)并随机选择两个进行交配。然后,这两个父母被传递给 create_cell 以产生具有来自两个父母共享特征的新的子代。最后,细胞通过一个新的 mutate 函数,然后返回。下面列表中使用的繁殖 selection 形式只是一个例子;我们将看到,这里有许多变体。
列表 2.10 EDL_2_4_Simulating_Evolution.ipynb:更新 reproduce
def reproduce(cells):
parents = sorted(cells, key=lambda d:
➥ d[‘fitness’])[int(len(cells)/2):] ❶
children = []
for I in range(len(parents)*2): ❷
mates = random.sample(parents, 2) ❸
children.append(create_cell(mates[0], mates[1])) ❹
return mutate(children) ❺
❶ 按 fitness 排序父母并选择上半部分
❷ 对剩余的父母进行两次循环
❸ 随机选择两个父母进行繁殖
❹ 将父母传递给 create_cell 以产生子代细胞
❺ 在返回列表前对子代进行突变
在 reproduce 中的最后一步是调用 mutate 函数,如下面的列表所示,它有一个随机机会修改子代。我们添加这个函数或规则来模拟生活中生物(细胞)可能超出其父母特征的 mutate 随机性。突变是进化中的一个关键因素,并负责地球上所有高级生命形式。
列表 2.11 EDL_2_4_Simulating_Evolution.ipynb:mutate 函数
def mutate(cells):
for cell in cells:
if random.randint(1,100) < MUTATE_RATE: ❶
cell“"”"] = clamp(
cell“"”"] + random.randint
➥ (-MUTATE_RNG, MUTATE_RNG), 1, 100) ❷
cell“"”"] = clamp(
cell“"”"] + random.randint
➥ (-MUTATE_RNG, MUTATE_RNG), 1, 100) ❷
cell“"”"] = clamp(
cell“"”"] + random.randint
➥ (-MUTATE_RNG, MUTATE_RNG), 1, 100) ❷
return cells
❶ 检查细胞突变的随机机会
❷ 添加来自 -+ MUTATE_RNG 的随机数
接下来,我们要查看更新的 evaluate 函数。这次我们使用一个简单的方程来 evaluate 特征 a、b 和 c 的值,该方程输出细胞的 fitness。我们可以看到这个函数将两倍的价值放在特征 a 上,对特征 b 附加一个负值,而特征 c 保持不变,如下面的列表所示。我们进化生命模拟的目标现在是通过优化这些特征来保持高 fitness。更高的 fitness 有助于提高繁殖的可能性,从而鼓励那些成功的特征得到进一步的传承。
列表 2.12 EDL_2_4_Simulating_Evolution.ipynb:mutate 函数
def evaluate(cells):
for cell in cells:
cell“"fitnes”"] = 2 * cell“"”"]–- cell“"”"] + cell“"”"] ❶
return cells
❶ 更新的 evaluate 函数
注意,我们移除了 death 函数,而是专注于 reproduce 函数。我们可以这样做,因为我们现在简单地假设繁殖后,所有父母都无法进一步繁殖;因此,这不是一个考虑因素。因此,我们不再关心 population 的增加,而是关注繁殖 population。这个假设简化了我们的过程和模拟的性能,并且是我们继续在大多数情况下使用的一个假设。显然,你也可以模拟跨多代的繁殖,但我们现在认为这是一个高级主题。
最后,我们来看看run_generation函数,看看它是如何简化的。在函数内部,第一次调用是evaluate,它更新细胞的fitness。接下来,调用reproduce函数来产生下一个繁殖generation。之后,我们再次在新的generation上调用evaluate函数来更新fitness值,如下所示。
列表 2.13 EDL_2_4_Simulating_Evolution.ipynb:run_generation函数
def run_generation(cells):
cells = evaluate(cells) ❶
cells = reproduce(cells) ❷
cells = evaluate(cells) ❶
return cells
❶ 评估当前和新生代的fitness
❷ 产生一个新的繁殖generation
图 2.6 显示了运行所有代码的输出(从菜单中选择“运行”>“运行所有”或按 Ctrl-F9,CMD-F9)。注意图 2.5 和 2.6 之间的明显差异,其中fitness有明显的改进,但population保持在 10。同时注意特性a、b和c都显示出良好的定义优化。在特性a的情况下,我们看到一个明显的增加,而在特性b的情况下,我们看到一个减少。这是evaluate函数和我们定义这些特性在fitness方程中的方式的结果。

图 2.6 运行进化生命模拟的结果
我们可以看到,通过将进化的概念添加到生命模拟中,我们能够看到fitness和特性优化之间的强烈相关性。这不仅使修改后的模拟更加优雅,而且更加健壮和可扩展。事实上,如此优雅,简单的进化概念现在成为了一类算法的骨干,其中许多我们在后面的章节中进行了探讨。
2.4.2 学习练习
使用这些练习来提高你的理解:
-
修改 2.12 列表中所示的
evaluate函数中的fitness计算。重新运行进化过程以确认新方程优化了不同的值。 -
向细胞添加一个新的属性
d。这需要你修改 2.9、2.11 和 2.12 列表。 -
将
mutation率MUTATE_RATE更改为0和1之间的新值。尝试几次,然后在每次更改后重新运行笔记本。观察mutation对细胞进化的影响。
2.4.3 关于达尔文和进化的背景知识
查尔斯·达尔文从他在南美洲大陆的航行中形成了他的初步概念和自然选择理论。从达尔文的工作中,我们对理解进化的渴望驱使我们探索地球上的生命如何通过遗传学共享和传递选择性特性。
经过二十年的写作,达尔文在 1859 年发表了其最著名的工作,《物种起源》(由约翰·默里出版),这是一部开创性的作品,颠覆了自然科学。他的工作挑战了智能创造者的观点,并成为我们今天自然和生物科学的基础。书中以下引用描述了达尔文对自然选择的看法:“不是最强的物种能够生存下来,也不是最聪明的物种,而是那些最能适应变化的物种。”
从这个定律出发,达尔文构建了他的进化理论,以及生命为了生存而将更成功的特征传递给后代的必要性。尽管他并不理解细胞有丝分裂和遗传学的过程,但他观察到了多个物种中特征的选择性传递。直到 1865 年,一位名叫格雷戈尔·孟德尔的德国僧侣才会通过观察豌豆植物的七个特征来概述他的基因遗传理论。
孟德尔使用因子或特征来描述我们现在所理解的基因。他的工作几乎又过了三十年才得到认可,遗传学领域才诞生。从那时起,我们对遗传学的理解已经扩展到包括基因治疗和破解复杂问题以及进化代码等领域。
2.4.4 自然选择和适者生存
“适者生存”这个术语经常被用来定义进化以及随后的进化计算。虽然这个术语通常被错误地归功于达尔文,但它最初是由一位早期的自然学家赫伯特·斯宾塞使用的,他比达尔文早 7 年提出了这个短语。斯宾塞,一个误入歧途的社会进化论者,会继续成为达尔文及其对进化的解释的批评者。
定义 社会达尔文主义——通常归功于赫伯特·斯宾塞的观点,即社会成功孕育成功,而那些在社会上失败的人天生注定要失败。
斯宾塞和其他人从达尔文的更大进化理论中遗漏的是,生存只是变化的一个后果。达尔文很好地解释了这个概念:“不是最强的物种能够生存下来,也不是最聪明的物种,而是那些最能适应变化的物种。”
在我们阅读这本书的章节时,没有什么比记住达尔文的这句话更好的想法了。进化不是关于发展最强壮或最适应的物种,而是关于能够最好地适应变化的物种。在实践意义上,这意味着虽然我们专注于开发产生最多适应性的算法,但我们的真正目标是发展进化变化。
在计算中,进化变化通过确保不仅仅是最强壮或最好的个体能够存活下来,被应用于我们的算法中。这意味着我们采用方法确保一个种群中的个体不仅仅是最好的,而是最多样化的。鼓励种群中的多样性通常能让我们更快地解决问题。
生物学应用于进化计算
进化计算借鉴了生物学和进化理论。并且像 DL(神经网络)与大脑的比较一样,并非所有术语都可以转移。在几个案例中,尝试使用与生物等效物相似或匹配的术语。在许多情况下,生物学术语已经被显著简化,以便更容易理解。这样做不是为了激怒生物学家、遗传学家或进化论者,而是为了让术语更容易接近。
2.5 Python 中的遗传算法
遗传算法(GAs)是代码中生命的模拟,借鉴了进化、自然选择和通过遗传传递成功特性的概念。这些算法模拟了在高级有机繁殖中发生的生物细胞级别的减数分裂。虽然你不需要成为遗传学家就能使用遗传算法,但了解生物关系可能会有所帮助。
在下一节中,我们回顾了一些遗传学的重要基本概念和减数分裂的过程。这是为了展示代码中遗传的关系和模仿。当然,如果你已经在遗传理论和减数分裂方面有很强的基础,你可以快速浏览这一节。
2.5.1 理解遗传学和减数分裂
遗传算法模拟了遗传层面的生命进化。然而,这种模拟更具体地针对高级生命形式——就像我们。我们在遗传过程(减数分裂)中也做了几个简化。因此,本节中涉及的概念旨在达到相同的高级水平。
每当我们谈论遗传学时,我们都需要从脱氧核糖核酸(通常称为 DNA)开始。DNA 链通常被称为生命的蓝图。关于我们的一切,包括我们的细胞,都在我们的 DNA 中定义。
DNA 本身由四种碱基对组成,这些碱基对排列成一定的模式。图 2.7 展示了 DNA 的形成和缠绕成双螺旋结构,然后折叠成染色体。这些染色体位于每个细胞的细胞核中,如图所示。

图 2.7 DNA、基因、染色体、细胞核和细胞
基因,那些孟德尔最初定义的事物,可以在 DNA 层面上被识别。基因是一段 DNA 序列,它定义了生物体的某些特征或属性。从 1990 年到 2003 年,人类基因组计划研究了并分类了我们染色体内的所有基因。
如图 2.7 所示,染色体是这些基因序列的容器。单个染色体可能包含数百或数千个基因。每个基因本身可能由数百到数千个 DNA 碱基对组成。这一切听起来相当复杂,但幸运的是,在遗传算法(GA)中,我们只关心基因和染色体。
遗传进化的模拟本身是通过模拟减数分裂的过程来完成的。减数分裂是精子和卵子细胞的性繁殖过程,不要与有丝分裂混淆,后者是基本细胞分裂的过程。
减数分裂是半个生物体的遗传物质与另一个生物体的一半遗传物质结合的过程。在人类中,这是精子与卵子的故事,其中男性将其一半的 DNA(精子细胞)与女性的一半 DNA(卵子)结合。
图 2.8 展示了减数分裂过程的一个片段,其中交配生物的染色体被结合。在这个过程中,同源染色体对(即相似的染色体)首先对齐。然后,发生交叉,即遗传物质的共享。由此产生的重组染色体被用来定义新的生物体。

图 2.8 细胞减数分裂过程的一个片段,展示了染色体的交叉
在遗传算法(GAs)中,我们在细胞层面上模拟基因、染色体以及这种交配或交叉过程。我们还需要模拟其他一些因素,这些因素将在下一节中介绍。
2.5.2 编码遗传算法
遗传算法(GA)的核心是描述个体所具有的各种特征(无论是好是坏)的基因。在 GA 中,我们认为一个个体由一个或多个包含在染色体内的基因序列组成。我们也可以模拟多个染色体,但通常我们只使用一个。
图 2.9 显示了一个个体的种群,每个个体在染色体中都有一个基因序列。每个基因由一个数字或布尔值描述,代表此例中的 0 或 1。一个基因可以包含任何信息,包括文本字符、颜色或您想要用来描述个体特征的任何其他信息。

图 2.9 遗传算法中的种群、基因和染色体
基因和染色体
一个基因可以映射到一个单一的数组值,或者可以由多个值定义。同样,您可能想要定义一个单一的染色体或多个。在大多数情况下,本书中我们只定义一个染色体。
2.5.3 构建种群
遗传算法(GA)可能相当抽象且难以想象,因此为了帮助理解,在本节中,我们将通过一些 Python 示例代码来阐述这些概念,这可能会使这些概念更加具体。您可以通过在浏览器中打开 Google Colab 中的笔记本 EDL_2_5_GeneticAlgorithms.ipynb 来跟随操作。如果需要帮助加载此笔记本,请参考附录。当笔记本加载完成后,使用菜单选择运行 > 运行所有来运行所有单元格。
我们可以从笔记本中的第一个代码单元格开始,它使用 NumPy 数组设置了一个个体的种群。种群中的每个人都是由一个大小为基因的单一n-维向量组成的。整个种群通过randint函数构建成一个 NumPy 张量,输入为0,2,张量的大小为(种群,基因)。这产生了一个输出张量,其中每一行代表一个大小为基因的向量,如下所示。
列表 2.14 EDL_2_5_Genetic_Algorithms.ipynb: 创建种群
population = 100 ❶
genes = 100 ❷
generations = 100 ❸
pop = np.random.randint(0,2, size=(population,genes)) ❹
print(pop)
========================
[[0 1 1 ... 1 0 0] ❺
[1 1 0 ... 1 1 0]
[1 0 0 ... 1 0 1]
...
[1 0 1 ... 0 0 1]
[1 0 0 ... 0 1 0]
[1 0 0 ... 0 0 1]]
❶ 整个种群中个体的数量
❷ 一个个体中的基因数量
❸ 评估的代数数量
❹ 创建一个初始随机种群,基因值为 0 或 1
❺ 打印输出的输出,显示数组中每个个体的基因序列
2.5.4 评估适应性
在一个个体种群中,我们想要确定哪个是最适应的或最有可能生存或解决问题的。在这个简单的例子中,我们的目标是进化个体,因此所有基因的值都是1。这被称为遗传算法中的最大一问题,是向新入门者介绍的一个常见问题。
为了确定一个个体的适应性,我们通常推导出一个适应性函数或计算方法,以确定一个个体距离达到目标目标有多近。通常,这个目标是最小化或最大化一个目标值。在这个例子中,我们的目标是最大化一个个体中所有基因的总和。由于每个基因只是一个0或1,最大化的总和代表一个所有基因都设置为1的个体,如列表 2.15 所示。
使用 NumPy,如果我们已经在张量中定义了种群,那么执行这一操作的代码非常简单,只需一行代码即可。滚动到笔记本中的下一个单元格,你可以看到调用np.max函数,它以种群``pop张量作为输入,并设置axis=1。下面的代码列表展示了如何通过使用np.sum来计算适应性。
列表 2.15 EDL_2_5_Genetic_Algorithms.ipynb: 计算适应性
fitness = np.sum(pop,axis=1) ❶
plt.hist(fitness)
❶ 所有个体的总和(轴 1)
图 2.10 显示了种群初始随机个体适应性的直方图输出。正如我们可能预期的那样,输出类似于值的大致 50 左右的正态分布。在这个例子中,由于每个人都有一个包含100个基因的单个染色体,每个基因的值为0或1,因此最大理想的适应性分数是100。

图 2.10 初始种群适应性的直方图
2.5.5 选择繁殖(交叉)
在评估了种群的适应性之后,我们可以确定哪些父母用于交配以产生后代。就像在现实生活中一样,我们模拟了个体的交配选择和繁殖。在自然界中,我们通常看到强壮的或适应性更强的个体生存并繁殖,产生具有部分遗传代码的后代。
在遗传算法中,我们通过首先确定种群中哪些个体足够适应以产生后代来模拟这个过程。我们可以使用几种策略来进行这种选择,但在这个简单的例子中,我们选择两个最适应的个体作为下一代的父母。这种选择形式被称为精英选择,执行它的代码如下所示。
列表 2.16 EDL_2_5_Genetic_Algorithms.ipynb:选择最适应的
def elite_selection(fitness):
return fitness.argsort()[-2:][::-1] ❶
parents = elite_selection(fitness)
print(pop[parents[0]])
❶ 按照适应性排序,然后返回前两个个体
elite_selection 函数接受我们之前计算的 population fitness 作为输入,并返回前两个父代的索引。它是通过使用 argsort 函数对 fitness 值进行排序,然后索引到前两个父代以返回索引。这些返回的索引可以用来通过 pop[parents[idx]] 从 population 中提取 individuals,其中 idx 为 0 或 1。
对于这个简单的例子,精英选择,即选择最佳的 individuals 进行繁殖,效果很好,但在更复杂的问题中,我们通常使用更多样化的 selection 方法。父母和配对 selection 中的多样性允许 individuals 传播那些在短期内可能不利的特征,但可能发展为长期解决方案。这类似于求解全局最大值并陷入局部最小值。
2.5.6 应用交叉:繁殖
在选择父母后,我们可以继续应用 crossover 或本质上创建子代的繁殖过程。在生物学中的细胞分裂过程中,我们通过 crossover 操作模拟 chromosomes 的结合,其中每个父代分享其 gene 序列的一部分并与另一个父代结合。
图 2.11 显示了使用两个父代应用 crossover 操作。在 crossover 中,随机选择或使用某种策略沿着 gene 序列选择一个点。在这个点上,父代的 gene 序列被分割并重新组合。在这个简单的例子中,我们不在乎每个子代与 gene 序列共享的百分比是多少。

图 2.11 对父母应用 crossover 操作以产生子代
对于需要成千上万或数百万 generations 的更复杂问题,我们可能更喜欢更平衡的 crossover 策略,而不是这种随机的 selection 方法。我们将在本章后面进一步介绍我们可以使用的策略来定义此操作。
在代码中,crossover 操作首先复制自身以创建原始子代。然后我们使用变量 crossover_rate 随机确定是否存在 crossover 操作。如果存在 crossover 操作,则生成一个沿 gene 序列的随机点作为 crossover 点。这个点用于分割 gene 序列,然后通过以下列表将两个父代的 gene 序列组合起来生成子代。
列表 2.17 EDL_2_5_Genetic_Algorithms.ipynb:Crossover 和繁殖
def crossover(parent1, parent2, crossover_rate):
child1, child2 = parent1.copy(), parent2.copy() ❶
if random.random() < crossover_rate: ❷
pt = random.randint(1, len(parent1)-2) ❸
child1 = np.concatenate((parent1[:pt], parent2[pt:])) ❹
child2 = np.concatenate((parent2[:pt], parent1[pt:])) ❹
return [child1, child2]
crossover(pop[parent[0]],pop[parent[1]], .5) ❺
❶ 子代最初是父母的副本。
❷ 随机允许交叉操作。
❸ 随机选择交叉点
❹ 交叉并创建子代。
❺ 使用父代 1 和 2 调用函数
在基因序列中,交叉可以以多种变体和方式应用。对于这个例子,选择一个随机的交叉点,然后简单地结合分裂点的序列即可。然而,在某些情况下,特定的基因序列可能或可能没有意义;在这些情况下,我们可能需要其他方法来保留基因序列。
2.5.7 应用变异和变体
在自然界中,我们偶尔会看到后代发展出父母双方都不具备的特征。在这些情况下,后代发生变异,导致出现父母双方未见过的特征。随着时间的推移,这些变异可以累积,创造出全新的特征或个体物种。变异是我们认为生命从单细胞生物进化到人类的关键操作。
在自然界中,遗传算法(GAs)和其他类似的进化过程中,变异通常是独特且罕见的。使用遗传算法,我们可以在交叉操作之后控制变异的数量和类型。因此,你可以将变异视为在笨拙的繁殖过程中可能出现的潜在奇怪产物。
在列表 2.18 的后代中应用变异操作就像翻转序列中的单个比特或基因一样简单。在变异函数中,对个体中的每个基因都进行了变异可能性的测试。为了测试函数,我们使用.5的变异率,即 50%,尽管通常变异率要低得多——低于 5%。
列表 2.18 EDL_2_5_Genetic_Algorithms.ipynb: 变异
def mutation(individual, mutation_rate):
for i in range(len(individual)): ❶
if random.random() < mutation_rate: ❷
individual[i] = 1 - individual[i] ❷
return individual
mutation(pop[parent[0]], .5)
❶ 我们对所有基因进行可能的变异测试。
❷ 如果进行变异,将基因 0 -> 1,1 -> 0。
同样,与选择和交叉的遗传操作一样,变异也可以采取多种形式。在某些情况下,你可能更喜欢保持变异的可能性低,而在其他情况下,一个种群可能从更多的随机影响中受益。变异类似于深度学习中的学习率,较低的学习率会导致更稳定的训练,但可能会陷入停滞,而较高的学习率会产生良好的初始结果,但可能永远不会稳定到解决方案。
2.5.8 整合所有内容
最后,当我们把所有的遗传操作整合在一起时,我们得到图 2.12 所示的流程图,它展示了整个遗传算法过程。在这个图中,我们从初始化开始,在我们的例子中,这是完全随机的。然后,第一个操作是计算所有个体的适应度。从适应度中,我们可以确定哪些个体将通过交叉操作繁殖后代。

图 2.12 遗传算法过程
在应用交叉操作后,应用变异,然后评估适应度。接下来,我们检查是否满足停止条件。通常,我们通过 GA 运行的代数来定义停止条件,其中每一代都被计为一个完整的 GA 流程。我们也可以使用其他停止条件,如达到最大或最小适应度。
我们可以将所有遗传算法(GA)的过程代码放入一个单独的函数中,如simple_GA函数所示。在这个函数中,我们可以看到对种群应用的每个遗传操作,这导致了新一代的子代。如下面的代码列表所示,这个子代种群被返回,以供进一步评估,并作为新一代传递给simple_GA。
列表 2.19 EDL_2_5_Genetic_Algorithms.ipynb:完整的 GA 过程
def simple_GA(pop, crossover_rate=.5, mutation_rate=.05):
fitness = np.sum(pop,axis=1) ❶
parents = elite_selection(fitness) ❷
children = np.zeros((population,genes)) ❸
for i in range(population): ❹
offspring = crossover(pop[parents[0]],
➥ pop[parents[1]], crossover_rate) ❺
children[i] = mutation(offspring[0],mutation_rate) ❻
return children
simple_GA(pop) ❼
❶ 计算整个种群的适应度
❷ 执行选择以选择父母
❸ 创建一个所有元素为 0 的空子代种群
❹ 遍历整个种群以创建新的子代
❺ 应用交叉
❻ 应用变异
❼ 每次调用代表一代。
这个单独的函数simple_ga代表对种群或个体的代的所有遗传操作的一个完整过程。我们可以使用笔记本中最后一个代码块中的代码来评估连续的代。如果笔记本已经完成训练,再次运行最后一个单元格,这允许你看到种群是如何进化的。下面的代码列表演示了模拟每个代进化的循环。
列表 2.20 EDL_2_5_Genetic_Algorithms.ipynb:运行模拟
pop = np.random.randint(0,2, size=(population,genes)) ❶
for i in range(generations): ❷
pop = simple_GA(pop)
fitness = np.sum(pop,axis=1)
plt.hist(fitness)
plt.show()
print(f"Generation {i+1}")
print(f" Max fitness {np.max(fitness)}")
print(f" Min fitness {np.min(fitness)}")
print(f" Mean fitness {np.mean(fitness)}")
print(f" Std fitness {np.std(fitness)}")
❶ 创建一个初始随机种群
❷ 遍历代数并处理 GA
图 2.13 显示了通过 100 代进化种群的结果。图中显示达到了98的适应度,最小适应度为88,平均为93.21,标准差为2。这些结果通常都是好的,并且与深度学习(DL)不同,在深度学习中,我们关注的是最大或最小损失或准确度,而在遗传算法(GA)中,我们希望确定整个种群的进步情况。

图 2.13 在单最大问题上的种群进化结果
虽然单个个体的适应度可以解决一个困难的问题,但确保整体保持一个健康的种群可以允许持续进化。与深度学习(DL)不同,在深度学习中,训练进度可能会随时间放缓,在遗传算法(GA)中,进化过程中往往会有晚期的突破,导致解决方案的激进变化和进步。因此,当我们使用进化算法时,通常希望考虑整个种群的适应度。
适应性生存
记住,我们训练进化算法的目标始终是确保种群能够适应变化。这意味着我们通常希望看到个体的种群得分一个正常的适应度分布。我们可以通过选择和突变操作符的类型和形式来控制这种适应变化。
2.5.9 理解遗传算法超参数
正如你可能已经注意到的,GA 为我们提供了几个超参数和遗传操作符选项,以优化解决方案的进化。我们在本章中探讨了各种操作符选项,因为这些选项对于理解我们可以使用哪些超参数来增强进化至关重要。以下是我们迄今为止探索的遗传超参数列表以及它们的工作方式和用途:
-
种群—这代表通过每一代进化模拟的
个体数量。种群值与染色体的大小或基因序列长度密切相关。因此,具有更复杂基因序列的个体需要更大的训练种群才能有效。 -
基因/染色体长度—
染色体的数量和长度或基因的类型通常由问题设定。在先前的示例练习中,我们选择了一个任意的基因数量值来展示不同的基因序列长度。 -
代数—类似于深度学习中的 epoch,
代数的数量代表进化的迭代次数。训练一词保留用于个体的改善,在 GA 中,我们进化整个物种或个体的种群。与种群一样,代数的数量通常由染色体长度和复杂性决定。这可能与种群大小相平衡,你可能有大型种群和少量代数。 -
交叉率—这可能决定了交叉的可能性,或者它可能规定了交叉的点或量。在最后的示例中,我们使用这个比率来确定父母共享
基因的频率。然而,在大多数情况下,交叉是假设的,比率然后可能决定交叉点。 -
突变率—这解释了在
交叉混合过程中可能出现错误的可能性。高突变率通常会导致种群中产生大量变异,这可能对更复杂的问题有益。然而,高突变率也可能阻止个体达到最佳性能。相反,较低的突变率会产生较少的种群多样性和更多的专业化。
在这一点上,了解这些超参数在实际中如何工作的一个好方法是回到最后的示例并更改它们,然后重新运行笔记本。请尝试这样做,因为这确实是学习和理解这些基本值如何改变种群进化的最佳方式。
遗传算法(GAs)为我们探索的以下章节中的几种进化计算(EC)方法奠定了基础。从根本上说,进化和适者生存的概念是任何 EC 方法的关键组成部分。我们在寻找优化深度学习(DL)系统更好方法的过程中,使用了达尔文 170 多年前提出的这些普遍规律。
2.5.10 学习练习
在本节中,我们已经涵盖了大量的基础材料。请确保至少完成以下练习之一:
-
修改列表 2.15 中的
适应度计算。看看这会对进化产生什么影响。 -
修改列表 2.19 中的
交叉和变异率。重新运行进化过程,看看改变每个参数对进化速度的影响。 -
你能想到其他父母选择配偶的方式吗?把它们写下来,稍后再回顾这个列表。
概述
-
康威生命游戏(Conway’s Game of Life)展示了基于规则的生命模拟的第一种基本形式。生命模拟可以帮助我们优化计算和模拟的实际情况问题。
-
可以使用函数定义繁殖和死亡来观察简单的行为。
-
通过基本生命模拟实现的进化可以展示成功性状传递给后代。通过进化传递的性状可以用来优化特定问题。
-
优化问题的进化成功是通过一个
适应度函数来衡量的。适应度函数量化了模拟的个体成功解决给定问题的能力。 -
使用 NumPy 的 Python 可以用来演示模拟遗传进化的基本概念或操作。在遗传进化(GAs)中,我们使用算子来模拟生物减数分裂或高等生物繁殖的基本操作。遗传模拟中使用的基操作是
选择、交叉、变异和评估/适应度:-
选择—这是选择
个体进行繁殖的阶段或操作。在遗传算法(GAs)中使用了多种选择方法。 -
交叉—这是两个选定的
个体交配并共享部分遗传材料的阶段或操作。 -
变异—为了模拟现实世界的生物过程,我们在前一个
交叉操作产生的后代中应用一定程度的随机化。 -
评估—通过一个函数对新生成的
个体进行评估,以产生一个适应度分数。这个分数决定了个体完成某些问题或任务的成功程度。
-
-
遗传进化的基本算子的输入和配置可以调整和修改。我们通常会修改以下典型配置参数:
-
种群大小—在一代中模拟的
个体数量。 -
代数数量—模拟的迭代次数。
-
交叉率—在
交叉操作期间个体共享遗传材料的频率。 -
突变率—新个体将遭受其遗传材料随机改变的频率。
-
3 使用 DEAP 介绍遗传算法
本章涵盖
-
使用 DEAP 创建遗传求解器
-
将 GA 应用于复杂的设计或放置问题
-
使用 GA 解决或估计数学难题
-
在解决问题时确定要使用的 GA 算子
-
构建复杂基因结构进行设计和绘图
在上一章中,我们探讨了生命模拟的起源以及如何利用进化和自然选择进行优化。我们学习了遗传算法,作为进化计算的一个子集,如何将这些概念进一步扩展为一个优雅的实用搜索优化方法。
在本章中,我们直接将上一章学到的知识扩展到使用遗传算法解决更大、更复杂的问题。作为这一旅程的一部分,我们采用了一个名为 Python 中的分布式进化算法(DEAP)的 EC 工具包,以使我们的工作更轻松。像 Keras 或 PyTorch 这样的框架一样,DEAP 提供了几个工具和算子,使编码更容易。
3.1 DEAP 中的遗传算法
尽管我们可以继续用纯 Python 编写我们需要的所有 GA 代码,但这本书不是关于构建 EC 框架的。相反,在本章中,我们使用成熟的 DEAP EC 框架。正如其名所示,这个框架帮助我们回顾各种 EC 方法,包括 GA。
DEAP,于 2009 年发布,是一个全面且简化的框架,用于以各种形式处理 EC 算法。在整个书中,它是我们构建 EDL 解决方案的主要工具。该框架提供工具抽象,使其能够与各种进化算法实现跨兼容。
3.1.1 使用 DEAP 实现一个最大值问题
没有比使用它来解决我们在上一章中使用纯 Python 和 GA 解决的问题更好的方式来了解 DEAP 了。这使我们能够熟悉它所使用的框架和工具包。在接下来的练习中,我们使用 DEAP 构建一个求解一个最大值问题的求解器。
在 Colab 中打开 Open EDL_3_1_OneMax_DEAP.ipynb,然后运行所有单元格。如需帮助,请参阅附录。
在第一个单元格中,我们使用以下 shell 命令安装 DEAP。!前缀表示这是一个 shell 命令,而不是 Python 代码。我们使用pip安装 DEAP,使用静默选项--quiet来抑制嘈杂的输出:
!pip install deap --quiet
跳过导入部分,然后查看下一个代码单元格,展示 DEAP 模块creator设置fitness标准和individual类。创建者接受前两个参数的名称和基类作为输入。如下所示,这创建了一个模板,用于首先定义最大fitness,然后基于numpy.ndarray定义individual,就像在上一例中一样。
列表 3.1 EDL_3_1_OneMax_DEAP.ipynb:creator
creator.create("FitnessMax", base.Fitness,
➥ weights=(1.0,)) ❶
creator.create("Individual", numpy.ndarray,
➥ fitness=creator.FitnessMax) ❷
❶ 创建最大适应度类
❷ 基于 ndarray 创建个体类
在下一个单元中,我们看到一个新的模块被用作构建 toolbox 的基础。toolbox 是一个容器,它包含超参数和选项,如遗传算子。在代码中,构建了一个 toolbox,然后注册了基本的 gene 类型 attr_bool。接下来,我们根据创建者注册 individual,使用 attr_bool gene 类型和大小的 n=100。在最后一行,将 population 注册为一个 list,其中填充了 toolbox.individual 的类型。这里的模式是构建并注册 gene 类型的模板,然后是 individual,最后是 population,如以下列表所示。
列表 3.2 EDL_3_1_OneMax_DEAP.ipynb:toolbox
toolbox = base.Toolbox() ❶
toolbox.register("attr_bool", random.randint, 0, 1) ❷
toolbox.register("individual", ❸
tools.initRepeat, creator.Individual, toolbox.attr_bool, n=100)
toolbox.register("population", tools.initRepeat,
➥ list, toolbox.individual) ❹
❶ 从基础创建 Toolbox
❷ 定义基本基因值 0 或 1
❸ 使用 attr_bool 作为基因模板注册大小为 n=100 的个体基因序列
❹ 注册一个类型为列表的种群,使用现有的个体
接下来,我们继续注册用于处理每一代的遗传算子。我们首先使用 evaluate 来评估 fitness,并使用一个名为 evalOneMax 的自定义函数来填充它。之后,我们添加了 crossover 的遗传操作,命名为 mate,并使用另一个名为 cxTwoPointCopy 的自定义函数。下一行设置了 mutate 操作符,这次使用了一个预定义的 DEAP 工具函数 mutFlipBit。这,就像之前一样,翻转了 gene 的位或逻辑。最后,这次注册的 selection 操作符是 select,使用了一个预构建的 selTournament 操作符,它代表了锦标赛 selection。锦标赛 selection 是一种随机配对的形式,它比较 fitness 和 evaluate,并选择下一代的父母,如以下列表所示。
列表 3.3 EDL_3_1_OneMax_DEAP.ipynb:遗传算子
toolbox.register("evaluate", evalOneMax) ❶
toolbox.register("mate", cxTwoPointCopy) ❷
toolbox.register("mutate", tools.mutFlipBit,
➥ indpb=0.05) ❸
toolbox.register("select", tools.selTournament,
➥ tournsize=3) ❹
❶ 注册评估适应度的函数
❷ 注册应用交叉的函数
❸ 注册应用突变和率的函数
❹ 注册选择方法
在这个练习中,我们使用了两个自定义函数和两个预定义函数来处理遗传算子。如果你向上滚动,你可以看到两个自定义函数 evalOneMax 和 cxTwoPointCopy。evalOneMax 函数是一行代码,返回 genes 的总和,如之前所示。
我们可以滚动到最后一行,看看如何运行进化。首先,我们设置 random.seed 为一个已知值,这允许运行一致性。然后我们使用 toolbox 来创建 population。接下来,我们创建一个 HallOfFame 对象,我们可以用它来跟踪表现最好的数量。在这个练习中,我们只对跟踪单个最佳表现者感兴趣,因为 individuals 是 NumPy 数组,我们需要覆盖排序的 similar 或 matching 算法,如以下列表所示。
列表 3.4 EDL_3_1_OneMax_DEAP.ipynb:设置进化
random.seed(64) ❶
pop = toolbox.population(n=300) ❷
hof = tools.HallOfFame(1, similar=numpy.array_equal) ❸
❶ 设置随机种子以保持一致性
❷ 创建种群
❸ 设置要观察的顶级个体数量
下面的代码行创建了一个新的Statistics对象stat,我们可以用它来跟踪种群的适应度进度。我们使用register函数添加描述性统计,传递相应的 NumPy 函数来评估统计量,如下所示。
列表 3.5 DL_3_1_OneMax_DEAP.ipynb:设置进化(继续)
stats = tools.Statistics(lambda ind: ind.fitness.values) ❶
stats.register("avg", numpy.mean) ❷
stats.register("std", numpy.std)
stats.register("min", numpy.min)
stats.register("max", numpy.max)
❶ 创建一个用于跟踪个体适应度的统计对象
❷ 注册统计函数名称和实现
最后,最后一行代码使用algorithms模块中的eaSimple函数进行进化。这个函数接受pop、toolbox、halloffame和stats对象作为输入,并设置超参数,包括交叉的概率(cxpb)、变异的概率(mutpb)和代数的数量(ngen),如下所示。
列表 3.6 EDL_3_1_OneMax_DEAP.ipynb:进化
algorithms.eaSimple(pop, toolbox, cxpb=0.5, mutpb=0.2, ngen=40, stats=stats,
halloffame=hof,verbose=None)
随着练习的进行,我们看到统计输出显示了进化的进度。这次有 40 多代,种群数量为300,我们应该看到 GA 达到100的最大适应度。这种确保成功的原因在于算子的选择。
在最后一种情况下,几乎所有内容都与我们在第二章最后部分讨论的笔记本相似。那么为什么这个种群的表现如此出色?DEAP 是否真的那么好?DEAP 并不比其他的好,但它确实为遗传算子和其他设置提供了广泛的选项。最后一个笔记本和前一个例子之间的关键区别是使用了赛选。
赛选选择通过随机选择竞争的个体配对,然后通过几个赛选,胜者是具有更好适应度的个体。在赛选结束时,胜者被选为下一代的父代。
DEAP 提供了一组有用的遗传算子库,我们可以轻松地替换,例如,开箱即用的赛选。在解决了一个最大值问题范围之外的一些实质性问题之后,我们将在下一节中仔细研究其广泛的选项。
3.1.2 学习练习
使用以下练习来帮助提高你对所讨论概念的理解:
-
通过修改列表 3.2 中的
creator.Individualtoolbox函数来增加序列中的基因数量。重新运行整个笔记本以查看结果。 -
增加或减少列表 3.4 中的
种群大小,然后重新运行以查看结果。 -
修改列表 3.6 中的
交叉和变异率,然后重新运行。这会对最终解决方案的进化产生什么影响?
现在我们已经了解了 DEAP 的基本知识,我们可以继续到下一节,解决更有趣的例子。
3.2 解决皇后棋
进化算法和遗传算法已被证明能够成功解决许多设计和布局的复杂问题。这些人工智能和机器学习方法之所以在这些类型的问题上表现出色,部分原因在于它们采用了受控的随机搜索元素。这通常使得使用 EA 或 GA 设计的系统能够超越我们的理解进行创新。
在下一个笔记本中,我们来看一个经典的设计和布局问题:皇后弃兵。这个问题使用典型的棋盘或棋盘风格棋盘,大小为n,经典棋盘大小为 8,或 8x8。目标是放置n个皇后棋子,使得没有棋子可以捕获另一个棋子而不移动。
象棋与皇后
在象棋中,皇后棋子是最强大的,可以朝任何方向和距离移动。通常,每位玩家只有一个皇后,但有一条特殊规则允许玩家在兵到达对手的后排时随时加冕更多皇后。皇后弃兵的假设是玩家已经加冕了几个皇后。然而,这种情况在现实游戏中可能永远不会发生,因为当玩家的国王棋子被捕获时,玩家就会输掉比赛。
打开 EDL_3_2_QueensGambit.ipynb,然后运行所有单元格。如果您需要打开笔记本的帮助,请参阅附录。
首先,我们想看看在棋盘上放置皇后的初始或随机位置。由于皇后可以朝任何方向移动,并且可以移动任何距离,这个假设游戏的最大皇后数量等于棋盘的大小。在这个例子中,我们使用八个,以下列表中我们查看的代码块绘制了皇后的初始放置。
列表 3.7 EDL_3_2_QueensGambit.ipynb:绘制棋盘
chessboard = np.zeros((board_size,board_size)) ❶
chessboard[1::2,0::2] = 1
chessboard[0::2,1::2] = 1
figure(figsize=(6, 6), dpi=80) ❷
plt.imshow(chessboard, cmap='binary') ❸
for _ in range(number_of_queens):
i, j = np.random.randint(0, board_size, 2) ❹
plt.text(i, j, '♕', fontsize=30, ha='center', va='center',
color='black' if (i - j) % 2 == 0 else 'white') ❺
plt.show()
❶ 设置棋盘 NumPy 数组 0s 和 1s
❷ 设置图形的大小
❸ 使用二进制颜色图绘制基础象棋棋盘网格
❹ 随机在棋盘上放置皇后
❺ 在棋盘上以文本形式绘制棋子
图 3.1 显示了渲染的棋盘和皇后棋子如何移动的提醒。注意,选定的棋子可以立即捕获几个其他棋子。记住,这个问题的目标是放置棋子,使得没有单个棋子可以捕获另一个棋子。

图 3.1 棋盘上的随机皇后放置
再次,这个笔记本中的大部分代码与之前的练习类似。接下来,我们将关注如何填充toolbox,如列表 3.8 所示。请注意,对于这个练习,我们使用了两个新的toolbox函数用于交叉和变异。我们将在本章末尾提供更多这些toolbox遗传操作员的示例。了解这些操作员的另一个优秀资源是 DEAP 文档deap.readthedocs.io/en/master/api/tools.html。
列表 3.8 EDL_3_2_QueensGambit.ipynb:填充toolbox
toolbox = base.Toolbox()
toolbox.register("permutation", random.sample,
range(number_of_queens),
➥ number_of_queens) ❶
toolbox.register("individual", tools.initIterate,
creator.Individual, toolbox.permutation) ❶
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
toolbox.register("evaluate", evalNQueens) ❷
toolbox.register("mate", tools.cxPartialyMatched) ❸
toolbox.register("mutate", tools.mutShuffleIndexes,
indpb=2.0/number_of_queens) ❹
toolbox.register("select", tools.selTournament, tournsize=3) ❺
❶ 设置皇后的数量和棋盘/个体大小
❷ 添加客户适应度函数 evalNQueens
❸ 使用工具箱函数进行交配/交叉
❹ 使用工具箱函数应用变异
❺ 使用锦标赛选择进行选择
皇后的适应度 评估函数evalNQueens通过捷径评估个体的适应度,而不是运行放置的每个迭代,该函数假设只有一位皇后可以放置在一行或一列上。因此,我们只需要评估皇后是否对角放置,这简化了适应度函数到以下列表中的代码。
列表 3.9 EDL_3_2_QueensGambit.ipynb:评估适应度
def evalNQueens(individual):
for i in range(size): ❶
left_diagonal[i+individual[i]] += 1
right_diagonal[size-1-i+individual[i]] += 1
sum_ = 0
for i in range(2*size-1): ❷
if left_diagonal[i] > 1:
sum_ += left_diagonal[i] - 1
if right_diagonal[i] > 1:
sum_ += right_diagonal[i] - 1
return sum_, ❸
❶ 遍历棋盘并评估对角放置
❷ 对放置进行循环并计算非冲突总和
❸ 返回非冲突的总和
在适应度 评估函数之后,还有一个名为eaSimple的函数,这个函数只是 DEAP 中标准alogirthms.eaSimple函数的一个副本。这个函数与我们上次练习中使用的是几乎相同的;然而,它移除了大部分的噪声日志,这使得我们可以自定义输出表现最佳的个体,以及测试早期停止。注意以下列表中个体的适应度与最大适应度的测试。这允许在达到最大适应度时提前停止进化。
列表 3.10 EDL_3_2_QueensGambit.ipynb:进化函数
for ind, fit in zip(invalid_ind, fitnesses): ❶
ind.fitness.values = fit
if fit[0] >= max: ❷
print("Solved") ❸
done = True ❸
❶ 通过个体和配对进行循环以与 zip 匹配适应度
❷ 测试个体的适应度是否达到或超过最大值
❸ 如果达到最大适应度,则打印已解决并设置退出标志
在笔记本的末尾,你可以看到种群是如何进化的。我们首先创建种群和用于最佳表现者的名人堂容器。然后,我们注册各种统计数据,最后调用eaSimple函数以进化种群。在以下列表中,注意使用max = number_of_queens作为输入来控制早期停止或当个体达到最大适应度时。
列表 3.11 EDL_3_2_QueensGambit.ipynb:进化
random.seed(seed)
pop = toolbox.population(n=100) ❶
hof = tools.HallOfFame(1) ❶
stats = tools.Statistics(lambda ind: ind.fitness.values) ❷
stats.register("Avg", np.mean)
stats.register("Std", np.std)
stats.register("Min", np.min)
stats.register("Max", np.max)
eaSimple(pop, toolbox, cxpb=0.5, mutpb=0.2, ngen=100, max = number_of_queens,
stats=stats, halloffame=hof) ❸
❶ 创建最佳表现者的种群和名人堂
❷ 注册用于监控种群的统计数据函数
❸ 调用进化函数以进化种群
最后,我们回顾进化的输出,看看算法如何进化出一个解决方案。图 3.2 显示了之前设置的给定种子参数的解决方案。你可以从输出中看到,进化能够提前停止——67 代——以创建一个可行的解决方案。

图 3.2 皇后棋的解决方案
随意回顾解决方案,并确认每个皇后都无法捕获彼此。你甚至可以回到增加棋盘大小或皇后数量到更大的值,如 16 或更多。这可能需要你同时增加种群大小和进化的代数数量。我建议你现在尝试一下,以获得更多使用 GA 的经验。
3.2.1 学习练习
通过探索这些有趣的练习来提高你的知识:
-
将列表 3.11 中的
种群大小进行进化,然后重新运行。更大的种群对进化有什么影响? -
修改列表 3.11 中的
交叉和变异率,然后重新运行。你能在更少的代数内解决问题吗? -
在列表 3.8 中增加或减少
选择锦标赛的大小,然后重新运行。锦标赛的大小对进化有什么影响?
女王棋局是一个有趣的问题来观察;我们将在下一节继续探讨其他用 EC 解决的经典问题。
3.3 帮助旅行商人
EA 和 GA 在优化难以解决的数学问题方面也取得了成功,例如经典的旅行商问题。你看,在互联网出现之前的日子里,商人需要亲自穿越国家来销售他们的商品。这个问题的概念是解决商人需要采取的路线,以确保他们不会两次访问同一地点,同时优化他们的行程长度。
图 3.3 展示了在 100 单位×100 单位的地图网格上描述的旅行商问题(TSP)的示例。在图中,商人已经优化了他们的路线,因此他们可以只访问每个城市一次,并在旅行的最后返回家中。
TSP 在数学上被认为是一个 NP-hard 问题,这意味着它不能在线性时间内计算解决。相反,解决此类问题的计算能力随着地点数量的增加而呈指数增长。在图 3.3 中,商人有 22 个目的地,包括家。

图 3.3 TSP 解决方案的可视化
计算和数学中的 NP 问题
在数学和计算中,算法根据解决它们所需的时间或计算能力来分类其难度。我们将此类问题分类为 NP,其中 N 代表解决所需元素的数量,P 是解决问题所需的时间。如果问题可以在线性时间内解决,则将其分类为 NP easy——即 N × P 以线性速率增加。相反,NP-hard 问题定义为不能在线性时间内解决,而是需要指数时间。NP-hard 解决方案定义为 N2 × P 或更高指数,这样随着元素数量的增加,问题的复杂性呈指数增长。
由于 TSP 问题是 NP 难的,我们还没有找到可以在线性时间内解决问题的数学解。相反,许多为解决 TSP 而开发的方法都是估计方法,它们借鉴了过程和优化的捷径。这些经过精心调整的方法已经在数千个点中成功应用。
使用大 O 符号,我们可以将 TSP 问题表示为 O(n²2^n)来计算得到答案的最大计算时间。对于每个新的目的地点,我们都需要重新计算相应的子点。相比之下,计算 22 个目的地需要最多 20 亿次的计算,而 23 个目的地则需要 45 亿次的计算。
为了将 22 个点的计算量放在一个可比较的视角中,假设每个计算需要 1 毫秒或 1/1000 秒来完成,那么 20 亿次的计算将需要 23 天来完成。随着每个额外目的地的增加,这个数字将以指数级增长,使得典型的编程解决方案变得不切实际。相反,EA/GA 等方法为解决这类复杂问题提供了替代方案。
3.3.1 构建 TSP 求解器
在下一个笔记本中,我们使用 DEAP 构建一个解决方案来解决开放端 TSP 问题类。TSP 的封闭形式是指旅行商被限制在一定的驾驶距离或长度。这意味着在问题中,旅行商可以旅行任何距离到达所有目的地。
在 Colab 中打开 EDL_3_3_TSP.ipynb 笔记本,然后运行所有单元格。如果您需要帮助,请参阅附录。
我们首先来看随机旅行商路径的初始化和可视化。我们首先定义的是包含旅行商路线所有位置的基地图。接下来,我们使用plt.scatter函数通过传递地图中的0和1值来绘制地图目的地。之后,我们使用plt.gca()获取当前图表并添加绘图边界限制,以便我们可以清楚地可视化整个目的地地图,如下所示。
列表 3.12 EDL_3_3_TSP.ipynb:设置地图
figure(num=None, figsize=(10, 10), dpi=80,
➥ facecolor='w', edgecolor='k') ❶
map = np.random.randint(min_bounds,max_bounds,
➥ size=(destinations,2)) ❷
plt.scatter(map[:,0], map[:,1]) ❸
axes = plt.gca()
axes.set_xlim([min_bounds,max_bounds]) ❹
axes.set_ylim([min_bounds,max_bounds]) ❹
plt.grid()
❶ 设置图形的大小和分辨率
❷ 定义一个大小为目的地的随机 NumPy 数组
❸ 在地图上绘制点
❹ 设置绘图限制
当我们应用 GA 时,种群中的每个个体将代表一个在目的地``地图上的索引列表。这个列表也代表了个体的基因序列,其中每个索引是一个基因。由于我们的地图代表了一组随机的点,我们可以假设起始个体只是按顺序访问这些点。这样做允许我们使用以下列表中的代码从个体构建一个简单的路径。
列表 3.13 EDL_3_3_TSP.ipynb:创建路径
def linear_path(map):
path = []
for i,pt in enumerate(map): ❶
path.append(i) ❷
return path
path = linear_path(map) ❸
❶ 列出地图上的点
❷ 将每个目的地添加到路径中
❸ 基于地图创建新的路径
接下来,我们希望有一种方法来可视化这条路径,以便我们可以看到随着进化的进行,路线将是什么样子。draw_path函数通过传递从上一步构建的路径来操作。在函数内部,代码遍历路径中的索引,并使用plt.arrow函数通过点对连接点,如下面的列表所示。遍历路径列表中的索引后,我们绘制一条指向起点的最终路径。图 3.4 显示了使用我们在上一步构建的起始路径调用draw_path函数的输出。

图 3.4 可视化起始随机路径
列表 3.14 EDL_3_3_TSP.ipynb:可视化路径
def draw_path(path):
figure(num=None, figsize=(10, 10), dpi=80, facecolor='w', edgecolor='k')
prev_pt = None
plt.scatter(map[:,0], map[:,1]) ❶
for I in path:
pt = map[i]
if prev_pt is not None:
plt.arrow(pt[0],pt[1], prev_pt[0]-pt[0],
➥ prev_pt[1]-pt[1]) ❷
else:
start_pt = pt ❷
prev_pt = pt
plt.arrow(pt[0],pt[1], start_pt[0]-pt[0],
➥ start_pt[1]-pt[1]) ❸
axes = plt.gca()
axes.set_xlim([min_bounds,max_bounds])
axes.set_ylim([min_bounds,max_bounds])
plt.grid()
plt.show()
draw_path(path) ❹
❶ 绘制基础地图目的地
❷ 从一点绘制到另一点的箭头
❸ 绘制一个指向起点的箭头
❹ 绘制整个路径
在draw_path函数下方,我们可以看到evaluate_path函数用于确定每个路径的fitness。在下面的列表中,这个函数遍历路径中的点索引,并计算 L1 或欧几里得距离。然后,将这些距离加起来得到总路径长度,这也对应于individual的fitness。
列表 3.15 EDL_3_3_TSP.ipynb:评估路径
def evaluate_path(path):
prev_pt = None
distance = 0
for i in path: ❶
pt = map[i]
if prev_pt is not None: ❷
distance += math.sqrt((prev_pt[0]-pt[0]) ** 2 + (prev_pt[1]-pt[1]) ** 2)
else:
start_pt = pt
prev_pt = pt
distance += math.sqrt((start_pt[0]-pt[0]) ** 2 + (start_pt[1]-pt[1]) ** 2)
return distance, ❸
evaluate_path(path)
❶ 遍历路径中的点索引
❷ 计算点之间的 L1 距离
❸ 返回距离作为集合
从这里,我们传递其他熟悉的代码,并查看toolbox的设置以及如何构建individuals。在这个例子中,我们构建一个chromosome,其索引等于destinations的数量,以存储destination map中的索引,如下面的列表所示。在这个练习中,每个individual代表destination map上的索引路径。
列表 3.16 EDL_3_3_TSP.ipynb:填充toolbox
toolbox = base.Toolbox()
toolbox.register("indices", random.sample,
range(destinations), destinations) ❶
toolbox.register("individual", tools.initIterate,
creator.Individual, toolbox.indices) ❷
toolbox.register("population", tools.initRepeat,
list, toolbox.individual) ❸
❶ 创建一个长度等于目的地的基因型索引
❷ 使用索引基因型创建一个个体
❸ 创建一个包含个体的种群列表
跳到最底部的单元格,查看执行进化的代码。再次强调,这段代码与之前的练习类似,只是这次我们没有提供早期停止参数。这是因为计算最小路径距离的成本可能与我们所使用的计算距离的算法一样高,甚至更高。相反,我们可以使用进化的输出来确认进化已经达到解决方案,如下面的列表所示。
列表 3.17 EDL_3_3_TSP.ipynb:进化
pop = toolbox.population(n=300)
hof = tools.HallOfFame(1)
stats = tools.Statistics(lambda ind: ind.fitness.values)
stats.register("avg", np.mean)
stats.register("std", np.std)
stats.register("min", np.min)
stats.register("max", np.max)
eaSimple(pop, toolbox, 0.7, 0.3, 200, stats=stats, halloffame=hof) ❶
❶ 使用硬编码的超参数调用进化函数
图 3.5 显示了 22 个点的目的地问题的解决方案。评估解决方案是否正确的一个简单方法是通过注意所有连接的点不会交叉,并且本质上形成一个循环,如图所示。

图 3.5 使用 22 个目的地在 52代中解决 TSP 问题
在大多数情况下,当目的地数量为 22 时,这个练习应该在 200代内完成。尽管我们设置了random.seed的种子,但我们仍然可以在目的地``地图以及最终的解决方案路径中获得多样化的变化。如果你发现笔记本在 200 代内无法解决问题,要么减少目的地点的数量,要么增加代数的数量。
尝试将目的地的数量增加 1 到 23 或最多 25,然后再次运行练习。记住,每增加一个点都会在复杂性上呈指数级增长,但在某些情况下,它同样容易解决。看看你是否能将目的地的数量增加到 25 以上以找到解决方案。如果你做到了,记住你可能还需要增加代数和/或种群的数量。
3.3.2 学习练习
使用这些练习来进一步探索笔记本中的概念:
-
增加或减少销售员需要访问的
目的地数量,然后每次更改后重新运行。你能创建多少个目的地的解决方案? -
调整
种群、交叉和变异率,然后重新运行笔记本。 -
改变用于进化的
选择函数的类型或参数。
现在,几个有趣且引人入胜的例子已经介绍完毕,我们可以在下一节中更深入地探讨选择各种遗传操作符的细节。
3.4 选择遗传操作符以改进进化
进化计算,与其他人工智能或机器学习学科一样,提供了广泛的超参数和选项来调整以适应问题。EA 和 GA 当然也不例外,正如我们之前所看到的,它们提供了各种超参数和遗传操作符选项。在本节中,我们将探讨并尝试更深入地了解这些选项。
DEAP 提供了几种遗传操作符选项,根据进化工具文档,在许多情况下可以轻松替换。其他操作符可能需要特殊的基因或个体类型环境,例如我们在最后两个练习中使用的mutShuffleIndexes操作符;其他操作符可能根据你的需求和判断定制制作,使可能性无限。
提示:DEAP 有一个优秀的文档资源,提供了关于本节及以后我们探讨的遗传操作符的更多详细信息。关于进化工具和遗传操作符的文档可以在以下网页上找到:deap.readthedocs.io/en/master/api/tools.html。
当然,将正确的遗传算子应用于你的进化器需要了解这些工具做什么以及如何做。在下一个练习中,我们将回顾一些最常见的算子,并看看它们如何修改目标种群(population)的进化。我们使用上一节中探索的简化版 TSP 来查看替换各种遗传算子的结果。
在 Colab 中打开 EDL_3_4_TSP_Operators.ipynb 并运行所有单元格。如有需要,请参考附录。这个练习借鉴了上一个练习的大部分代码,并添加了一些额外的可视化注释。
这个笔记本大量使用 Colab 表单来提供用户界面,以修改各种选项和超参数。跳转到标题为“选择遗传算子”的单元格,如图 3.6 所示。

图 3.6 Google Colab 表单界面用于选择遗传算子
让我们从测试 selection 遗传算子的变体开始。这个笔记本提供了我们可以选择和测试的 selection 算子类型选项,以及用于交叉和选择的选项。由于我们使用的是交换索引的特殊变异形式,因此无法更换变异算子。选择一个 selection 算子,然后从菜单中选择运行 > 运行后,以应用更改并重新运行笔记本中的剩余单元格。以下列表显示了每个算子及其简要操作描述:
-
锦标赛—此算子通过进行 n 次重复的锦标赛选择来选择。初始锦标赛是随机选择的,获胜者进入下一轮锦标赛。此算子效果良好,在保持一定多样性的同时优化最佳个体。
-
随机—此算子通过随机从种群中挑选父母来选择。如果你发现种群很快专业化或似乎陷入局部最大值/最小值,这是一个很好的算子。对于像 TSP 这样的问题,这个算子可能有效,具体取决于选择的交叉和变异算子。
-
最佳 —此算子选择表现最佳的个体作为父母。正如我们在第一个例子中看到的那样,使用精英选择或最佳选择可以快速找到解决方案,但长期效果较差。这是因为种群没有足够多的多样性来克服停滞点。
-
最差—与最佳算子的相反,此算子选择表现最差的个体作为父母。使用最差个体作为父母的优点是可以完全避免专业化。当你发现种群专业化并且陷入错误解决方案时,这一点尤其有效。
-
NSGA2——此操作符基于 Deb 等人于 2002 年(Springer)发表的论文“A Fast Elitist Non-dominated Sorting Genetic Algorithm for Multi-objective Optimization: NSGA-II”。它是一个既能够很好地维护
fitness优化,又能在长期内保持population多样性的算法/操作符。使用此算法会导致population倾向于保持在正态分布内。这使得这种方法对于需要长期进化的问题很有用。 -
SPEA2——此操作符起源于 Zitzler 等人于 2001 年(ETH Zurich,计算机工程和网络实验室)发表的论文“SPEA 2: Improving the Strength Pareto Evolutionary Algorithm”。它试图在最大/最小
fitness附近保持population的 Pareto 前沿分布,从而形成一个几乎 U 形的分布。这是一个适用于需要长期进化的问题的良好操作符,因为它保持平衡的多样性,避免了停滞点。
一定要将population设置为1000,并将generations的数量设置为15,以便看到selection操作符的完整效果。在遍历每个selection操作符时,请特别注意最后进行进化的单元格生成的输出。图 3.7 展示了各种selection操作符的直方图输出。注意,锦标赛selection操作符形成了一个fitness的 Pareto 分布,而其他方法则倾向于多样化fitness。记住,更多的多样性有利于更长时间的进化,并且通常能更好地适应关键点。然而,与此同时,更多的多样性需要更多的generations来进化出最佳的fitness。

图 3.7 selection操作符对种群多样性的比较
随意探索各种选择操作符,并确保更改destinations和generations的数量以及populations的大小。注意较大和较小的populations对selection的影响。当你完成对selection的探索后,我们可以继续更改如图 3.7 所示的crossover操作符。有几种类型的crossover操作符可以提供更有效的解决方案。在应用crossover时,由于不匹配或序列错位,产生的后代往往表现不佳。在某些情况下,genes不需要在序列中对齐,但在这个练习中,它们需要。以下是在这个示例笔记本中使用的crossover操作符列表以及它们对进化的影响:
-
部分匹配——这种方法通过匹配索引类型
genes的序列并在这序列上执行crossover操作来实现。对于其他gene类型,它可能工作得不好或不符合预期。通过保留索引序列,这种crossover操作更好地保留了后代中最好的部分,而其他操作符可能会破坏路径或重要的进化序列。 -
均匀部分匹配—这种方法类似于部分匹配操作符,但不同之处在于它试图在父母的“交叉”之间保持平衡和均匀的序列交换。这种方法的好处是长期坚持序列的更强一致性,但它可能使初始进化变得困难。
-
有序—这个“交叉”操作符执行有序的索引序列交换。这样做可以保持顺序,但允许序列基本上旋转。这是一个很好的操作符,用于可能陷入进化序列操作(如 TSP)的“种群”。
-
一点/两点—与本章前面提到的第一个“交叉”操作符类似,一点“交叉”选择一个点来分割父母的“基因”序列。扩展这个概念,两点“交叉”执行相同的操作,但使用两个点来切割“基因”序列。这些是很好的通用方法,但在处理像 TSP 这样的索引序列时并不是选项。
为了理解改变“交叉”的影响,我们需要引入一种新的图表类型,称为“家谱图”,如图 3.8 所示。在图中,每个圆圈代表一代,如果这一代产生了好的后代,那么就会有一条箭头连接到下一代。家谱图有助于确认你的“交叉”操作是否产生了有活力的、适应良好的后代。一个好的图表显示了从不太适应到更适应的后代的流动。基本上,箭头和连接越多,越好;这表明进化过程的进展,其中从一个节点到另一个节点的每个箭头代表一个连接步骤,或进化命名法中的“亚种”。显示较少连接或根本没有连接的家谱图表明你的“交叉”没有产生有活力的后代。这个图上的孤立点表示可能具有良好“适应性”但未产生有活力后代的随机后代。

图 3.8 展示使用“交叉”进化的个体家谱图
将“目的地”数量增加到10,并将“种群”和“代数”减少到5。在进化过程中生成家谱图非常昂贵且难以阅读,所以这个笔记本限制渲染那些图只针对10以下的“种群”。你完成这个操作后,更改各种“交叉”操作符,然后从菜单中选择运行 > 运行全部来重新运行整个笔记本。
图 3.9 展示了该笔记本支持的三个不同交叉操作的谱系图示例。从图中可以看出,对于这个例子来说,部分匹配选项似乎是最佳的交叉操作。如图所示,第一个算子能够成功生成更适应的后代,而均匀部分匹配算子可以产生可行的后代,但适应度的增加并不显著。为了更清楚地看到差异,请确保运行笔记本并自行可视化。

图 3.9 交叉操作的谱系图比较
完成这个练习后,你现在应该对使用各种遗传算子进行交叉和选择之间的差异有所了解。在未来的章节中,我们将探讨在应用 EDL 时变异算子的各种实现。
3.4.1 学习练习
通过以下练习回顾笔记本:
-
将图 3.6 中的 Colab 表单中的
选择或交叉算子进行更改。尝试每个算子,看看哪个最适合这个问题。 -
看看改变算子和超参数(
种群、交叉和变异率)对基因谱系有什么影响。 -
回顾笔记本 EDL_3_2_QueensGambit.ipynb,并更改
选择或交叉算子,看看这会对进化产生什么影响。
在前面的例子基础上,我们现在来看本章最后部分的一个有趣例子。
3.5 使用 EvoLisa 绘画
在 2008 年,罗杰·约翰逊展示了使用遗传算法通过一系列多边形绘制蒙娜丽莎的过程。图 3.10 展示了该实验在进化后期阶段的优秀结果。从这些图像集合中,你可以看到这些结果几乎需要一百万个代才能进化完成。

图 3.10 展示了罗杰·约翰逊博客中的 EvoLisa 输出示例([rogerjohansson.blog/](https://rogerjohansson.blog/2008/12/16/evolisa-optimizations-and-improved-quality/))
EvoLisa 是一种生成建模的形式,算法的目标是模拟或复制某些过程的输出。近年来,生成模型(GM)随着生成深度学习(GDL)在生成对抗网络(GANs)中的出现而爆炸式增长。从第八章开始,我们将更深入地探讨 GM 和 GDL 与 GANs,以及如何通过 EDL 改进这些技术。
DEAP 使得复制 EvoLisa 项目变得相当容易,但它确实要求我们以更复杂和结构化的方式思考我们的简单基因序列。以前,一个基因是列表中的一个单一元素,我们现在需要将基因视为列表中元素的一个子集或一组。这些元素子组或基因中的每一个定义了一个绘图多边形,一个个体拥有多个用于在画布上绘制的多边形基因。
让我们开始下一个项目:使用 DEAP 和 GA 构建 EvoLisa。如图 3.10 所示,获得良好的结果可能需要相当多的时间。虽然我们可能不想复制那些结果,但回顾创建复杂 基因 的过程对后续章节和其他你可能工作的项目是有益的。
在浏览器中打开 EDL_3_5_EvoLisa.ipynb,让我们开始。如需进一步帮助,请参阅附录。
我们首先需要理解的是,如何将数字序列转换为表示绘图多边形或笔刷的 基因。图 3.11 概述了如何从序列中提取一组属性并将其转换为绘图笔刷,其中前六个元素代表简单多边形的三个点。之后,接下来的三个表示颜色,最后,最后一个元素代表 alpha,或透明度。通过引入透明度,我们允许每个笔刷叠加在其他笔刷之上,产生更复杂的功能。

图 3.11 从属性序列中提取 基因
在我们之前的场景中,遗传序列中的每个属性代表一个单独的 基因。现在,属性的一个子集代表一个 基因,该 基因 表示一个绘图笔刷。以下代码列表接受一个属性序列(基因)并将它们按 基因 长度分割。此示例构建为使用多边形作为绘图笔刷,但额外的注释代码演示了使用其他笔刷,如圆形或矩形。
列表 3.18 EDL_3_5_EvoList.ipynb:提取基因
def extract_genes(genes, length):
for i in range(0, len(genes), length):
yield genes[i:i + length] ❶
❶ 从属性序列中提取并产生单个基因
在相同的代码块中,我们可以看到绘制每个 基因 的渲染代码。这是一段复杂的代码,我们将其分为两个列表。第一个列表展示了绘制画布的构建和从 基因 中提取以循环的内容。
列表 3.19 EDL_3_5_EvoList.ipynb:渲染基因
def render_individual(individual):
if isinstance(individual,lst): ❶
individual = np.array(individual) ❶
canvas = np.zeros(SIZE+(3,)) ❷
radius_avg = (SIZE[0] + SIZE[1]) / 2 / 6 ❸
genes = extract_genes(individual, GENE_LENGTH) ❹
for gene in genes: ❺
❶ 如果是列表,将个体转换为 NumPy 数组
❷ 根据图像大小创建画布并添加一个颜色维度
❸ 不用于多边形笔刷
❹ 使用 extract_genes 生成器加载基因
❺ 遍历基因
列表 3.20 展示了如何从提取的相关 基因 属性中定义每个笔刷,其中前六个值代表使用 cv2.fillPoly 函数绘制的多边形的三个点或坐标。然后,提取的 alpha 用于使用 cv2.addWeighted 函数将笔刷(叠加)混合到画布上。最后,在绘制了所有 基因 笔刷之后,函数返回要评估的最终画布。
列表 3.20 EDL_3_5_EvoList.ipynb:渲染 基因
try:
overlay = canvas.copy() ❶
# polyline brush uses GENE_LENGTH = 10
# pts = (0, 1), (2, 3), (4, 5) [6]
# color = (6, 7, 8) [9]
# alpha = (9) [10]
x1 = int(gene[0] * SIZE[0]) ❷
x2 = int(gene[2] * SIZE[0]) ❷
x3 = int(gene[4] * SIZE[0]) ❷
y1 = int(gene[1] * SIZE[1]) ❷
y2 = int(gene[3] * SIZE[1]) ❷
y3 = int(gene[5] * SIZE[1]) ❷
color = (gene[6:-1] * 255).astype(int).tolist() ❸
pts = np.array([[x1,y1],[x2,y2],[x3,y3]])
cv2.fillPoly(overlay, [pts], color) ❹
alpha = gene[-1] ❺
canvas = cv2.addWeighted(overlay, alpha,
➥ canvas, 1 - alpha, 0) ❻
except: ❼
pass
return canvas
❶ 复制基础图像和 NumPy 数组
❷ 从值中提取并缩放为整数以进行绘制
❸ 从下一个点提取颜色
❹ 绘制多边形
❺ 提取 alpha
❻ 使用 alpha 透明度将叠加与画布合并
❼ 包裹在错误处理中,以防万一
图 3.12 展示了使用render_individual函数渲染的随机个体的结果。您可以通过在菜单中选择“运行”>“运行所有”来运行所有笔记本代码以生成此图像。现在就去做吧,因为这个笔记本需要相当长的时间才能完全运行。

图 3.12 目标图像和从随机个体渲染的图像
在这个演示中,我们使用一幅经典的蒙娜丽莎画像。然而,如果您滚动到笔记本的顶部,您可以看到其他选项来加载各种其他图像,从停车标志到名人照片,比如道恩·强森(Dwayne “the Rock” Johnson)。如果您想使用不同的图像,请从提供的 Colab 表单下拉菜单中选择,然后重新运行笔记本。
我们可以使用简单的像素级颜色值比较,从一个 NumPy 数组到另一个数组,使用均方误差来评估函数的fitness。以下列表中的函数计算渲染图像和原始图像之间的 MSE,然后这个误差作为individual的fitness分数返回。请记住,EvoLisa 的目标是最小化这个误差。
列表 3.21 EDL_3_5_EvoList.ipynb:fitness和evaluate函数
def fitness_mse(render):
error = (np.square(render - target)).mean(axis=None) ❶
return error
def evaluate(individual):
render = render_individual(individual) ❷
print('.', end='') ❸
return fitness_mse(render), ❹
❶ 从渲染结果与目标之间的比较计算 MSE 误差
❷ 渲染图像
❸ 为每次评估打印一个点,一个简单的进度条
❹ 返回 MSE 作为个体的适应度
我们需要查看的最后部分是遗传算子的设置,如列表 3.22 所示。这里只有一些新的内容。我们定义了一个均匀函数,用于从定义的下限和上限的均匀分布中生成浮点属性。这个函数注册了一个attr_float算子,并在creator.Individual算子的注册中使用。最后,我们可以看到evaluate函数是如何注册为evaluate算子的。
列表 3.22 EDL_3_5_EvoList.ipynb:设置 GA 算子
def uniform(low, up, size=None): ❶
try:
return [random.uniform(a, b) for a, b in zip(low, up)]
except TypeError:
return [random.uniform(a, b) for a, b in zip([low] * size, [up] * size)]
toolbox = base.Toolbox()
toolbox.register("attr_float", uniform, 0, 1, NUM_GENES) ❷
toolbox.register("individual", tools.initIterate, creator.Individual,
➥ toolbox.attr_float) ❸
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
toolbox.register("mutate", tools.mutGaussian, mu=0.0, sigma=1, indpb=.05)
toolbox.register("evaluate", evaluate) ❹
❶ 均匀函数生成个体
❷ 为个体创建注册 attr_float 算子
❸ 使用 attr_float 算子注册创建的个体
❹ 注册evaluate函数
图 3.13 展示了使用矩形笔刷和多段线笔刷运行此示例大约 5,000代的结果。实现圆形或矩形笔刷的代码已被注释,但作为对感兴趣读者的一个选项展示。

图 3.13 使用不同笔刷格式(矩形和多边形填充)的 EvoLisa 示例输出
一定要回到这个例子中提供的 Colab 表单选项,更改设置,然后重新运行 EvoLisa。您能否提高 EvoLisa 复制图像的速度?请确保还尝试调整 EvoLisa 使用的多边形数量(笔刷/基因)。
EvoLisa 是十多年前使用遗传算法进行生成建模的一个很好的演示。从那时起,GDL 的出现已经使用 GANs 展示了远超预期的结果。然而,EvoLisa 展示了我们如何编码和进化一系列基因或指令,这可以定义一个更复杂的过程。虽然应用看起来相似,但 EvoLisa 的底层机制展示了不同形式的优化。
3.5.1 学习练习
完成以下练习,以巩固本节讨论的概念:
-
切换到不同的图像,看看进化如何很好地复制原始图像。您甚至可能想添加自己的图像。
-
增加或减少用于渲染图像的
基因和多边形的数量,然后看看这有什么影响。 -
修改
变异、交叉和种群,看看它们对进化有什么影响。您能否将代数的数量减少到所选图像的更好复制品? -
尽可能地制作出最好的复制品。如有需要,请随时联系作者展示您令人印象深刻的结果。
在阅读本章之后,我们现在了解了进化计算的基本原理以及遗传算法的内部工作原理。
摘要
-
Python 中的分布式进化算法(DEAP)是一个优秀的工具,它集成了各种进化算法,包括遗传方法。
种群大小、代数数量、交叉率和变异率的遗传算法超参数可以根据个别解决方案进行调整。DEAP 可以快速配置和设置,以使用基本构建块解决各种问题:-
创建者—这包含个体的定义。
-
工具箱—这定义了一组辅助函数以及定义遗传算子和操作的位置。
-
名人堂—这跟踪最成功的个体。
-
统计数据—这些跟踪基本指标,可用于评估
种群的成功率。 -
历史对象—这些提供跟踪自定义或其他外部操作的能力。
-
-
《后翼弃兵》是一个模拟的棋局问题,可以使用 DEAP 中的遗传算法来解决。
-
旅行商问题,一个经典的复杂路径组织问题,可以使用 DEAP 中的遗传算法来解决。
-
使用直方图可视化
种群的适应度多样性可以识别可能停滞的进化种群。 -
通过家谱图可以了解在进化过程中
交叉操作的效果如何。它们提供了对各种选择遗传算子对种群进化的评估和性能的洞察。可以使用家谱图评估特定交叉操作执行得有多好。 -
一个个体的模拟遗传密码可以代表一个过程或操作顺序。他们甚至可以实施一个复杂的遗传结构来表示复制或复制图像的复杂操作顺序。
4 使用 DEAP 进行更多进化计算
本章涵盖
-
在 DEAP 中使用遗传编程开发回归求解器
-
将粒子群优化应用于求解复杂函数中的未知数
-
将问题分解成组件并协同进化解决方案
-
理解和应用进化策略来近似解
-
使用可微分的进化近似连续和离散解
在第三章中,我们通过介绍 GA 开始探索进化计算的外观,只是触及了表面。从 GA 开始帮助我们建立几个在本章中继续发展的基础。我们还通过探索其他用于解决更专业和复杂问题的进化搜索方法来继续我们的 GA 探索。在本章中,我们考察了其他形式的进化搜索来解决更广泛的问题。
存在着各种各样和形式的进化算法,每种算法都有其不同的优势和劣势。了解其他可用的选项可以加强我们对在哪里应用哪种算法的理解。正如本章所看到的,剥橙子的方法不止一种。
4.1 使用 DEAP 进行遗传编程
我们已经广泛使用 DEAP 开发 GA 解决方案来解决各种问题。在下面的笔记本中,我们继续使用 DEAP 来探索 EC/GA 的一个子集,称为遗传编程(GP)。GP 遵循与 GA 相同的原理,并采用许多相同的遗传算子。GA 和 GP 之间的关键区别在于基因或染色体的结构以及如何评估适应度。遗传编程和基因表达编程(GEP)可以用于解决各种自动化和控制问题,如本书后面所讨论的。
本节中开发的笔记本展示了遗传编程在解决回归问题中的一个应用。GEP 也可以应用于各种其他问题,从优化到搜索。然而,展示回归对于我们的目的来说最为相关,因为它与我们如何使用深度学习(DL)解决相同问题的方法相似。
在这个笔记本中,我们通过使用 GEP 推导出一个解方程来解决多元回归问题。目标是这个方程能够成功回归或预测给定几个输入值的一个输出值。这个例子只使用预先输入到目标方程中的随机输入来验证结果。然而,这种方法可以并且已经被用来执行回归,类似于我们在深度学习(DL)中使用的样子。
4.1.1 使用遗传编程解决回归问题
您可以通过在 Google Colab 中打开 EDL_4_1_GP_Regression.ipynb 笔记本开始练习。如果您需要打开文件的帮助,请参阅附录。这个练习可能感觉与第三章中的 DEAP 练习相似。为了方便使用,请使用菜单“运行”>“运行所有单元格”来运行笔记本中的所有单元格。
我们可以跳过前几个设置和导入的代码单元,专注于列表 4.1 中显示的第一个新代码片段。这段代码本质上定义了我们可以在 个体 中表示的特殊 基因 集合。在这段代码中,我们可以看到三种不同类型 基因 的定义:运算符、常量和输入或参数。为了理解这段代码,让我们退一步看看 GP 是如何工作的。
列表 4.1 EDL_4_1_GP_Regression.ipynb:设置表达式
pset = gp.PrimitiveSet("MAIN", 4) ❶
pset.addPrimitive(np.add, 2, name="vadd") ❷
pset.addPrimitive(np.subtract, 2, name="vsub")
pset.addPrimitive(np.multiply, 2, name="vmul")
pset.addPrimitive(protectedDiv, 2)
pset.addPrimitive(np.negative, 1, name="vneg")
pset.addPrimitive(np.cos, 1, name="vcos")
pset.addPrimitive(np.sin, 1, name="vsin")
pset.addEphemeralConstant("rand101", lambda:
➥ random.randint(-1,1)) ❸
pset.renameArguments(ARG0='x1') ❹
pset.renameArguments(ARG1='x2')
pset.renameArguments(ARG2='x3')
pset.renameArguments(ARG3='x4')
❶ 首先创建并命名原始集合
❷ 将运算符添加到集合中
❸ 向集合中添加临时常量
❹ 添加变量输入
遗传编程(GP)使我们能够更专注于我们解决的问题类型以及解决方式。使用 GP,我们不仅寻找新的解决方案,而且开发出可以用来推导这些解决方案的数学函数或程序代码。这里的优势在于,这些函数可以被重用或研究,以更好地理解特定问题。
在 GEP 中,每个 基因 代表一个运算符、常量或输入,整个 染色体 或 基因 序列代表一个表达式树,其中运算符可以代表像加法或减法这样简单的操作,或者像程序函数这样复杂的操作。常量和输入/参数,然后,代表单个标量值或更复杂的数组和张量。
图 4.1 展示了 GP 可能消费的 个体 的 基因 序列。在图中,你可以看到运算符和输入/常量的顺序如何形成一个可以评估为方程的表达式树,然后可以使用数学规则进行评估。该方程的输出可以与某个目标值进行比较,以确定 个体 的错误量或 适应度。

图 4.1 示例 GP 个体 到表达式树到方程到 适应度
图 4.1 中所示的 基因 序列展示了运算符、输入/参数和常量的顺序如何形成一个不均匀的叶节点表达式树。查看 基因 序列,我们可以看到第一个运算符是 *,它映射到树的根。从这些运算符中,接下来的两个 基因 扩展到第一级节点,由 + 和 √ 运算符表示。序列中的下一个两个 基因 映射到 + 节点。随后,– 运算符附加到 √ 节点,最后,最后的 基因 映射到底部节点。
每个节点形成的子节点数量取决于运算符的顺序。运算符的顺序可以是单目、双目、三目或 n-目。为了简化,在这个例子中,我们使用双目运算符 *, – 和 + 以及单目运算符 √。输入和常量没有顺序,并且始终表示表达式树中的叶节点。
从表达式树中,可以通过一个表示某些目标输出的结果来评估一个方程。这个目标输出与监督学习中的预期值进行比较,差异表示错误或,在这种情况下,fitness。在这个问题中的目标是减少fitness或错误到最小值。
我们可以从笔记本中查看的最后一段代码块向下滚动,跳过创建creator的单元格,直到toolbox设置的开始。在创建toolbox之后,下一行设置了表达式树评估器。在下面的列表中,我们使用了一个现成的表达式树生成器genHalfandHalf,它有 50%的概率使用其两种不同形式的树之一。
列表 4.2 EDL_4_1_GP_Regression.ipynb:设置toolbox
toolbox = base.Toolbox()
toolbox.register("expr", gp.genHalfAndHalf,
➥ pset=pset, min_=1, max_=2) ❶
toolbox.register("individual", tools.initIterate, creator.Individual,
➥ toolbox.expr)
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
toolbox.register("compile", gp.compile, pset=pset) ❷
❶ 定义表达式生成的类型
❷ 使用原始集定义创建编译函数
在表达式树生成中,我们可以假设树是通过两个基本规则生成的。一个规则假设树中所有叶节点处于同一级别,或者说是偶数级别。另一个规则假设叶节点可以是奇数级别。图 4.1 中所示的是一种奇数级别叶节点的表达式树示例。列表 4.2 中的代码使用genHalfAndHalf函数允许生成两种形式的树。
接下来,我们看看如何使用 NumPy 的几行代码随机生成样本输入值,如以下列表所示。x输入是通过 NumPy 的随机rand函数生成的,形状为4, 10000,表示在 10,000 行中有 4 个输入。然后,我们使用一个临时的方程来计算目标y值,我们的解决方案稍后应该复制这个方程。
列表 4.3 EDL_4_1_GP_Regression.ipynb:生成数据
x = np.random.rand(4, 10000) ❶
y = (x[3] + x[0]) / x[2] * x[1] ❷
❶ 创建一个 4, 10000 的随机张量
❷ 使用定义的方程评估目标值
我们 GP 进化的目标是重新创建我们在列表 4.3 中使用的方程来计算目标值。在这个例子中,我们使用随机数据,但你当然可以将相同的原理应用于由输入特征x和目标输出y定义的结构化数据,以解决其他形式的回归问题。
在我们继续之前,让我们回到前面代码中的一个早期代码单元格,如列表 4.4 所示。我们可以看到函数protected_div的定义,该函数替换了我们通常使用的除法运算符。我们需要这样做以避免使用 NumPy 可能遇到的除以零错误。参考列表 4.1 以了解该函数是如何用来定义表达式primitive集的除法运算符的。
列表 4.4 EDL_4_1_GP_Regression.ipynb:protected_div函数
def protectedDiv(left, right): ❶
with np.errstate(divide='ignore',invalid='ignore'):
x = np.divide(left, right)
if isinstance(x, np.ndarray):
x[np.isinf(x)] = 1
x[np.isnan(x)] = 1
elif np.isinf(x) or np.isnan(x):
x = 1
return x
❶ 防止除以零的包装器
从这里,我们继续审查用于评估编译表达式与值之间差异量的fitness函数,在这个例子中称为evalSymbReg。通过传递每个输入来评估差异量。注意以下列表中 NumPy 如何允许我们一次性处理所有 10,000 个样本行数据,以输出总误差或差异。
列表 4.5 EDL_4_1_GP_Regression.ipynb:评估fitness
def evalSymbReg(individual):
func = toolbox.compile(expr=individual) ❶
diff = np.sum((func(x[0],x[1],x[2],x[3]) - y)**2) ❷
return diff, ❸
❶ 将表达式编译成树
❷ 评估编译表达式与值和平方之间的差异
❸ 将差异或误差作为个体fitness返回
代码的其余部分与上一章的练习相当,所以我们在这里不需要回顾它。图 4.2 显示了找到一个最小错误量或fitness低于一的进化输出表达式树。这个表达式树图是用network创建的,正如在列表 4.6 中显示的plot_expression函数定义的那样。当这个表达式树被评估时,我们可以看到产生的方程和结果代码与计算y的原始函数相匹配,如列表 4.3 所示。你也许还会注意到,表达式树引入了另一个受保护的除法运算符,这导致最后一个项从实际方程中翻转。从数学上讲,求解后的输出表达式树与用于生成y的原始方程相匹配。

图 4.2 求解的表达式树图,它评估为方程和代码
列表 4.6 显示了如何使用plot_expression函数绘制表达式树。与第三章的练习一样,我们继续按fitness输出顶部或最佳individual。在这种情况下,我们希望fitness最小化或接近零的值。使用plot_expression函数绘制的表达式树使用 Fruchterman-Reingold 力导向算法定位节点。Networkx 提供了各种位置算法或布局,但弹簧布局适用于大多数情况。
列表 4.6 EDL_4_1_GP_Regression.ipynb:绘制表达式树
import matplotlib.pyplot as plt
import networkx as nx ❶
def plot_expression(individual):
options = {"node_size": 500, "alpha": 0.8}
nodes, edges, labels = gp.graph(individual)
g = nx.Graph()
g.add_nodes_from(nodes)
g.add_edges_from(edges)
pos = nx.spring_layout(g) ❷
nx.draw_networkx_nodes(g, pos, **options) ❸
nx.draw_networkx_edges(g, pos, width=1.0, alpha=0.5)
nx.draw_networkx_labels(g, pos, labels, font_size=9, font_color='k')
plt.show()
❶ networkx 是一个节点图绘图库。
❷ 使用弹簧布局表示节点
❸ 渲染图表的节点、边和标签
在这个例子中,我们使用了随机数据来进化一个回归函数,该函数可以生成我们的原始方程。完全可以修改这个例子以消费 CSV 结构化数据,从而生成解决现实世界问题的回归方程。
GP 提供了使用表达式树生成相同概念来生成方程或实际程序代码的能力。这是因为,从根本上讲,所有编程代码都可以表示为一个表达式树,其中像if语句这样的布尔运算符是接受二进制输入或复杂函数的单个返回值的n-元运算符。
基因表达式编程
在本例中我们使用的遗传编程形式更具体地称为基因表达编程(GEP)。GEP 由 Candida Ferreira 于 2002 年开发,她目前是 Gepsoft 的负责人,Gepsoft 是一家生产名为 Gene Expression Programming Tools 的工具的 AI/ML 工具软件组织。这个工具可以用来在结构化数据上执行 GEP,产生从方程到跨多种语言的实际程序代码的各种形式输出。如果你想探索使用 GEP 处理结构化数据,那么你绝对应该访问 Gepsoft 网站并下载软件的试用版(www.gepsoft.com)。
GEP 的好处是生成实际的数学函数或编程代码,这些代码可以稍后进行优化和重用。然而,GEP 也可能生成过于复杂的函数或代码,这可能会使解决方案不可用。如果你回过头去运行最后一个练习,输入超过四个时,你可以验证在进化过程中生成的表达式树的复杂性增加。
4.1.2 学习练习
请完成以下练习,以帮助提高你对概念的理解:
-
修改列表 4.3 中的目标函数,然后重新运行笔记本。如果你使方程更复杂会发生什么?
-
删除或注释列表 4.1 中的某些算子,然后重新运行。当进化算子较少时会发生什么。这是你所期望的吗?
-
修改遗传算子以及/或
交叉、选择或变异参数,然后重新运行。
在使用进化构建基因序列之后,我们现在想转向更具体的适者生存实现。在下一节中,我们回到第二章中生命模拟的根源,并介绍粒子群优化。
4.2 使用 DEAP 的粒子群优化
粒子群优化(PSO)是另一种受适者生存和集群行为概念启发的进化计算方法。在下一笔记本中,我们使用 PSO 来近似使用 DEAP 解决函数所需的最佳参数。这是一个简单的例子,展示了 PSO 在解决参数函数输入方面的强大能力。
PSO 的一个常见用例是解决已知方程或函数中的所需输入参数。例如,如果我们想将炮弹射到指定的距离,我们会考虑图 4.3 中显示的物理方程。

图 4.3 计算炮弹射击距离
4.2.1 使用 PSO 求解方程
我们可以尝试使用几种数学和优化方法来解决图 4.3 中的方程。当然,我们不会这么做,而是使用 PSO 来找到所需的初始速度和射击角度。在 Google Colab 中打开 EDL_4_2_PSO.ipynb 并开始以下练习。
本练习中使用的代码是 DEAP,所以其中大部分应该都很熟悉。在这里,我们关注几个独特的代码关键部分,这些部分定义了 PSO。首先,滚动到设置toolbox的位置,如下所示。toolbox注册了几个关键函数,用于生成、更新和评估粒子。可以将其视为粒子操作符,确保不要将它们称为遗传操作符。
列表 4.7 EDL_4_2_PSO.ipynb:设置toolbox
toolbox = base.Toolbox()
toolbox.register("particle",
generate, size=2, pmin=-6,
➥ pmax=6, smin=-3, smax=3) ❶
toolbox.register("population",
tools.initRepeat, list,
➥ toolbox.particle) ❷
toolbox.register("update",
updateParticle, phi1=200, phi2=200) ❸
toolbox.register("evaluate", evaluate) ❹
❶ 注册generate函数以创建一个新的粒子
❷ 将粒子作为种群中的个体进行注册
❸ 注册updateParticle函数以更新粒子
❹ 注册函数以评估个体的适应性
我们首先查看generate操作符和同名的函数。图 4.8 中显示的generate函数创建了一个由粒子组成的数组,其起始位置由pmin和pmax设置。在群集优化过程中,每个粒子都有一个固定的速度或距离,它可以在更新中使用。在更新过程中,粒子通过评估fitness被移动或群集到位置。
列表 4.8 EDL_4_2_PSO.ipynb:生成粒子
def generate(size, pmin, pmax, smin, smax):
part = creator.Particle(np.random.uniform(pmin,
➥ pmax, size)) ❶
part.speed = np.random.uniform(smin, smax, size) ❷
part.smin = smin
part.smax = smax
return part
❶ 创建一个粒子数组
❷ 创建一个随机速度向量
接下来,我们查看更新操作符函数updateParticle,如下所示。这个函数负责在群集优化的每次迭代中更新粒子的位置。在 PSO 中,想法是不断群集粒子围绕最适应的粒子。在更新函数中,通过改变速度和位置来群集粒子。
列表 4.9 EDL_4_2_PSO.ipynb:更新粒子
def updateParticle(part, best, phi1, phi2):
u1 = np.random.uniform(0, phi1, len(part)) ❶
u2 = np.random.uniform(0, phi2, len(part))
v_u1 = u1 * (part.best - part) ❷
v_u2 = u2 * (best - part)
part.speed += v_u1 + v_u2
for i, speed in enumerate(part.speed): ❸
if abs(speed) < part.smin:
part.speed[i] = math.copysign(part.smin, speed)
elif abs(speed) > part.smax:
part.speed[i] = math.copysign(part.smax, speed)
part += part.speed
❶ 通过一些随机量偏移粒子
❷ 计算二维速度偏移
❸ 遍历速度并调整
图 4.4 展示了 PSO 如何将各种粒子围绕优化目标区域进行群集。如果我们把角度和速度画在图上,那么我们可以把每个粒子或点看作是射击大炮的猜测或尝试。因此,PSO 的目标是找到最优参数(速度和角度),使大炮弹射到目标距离。

图 4.4 粒子群优化
接下来,我们查看列表 4.7 中注册到toolbox的评估函数evaluate。在这段代码的开始,我们定义了目标距离,将其作为笔记本滑块输入暴露出来。由于图 4.3 中的方程中速度项是平方的,我们只想允许正值。这就是我们防止负值的原因。同样,我们假设角度是以度为单位,然后将其转换为弧度用于方程。最后,我们使用方程计算距离,并从目标距离中减去。然后我们返回平方值,作为函数返回的元组,以返回平方误差项的总和。
列表 4.10 EDL_4_2_PSO.ipynb:评估粒子
distance = 575 #@param {type:"slider", min:10,
➥ max:1000, step:5} ❶
def evaluate(individual):
v = individual[0] if individual[0] > 0 else 0
➥ #velocity ❷
a = individual[1] * math.pi / 180 #angle to radians ❸
return ((2*v**2 * math.sin(a) * math.cos(a))/9.8 –
➥ distance)**2, ❹
❶ 允许输入距离以滑动条表单控件的形式暴露
❷ 确保速度是一个正值
❸ 将角度从度转换为弧度
❹ 从距离到计算值的平方误差返回
在设置好基本操作后,我们可以继续到群聚代码。这个代码块,如图 4.11 所示,比我们之前的 GA 或 GP 示例简单得多。与 GA 或 GP 不同,在 PSO 中,粒子在整个模拟过程中都存在。由于粒子的寿命较长,我们可以跟踪每个粒子的最佳 fitness 值。跟踪每个粒子的最佳 fitness 值允许当前最佳粒子(在代码列表中以 best 表示)进行交换。最后一行上的 toolbox.update 调用是群中粒子根据最佳粒子的位置重新定位的地方,使用列表 4.9 中的 updateParticle 函数。
列表 4.11 EDL_4_2_PSO.ipynb:群聚
GEN = 100 ❶
best = None
for g in range(GEN):
for part in pop: ❷
part.fitness.values = tuple(np.subtract((0,), toolbox.evaluate(part)))
if part.best is None or part.best.fitness <
➥ part.fitness: ❸
part.best = creator.Particle(part)
part.best.fitness.values = part.fitness.values
if best is None or best.fitness < part.fitness: ❹
best = creator.Particle(part)
best.fitness.values = part.fitness.values
for part in pop: ❺
toolbox.update(part, best)
❶ 设置群聚代数数量
❷ 在种群中循环粒子
❸ 检查粒子的最佳适应度
❹ 检查它是否比最佳粒子更好
❺ 遍历种群并更新工具箱
随着模拟的进行,你会看到粒子如何开始收敛到最佳或多个最佳解,如图 4.5 所示。注意,在这个问题中角度和速度有两个正确解。同时注意,粒子在解的某些距离上仍然分布较广。这是由于超参数 pmin、pmax、smin、smax、phi1 和 phi2 的选择,你可以通过调整这些值来改变粒子的分布量。如果你想看到更小的粒子分布,将那些超参数调整到更小的值,然后再次运行笔记本。

图 4.5 粒子群迭代散点图
最后,笔记本中的最后一块代码允许我们评估最佳粒子的指定解。然而,由于问题有两个解,我们可能会评估多个最佳解。从输出值中,你可以看到 PSO 可以相对快速地近似射击一定距离的解,如下面的列表所示。
列表 4.12 EDL_4_2_PSO.ipynb:输出最佳结果
v, a = best ❶
a = a * math.pi / 180 #angle to radians ❷
distance = (2*v**2 * math.sin(a) * math.cos(a))/9.8 ❸
print(distance)
❶ 从最佳解中提取速度和角度
❷ 将角度从度转换为弧度
❸ 计算射击的距离
PSO 可以应用于各种其他问题,并具有不同的有效性。群聚优化是一种轻量级方法,用于寻找未知参数。如第五章所示,PSO 可以提供对深度学习系统超参数的简单优化。
4.2.2 学习练习
通过探索以下练习中的某些或全部内容来提高你的知识:
-
修改列表 4.10 中的目标距离。这会对 PSO 解有什么影响?
-
修改列表 4.7 中的
pmin、pmax、smin和smax输入,然后重新运行。 -
修改列表 4.7 中的
phi1和phi2参数,然后重新运行。这会对找到解决方案有什么影响?
现在我们已经涵盖了优化particles,在下一节中,我们将探讨更复杂的进化过程,例如相互依赖或协同进化的解决方案。
4.3 使用 DEAP 协同进化解决方案
我们星球上的生命存在于一种共生关系中,数百万物种相互依赖以生存。描述这种关系的术语有协同进化和相互依赖的进化。当我们试图解决更复杂的问题时,我们同样可以使用进化方法来模拟协同进化。
在下一个笔记本中,我们回顾了本章第一个例子中使用 GP 解决的回归问题。这次我们从玩具问题转向一个更接近现实世界的例子,使用一个名为波士顿住房(BH)市场的样本结构化数据集。
BH 数据集包含 13 个特征列,有助于预测波士顿市场的房价。当然,我们可以单独使用 GP 来尝试推导出一个方程,但结果会过于复杂。相反,在下面的例子中,我们考虑使用协同进化将 GP 与 GA 配对,希望得到一个更简单的输出。
4.3.1 使用遗传算法与遗传编程协同进化
这个练习在两个笔记本之间交替进行。第一个是从我们早期的 GP 例子升级到 EDL_4_GP_Regression.ipynb,用 BH 数据集替换了玩具问题。第二个笔记本,EDL_4_3_CoEV_Regression.ipynb,展示了使用协同进化来解决相同的问题——这次使用 GP 和 GA。
打开 EDL_4_GP_Regression.ipynb 笔记本,然后从菜单中选择运行 > 运行所有单元格。这个笔记本与 EDL_4_3_COEV_Regression.ipynb 之间的关键区别是使用了我们可以导入的 BH 数据集,如列表 4.13 所示。BH 数据集从sklearn.datasets加载,我们返回特征x和目标y。然后交换轴以适应行、特征格式和提取的输入数量。输入数量定义了导出方程中的参数数量。
列表 4.13 EDL_4_3_COEV_Regression.ipynb:设置数据
from sklearn.datasets import load_boston ❶
x, y = load_boston(return_X_y=True) ❷
x = np.swapaxes(x,0,1) ❸
inputs = x.shape[0] ❹
❶ 从 sklearn.datasets 模块导入
❷ 加载数据然后返回目标值 y
❸ 交换轴以匹配笔记本
❹ 提取输入数量、参数
图 4.6 显示了将方程进化到小于 135 的最小fitness分数的结果,这是一个用于简化解决方案的值。得到的方程表明,并非所有特征都在导出的方程中使用,只有 ARG5、ARG7、ARG10、ARG11 和 ARG12 是相关的。这也意味着 GP 解决方案自动通过忽略不那么相关的特征来执行特征选择。

图 4.6 GP_Regression 笔记本的输出
接下来,在 Colab 中打开 EDL_4_3_COEV_Regression.ipynb 笔记本,然后运行所有单元格。这个笔记本中有大量的代码,但我们之前在各种其他示例中都见过。这个例子中的主要区别在于我们同时使用 GP 和 GA 方法来精细调整一个派生的方程。这意味着代码量加倍,但其中大部分我们都见过。首先要注意的是,如下所示,我们构建了两个 toolboxes——一个用于 GA population,另一个用于 GP。
列表 4.14 EDL_4_3_COEV_Regression.ipynb:Toolbox 注册
toolbox_ga = base.Toolbox() ❶
toolbox_ga.register("float", random.uniform, -1, 1) ❷
toolbox_ga.register("individual",
tools.initRepeat, creator.IndGA,
➥ toolbox_ga.float, inputs) ❸
toolbox_ga.register("population",
tools.initRepeat, list, toolbox_ga.individual)
toolbox_gp = base.Toolbox() ❹
toolbox_gp.register("expr", gp.genHalfAndHalf, pset=pset, min_=1, max_=2)
toolbox_gp.register("individual",
tools.initIterate, creator.Individual, toolbox_gp.expr)
toolbox_gp.register("population",
tools.initRepeat, list, toolbox_gp.individual)
toolbox_gp.register("compile", gp.compile, pset=pset)
❶ 为 GA 种群创建工具箱
❷ 每个基因由一个介于 -1 到 +1 之间的单个浮点数定义。
❸ 基因序列的大小由输入数量定义(对于 BH 为 13)。
❹ 为 GP 种群创建工具箱
在这个例子中,GP 求解器正在努力构建一个派生方程。GA 求解器同时与一个大小等于输入/特征数量的缩放器 genes 序列协同进化。对于 BH 数据集,输入的数量等于 13。每个 GA 缩放器 gene 值用于缩放输入到方程中的特征。我们可以在下面的列表中看到的 evalSymbReg 评估函数中看到这一点。当此函数用于评估 fitness 时,我们传递两个 individuals。individual 输入代表一个 GP individual,而 points 输入代表一个 GA individual。这个函数的每次评估都是使用来自 GA 和 GP populations 的两个 individuals。
列表 4.15 EDL_4_3_COEV_Regression.ipynb:fitness 评估
def evalSymbReg(individual, points):
func = toolbox_gp.compile(expr=individual) ❶
p = np.expand_dims(points, axis=1) ❷
x = X * np.asarray(p) ❸
diff = math.sqrt(np.sum((func(*x.tolist()) - y)**2)) ❹
return diff,
❶ 从工具箱编译函数
❷ 将 GA 点数组从 (13,) 转换为 (13,1)
❸ 通过点数组缩放输入数据
❹ 计算派生方程的 fitness
通常,在协同进化场景中,你不会希望两种方法以相同的速率进化。在这个例子中,例如,我们允许 GA population(种群),缩放器,比 GP population(种群)进化得更快。这允许 GA 方法精细调整在派生 GP 方程中使用的参数的缩放或权重。本质上,这允许 GA 方法对方程进行精细调整以获得更好的拟合。对于这个笔记本,我们在 GA 到 GP 之间设置了一个进化的比率,为 10 到 1。这些超参数在下面的代码列表中设置。
列表 4.16 EDL_4_3_COEV_Regression.ipynb:控制进化步骤
GA_GEN, GP_GEN, BASE_POP = 1, 10, 10000 ❶
pop_ga = toolbox_ga.population(n=BASE_POP*GA_GEN) ❷
pop_gp = toolbox_gp.population(n=BASE_POP*GP_GEN)
❶ 控制生成频率的超参数
❷ 通过生成频率调整种群起始点
在进化代码中,我们通过以下列表中的代码控制每个 population 进化的频率。此代码块显示了 GA population 的进化,但对于 GP 也应用了相同的过程。超参数 GA_GEN 控制进化的频率。
列表 4.17 EDL_4_3_COEV_Regression.ipynb:进化 population
if (g+1) % GA_GEN == 0: ❶
off_ga = toolbox_ga.select(pop_ga, len(pop_ga))
off_ga = [toolbox_ga.clone(ind) for ind in off_ga]
for ind1, ind2 in zip(off_ga[::2], off_ga[1::2]): ❷
if random.random() < CXPB:
toolbox_ga.mate(ind1, ind2)
del ind1.fitness.values
del ind2.fitness.values
for ind in off_ga: ❸
if random.random() < MUTPB:
toolbox_ga.mutate(ind)
del ind.fitness.values
pop_ga = off_ga ❹
❶ 如果这是进化步骤,则进行进化。
❷ 交叉
❸ 突变
❹ 将后代分配以替换种群
记住,当我们评估适应度时,该函数需要来自两个种群——GA 和 GP 的个体。这意味着评估个体的代码与每个种群相关联。注意,当我们为 GA 或 GP种群评估适应度时,我们使用另一个种群的最佳表现者。这种简化近似了每个种群的最高适应度。另一种方法是循环遍历两个种群并测试每种组合。这种方法将是计算上昂贵的,所以我们退回到以下列表中显示的简化。
列表 4.18 EDL_4_3_COEV_Regression.ipynb:评估种群``适应度
for ind in pop_gp:
ind.fitness.values = toolbox_gp.evaluate
➥ (ind, points=best_ga) ❶
for ind in pop_ga:
ind.fitness.values = toolbox_gp.evaluate
➥ (best_gp, points=ind) ❷
❶ 使用 GA 种群中的最佳者评估 GP 适应度
❷ 使用 GP 种群中的最佳者评估 GA 适应度
图 4.7 显示了运行此示例到解决方案的最终输出,再次假设适应度低于 135。如图所示,推导出的方程已被显著简化。最佳遗传算法(GA)个体分配用于修改方程输入的缩放因子。在图 4.7 中,你可以看到最终方程如何应用输入缩放。

图 4.7 最佳协同进化解决方案
最终推导出的方程显示了预测 BH 市场价值的潜在解决方案。如果我们查看结果方程并参考笔记本中显示的 BH 特征,我们可以看到 ARG5 被识别为 NOX(氧化亚氮浓度),ARG12 被识别为 LSTAT(低阶层人口百分比),它们被确定为使用的主要特征。
如果你参考图 4.6 中显示的 GP 回归笔记本中解决的方程,你也会注意到 ARG5 和 ARG12 被认为与 ARG7、ARG10 和 ARG11 一样是重要特征。协同进化解决方案能够进一步通过传递到方程中的输入减少特征权重。这导致了一个可能过于简化的方程,但有了它,我们可以在 BH 数据集中识别 NOX 和 LSTAT 特征之间的关键相关性。
GP 回归结果
随着遗传规划的进化,它通常会创建更复杂或过于复杂的方程。长时间运行的进化甚至可能超出表达式树的大小。将遗传算法(GA)引入遗传规划允许方程进行微调。然而,这可能会导致问题的过度简化。数据集的大小也可能特别有问题,因为 BH 集只有 500 多行。在大多数情况下,你将得到更多数据时的更好结果。
现在,我们刚刚评估的协同进化解决方案并非完美无缺。它确实解决了在遗传规划(GP)中经常遇到的复杂性问题。然而,显然最终的答案可能过于简单,缺少其他关键特征,无法推断出足够的准确性。但这也足够简单,可以用基本的计算器即时计算。
我们将在后续章节中使用其他协同进化的解决方案来平衡应用于 DL 的多种形式的 EC。正如我们在前面的练习中看到的,协同进化可以将多种形式的 EC 绑定在一起来解决一个共同的复杂问题。在后续的例子中,我们花费时间掌握平衡协同进化的方法。
4.4 使用 DEAP 的进化策略
进化策略是进化方法和遗传方法的扩展,它添加了控制子基因或表型,称为 策略。这些策略只不过是一个额外的向量,它控制或影响 mutation 操作符。这为 ES 提供了更有效地解决各种复杂问题的能力,包括函数逼近。
在下一个笔记本中,我们探索一个函数逼近问题,我们稍后将在查看使用深度学习(DL)的进化时再次回顾这个问题。为了保持简单,我们在这里查看逼近已知连续多项式解的函数参数。然后,我们转向更复杂的不连续解,并观察 ES 的表现如何。
4.4.1 将进化策略应用于函数逼近
ES 与“vanilla” GAs 的不同之处在于一个 individual 携带一个额外的 gene 序列或向量,称为策略。在进化的过程中,这个策略向量学会调整和应用更好的、微调的 mutation 来优化 individual 的进化。
正如我们在第三章中发现的,mutation 和 mutation 率类似于 DL 中的学习率。mutation 控制进化过程中 population 的变异性。mutation 率越高,population 的变异性就越大,多样性也越高。能够控制并在迭代中学习这个 mutation 率使我们能够更有效地确定解决方案。
在接下来的笔记本中,我们设置了一个 ES 算法来逼近已知解。我们还讨论了如何学习在时间上优化 mutation,这允许 population 更好地收敛并逼近解。让我们首先打开 Google Colab 中的笔记本 EDL_4_4_ES.ipynb 并运行整个笔记本。
进化策略是 GA 的扩展,因此我们使用 DEAP 需要的大部分代码与我们之前看到的类似。我们在这里回顾关键差异,重点关注 ES 的实现,从超参数定义开始。IND_SIZE 值控制解决的多项式函数的维度或,实际上是 gene 的大小。MAX_TIME 超参数用于控制进化的总时间。这是一种有效控制进化运行时间的方法,而不是依赖于 generations 的数量。最后,策略分配超参数 MIN_VALUE、MAX_VALUE、MIN_STRATEGY 和 MAX_STRATEGY 控制突变向量,并在以下列表中进一步讨论。
列表 4.19 EDL_4_4_ES.ipynb:检查超参数
IND_SIZE = 6 ❶
NGEN = 1000 ❷
MIN_VALUE = 4 ❸
MAX_VALUE = 5
MIN_STRATEGY = 0.5
MAX_STRATEGY = 3 ❸
CXPB = .6 ❹
MUTPB = .3 ❹
GEN_OUTPUT = 25 ❺
MAX_TIME = 100 ❻
❶ 解决的多项式维度
❷ 最大进化代数数量
❸ 控制策略分配的值
❹ 交叉和变异率
❺ 产生输出的代数数量
❻ 进化运行的最大时间
继续到下一个单元格,我们可以看到如何构建初始的目标数据集。在这个练习中,如列表 4.20 所示,我们提供了三个选项或方程进行评估:一个五次多项式连续函数和两个不连续函数,abs 和 step。数据通过范围参数进行处理,生成 X 和 Y 值,这些值被压缩到一个名为 data 的列表中。在单元格底部,我们绘制了数据的散点图以可视化目标函数。
列表 4.20 EDL_4_4_ES.ipynb:准备数据
equation_form = "polynomial" #@param ["polynomial",
➥ "abs", "step"] ❶
X_START = -5 ❷
X_END = 5 ❷
X_STEP = 0.5 ❷
def equation(x): ❸
if equation_form == "polynomial":
return (2*x + 3*x**2 + 4*x**3 + 5*x**4 + 6*x**5 + 10)
elif equation_form == "abs":
return abs(x)
else:
return np.where(x>1, 1, 0)
X = np.array([x for x in np.arange(X_START, X_END, X_STEP)]) ❹
Y = equation(X) ❺
data = list(zip(X, Y))
plt.scatter(X,Y) ❻
❶ 提供三个目标方程的选项
❷ x 的数据集范围值
❸ 评估目标方程的函数
❹ 构建输入 X 值
❺ 运行方程然后生成 Y 值
❻ 在散点图中绘制函数
图 4.8 展示了五次多项式函数的图像,以及步进和绝对函数。让我们首先针对连续的多项式函数进行目标定位,看看 ES 如何有效地逼近一个解。其他两个函数代表的是不连续的函数,它们不可导,因此通常不能通过 DL 网络求解。

图 4.8 函数逼近选项
接下来,我们看看代码块中的 creator 部分,如列表 4.21 所示,以了解 ES 与典型 GA 的不同之处。我们可以看到 FitnessMin 和 Individual 正常注册,但有一个区别。当 individual 被注册时,我们添加了一个名为 Strategy 的属性,设置为 None。Strategy 最后注册,类型为 double d 数组。
列表 4.21 EDL_4_4_ES.ipynb:创建 Individual 和 Strategy
creator.create("FitnessMin", base.Fitness, weights=(-1.0,))
creator.create("Individual", array.array, typecode="d",
➥ fitness=creator.FitnessMin, strategy=None) ❶
creator.create("Strategy", array.array, typecode="d") ❷
❶ 创建一个具有策略的数组类型的个体
❷ 创建一个数组类型的策略
我们现在跳到设置 toolbox 的单元格,如列表 4.22 所示。我们首先注意到使用 generatES 函数初始化一个 individual,输入为 creator.Individual、creator.Strategy、IND_SIZE、MIN_VALUE、MAX_VALUE、MIN_STRATEGY 和 MAX_STRATEGY。交叉 或 mate 操作使用特殊的 ES 混合算子来组合父代,而不是在正常 交叉 中找到的替换算子。同样,变异 操作使用 ES 对数正态操作来控制策略的变异。然后在代码块的底部,我们可以看到一个装饰器被应用于 mate 或 mutate 操作符。装饰器为输入提供了一个过滤机制,在这种情况下,我们使用了稍后展示的 checkStrategy 函数。
列表 4.22 EDL_4_4_ES.ipynb:设置 toolbox
toolbox = base.Toolbox()
toolbox.register("individual", generateES, creator.Individual,
➥ screator.Strategy,
IND_SIZE, MIN_VALUE, MAX_VALUE, MIN_STRATEGY, MAX_STRATEGY) ❶
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
toolbox.register("mate", tools.cxESBlend, alpha=0.1) ❷
toolbox.register("mutate", tools.mutESLogNormal, c=1.0, indpb=0.03) ❸
toolbox.register("select", tools.selTournament, tournsize=3)
toolbox.decorate("mate", checkStrategy(MIN_STRATEGY)) ❹
toolbox.decorate("mutate", checkStrategy(MIN_STRATEGY)) ❹
❶ 使用生成输入的功能注册个人
❷ 矩阵/交叉算子是 cxESBlend。
❸ 变异算子是 mutESLogNormal。
❹ 使用 checkStrategy 装饰 mate/mutate
跳到上一个单元,我们可以看到 generateES 和 checkStrategy 函数的定义,如列表 4.23 所示。第一个函数使用传递给函数的输入创建 individual,其中输入 icls 代表用于构建 individual 的类,而 scls 代表用于构建策略的类。第二个函数使用装饰器模式检查策略,以确保向量保持在某个最小值之上。使用初始化的 individual 设置,gene 序列中的每个随机值都设置在最小值和最大值之间。同样,策略的初始化遵循相同的模式,使用不同的最小/最大值。这产生了一个具有两个大小为 IND_SIZE 或 NDIM 的向量 individual——一个用于定义主要的 gene 序列,另一个作为在 mate 和 mutate 操作符期间应用于每个 gene 的学习 mutation 和混合率。
列表 4.23 EDL_4_4_ES.ipynb:核心函数
def generateES(icls, scls, size, imin, imax,
➥ smin, smax): ❶
ind = icls(random.uniform(imin, imax) for _ in range(size))
ind.strategy = scls(random.uniform(smin, smax) for _ in range(size))
return ind
def checkStrategy(minstrategy): ❷
def decorator(func):
def wrappper(*args, **kargs):
children = func(*args, **kargs)
for child in children:
for i, s in enumerate(child.strategy):
if s < minstrategy:
child.strategy[i] = minstrategy
return children
return wrappper
return decorator
❶ 根据输入参数创建个体
❷ 确保策略保持在边界内的装饰器
我们需要添加的最后 toolbox 注册是用于评估 fitness。在这个块中,如列表 4.24 所示,有两个函数。第一个函数 pred 用于通过遍历 individual genes 并将它们乘以 x 的因子 i 来推导一个值。另一个函数 fitness 使用 pred 函数遍历 data 中的 x,y 值,以确定均方误差,返回的最终值是平均 MSE。注意在这个例子中,我们如何通过在 register 函数中将它作为参数传递给 evaluate 函数来将数据集传递给 evaluate 函数。
列表 4.24 EDL_4_4_ES.ipynb:评估 fitness
def pred(ind, x): ❶
y_ = 0.0
for i in range(1,IND_SIZE):
y_ += ind[i-1]*x**I ❷
y_ += ind[IND_SIZE-1]
return y_
def fitness(ind, data): ❸
mse = 0.0
for x, y in data:
y_ = pred(ind, x)
mse += (y - y_)**2 ❹
return mse/len(data),
toolbox.register("evaluate", fitness, data=data)
❶ 从个体和 x 生成预测
❷ 计算多项式因子 i
❸ 计算适应度的函数
❹ 评估总均方误差
如同往常,进化代码位于最后一个块中,如列表 4.25 所示,应该看起来很熟悉。我们首先定义了两个超参数,MU 和 LAMBDA,它们分别代表父母的 population 和衍生后代的数量。这意味着在 selection 中,我们使用 DEAP 算法 eaMuCommaLambda 从 MU 个父母生成 LAMBDA 个后代。对于这个练习,我们不仅通过总 generations 限制,还通过已过时间来限制。如果已过时间(以秒为单位)超过阈值 MAX_TIME,则进化停止。跟踪已过时间使我们能够评估比较 EC 方法,正如我们在下一个练习中所看到的。
列表 4.25 DL_4_4_ES.ipynb:进化
MU, LAMBDA = 250, 1000 ❶
#omitted
start = time.time() ❷
for g in range(NGEN):
pop, logbook = algorithms.eaMuCommaLambda(pop, toolbox, mu=MU,
lambda_=LAMBDA,cxpb=CXPB,
mutpb=MUTPB, ngen=1, ❸
stats=stats, halloffame=hof, verbose=False)
if (g+1) % GEN_OUTPUT == 0: ❹
plot_fitness(g, hof[0], pop, logbook)
end = time.time()
if end-start > MAX_TIME: ❺
break
print("Best individual is ", hof[0], hof[0].fitness.values[0])
❶ 定义种群起始和后代
❷ 跟踪进化开始的时间
❸ 在单一代中使用 eaMuCommaLambda
❹ 通过代数数量限制输出
❺ 检查模拟时间是否已到
图 4.9 显示了运行进化到最大 5 秒后的最终输出示例,这是很好的。然而,如果我们把输入数据绘制到 Excel 中,我们同样可以快速地使用趋势线功能生成准确的功能逼近,而且时间更短。目前 Excel 限制为 6 次多项式函数,而我们可以通过这个例子快速超越这一点。

图 4.9 ES 函数逼近的示例输出
到目前为止,你可以返回并调整进化的运行时间,看看是否可以得到更好的结果,或者尝试其他函数 abs 和 step。你可能会发现,ES 在处理不连续解方面并不那么有效。这主要是因为算法近似函数的方式。
然而,如果我们把 ES 与第三章中的先前练习进行比较,我们会看到连续问题有更快的收敛速度。这是因为 ES 通过学习到的 变异 和 交配 策略来管理 种群 的多样性。如果你将图 4.9 中的输出直方图与之前的示例练习进行比较,就可以看到这一点。
4.4.2 回顾 EvoLisa
函数逼近是一个很好的基线问题,但要看到 ES 的全部威力,在本节中,我们重新审视了我们之前最复杂的问题之一:EvoLisa。在这里,我们修改了问题,采用 ES 作为我们的解决方案策略。这是一个非常快速的例子,它对 ES 和常规 GA 之间的比较非常有用。
在 Colab 中打开笔记本 EDL_4_4_EvoLisa.ipynb。如果你需要帮助,请参阅附录。然后运行笔记本中的所有单元格(从菜单中选择“运行”>“运行所有”)。
我们已经介绍了笔记本 EDL_3_5_EvoLisa.ipynb 和 EDL_4_4_ES.ipynb 中的主要代码元素。这个笔记本展示了如何将 GA 笔记本升级以使用 ES。
让笔记本运行几千个 世代,以查看图 4.10 所示的显著改进。图中还显示了使用“vanilla” GA 在双倍 世代 数——7,000 比 3,000 产生的结果比较。

图 4.10 使用 ES 与之前的 GA 解决方案相比的 EvoLisa 输出
4.4.3 学习练习
以下练习可以帮助你进一步理解进化策略的概念:
-
修改列表 4.20 中的目标函数,然后重新运行以查看这会产生什么影响。
-
修改列表 4.19 中的几个参数,看看每个变化对结果进化的影响。尝试使用函数逼近和 EvoLisa 笔记本版本都进行这一操作。
-
将这个版本的 EvoLisa 的结果与我们在第三章中介绍的 GA 示例进行比较。ES 引导的
变异增强对输出有多大的改进? -
进化出你能做到的最佳蒙娜丽莎复制品。鼓励你将你的结果与作者联系。
现在,你可能从这个最后的笔记本中得到的启示是将我们所有的解决方案升级到使用 ES(进化策略)。虽然 ES 是一个非常好的进步,我们可以将其保留在我们的工具箱中,但它仍然缺乏快速有效地收敛不连续解的能力。要做到这一点,我们需要了解常规 GA 和修改后的 ES 在解决更复杂函数时遇到的困难。这是我们将在下一个练习中进一步探讨的内容。
4.5 使用 DEAP 的差分进化
深度学习系统通常被简单地描述为好的函数或凸逼近器。函数逼近绝对不仅限于深度学习,但目前在大多数解决方案中排名第一。
幸运的是,进化计算(EC)包括几种方法。它不仅限于连续解,也可以解决不连续解。一种专注于连续和不连续解的函数逼近方法是差分进化(DE)。DE 不是基于微积分的,而是依赖于减少优化解之间的差异。
在我们的下一个笔记本中,我们使用差分进化来逼近我们上一个练习中已知的一个连续多项式解以及基本的不连续和复杂函数的例子。这为我们提供了另一个工具,当我们考虑在以后构建与 DL 结合的解决方案时。
4.5.1 使用 DE 逼近复杂和不连续函数
差分进化(Differential evolution)与 PSO(粒子群优化)比 GAs(遗传算法)或编程有更多的共同点。在差分进化中,我们维护一个种群的代理,每个代理的大小都是一些相等的向量大小。像 PSO 一样,代理是长期运行的,并且不产生后代,但它们的分量向量通过与其他随机代理的差异比较来修改,以产生新的更好的代理。
图 4.11 显示了 DE 的基本工作流程。在这个图的开头,从更大的代理池中随机选择了三个代理。然后,这三个代理被用来通过取第一个代理a并将其值加到代理b和c之间的缩放差异上来修改每个索引值的目标Y。结果Y代理被评估为fitness,如果这个值更好,那么就用新的代理Y替换它。

图 4.11 产生新代理的 DE 工作流程
这种方法微妙地增强了其功能,以及为什么它对不连续函数如此有效的原因,在于计算个体维度差异。与通常需要混合结果(如深度学习中的 DL)或泛化结果(如遗传进化)的正常优化函数不同,差分进化进行的是分量级的微分。
在深度学习(DL)中,我们用于在训练期间反向传播错误或差异的梯度优化方法是一个全局优化问题。DE 将优化提取为值的分量级微分,因此不受全局方法的限制。这意味着 DE 可以用来逼近不连续或困难的函数,正如我们将看到的。
对于下一个场景,在 Colab 中打开笔记本 EDL_4_5_DE.ipynb 并运行所有单元格。此示例与上一个练习中的相同问题集工作。因此,我们有三个问题可以对此示例进行测试:一个多项式、绝对值和步进函数。为了比较,我们首先运行我们刚刚看过的相同的多项式函数逼近示例。
对于 5 秒的 MAX_TIME 运行整个示例,与刚刚看到的 ES 示例相比,输出的是一个不错但并不出色的函数逼近。完成之后,我们想要通过更改准备数据中的函数类型(如列表 4.20 所示)来运行绝对值或步进函数示例,使用 Colab 表单下拉菜单来选择一个不连续的函数。
在重新运行笔记本之前,我们想要修改最大时间超参数,如下所示列表所示。将 MAX_TIME 值从 5 秒更改为类似 100 的值。绝对值和步进函数由于更复杂,需要更多时间。
列表 4.26 EDL_4_5_DE.ipynb:超参数
NDIM = 6 ❶
CR = 0.25 ❷
F = 1 ❸
MU = 300 ❹
NGEN = 1000
GEN_OUTPUT = 25
MAX_TIME = 100 ❺
❶ 代理中的维度数量
❷ 类似于遗传操作中的交叉率
❸ 差分缩放因子 [0.0, 2.0]
❹ MU,或代理数量
❺ 限制模拟时间的时长
设置函数后,使用菜单中的“运行时”>“工厂重置运行时”重置笔记本的运行时。然后,使用菜单中的“运行时”>“运行所有”重新运行笔记本中的所有单元格。
为了获得良好的比较,回到 EDL_4_ES.ipynb,然后将 MAX_TIME 更改为 100 秒,将目标函数设置为步进,重新启动运行时,并重新运行所有单元格。图 4.12 显示了 DE 和 ES 对步进函数的运行差异。值得注意的是,DE 方法比 ES 方法表现好了 10 多倍,这与微分方法有关。另一方面,注意 ES 直方图是正态分布的,而 DE 图表分布类似于狭窄的帕累托或柯西分布。

图 4.12 DE 和 ES 在步进函数上的比较
接下来,我们可以查看合并代码列表中的 creator 和 toolbox 设置。对于 toolbox,我们注册一个类型为 float 的属性,初始值为 -3 到 +3,类似于遗传进化中的 gene。然后,我们定义类型为 float 且大小为 NDIM 或维度数量的 individual 或代理。在以下代码列表的末尾,我们可以看到一个已注册的选择函数,它使用随机方法选择三个元素。回想一下图 4.11,其中我们选择三个代理(a、b、c)来应用微分算法。
列表 4.27 EDL_4_5_DE.ipynb:creator 和 toolbox
creator.create("FitnessMin", base.Fitness, weights=(-1.0,))
creator.create("Individual", array.array,
typecode='d', fitness=creator.FitnessMin)
toolbox = base.Toolbox()
toolbox.register("attr_float", random.uniform, -3, 3) ❶
toolbox.register("individual", tools.initRepeat, creator.Individual,
toolbox.attr_float, NDIM) ❷
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
toolbox.register("select", tools.selRandom, k=3) ❸
❶ 每个维度或属性初始化为 -3 到 +3。
❷ 由大小为 NDIM 的浮点数定义的个体/代理
❸ 选择方法使用随机并选择 k=3。
在这个例子中的大部分代码与之前的 ES 练习共享,因为我们正在解决相同的问题。回顾这两个样本之间的关键差异,以了解每个方法由哪些元素组成。
我们的模拟训练代码位于笔记本的底部,但我们只需要关注使 DE 独特的部分,如列表 4.28 所示。代码中有两个for循环——第一个循环遍历代数的数量,第二个循环遍历每个代理。在内循环中,我们首先采样三个代理(a, b, c),然后将代理克隆为目标y。然后,我们从一个代理的向量中选择一个随机索引,并使用该索引与CR值来确定是否计算可能的差异,如图 4.10 所示。最后,我们检查新代理的fitness是否更好,如果是,我们就用新代理替换旧代理。
列表 4.28 EDL_4_5_DE.ipynb:代理可微分模拟
for g in range(1, NGEN):
for k, agent in enumerate(pop): ❶
a,b,c = toolbox.select(pop) ❷
y = toolbox.clone(agent) ❸
index = random.randrange(NDIM) ❹
for i, value in enumerate(agent):
if i == index or random.random() < CR: ❺
y[i] = a[i] + F*(b[i]-c[i])
y.fitness.values = toolbox.evaluate(y)
if y.fitness > agent.fitness: ❻
pop[k] = y
hof.update(pop)
❶ 枚举种群中的每个代理
❷ 从种群中随机选择三个代理
❸ 将代理克隆到 y
❹ 从代理的向量中选择一个随机索引
❺ 匹配向量索引并检查交叉 CR
❻ 评估 y 的适应度,如果它更好,则替换当前代理
随意返回并尝试比较 ES 和 DE 方法之间的绝对函数。你还可以尝试调整超参数,看看它们对使用 ES 和 DE 逼近函数有什么影响。
4.5.2 学习练习
通过完成以下练习,继续探索最后一个笔记本:
-
修改列表 4.26 中的各种超参数,然后重新运行。你是否能够提高不连续函数逼近的性能?
-
比较 ES 和 DE 对各种函数的函数逼近结果。对于哪些类型的函数,哪种方法看起来表现更好或更差?
DE 和 ES 都为连续问题提供了优秀的函数逼近器。对于不连续问题,通常应用 DE 更好,因为它不受限于全局空间中的渐进逼近。正如我们在后面的章节中讨论的,当应用 EDL 时,拥有这两种工具可以使我们的工作更容易。在本章中,我们扩展了我们对 EC 的知识,并探讨了更多样化和专业的方法,这些方法可以解决新颖或难以解决的问题。
摘要
-
遗传编程是使用遗传序列定义一个过程或步骤的程序。
-
DEAP 采用了一种遗传编程扩展,使得将问题从 GA 转换为 GP 变得容易。GP 的一个应用是推导出已知或未知问题的方程。
-
DEAP 还提供了可视化工具,允许将个体基因序列解释为基因表达树,并评估其如何对应于一个函数。
-
DEAP 提供了几个二级进化算法。其中一个例子是粒子群优化:
-
粒子群优化使用一个
个体的种群在解空间中游动。 -
当
粒子群游动时,更适应的个体引导群体关注更好的解决方案。 -
PSO 可以用于寻找函数或更复杂问题的解参数。
-
-
DEAP 支持协同进化场景。这是指在特定问题时,识别出两个或更多
个体的种群来处理独特的任务。协同进化可以通过在导出的方程中最小化和缩放特征来找到复杂解。 -
进化策略是遗传算法的扩展,它强调战略性地更新
变异函数。这种方法对于解决或引导需要具有大或复杂遗传结构或序列的个体的解决方案非常有效。 -
差分进化与 PSO 类似,但仅使用三个代理来三角化和缩小解决方案搜索空间。DE 在复杂问题中表现良好,这些问题使用较短的遗传序列。在 DEAP 中使用差分进化来解决连续和断续函数逼近问题。
第二部分. 优化深度学习
在本书的这一部分,我们探讨可用于优化和改进深度学习系统的进化算法和遗传算法。我们从第五章开始,解决深度学习中的一个核心问题:超参数优化。本章展示了从随机搜索和网格搜索到遗传算法、粒子群优化、进化策略和微分进化的各种方法。
在第六章,我们转向神经进化,以优化深度学习架构和参数。我们演示了如何在无需使用反向传播或深度学习优化器的情况下优化网络参数或权重。
然后在第七章,我们继续展示神经进化在增强卷积神经网络架构和参数方面的应用。接着,我们探讨使用遗传算法和自定义架构编码来开发 EvoCNN 网络模型。
5 自动化超参数优化
本章涵盖
-
开发一个手动优化深度学习网络超参数的过程
-
使用随机搜索构建自动超参数优化
-
通过使用网格搜索算法形式化自动 HPO
-
使用 PSO 将进化计算应用于 HPO
-
通过使用进化策略扩展进化超参数优化
-
将 DE 应用于 HPO
在过去几章中,我们一直在探索各种进化计算形式,从遗传算法到粒子群优化,甚至更高级的方法如进化策略和差分进化。我们将在本书的其余部分以某种形式继续使用所有这些 EC 方法来改进深度学习。我们将这些方法结合成一个我们俗称为“进化深度学习”(EDL)的过程。
然而,在构建一系列针对各种深度学习问题的 EDL 解决方案之前,如果我们不了解我们试图解决的问题以及它们在没有 EC 的情况下是如何解决的,那么我们将犯下错误。毕竟,EC 工具只是我们可以用来改进深度学习的大量“工具箱”中的一小部分。因此,在我们开始应用 EC 方法到 HPO 之前,我们首先探讨超参数优化的重要性以及一些手动策略。其次,在考虑自动 HPO 时,我们希望通过首先回顾其他搜索方法,如随机搜索和网格搜索,来建立一个基线。
5.1 选项选择和超参数调整
深度学习实践者面临的最困难问题之一是确定哪些选项和“旋钮”需要调整以改进他们的模型。大多数致力于教授深度学习的文本都涉及众多选项和超参数,但很少详细说明变化的影响。这种情况被一个展示最先进模型的 AI/ML 社区所加剧,这些模型往往省略了达到这些模型所需的大量工作。
对于大多数实践者来说,学习如何使用众多选项和调整超参数主要来自于构建模型时的数小时经验。没有这种调整,正如上一节所展示的,许多此类模型可能会存在严重缺陷。这不仅是一个新手的难题,也是深度学习领域本身的问题。
我们首先研究一个使用 PyTorch 来近似函数的基本深度学习模型。本书后面的例子将使用 Keras 和/或 PyTorch 来展示这些技术如何在框架之间轻松互换。
5.1.1 超参数调整策略
在本节中,我们探讨一些技术和策略来选择选项和调整深度学习模型的超参数。其中一些是从多年经验中获得的,但意识到这些策略需要发展。深度学习不断增长,新的模型选项也在不断被采用。
深度学习知识
本书假设你理解基本深度学习原理,例如感知器、多层感知器、激活函数和优化方法。如果你觉得需要复习一下深度学习的基础,网上和 Manning Publications 出版的资源有很多。
添加了一些关键差异以展示如何处理超参数和其他选项,以供以下练习使用。在浏览器中打开 EDL_5_1_Hyperparameter_Tuning.ipynb 笔记本。如果需要帮助,请查阅附录。
首先,使用菜单中的“运行”>“运行所有”来运行整个笔记本。确认输出类似于图 5.1 中的初始函数和预测解。

图 5.1 数据点和目标函数的解
接下来,向下滚动,查看如图 5.1 所示的超参数代码块。这里添加了两个新的超参数:batch_size和data_step。第一个超参数batch-size决定了每次前向传递我们向网络输入的输入数量。回想一下,在上一个练习中,这个值是1。另一个超参数data_step不是一个典型的超参数,但它允许我们控制为训练生成数据的数量。
列表 5.1 EDL_5_1_Hyperparameter_Tuning.ipynb:超参数
hp_test = "test 1" #@param {type:"string"} ❶
learning_rate = 3.5e-03
epochs = 500
middle_layer = 25
batch_size = 2 ❷
data_step = 1 ❸
❶ 形成设置测试名称的参数
❷ 单次前向传递中要输入的元素数量
❸ 控制数据生成中数据样本的频率
将测试名称hp_test更改为类似test 2的内容。然后修改middle_layer的值到25或更大。运行该单元格,然后使用“运行”>“运行后续单元格”运行笔记本中的剩余单元格。
图 5.2 显示了两个测试的预测输出,其中也显示了test 2的输出拟合得更好。注意训练模型的时间略有差异。这种差异来自更大的模型需要更多时间来训练。

图 5.2 比较中间层超参数调整的差异
你现在可以返回并修改其他超参数——batch_size和data_step。但请注意,这些值是相互关联的,如果你通过将data_step减小到.1来大幅增加数据量,那么你同样需要增加batch_size。
图 5.3 显示了在增加数据量时改变和未改变批量大小时的结果。图 5.3 中显示的结果与完成 500 个 epoch 的训练时间相比非常显著。

图 5.3 比较改变数据大小,有和无批量大小变化的情况
继续更改测试名称,修改超参数,然后通过“运行”>“运行后续”来运行剩余的单元格。如果你发现图表变得过于混乱,你可以通过从菜单中运行“运行”>“运行所有”来重置图表结果。图 5.4 显示了将learning_rate从3.5e-06更改为3.5e-01的示例。你在调整超参数时的总体目标是创建一个最小的模型,该模型可以最快地训练并产生最佳结果。

图 5.4 改变学习率的影响
即使在我们只有五个超参数的简单例子中,你也可能感到难以确定从哪里开始,因此一个好的起点是遵循以下步骤:
-
设置网络大小—在我们的例子中,这是修改
middle_layer值。通常,你将想从调整网络大小和/或层数开始。但请注意,增加线性层数的重要性不如增加层中的网络节点数。超参数训练规则 #1:网络大小
增加网络层数以从数据中提取更多特征。扩展或减小模型宽度(节点)以调整模型的拟合度。
-
理解数据变异性—我们期望深度学习模型需要消耗大量数据。虽然深度学习模型确实可能从更多数据中受益,但成功更多地取决于源数据的变异性。在我们的例子中,我们能够通过使用
data_step值来控制数据的变异性。然而,控制数据变异性的能力通常并不是一个选项。反过来,如果你的模型高度变异,因此需要更多数据,你可能会需要增加模型在层数和/或宽度上的大小。例如,手写数字图片,如 MNIST 数据集,比 Fashion-MNIST 数据集中描述的时尚图片具有更少的变异性。超参数训练规则 #2:数据变异性
理解你的源数据的变异性。更多变性的数据需要更大的模型来提取更多特征或学习如何拟合更复杂的解决方案。
-
选择批量大小—正如我们在例子中所看到的,调整模型的批量大小可以使其训练效率显著提高。然而,这个超参数并不是修复训练性能的万能药,增加它可能会对最终结果产生不利影响。相反,批量大小需要根据输入数据的变异性进行调整。更多变性的输入数据通常从较小的批量大小中受益,范围在 16-64 之间,而较少变性的数据可能从较大的批量大小中受益,范围在 64-256 之间——甚至更高。
超参数训练规则 #3:批量大小
如果输入数据高度变异,则减小批量大小。对于较少变异和更均匀的数据集,增加批量大小。
-
调整学习率—学习率控制模型学习的速度,通常是新入门者首先滥用的超参数。与批量大小类似,学习率由模型的复杂度决定,这种复杂度是由输入数据的方差驱动的。更高的数据方差需要更小的学习率,而更均匀的数据可以支持更高的学习率。这在图 2.6 中得到了很好的体现,我们可以看到,由于数据非常均匀,模型从更高的学习率中受益。调整模型大小可能还需要降低学习率,因为模型复杂度增加。
超参数训练规则 #4:学习率
调整学习率以匹配输入数据的变异性。如果你需要增加模型的大小,通常,也需要降低学习率。
-
调整训练迭代次数—如果你正在处理较小的问题,你通常会看到模型快速收敛到某个基本解决方案。从那时起,你可以简单地减少模型的 epoch 数(训练迭代次数)。然而,如果模型更复杂且训练时间更长,确定总的训练迭代次数可能会更成问题。幸运的是,大多数深度学习框架提供了早期停止机制,它会监视损失的一些任意值,当达到该值时,将自动停止训练。因此,一般来说,你通常会想选择你认为需要的最高训练迭代次数。另一个选项是让模型定期保存其权重。然后,如果需要,可以重新加载相同的模型并继续训练。
超参数训练规则 #5:训练迭代次数
总是使用你认为需要的最高训练迭代次数。使用早期停止和/或模型保存等技术来减少训练迭代次数。
使用这五条规则来指导你在训练超参数时,但请注意,提到的技术仅是一般性指南。可能会有网络配置、数据集和其他因素改变这些一般规则。在下一节中,我们将进一步探讨在构建鲁棒模型时可能需要决定的各种模型选项。
5.1.2 选择模型选项
除了超参数之外,调整模型的最大来源是你在内部决定使用的各种选项。深度学习模型提供了许多选项,有时由问题或网络架构决定,但通常,细微的变化会极大地改变模型拟合的方式。
模型选项的范围从激活和优化器函数到层数量和尺寸的增加。如前所述,层深度通常由模型需要提取和学习的特征数量决定。层的类型,无论是卷积还是循环网络,通常由需要学习的特点类型决定。例如,我们使用 CNN 层来学习特征簇,使用 RNN 来确定特征如何对齐或按什么顺序排列。
因此,大多数深度学习模型的网络大小和层类型是由数据的方差和需要学习的特点类型驱动的。对于图像分类问题,CNN 层用于提取视觉特征,如眼睛或嘴巴。另一方面,RNN 层用于处理语言或时间数据,其中需要理解一个特征如何与另一个特征在序列中相关联。
这意味着在大多数情况下,深度学习从业者需要关注的选项是激活、优化和损失的基础函数。激活函数通常由问题的类型和数据的形式决定。我们通常避免在调整的最后步骤之前更改激活函数。
最常见的是,优化器和损失函数的选择决定了模型是否能够良好地训练。以图 5.5 为例,它显示了选择三个不同的优化器来训练我们最后一个练习的结果,使用middle_layer超参数为 25。注意在图中,与 Adam 和 RMSprop 相比,随机梯度下降(SGD)和 Adagrad 的表现相当差。

图 5.5 优化器函数的比较
同样,用于评估网络学习的损失函数的形式可以对模型训练产生重大影响。在我们的简单回归示例中,我们有两个选项:均方误差和平均绝对误差,或者 L1 损失。图 5.6 显示了在最后一个样本练习中使用的两个损失函数的比较。从图中可以看出,最后一个练习中更好的损失函数可能是 L1 损失,或 MAE。

图 5.6 损失标准的比较
超参数训练规则 #6:更改模型
作为一般规则,每当模型架构或关键模型选项,如优化和/或损失函数发生变化时,您需要重新调整所有超参数。
到现在为止,希望您已经意识到,如果没有非常敏锐的视角和对细节的关注,很容易错过找到适合您问题的最佳模型。事实上,您可能会花费无数小时调整模型超参数,最终发现更好的损失或优化函数可能表现得更好。
超参数调整和模型选项选择容易出错,即使在经验丰富的深度学习者中也是如此。在第四章中,我们首先通过使用进化计算来训练模型超参数介绍了 EDL。
当涉及到构建工作的深度学习模型时,你通常会定义模型并选择你认为最适合你问题的选项。然后,你可能需要调整和微调各种超参数,从之前提到的策略开始。然而,不幸的是,这种情况经常发生,你可能会决定更改模型选项,如优化器或损失函数。这反过来又通常要求你根据更改的模型回过头来重新调整超参数。
5.2 使用随机搜索自动化 HPO
我们刚刚看了使用函数逼近问题进行深度学习手动 HPO。在这种情况下,我们提供了一套工具,让从业者可以连续运行笔记本,使用不同的超参数生成比较。正如你很可能从完成那个练习中发现的那样,手动 HPO 既耗时又相当无聊。
当然,现在有众多工具可以自动执行 HPO。这些工具从 Python 包到作为 AutoML 解决方案一部分集成到云技术中的完整系统都有。我们当然可以使用这些工具中的任何一个来对 EC 方法进行基线比较,但就我们的目的而言,我们希望更深入地理解自动化和搜索过程。
随机搜索 HPO,正如其名所示,是从给定范围内的已知超参数集中采样随机值并评估其有效性的过程。随机搜索的希望是最终找到最佳或期望的解决方案。这个过程可以比作某人蒙着眼睛投掷飞镖,希望击中靶心。蒙眼的人可能不会在几次投掷中击中靶心,但经过多次投掷,我们预计他们可能会。
5.2.1 将随机搜索应用于 HPO
Notebook EDL_5_2_RS_HPO.ipynb 是我们之前笔记本的升级版本,它使用简单的随机搜索算法自动化 HPO。在 Colab 中打开该笔记本,然后通过菜单中的“运行”>“运行所有”来运行所有单元格。作为比较,你可以自由打开 EDL_5_1_Hyperparameter_Tuning.ipynb 笔记本。
让我们首先探索我们希望我们的深度学习网络逼近的问题函数。代码的第一个单元格回顾了第四章中的多项式函数,如图 5.1 所示。以下列表包含生成训练网络的输入和目标数据点的样本集的代码。
列表 5.2 EDL_5_2_RS_HPO.ipynb:定义数据
def function(x):
return (2*x + 3*x**2 + 4*x**3 + 5*x**4 + 6*x**5 + 10) ❶
data_min = -5 ❷
data_max = 5
data_step = .5
Xi = np.reshape(np.arange(data_min, data_max, data_step), (-1, 1)) ❸
yi = function(Xi) ❹
inputs = Xi.shape[1] ❺
yi = yi.reshape(-1, 1)
plt.plot(Xi, yi, 'o', color='black')
❶ 定义多项式目标函数
❷ 设置数据的界限和步长
❸ 生成并重塑输入数据
❹ 生成目标输出
❺ 找到网络的输入数量
接下来,我们回顾我们用作学习近似该函数的网络的基础模型/类,如下所示列表中定义。这是我们用来评估第二章中更简单示例的相同基础模型。
列表 5.3 EDL_5_2_RS_HPO.ipynb:定义模型
class Net(nn.Module):
def __init__(self, inputs, middle): ❶
super().__init__()
self.fc1 = nn.Linear(inputs,middle) ❷
self.fc2 = nn.Linear(middle,middle)
self.out = nn.Linear(middle,1)
def forward(self, x): ❸
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.out(x)
return x
❶ 定义输入节点和中层的大小
❷ 设置第一个全连接层
❸ 定义前向函数
现在,对于自动化魔法。我们自动化 HPO 的过程包括使用一个新类来包含和管理搜索。对于随机搜索,此Hyperparameters类的版本如列表 5.4 所示。此init函数接收输入超参数并将它们转换为类属性,使用update。当我们使用此类时,我们首先设置基本属性作为输入,然后对于每个超参数属性,我们定义一个提供下一个值的生成器。在基础Hyperparameters对象上调用next函数生成一个新的生成对象,用于单次评估。如果这还不那么明显,请不要担心;我们正在使用一些高级功能,这些功能最好通过即将到来的代码来解释。
列表 5.4 EDL_5_2_RS_HPO.ipynb:超参数类
class Hyperparameters(object):
def __init__(self, **kwargs): ❶
self.__dict__.update(kwargs)
def __str__(self): ❷
out = ""
for d in self.__dict__:
ds = self.__dict__[d]
out += f"{d} = {ds}\n"
return out
def next(self): ❸
dict = {}
for d in self.__dict__: ❹
dict[d] = next(self.__dict__[d])
return Hyperparameters(**dict) ❺
❶ init函数将输入参数放入字典中
❷ 覆盖 str 函数以实现更友好的打印
❸ 获取超参数对象下一个实例的函数
❹ 遍历 args 字典,然后对参数调用 next 方法
❺ 返回超参数对象的新实例
Hyperparameters类内部使用 Python 生成器模式遍历所有属性以创建一个新实例。对于我们的随机搜索方法,我们使用一个名为sampler的函数生成器,如列表 5.5 所示。sampler函数旨在从由min和max设置的某个范围内连续采样给定函数。Python 支持两种形式的生成器——我们使用的一种使用yield关键字来中断循环并返回值。要执行生成器,您需要将函数包裹在next中,如前一个列表(列表 5.4)所示。
列表 5.5 EDL_5_2_RS_HPO.ipynb:采样生成器
def sampler(func, min, max): ❶
while True: ❷
yield func(min,max) ❸
❶ 输入是函数和范围,从 min 到 max。
❷ 使用无限循环设置的无限生成器
❸ 通过调用函数并指定 min,max 范围来产生
我们可以在最初设置基本或父Hyperparameters对象时将这些部分组合起来,如列表 5.6 所示。在父对象中,我们将每个输入定义为由各种函数和 min/max 范围定义的sampler生成器。注意我们使用的采样函数如何从random.ranint更改为random.uniform,其中两个函数都从均匀分布生成随机变量。调用 next 函数生成一个子超参数对象,可用于实验评估。
列表 5.6 EDL_5_2_RS_HPO.ipynb:创建超参数父对象
hp = Hyperparameters(
epochs = sampler(random.randint,20,400), ❶
middle_layer = sampler(random.randint, 8, 64),
learning_rate = sampler(random.uniform,3.5e-01,
➥ 3.5e-03), ❷
batch_size = sampler(random.randint, 4, 64)
)
print(hp.next()) ❸
❶ 将输入的 epochs 添加到生成器函数中
❷ 使用随机.input 基本函数添加额外输入
❸ 采样下一个对象然后打印
要了解这是如何工作的,请跳转到包含训练函数train_function的大块代码,如列表 5.7 所示。在这个函数内部,我们首先调用hp.next()来生成一个子对象。然后,我们可以通过在对象上使用名称作为属性来使用我们的训练算法中的值。由于我们每次调用hp.next()时都使用sampler函数与随机评估器一起使用,所以输出是一组随机的超参数。
列表 5.7 EDL_5_2_RS_HPO.ipynb:使用超参数子对象
def train_function(hp):
hp = hp.next() ❶
...
for i in range(hp.epochs): ❷
❶ 生成子超参数对象
❶ 通过调用属性来使用超参数
最后,我们可以查看如何将这些内容整合并自动化以执行 HPO,如列表 5.8 所示。由于我们已经将所有随机采样封装在 HP 类中,所以其余代码相当简单。由于这是一个最小化问题,我们希望调整超参数以最小化目标网络的损失,因此我们将起始最佳值设置为最大无穷大。然后,在由运行定义的循环中,我们使用父超参数对象调用train_function。在训练函数内部,HP生成一个新的随机超参数实例,并使用这些值来评估网络损失。我们通过在模型的所有点上进行完整预测来评估整体fitness。
列表 5.8 EDL_5_2_RS_HPO.ipynb:自动 HPO
runs = 10000
best = float("inf") ❶
best_hp = None
run_history = []
for i in range(runs):
span, history, model, hp_out = train_function(hp) ❷
y_ = model(torch.Tensor(Xi))
fitness = loss_fn(y_, torch.Tensor(yi)).data.item() ❸
run_history.append([fitness,*hp_out.__dict__.values()])
if fitness < best: ❹
best = fitness
best_hp = hp_out
❶ 设置最佳值的初始最大值
❷ 运行训练函数
❸ 在所有数据上评估 fitness
❹ 检查这是否是新的最佳值
图 5.7 展示了使用随机搜索执行自动 HPO 的输出。图表顶部显示的是整体最佳fitness,并列出了超参数。下面是三个图表,显示了网络训练的损失历史、模型对函数的近似程度,以及迄今为止所有评估的映射。评估图显示了最佳fitness的灰度输出,其中黑色六边形代表迄今为止评估的最佳整体fitness。图例底部,你可以看到运行 10,000 次后的结果,其中单个黑色点,难以看清,代表最小fitness。

图 5.7 随机超参数搜索的运行结果
最后,我们回顾生成输出的代码,因为我们用它来完成本章的所有练习。此绘图输出首先绘制了最后一次运行的损失历史和函数近似,如下面的列表所示。最终图是所有运行历史的六边形图,通过学习率和中间层超参数绘制。随着自动 HPO 的运行,你会看到这个图随时间变化。
列表 5.9 EDL_5_2_RS_HPO.ipynb:绘制输出
clear_output()
fig, (ax1, ax2, ax3) = plt.subplots(1, 3,
➥ figsize=(18,6)) ❶
fig.suptitle(f"Best Fitness {best} \n{hp_out}")
ax1.plot(history) ❷
ax1.set_xlabel("iteration")
ax1.set_ylabel("loss")
ax2.plot(Xi, yi, 'o', color='black') ❸
ax2.plot(Xi,y_.detach().numpy(), 'r')
ax2.set_xlabel("X")
ax2.set_ylabel("Y")
rh = np.array(run_history) ❹
hexbins = ax3.hexbin(rh[:, 2], rh[:, 3], C=rh[:, 0],
bins=25, gridsize=25, cmap=cm.get_cmap('gray'))
ax3.set_xlabel("middle_layer")
ax3.set_ylabel("learning_rate")
plt.show()
time.sleep(1)
❶ 设置包含三个水平子图的组合图
❷ 绘制损失训练历史
❸ 绘制函数近似
❹ 对所有运行历史进行六边形图绘制
图 5.8 中显示的结果生成花费了好几个小时,你可以清楚地看到最佳结果落在何处。在这种情况下,我们只为 HPO 使用了两个超参数,因此我们可以清楚地从两个维度可视化结果。当然,我们可以在超过两个变量上使用所有这些技术,但正如预期的那样,这可能会需要更多的运行和时间。在后续场景中,我们将介绍更高级的技术来可视化和跟踪多个超参数。

图 5.8 HPO 随机搜索的一个示例输出,从 10 到 10,000 次运行
随机搜索对于找到快速答案很好,但这个方法的问题在于,随机方法就是随机。我们无法知道我们是否接近解决方案,甚至一个可能的最佳解决方案可能是什么样子。有一些统计方法可以跟踪进度并促进更好的解决方案,但这些仍然需要数百——或者数千——次迭代。
在我们这里的简单示例中,我们只是在相对较小的范围内管理两个超参数。这意味着在相对较短的时间内,我们可能有一个相当好的猜测。然而,这个猜测并没有给我们提供关于它有多接近的洞察,除了它是在一定数量的随机采样中最好的。随机搜索对于快速近似效果很好,但正如将在后续章节中讨论的,有更好的方法。
5.3 网格搜索和 HPO
虽然随机搜索可以是一个快速做出更好猜测的有效方法,以找到准确的 HPO,但它过于耗时。生成图 5.7 的最终输出花费了超过 8 个小时,这很慢,但可以产生准确的结果。寻找快速且准确的自动 HPO 需要更高级的技术。
一种简单而有效的技术,适用于从考古挖掘到搜救队的一切,就是网格搜索。网格搜索通过以网格模式布置搜索区域或表面,然后系统地遍历网格中的每个单元格。网格搜索最好在二维中可视化,但这种技术在任何维度的问题上都是有效的。
图 5.9 显示了在超参数空间中随机搜索和网格搜索之间的比较。该图演示了通过网格的可能模式之一,并且在每个单元格中,它评估learning_rate和middle_layer变量。网格搜索是一种以系统化和有效的方式评估一系列可能组合的有效方法。

图 5.9 随机搜索和网格搜索的比较
5.3.1 使用网格搜索进行自动 HPO
在我们接下来的练习中,我们将我们早期的随机搜索尝试升级为使用更复杂的网格搜索技术。虽然这种技术更加稳健和高效,但它仍然受限于网格的大小。使用较大的网格单元通常会将结果限制在局部最小值或最大值。更细小和密集的单元可以定位全局最小值和最大值,但代价是增加了搜索空间。
下一个练习笔记本中的代码,EDL_5_3_GS_HPO.ipynb,是从我们早期的随机搜索示例派生出来的。因此,大部分代码是相同的,并且,像往常一样,我们只关注使这个样本独特的部分。在 Colab 中打开 EDL_5_3_GS_HPO.ipynb 并通过“运行”>“运行所有单元格”来运行所有单元格。
这个示例的代码与主要区别在于超参数对象现在需要跟踪一个参数网格。我们首先看看新的 HyperparametersGrid 类和 init 函数的构建。在这个函数中,如列表 5.10 所示,我们将输入参数的名称提取到 self.hparms 中,然后测试第一个输入是否指向一个生成器。如果是,那么我们使用 self.create_grid 生成一个参数网格;否则,该实例将只是一个子超参数容器。
列表 5.10 EDL_5_3_GS_HPO.ipynb:HyperparametersGrid 初始化函数
class HyperparametersGrid(object):
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
self.hparms = [d for d in self.__dict__] ❶
self.grid = {}
self.gidx = 0
if isinstance(self.__dict__[self.hparms[0]],
➥ types.GeneratorType): ❷
self.grid = self.create_grid() ❸
self.grid_size = len(self.grid) ❹
❶ 提取所有输入参数名
❷ 仅在它是父节点时创建网格
❸ 创建参数网格
❹ 获取网格的大小
接下来,我们看看如何在 self.create_grid 函数中构建参数网格。该函数,如列表 5.11 所示,首先创建一个空的 grid 字典,然后遍历超参数列表。它调用超参数生成器,使用 next 返回,在这种情况下,一个值及其总数量。然后我们再次遍历生成器以提取每个唯一值并将其追加到 row 列表中。之后,我们将行追加到网格中,然后通过将 grid 注入到 ParameterGrid 类来完成。ParameterGrid 是 scikit-learn 中的一个辅助类,它接受一个输入字典和值列表作为输入,然后构建一个网格,其中每个单元格代表各种超参数组合。虽然我们在这个示例中只运行了两个超参数在二维网格上的示例,但 ParameterGrid 可以管理任意数量的维度。
列表 5.11 EDL_5_3_GS_HPO.ipynb:create_grid 函数
def create_grid(self):
grid = {}
for d in self.hparms: ❶
v,len = next(self.__dict__[d]) ❷
row = []
for i in range(len): ❸
v,_ = next(self.__dict__[d])
row.append(v) ❹
grid[d] = row
grid = ParameterGrid(grid) ❺
return grid
❶ 遍历所有超参数生成器
❷ 提取一个值及其总数量
❸ 遍历值的范围
❹ 将值追加到一行,然后将其添加到网格中
❺ 从字典网格创建一个 ParameterGrid 对象
在内部参数网格包含所有超参数组合后,我们现在可以看看更新的 next 函数是如何工作的,如列表 5.12 所示。在顶部,我们有一个 reset 函数,它用于重置参数网格的索引。每次调用 next 都会增加索引,从而从参数网格 (self.grid) 中提取下一个值。代码的最后一行使用 ** 将网格值解包为 HyperparametersGrid 的新实例的输入。
列表 5.12 EDL_5_3_GS_HPO.ipynb:HyperparametersGrid 的 next 函数
def reset(self): ❶
self.gidx = 0
def next(self):
self.gidx += 1 ❷
if self.gidx > self.grid_size-1: ❸
self.gidx = 0
return HyperparametersGrid(**self.grid[self.gidx]) ❹
❶ 重置函数以重置网格索引
❷ 增加网格索引
❸ 检查索引的界限
❹ 返回下一个参数网格作为子超参数对象
使用新的网格超参数类也要求我们升级我们用来控制超参数创建的生成器。为了简化,我们定义了两个函数:一个用于浮点数,另一个用于整数。在每一个函数内部,我们从一个最小值到最大值以步长间隔创建一个名为 grid 的值数组。我们遍历这个值列表,生成一个新的值和总列表长度,如下所示列表。拥有总列表长度允许我们遍历生成器以创建参数网格,正如之前所见。
列表 5.13 EDL_5_3_GS_HPO.ipynb:生成器
def grid(min, max, step): ❶
grid = cycle(np.arange(min, max, step)) ❷
len = (max-min) / step
for i in grid:
yield i, int(len) ❸
def grid_int(min, max, step): ❹
grid = cycle(range(min, max, step)) ❷
len = (max-min) / step
for i in grid:
yield i, int(len) ❺
❶ 浮点函数接受最小/最大值和网格步长。
❷ 遍历值列表
❸ 生成网格单元格长度的值
❹ 整数函数接受最小/最大值和网格步长。
❺ 生成网格单元格长度的值
现在,我们可以看到如何使用这个新类和这些生成器函数来创建父 hp 对象,如列表 5.14 所示。设置变量与之前所见相同;然而,这次我们使用的是 grid 生成器函数。在类初始化后,内部创建了一个参数网格。我们可以查询有关网格的信息,例如获取组合的总数或值。然后我们也可以在父 hp 对象上调用 next 来生成几个子对象。我们可以通过将每个超参数的值相乘来计算网格组合的数量。在我们的例子中,middle_layer 有 9 个值,learning_rate 有 10 个值,epochs 有 1 个值,batch_size 也有 1 个值,总共是 90,即 10 x 9 x 1 x 1 = 90。网格大小可以迅速增大,尤其是在处理多个变量和较小的步长时。
列表 5.14 EDL_5_3_GS_HPO.ipynb:创建网格
hp = HyperparametersGrid(
middle_layer = grid_int(8, 64, 6),
learning_rate = grid(3.5e-02,3.5e-01, 3e-02),
batch_size = grid_int(16, 20, 4),
epochs = grid_int(200,225,25)
)
print(hp.grid_size) ❶
print(hp.grid.param_grid) ❷
print(hp.next()) ❸
print(hp.next()) ❸
#OUTPUT#
90
[{'middle_layer': [14, 20, 26, 32, 38, 44, 50, 56, 62], 'learning_rate':
➥ [0.065, 0.095, 0.125, 0.155, 0.185, 0.215, 0.245, 0.275, 0.305...,
➥ 0.3349...], 'batch_size': [16], 'epochs': [200]}]
middle_layer = 20 learning_rate = 0.065 epochs = 200 batch_size = 16
middle_layer = 26 learning_rate = 0.065 epochs = 200 batch_size = 16
❶ 打印网格组合的数量
❷ 打印参数网格输入
❸ 打印下一个子超参数对象
此示例使用 GPU 进行训练,但实现代码的更改很小,不会展示。相反,我们关注自动化设置中的细微变化。Runs现在由hp.grid_size定义,我们创建了一个新变量grid_size,它由运行次数定义,如下所示。第二个变量用于定义我们在hexbins fitness评估图上绘制的网格单元格的大小。
列表 5.15 EDL_5_3_GS_HPO.ipynb:创建网格
runs = hp.grid_size ❶
grid_size = int(math.sqrt(runs))-1 ❷
hp.reset() ❸
❶ 现在的runs等于grid_size
❷ 根据总运行次数定义plot_grid_size
❸ 在开始之前重置父hp对象
图 5.10 显示了运行此练习直到完成的结果——全部 90 次运行大约需要 10 分钟。这比随机搜索示例的结果快得多,但准确性不高。注意图 5.3 中的最终fitness(约 17,000)是图 5.5 中显示的fitness(约 55,000)的三分之一。因此,我们的网格搜索结果并不那么准确,但它确实更快、更高效。我们总是可以回过头来缩小搜索范围,并减小步长以提高准确性。
我们最后要考虑的是修改输出评估图,根据我们之前计算出的变量设置grid_size。我们使用六边形图自动通过颜色映射fitness值。然后,我们根据组合数量设置grid_size。在以下列表中显示的简单示例中,我们假设参数是正方形网格,但这可能并不总是准确。
列表 5.16 EDL_5_3_GS_HPO.ipynb:设置grid_size
hexbins = ax3.hexbin(rh[:, 1], rh[:, 2], C=rh[:, 0], ❶
bins=25, ❷
gridsize=grid_size, ❸
cmap=cm.get_cmap('gray')) ❹
❶ 绘制带有超参数数据的六边形图
❷ 保持单元格数量不变
❸ 设置每个单元格的大小
❹ 设置要使用的颜色图
网格搜索是一种在希望系统地查看各种超参数组合时非常出色的技术。然而,再次特别关注图 5.10 中的输出,并注意最佳fitness(深色区域)位于最差fitness(浅色区域)的两个单元格之内。然而,我们可以看到许多具有良好fitness的区域围绕这个浅色单元格,这表明我们可能遗漏了全局最小值和/或最大值。解决这个问题的方法是回到并缩小网格,仅覆盖这个两到三个单元格的区域——这需要我们手动干预以更好地隔离最佳超参数。

图 5.10 网格搜索的最终输出
既然我们现在使用 EC 方法解决各种问题已有一些经验,那么接下来我们采取的步骤就是使用它们进行 HPO。在本章的其余部分,我们将探讨使用 EC 在执行 HPO 时提供更好的搜索机制。
5.4 用于 HPO 的进化计算
现在我们已经对 HPO 问题有了良好的背景了解,我们可以开始使用一些 EC 方法来提高速度和准确性。正如我们在前面的章节中看到的,进化方法提供了一套优秀的工具集,可以用于优化各种问题的搜索。因此,评估使用 EC 进行 HPO 的实际应用是很有意义的。
5.4.1 粒子群优化用于 HPO
我们通过使用 PSO 进行 HPO,将 EC 引入到 DL 中。PSO,如第四章所述,使用一群particles来寻找最优解。PSO 不仅足够简单,可以用 DEAP 实现,而且展示了 EC 在解决像 HPO 这样的问题上的强大能力。
5.4.2 将 EC 和 DEAP 添加到自动 HPO
在下一个练习中,我们关注两个关键方面:将 EC/DEAP 添加到执行自动 HPO,以及将 EC 方法(如 PSO)应用于问题。我们再次使用相同的基本问题来比较不同方法的结果。这不仅使理解代码变得更容易,而且在比较其他方法时也提供了一个良好的基线。
在 Colab 中打开 EDL_5_4_PSO_HPO_PCA.ipynb 笔记本,并通过“运行”>“运行所有单元格”来运行所有单元格。滚动到包含HyperparametersEC类定义的单元格,如下所示。再次,将 EC 与 DL 结合的重任大部分发生在HyperparametersEC类中。这次,我们创建了一个名为HyperparametersEC的专用版本。我们首先关注的是基本函数。
列表 5.17 EDL_5_4_PSO_HPO.ipynb:HyperparametersEC基本函数
class HyperparametersEC(object):
def __init__(self, **kwargs): ❶
self.__dict__.update(kwargs)
self.hparms = [d for d in self.__dict__]
def __str__(self): ❷
out = ""
for d in self.hparms:
ds = self.__dict__[d]
out += f"{d} = {ds} "
return out
def values(self): ❸
vals = []
for d in self.hparms:
vals.append(self.__dict__[d])
return vals
def size(self): ❹
return len(self.hparms)
❶ 使用输入参数初始化类
❷ 重写字符串函数
❸ 提供当前值
❹ 返回超参数的大小
在基本函数之后,查看用于调用父HyperparametersEC对象的特殊next函数。这是一段复杂的代码,直到我们查看新的生成器方法之前,它可能不会完全有意义。注意这个函数(如列表 5.18 所示),它接受一个individual或值的向量作为输入。在这个例子中,individual代表一个particle,但它也可以代表我们使用向量描述的任何形式的individual。另一个需要关注的细节是生成器上send函数的使用。send类似于 Python 中的next函数,但它允许生成器初始化或输入值。
列表 5.18 EDL_5_4_PSO_HPO.ipynb:HyperparametersEC next函数
def next(self, individual):
dict = {}
for i, d in enumerate(self.hparms): ❶
next(self.__dict__[d]) ❷
for i, d in enumerate(self.hparms):
dict[d] = self.__dict__[d].send(individual[i]) ❸
return HyperparametersEC(**dict) ❹
❶ 列出超参数
❷ 为每个超参数初始化生成器
❸ 将索引值发送到生成器并产生值
❹ 返回子对象
由于send函数允许将值传递到生成器中,我们现在可以重写生成器函数以适应。我们感兴趣的函数是linespace和linespace_int生成器,如列表 5.19 所示。这些生成器允许使用i = yield传递输入,其中yield成为使用send函数输入的值。值i然后成为-1.0和1.0之间的线性插值空间的索引,通过应用clamp函数。如您所回忆的,send函数发送了从individual中的索引值。因此,individual中的每个向量元素都成为由设置父hp时使用的最小/最大值定义的超参数线性空间中的索引。
列表 5.19 EDL_5_4_PSO_HPO.ipynb:生成器函数
def clamp(num, min_value, max_value): ❶
return max(min(num, max_value), min_value)
def linespace(min,max):
rnge = max - min
while True:
i = yield ❷
i = (clamp(i, -1.0, 1.0) + 1.0) / 2.0 ❸
yield i * rnge + min
def static(val): ❹
while True:
yield val
❶ 将值夹在最小/最大范围内
❷ 将 i 设置为输入值 yield
❸ 线性插值值
❹ 返回一个静态值
现在,我们可以查看以下单元格中如何实现这一功能,其中我们实例化类并创建一个子对象。再次强调,创建父超参数对象是通过传递我们想要跟踪的每个超参数的生成器来完成的。之后,使用范围在-1.0到1.0之间的值列表定义了一个简单的individual,其中每个值代表由设置最小/最大值到生成器定义的线性空间中的索引。这次,当我们对超参数父对象调用next时,我们得到一个由输入individual的索引值定义的子对象,如下所示。
列表 5.20 EDL_5_4_PSO_HPO.ipynb:创建父超参数对象
hp = HyperparametersEC(
middle_layer = linespace_int(8, 64), ❶
learning_rate = linespace(3.5e-02,3.5e-01),
batch_size = static(16), ❷
epochs = static(200)
)
ind = [-.5, -.3, -.1, .8] ❸
print(hp.next(ind)) ❹
## OUTPUT ##
middle_layer = 22 learning_rate = 0.14525 batch_size = 16 epochs = 200
❶ 使用最小/最大值定义的线空间生成器
❷ 配置了静态生成器
❸ 创建一个大小为 4 的向量来表示个体
❹ 调用 next 创建一个新的超参数子对象
PSO 设置和操作代码的大部分是从第四章中介绍的 EDL_4_PSO.ipynb 笔记本借用的。我们在这里的重点是toolbox和evaluate函数的配置。在这段代码中,我们根据hp.size或我们想要跟踪的超参数数量设置particle的大小。接下来,我们减小pmax/pmin和smin/smax的值以适应更小的搜索空间。务必根据您自己的情况修改这些值以查看这对 HPO 的影响。在以下列表中的代码末尾,我们可以看到evaluate函数的注册,其中评估了每个particle的fitness。
列表 5.21 EDL_5_4_PSO_HPO.ipynb:创建父超参数对象
toolbox = base.Toolbox()
toolbox.register("particle",
generate, size=hp.size(), ❶
pmin=-.25, pmax=.25, smin=-.25, smax=.25) ❷
toolbox.register("population",
tools.initRepeat, list, toolbox.particle)
toolbox.register("update",
updateParticle, phi1=2, phi2=2)
toolbox.register("evaluate", evaluate) ❸
❶ 粒子的大小,由超参数数量定义
❷ 配置粒子搜索空间
❸ 注册评估函数
现在的evaluate函数需要通过传递子超参数对象来调用train_function。注意这与我们之前调用网络训练函数的方式略有不同。这次,我们通过在父对象上调用next并传递individual来生成子超参数对象。然后,将子超参数输入到train_function中以生成输出。为了获得完整的评估,我们检查整个数据集上的模型损失,然后将此作为fitness返回,如以下列表所示。
列表 5.22 EDL_5_4_PSO_HPO.ipynb:创建父超参数对象
def evaluate(individual):
hp_in = hp.next(individual) ❶
span, history, model, hp_out = train_function(hp_in) ❷
y_ = model(torch.Tensor(Xi).type(Tensor)) ❸
fitness = loss_fn(y_, torch.Tensor(yi).type(Tensor)).data.item() ❸
return fitness, ❹
❶ 通过传递个体生成子超参数
❷ 通过传递子超参数调用训练
❸ 预测完整模型损失
❹ 返回fitness
我们现在可以继续到最后一个代码块,如图 5.23 所示,并检查粒子群集是如何与我们的更改一起工作的。更改已被加粗,并添加以更好地跟踪particle fitness和相关的超参数。在调用evaluate函数对部分particle进行评估后,我们调用hp.next(part)来创建一个子副本。这不是 PSO 功能所必需的,但它有助于我们跟踪particle历史。
列表 5.23 EDL_5_4_PSO_HPO.ipynb:创建父超参数对象
for i in range(ITS):
for part in pop:
part.fitness.values = toolbox.evaluate(part)
hp_eval = hp.next(part) ❶
run_history.append([part.fitness.values[0],
➥ *hp_eval.values()]) ❷
if part.best is None or part.best.fitness < part.fitness:
part.best = creator.Particle(part)
part.best.fitness.values = part.fitness.values
if best is None or best.fitness > part.fitness:
best = creator.Particle(part) ❸
best.fitness.values = part.fitness.values
best_hp = hp.next(best)
for part in pop:
toolbox.update(part, best)
❶ 捕获超参数子对象
❷ 将值追加到运行历史记录中
❸ 捕获最佳fitness和超参数对象
图 5.6 是应用 PSO 进行 10 次迭代群集优化后的最终输出截图。你可以在最左边的图中清楚地看到,fitness评估图显示了particles如何围绕一个预测的最佳解收敛。注意,最终的最佳fitness值,大约为 34,000,比我们的网格搜索实现的结果要好。更重要的是,PSO 能够在网格搜索的一小部分时间内完成这一点。
与我们之前的随机和网格搜索示例相比,图 5.11 的结果看起来相当令人印象深刻。然而,PSO 并非没有自己的问题,尽管它似乎在性能上优于网格搜索,但它并不总是如此。此外,PSO 的参数由smin/smax和pmin/pmax紧密定义,正确调整这些值通常需要仔细思考或试错。

图 5.11 使用 PSO 进行 HPO 的输出
查看图 5.10 的第三个子图,我们可以看到 PSO 如何收敛到一个区域,然后在该区域上群集particles以找到更好的最优解。这种方法的问题在于,群集往往会在寻找群集区域内的全局最小/最大值时陷入局部最小/最大值。如果该区域内没有这样的全局值,那么群集就会陷入局部最小/最大值。
考虑到 PSO 的这种潜在困境,我们可以考虑其他可以更好地执行 HPO 并避免或最小化这种问题的 EC 方法。正如我们在第四章中看到的,有一些高级 EC 方法可能有助于我们克服这些担忧。
5.5 针对 HPO 的遗传算法和进化策略
我们在第三章花了一些时间来理解遗传算法(GA)的工作原理,然后在第四章中当我们采用进化策略(ES)时,对这些概念进行了扩展。如果你还记得,ES 是一种特殊的 GA,它应用策略来改进遗传操作,如突变。我们继续使用相同的突变策略与 GA 和 ES 进行自动 HPO。
5.5.1 将进化策略应用于 HPO
在第四章中,我们探讨了如何将进化策略作为一个额外的向量来控制突变的速度和应用。通过这种方式控制突变,我们可以更好地集中整个population以更快地到达解决方案。在我们的下一个项目中,我们将采用 ES 和一种执行自动 HPO 的方法。
在 Colab 中打开 EDL_5_5_ES_HPO.ipynb 笔记本,并通过“运行”>“运行所有单元格”来运行所有单元格。这个笔记本基于 EDL_4_4_ES.ipynb,并从该示例中借用了很多代码。我们还从上一个练习中借用了一些内容来构建这个示例,这意味着这段代码可能看起来很熟悉。
我们通过回顾 ES 超参数来关注第一个差异。第一个修改是将IND_SIZE变量设置为超参数的数量。然后,我们将MAX_STRATEGY更改为 5,以适应更大的搜索空间,如下列表所示。
列表 5.24 EDL_5_5_ES_HPO.ipynb:设置 ES 超参数
IND_SIZE = hp.size() ❶
NGEN = 10
MIN_VALUE = -1
MAX_VALUE = 1
MIN_STRATEGY = 0.5
MAX_STRATEGY = 5 ❷
CXPB = .6
MUTPB = .3
❶ 将个体大小设置为超参数的数量
❷ 增加最大策略以适应更宽的搜索空间
接下来,我们跳转到设置toolbox的代码块,如列表 5.25 所示。我们在这里做的唯一关键更改是修改了几个超参数,即mate操作符中使用的 alpha 和突变的概率。回想一下,alpha 表示父母之间混合的大小,而不是直接的crossover。
列表 5.25 EDL_5_5_ES_HPO.ipynb:创建toolbox
toolbox = base.Toolbox()
toolbox.register("individual", generateES, creator.Individual, creator.Strategy,
IND_SIZE, MIN_VALUE, MAX_VALUE, MIN_STRATEGY, MAX_STRATEGY)
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
toolbox.register("mate", tools.cxESBlend, alpha=0.25) ❶
toolbox.register("mutate", tools.mutESLogNormal,
➥ c=1.0, indpb=0.06) ❷
toolbox.register("select", tools.selTournament, tournsize=3)
toolbox.decorate("mate", checkStrategy(MIN_STRATEGY))
toolbox.decorate("mutate", checkStrategy(MIN_STRATEGY))
❶ 增加 alpha 掩码的大小
❷ 增加突变发生的概率
最后,我们可以查看以下列表中的进化代码,以了解如何使population进化到解决方案。
列表 5.26 EDL_5_5_ES_HPO.ipynb:创建toolbox
for g in range(NGEN):
pop, logbook = algorithms.eaMuCommaLambda(pop, toolbox, mu=MU, lambda_=LAMBDA,
cxpb=CXPB, mutpb=MUTPB, ngen=1,
stats=stats, halloffame=hof, verbose=False) ❶
best = hof[0]
span, history, model, hp_out = train_function
➥ (hp.next(best)) ❷
y_ = model(torch.Tensor(Xi).type(Tensor)) ❸
fitness = loss_fn(y_, torch.Tensor(yi).type(Tensor)).data.item()
run_history.append([fitness,*hp_out.values()]) ❹
best_hp = hp_out
❶ 使用算法函数进行单次迭代进化
❷ 再次使用进化中的最佳结果进行训练
❸ 从模型预测输出
❹ 将适应度和超参数子项添加到结果中
图 5.12 显示了运行 ES 进行 HPO 的最终输出。注意最后一个图表,即fitness评估,显示了更紧密的集中。这种集中比 PSO 更紧密,ES 也遇到了一些相同的问题。

图 5.12 ES 在 HPO 上的示例输出
在 PSO 中,我们看到了一群 粒子 卡在局部最小值或最大值上的问题。虽然这似乎与 ES 中的问题相似,但重要的是要注意,收敛速度更快,更集中。增加更大的 种群 可以减少或帮助 ES 更频繁地识别全局最小值。
5.5.2 使用主成分分析扩展维度
到目前为止,我们一直在测试仅跨越两个超参数(学习率和批量大小)的各种方法,以使在二维中可视化结果更容易。如果我们想在更高维中可视化更多超参数,我们需要将维度减少到二维或三维以进行可视化。幸运的是,有一个简单的技术我们可以应用,以在二维中可视化高维输出,称为 主成分分析 (PCA)。
PCA 是将多维向量数据从高维降低到低维的过程。在我们的例子中,我们将四维输出降低到二维以进行可视化。你可以将这个过程视为从高维到低维的投影。我们将在下一个练习中展示它是如何工作的以及如何应用于可视化 HPO。
在 Colab 中打开 Open notebook EDL_5_5_ES_HPO_PCA.ipynb,然后通过运行 > 运行所有单元格来运行所有单元格。EDL_5_5_ES_HPO.ipynb 的变体添加了 PCA,因此我们可以自动化额外的超参数,并且仍然可以在二维中可视化结果。
大部分代码都是相同的,但我们专注于一个单元格,该单元格演示了设置 PCA 并在二维中绘制一些多维输出。scikit-learn 提供了一个 PCA 类,可以轻松地将数据从高维转换到更简单的组件输出。在下面的代码列表中,我们将示例 个体 对象从四维降低到两个组件。
列表 5.27 EDL_5_5_ES_HPO_PCA.ipynb:添加 PCA
pop = np.array([[-.5, .75, -.1, .8],
[-.5, -.3, -.5, .8],
[-.5, 1, -.5, -.8],
[ 1, -.3, -.5, .8]]) ❶
pca = PCA(n_components=2) ❷
reduced = pca.fit_transform(pop) ❸
t = reduced.transpose() ❹
plt.scatter(t[0], t[1]) ❺
plt.show()
❶ 创建一个样本个体种群
❷ 创建一个具有两个维度的 PCA 对象
❸ 拟合数据
❹ 将结果转置到新的向量中
❺ 在二维中绘制输出
图 5.13 显示了列表 5.27 的示例输出以及 PCA 在虚构的 种群 数据中的应用。重要的是要理解每个轴都是一个组件,它代表向量空间中元素之间的距离。PCA 输出是通过测量元素之间的方差或差异来计算的,生成组件或轴,每个元素都落在这个轴上。重要的是要理解 PCA 图是相对于正在可视化的数据而言的。如果你需要或想要了解更多关于 PCA 算法的知识,请务必查看 sklearn 文档或其他在线资源。

图 5.13 组件空间中四个维度点的 PCA 图
由于我们能够可视化超过两个维度的数据点,我们还可以将我们的hyperparameter对象扩展到变化额外的输入。我们现在将batch size和epochs添加为要变化的超参数,如列表 5.28 所示。暂时考虑将这两个额外超参数添加到网格搜索问题中。如果我们假设每个超参数的范围跨越 10 个单元格或步骤,那么有 4 个输入,我们的搜索空间将等于 10 x 10 x 10 x 10 = 10,000,或 10,000 个单元格。回想一下,我们的随机搜索例子是为 10,000 次运行设置的,并且花费了超过 12 小时来完成。这同样是执行相同四维空间网格搜索所需的时间。
列表 5.28 EDL_5_5_ES_HPO_PCA.ipynb:添加超参数
hp = HyperparametersEC(
middle_layer = linespace_int(8, 64),
learning_rate = linespace(3.5e-02,3.5e-01),
batch_size = linespace_int(4,20), ❶
epochs = linespace_int(50,400) ❷
)
ind = [-.5, .75, -.1, .8]
print(hp.next(ind))
❶ 改变批量大小
❷ 改变 epoch 的数量
我们需要做的唯一其他代码更改是修改评估函数输出图,如列表 5.29 所示。我们可以借鉴列表 5.27 中的代码来应用相同的过程,将运行历史中的超参数输出减少到两个组件。然后,我们使用hexbins函数将这些组件的转置绘制到二维空间中。
列表 5.29 EDL_5_5_ES_HPO_PCA.ipynb:绘制fitness评估的代码
rh = np.array(run_history)
M = rh[:,1:IND_SIZE+1] ❶
reduced = pca.fit_transform(M)
t = reduced.transpose()
hexbins = ax3.hexbin(t[0], t[1], C=rh[ :, 0], ❷
bins=50, gridsize=50, cmap=cm.get_cmap('gray'))
❶ 从运行历史中提取超参数
❷ 输出 PCA 成分
图 5.14 显示了将 ES 应用于 HPO 的输出,其中第三个图现在由 PCA 成分组成。这个图使我们能够可视化跨多个维度的最优超参数的搜索。我们仍然可以看到一些最佳解决方案的聚类,但很明显,其他点现在分布得更广。同时,请注意fitness与我们的早期例子相比有了多大的提高,这可以归因于额外超参数的变化。

图 5.14 带 PCA 的 ES HPO 输出
我们之前仅改变两个超参数时的例子之间的差异相对较小。通过向搜索空间添加额外的超参数,从而增加维度,我们可以看到 ES 与网格搜索或随机搜索之间的性能明显提高。然而,请记住,ES 仍然容易陷入局部最小值,这表明我们需要考虑替代方法。
5.6 用于 HPO 的差分进化
在第四章末尾,当我们使用这种方法在 ES 上解决不连续解时,我们看到了 DE(差分进化)的力量。鉴于 DE 用于进化解决方案的独特方法,它自然成为自动化 HPO 的良好候选者。DE 也可能克服我们在 PSO 和 ES 中观察到的粘滞条件。
5.6.1 用于演化的 HPO 的微分搜索
差分进化使用一个简单的迭代算法,从种群中随机选择三个个体。然后,它计算两个个体之间的差异,并将一个缩放值加到第三个个体上。结果产生第四个点:下一个搜索目标区域。
图 5.15 展示了差分进化算法在二维空间中的单个评估。在图中,三个点(A、B、C)是从种群中随机抽取的。计算从 A 到 B 的差异向量(A – B),然后通过缩放函数 F 传递。在大多数情况下,我们通过乘以 1.0 的缩放值来保持简单。然后,我们将缩放后的差异向量加到第三个点上,以创建一个新的目标搜索点。

图 5.15 差分进化搜索
差分进化使用的搜索机制足够灵活,可以摆脱我们之前在 PSO 和 ES 中看到的群或聚类问题。当然,我们希望看到 DE 的实际应用来确认这是我们进行超参数优化(HPO)的更好方法。在下一个练习中,我们继续使用 DE 在优化四个超参数的同一问题上进行求解。
在 Colab 中打开 EDL_5_ES_HPO_PCA.ipynb 笔记本,然后通过运行 > 运行所有单元格来运行所有单元格。如果您愿意,您还可以通过探索 EDL_5_ES_HPO.ipynb 来查看仅运行在两个维度上的此笔记本的非 PCA 版本。我们之前已经看到了这个练习中的所有代码,所以我们只需回顾使其独特的部分,从以下列表中的超参数开始。
列表 5.30 EDL_5_6_DE_HPO_PCA.ipynb:设置creator和toolbox
NDIM = hp.size() ❶
CR = 0.25 ❷
F_ = 1 ❸
MU = 50 ❹
NGEN = 10
❶ 超参数维度的数量
❷ 交叉率
❸ 缩放因子/函数
❹ 总种群
接下来,我们再次回顾设置creator和toolbox的 DEAP 代码——我们之前已经覆盖了这段代码中的所有内容。注意在individual注册中使用NDIM值来设置以下列表中的大小。在最后一行,我们可以选择将注册设置为输出三个元素的随机选择操作符,k=3。
列表 5.31 EDL_5_6_DE_HPO_PCA.ipynb:设置creator和toolbox
creator.create("FitnessMin", base.Fitness, weights=(-1.0,))
creator.create("Individual",
array.array, typecode='d', fitness=creator.FitnessMin)
toolbox = base.Toolbox()
toolbox.register("attr_float", random.uniform, -1, 1)
toolbox.register("individual", tools.initRepeat,
creator.Individual, toolbox.attr_float, NDIM) ❶
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
toolbox.register("select", tools.selRandom, k=3) ❷
❶ 创建一个具有与超参数维度相等大小的个体
❷ 注册一个大小为 3 的随机选择函数
另外一个值得关注的代码是在进化部分。这是我们在第四章中已经审查过的代码,但值得再次审查。在差分进化中,我们称individual对象为代理,因为它们像粒子一样有很长的生命周期,但像代理一样进化。注意高亮显示的行,其中计算并应用了缩放差异向量到向量y的单个分量上。这个计算保证对于每个随机抽取的与当前向量分量匹配的索引只会发生一次。然而,交叉率确实提供了改变其他分量值以创建新的y的机会,如下面的列表所示。
列表 5.32 EDL_5_6_DE_HPO_PCA.ipynb:进化差分进化
for g in range(1, NGEN):
for k, agent in enumerate(pop): ❶
a,b,c = toolbox.select(pop) ❷
y = toolbox.clone(agent)
index = random.randrange(NDIM) ❸
for i, value in enumerate(agent):
if i == index or random.random() < CR: ❹
y[i] = a[i] + F_*(b[i]-c[i]) ❺
y.fitness.values = toolbox.evaluate(y)
if y.fitness > agent.fitness:
pop[k] = y
hof.update(pop)
❶ 遍历种群
❷ 选择三个代理
❸ 找到一个随机索引
❹ 检查是否存在交叉
❺ 应用缩放向量函数
图 5.16 显示了使用 DE 解决目标问题的 HPO 的最终输出,使用了 10 代。具体来说,注意第三个评估图,以及点并没有聚集在一起。此外,注意从这个方法生成的最佳“适应度”大约为 81,这个数字明显超过了我们之前的所有其他尝试。

图 5.16 DE 用于 HPO 的一个示例输出
如我们所见,将 DE 应用于 HPO 似乎提供了一个避免 PSO 和 ES 中观察到的局部最小值粘滞问题的优秀机制。我们可以通过将 PSO 示例升级为使用 PCA 来比较三种技术,如 EDL_5_4_PSO_HPO_PCA.ipynb 笔记本中所示。请随意运行该笔记本,以观察 PSO、ES 和 DE 之间的差异。
图 5.17 显示了 PSO、ES 和 DE 的评估图比较。注意 PSO 产生了一个广泛的“粒子”群,大致集中在它期望的最佳解附近。同样,ES 产生了一个更紧密的尝试集群,但分布更像是输出上的一个窄带。我们可以清楚地看到,DE 图表明该算法非常适合探索边界,并避免陷入局部最小值。

图 5.17 HPO 的 EC 方法比较
这是我们第一次通过将 EC 与 DL 集成来应用 EDL 原则的章节。我们一开始探索了基本的随机搜索,然后提升到网格搜索,为其他方法设定基准。从那里,我们扩展到应用 PSO、ES,最终是 DE。
通过本章对技术的探索,现在应该很明显,EC 方法在 DL 中有明确的应用。正如我们在本书的其余部分所探讨的,这些以及其他技术可以应用于 DL 的改进。然而,现在,让我们通过总结我们所学的内容来结束本章。
摘要
-
这是第一个我们将 EC 方法与 DL 结合的章节,也是我们第一次接触 EDL。在这个过程中,我们学习了几个新技巧,我们可以将其应用于 PyTorch,以及其他框架。
-
DL 超参数搜索(超参数优化 HPO)要正确执行需要广泛的知识和经验:
-
使用基本规则和模板,可以开发出用于各种问题进行手动超参数搜索的策略。
-
使用 Python 快速演示编写基本的超参数搜索工具。
-
-
随机超参数搜索是一种使用随机抽样在图表上生成结果的搜索方法。通过观察这些随机观察结果,调优器可以将搜索范围缩小到特定感兴趣的区域。
-
网格搜索是一种将超参数映射到离散值网格的方法,然后按顺序评估这些值。可视化结果网格可以帮助调优器进行微调和选择特定区域进行进一步调优。
-
DEAP 可以快速提供多种进化方法用于超参数优化(HPO):
-
从遗传算法(GAs)到差分进化(DE),进化超参数搜索通常比网格搜索或随机搜索更有效。
-
对于复杂的多维超参数优化,我们可以通过使用降维技术来生成二维图,以可视化各种搜索形式之间的差异。
-
主成分分析(PCA)是可视化超参数优化(HPO)的良好降维技术。
-
-
粒子群优化(PSO)是处理相对较少超参数问题的优秀方法。
-
差分进化非常适合更系统化和高效地搜索超参数,以避免局部最小值聚类。始终评估各种搜索形式之间的关键差异,并理解何时使用哪种方法以及何时使用。
6 神经进化优化
本章涵盖
-
深度学习网络如何优化或学习
-
用遗传算法替换神经网络的反向传播训练
-
神经网络的进化优化
-
将进化优化应用于 Keras 深度学习模型
-
将神经进化扩展到处理图像分类任务
在上一章中,我们通过使用进化算法来优化深度学习网络的超参数来尝试涉水。我们看到了如何使用 EA 可以改进超参数搜索,超越简单的随机或网格搜索算法。采用 EA 的变体,如 PSO(粒子群优化)、进化策略和差分进化,揭示了用于搜索和超参数优化(HPO)的方法。
进化深度学习是我们用来涵盖所有用于改进深度学习的进化方法的术语。更具体地说,术语“神经进化”已被用来定义应用于深度学习的特定优化模式。我们在上一章中查看的一个模式是将进化算法应用于 HPO。
神经进化包括用于 HPO(超参数优化)、参数优化(权重/参数搜索)和网络优化的技术。在本章中,我们将深入了解进化方法如何直接应用于优化网络参数,从而消除通过网络反向传播错误或损失的需求。
神经进化通常用于改进单个深度学习网络模型。还有其他将进化应用于深度学习的方法,这些方法将搜索范围扩展到多个模型。然而,现在,让我们看看如何使用 NumPy 作为神经进化的基础来构建一个简单的多层感知器(MLP)。
6.1 NumPy 中的多层感知器
在我们深入探讨神经进化网络参数之前,让我们更仔细地看看一个基本的深度学习系统。其中最基本的是用 NumPy 编写的多层感知器。我们不使用像 Keras 或 PyTorch 这样的框架,因此我们可以清楚地可视化内部过程。
图 6.1 显示了简单的 MLP 网络。在图的上部,我们可以看到通过将计算损失推过网络来工作反向传播。图的底部显示了通过用“基因”序列中的值替换网络的每个权重/参数来工作的神经进化优化。实际上,我们正在进行类似于上一章中用于超参数的进化搜索。

图 6.1 反向传播与神经进化优化
希望如果你在深度学习方面有扎实的背景,你已经理解了 MLP 及其内部工作原理。然而,为了全面起见,我们回顾了仅使用 NumPy 编写的 MLP 结构。然后,我们看看这个简单的网络如何在各种样本分类问题中进行训练。
在 Colab 中打开 EDL_6_1_MLP_NumPy.ipynb 笔记本。如果你需要帮助,请参阅附录。通过选择菜单中的“运行”>“运行所有”来运行所有单元格。
图 6.2 显示了你可以选择的第二个单元格和选项。按照图中的选项选择,然后通过选择运行 > 运行所有来再次运行笔记本中的所有单元格。

图 6.2 选择问题数据集生成参数
处理错误
通常,如果你在运行笔记本时遇到错误,那是因为重复运行代码或代码运行不同步。最简单的修复方法是工厂重置笔记本(运行 > 工厂重置运行时),然后再次运行单元格。
生成问题数据集的代码是使用 sklearn 的make datasets函数族构建的。我们不会关注具体的代码,而是专注于表 6.1 中的参数选项(见图 6.2)。
表 6.1 参数和值范围的摘要描述
| 参数 | 描述 | 范围 |
|---|---|---|
number_samples |
样本数据点的数量 | 100–1,000 |
difficulty |
一个增加问题难度的任意因素 | 1–5 |
problem |
定义用于问题数据集的函数 | classification = make_classificationmoons = make_moonscircles = make_circlesblobs = make_blobsGaussian quantiles = make_gaussian_quantiles |
middle_layer |
设置中间网络层的节点数 | 5–25 |
epochs |
在 MLP 上运行的训练迭代次数 | 1000–25000 |
图 6.3 显示了在难度级别 1 下每种数据集类型的示例。尝试更改问题类型,以查看每个数据集的变体。对于简单的 MLP 网络来说,最困难的数据集是圆形,但请确保探索所有数据集。

图 6.3 难度级别 1 下样本数据集类型的示例
作为基线,我们比较了来自 sklearn 的一个简单逻辑回归(分类)模型。向下滚动查看以下列表中显示的代码。
列表 6.1 EDL_6_1_MLP_NumPy.ipynb:Sklearn 逻辑回归
clf = sklearn.linear_model.LogisticRegressionCV()
clf.fit(X, Y) ❶
show_predictions(clf, X, Y, "Logistic regression") ❷
LR_predictions = clf.predict(X) ❸
print("Logistic Regression accuracy : ",
np.sum(LR_predictions == Y) / Y.shape[0]) ❹
❶ 将模型拟合到数据
❷ 使用辅助函数显示预测的可视化
❸ 生成一组预测
❹ 评估预测的准确性并打印
图 6.4 显示了调用辅助show_predictions函数的输出。此函数绘制了模型如何对数据进行分类的漂亮可视化。如图所示,结果并不十分出色。

图 6.4 展示了逻辑回归模型对分类数据集进行分类的图
接下来,点击标题为“Python 中的 MLP”的单元格上的“显示代码”链接。务必在空闲时间查看init、forward、back_prop和train函数。我们在这里不会花时间查看代码;我们使用这个简单的例子来展示不同的函数。此代码将在未来的项目中重用,但不包括back_prop和training函数。笔记本中最后一个代码块,如下所示,创建 MLP 网络,对其进行训练,并输出结果的可视化。
列表 6.2 EDL_6_1_MLP_NumPy.ipynb:创建和训练网络
nn = Neural_Network(2, middle_layer, 1) ❶
nn.train(X, Y, epochs, 1.2) ❷
show_predictions(nn, X, Y, "Neural Network") ❸
nn_predictions = nn.predict(X) ❹
print("Neural Network accuracy : ",
np.sum(nn_predictions == Y) / Y.shape[0]) ❹
❶ 创建 MLP 网络
❷ 训练网络
❸ 展示训练结果
❹ 打印出模型准确率
图 6.5 展示了训练 MLP 网络的结果。正如我们在基本的分类示例中可以看到的,使用 MLP 网络的结果显著优于 sklearn 中的逻辑回归模型。这就是为什么神经网络和深度学习变得如此成功的一部分原因。然而,这个简单的网络仍然难以解决所有的问题数据集。

图 6.5 在问题数据集上训练简单 MLP 网络的成果
图 6.6 展示了 MLP 网络尝试解决圆和月亮问题集的输出。如图所示,圆的准确率峰值在 0.5,即 50%,而月亮的准确率在 0.89,即 89%。我们当然可以查看更强大的优化器,如 Adam,但让我们考虑另一种方法。如果我们使用遗传算法,比如找到最优的网络权重,就像我们之前的许多例子一样,会怎样呢?

图 6.6 在圆和月亮问题数据集上训练 MLP 的结果
6.1.1 学习练习
使用以下练习来提高你的知识:
-
增加或减少图 6.2 中的样本数量,然后重新运行笔记本。
-
更改图 6.2 中的问题类型和难度,并在每次更改后重新运行笔记本。保持模型大小一致。
-
更改图 6.2 中的模型参数和中层,然后重新运行。
现在我们有了感知器 MLP 模型,我们可以在下一节中继续使用遗传算法来优化它。
6.2 遗传算法作为深度学习优化器
在上一个项目设置的基础上,我们现在可以继续将我们的 MLP 中使用的深度学习优化方法从反向传播更改为神经进化优化。因此,我们不再使用任何形式的通过优化器(如梯度下降或 Adam)通过损失反向传播,而是完全依赖于遗传算法。
我们接下来要看的下一个项目使用上一个项目的代码作为我们的基础网络模型,然后我们用 DEAP 中的遗传算法包裹训练优化过程。因此,很多代码现在应该感觉非常熟悉,所以我们只考虑重点。如果你需要复习如何使用 DEAP 设置遗传算法,请考虑回顾第 3-5 章。
在 Colab 中打开笔记本 EDL_6_2_MLP_GA.ipynb。如需帮助,请参阅附录 A。确保通过菜单选择运行 > 运行所有来运行模型中的所有单元格。
我们通过开始查看 MLP 网络代码块中的主要变化来关注主要变化。本项目使用相同的 MLP 网络模型,但将Neural_Network类中的train和back_prop函数替换为新的set_parameters函数,如列表 6.3 所示。
此代码遍历模型中的参数列表,找到大小和形状,然后从 individual 中提取匹配数量的 genes。然后,构建一个新的张量并将其重塑以匹配原始参数/权重张量。我们从原始张量中减去自身以将其置零并保持引用,然后添加新的张量。实际上,我们将 individual 的 gene 序列的部分交换到张量中,然后将其作为模型中的新权重替换。
列表 6.3 EDL_6_2_MLP_GA.ipynb: set_parameters 函数
def set_parameters(self, individual):
idx = 0
for p in self.parameters: ❶
size = p.size ❷
sh = p.shape ❷
t = individual[idx:idx+size] ❷
t = np.array(t)
t = np.reshape(t, sh) ❸
p -= p ❹
p += t ❹
idx += size ❺
❶ 遍历模型权重/参数的列表
❷ 获取参数张量的大小,然后提取基因集
❸ 创建一个新的张量,然后从基因序列中重塑它
❹ 将张量重置为零,然后添加一个新的张量
❺ 将索引位置更新为个体
请注意,train 和 back_prop 函数已被完全删除,从而防止网络执行任何形式的传统反向传播训练。set_parameters 函数设置模型的权重/参数,并允许我们使用 GA 搜索这些值。我们接下来要查看的代码实例化了我们的网络,将所有参数设置为 1.0,然后输出图 6.7 中显示的结果。
列表 6.4 EDL_6_2_MLP_GA.ipynb: 创建网络和设置样本权重
nn = Neural_Network(2, middle_layer, 1) ❶
number_of_genes = sum([p.size for p in nn.parameters]) ❷
print(number_of_genes) ❷
individual = np.ones(number_of_genes) ❸
nn.set_parameters(individual) ❸
print(nn.parameters)
show_predictions(nn, X, Y, "Neural Network") ❹
nn_predictions = nn.predict(X) ❺
print("Neural Network accuracy : ",
np.sum(nn_predictions == Y) / Y.shape[0]) ❺
❶ 创建 MLP 网络
❷ 计算模型参数的数量,该数量等于基因的数量
❸ 将每个模型权重设置为 1
❹ 生成预测图
❺ 计算准确率然后打印

图 6.7 在圆圈数据集上所有权重设置为 1 的网络预测
图 6.7 显示了模型预测的输出,其中所有权重/参数都设置为 1.0。设置 GA 的 DEAP 代码如下所示,但现在应该已经很熟悉了。
列表 6.5 EDL_6_2_MLP_GA.ipynb: DEAP toolbox 设置
toolbox = base.Toolbox()
toolbox.register("attr_float", uniform, -1, 1,
➥ number_of_genes) ❶
toolbox.register("individual", tools.initIterate, creator.Individual,
➥ toolbox.attr_float)
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
toolbox.register("select", tools.selTournament,
➥ tournsize=5) ❷
toolbox.register("mate", tools.cxBlend, alpha=.5) ❸
toolbox.register("mutate", tools.mutGaussian, mu=0.0,
➥ sigma=.1, indpb=.25) ❹
❶ 创建长度为 number_of_genes 的浮点基因序列
❷ 将选择设置为大小为 5 的锦标赛
❸ 使用混合函数进行交叉
❹ 使用高斯变异
同样,我们可以回顾 evaluate 函数,如下所示。注意我们返回准确率的倒数。这允许我们在进化过程中最小化 fitness,从而最大化 individual 的准确率。
列表 6.6 EDL_6_2_MLP_GA.ipynb: evaluate 函数
def evaluate(individual):
nn.set_parameters(individual) ❶
nn_predictions = nn.predict(X) ❷
return 1/np.sum(nn_predictions == Y) / Y.shape[0], ❸
toolbox.register("evaluate", evaluate) ❹
❶ 根据个体基因设置模型参数
❷ 在问题数据集上评估模型预测
❸ 返回计算出的准确率的倒数
❹ 将函数注册到工具箱中
最后,我们可以跳到进化模型以优化模型的代码,如列表 6.7 所示。正如你所期望的,我们使用eaSimple函数在一系列代中训练种群。然后,我们输出上一代种群中的一个样本个体和当前最佳个体作为比较。在代码的末尾,如果准确率达到某个值,我们检查早期停止条件。检查早期停止条件允许我们的代码一旦找到可接受的解决方案就立即中断。
列表 6.7 EDL_6_2_MLP_GA.ipynb:进化模型
for g in range(NGEN):
pop, logbook = algorithms.eaSimple(pop, toolbox,
cxpb=CXPB, mutpb=MUTPB, ngen=RGEN,
➥ stats=stats, halloffame=hof, verbose=False) ❶
best = hof[0]
clear_output()
print(f"Gen ({(g+1)*RGEN})")
show_predictions(nn, X, Y, "Neural Network") ❷
nn_predictions = nn.predict(X)
print("Current Neural Network accuracy : ",
➥ np.sum(nn_predictions == Y) / Y.shape[0]) ❷
plt.show() ❷
nn.set_parameters(best) ❸
show_predictions(nn, X, Y, "Best Neural Network") ❸
plt.show() ❸
nn_predictions = nn.predict(X) ❸
acc = np.sum(nn_predictions == Y) / Y.shape[0] ❸
print("Best Neural Network accuracy : ", acc) ❸
if acc > .99999: #stop condition ❹
break
❶ 调用进化函数以进化种群
❷ 显示了上一代最后一个个体的结果
❸ 显示了最佳个体的结果
❹ 如果满足早期停止条件则中断
图 6.8 展示了将种群进化到一个可以以 100%准确率解决圆问题的个体的例子。当你考虑到我们使用反向传播的 MLP 网络在这个问题上只能达到 50%时,这相当令人印象深刻。

图 6.8 使用 GA 解决圆问题的进化过程
用 GA 探索其他问题数据集,看看这种方法与简单的反向传播和梯度下降优化相比如何。再次强调,还有更强大的优化器,比如我们稍后比较的 Adam,但请花时间欣赏 GA 如何优化一个简单的 MLP 网络。
6.2.1 学习练习
使用以下练习来提高你的神经进化知识:
-
增加或减少样本数量然后重新运行。使用更少或更多的样本收敛网络参数是否更难?
-
修改
交叉和变异率然后重新运行。你能提高给定问题的进化性能吗? -
增加或减少中间层的大小,然后重新运行。网络大小对进化有什么影响?
当然,我们还有更强大的进化方法,如进化策略和差分进化,可能表现更好。我们将在下一节花时间查看这两种更先进的进化方法。
6.3 用于神经优化的其他进化方法
在第五章中,当我们调整超参数时,我们看到了使用其他进化方法(如进化策略和差分进化)的一些很好的结果。看到这样的好结果,将 ES 和 DE 应用于上一节中解决的问题集是很有意义的。
在这个项目中,我们将 ES 和 DE 作为神经进化优化器应用。这两个代码示例是上一个项目的扩展,位于单独的笔记本中。我们在两个笔记本和上一个项目之间来回跳转,以便进行比较。
在 Colab 中打开两个独立的浏览器标签页中的 EDL_6_3_MLP_ES.ipynb 和 EDL_6_3_MLP_DE.ipynb。你可能还想保持最后一个项目笔记本 EDL_6_2_MLP_GA.ipynb 的打开状态。如果你需要帮助,请参阅附录。
从笔记本的“数据集参数”单元格中选择相同的问题,圆或月亮。如果你不确定选择哪个问题,请参阅图 6.2 和相应的表格,以更详细地解释选项。
通过菜单中的“运行”>“运行所有”来运行两个笔记本的所有单元格。在它们运行时在两个笔记本之间切换,以查看每种方法如何优化权重。
图 6.9 展示了在圆和月亮问题上的 ES 和 DE 笔记本运行到完成(最多 1,000 代)的示例。特别值得注意的是,DE 和 ES 如何为每个问题进化权重。注意 ES 笔记本的进化以及 2D 可视化产生的几条直线。ES 不仅擅长解决这些更难的数据集,而且还有解决更难问题的潜力。

图 6.9 ES 与 DE 在圆和月亮问题数据集上的比较
我们已经回顾了两个笔记本中的所有主要代码元素,所以在这里我们不会重新审视任何代码。然而,你可以自己查看代码的结构,看看从使用 GA 转换为 ES 和 DE 有多容易。你也可以回过头去尝试其他问题或调整数据集参数单元格中的其他设置,以查看 ES 或 DE 的表现。
对于本套项目中展示的样本数据集,简单的 GA 方法通常表现最好。虽然这可能会略有不同,但 DE 显然不是最佳选择,但 ES 有一些明确的潜力。在后面的章节中,我们再次回顾这种方法的选择,以深入了解哪种选项是最好的。
6.3.1 学习练习
完成以下练习,以帮助提高你的理解:
-
找到一个问题类别,其中 ES 的表现优于 DE,反之亦然。
-
调整各种超参数选项,然后看看它们对 DE 或 ES 笔记本有什么影响。
-
玩转特定的进化方法超参数——ES 的最小和最大策略,以及 DE 的 pmin/pmax 和 smin/smax。
在本节中,我们探讨了如何使用其他进化方法来对简单的 NumPy 网络权重进行优化。在下一节中,我们将应用同样的原理,但这次是针对深度学习框架,例如 Keras。
6.4 将神经进化优化应用于 Keras
虽然在前一个项目中,我们用来进行比较的 MLP 网络功能相对较弱且有限。为了进行有效的比较,我们应该“提高我们的水平”并查看一个更健壮的深度学习平台,如 Keras。Keras 与 PyTorch 和其他许多深度学习框架类似,提供了一系列开箱即用的高级优化器。
在以下项目中,我们设置了一个 Keras 多层深度学习网络来解决分类数据集。这不仅提供了使用鲁棒且成熟的优化器(如 Adam)之间的良好比较,而且还展示了我们如何将 神经进化优化(NO)集成到 Keras 网络中。
在 Colab 中打开 EDL_6_4_Keras_GA.ipynb 笔记本。如需帮助,请参阅附录。暂不运行笔记本中的所有单元格,因为我们一步一步地进行。
定位并选择如图 6.8 所示的 Keras 模型设置代码单元格,然后通过选择菜单中的“运行之前”来运行笔记本的所有前一个单元格。该代码创建了一个简单的 Keras 模型,具有输入、隐藏和输出层。输出是一个单一的二进制节点;我们使用二元交叉熵来计算损失。我们还确定了模型的可训练参数数量,因为这也与后面的 genes 数量有关。
列表 6.8 EDL_6_4_Keras_GA.ipynb:设置 Keras 模型
model = tf.keras.models.Sequential([ ❶
tf.keras.layers.Dense(16, activation='relu', ❶
➥ input_shape=(X.shape[1],)), ❶
tf.keras.layers.Dense(32, activation='relu'), ❶
tf.keras.layers.Dense(1, activation='sigmoid') ❶
])
optimizer = tf.keras.optimizers.Adam
➥ (learning_rate=.001) ❷
model.compile(optimizer=optimizer,
loss='binary_crossentropy', ❸
metrics=['accuracy']) ❹
model.summary() ❺
trainableParams = np.sum([np.prod(v.get_shape()) for v in ❺
➥ model.trainable_weights]) ❺
print(f"Trainable parameters: {trainableParams}") ❺
❶ 创建一个简单的 Keras Sequential 模型
❷ 创建一个具有学习率的 Adam 类型优化器
❸ 将损失设置为二元交叉熵
❹ 使用准确性指标
❺ 打印模型的摘要并输出可训练参数
运行 Keras 设置单元格;你将得到图 6.10 所示的输出。输出显示了模型摘要以及每层的参数/权重数量。输出底部还打印了可训练参数的总数。这很重要,因为它代表了 individual 中的 genes 数量。

图 6.10 模型总结输出和参数计数
返回到数据集参数单元格,如图 6.2 所示,并选择一个困难的问题,如月亮或圆圈。这会重新运行单元格并生成问题数据集的视图。
滚动到模型训练代码单元格,如下所示列表,然后运行单元格。作为这部分训练代码的一部分,我们使用了一个有用的回调函数:来自 LiveLossPlot 模块的 PlotLossesKeras。
列表 6.9 EDL_6_4_Keras_GA.ipynb:拟合模型
model.fit(X, Y, epochs=epochs, ❶
callbacks=[PlotLossesKeras()], ❷
verbose=0) ❸
❶ 在数据集上训练模型多个时期
❷ 使用 PlotLossesKeras 输出进度图
❸ 关闭噪声输出
运行训练单元格。你将得到与图 6.11 中所示类似的输出。

图 6.11 使用 Adam 优化器在 50 个时期内训练 Keras 模型的示例输出
运行接下来的几个单元格以评估模型的准确性并输出结果。图 6.12 显示了 show_predictions 辅助方法的输出。彩虹图案代表了模型的输出,是一个从 0 到 1 的值。类别的分离在中间的 0.5 处,由黄色带表示。

图 6.12 使用 show_predictions 的模型输出
移动到下一个代码单元格,其中有一个辅助函数,用于提取一个个体的基因并将它们插入到 Keras 模型的权重/参数中。这段代码与我们在简单 MLP 网络中设置模型权重的方式非常相似。它遍历模型层和模型权重,提取一个权重张量。从这个信息中,我们重新构建一个张量,从下一个部分的个体权重中提取,并将其添加到张量列表中。
最后,使用set_weights函数设置模型权重,如下所示。
列表 6.10 EDL_6_4_Keras_GA.ipynb:拟合模型
def set_parameters(individual):
idx = 0
tensors=[]
for layer in model.layers: ❶
for na in layer.get_weights(): ❷
size = na.size
sh = na.shape
t = individual[idx:idx+size]
t = np.array(t)
t = np.reshape(t, sh)
idx += size
tensors.append(t) ❸
model.set_weights(tensors) ❹
❶ 遍历模型层
❷ 遍历层的权重张量
❸ 将新的张量追加到列表中
❹ 从张量列表中设置模型的权重
下一个单元格将所有模型权重设置为 1,并使用show_predictions输出结果。同样,我们遵循在 MLP 项目中使用的相同程序。
其余的代码与之前的 GA 示例相同,因此请从菜单中选择运行 > 运行后续单元格来运行其余的单元格。只需确保您已选择一个单元格,其中代码和之前的单元格已经完全运行。如果您不确定哪个单元格是最后运行的,您也可以简单地运行所有单元格。
图 6.13 显示了使用 Keras 网络运行 GA 优化的输出。注意模型在没有使用任何深度学习优化器的情况下优化得有多好。如果你是经验丰富的 Keras 用户,你可以尝试替换各种其他优化器,看看是否有任何可以击败进化优化器的。

图 6.13 使用 GA 在圆形问题上优化的 Keras 模型的输出
6.4.1 学习练习
以下练习旨在展示 Keras 中神经进化的局限性:
-
将问题类型更改为圆形,然后重新运行问题。网络进化的权重能否解决这个问题?
-
修改列表 6.8 中的 Keras 模型,然后重新运行笔记本。当你从模型中移除或添加新层时会发生什么?
-
在列表 6.8 中,将网络损失更改为使用均方误差(MSE)而不是二元交叉熵。这对进化和结果的表现有何影响?
现在,我们的工具箱中有一个强大的新工具——这似乎肯定会对所有深度学习(DL)都有益。不幸的是,这种方法以及进化搜索在一般意义上都有一些局限性。我们将在下一节中查看这些局限性的一个例子。
6.5 理解进化优化的局限性
深度学习模型的大小一直在爆炸式增长,从早期模型拥有数百个参数到最新的变换器拥有数十亿个参数。优化或训练这些网络需要大量的计算资源,因此尝试评估更好的方法将始终是一个优先事项。因此,我们希望从玩具数据集转向更实际的进化优化应用。
在下一个项目中,我们将从玩具数据集提升到一级示例问题,即对修改后的国家标准与技术研究院(MNIST)手写数字数据集进行分类。作为你的深度学习教育的一部分,你很可能已经以某种方式使用过 MNIST。MNIST 通常是我们的第一个数据集,我们用它来构建深度学习网络进行分类。
在 Colab 中打开 EDL_6_5_MNIST_GA.ipynb 笔记本。附录可以帮助你在需要时完成任务。运行笔记本顶部的两个单元格——pip install和import——以设置笔记本代码的基础。下一个单元格加载 MNIST 数据集,归一化值,并将它们放入训练术语x和y中,如下所示。
列表 6.11 EDL_6_5_MNIST_GA.ipynb:加载数据
mnist = tf.keras.datasets.mnist ❶
(x_train, y_train), (x_test, y_test) =
➥ mnist.load_data() ❶
X, Y = x_train / 255.0, y_train ❷
plt.imshow(X[0]) ❸
print(Y[0]) ❹
❶ 加载 MNIST 数据集进行训练和测试。
❷ 将字节数值归一化到 0-1 浮点数。
❸ 绘制集合中的一个示例图像。
❹ 打印出图像对应的标签。

图 6.14 MNIST 的一个示例图像
图 6.14 显示了数据集中单个数字的样本输出。下一个单元格包含模型构建代码,因此运行该单元格和列表 6.12 中显示的训练代码。此代码训练模型,并再次使用 livelossplot 模块的PlotLossesKeras函数来显示实时结果。之后,显示模型准确率并生成类别分类报告。
列表 6.12 EDL_6_5_MNIST_GA.ipynb:训练模型
model.fit(X, Y, epochs=epochs, ❶
validation_data=(x_test,y_test), ❷
callbacks=[PlotLossesKeras()], ❸
verbose=0)
print("Neural Network accuracy : ",
➥ model.evaluate(X,Y)[1]) ❹
y_pred = model.predict(x_test)
y_pred = np.argmax(y_pred, axis=1) ❺
print(classification_report(y_test, y_pred)) ❻
❶ 使用数据训练模型
❷ 使用测试数据验证模型
❸ 绘制准确率和损失图
❹ 执行测试预测
❺ 将最高预测值作为类别
❻ 打印分类报告
图 6.15 显示了基于测试预测结果的 sklearn 模块classification_report函数生成的类别分类报告。正如你所看到的,我们的网络在分类所有类别的数字方面明显非常出色。
从菜单中选择运行 > 运行后,以运行笔记本中剩余的所有单元格。同样,这个笔记本中的大部分代码与我们的前一个项目相同,所以我们不需要对其进行审查。

图 6.15 MNIST 数字分类的分类报告
图 6.16 展示了最后单元格执行进化的样本输出。此图显示了随着时间的推移准确率的进展以及分类报告。从这个快速示例中可以看出,当进化优化接近更大的模型时,它具有关键的限制。

图 6.16 展示了 GA EO 的示例输出,显示了较差的结果
如上一次项目所示,使用进化优化/搜索来寻找最佳网络权重/参数会产生较差的结果,而网络在几小时的训练后可以达到高达 60%的准确率,这比随机要好得多。然而,每个类别的准确率结果都不理想,不能接受。
6.5.1 学习练习
这些练习是为希望测试神经进化权重/参数优化极限的高级读者准备的:
-
通过改变网络大小和形状来更改基础 Keras 模型。使用更小的网络您能得到更好的结果吗?
-
向模型添加卷积层和最大池化。这可以帮助减少需要进化的模型参数总数。
-
将笔记本代码调整以适应您过去使用或合作过的其他模型。
显然,最后一个项目的结果表明,使用进化搜索进行深度学习优化在更大的参数模型上不会奏效。但这并不意味着这项技术完全没有价值,正如我们在后面的章节中讨论的那样。
摘要
-
可以使用 NumPy 库开发一个简单的多层感知器网络。Sklearn 可用于生成各种单标签分类数据集,这些数据集通过简单的 NumPy MLP 网络展示了二元模型分类。
-
DEAP 和遗传算法可用于寻找简单深度学习网络的权重/参数。
-
结合进化策略和微分演化的 DEAP 可用于优化简单多层感知器网络的权重/参数搜索。比较这两种方法可以有助于评估用于各种进化优化方法和各种样本分类在问题数据集上使用的工具。
-
Keras 深度学习模型可以调整以使用进化搜索进行权重优化,而不是传统的微分反向传播方法。
-
进化权重优化可以成功解决复杂且不可微分的难题。
-
使用自动微分和反向传播的深度学习问题仅限于解决连续问题。
-
进化优化可用于解决以前深度学习网络无法解决的离散问题。
-
随着问题规模的扩大,进化优化变得不太成功。将 EO 应用于更复杂的问题,如图像分类,通常不会成功。
7 进化卷积神经网络
本章涵盖
-
带有 Keras 入门的卷积神经网络
-
使用基因序列定义神经网络架构
-
构建自定义交叉算子
-
应用自定义变异算子
-
为给定数据集进化最佳卷积网络架构
最后一章向我们展示了当将进化算法应用于像参数搜索这样的复杂问题时,其局限性。正如我们所见,遗传算法在处理某一类问题时可以提供出色的结果。然而,当用于更大的图像分类网络时,它们却无法达到预期效果。
在本章中,我们继续探讨用于图像分类的更大网络。然而,这一次,我们不是优化参数权重或模型超参数,而是关注改进网络架构。更具体地说,我们涵盖了卷积神经网络(CNN)的网络架构。
CNN 对于图像分类和其他任务的深度学习采用起到了关键作用。它们是深度学习实践者工具箱中的绝佳工具,但通常被误解和低效使用。在下一节中,我们将回顾 CNN 模型以及它们在 TensorFlow 和 Keras 中的构建方式。
7.1 在 Keras 中回顾卷积神经网络
本节的项目是对使用 Keras 构建图像分类 CNN 模型的回顾。虽然我们涵盖了 CNN 的一些基础知识,但我们的重点更多地在于构建这些类型网络所面临的细节问题。
CNN 的未来
CNN 层正迅速被更先进的技术所取代,如残差网络和注意力机制(即转换器)。我们在本章中学到的相同原则可以应用于优化这些其他架构。
在这个项目中,我们在 Fashion-MNIST 数据集上执行图像分类,如图 7.1 所示。这是一个很好的基本测试数据集,可以缩减数据量,而不会对结果产生太大的影响。减少用于训练或推理的数据量可以缩短我们后续进化的运行时间。
GPU 训练
本章中使用的笔记本项目已经准备好使用 GPU,因为处理量很大。然而,Colab 可能会对你的 GPU 实例访问设置限制或限制。如果你发现这有问题,并且你有访问带有 GPU 的机器,你总是可以通过连接到本地实例来运行 Colab。
在 Colab 中打开 EDL_7_1_Keras_CNN.ipynb 笔记本。如果你需要帮助打开笔记本,请查看附录。像往常一样,前几个单元是安装、导入和设置。我们可以忽略这些,并通过菜单中的“运行”>“运行所有”来运行整个笔记本。
我们首先想要查看的是数据加载,如列表 7.1 所示。在这里我们加载 Fashion 数据集,将数据归一化并重塑为 28, 28, 1 张量,其中末尾的 ,1 表示通道。我们这样做是因为数据集以没有定义通道的二维数组形式提供。在代码块末尾,我们提取原始数据的前 1,000 个样本用于训练,100 个用于测试。

图 7.1 Fashion-MNIST 数据集
这样大幅减少数据集并不是最佳选择,但当我们尝试优化成百上千的 个体 或众多 代 时,这将节省我们几分钟或几小时的时间。
列表 7.1 EDL_7_1_Keras_CNN.ipynb:加载数据
dataset = datasets.fashion_mnist ❶
(x_train, y_train), (x_test, y_test) = ❶
➥ dataset.load_data() ❶
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1) ❷
.astype("float32") / 255.0 ❷
x_test = x_test.reshape(x_test.shape[0], 28, 28, 1) ❷
.astype("float32") / 255.0 ❷
x_train = x_train[:1000] ❸
y_train= y_train[:1000] ❸
x_test = x_test[:100] ❸
y_test= y_test[:100] ❸
❶ 加载数据集。
❷ 归一化和重塑数据。
❸ 提取数据的一个较小子集。
接下来的几个单元格构建了图 7.1 中所示的输出。我们在此不再进一步讨论它们。
图 7.2 展示了如何定义单个层,从代码到可视化实现。每个 CNN 层定义了一组滤波器或神经元,它们描述了一个块或核。单个核通过步长在图像上移动,通常为 1 像素乘以 1 像素。为了简单起见,我们将步长固定在 1, 1。

图 7.2 在 Keras 中定义 CNN 层
构建模型卷积层的代码如列表 7.2 所示。每个 Conv2D 层定义了对输入应用的卷积操作。在每一连续层中,滤波器或通道的数量从上一层扩展。例如,第一个 Conv2D 层将输入通道从 1 扩展到 64。然后,连续层将其减少到 32,然后是 16,其中每个卷积层后面都跟着一个 MaxPooling 层,该层收集或总结特征。
列表 7.2 EDL_7_1_Keras_CNN.ipynb:构建 CNN 层
model = models.Sequential()
model.add(layers.Conv2D(64, (3, 3), activation='relu', padding="same",
➥ input_shape=(28, 28, 1))) ❶
model.add(layers.MaxPooling2D((2, 2), padding="same")) ❷
model.add(layers.Conv2D(32, (3, 3), activation='relu',
➥ padding="same")) ❸
model.add(layers.MaxPooling2D((2, 2))) ❹
model.add(layers.Conv2D(16, (3, 3), activation='relu')) ❺
model.add(layers.MaxPooling2D((2, 2))) ❻
model.summary()
❶ 第一层卷积神经网络(CNN)接收张量输入形状。
❷ 最大池化层
❸ 中间 CNN 层
❹ 最大池化层
❺ 中间 CNN 层
❻ 最大池化层
图 7.3 展示了单个滤波器或核操作如何应用于单个图像块,以及它如何提取与输出相对应的值。相应的输出是通过在图像上滑动滤波器块产生的,其中每个核操作代表一个单独的输出值。请注意,滤波器中的核值或权重/参数是学习得到的。

图 7.4 最大池化操作
在设置好模型的卷积和最大池化层之后,使用model.summary()打印摘要,如下所示。请注意,这仅仅是完整模型的上部,或特征提取器部分。
列表 7.3 EDL_7_1_Keras_CNN.ipynb:CNN 模型摘要
Model: "sequential_4"
_____________________________________________________________
Layer (type) Output Shape Param #
=============================================================
conv2d_8 (Conv2D) (None, 28, 28, 64) 640 ❶
max_pooling2d_7 (MaxPooling (None, 14, 14, 64) 0 ❷
2D)
conv2d_9 (Conv2D) (None, 14, 14, 32) 18464
max_pooling2d_8 (MaxPooling (None, 7, 7, 32) 0
2D)
conv2d_10 (Conv2D) (None, 5, 5, 16) 4624
max_pooling2d_9 (MaxPooling (None, 2, 2, 16) 0
2D)
=================================================================
Total params: 23,728 ❸
Trainable params: 23,728
Non-trainable params: 0
________________________________________________________________
❶ 一个 3×3 的核加上偏置为每个过滤器提供 10 个参数——10×64 = 640。
❷ 池化层不可训练且没有参数。
❸ 参数总数
在下一个单元中,CNN 层的输出被展平并输入到一个单一的密集层,该层输出到 10 个类别,如下所示。
列表 7.4 EDL_7_1_Keras_CNN.ipynb:完成模型
model.add(layers.Flatten()) ❶
model.add(layers.Dense(128, activation='relu')) ❷
model.add(layers.Dense(10)) ❸
model.summary()
❶ 将 2D 卷积的输出展平到 1D
❷ 添加一个密集层进行分类推理。
❸ 添加一个最终的密集层以输出 10 个类别。
图 7.5 显示了在大量减少的数据集上训练的模型输出。通常,这个数据集被优化以在约 98%的准确率下运行。然而,由于前面提到的原因,在完整数据集上训练既耗时又不切实际,尤其是在我们应用进化算法时。相反,关注这个减少数据集的准确率;我们不会回顾模型的编译和训练代码,因为我们在第六章中已经讨论过。

图 7.5 在减少数据集上模型训练
您的结果可能会有所不同,但您应该看到训练或验证数据的最大值大约在 81%。如果您决定为这个项目使用其他数据集,请注意,您的结果可能会有很大差异。Fashion-MNIST 适用于这个应用,因为类之间的差异很小。这当然不会是像 CIFAR-10 或 CIFAR-100 这样的数据集的情况。
参考图 7.5;查看训练和测试中的问题差异,以及损失和准确率。我们可以看到,模型在 3 个 epoch 左右就崩溃了,无法对盲测试数据进行任何好的推理。这可能与我们的数据量减少有关,部分也与模型的结构有关。在下一节中,我们将介绍一些明显的 CNN 层架构,并看看它们引入了什么问题。
7.1.1 理解 CNN 层问题
在本节中,我们探索了一些 CNN 层架构的进一步示例,并了解它们引入的问题。CNN 是一个很好的工具,如果使用得当,但如果不有效地使用,它很快就会变成一场灾难。了解何时出现问题是我们在进化优化后续尝试中受益的。
重新打开 EDL_7_1_Keras_CNN.ipynb 笔记本,然后导航到标记为 SECTION 7.1.1 的部分。请确保使用菜单中的“运行”>“运行所有”运行所有单元格。
第一个单元格包含了一个新模型的代码,这次只有一个 CNN 层。正如你所见,我们定义了一个包含 64 个滤波器/神经元和 3×3 核的单层。图 7.6 显示了运行此单元格的输出;注意以下列表中此模型的总参数(超过 600 万个)与之前模型(2.3 万个)在列表 7.3 中的参数之间的极端差异。
列表 7.5 EDL_7_1_Keras_CNN.ipynb:单个 CNN 层
model = models.Sequential()
model.add(layers.Conv2D(64, (3, 3), activation='relu',
➥ padding="same", input_shape=(28, 28, 1))) ❶
model.add(layers.Flatten())
model.add(layers.Dense(128, activation='relu')) ❷
model.add(layers.Dense(10)) ❸
model.summary())
❶ 单个 2D 卷积层
❷ 单个密集层
❸ 输出到 10 个类别

图 7.6 单个 CNN 层模型的总结
图 7.7 显示了运行下一个单元格的模型的训练输出。注意模型在训练数据上的表现如何良好,但在验证/测试数据上的表现如何糟糕。这是因为具有超过 600 万个参数的模型记忆了减少的数据集。因此,你可以看到训练集的准确率几乎达到了 100%,这是非常棒的。然而,测试/验证集开始下降。

图 7.7 单层 CNN 模型训练输出
模型记忆/专业化与泛化
我们经常希望构建泛化的模型,因此我们将数据分为训练集和测试集以验证这种泛化。我们还可以应用一些其他技术来帮助泛化,比如批量归一化和 dropout,我们稍后会讨论。然而,在某些情况下,泛化可能不是你的最终目标,你可能希望识别非常具体的数据集。如果是这样,那么一个记忆数据的模型是理想的。
现在,我们继续讨论池化对卷积输出的影响。列表 7.6 显示了模型的变化和总训练参数的总结。值得注意的是,这个模型的大小大约是之前模型的四分之一,这是由于添加了池化。我们还在池化层之间添加了一个批量归一化层,以更好地泛化模型。
列表 7.6 EDL_7_1_Keras_CNN.ipynb:添加池化
model = models.Sequential()
model.add(layers.Conv2D(64, (3, 3), activation='relu',
➥ padding="same", input_shape=(28, 28, 1))) ❶
model.add(layers.BatchNormalization()) ❷
model.add(layers.MaxPooling2D((2, 2), padding="same")) ❸
model.add(layers.Flatten())
model.add(layers.Dense(128, activation='relu'))
model.add(layers.Dense(10))
model.summary()
...
=================================================================
Total params: 1,607,946
Trainable params: 1,607,818 ❹
Non-trainable params: 128
_________________________________________________________________
❶ 2D 卷积层
❷ 批量归一化层
❸ 使用 2×2 核的池化层
❹ 可训练参数总数
图 7.8 显示了在 10 个周期上训练模型的输出。虽然这个模型仍然显示出记忆的迹象,但模型在泛化方面也做得更好。我们可以通过查看不断提高的验证准确率和相应的损失下降来看到这一点。

图 7.8 训练更高级 CNN 模型的输出
我们当然可以继续通过模型的多种变体进行尝试,添加更多的 CNN 层或类似批量归一化、dropout 或池化的层。然后,我们会调整各种超参数,如核大小、神经元和滤波器的数量,但这显然会消耗大量时间。
7.1.2 学习练习
如果需要,使用以下学习练习来帮助提高你对卷积的理解:
-
在列表 7.6 中增加或减少内核大小,然后看看这会对结果产生什么影响。
-
在列表 7.6 中将池化大小从(2,2)增加或减少,然后重新运行。
-
在列表 7.6 中将额外的卷积层添加到模型中,然后重新运行。
最终,理解如何以及在哪里使用 CNN 层需要一些尝试和错误——这与超参数优化类似。即使你深刻理解了卷积过程,定义正确的 CNN 架构也可能很困难。这当然使得使用某种进化过程来优化 CNN 网络架构成为理想的选择。
7.2 使用基因编码网络架构
在本节的项目中,我们查看将 CNN 模型的网络架构编码为基因的细节。这是进化这些个体``基因序列以产生针对给定数据集的优化模型的先决条件。
已有数篇论文和少数工具被发布,用于进化网络架构。本项目中的代码部分源自一篇题为“用于图像分类的进化深度卷积神经网络”的论文,作者为 Yanan Sun 等人。在这篇论文中,作者开发了一个名为 EvoCNN 的过程,用于构建 CNN 模型架构。
EvoCNN 定义了一个将卷积网络编码为可变长度基因序列的过程,如图 7.9 所示。在构建我们的基因序列时,我们希望定义一个基本规则,即所有序列都将从卷积层开始,并以一个将输入到另一个密集输出层的密集层结束。为了简化问题,我们在这里不考虑编码最后一个输出层。

图 7.9 网络架构的可变长度基因编码
在每个主要组件层内部,我们还想定义相应的超参数选项,例如滤波器/神经元的数量和内核大小。为了编码这种多样的数据,我们使用否定技巧来分离主要层组件和相关超参数。这个下一个笔记本项目中的代码只关注构建编码序列;我们稍后处理剩余的部分。
在 Colab 中打开 EDL_7_2_Encoding_CNN.ipynb 笔记本。如果你不能为这个项目使用 GPU,请不要担心;我们目前只关注架构编码,还没有进行进化训练。
我们首先查看的代码块(列表 7.7)是我们设置的常数,帮助我们定义层类型和长度,以封装各种相关超参数。我们首先定义了总的最大层数和其他各种层超参数的范围。之后,我们可以看到每种类型的块标识符及其对应的大小。这个大小值表示每个层定义的长度,包括超参数。
列表 7.7 EDL_7_2_Encoding_CNN.ipynb:编码常量
max_layers = 5 ❶
max_neurons = 128
min_neurons = 16
max_kernel = 5
min_kernel = 2
max_pool = 3
min_pool = 2
CONV_LAYER = -1 ❷
CONV_LAYER_LEN = 4 ❸
POOLING_LAYER = -2 ❷
POOLING_LAYER_LEN = 3 ❸
BN_LAYER = -3 ❷
BN_LAYER_LEN = 1 ❸
DENSE_LAYER = -4 ❷
DENSE_LAYER_LEN = 2 ❸
❶ 设置最大和最小构建参数。
❷ 识别层块开始。
❸ 识别层块大小。
图 7.10 展示了带有编码层块及其相应超参数的 gene 序列的外观。注意,负值 -1、-2、-3 和 -4 代表层组件的开始。然后,根据层类型,进一步定义了过滤器/神经元数量和内核大小的额外超参数。

图 7.10 CNN 模型架构的 gene 编码
现在我们可以查看构建一个 individual 的 gene 序列(染色体)的代码,如列表 7.8 所示。首先,我们来看 create_offspring 函数,这是构建序列的基础。此代码遍历最大层计数,并带有 50% 的概率添加一个卷积层。如果是这样,它将进一步检查,带有 50% 的概率添加批量归一化和/或池化层。
列表 7.8 EDL_7_2_Encoding_CNN.ipynb:创建后代(gene 序列)
def create_offspring():
ind = []
for i in range(max_layers):
if random.uniform(0,1)<.5:
ind.extend(generate_conv_layer()) ❶
if random.uniform(0,1)<.5:
ind.extend(generate_bn_layer()) ❷
if random.uniform(0,1)<.5:
ind.extend(generate_pooling_layer()) ❸
ind.extend(generate_dense_layer())
return ind
❶ 添加一个卷积层。
❷ 添加一个批量归一化层。
❸ 添加一个池化层。
为了完整性,我们还可以回顾各种层构建函数。以下列表中并未显示所有代码,但所示内容应能让你了解辅助函数的工作方式。
列表 7.9 EDL_7_2_Encoding_CNN.ipynb:层组件辅助函数
def generate_pooling_layer():
part = [POOLING_LAYER] ❶
part.append(random.randint(min_pool, max_pool)) ❷
part.append(random.randint(min_pool, max_pool)) ❷
return part
def generate_dense_layer():
part = [DENSE_LAYER] ❶
part.append(generate_neurons()) ❸
return part
def generate_conv_layer():
part = [CONV_LAYER] ❶
part.append(generate_neurons()) ❸
part.extend(generate_kernel()) ❷
return part
❶ 添加一个层标记以开始序列块。
❷ 为内核大小添加超参数。
❸ 为过滤器/神经元添加超参数。
调用 create_offspring 生成一个 gene 序列,如运行最后一个单元格的输出所示。请运行单元格几次,以查看创建的 gene 序列的变体,如下列所示。
列表 7.10 EDL_7_2_Encoding_CNN.ipynb:检查生成的 gene 序列
individual = create_offspring() ❶
print(individual)
[-1, 37, 5, 2, -3, -1, 112, 4, 2, -4, 25] ❷
❶ 创建一个后代个体。
❷ 随机基因序列的示例输出
使用 gene 序列,我们现在可以继续构建模型,本质上解析 gene 序列并创建一个 Keras 模型。如你所见,build_model 的输入是一个单一的 gene 序列,它产生一个 Keras 模型。否则,代码是一个标准的标记解析器,寻找层组件标记 -1、-2、-3 或 -4。在定义层之后,它根据层类型添加额外的超参数,如下列所示。
列表 7.11 EDL_7_2_Encoding_CNN.ipynb:构建模型
def build_model(individual):
model = models.Sequential()
il = len(individual)
i = 0
while i < il:
if individual[i] == CONV_LAYER: ❶
n = individual[i+1]
k = (individual[i+2], individual[i+3])
i += CONV_LAYER_LEN
if i == 0: ❷
model.add(layers.Conv2D(n, k, activation='relu', padding="same",
➥ input_shape=(28, 28, 1)))
else:
model.add(layers.Conv2D(n, k, activation='relu', padding="same"))
elif individual[i] == POOLING_LAYER: ❸
k = k = (individual[i+1], individual[i+2])
i += POOLING_LAYER_LEN
model.add(layers.MaxPooling2D(k, padding="same"))
elif individual[i] == BN_LAYER: ❹
model.add(layers.BatchNormalization())
i += 1
elif individual[i] == DENSE_LAYER: ❺
model.add(layers.Flatten())
model.add(layers.Dense(individual[i+1], activation='relu'))
i += 2
model.add(layers.Dense(10))
return model
❶ 添加一个卷积层。
❷ 将输入形状添加到第一个卷积层。
❸ 添加一个池化层。
❹ 添加一个批量归一化层。
❺ 添加一个密集层。
下一段代码创建一个新的 individual gene 序列,从序列构建模型,然后训练模型,输出训练/验证图,正如我们之前所看到的。
你的结果可能非常差或相对较好,这取决于随机的初始序列。继续运行这个最后的单元格几次,以查看不同初始随机 individuals 之间的差异。
7.2.1 学习练习
使用以下练习来提高你的理解:
-
通过在循环中调用
create_offspring从列表 7.8 创建新的gene编码序列。打印,然后比较individuals。 -
修改列表 7.6 中的最大/最小范围超参数,然后生成新的后代列表(见练习 1)。
-
向
create_offspring添加一个新的输入,将静态概率从 0.5 更改为新值。然后,生成后代列表(见练习 1)进行比较。
现在我们有了一种定义表示模型架构的 gene 序列的方法,我们可以继续构建支持此类序列的遗传算子。不幸的是,我们无法使用 DEAP 的内置算子,而必须为交配(crossover)和 mutation 创建自己的算子。
7.3 创建交配交叉操作
DEAP toolbox 中可用的标准遗传算子不足以满足我们自定义网络架构 gene 序列的需求。这是因为任何标准的交配算子都可能破坏我们 gene 序列的格式。因此,我们需要为交配(crossover)和 mutation 建立自己的自定义算子。
图 7.11 显示了当应用于两个交配父母时,这个自定义 crossover 操作看起来是什么样子。该操作通过获取两个父母并提取各种层集合到列表中——一个用于卷积,一个用于池化等。从每个列表中,随机选择一对层在 gene 序列之间交换。生成的 genes 序列成为产生的后代。

图 7.11 crossover 操作的可视化
执行这个自定义 crossover 操作的代码在我们的下一个笔记本中,但实质上,它是我们之前查看的最后一个笔记本的扩展。在审查此代码时,请记住,这只是执行 crossover 的一种选项,你可能还会考虑其他选项。重要的是在 crossover 操作后保持 gene 序列的正确格式。
在 Colab 中打开 EDL_7_3_Crossover_CNN.ipynb 笔记本。运行所有单元格(运行 > 运行所有),然后滚动到笔记本的底部附近。再次强调,这个笔记本只是基于我们之前的练习,我们在这里不需要回顾之前的代码。
滚动到标题为“自定义交叉算子”的单元格。这里有一些代码,所以我们将其分解成几个部分来审查,从下面的列表中的主要 crossover 函数开始。这个主要函数为每一组层调用 swap_layers 函数。
列表 7.12 EDL_7_3_Crossover_CNN.ipynb:自定义 crossover 函数
def crossover(ind1, ind2): ❶
ind1, ind2 = swap_layers(ind1, ind2, CONV_LAYER, ❷
➥ CONV_LAYER_LEN) ❷
ind1, ind2 = swap_layers(ind1, ind2, POOLING_LAYER, ❷
➥ POOLING_LAYER_LEN) ❷
ind1, ind2 = swap_layers(ind1, ind2, BN_LAYER, BN_LAYER_LEN) ❷
ind1, ind2 = swap_layers(ind1, ind2, DENSE_LAYER, DENSE_LAYER_LEN) ❷
return ind1, ind2 ❸
❶ 该函数接受两个个体作为输入。
❷ 交换各种层组。
❸ 返回两个新的后代。
swap_layers函数是每个层类型从序列中提取并随机交换的地方。我们首先通过类型从每个序列中获取层列表。c1和c2都是我们循环以确定交换点的索引列表。从这些列表中,我们随机抓取一个值来交换每个序列,然后使用swap函数执行交换,如下面的列表所示。
列表 7.13 EDL_7_3_Crossover_CNN.ipynb:交换层
def swap_layers(ind1, ind2, layer_type, layer_len):
c1, c2 = get_layers(ind1, layer_type),
➥ get_layers(ind2, layer_type) ❶
min_c = min(len(c1), len(c2)) ❷
for i in range(min_c):
if random.random() < 1:
i1 = random.randint(0, len(c1)-1) ❸
i2 = random.randint(0, len(c2)-1) ❸
iv1 = c1.pop(i1)
iv2 = c2.pop(i2)
ind1, ind2 = swap(ind1, iv1, ind2, iv2, layer_len) ❹
return ind1, ind2
❶ 获取每个序列中层的列表。
❷ 找到层列表的最小长度。
❸ 从每个层组中随机选择索引。
❹ 交换层。
get_layers函数是我们从每个gene序列中提取层索引的地方。这可以通过列表推导式简洁地完成,通过检查序列中的每个值并提取列表中匹配的位置,如下面的列表所示。
列表 7.14 EDL_7_3_Crossover_CNN.ipynb:查找层索引
def get_layers(ind, layer_type): ❶
return [a for a in range(len(ind)) if ind[a]
➥ == layer_type] ❷
❶ 输入序列和要提取的层类型
❷ 返回序列中层类型的索引列表
我们在这里查看的最后一个函数是swap函数,如下面的列表所示,它负责交换每个individual的层块。swap通过从给定索引的序列中提取每个层块来工作。由于层类型总是相同长度,简单的索引替换是合适的。记住,如果我们的层块长度可变,我们就必须开发一个更高级的解决方案。
列表 7.15 EDL_7_3_Crossover_CNN.ipynb:swap函数
def swap(ind1, iv1, ind2, iv2, ll):
ch1 = ind1[iv1:iv1+ll] ❶
ch2 = ind2[iv2:iv2+ll] ❶
print(ll, iv1, ch1, iv2, ch2) ❷
ind1[iv1:iv1+ll] = ch2 ❸
ind2[iv2:iv2+ll] = ch1 ❸
return ind1, ind2
❶ 从序列中提取块
❷ 打印层交换的输出
❸ 交换块序列
图 7.12 显示了在两个初始后代上执行crossover函数的结果。注意从图中我们是如何交换三个卷积层、一个池化层、一个批量归一化层和一个密集层组的。结果输出序列显示在图 7.12 中。

图 7.12 检查crossover输出
笔记本的其余部分构建、编译和训练生成的individuals,并输出结果。务必查看最后几个单元格,以确认crossover操作没有破坏gene序列格式。现在我们有了用于交配和产生后代的crossover操作,我们可以继续开发最后一个操作:mutation。
7.4 开发定制的突变算子
再次强调,DEAP 中可用的标准mutation算子对我们定制的gene序列没有用。因此,我们需要开发一个定制的mutation算子来模拟我们希望应用于gene序列的mutations类型。在本项目的目的上,我们保持mutation相对简单,仅改变当前层块。在更高级的应用中,mutation可以添加或删除新的层块,但这留给你来实现。
在 Colab 中打开笔记本 EDL_7_4_Mutation_CNN.ipynb。运行所有单元格(运行 > 运行所有)。滚动到笔记本底部附近的标题为“自定义变异算子”的部分。
我们首先检查主要的变异函数,如下所示列表。函数开始时检查个体是否为空。如果不为空,我们继续使用mutate_layers函数对每个层组进行变异。最后,我们按照 DEAP 的约定返回结果。
列表 7.16 EDL_7_4_Mutation_CNN.ipynb:自定义变异算子
def mutation(ind):
if len(ind) > CONV_LAYER_LEN: ❶
ind = mutate_layers(ind, CONV_LAYER, ❷
➥ CONV_LAYER_LEN) ❷
ind = mutate_layers(ind, DENSE_LAYER, ❷
➥ DENSE_LAYER_LEN) ❷
ind = mutate_layers(ind, POOLING_LAYER, ❷
➥ POOLING_LAYER_LEN) ❷
return ind, ❸
❶ 仅变异卷积网络。
❷ 按类型变异层。
❸ 按 DEAP 约定返回元组。
mutate_layers函数遍历特定类型的层组,并仅变异相应的超参数。首先,使用get_layers提取给定类型的层组索引,如上一节所示。然后,在try/except块中,我们通过调用mutate函数来替换给定的索引层块,如下所示列表。
列表 7.17 EDL_7_4_Mutation_CNN.ipynb:mutate_layers函数
def mutate_layers(ind, layer_type, layer_len):
layers = get_layers(ind1, layer_type) ❶
for layer in layers: ❷
if random.random() < 1:
try:
ind[layer:layer+layer_len] = mutate(
ind[layer:layer+layer_len], layer_type) ❸
except:
print(layers) ❹
return ind
❶ 使用 get_layers 提取按类型的层索引。
❷ 遍历索引。
❸ 调用变异函数以替换层块。
❹ 打印出导致错误的层。
mutate函数是所有工作发生的地方。我们首先检查提取的部分具有正确的长度,如列表 7.18 所示。这样做是为了防止任何可能发生的格式化损坏问题,这些问题可能发生在个体上。接下来,根据层类型,我们可能会改变滤波器/神经元和核的大小。注意我们如何将核的大小限制在原始的最小/最大范围内,但将滤波器/神经元的数量留出增长或缩小的空间。在此阶段,我们还检查个体基因序列是否有任何损坏的块——这些块不匹配所需的长度。如果我们发现在变异过程中基因序列是损坏的,那么我们将抛出一个异常。这个异常将在变异函数中被捕获。
列表 7.18 EDL_7_4_Mutation_CNN.ipynb:变异函数
def mutate(part, layer_type):
if layer_type == CONV_LAYER and len(part)==CONV_LAYER_LEN: ❶
part[1] = int(part[1] * random.uniform(.9, 1.1)) ❷
part[2] = random.randint(min_kernel, max_kernel) ❸
part[3] = random.randint(min_kernel, max_kernel) ❸
elif layer_type == POOLING_LAYER and len(part)==POOLING_LAYER_LEN: ❶
part[1] = random.randint(min_kernel, max_kernel) ❸
part[2] = random.randint(min_kernel, max_kernel) ❸
elif layer_type == DENSE_LAYER and len(part)==DENSE_LAYER_LEN: ❶
part[1] = int(part[1] * random.uniform(.9, 1.1)) ❷
else:
error = f"mutate ERROR {part}" ❹
raise Exception(error)
return part
❶ 检查层类型和部分是否具有适当的长度。
❷ 对滤波器/神经元应用随机增加/减少。
❸ 随机更改核大小。
❹ 如果格式损坏,则抛出错误。
图 7.13 显示了在个体基因序列上运行变异函数/算子的结果。注意定义层组数量、神经元/滤波器或核大小的超参数是唯一被修改的东西。当你运行笔记本时,你可能会看到不同的结果,但你应该仍然观察到图 7.13 中突出显示的变化。

图 7.13 应用变异算子的示例
再次,笔记本的其余部分构建、编译和训练 突变基因 序列,以确认我们仍然可以生成有效的 Keras 模型。请运行几次 突变 代码块以确认输出的 基因 序列是有效的。通过构建用于处理 交叉 和 突变 操作的自定义算子,我们现在可以继续在下一节应用进化。
使用 Keras 的优势
Keras 模型编译稳健且宽容,这在可能随机构建的一些模型存在问题且无法产生良好结果时很有用。相比之下,像 PyTorch 这样的框架宽容度要小得多,可能会对几个构建问题进行抱怨,产生阻塞错误。使用 Keras,我们可以通过最小的错误处理来避免问题,因为大多数模型都会运行;然而,它们可能运行不佳。如果我们将这种相同的进化应用于 PyTorch,我们可能会遇到更多由于次要问题而产生的构建问题,从而产生较少的幸存后代。相反,Keras 会产生更多可行的后代,这些后代可以发展成为更合适的解决方案。这并不一定意味着 PyTorch 作为深度学习框架缺乏功能;相反,它更多地指向了这两个框架的僵化性。
7.5 进化卷积网络架构
进化卷积网络架构现在只是添加 DEAP 以使用遗传算法的问题。本节中我们涵盖的大部分内容是前几章的复习,但它应该有助于理解自定义算子是如何工作的。在本节中,我们继续使用之前的笔记本,并扩展它们以执行进化架构搜索。
在 Colab 中打开 EDL_7_5_Evo_CNN.ipynb 笔记本。请运行所有单元格(运行 > 运行所有)。注意,在这个笔记本的顶部,我们使用 pip 安装 DEAP 并导入我们在前几章中使用的标准模块。
向下滚动到名为进化 CNN 的部分,检查 DEAP toolbox 设置代码,如下所示。注意我们如何重用列表 7.8 中的 create_offspring 函数并将其注册到 toolbox 中,使用名称 network。这个函数负责创建新的第一代后代。然后,使用列表来保存 individual 基因 序列。在这里使用列表的好处是,一组 individuals 的长度可以不同。
列表 7.19 EDL_7_5_Evo_CNN.ipynb:DEAP toolbox 设置
toolbox = base.Toolbox()
toolbox.register("network", create_offspring) ❶
toolbox.register("individual", tools.initIterate,
➥ creator.Individual, toolbox.network) ❷
toolbox.register("population", tools.initRepeat,
➥ list, toolbox.individual) ❸
toolbox.register("select", tools.selTournament,
➥ tournsize=5) ❹
❶ 添加名为 network 的自定义 create_offspring 函数。
❷ 注册新的网络初始化函数。
❸ 使用列表来包含种群中的个体。
❹ 使用标准的锦标赛选择算子。
向下滚动一点,查看如何注册我们之前创建的自定义 交叉(列表 7.12)和 突变(列表 7.16)函数,如下所示。
列表 7.20 EDL_7_5_Evo_CNN.ipynb:注册自定义函数
toolbox.register("mate", crossover) ❶
toolbox.register("mutate", mutation) ❷
❶ 注册自定义配对函数。
❷ 注册自定义突变函数。
下一个单元格,如列表 7.21 所示,包含构建、编译、训练和评估模型的代码。我们首先查看 evaluate 函数。该函数首先使用 build_model 函数(列表 7.11)构建模型,然后使用新的函数 compile_train 编译和训练模型。之后,它将 1/准确度 钳位到几乎 0 到 1 之间。我们这样做是因为我们想要通过 1/准确度 最小化 适应性。请注意,我们将代码包裹在 try/except 中,以确保如果发生任何错误,我们可以优雅地恢复。我们的代码仍然有可能构建无意义的模型,这是防止失败的一种方法。如果代码确实失败,我们返回 1/.5 或 50% 的准确度——而不是 0 或接近 0。通过这样做,我们允许这些失败保留在 种群 中,并希望它们以后能够 变异 成更好的东西。
列表 7.21 EDL_7_5_Evo_CNN.ipynb:evaluate 函数
def evaluate(individual):
try:
model = build_model(individual) ❶
model = compile_train(model) ❷
print('.', end='')
return 1/clamp(model.evaluate(x_test,
➥ y_test, verbose=0)[1], .00001, 1), ❸
except:
return 1/.5, ❹
toolbox.register("evaluate", evaluate) ❺
❶ 构建模型。
❷ 编译和训练模型。
❸ 返回 1/准确度钳位。
❹ 如果出现失败,返回基本准确度。
❺ 注册该函数。
适者生存
通过允许失败的 个体 一些基本的 适应性,我们鼓励这些 基因 序列可能仍然保留在 种群 池中。在自然界中,具有严重 变异 的 个体 几乎肯定会迅速失败。像人类这样的合作物种,在照顾有潜力的较弱 个体 方面做得更好。这,无疑是人类婴儿可以如此虚弱和脆弱地出生,但仍然能够成长和生存,成为贡献者的原因。
compile_train 函数与我们早期的训练代码非常相似,但以下列表中快速看一下是值得的。这里没有太多不同,但请注意,我们为了简洁起见将训练固定在 3 个周期。再次提醒,你可能想要改变这一点,看看它对结果有什么影响。
列表 7.22 EDL_7_5_Evo_CNN.ipynb:compile 和 train 函数
def compile_train(model):
model.compile(optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['accuracy']) ❶
model.fit(x_train, y_train, epochs=3, ❷
verbose=0)
return model
❶ 为准确度进行训练。
❷ 在 3 个周期内拟合模型。
滚动到我们在前几章中审查的进化设置代码,并查看在 5 个 代 中进化 种群 的输出,如图 7.14 所示。由于我们的 基因 序列相对较小,我们通常可以期望快速收敛。你的结果可能会有所不同,但在大多数情况下,你的准确度应该在大约 0.81,或 81% 左右达到最大值。尝试增加 种群 的大小或 代 的数量,看看这会产生什么影响。

图 7.14 在 5 个 代 中进化 种群 的结果
进化完成后,我们构建、编译和训练最佳 个体,以查看图 7.15 中的结果。我们仍然在 3 个周期后看到发散,这表明如果我们想要一个更持久的模型,我们可能需要增加进化中的训练周期。这很容易实现,但它会显著增加进化时间。

图 7.15 评估进化后最佳个体的结果
最后,我们可以查看图 7.16 中进化模型架构的摘要。您的结果可能会有所不同,但您应该会看到与图中显示的类似层结构。实际上,如果您之前已经使用过 Fashion-MNIST 数据集,这可能是您已经见过的架构。

图 7.16 从进化产生的模型摘要结果
当然,您可以根据需要修改这个笔记本,并添加我们在本章中讨论的几个自定义功能。以下是对您可能想要对此笔记本进行的修改的总结:
-
数据集大小——我们将原始数据集的大小大幅减少以减少运行时间。预期如果您增加数据集大小,您也会看到模拟运行时间的增加。
-
训练轮数——在我们早期的评估中,我们决定使用 3 轮作为我们的训练限制。根据您的数据,您可能想要增加或减少这个值。
-
层类型——在这个简单的演示中,我们坚持使用标准层类型,如卷积、池化、批量归一化和密集层。您可能想要添加不同的层类型,如 dropout,以及/或者增加密集层或其他变体的数量。
-
交叉/变异——我们为配对和
变异构建的自定义算子只是其中一种实现方式。正如之前提到的,在构建变异函数时,有很多空间进行进一步定制,也许可以通过让变异添加或删除层块来实现。 -
适应度/评估函数——我们基于直接的准确率分数来评估
个体的适应度。如果我们想最小化可训练参数或层的数量,我们可以在evaluate函数中添加相应的逻辑。
7.5.1 学习练习
使用以下练习来提高您对 EvoCNN 的理解:
-
修改数据集的大小或类型。探索不同的数据集,注意进化后的 CNN 模型之间的差异。
-
在
gene序列中添加一个新的Dropout层类型。这可能需要一些工作,但可能为增强 CNN 模型构建提供一个基础。 -
考虑如何将其他形式的进化应用于从超参数优化到神经进化权重/参数。
随着自动机器学习模型进化优化概念的发展,我们有望期待框架为我们打包所有这些功能。然而,执行这种强大优化的代码量并不难产生,正如您在本章中看到的。最终,即使出现一个全面的框架,您可能也需要自定义像mate和crossover这样的函数。
概述
-
卷积神经网络是深度学习模型的层扩展,提供了局部特征提取:
-
通常用于 2D 图像处理,CNN 在增强分类或其他任务方面可以非常成功。
-
由于超参数、配置和放置的数量,CNN 层对于各种图像识别任务来说设置和定义都很复杂。
-
-
神经进化是描述深度学习优化进化方法的另一个术语,特别是那些与架构和参数优化相关的:
-
可以使用遗传算法和 DEAP 来优化深度学习网络的 CNN 架构。
-
CNN 层的复杂架构包括可以编码在定制遗传序列中的层的类型、大小和位置。
-
这种遗传编码考虑了各种 CNN 层的数量、核大小、步长、归一化和激活函数。
-
-
需要开发定制的
交叉(配对)和变异遗传算子来支持定制的遗传编码结构。 -
使用遗传算法进化一个
种群的个体,以优化特定数据集上的 CNN 模型架构。 -
EvoCNN 定制的编码架构对模型中使用的层数有限制。然而,使用神经进化可以快速协助完成定义复杂 CNN 架构的复杂任务。
第三部分. 高级应用
当我们进入书籍的最后一部分时,我们的关注点转向了生成建模、增强拓扑网络的神经进化、强化学习和本能学习领域中的更复杂示例。在我们添加这些进化方法之前,我们介绍了每个这些高级主题。
第八章和第九章介绍了并探讨了生成建模或生成深度学习领域。第八章展示了基本的自动编码器以及它如何被增强为进化自动编码器。然后,在第九章中,我们介绍了生成对抗网络的基本概念。由于生成对抗网络因其训练难度而闻名,我们展示了进化方法如何更好地优化训练。
增强拓扑的神经进化在第十章和第十一章中展示。我们首先在第十章中介绍 NEAT 的基本概念,以及如何配置这个强大的算法来增强物种形成。然后,在第十一章中,我们将 NEAT 应用于解决 OpenAI Gym 中发现的深度强化学习问题。
第十二章结束了部分和书籍的内容,其中我们探讨了机器学习中进化方法的未来以及介绍了本能学习。本能学习是一个更广泛的概念,涉及对可重用功能组件或本能的搜索。在本章中,我们介绍了一些将本能学习应用于解决 OpenAI Gym 中发现的强化学习问题的例子,使用基因表达编程和遗传算法。这些问题的结果展示了在深度学习系统中如何隔离常见的可重用函数/组件。
8 进化自编码器
本章涵盖
-
介绍卷积自编码器
-
讨论卷积自编码器网络中的遗传编码
-
应用变异和交配来开发进化自编码器
-
构建和进化自编码器架构
-
介绍卷积变分自编码器
在上一章中,我们介绍了如何使用进化算法来调整卷积神经网络(CNN)架构。我们使用遗传算法来编码定义 CNN 模型的基因序列,用于图像分类。结果是成功构建了更优化的网络,用于图像识别任务。
在本章中,我们继续扩展基础知识并探索进化的自编码器(AE)。我们从上一章构建进化的 CNN 架构的经验中汲取了一些经验,并将其应用于卷积自编码器。然后,我们转向更高级的变分自编码器,并探索进化模型损失的新方法。
自编码器是深度学习的基础,它引入了无监督和代表性学习。很可能,如果你花了一些时间研究深度学习,你一定遇到过自编码器和变分自编码器。从进化深度学习的角度来看,它们引入了一些我们在本章中探索的新应用。
自编码器有多种变体,从不完全或标准到深度和卷积。深度卷积自编码器是一个很好的起点,因为它扩展了前几章中的许多想法,这也是我们本章的起点。
8.1 卷积自编码器
在本节中,我们探索并回顾了一个用 Keras 编写的卷积自编码器。这是我们在本章后面构建进化型或 evo 自编码器时使用的相同代码。对于新接触自编码器的人来说,下一节回顾了训练、构建和重新训练的主要原则。
8.1.1 介绍自编码器
自编码器通常用于介绍无监督和代表性学习的概念。无监督学习是使用无标签训练模型的过程。代表性学习是指我们训练模型来理解输入特征之间的差异。
图 8.1 显示了一个简单的由卷积、MaxPool和UpSampling层组成的卷积自编码器。除了添加卷积之外,这种模型架构对于自编码器来说是标准的。

图 8.1 卷积自编码器
自编码器通过将输入通过称为潜在或特征表示视图的狭窄通道——中间部分——来工作。这个中间部分也被称为图像的潜在或隐藏编码。
通过将图像迭代地输入编码器并测量输出差异来学习图像的潜在编码。通常,我们使用均方误差或输入和输出图像的像素级损失来测量这个差异或损失。通过迭代,中间部分学会封装输入图像的特征。
图 8.2 展示了在 MNIST 手写数字数据集上训练的 AE 学习到的编码的示例。在图中,编码/潜在空间通过 t-分布随机邻域嵌入 (t-SNE) 转换为二维。通过可视化这个图表,你可以清楚地看到模型是如何学习区分不同数字类别的。

图 8.2 展示了 AE 潜在空间的一个映射,显示了类别的聚类
AE 使用无监督学习进行训练,这意味着模型中输入的数据无需标记。本质上,模型通过比较输入和生成的输出如何表示编码特征的好坏来通过自我训练进行学习。这简化了模型的训练,同时,也创建了一个强大的特征编码提取器。
表示学习,或可能也被称为 生成式深度学习,是一个相对较新的领域。我们将在下一章中详细探讨 GDL,但现在,让我们回到代码中,看看 AE 是如何工作的。
8.1.2 构建卷积自编码器
我们在下一个笔记本中查看的 AE 集成了卷积层,以更好地从图像中提取特征。将卷积应用于 AE 模型会在网络架构中引入额外的复杂性。在未来的章节中,这个例子还将展示如何应用进化来优化这些网络的优势。
在 Colab 中打开 EDL_8_1_Autoencoders.ipynb 笔记本。如需回顾在 Colab 中打开笔记本的方法,请参阅附录。
滚动页面,然后选择“阶段 1:AE 细胞”。从菜单中选择“运行前”。这将运行笔记本,加载数据并显示如图 8.3 所示的示例图表。过去几章中已经涵盖了几个代码部分,这里将不再进行回顾。

图 8.3 Fashion-MNIST 标准训练数据集
接下来,我们将来到第一个感兴趣的代码块,如图表 8.1 所示:构建 AE。第一层设置是输入层,由图像形状(28×28 和 1 个通道)定义。接下来,添加了一个使用 3×3 核大小的 64 个滤波器的卷积层。然后,在每个 CNN 层之后,一个 MaxPool 层将输入减少/聚合到下一层。最后添加的层是一个 MaxPool 层,它代表了输入的潜在或隐藏视图。
列表 8.1 EDL_8_1_AE.ipynb:编码器
input_layer = Input(shape=(28, 28, 1)) ❶
encoded_layer1 = layers.Conv2D(64, (3, 3),
➥ activation=’relu’, padding=’same’)(input_layer) ❷
encoded_layer1 = layers.MaxPool2D( (2, 2),
➥ padding=’same’)(encoded_layer1)
encoded_layer2 = layers.Conv2D(32, (3, 3),
➥ activation=’relu’, padding=’same’)(encoded_layer1) ❷
encoded_layer2 = layers.MaxPool2D( (2, 2),
➥ padding='same')(encoded_layer2)
encoded_layer3 = layers.Conv2D(16, (3, 3),
➥ activation='relu', padding='same')(encoded_layer2) ❷
latent_view = layers.MaxPool2D( (2, 2),
➥ padding='same')(encoded_layer3) ❸
❶ 定义输入层
❷ 2D 卷积层
❸ 最大池化层
现在我们已经构建了编码器模型以输出潜在或编码视图,我们需要使用进一步的卷积层和称为UpSampling的特殊层来重建图像。UpSampling层可以被认为是pooling层的相反。它们的效果是将编码器生成的潜在视图转换回完整图像。这是通过连续卷积输入和UpSampling到连续层来完成的。在这个输出链的末尾,我们添加一个最终的 CNN 层,将卷积输出转换为单个通道。如果我们使用彩色图像,我们将输出转换为三个通道,如下面的列表所示。
列表 8.2 EDL_8_1_AE.ipynb:解码器
decoded_layer1 = layers.Conv2D(16, (3, 3), activation='relu',
➥ padding='same')(latent_view) ❶
decoded_layer1 = layers.UpSampling2D((2, 2))
➥ (decoded_layer1) ❷
decoded_layer2 = layers.Conv2D(32, (3, 3),
➥ activation='relu', padding='same')(decoded_layer1) ❶
decoded_layer2 = layers.UpSampling2D((2, 2))
➥ (decoded_layer2) ❷
decoded_layer3 = layers.Conv2D(64, (3, 3), activation='relu')(decoded_layer2) ❶
decoded_layer3 = layers.UpSampling2D((2, 2))
➥ (decoded_layer3) ❷
output_layer = layers.Conv2D(1, (3, 3),
➥ padding='same')(decoded_layer3) ❸
❶ 2D 卷积层
❷ 2D 上采样层
❸ 输出到 1 个通道的最终 CNN 层
我们通过将相应的输入和输出层输入到 Keras 模型中来组合模型。然后,我们使用 Adam 优化器和 MSE 损失函数来编译模型。之后,我们绘制模型摘要并使用plot_model输出完成的模型的美观视觉,如下面的列表所示。
列表 8.3 EDL_8_1_AE.ipynb:构建模型
model = Model(input_layer, output_layer) ❶
model.compile(optimizer='adam', loss='mse') ❷
model.summary() ❸
plot_model(model) ❹
❶ 从输入和输出层构建
❷ 使用 Adam 和 MSE 编译
❸ 输出模型摘要
❹ 生成模型的图表
运行构建编码器和解码器以及构建模型的单元格。图 8.4 显示了构建模型的摘要输出。通过查看每个连续层,你可以可视化模型如何在潜在编码中缩小输入空间,然后重建它。重要的是要注意各个 CNN 层的大小以及它们如何减少然后增加大小。

图 8.4 自动编码器模型摘要解释
接下来的几个单元格设置了训练模型的输出代码。请运行这些单元格,包括训练代码。由于之前已经审查过这段代码,这里我们不再重复,除了查看图 8.5 所示的 10 个训练周期后的示例输出。

图 8.5 训练自动编码器示例单元格输出
随着模型的训练,输出,如图 8.5 所示,从模糊表示变为清晰特征。自动编码器可能需要大量的训练,这个简单的例子可能永远无法准确描绘更细粒度的特征。然而,它确实有效地区分了各种类别。一个衡量模型训练效果的良好指标是将凉鞋类图像与原始图像或运动鞋进行比较。
8.1.3 学习练习
使用以下练习来提高你对自动编码器(AE)的理解:
-
尝试使用不同的数据集,例如 MNIST 手写数字数据集。
-
修改模型超参数,如学习率和批量大小,以查看这对训练有何影响。
-
从编码器和解码器中添加或删除卷积层。确保保持自动编码器的两边平衡。
虽然这个简单的自动编码器工作得相当不错,但我们希望提高模型泛化学习表示的能力。在下一节中,我们将添加泛化特征,如 dropout 和批归一化层。
8.1.4 泛化卷积自动编码器
在第七章中,我们深入探讨了卷积层如何通过提取特征来工作。我们还了解到,CNN 模型在识别特征方面可能做得太好。为了补偿这一点,我们通常会添加一个名为Dropout的层,这有助于泛化特征提取。
图 8.6 展示了 dropout 层如何通过在每个训练迭代中随机禁用网络节点来工作,而不是在每个 epoch 中。通过每个训练迭代禁用随机神经元导致模型更好地泛化并减少记忆化。这导致训练损失和验证损失保持一致。

图 8.6 Dropout的演示
消失和爆炸梯度是训练 CNN 时,特别是在具有多层网络的 CNN 中起作用的另一个因素。这是因为网络中的权重/参数可能需要变得非常大或非常小,因为输入通过多个层。为了补偿这一点,我们在层之间引入一个称为BatchNormalization的正则化步骤。
图 8.7 展示了如何在卷积特征图上计算BatchNormalization。在图中,为每个特征图计算均值和方差,并使用这些值来正则化特征图的值,作为下一层的输入。这导致数据保持在约 0 的位置,也显著减少了消失或爆炸梯度的问题。

图 8.7 BatchNormalization过程
正则化使用以下方程进行。每个输入值都从平均值中减去,然后除以方差的平方根,即标准差σ。

现在我们已经了解了如何创建更通用的模型并避免爆炸和消失梯度,我们可以继续将这些特性集成到自动编码器中。
8.1.5 改进自动编码器
通过添加BatchNormalization和Dropout层,我们可以改进之前查看的简单自动编码器。我们继续使用相同的笔记本,但现在在以下说明中查看添加这些新层类型。
在 Colab 中重新打开 EDL_8_1_Autoencoder.ipynb 笔记本。如有需要,请参考附录。通过菜单中的运行 > 运行所有来运行笔记本中的所有单元格。向下滚动到以改进自动编码器开始的章节。
我们首先查看列表 8.4 中模型的更新编码器部分。大部分代码与上次我们查看的代码相同,但请注意包括了批量归一化和 dropout 层。传递给 dropout 层的参数是每个训练迭代中将被禁用的神经元数量或百分比。
列表 8.4 EDL_8_1_AE.ipynb:改进的编码器
inputs = layers.Input(shape=(28, 28 ,1))
x = layers.Conv2D(32, 3, activation='relu', padding='same')(inputs)
x = layers.BatchNormalization()(x) ❶
x = layers.MaxPool2D()(x)
x = layers.Dropout(0.5)(x) ❷
x = layers.BatchNormalization()(x) ❶
x = layers.MaxPool2D()(x)
x = layers.Dropout(0.5)(x) ❷
x = layers.Conv2D(64, 3, activation='relu', padding='same')(x)
x = layers.BatchNormalization()(x) ❶
encoded = layers.MaxPool2D()(x)
❶ BatchNormalization 层
❷ Dropout 层
在此之后,我们当然会查看改进的解码器部分,如下所示。同样,唯一的区别是在解码器部分包括了BatchNormalization和Dropout层。
列表 8.5 EDL_8_1_AE.ipynb:改进的解码器
x = layers.Conv2DTranspose(64, 3,activation='relu',strides=(2,2))(encoded)
x = layers.BatchNormalization()(x) ❶
x = layers.Dropout(0.5)(x) ❷
x = layers.Conv2DTranspose(32, 3, activation='relu',strides=(2,2),
➥ padding='same')(x)
x = layers.BatchNormalization()(x) ❶
x = layers.Dropout(0.5)(x) ❷
x = layers.Conv2DTranspose(32, 3, padding='same')(x)
x = layers.LeakyReLU()(x)
x = layers.BatchNormalization()(x) ❶
decoded = layers.Conv2DTranspose(1, 3, activation='sigmoid',strides=(2,2),
➥ padding='same')(x)
❶ BatchNormalization 层
❷ Dropout 层
图 8.8 显示了在 10 个 epoch 上训练这个“改进”模型的输出。如果你将这个图与图 8.5 进行比较,你可以清楚地看到这些“改进”并不像原始模型那样有效。

图 8.8 训练改进的 AE 示例单元格输出
因此,如果这些层类型是关于提高模型性能的,为什么我们会得到如此糟糕的结果呢?在这个情况下,答案是简单的:我们过度使用了BatchNormalization和Dropout的功能。这通常意味着我们需要手动调整网络架构以提高模型性能。相反,我们来看看如何使用 EC 优化 AE 模型开发。
8.2 进化 AE 优化
我们已经看到如何使用名为 EvoCNN 的 GAs 自动优化 CNN 模型。在接下来的教程中,我们采取与之前相同的方法,但引入了 AE 的附加复杂性。这意味着我们的模型架构需要遵循更严格的指南。
8.2.1 构建 AE 基因序列
构建 GA AE 优化器的第一步是构建一个模式,将架构编码成一个基因序列。我们基于之前的例子,但这次引入了 AE 的约束。同样,这个模型也通过允许添加BatchNormalization和Dropout来改进了 EvoCNN 项目。
在 Colab 中打开 EDL_8_2_Evo_Autoencoder_Encoding.ipynb 笔记本。如需说明,请参阅附录。通过运行 > 运行所有单元格来运行笔记本中的所有单元格。滚动到标题为“编码自动编码器”的部分。我们在第七章中回顾了以下大部分代码,所以这里我们只回顾要点。
首先查看create_offspring函数。如果您还记得,这是创建整个基因序列的主要函数,但这次版本有所不同。这次,函数被拆分为两个循环:一个用于编码部分,另一个用于解码部分。编码部分遍历层并随机检查是否应该添加另一个卷积层。如果添加了层,然后它将继续随机检查是否也应该添加 BN 和/或 dropout 层。注意在这个例子中,我们自动添加一个MaxPool层来考虑自动编码机的漏斗或减少架构。
解码器的第二个循环设置为与编码器架构相匹配。因此,它遍历与编码器相同的迭代次数。这次,它添加了卷积层代码来表示UpSampling和卷积层的组合。之后,应用一个机会检查来添加BatchNormalization和/或Dropout层,如下面的列表所示。
列表 8.6 EDL_8_2_Evo_AE_Encoding.ipynb:创建基因序列
def create_offspring():
ind = []
layers = 0
for i in range(max_layers):
if i==0: ❶
ind.extend(generate_conv_layer())
layers += 1
elif random.uniform(0,1)<.5: ❷
ind.extend(generate_conv_layer())
layers += 1
if random.uniform(0,1)<.5: ❸
ind.extend(generate_bn_layer())
if random.uniform(0,1) < .5: ❹
ind.extend(generate_dropout_layer())
for i in range(layers): ❺
ind.extend(generate_upconv_layer())
if random.uniform(0,1)<.5:
ind.extend(generate_bn_layer())
if random.uniform(0,1) < .5:
ind.extend(generate_dropout_layer())
return ind
❶ 第一层总是卷积。
❷ 有机会添加另一个卷积层
❸ 有机会添加一个批归一化层
❹ 有机会添加一个 Dropout 层
❺ 遍历编码器层以创建解码器。
注意,我们改变了基因序列的编码模式,以考虑卷积/MaxPool层和UpSampling/卷积层。您可以在代码单元格中设置的标记中看到这个小的变化。现在,表示编码器卷积层的编码标记被定义为CONV_LAYER,解码器UpSampling或卷积层被定义为UPCONV_LAYER,如下面的列表所示。
列表 8.7 EDL_8_2_Evo_AE_Encoding.ipynb:基因序列标记
CONV_LAYER = -1 ❶
CONV_LAYER_LEN = 4
BN_LAYER = -3
BN_LAYER_LEN = 1
DROPOUT_LAYER = -4
DROPOUT_LAYER_LEN = 2
UPCONV_LAYER = -2 ❷
UPCONV_LAYER_LEN = 4
❶ 编码器卷积/池化层
❷ 解码器 UpSampling/卷积层
同样,生成编码器层(CONV_LAYER)和解码器层(UPCONV_LAYER)的函数也简化了,如下面的列表所示。
列表 8.8 EDL_8_2_Evo_AE_Encoding.ipynb:生成层
def generate_conv_layer(): ❶
part = [CONV_LAYER]
part.append(generate_neurons())
part.extend(generate_kernel())
return part
def generate_upconv_layer(): ❷
part = [UPCONV_LAYER]
part.append(generate_neurons())
part.extend(generate_kernel())
return part
❶ 编码器卷积/池化层
❷ 解码器 UpSampling/卷积
同样,添加 BN 和 dropout 层的函数也简化了,如下面的列表所示。
列表 8.9 EDL_8_2_Evo_AE_Encoding.ipynb:生成特殊层
def generate_bn_layer(): ❶
part = [BN_LAYER]
return part
def generate_dropout_layer(): ❷
part = [DROPOUT_LAYER]
part.append(random.uniform(0,.5))
return part
❶ 生成 BN 层
❷ 生成 Dropout 层
接下来,我们通过解析基因序列来构建模型。这段代码相当长,所以我们将其拆分为相关部分,从列表 8.10 中的初始解析开始。我们首先遍历每个基因并检查它是否匹配层标记。如果匹配,我们将相应的层和选项添加到模型中。在编码器卷积层(CONV_LAYER)的情况下,如果输入形状大于(7, 7),我们将添加一个MaxPool层。这确保我们的模型保持固定的潜在视图。
列表 8.10 EDL_8_2_Evo_AE_Encoding.ipynb:构建模型——解析
def build_model(individual):
input_layer = Input(shape=(28, 28, 1)) ❶
il = len(individual)
i = 0
x = input_layer
while i < il: ❷
if individual[i] == CONV_LAYER: ❸
pad="same"
n = individual[i+1]
k = (individual[i+2], individual[i+3])
i += CONV_LAYER_LEN
x = layers.Conv2D(n, k, activation='relu', padding=pad)(x)
if x.shape[1] > 7: ❹
x = layers.MaxPool2D( (2, 2), padding='same')(x)
❶ 输入层始终相同。
❷ 遍历基因
❸ 编码卷积层
❹ 如果形状大于 7, 7,则添加池化。
向下移动一点,我们可以通过检查标记来看到添加层的延续,如下面的列表所示。不过,这次对于 UPCONV_LAYER 解码器层,我们检查模型是否与输入大小相同。毕竟,我们不想生成的图像太大或太小。
列表 8.11 EDL_8_2_Evo_AE_Encoding.ipynb:构建模型——层
elif individual[i] == BN_LAYER: ❶
x = layers.BatchNormalization()(x)
i += BN_LAYER_LEN
elif individual[i] == DROPOUT_LAYER: ❷
x = layers.Dropout(individual[i+1])(x)
i += DROPOUT_LAYER_LEN
elif individual[i] == UPCONV_LAYER: ❸
pad="same"
n = individual[i+1]
k = (individual[i+2], individual[i+3])
x = layers.Conv2D(n, k, activation='relu', padding=pad)(x)
x = layers.UpSampling2D((2, 2))(x)
i += CONV_LAYER_LEN
if x.shape[1] == (28): ❹
break #model is complete
else:
break
❶ 添加 BN 层
❷ 添加 Dropout 层
❸ 添加解码器 UpSampling/卷积层
❹ 检查模型是否完整
函数通过构建模型、编译它并返回它来完成。但在做这些之前,我们通过检查最后一个解码器层的形状来确认模型不是太小,如下面的列表所示。如果输出太小,我们添加另一个 UpSampling 层,将大小从 14, 14 增加到 28, 28。
列表 8.12 EDL_8_2_Evo_AE_Encoding.ipynb:构建模型——编译
if x.shape[1] == 14: ❶
x = layers.UpSampling2D((2, 2))(x)
output_layer = layers.Conv2D(1, (3, 3),
➥ padding='same')(x) ❷
model = Model(input_layer, output_layer) ❸
model.compile(optimizer='adam', loss='mse')
return model
❶ 确保最终模型不是太小
❷ 转换回单通道
❸ 合并输入/输出层
为了测试 build_model 函数,下一个代码块,如列表 8.13 所示,创建了 100 个随机后代并评估了模型的大小。此代码生成随机的 individual gene 序列,然后从这些序列构建相应的模型。在这个过程中,代码跟踪生成的最小和最大模型。
列表 8.13 EDL_8_2_Evo_AE_Encoding.ipynb:评估 build_model
max_model = None
min_model = None
maxp = 0
minp = 10000000
for i in range(100):
individual = create_offspring() ❶
model = build_model(individual) ❷
p = model.count_params() ❸
if p > maxp:
maxp = p
max_model = model
if p < minp:
minp = p
min_model = model
max_model.summary()
min_model.summary()
❶ 创建一个随机基因序列
❷ 从序列构建模型
❸ 计算模型参数
向下滚动进一步可以看到输出,如图 8.9 所总结。在图中,使用最小尺寸参数模型在 10 个周期内训练模型。

图 8.9 从随机生成的后代构建的最小尺寸模型的输出
这个使用 create_offspring 和 build_model 随机生成的模型似乎比我们上一个“改进”的 AE 更好,这是有希望的,因为它也是一个近似的最小尺寸模型。务必检查代码并测试最大尺寸模型的训练。记住,在这个例子中,样本大小只使用了 100 种变化。
8.2.2 学习练习
使用以下练习来提高你的理解:
-
通过在循环中调用
create_offspring创建individuals列表,然后打印并比较各种模型。 -
将列表 8.6 中的基本概率从 0.5 改为另一个值。通过练习 1 看看这会对生成的模型产生什么影响。
现在我们有了一种创建基因序列的方法,反过来,可以使用它来构建 AE 模型。正如我们在第六章所学,我们的下一步是构建自定义的配对和变异这些基因序列的函数。这就是我们在下一节继续这个项目的地方。
8.3 自动编码器基因序列的配对和变异
正如我们在第七章的 EvoCNN 项目中做的那样,我们还需要创建自定义的变异和配对/交叉算子。这些自定义算子与我们之前使用的非常相似,所以我们再次只回顾一些亮点。在添加遗传算子后,我们测试了 EvoAE。
在 Colab 中打开 EDL_8_3_EvoAutoencoder.ipynb 笔记本。如需帮助,请参阅附录。向下滚动到创建配对/变异算子部分,然后选择下一个代码单元格。从菜单中选择运行 > 运行之前,以执行笔记本中的所有前一个单元格。请耐心等待,直到样本训练完成。
图 8.10 展示了交叉过程——这次使用的是代表 AE 架构的修改后的基因序列。请注意,编码器卷积层和解码器卷积层的数量始终相等。这是实现 AE 的漏斗效应所必需的。

图 8.10 进化 AE 交叉
幸运的是,第七章中编写的大多数交叉/配对代码都可以在我们的更新后的基因序列上运行,我们不需要在这里重新访问它。DEAP 在进化过程中调用交叉函数/算子,在进化过程中传入两个父个体。在这个函数内部,我们之前介绍过的swap_layers函数是工作的核心。如下面的列表所示,这个函数的唯一区别是我们想要支持的顶层结构的修改:卷积(编码器)、上卷积(解码器)、批归一化和Dropout。
列表 8.14 EDL_8_3_EvoAE.ipynb:执行交叉
def crossover(ind1, ind2):
ind1, ind2 = swap_layers(ind1, ind2, CONV_LAYER,
➥ CONV_LAYER_LEN) ❶
ind1, ind2 = swap_layers(ind1, ind2, UPCONV_LAYER,
➥ UPCONV_LAYER_LEN) ❷
ind1, ind2 = swap_layers(ind1, ind2, BN_LAYER,
➥ BN_LAYER_LEN) ❸
ind1, ind2 = swap_layers(ind1, ind2, DROPOUT_LAYER,
➥ DROPOUT_LAYER_LEN) ❹
return ind1, ind2
❶ 交换编码器卷积层
❷ 交换解码器上卷积层
❸ 交换 BN
❹ 交换 Dropout
执行变异需要对架构中各种层类型的修改更加注意。我们首先查看主要的变异函数,该函数由 DEAP 进化调用并接受单个个体。此函数大量使用mutate_layers函数并将其应用于仅可修改的层。请注意,我们省略了 BN 层,因为它们不需要额外的参数,如下面的列表所示。
列表 8.15 EDL_8_3_EvoAE.ipynb:变异函数
def mutation(ind):
ind = mutate_layers(ind, CONV_LAYER,
➥ CONV_LAYER_LEN) ❶
ind = mutate_layers(ind, DROPOUT_LAYER,
➥ DROPOUT_LAYER_LEN) ❷
ind = mutate_layers(ind, UPCONV_LAYER,
➥ UPCONV_LAYER_LEN) ❸
return ind,
❶ 变异编码器卷积层
❷ 变异 Dropout 层
❸ 变异解码器上卷积层
mutate_layers 函数突出了如何为 mutation 选择每一层。层按类型收集,并检查 mutation 的可能性。注意,目前,这种可能性总是 100%。如果选择了一层进行 mutation,则其序列将传递给 mutate 函数进行 mutation,如下所示。
列表 8.16 EDL_8_3_EvoAE.ipynb:mutate_layers 函数
def mutate_layers(ind, layer_type, layer_len):
layers = get_layers(ind1, layer_type) ❶
for layer in layers:
if random.random() < 1: ❷
try: ❸
ind[layer:layer+layer_len] = mutate( ❹
ind[layer:layer+layer_len], layer_type)
except: ❸
print(layers)
return ind
❶ 获取类型为的层
❷ 检查随机突变
❸ 捕获异常
❹ 突变层
mutate 函数执行相应层类型的特定 mutation,如下所示。每种层类型都应用了与层类型相关的不同形式的 mutation。如果将未知层类型传递给 mutate,则会抛出错误,这意味着 gene 序列可能已损坏或中断。这,正如自然界一样,导致了一个不可行的后代,该后代将终止进一步执行。
列表 8.17 EDL_8_3_EvoAE.ipynb:mutate 函数
def mutate(part, layer_type):
if layer_type == CONV_LAYER and
➥ len(part)==CONV_LAYER_LEN: ❶
part[1] = int(part[1] * random.uniform(.9, 1.1))
part[2] = random.randint(min_kernel, max_kernel)
part[3] = random.randint(min_kernel, max_kernel)
elif layer_type == UPCONV_LAYER and
➥ len(part)==UPCONV_LAYER_LEN: ❷
part[1] = random.randint(min_kernel, max_kernel)
part[2] = random.randint(min_kernel, max_kernel)
elif layer_type == DROPOUT_LAYER and
➥ len(part)==DROPOUT_LAYER_LEN: ❸
part[1] = random.uniform(0, .5)
else:
error = f"mutate ERROR {part}" ❹
raise Exception(error)
return part
❶ 突变编码器 CNN 层
❷ 突变解码器 CNN 层
❸ 突变 dropout 层
❹ 层代码不匹配,因此抛出错误。
在 mating/mutation 单元结束时,有代码用于通过创建新的后代并传递给 crossover 或 mutation 函数来测试相应的操作符。构建了 mating/mutation 操作符后,我们可以继续到下一节,进化 AE 架构。
8.4 生成自动编码器
现在,进化 AE 只是一个相对简单的过程,只需添加 DEAP。同样,这里的大部分代码与之前的示例相同。这意味着我们在这里只关注重点、变更和感兴趣的点。
在 Colab 中打开 EDL_8_4_EvoAutoencoder.ipynb 笔记本。通过菜单中的“运行”>“运行所有”来运行整个笔记本。这个笔记本运行可能需要很长时间,因此最好尽早开始。
自动编码器(AE)架构的进化可能需要相当长的时间。因此,我们使用之前介绍过的数据减少技巧,以使进化过程更加高效。参考数据加载单元,注意我们如何通过简单地从原始数据集中取切片来减少训练集和验证集的大小,如下所示。这只是为了演示代码的运行和操作方式。显然,如果目标是创建一个优化的模型,我们最好使用完整的数据集。
列表 8.18 EDL_8_4_EvoAE.ipynb:减少数据集大小
train_images = train_images[1000:] ❶
test_images = test_images[100:] ❷
❶ 减少训练大小
❷ 减少测试/验证大小
接下来,我们回顾所有基础 DEAP 设置代码,以创建用于执行架构优化的 GA 求解器,如列表 8.19 所示。我们将主要的 fitness 函数注册为 FunctionMin,因为我们的目标是最小化 fitness。然后,注册 create_offspring 函数以创建新的 individuals。最后,代码通过注册自定义的 crossover 和 mutation 函数来完成。
列表 8.19 EDL_8_4_EvoAE.ipynb:设置 DEAP
creator.create("FitnessMin", base.Fitness, weights=(-1.0,))
creator.create("Individual", list,
➥ fitness=creator.FitnessMin) ❶
toolbox = base.Toolbox()
toolbox.register("AE", create_offspring) ❷
toolbox.register("individual", tools.initIterate, creator.Individual,
➥ toolbox.AE)
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
toolbox.register("select", tools.selTournament, tournsize=5)
toolbox.register("mate", crossover) ❸
toolbox.register("mutate", mutation) ❹
❶ 注册目标函数和最小适应度
❷ 注册初始 AE 函数
❸ 注册自定义交叉
❹ 注册自定义变异
如列表 8.20 所示,下一个是evaluate函数。这是评估每个网络模型架构的地方。之前,我们注册了一个名为fitness的列表来保存所有评估的fitness。我们这样做是为了更好地跟踪最大观察到的fitness。在函数内部,我们首先调用build_model根据个体``基因序列创建模型。之后,我们调用train函数来训练模型并返回模型加history。从history函数中,我们提取最后一个验证历史值并将其视为模型的fitness。如果没有错误生成模型和训练,则返回fitness,夹在 0 和最大fitness之间。我们使用np.nanman函数来避免返回nan值。如果遇到错误,则返回最大观察到的fitness。
列表 8.20 EDL_8_4_EvoAE.ipynb:evaluate函数
fits = [] ❶
def evaluate(individual):
global fits
try:
model = build_model(individual) ❷
model, history = train(model) ❸
fitness = history.history["val_loss"]
fits.append(fitness)
print(".", end='')
return clamp(fitness, 0, np.nanmax(fits)), ❹
except:
return np.nanmax(fits), ❺
toolbox.register("evaluate", evaluate)
❶ 用于跟踪适应度的全局变量
❷ 从基因序列构建模型
❸ 训练模型
❹ 返回夹紧的适应度值
❺ 如果出现错误,返回最大观察到的适应度。
图 8.11 显示了使用初始种群100 个体运行 3 代进化架构的结果。从这些初始结果中,你可以看到这提供了一种自我优化模型架构的有趣方法。

图 8.11 进化 AE 架构的结果
很可能你的结果会有所不同,这个样本在更大的初始种群下运行得更好,这再次表明运行起来可能很耗时。将图 8.11 与图 8.5 和图 8.8 进行比较。结果是否如你所预期的好或坏?
训练 AE 和其他 RL 网络可能很耗时,我们通常需要超过 3 个 epoch,就像上一个笔记本中那样。这个笔记本中的进化输出展示了从进化架构中产生的可能性。
8.4.1 学习练习
通过完成以下练习继续探索进化的 AE:
-
在列表 8.18 中增加或减少训练样本的数量。
-
更改目标数据集。一个好的选择是 MNIST 手写数字数据集。
-
尝试调整学习率和批大小超参数,看看这对进化模型有什么影响。
为了总结本章,我们继续研究 AE,但有一些变化。不是直接将编码映射到解码,我们尝试在下一节中添加一个采样层来实现变分 AE。
8.5 构建变分自编码器
变分自动编码器(VAEs)是自动编码器(AEs)的扩展,通过理解采样损失中的学习表示差异来学习。在我们跳入下一章关于进化生成式深度学习之前,这是一个我们需要覆盖的重要概念。
对于下一个笔记本项目,我们将探讨构建一个变分自动编码器(VAE),以执行与之前笔记本相同的分析。深度学习(DL)的资深从业者可能对这种模式很熟悉,但为了以防万一,我们将在下一节进一步回顾。
8.5.1 变分自动编码器:回顾
在架构上,变分自动编码器(VAEs)几乎完全相同,除了中间编码层中的一个关键差异:在 VAE 中,中间层变成了一个采样层,该层学习表示编码输入并将这种学习到的表示转换回原始图像。通过学习输入的表示,VAE 能够根据这种理解生成新的输出。
图 8.12 展示了 VAE 与图 8.1 中所示的传统自动编码器架构的不同之处。我们可以看到,潜在编码向量被两个学习到的参数所取代:均值(µ)和方差(σ)。这些学习到的参数随后用于采样或生成一个新的潜在编码向量,称为Z,然后将其推入解码器。

图 8.12 变分自动编码器
与在自动编码器(AE)中学习压缩和提取相关特征不同,VAE 通过训练网络输出输入的均值和方差来学习输入的表示方式。然后基于这种学习到的表示,采样层生成一个新的潜在编码向量,称为Z,并将其反馈到解码器中。
由于 VAE 在已知空间中学习表示,我们可以通过遍历模型学习到的均值和方差的范围来生成该空间中的值。图 8.13 通过遍历均值和方差的范围来演示 VAE 的结果,从而输出模型正在学习的内容。现在我们已经对 VAE 有了概述,我们可以在下一节继续构建它。

图 8.13 展示了 VAE 学习到的 2D 流形样本输出
8.5.2 实现 VAE
VAE 简单来说就是一个将中间编码替换为采样机制的自动编码器。在结构上,VAE 和 AE 是相同的,但在这个笔记本中,我们介绍了实现这种架构的另一种模式。这不仅简化了架构,还为后续章节中的其他创新奠定了基础。
在 Colab 中打开 EDL_8_5_VAE.ipynb 笔记本。如需帮助,请参阅附录。从菜单中选择运行 > 运行所有单元格来运行笔记本中的所有单元格。
滚动到标记为“网络超参数”的单元格。我们首先详细查看笔记本的超参数,如列表 8.21 所示,因为引入了一些新的输入。代码首先使用前一个单元格中加载的数据集提取的image_size设置input_shape。然后,设置基础内核大小和过滤器数量;我们稍后会看到这些是如何被使用的。之后,将latent_dim设置为2,表示编码器输出的中间维度的数量。在这种情况下,潜在的维度2代表输入的均值和方差。
列表 8.21 EDL_8_5_VAE.ipynb:VAE 超参数
input_shape = (image_size, image_size, 1) ❶
batch_size = 128
kernel_size = 3 ❷
filters = 16 ❸
latent_dim = 2 ❹
epochs = 5
❶ 输入形状由 image_size 和通道定义。
❷ 卷积层的内核大小
❸ CNN 的基础过滤器数量
❹ 潜在的/中间维度的尺寸
下一个单元格显示了编码器卷积层的构建,与 AE 相比相当不同,如列表 8.22 所示。VAE 是通过循环添加连续的 CNN 层来构建的。在每次迭代中,下一层将过滤器数量加倍。需要注意的是,省略了池化层以创建 AE 的漏斗效应的减少。相反,我们将strides从1增加到2,这减少了输出维度的一半。因此,28×28 的图像将减少到 14×14。
列表 8.22 EDL_8_5_VAE.ipynb:构建编码器 CNN
inputs = Input(shape=input_shape, name='encoder_input') ❶
x = inputs
for i in range(2):
filters *= 2 ❷
x = Conv2D(filters=filters,
kernel_size=kernel_size,
activation='relu',
strides=2, ❸
padding='same')(x)
shape = K.int_shape(x) ❹
❶ 设置初始输入层
❷ 将 CNN 每一步的过滤器数量加倍
❸ 通过增加步长来替换池化
❹ 捕获最终形状
向下滚动到下一个单元格,我们可以看到编码器 CNN 层的输出如何减少到潜在维度并输出均值和方差,如列表 8.23 所示。为此,添加了一个Flatten层来压缩编码器的输出到一个Dense层。之后,添加了两个更多的Dense层,它们产生样本均值z_mean和方差z_log_var。这些值随后被传递到采样层z,这是一个使用 lambda 构建的自定义层,它接受sampling函数、期望的输出形状以及均值和方差作为输入。特别注意latent_space超参数是如何用来定义采样层的输入和输出形状的。
列表 8.23 EDL_8_5_VAE.ipynb:构建潜在采样
x = Flatten()(x) ❶
x = Dense(16, activation='relu')(x)
z_mean = Dense(latent_dim, name='z_mean')(x) ❷
z_log_var = Dense(latent_dim, name='z_log_var')(x) ❷
z = Lambda(
sampling,
output_shape=(latent_dim,),
name='z')([z_mean, z_log_var]) ❸
encoder = Model(inputs, [z_mean, z_log_var, z],
➥ name='encoder') ❹
encoder.summary()
❶ 将编码器的输出展平
❷ 将潜在维度减少到产生均值和方差
❸ 生成采样层
❹ 实例化编码器模型
图 8.14 显示了编码器模型的model.summary,旁边标注了层结构。注意模型如何展平编码器的卷积层,然后将输入推入大小为16的Dense层。这进一步分解成两个并行层——一个用于均值,另一个用于方差。这些值随后被传递到采样层z,这是一个使用 lambda 构建的自定义层,它接受sampling函数、期望的输出形状以及均值和方差作为输入。特别注意latent_space超参数是如何用来定义采样层的输入和输出形状的。

图 8.14 编码器模型的注释总结
在我们到达解码器之前,让我们回顾一下 sampling 函数,该函数在文件的顶部代码单元格中显示,并在列表 8.24 中列出,它接受均值和方差作为输入。在函数内部,均值和方差的值从 args 中解包。然后,我们从均值输入中提取两个形状值,首先使用 K.shape 返回张量形状,然后使用 K.int_shape 返回一个元组。简单来说,这设置了采样向量的输出大小。然后,我们创建一个大小为 (batch, dim) 的随机样本张量,称为 epsilon,它成为基础随机向量。之后,通过应用均值和方差来缩放向量,以确定最终的输出向量 z。
列表 8.24 EDL_8_5_VAE.ipynb:sampling 函数
def sampling(args):
z_mean, z_log_var = args
batch = K.shape(z_mean)[0] ❶
dim = K.int_shape(z_mean)[1] ❷
epsilon = K.random_normal(shape=(batch, dim)) ❸
return z_mean + K.exp(0.5 * z_log_var) * epsilon ❹
❶ 提取批处理参数的张量大小
❷ 提取维度元组
❸ 从正态分布中采样
❹ 返回偏移 epsilon 的采样向量
解码器模型的架构也已经简化,但我们仍然需要处理来自编码器的 z 采样层输出。这次,我们构建一个大小为 latent_dim 的 Input 层,以匹配编码器的最终采样输出 z,如列表 8.25 所示。接下来,一个新的 Dense 层被扩展以匹配最终解码器输出的大小,然后使用 Reshape 层重塑以匹配原始编码器输出。简单来说,新的中间采样函数只是用我们构建的 AE 的中间潜在编码交换。然而,我们仍然需要保持数据维度的一致性。
列表 8.25 EDL_8_5_VAE.ipynb:解包解码器输入
latent_inputs = Input(shape=(latent_dim,),
➥ name='z_sampling') ❶
x = Dense(shape[1] * shape[2] * shape[3],
➥ activation='relu')(latent_inputs) ❷
x = Reshape((shape[1], shape[2], shape[3]))(x) ❸
❶ 构建输入层
❷ 添加一个密集层来重建形状
❸ 将输出重塑以匹配解码器输入
之后,我们可以看到解码器的其余部分正在构建,如列表 8.26 所示。首先要注意的是,使用 Conv2DTranspose 层而不是之前使用的 Conv2D 和 UpSampling。简单来说,这种层类型是卷积过程的更明确的反转。同样,层是通过循环添加的,但这次在每个迭代之后,filters 的数量减少,剩余的 filters 在构建编码器后留下。之后,使用单个 Conv2DTranspose 层将输出减少到单个通道。
列表 8.26 EDL_8_5_VAE.ipynb:构建解码器
for i in range(2):
x = Conv2DTranspose(filters=filters, ❶
kernel_size=kernel_size,
activation='relu',
strides=2, ❷
padding='same')(x)
filters //= 2 ❸
outputs = Conv2DTranspose(filters=1, ❹
kernel_size=kernel_size,
activation='sigmoid',
padding='same',
name='decoder_output')(x)
decoder = Model(latent_inputs, outputs, name='decoder') ❺
decoder.summary()
❶ 使用 Conv2DTranspose 层
❷ 将步长值设置为 2 以进行扩展。
❸ 在每个层后减少过滤器数量
❹ 添加一个最终的转置层以实现单通道输出
❺ 实例化模型
图 8.15 显示了解码器模型摘要的注释视图。正如你所见,这部分比 AE 简单且更模块化。注意模型中的输入现在只是来自编码器sampling层的两个输入。然而,这使我们能够遍历或采样解码器学习生成的空间。

图 8.15 解码器模型的注释视图
VAE 与 AE 之间的另一个主要区别在于损失计算的方式。回想一下,在 AE 中,我们使用 MSE(均方误差)计算像素级的损失。这很简单且效果良好。然而,如果我们以这种方式为 VAE 计算损失,我们将错过学习输入分布的细微差别。相反,使用 VAE,我们通过一个发散度度量来衡量输入和输出之间的分布损失。
这需要我们在 Keras 模型中添加一个专门的损失确定。我们首先计算基础重建损失——通过比较输入图像与输出图像来计算的损失。之后,我们计算kl_loss,即 Kullback-Leibler 发散度,这是两个概率分布之间的统计距离。从输入和学习的表示中确定这种发散度的计算方法在下面的列表中展示。我们在第九章中更深入地讨论了类似这样的统计距离和损失计算。最后,使用add_loss函数将kl_loss和reconstruction_loss的均值添加为新的损失度量到模型中。
列表 8.27 EDL_8_5_VAE.ipynb:构建 VAE 模型
use_mse = False
if use_mse:
reconstruction_loss = mse(
K.flatten(inputs), K.flatten(outputs)) ❶
else:
reconstruction_loss = binary_crossentropy(
K.flatten(inputs), K.flatten(outputs)) ❶
reconstruction_loss *= image_size * image_size ❷
kl_loss = 1 + z_log_var - K.square(z_mean) –
➥ K.exp(z_log_var) ❸
kl_loss = K.sum(kl_loss, axis=-1) ❸
kl_loss *= -0.5 ❸
vae_loss = K.mean(tf.math.add(reconstruction_loss,
➥ kl_loss)) ❹
vae.add_loss(vae_loss) ❺
vae.compile(optimizer='adam')
vae.summary()
❶ 构建基础重建损失
❷ 通过图像大小扩展基础重建损失
❸ 计算 kl_loss
❹ 计算重建损失和 kl 损失的均值
❺ 将损失度量添加到模型中
向下滚动一点,检查图 8.16 所示的训练输出。在这个例子中,我们使用归一化损失度量,即损失除以最大观察到的损失,来跟踪模型的训练。通过跟踪归一化损失,我们可以切换到其他形式的重建和统计距离/发散度度量。尝试将重建损失的标志从 MSE 切换到二元交叉熵,以观察训练差异。这两种度量在不同的尺度上生成输出,但通过归一化损失,我们可以比较度量。
最后,我们可以观察到如图 8.13 所示的输出,它显示了通过循环样本均值和方差参数来生成样本图像。此代码创建了一个大小为 10×10 的大网格图像,它只是一个 NumPy 数组。然后,为均值和方差的多个值生成线性空间或值列表,如列表 8.28 所示。之后,代码遍历这些值,均值/方差,并将它们作为解码器模型的输入。解码器模型然后使用predict函数根据均值/方差值生成图像。然后,这个预测或生成的图像被绘制到网格中,如图 8.13 的结果所示。
列表 8.28 EDL_8_5_VAE.ipynb:生成流形图像
n = 10
digit_size = 28
figure = np.zeros((digit_size * n, digit_size * n))
grid_x = np.linspace(-4, 4, n) ❶
grid_y = np.linspace(-1, 1, n)[::-1]
for i, yi in enumerate(grid_y): ❷
for j, xi in enumerate(grid_x): ❷
z_sample = np.array([[xi, yi]]) ❸
x_decoded = decoder.predict(z_sample) ❹
digit = x_decoded[0].reshape
➥ (digit_size, digit_size) ❺
figure[i * digit_size: (i + 1) * digit_size,
j * digit_size: (j + 1) * digit_size] = digit
❶ 创建线性空间值
❷ 遍历值
❸ 创建一个样本参数张量
❹ 解码器生成输出图像。
❺ 将图形绘制到图像网格中

图 8.16 一个 VAE 输出示例
VAE 在短时间内产生的结果可以相当好。除此之外,我们对模型有更多的控制权,可以轻松地识别模型正在学习的内容。这反过来又使得优化模型变得更容易,因为有几个选项可供选择。
8.5.3 学习练习
使用以下练习来帮助提高你对 VAE 的理解:
-
修改列表 8.21 中的各种超参数,然后重新运行笔记本。看看每个超参数对生成的结果有什么影响。
-
在列表 8.22 和 8.26 中增加或减少编码器和解码器模型层的数量,然后重新运行笔记本。
-
调整和调整 VAE 模型,使其产生最佳版本的图 8.13。
审查和理解 VAE 的工作原理是第九章的必要背景。抓住机会理解我们刚刚覆盖的基本内容,特别是理解如何从学习到的表示空间生成图像。这些信息是下一章进入进化生成深度学习旅程的基础。
摘要
-
AEs 是生成建模/学习的基础,并使用无监督学习来训练模型。
-
AEs 通过将数据编码到潜在/中间表示,然后重建或解码数据回到其原始形式来工作。
-
内部中间/潜在表示需要一个中间瓶颈来减少或压缩数据。压缩过程允许模型学习数据的潜在表示。
-
使用卷积(CNN)层的复杂 AE 可能很难构建。可以使用神经进化来构建定义编码器和解码器部分的分层架构。在编码器和解码器中使用卷积层需要额外的
UpSampling层和匹配的层配置。这些专用配置可以编码到定制的遗传序列中。 -
可以开发定制的
变异和交叉算子来处理构建进化 AE 所需的定制遗传编码。 -
训练一个构建进化架构的进化自动编码器可能需要一些时间来探索多个架构。
-
变分自动编码器(VAE)中学习的潜在表示可以用来可视化内部表示的样子。变分自动编码器是对自动编码器的一种扩展,它使用一个中间采样层来将编码器与解码器断开连接。断开编码器或解码器可以提供更好的性能。
9 生成深度学习和进化
本章涵盖
-
概述生成对抗网络
-
理解生成对抗网络优化中的问题
-
通过应用 Wasserstein 损失修复生成对抗网络问题
-
为进化优化创建生成对抗网络编码器
-
使用遗传算法进化深度卷积生成对抗网络
在上一章中,我们介绍了自编码器(AEs)并学习了如何提取特征。我们学习了如何将进化应用于 AE 的网络架构优化,然后我们介绍了变分自编码器,它引入了生成深度学习或表示学习的概念。
在本章中,我们继续探索表示学习,这次通过研究生成对抗网络(GANs)。GANs 是一个值得几本书来探讨的有趣话题,但就我们的目的而言,我们只需要探索其基础。因此,在本章中,我们研究 GAN 的基础以及它如何通过进化进行优化。
GANs 的训练非常困难,因此能够通过进化优化这个过程将是有益的。我们将在下一节介绍基本的 GAN,通常被称为“原味”GAN。
9.1 生成对抗网络
GAN 是深度学习中的艺术家,虽然它可以创造出美丽的表示,但它也有恶意用途。虽然我们在这个部分不探讨这些极端情况,但我们确实探讨了基本 GAN 的工作原理。然而,在我们跳入代码之前,我们将快速介绍或复习 GAN 是如何工作的。
9.1.1 介绍 GANs
我们经常用艺术伪造者和艺术判别器、侦探或评论家的类比来浪漫化 GANs 的解释。艺术伪造是一个有利可图的行业,艺术伪造者试图生成可以欺骗艺术评论家或侦探的伪造品。同样,侦探使用他们的知识库来确定生成艺术的真伪,防止伪造品被出售。
图 9.1 是艺术生成器与艺术判别器或侦探之间的对抗战的经典表示。判别器通过评估真实艺术和伪造者生成的伪造艺术来学习。生成器或伪造者对真实艺术一无所知,只从侦探或判别器的反馈中学习。

图 9.1 GAN 的经典解释
图 9.2 将图 9.1 的过程描绘为一个深度学习系统。生成器(G)接收一个随机化的潜在空间作为输入,并添加了一些噪声。这代表了艺术伪造者从图 9.1 中作为输入(I)的随机想法。从这些随机想法中,生成的图像被发送到判别器(D)以验证艺术的真伪——是真是假。

图 9.2 生成对抗网络
同样,D 将真实图像样本和 G 生成的假图像作为输入。D 通过对其预测的反馈来学习检测真实与假货——如果 D 将真实预测为假,则得到消极反馈;如果它成功检测到真实图像,则得到积极反馈。
相反,G 从 D 的反馈中学习。同样,如果生成器能够欺骗 D,则这种反馈是积极的;如果它失败了,则反馈是消极的。反过来,如果 D 认证了一个假货为真,它就会收到消极反馈;如果它检测到一个假货为假,它就会收到积极反馈。
表面上看,对抗训练过程的简单优雅被其训练的难度所掩盖。GANs 在 G 和 D 以相同速度学习时效果最佳。如果其中任何一个学习得更快或更慢,那么系统就会崩溃,双方都无法从这种共生关系中受益。
如本章所讨论的,GAN 的训练和优化是应用进化优化策略的一个很好的候选者。然而,在我们到达那里之前,回顾 GAN 如何工作以及如何在下一节中构建的技术实现是有帮助的。
9.1.2 在 Keras 中构建卷积生成对抗网络
在本节中,我们查看使用卷积的基本的、“经典”GAN,我们通常将其称为 DCGAN 的双卷积 GAN。DC 的添加仅仅意味着 GAN 通过添加卷积变得更加专业化。我们在这里涵盖的很多内容是对之前章节中关于卷积和自编码器的回顾。因此,我们在这里不涵盖那些细节,而是简单地研究如何构建和训练 GAN。
打开 Google Colab 中的 EDL_9_1_GAN.ipynb 笔记本。如果需要帮助,请参考附录。通过选择菜单中的“运行”>“运行所有”来运行笔记本中的所有单元格。
在本章中,我们使用 MNIST 手写数字和 Fashion-MNIST 数据集。然而,为了减少训练时间,我们使用extract函数从数据集中提取单个类别的图像,如列表 9.1 所示。extract函数接受图像批次和标签以及要提取的类别号作为输入。第一行提取与类别号相等的标签匹配的索引。然后,使用索引列表从原始数据集中隔离出匹配类别的图像子集。结果是只包含一个类别图像的数据集:train_images。我们可以从对extract函数的调用中看到,使用类别号5代表数据集中的数字 5,如图表输出所示。
列表 9.1 EDL_9_1_GAN.ipynb:extract函数
def extract(images, labels, class_):
idx = labels == class_ ❶
print(idx)
imgs = images[idx] ❷
print(imgs.shape) ❸
return imgs
train_images = extract(train_images, train_labels, 5)
❶ 提取匹配类别的图像索引
❷ 提取匹配索引的图像
❸ 打印出新图像数据集的形状/大小
接下来,我们查看为生成器和判别器设置一些基本超参数和优化器。第一个参数是一个超参数,它定义了输入到生成器的潜在空间或随机想法的大小。接下来,我们为 G 和 D 创建不同的优化器,以尝试平衡训练,如列表 9.2 所示。之后,我们计算一个卷积常数,我们将使用它来构建网络以及提取通道数和图像形状。这个笔记本是为了支持各种其他样本数据集而开发的,包括 CIFAR。
列表 9.2 EDL_9_1_GAN.ipynb:优化器和超参数
latent_dim = 100 ❶
g_optimizer = Adam(0.0002, 0.5) ❷
d_optimizer = RMSprop(.00001) ❷
cs = int(train_images.shape[1] / 4) ❸
print(train_images.shape)
channels = train_images.shape[3] ❹
img_shape = (train_images.shape[1], ❹
➥ train_images.shape[2], channels), 5) ❹
❶ 定义潜在空间输入大小
❷ 为 G 和 D 创建优化器
❸ 计算卷积空间常数
❹ 提取图像通道和大小
如列表 9.3 所示,GAN 是通过将判别器和生成器分解为单独的模型并组合它们来构建的,这与构建自动编码器(AE)的方式类似。生成器架构类似于 AE 中的解码器,其中 build_generator 函数创建一个卷积网络,从随机和嘈杂的潜在空间生成图像。
列表 9.3 EDL_9_1_GAN.ipynb:构建生成器
def build_generator():
model = Sequential()
model.add(Dense(128 * cs * cs, activation="relu",
➥ input_dim=latent_dim)) ❶
model.add(Reshape((cs, cs, 128))) ❷
model.add(UpSampling2D()) ❸
model.add(Conv2D(128, kernel_size=3, padding="same"))
model.add(BatchNormalization(momentum=0.8))
model.add(Activation("relu"))
model.add(UpSampling2D())
model.add(Conv2D(64, kernel_size=3, padding="same"))
model.add(BatchNormalization(momentum=0.8))
model.add(Activation("relu"))
model.add(Conv2D(channels, kernel_size=3,
➥ padding="same")) ❹
model.add(Activation("tanh"))
model.summary()
noise = Input(shape=(latent_dim,)) ❺
img = model(noise)
return Model(noise, img)
❶ 第一层输入潜在空间。
❷ 重新塑形输出以适应卷积
❸ 使用上采样来增加分辨率
❹ 将通道展平以匹配图像输出
❺ 将随机噪声作为输入添加到模型中
图 9.3 显示了运行 build_generator 函数后的模型摘要。请注意,这仅是内部模型的摘要,我们还在基础生成器周围添加了另一个模型包装器以添加噪声输入。

图 9.3 生成器模型输出的摘要
判别器以类似的方式构建,但这次我们在列表 9.4 中添加了一个验证输入。模型从以图像作为输入的卷积层开始,使用大小为 2 的步长来减少或池化图像以供下一层使用。在这里增加步长与池化一样,可以减少图像大小。判别器的输出是一个单一值,它将输入图像分类为伪造或真实。
列表 9.4 EDL_9_1_GAN.ipynb:构建判别器
def build_discriminator():
model = Sequential()
model.add(Conv2D(32, kernel_size=3, strides=2,
➥ input_shape=img_shape, padding="same")) ❶
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Conv2D(64, kernel_size=3, strides=2,
➥ padding="same")) ❷
model.add(ZeroPadding2D(padding=((0,1),(0,1))))
model.add(BatchNormalization(momentum=0.8))
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Conv2D(128, kernel_size=3, strides=2,
➥ padding="same")) ❷
model.add(BatchNormalization(momentum=0.8))
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Conv2D(256, kernel_size=3, strides=1,
➥ padding="same")) ❷
model.add(BatchNormalization(momentum=0.8))
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(1, activation='sigmoid')) ❸
model.summary()
img = Input(shape=img_shape) ❶
validity = model(img) ❹
return Model(img, validity)) ❺
❶ 第一层卷积以图像作为输入。
❷ 卷积层使用步长=2 来减少池化图像。
❸ 最终输出是一个单一值。
❹ 将有效性(真实或伪造)作为输入添加到模型中
❺ 返回组合模型
由于我们分别从生成器中训练判别器,因此我们还使用 d_optimizer 编译创建的模型,损失为二元交叉熵,并使用准确度作为指标,如下列所示。
列表 9.5 EDL_9_1_GAN.ipynb:编译判别器
d = build_discriminator()
d.compile(loss='binary_crossentropy',
optimizer=d_optimizer, ❶
metrics=['accuracy'])
❶ 使用优化器编译模型
现在,我们可以使用我们之前构建的 D 和 G 模型来构建联合 GAN 模型。在单元格内部,我们创建一个表示生成器潜在空间输入的输入,如列表 9.6 所示。然后,我们创建一个来自 G 的输出,称为 img,用于生成的图像。之后,我们关闭将在联合 GAN 中使用的判别器模型的训练。我们不在联合 GAN 中训练判别器。相反,生成器在联合 GAN 中使用单独的优化器 g_optimizer 分别进行训练。判别器输出的图像的有效性用于在联合模型中训练生成器。
列表 9.6 EDL_9_1_GAN.ipynb:编译判别器
z = Input(shape=(latent_dim,))
img = g(z) ❶
d.trainable = False ❷
valid = d(img) ❸
gan = Model(z, valid) ❹
gan.compile(loss='binary_crossentropy',
➥ optimizer=g_optimizer)]) ❺
❶ 从潜在空间生成图像
❷ 在 GAN 中关闭判别器训练
❸ 引入对抗性真实值
❹ 构建联合模型
❺ 编译带有损失和优化器的模型
由于我们有独立的训练流程,我们不能简单地使用 Keras 的 model.fit 函数。相反,我们必须分别训练判别器和生成器。代码,如列表 9.7 所示,首先创建真实和伪造图像集的对抗性真实值,为有效图像创建一个 1s 的张量,为伪造图像创建一个 0s 的张量。训练在两个循环中进行:外循环由 epoch 数量控制,内循环由批次的计算数量控制。在循环内部,我们随机采样一组真实图像 imgs,然后使用随机噪声生成一组伪造图像 gen_images。然后,我们在判别器上训练并计算真实和伪造图像的损失。注意,对于每一组训练,我们如何传递相应的真实值。通过取真实和伪造损失的均值来计算最终的联合判别器损失。最后,我们通过传递有效真实值与生成的伪造图像来训练联合 GAN 或仅生成器。
列表 9.7 EDL_9_1_GAN.ipynb:训练 GAN
batches = int(train_images.shape[0] / BATCH_SIZE)
# Adversarial ground truths
valid = np.ones((BATCH_SIZE, 1)) ❶
fake = np.zeros((BATCH_SIZE, 1)) ❶
for e in range(EPOCHS):
for i in tqdm(range(batches)):
idx = np.random.randint(0, train_images.shape[0],
➥ BATCH_SIZE) ❷
imgs = train_images[idx] ❷
noise = np.random.normal(0, 1, (BATCH_SIZE,
➥ latent_dim)) ❸
gen_imgs = g.predict(noise) ❸
d_loss_real = d.train_on_batch(imgs, valid) ❹
d_loss_fake = d.train_on_batch(gen_imgs, fake) ❹
d_loss = 0.5 * np.add(d_loss_real, d_loss_fake) ❹
g_loss = gan.train_on_batch(noise, valid) ❺
❶ 生成对抗性真实值
❷ 一个样本随机批次的真实图像
❸ 创建噪声并生成伪造图像
❹ 训练判别器并在真实和伪造图像上计算损失
❺ 使用有效真实值训练生成器
图 9.4 显示了在单个数据类别(数字 5)上训练 GAN 10 个周期的结果。你可以开始看到生成器创建的图像类似于手绘的 5。结合生成的图像,我们还看到了判别器的损失训练结果,按真实和伪造分开以及生成器。不深入数学,训练“vanilla”GAN 的目标是最大化伪造图像的损失并最小化真实图像的损失。本质上,D 需要变得更好于识别真实图像,但也需要变得更差于识别伪造图像。相反,生成器必须最小化创建伪造图像的损失。在图中,这似乎是相反的,但这是因为它仍然处于训练的早期阶段。

图 9.4 训练 GAN 10 个周期
前进并增加训练 GAN 的EPOCHS数量,然后通过菜单中的 Runtime > Run All 再次运行笔记本。你会看到各种真实和伪造损失如何分别最大化或最小化。
9.1.3 学习练习
使用以下练习来帮助提高你对基本 GAN 的理解:
-
增加或减少
BATCH_SIZE,然后重新运行笔记本。改变这个超参数对 GAN 训练有什么影响? -
在 9.2 列表中增加或减少
g_optimizer和d_optimizer的学习率。改变任一优化器对 GAN 训练有什么影响? -
在 9.1 列表中不要使用
extract函数将数据集限制为单个类别。这会如何影响 GAN 的训练?
现在,我们有一个可以学习生成给定类别真实且准确的数字的正在工作的 GAN。虽然概念很简单,但希望你现在也能欣赏到我们刚刚快速覆盖的代码的微妙复杂性和细微差别。我们将在下一节中探讨这些技术细微差别,并尝试理解训练 GAN 的难度。
9.2 训练 GAN 的挑战
GAN 最好被描述为判别器和生成器之间的平衡行为,如果任一模型超过另一个,整个系统就会失败。由于判别器是单独训练的,它仍然可能产生有效的模型,但在更广泛的应用中这很少是有用的。
判别器再利用
虽然构建和训练 GAN 的目标是能够生成逼真的伪造品,但另一个好处是拥有一个健壮的判别器,可以区分真实图像和伪造图像。判别器本质上成为分类器,可以识别给定数据集中真实或伪造图像之间的差异,使得模型可以作为整个数据集的简单分类器被重用。例如,如果你在面部上训练了一个 GAN,那么生成的判别器可以用来判断任何图像是否为面部或非面部。
构建和训练一个能够进行这种平衡操作并产生优秀结果的 GAN 是出了名的困难。在本节中,我们探讨了在训练 GAN 时可能出现的明显和不那么明显的失败点。当然,随着我们进入本章,我们将探讨各种手动和通过进化的策略来解决这些问题。然而,在我们这样做之前,让我们回顾一下为什么 GAN 优化是一个问题。
9.2.1 GAN 优化问题
通常,GAN 训练的主要问题是在一个有效且同步的解决方案中让生成器和判别器收敛。幸运的是,当这些问题出现时,它们通常以各种现象的形式变得明显。以下是在训练 GAN 时可能遇到的一些最常见和可识别的问题的总结:
-
梯度消失—如果判别器在识别伪造内容方面变得强大,这通常会减少反馈给生成器的损失量。反过来,这种减少的损失会降低应用于生成器的训练梯度,导致梯度消失。
-
模式坍塌或过拟合—生成器可能会陷入不断生成几乎相同输出的状态,变化很小。这是因为模型变得过于专业化,实际上过度拟合了生成的输出。
-
无法收敛—如果在训练过程中生成器改进得太快,判别器会感到不知所措和困惑。这导致判别器崩溃,本质上是在观察到的真实或伪造图像之间随机做出 50/50 的猜测。
观察这些问题并能够识别它们何时发生,对于理解和训练生成对抗网络(GAN)是有用的。在接下来的几个小节中,我们将探讨修改原始笔记本以复制和观察这些现象。
9.2.2 观察梯度消失
为了在生成器中复制梯度消失现象,我们通常只需要调整用于判别器的优化器。网络架构也可能对梯度消失问题产生影响,但我们将展示我们已经采取的一些措施来解决这个问题。打开您的浏览器,让我们跳转到下一个笔记本。
在 Colab 中打开 EDL_9_2_GAN_Optimization.ipynb 笔记本。在我们回顾一些代码部分之前,不要运行整个笔记本。向下滚动几个单元格,找到优化器设置的地方,然后寻找注释 vanishing gradients,如下所示。取消以下设置判别器优化器的行注释,即取消注释 disc_optimizer。注释掉原始的判别器优化器,然后取消注释标记为 vanishing gradients 的优化器。
列表 9.8 EDL_9_2_GAN_Optimization.ipynb: 设置优化器
gen_optimizer = Adam(0.0002, 0.5)
#disc_optimizer = RMSprop(.00001) ❶
# vanishing gradients
disc_optimizer = Adam(.00000001, .5) ❷
❶ 取消原始优化器的注释
❷ 取消 Adam 优化器的注释
将优化器替换为判别器的结果,实际上是使其在识别伪造和真实图像方面变得更好或非常好。因此,我们应该看到随着训练的进行,生成器的损失最小化,而输出没有明显的改进。
在你做出更改后,通过选择菜单中的“运行”>“运行所有”来运行笔记本中的所有单元格。向下滚动到训练输出;你应该看到与图 9.5 中显示的类似输出。结果显示了经典的指示,表明生成器由于梯度消失而陷入困境。GAN 可能遇到此问题的两个强烈指标是伪造生成器的损失和判别器的损失。如图所示,判别器的伪造损失在整个训练过程中保持在很小的范围内。对于生成器来说,这导致损失随时间的变化观察到的变化较小,产生梯度消失。

图 9.5 GAN 训练输出显示梯度消失
通常,当我们在一个深度学习模型中观察到梯度消失时,我们会审查模型架构并寻找可能导致 VG 的区域。如果你参考生成器模型构建的地方,你可能注意到我们正在使用 ReLU 激活函数。我们可以通过取消注释代码来实现这一点,如下面的列表所示。
列表 9.9 EDL_9_2_GAN_Optimization.ipynb:尝试使用 LeakyReLU
model.add(Dense(128 * cs * cs, activation="relu", input_dim=latent_dim))
model.add(Reshape((cs, cs, 128)))
model.add(UpSampling2D())
model.add(Conv2D(128, kernel_size=3, padding="same"))
model.add(BatchNormalization(momentum=0.8))
#model.add(Activation("relu")) ❶
model.add(LeakyReLU(alpha=0.2)) ❷
model.add(UpSampling2D())
model.add(Conv2D(64, kernel_size=3, padding="same"))
model.add(BatchNormalization(momentum=0.8))
#model.add(Activation("relu")) ❶
model.add(LeakyReLU(alpha=0.2)) ❷
model.add(Conv2D(channels, kernel_size=3, padding="same"))
model.add(Activation("tanh"))
❶ 注释掉原始激活函数
❷ 取消注释 LeakyReLU 激活函数
运行笔记本中的所有单元格。不幸的是,我们观察到非常小的改进。这是因为交换生成器的激活函数几乎没有效果,因为问题是判别器。如果你想观察这个 GAN 应该如何工作,请继续通过注释和取消注释代码,然后重新运行笔记本。
解决 GAN 中梯度消失的典型方法是对优化器进行调整或解决损失计算的方式。我们试图在本章的后面部分理解和改进损失计算,但在那之前,让我们跳到下一节观察其他形式的 GAN 失败。
9.2.3 观察 GAN 中的模式崩溃
当一个 GAN 在输出中难以产生变化时,就会发生模式崩溃。这种情况发生在生成器只找到可以欺骗评论家的一个或一小组输出时。然后,随着评论家的改进,生成器就会陷入只产生输出的小范围变化的困境。
在 Colab 中打开 EDL_9_2_GAN_Optimization.ipynb 笔记本。如果你在上一个部分中进行了任何修改,请确保从存储库中加载一个全新的副本。再次向下滚动到优化器设置部分,然后取消注释注释 mode collapse 下的代码,如下面的列表所示。
列表 9.10 EDL_9_2_GAN_Optimization.ipynb:再次设置优化器
gen_optimizer = Adam(0.0002, 0.5)
#disc_optimizer = RMSprop(.00001) ❶
# mode collapse
disc_optimizer = Adam(.002, .9))) ❷
❶ 注释掉原始优化器
❷ 取消 Adam 优化器的注释
在您做出更改后,通过菜单中的“运行”>“运行所有单元格”来运行笔记本中的所有单元格。图 9.6 显示了在 25 个时期上训练 GAN 的输出。您的结果可能会有所不同,但您应该观察到输出图像的模式崩溃,如图所示。

图 9.6 观察 GAN 上的模式崩溃
克服模式崩溃的简单修复当然是找到正确的优化器。还有其他方法也可以帮助最小化这个问题,包括调整损失函数和展开 GAN。
展开 GAN
展开 GAN 背后的想法是将生成器在判别器的当前和未来状态上训练。这允许生成器向前看,并以时间旅行的形式考虑到判别器的未来形态。虽然这个概念并不复杂,但实现这种当前和未来状态管理的代码却是。
我们将在后面介绍交换 GAN 损失函数,而展开 GAN 对于我们的简单需求来说过于复杂。相反,我们选择一个非常简单的方法来缓解模式崩溃:使用噪声。
滚动到训练函数;注意添加了一个新的ADD_NOISE布尔形式常量,如下面的列表所示。您可以使用 Colab 表单在True和False之间切换此变量。将其切换为True,然后通过菜单中的“运行”>“运行所有单元格”来运行笔记本中的所有单元格。
列表 9.11 EDL_9_2_GAN_Optimization.ipynb:向对抗性地面真实值添加噪声
if ADD_NOISE:
fake_d = np.random.sample(BATCH_SIZE) * 0.2 ❶
valid_d = np.random.sample(BATCH_SIZE) * 0.2 + 0.8 ❷
valid_g = np.ones((BATCH_SIZE, 1)) ❸
else:
valid_d = np.ones((BATCH_SIZE, 1))
fake_d = np.zeros((BATCH_SIZE, 1))
valid_g = np.ones((BATCH_SIZE, 1))
❶ 假设的地面真实值现在介于 0 和 0.2 之间。
❷ 有效的地面真实值现在介于 0.8 和 1.0 之间。
❸ 保持生成器的有效地面真实值不变
图 9.7 显示了在添加噪声的情况下,经过 25 个时期训练 GAN 的结果。由于优化器的差异,结果仍然不算出色,但我们可以看到输出变异性有所改善。

图 9.7 显示模型输出增加变异性的一例输出
如您从最后一个笔记本中的变化中可以看到,我们通过简单地向对抗性地面真实值添加噪声来纠正了模式崩溃问题。在下一节中,我们将解决 GAN 的另一个问题领域。
9.2.4 观察 GAN 中的收敛失败
收敛是 GAN 的一个基本问题,可能是模式崩溃、梯度消失或优化不平衡的后果。因此,我们可以相对容易地复制收敛失败。然而,在这个观察中,我们想要看看一个例子,其中只是生成器或判别器未能收敛。
在 Colab 中打开 EDL_9_2_GAN_Optimization.ipynb 笔记本。如果您对其进行了修改,请确保从存储库中开始一个新的副本。滚动到设置优化器的单元格,并取消注释/注释标记为convergence的适当行,如下面的列表所示。
列表 9.12 EDL_9_2_GAN_Optimization.ipynb:设置优化器
# original optimizers
#gen_optimizer = Adam(0.0002, 0.5) ❶
disc_optimizer = RMSprop(.00001)
# convergence
gen_optimizer = RMSprop(.00001) ❷
❶ 注释掉原始生成器优化器
❷ 取消优化器的注释
通过菜单中的“运行”>“运行所有单元格”来运行笔记本中的所有单元格。图 9.8 显示了 GAN 生成器的收敛失败。虽然判别器看起来收敛得很好,但我们可以看到生成器损失的不断增加导致无法收敛。

图 9.8 GAN 生成器未能收敛
与我们之前的例子一样,有一个相对简单的解决方案来纠正收敛问题。一个解决方案是打破生成器和判别器训练之间的紧密循环。我们可以通过允许 D 和 G 训练相互独立地循环来实现这一点。
为了支持这种独立的迭代方案,我们添加了更多的代码和额外的输入来控制它,如列表 9.13 所示。列表中的代码只显示了训练循环的基本部分,在这个过程中,我们添加了两个内部循环——一个用于训练判别器,另一个用于训练生成器。这些循环的运行频率可以通过 Colab 中的变量CRITIC_ITS来控制,以控制判别器迭代次数,以及GEN_ITS来控制生成器迭代次数。
列表 9.13 EDL_9_2_GAN_Optimization.ipynb:打破训练循环
CRITIC_ITS = 5 #@param {type:"slider", min:1, ❶
➥ max:10, step:1} ❶
GEN_ITS = 10 #@param {type:"slider", min:1, ❶
➥ max:10, step:1} ❶
for e in range(EPOCHS):
for i in tqdm(range(batches)):
for _ in range(CRITIC_ITS): ❷
idx = np.random.randint(0, train_images.shape[0], BATCH_SIZE)
imgs = train_images[idx]
noise = np.random.normal(0, 1, (BATCH_SIZE, latent_dim))
gen_imgs = g.predict(noise)
d_loss_real = d.train_on_batch(imgs, valid_d)
d_loss_fake = d.train_on_batch(gen_imgs, fake_d)
d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)
for _ in range(GEN_ITS): ❸
g_loss = gan.train_on_batch(noise, valid_g)
❶ 控制判别器/生成器迭代次数的变量
❷ 判别器的循环
❸ 生成器的循环
将CRITIC_ITS值设置为5,将GEN_ITS设置为10,然后通过“运行”>“运行所有单元格”重新运行所有笔记本单元格。图 9.9 显示了打破生成器和判别器之间紧密依赖关系以及 GAN 收敛的结果。

图 9.9 打破紧密耦合后 GAN 收敛
9.2.5 学习练习
使用以下练习来提高你对 GAN 训练的理解:
-
在 GAN 中训练生成器时,如何减少模式崩溃的可能性?
-
生成器收敛失败的主要原因是什么?
-
你如何减少 GAN(生成器)中的梯度消失问题?
在生成器和判别器之间获得正确的迭代次数成为了一个通过使用不同值重新运行模型的问题。当然,这可以手动完成,或者,正如你所猜想的,通过某种形式的进化优化来完成。当这个 GAN 运行得更好时,我们可以在下一节中通过更新我们使用的损失函数来添加另一个改进。
9.3 使用 Wasserstein 损失修复 GAN 问题
在下一个笔记本中,我们将通过减少或消除收敛失败、梯度消失和模式崩溃来提高 GAN 性能并解决问题。我们通过在 GAN 中引入一种称为Wasserstein 损失的替代损失或距离度量方法来实现这一点。在跳入笔记本之前,让我们在下一节中回顾一下 Wasserstein 损失是什么。
9.3.1 理解 Wasserstein 损失
当我们训练 GAN 时,面临的一个关键问题是如何解决和平衡生成器和判别器之间的损失。在标准的 GAN 中,判别器通过概率来衡量损失,即图像是伪造的还是真实的概率。在数学上,测量概率差异,即不确定性的度量,在连续的训练迭代中会变得不准确。
2017 年,Martin Arjovsky 等人在其题为“Wasserstein GAN”的论文中提出了一种改进的损失方法:用评论家替换判别器。在他们提出的方法中,评论家不是测量概率,而是预测一个表示图像真实或伪造程度的值。因此,生成的图像可以在真实到伪造的尺度上进行测量。
基本上,当我们训练 GAN 时,我们的目标是缩小或优化真实与伪造之间的距离。当我们用概率来衡量这个距离时,我们就固定使用不确定性的度量。通过引入损失的缩放距离,我们引入了距离优化的另一种度量。
图 9.10 显示了测量变分距离或概率距离与 Wasserstein 距离之间的比较。Wasserstein 距离被称为地球迁移距离,因为它更好地描述了如何测量两个分布。Kullback-Lieber 和 Jensen Shannon 距离衡量的是水平距离,而地球迁移距离则考虑了垂直差异。

图 9.10 变分距离与 Wasserstein 距离的差异
使用地球迁移距离的好处是,它更好地量化了真实或伪造图像之间的损失或距离,从而产生一个更稳健且更不敏感的模型。使用 Wasserstein 距离还可以消除或减少 GAN 遇到模式崩溃、无法收敛和梯度消失的可能性,正如我们在下一节中实现 Wasserstein 损失在 GAN 中看到的那样。
9.3.2 使用 Wasserstein 损失改进 DCGAN
现在,我们可以跳进去回顾如何在 GAN 中实现 Wasserstein,或地球迁移,损失。这个笔记本与我们在 DCGAN 中构建的相同,只是扩展了 Wasserstein 损失。
在 Colab 中打开 EDL_9_3_WGAN.ipynb 笔记本。然后,通过菜单中的“运行”>“运行所有单元格”来运行笔记本中的所有单元格。
滚动到实例化优化器的位置。你可能会首先注意到我们将discriminator模型的名称更改为critic,如下所示。这是因为critic预测的是真实度或伪造度的度量,而不是它真实或伪造的概率。此外,请注意我们现在正在为generator和critic使用相同的优化器。我们也可以这样做,因为真实与伪造的比例将标准化度量之间的差异。
列表 9.14 EDL_9_3_WGAN.ipynb:优化器设置
gen_optimizer = RMSprop(lr=0.00005) ❶
critic_optimizer = RMSprop(lr=0.00005) ❷
❶ 生成器优化器
❷ 判别器替换为评论家
移动到下一个单元格,如列表 9.15 所示;我们可以看到一个名为wasserstein_loss的函数中的 Wasserstein 损失的计算。从单行代码中,你可以看到真实或实际输入的平均值是如何乘以预测的。这个输出的结果是两个分布之间的地球搬运距离。
列表 9.15 EDL_9_3_WGAN.ipynb:Wasserstein 损失函数
def wasserstein_loss(y_true, y_pred):
return K.mean(y_true * y_pred) ❶
❶ 计算真实和预测的平均值。
我们可以通过查看以下列表中的critic构建代码来看到wasserstein_loss函数的使用。再次注意,我们如何更新了discriminator的名称为critic,并在编译模型时使用了wasserstein_loss函数。
列表 9.16 EDL_9_3_WGAN.ipynb:构建评论者。
critic = build_critic()
critic.compile(loss=wasserstein_loss, ❶
optimizer=critic_optimizer, ❷
metrics=['accuracy'])
❶ 使用 Wasserstein 损失。
❷ 使用选定的优化器。
我们需要考虑的最后的主要变化是更新评论者训练代码。使用critic计算损失与discriminator相同——除了命名之外没有变化。实现 Wasserstein 损失引入了梯度爆炸的可能性;为了克服这一点,我们添加了一个剪辑步骤,如列表 9.17 所示。对于critic的每个训练迭代,我们现在确保将每个模型权重剪辑到clip_value超参数内。这种权重的剪辑消除了梯度爆炸的可能性,并减少了收敛模型空间。
列表 9.17 EDL_9_3_WGAN.ipynb:训练评论者。
c_loss_real = critic.train_on_batch(imgs, valid)
c_loss_fake = critic.train_on_batch(gen_imgs, fake)
c_loss = 0.5 * np.add(c_loss_real, c_loss_fake) ❶
for l in critic.layers: ❷
weights = l.get_weights()
weights = [np.clip(w, -clip_value, clip_value)
➥ for w in weights] ❸
l.set_weights(weights))
❶ 真实和伪造损失的平均值。
❷ 遍历评论层。
❸ 使用范围剪辑层权重。

图 9.11 在提取的数字上训练 Wasserstein GAN。
图 9.11 显示了在 MNIST 手写数字数据集的一个单独提取类别上训练这个 GAN 在 80 个 epoch 的结果。如果你想看到这个数据集在 Fashion-MNIST 数据集上的表现,请重新运行整个笔记本,并在列表 9.18 中更改代码。你也可以移除对 extract 函数的调用,以查看模型在数据集的所有类别上的表现。
列表 9.18 EDL_9_3_WGAN.ipynb:切换到 Fashion-MNIST 数据集
from tensorflow.keras.datasets import mnist as data ❶
#from tensorflow.keras.datasets import fashion_mnist
➥ as data ❷
❶ 注释掉该行。
❷ 取消注释该行。
将 Wasserstein 损失引入 DCGAN,使其成为 WGAN 或 WDCGAN,可以缓解标准 GAN 的几个缺点。减少这些额外的复杂性使得我们更容易构建一个进化优化器来找到我们可能的最佳 GAN。在我们这样做之前,我们需要将我们的 WDCGAN 包装起来,以便它可以在下一节中用作进化优化中的编码模型。
9.4 编码 Wasserstein DCGAN 以进行进化。
我们已经在之前的章节中经历了将各种模型的超参数或架构编码的过程。对于我们的下一个笔记本,我们打算做同样的事情,但仅限于超参数的编码。这允许进化优化器探索一个更简洁的优化空间。
进化优化复杂模型。
随着我们尝试优化的模型变得更加复杂,我们面临着进行更多强度训练操作的迭代。我们不能再依赖于模型只训练 3 个 epoch 就能给出合理的结果;相反,一些模型可能需要训练数百个 epoch。GAN 优化是那些训练成本可能很高的问题集之一。正因为如此,如果你想看到有趣或好的结果,预期一些优化可能需要数小时甚至数天的时间来训练。
下一个笔记本是对本章中我们一直在开发的 GAN 代码的扩展和整合,将其合并为一个单独的类。这个类通过传递一个individual gene序列来实例化,以填充各种模型超参数。gene序列是我们之前在应用遗传算法时多次见过的简单浮点数数组。
在 Colab 中打开 EDL_9_4_WDCGAN_encoder.ipynb 笔记本。然后从菜单中选择运行 > 运行所有来运行所有单元格。
在这个笔记本中,一个单独的类封装了整个模型以及每个类的变体,每个类的变体由一个输入gene序列控制:一个浮点数数组,其中数组中的每个元素都有一个由序列索引定义的对应控制的超参数。定义这些索引和超参数值的最小/最大限制的代码如下所示。
列表 9.19 EDL_9_4_WDCGAN_encoder.ipynb: gene编码参数
FILTERS = 0 ❶
MIN_FILTERS = 16
MAX_FILTERS = 128
ALPHA = 1 ❷
MIN_ALPHA = .05
MAX_ALPHA = .5
CRITICS = 2 ❸
MIN_CRITICS = 1
MAX_CRITICS = 10
CLIP = 3 ❹
MIN_CLIP = .005
MAX_CLIP = .1
LR = 4 ❺
MIN_LR = .00000001
MAX_LR = .0001
❶ 卷积中使用的基滤波器数量
❷ LeakyReLU 中使用的 Alpha 参数
❸ 每个生成器迭代的批评次数
❹ 裁剪批评权重的范围
❺ 优化器的学习率
我们接下来可以查看DCGAN类的__init__函数,以了解gene序列i如何定义模型中使用的每个超参数,如列表 9.20 所示。首先,我们确保image_shape能被 4 整除并且可以适应模型的卷积架构。接下来,每个超参数值通过将浮点数映射到相应的空间来生成。代码还初始化权重在零值周围,以便更好地将权重与裁剪函数对齐。最后,代码创建一个单独的优化器,然后构建各种模型。
列表 9.20 EDL_9_4_DCGAN_encoder.ipynb: 初始化模型
class DCGAN:
def __init__(self, i):
assert image_shape[0] % 4 == 0, "Image shape must
➥ be divisible by 4." ❶
self.image_shape = image_shape
self.z_size = (1, 1, latent_dim)
self.n_filters = linespace_int(i[FILTERS],
➥ MIN_FILTERS, MAX_FILTERS)
self.alpha = linespace_int(i[ALPHA], MIN_ALPHA, ❷
➥ MAX_ALPHA) ❷
self.lr = linespace(i[LR], MIN_LR, MAX_LR) ❷
self.clip_lower = -linespace(i[CLIP], MIN_CLIP, ❷
➥ MAX_CLIP) ❷
self.clip_upper = linespace(i[CLIP], MIN_CLIP, ❷
➥ MAX_CLIP) ❷
self.critic_iters = linespace_int(i[CRITICS], ❷
➥ MAX_CRITICS, MIN_CRITICS) ❷
self.weight_init = RandomNormal(mean=0.,
➥ stddev=0.02) ❸
self.optimizer = RMSprop(self.lr) ❹
self.critic = self.build_critic() ❺
self.g = self.build_generator() ❺
self.gan = self.build_gan() ❺
❶ 确认图像大小能被 4 整除
❷ 将浮点数转换为超参数
❸ 初始化起始权重
❹ 创建一个单独的优化器
❺ 构建模型
我们之前已经看到了大部分代码,但我们应该突出显示训练函数中的一个更新部分,如列表 9.21 所示。在 GANs 中比较模型之间的损失比较复杂,因为损失函数和模型性能存在差异。为了比较一个 GAN 和另一个 GAN 之间的损失,我们标准化损失。我们通过跟踪critic和generator的最小和最大损失来实现这一点,然后使用reverse_space函数在0和1之间的线性空间上输出这个值。
列表 9.21 EDL_9_4_DCGAN_encoder.ipynb:归一化输出损失
min_g_loss = min(min_g_loss, g_loss) ❶
min_fake_loss = min(min_fake_loss, c_loss[1]) ❶
min_real_loss = min(min_real_loss, c_loss[0]) ❶
max_g_loss = max(max_g_loss, g_loss) ❷
max_fake_loss = max(max_fake_loss, c_loss[1]) ❷
max_real_loss = max(max_real_loss, c_loss[0]) ❷
loss = dict(
Real = reverse_space(c_loss[0],min_real_loss, ❸
➥ max_real_loss), ❸
Fake = reverse_space(c_loss[1],min_fake_loss, ❸
➥ max_fake_loss), ❸
Gen = reverse_space(g_loss, min_g_loss, max_g_loss) ) ❸
❶ 跟踪最小损失
❷ 跟踪最大损失
❸ 将值归一化到 0–1 范围内
通过将包括训练函数在内的所有内容封装到一个类中,我们可以快速实例化一个具有已知 gene 序列的 GAN 来测试结果。为此,如列表 9.22 所示,我们使用 reverse_space 函数将已知的超参数值转换为序列中嵌入的适当浮点值,称为 individual。然后,将这个 individual 传递到 DCGAN 构造函数中创建模型。之后,调用类的 train 函数,使用 verbose=1 选项显示训练结果。
列表 9.22 EDL_9_4_DCGAN_encoder.ipynb:测试编码和 GAN 训练
individual = np.random.random((5)) ❶
individual[FILTERS] = reverse_space(128, MIN_FILTERS, MAX_FILTERS) ❷
individual[ALPHA] = reverse_space(.2, MIN_ALPHA, MAX_ALPHA) ❷
individual[CLIP] = reverse_space(.01, MIN_CLIP, MAX_CLIP) ❷
individual[CRITICS] = reverse_space(5, MIN_CRITICS, MAX_CRITICS) ❷
individual[LR] = reverse_space(.00005, MIN_LR, MAX_LR) ❷
print(individual)
dcgan = DCGAN(individual) ❸
history = dcgan.train(train_images, verbose=1) ❹
❶ 创建一个随机个体
❷ 将值转换为 0–1 空间
❸ 使用个体创建模型
❹ 训练模型并显示结果

图 9.12 训练 DCGAN 10 个周期的输出
图 9.12 展示了在 MNIST 手写数字数据集的一个提取类别上训练模型 10 个周期的结果。通过归一化损失,我们可以清楚地看到模型正在努力优化的内容。如果你将这些结果与图 9.11 进行比较,你可以清楚地看到在已知范围内识别优化目标是多么容易。这是在使用遗传算法优化模型时一个关键的部分,正如本章后面所讨论的。
尝试其他超参数值,看看这些如何影响模型训练。你可能想尝试使用完全随机的 gene 序列来查看模型生成的结果。
9.4.1 学习练习
使用以下练习来提高你对 WGAN 的理解:
-
调整列表 9.19 中的
gene编码超参数,然后重新运行笔记本。 -
不要使用
extract函数将数据集限制为单个类别,然后使用所有数据重新运行笔记本。 -
使用不同的数据集,例如 Fashion-MNIST,然后重新运行笔记本。
现在我们已经封装了一个代表 GAN 的类,并且能够传递一个代表性的 gene 序列来初始化模型,我们可以继续进行优化。在下一节中,我们将遗传算法代码添加到优化这个 DCGAN 模型中。
9.5 使用遗传算法优化 DCGAN
现在我们已经为复制 DCGAN 构建了遗传编码器,我们可以将所有内容整合在一起。在这个阶段,优化封装的 DCGAN 类只是简单地添加 DEAP 并定义我们需要的进化 GA 参数。再次强调,添加进化搜索为我们提供了自我优化 GAN 网络的能力——这正是我们在下一个笔记本中所做的。
在 Colab 中打开 EDL_9_5_EVO_DCGAN.ipynb 笔记本。通过选择菜单中的“运行”>“运行所有”来运行整个笔记本。
如你可能注意到的,这个笔记本安装了 DEAP,并添加了执行 GA 进化的所需工具和算子。不相关的代码单元格被隐藏了,但如果你想查看它们的内 容,只需点击“显示代码”链接或双击单元格。我们之前已经看到过大部分的代码,而且像往常一样,我们只是在这里引用相关的代码部分。
我们首先来看evaluate函数,如列表 9.23 所示,其中我们评估模型的fitness。在函数的开始部分,我们将individual转换为字符串,以便在trained字典中用作索引。注意我们是如何将数值四舍五入到小数点后一位的。因此,起始值[.2345868]变成了[.2],这简化或离散化了字典中的条目数量。这样做是为了将训练从无限探索空间简化为有限空间。更准确地说,通过将数值四舍五入到一位数字,并且知道gene序列长度为5,我们可以确定有 10 × 10 × 10 × 10 × 10 = 100,000 个可能的模型需要测试。这样做的好处是它允许进化更大的populations,而无需重新评估相似的individuals。正如本节所示,评估每个模型需要相当多的时间。
列表 9.23 EDL_9_5_EVO_DCGAN_encoder.ipynb:evaluate函数
trained = {} ❶
generation = 0
def evaluate(individual):
ind = str([round(i, 1) for i in individual]) ❷
if ind in trained: ❸
return trained[ind], ❸
print(f"Generarion {generation} individual {ind}")
dcgan = DCGAN(individual)
history = dcgan.train(train_images, verbose=0)
min_loss_real = 1/loss(history, "Real") ❹
min_loss_gen = 1/loss(history, "Gen") ❹
min_loss_fake = loss(history, "Fake") ❹
total = (min_loss_real + min_loss_gen +
➥ min_loss_fake)/3 ❺
print(f"Min Fitness {min_loss_real}/{min_loss_gen}:{total}")
trained[ind] = total ❻
return total,
toolbox.register("evaluate", evaluate)
❶ 保存评估历史的字典
❷ 四舍五入数值
❸ 训练历史的字典
❹ 计算优化损失
❺ 计算损失的平均值
❻ 训练历史的字典
优化 DCGAN 不是简单比较准确性的问题。我们需要考虑模型输出的三个值或损失:真实、伪造和生成损失。这些损失中的每一个都需要以不同的方式进行优化——在这种情况下,最小化。如果你参考列表 9.23,你可以看到每个损失是如何提取的,在真实和生成损失的情况下,它们被反转以产生部分fitness。总fitness是三个派生损失或fitness值的平均值。
笔记本中的输出显示了进化 DCGAN 最优解的部分结果。我们留给你自己运行这个示例并探索你能产生的最佳潜在 GAN。
最后一个笔记本可能需要相当长的时间来进化,但它自动化了,最终会得到好的结果。在 AutoML 解决方案中,GAN 或其他复杂模型通常不是需要自动化优化的列表上的高优先级。随着时间的推移和 AI/ML 领域的发展,我们这里介绍的方法可能会变得更加主流。
9.5.1 学习练习
使用以下练习继续探索这个 Evo DCGAN 版本:
-
抽出时间进化一个 GAN 模型。然后,使用这个模型在数据集上继续训练,看看你能够生成多好的新输出。
-
将在一个数据集上开发的进化模型重新用于在新的数据集上训练 GAN。这在数据集大小相似的情况下效果最佳,例如 Fashion-MNIST 和 MNIST 手写数字数据集。
-
将这个笔记本调整为使用进化策略和/或微分进化。评估这种方法可能或可能不会改善 GAN 训练超参数的进化。
摘要
-
生成对抗网络是一种生成模型,它使用双网络——一个用于数据判别,另一个用于数据生成:
-
GANs 通过向判别器提供真实样本,同时允许生成器生成假样本来工作。
-
生成器通过从判别器获得反馈来学习生成更好的输出,而判别器也在学习更好地将数据分类为真实或假。
-
使用 Python 和 Keras 可以简单地构建 GAN。
-
-
GANs 以其难以有效训练而闻名:
-
训练 GAN 的核心问题是在判别器和生成器学习速度之间取得平衡。
-
两个网络需要以相同的速率学习,以平衡它们之间的对抗关系。
-
当 GANs 失去平衡时,可能会出现许多常见问题,例如无法收敛、模式坍塌和梯度消失。
-
通过使用进化算法来解决这个训练问题。
-
-
水晶距离(Wasserstein loss)或地球迁移距离是一种衡量损失的措施,可以帮助 GAN 解决或最小化常见的训练问题。
-
通过将 GAN(DCGAN)封装到一个接受遗传编码的
genome表示形式的类中,可以帮助平衡 GAN 的训练超参数。 -
可以使用遗传算法来平衡 GAN 中判别器和生成器的训练。
10 NEAT:增强拓扑结构的神经进化
本章涵盖
-
构建进化的增强拓扑结构网络
-
可视化增强拓扑结构的神经进化网络
-
锻炼增强拓扑结构的神经进化能力
-
锻炼增强拓扑结构的神经进化以对图像进行分类
-
揭示物种形成在神经进化中的作用
在过去几章的内容中,我们探讨了生成对抗网络和自编码器网络的进化优化。与之前的章节类似,在这些练习中,我们在深度学习网络周围叠加或包裹了进化优化。在本章中,我们脱离了 Python 中的分布式进化算法(DEAP)和 Keras,来探索一个名为增强拓扑结构的神经进化(NEAT)的神经进化框架。
NEAT 是由肯·斯坦利在 2002 年开发的,当时他在德克萨斯大学奥斯汀分校。当时,遗传算法(进化计算)和深度学习(高级神经网络)是平等的,并且都被认为是人工智能的下一个大事物。斯坦利的 NEAT 框架吸引了众多人的注意,因为它将神经网络与进化相结合,不仅优化了超参数、权重参数和架构,而且还优化了实际的神经网络连接本身。
图 10.1 显示了常规深度学习网络和进化 NEAT 网络之间的比较。在图中,已经添加和删除了新的连接,并且在进化 NEAT 网络中删除和/或改变了节点的位置。注意这与我们之前仅仅改变深度学习连接层中节点数量的努力有何不同。

图 10.1 深度学习网络和 NEAT 网络的比较
NEAT 通过允许网络的神经连接和节点数量进化,将进化深度学习网络的概念推向了极致。由于 NEAT 还内部进化每个节点的网络权重,因此消除了使用微积分计算误差反向传播的复杂性。这使得 NEAT 网络能够进化到一些复杂的交织和互联的图。它甚至允许网络进化循环连接,正如我们在下一章深入探讨 NEAT 时所看到的。
在本章中,我们探讨了 NEAT 的基本原理,并深入研究了名为 NEAT-Python 的 Python 实现。这个框架很好地抽象了设置进化和深度学习系统的细节。以下列表标识了 NEAT 的每个组件,这些组件涵盖了本书中使用的许多其他方法:
-
参数的神经进化——NEAT 中的权重和参数作为系统的一部分进化。在第六章中,我们使用进化来改变神经网络的权重。
-
架构的神经进化——NEAT 进化网络层,结构本身通过进化进行适应。参见第七章,其中我们介绍了使用遗传算法进行神经进化架构的内容,以了解更多相关信息。
-
超参数优化——NEAT 不使用学习率、优化器或其他标准深度学习辅助工具。因此,它不需要优化这些参数。然而,正如我们将看到的,NEAT 引入了几个超参数来控制网络进化。
在下一节中,我们从基础知识开始,开始探索 NEAT。
10.1 使用 NEAT-Python 探索 NEAT
本章中我们首先考虑的笔记本设置了一个 NEAT 网络来解决经典的一阶 XOR 问题。NEAT 提供了几个配置选项,这个第一个练习展示了其中一些最重要的选项。打开您的网络浏览器,让我们开始查看一些代码。
在 Google Colab 中打开 EDL_10_1_NEAT_XOR.ipynb 笔记本。如需帮助,请参阅附录。通过选择菜单中的“运行”>“运行所有”来运行笔记本中的所有单元格。
NEAT-Python 仍在积极开发中,截至写作时,最佳实践是直接从 GitHub 仓库安装它,而不是从 PyPi 包安装。笔记本中的第一个代码单元格使用pip在第一行执行此操作,如下所示。然后,下一行使用import neat导入包。
列表 10.1 EDL_10_1_NEAT_XOR.ipynb:安装 NEAT-Python
!pip install
➥ git+https://github.com/CodeReclaimers/neat-python.git ❶
#then import
import neat ❷
❶ 从 GitHub 仓库安装
❷ 导入包
滚动到下一个单元格,显示数据的设置,分为xor_inputs(X)和xor_outputs(Y),如下所示。
列表 10.2 EDL_10_1_NEAT_XOR.ipynb:数据设置
xor_inputs = [(0.0, 0.0), (0.0, 1.0), (1.0, 0.0),
➥ (1.0, 1.0)] ❶
xor_outputs = [ (0.0,), (1.0,), (1.0,),
➥ (0.0,)] ❷
❶ 输入:X
❷ 输出:Y
接下来,正如我们之前多次做的那样,我们构建一个评估函数来计算进化 NEAT 网络的“适应度”。这个概念现在应该相当熟悉了,代码与之前的练习相似。与 DEAP 不同,评估函数接受一组称为“基因组”的“基因”序列。函数遍历“基因组”,并为每个计算“适应度”,首先分配一些最大“适应度”,如列表 10.3 所示。然后,它使用FeedForwardNetwork.create函数创建一个经典的前馈网络的新版本,传入genome和配置。然后,通过使用net.activate函数对所有数据进行测试,传入X或xi值之一,并产生Y输出。每次激活输入后,输出与预期的输出xo进行比较,从genome.fitness中减去均方误差(MSE)。最后,eval_genomes函数的结果更新了每个进化“基因组”的当前“适应度”。
列表 10.3 EDL_10_1_NEAT_XOR.ipynb:创建评估函数
def eval_genomes(genomes, config):
for genome_id, genome in genomes: ❶
genome.fitness = 4.0 ❷
net = neat.nn.FeedForwardNetwork.create
➥ (genome, config) ❸
for xi, xo in zip(xor_inputs, xor_outputs): ❹
output = net.activate(xi)
genome.fitness -= (output[0] - xo[0]) ** 2 ❺
❶ 遍历基因组
❷ 分配最大适应度
❸ 从基因组创建 NEAT 网络
❹ 遍历数据
❺ 计算均方误差(MSE)然后从适应度中减去
下一个单元格设置了用于配置和运行 NEAT 代码的配置文件。NEAT-Python(NP)主要是由配置驱动的,你可以更改或微调几个选项。为了保持简单,我们只回顾列表 10.4 中的主要选项:前两个部分。config首先设置fitness标准、fitness阈值、种群大小和reset选项。之后,设置默认的genome配置,首先是激活选项,在这种情况下,只是sigmoid函数。NEAT 允许你从多个激活函数中选择选项,这些选项用于内部互联节点和输出。
列表 10.4 EDL_10_1_NEAT_XOR.ipynb:配置设置
%%writefile config ❶
[NEAT] ❷
fitness_criterion = max
fitness_threshold = 3.99
pop_size = 150 ❸
reset_on_extinction = False
[DefaultGenome] ❹
# node activation options
activation_default = sigmoid
activation_mutate_rate = 0.0
activation_options = sigmoid ❺
❶ 将单元格内容写入名为 config 的文件
❷ 一般配置参数
❸ 设置要进化的个体数量
❹ 基因配置参数
❺ 默认激活函数
笔记本中的最后一个代码单元格包含我们进化 NEAT 网络所需的所有代码。我们首先回顾列表 10.5 中的前几行。代码首先加载配置并设置基因、繁殖、物种和停滞的基础假设。我们将在本章和下一章中介绍这些默认值。之后,从config创建genome种群。然后,向population对象添加报告器StdOutReporter以跟踪进化过程。注意population对象p是如何成为进化的焦点,以及它与 DEAP 的不同之处。
列表 10.5 EDL_10_1_NEAT_XOR.ipynb:设置 NEAT 进化
config = neat.Config(neat.DefaultGenome, neat.DefaultReproduction,
neat.DefaultSpeciesSet, neat.DefaultStagnation,
'config') ❶
p = neat.Population(config) ❷
p.add_reporter(neat.StdOutReporter(False)) ❸
❶ 从配置文件中加载配置
❷ 创建种群
❸ 在进化过程中添加报告器以查看结果
通过在population对象上调用run函数,简单地运行或执行种群的进化,如下所示。进化完成后,代码将打印出获胜者,即具有最佳fitness的genome,并输出对 XOR 输入的预测。
列表 10.6 EDL_10_1_NEAT_XOR.ipynb:进化种群
winner = p.run(eval_genomes) ❶
print('\nBest genome:\n{!s}'.format(winner)) ❷
print('\nOutput:') ❸
winner_net = neat.nn.FeedForwardNetwork.create(winner, config)
for xi, xo in zip(xor_inputs, xor_outputs):
output = winner_net.activate(xi)
print(" input {!r}, expected output {!r}, got {!r}".format(xi, xo,
➥ output))
❶ 对种群执行进化
❷ 打印出最佳基因
❸ 使用基因预测 XOR 并显示
运行此示例的输出如图 10.2 所示。与 DEAP 不同,NP 使用fitness阈值的概念来控制进化迭代次数。如果你还记得,在config设置中,我们将fitness_threshold设置为3.99(见列表 10.4)。图中还显示了网络配置和权重的文本输出。当然,这并不是容易可视化的东西,但我们将在未来的章节中介绍。在图的下端,你可以看到 XOR 输入被正确预测的程度。

图 10.2 在 XOR 上进化 NEAT 网络的最终输出
这个练习演示了我们可以如何快速设置 NEAT 进化来创建一个能够预测 XOR 函数的网络。正如你所看到的,代码中抽象了很多细节,但希望在这个阶段,你理解了进化是如何被应用的内部工作原理的一些内容。除了增强节点拓扑结构之外,我们之前使用 DEAP 完成了所有内部操作。
10.1.1 学习练习
使用以下学习练习来了解更多关于 NEAT 的信息:
-
在列表 10.4 中更改
population大小(pop_size),然后重新运行笔记本。population大小如何影响进化? -
在列表 10.4 中减小
fitness_threshold,然后重新运行笔记本,看看这会对结果产生什么影响。 -
更改输入或输出以匹配另一个函数,或者编写一个函数来创建列表 10.2 中的输出。然后,重新运行笔记本以查看近似新函数的结果。
从这个基本介绍开始,我们将在下一节中探索可视化进化的 NEAT 网络看起来是什么样子。
10.2 可视化进化的 NEAT 网络
现在我们已经将 NEAT-Python 的基本设置完成,我们可以看看添加一些有用的工具到我们的工具箱中。可视化 NEAT 网络对于理解网络架构是如何形成的非常有用。它还突出了网络在拟合或欠拟合问题上的表现如何。
在本节中,我们采用之前的笔记本示例,并添加了可视化进化的最佳genome网络的能力。我们还仔细研究了评估fitness函数是如何开发的。
在 Google Colab 中打开 EDL_10_2_NEAT_XOR_Visualized.ipynb 笔记本。如需帮助,请参阅附录。通过选择菜单中的“运行”>“运行所有”来运行笔记本中的所有单元格。
跳到加载config后的代码单元格开始。所有这些代码通常都在population类中处理,但我们提取了这个小节,如列表 10.7 所示,以突出构建评估函数。在 NP 中,所有genomes都需要一个键或唯一标识符,这里我们任意使用fred。然后,根据默认类型从config创建一个genome——在这个例子中,是一个DefaultGenome。之后,使用genome_config通过genome.configure_new配置genome。最后,通过FeedForwardNetwork.create传递genome和config创建了一个新的fred 1.0 随机网络。
列表 10.7 EDL_10_2_NEAT_XOR_Visualized.ipynb:创建genome网络
key = "fred" ❶
genome = config.genome_type(key) ❷
genome.configure_new(config.genome_config) ❸
net = neat.nn.FeedForwardNetwork.create(genome, config) ❹
❶ 为基因组分配一个键
❷ 创建基因组类型
❸ 从配置中配置基因组
❹ 从基因组创建前馈网络
接下来,网络net被评估,使用for循环遍历数据,从最大适应度中减去 MSE,如列表 10.8 所示。回想一下,在实际评估函数列表 10.4 中,代码也遍历了整个基因组的种群。为了简单起见,我们在这里只评估了基因组 fred。此代码块的输出显示了网络的输入和输出以及总适应度。
列表 10.8 EDL_10_2_NEAT_XOR_Visualized.ipynb:评估基因组
fitness = 4 ❶
for x, y in zip(X, Y): ❷
output = net.activate(x) ❸
print(output, y)
fitness -= (output[0]-y[0])**2 ❹
print(fitness)
❶ 分配最大适应度值
❷ 遍历 x 和 y 值
❸ 在输入上激活网络
❹ 计算 MSE 然后减去
在设置fred之后,我们继续到draw_net函数。此函数已直接从 NEAT 示例中提取,并使用 Graphviz 绘制一个进化的网络。您可以自己查看代码,但在这里我们不会关注具体细节。相反,我们想看看如何调用该函数以及它生成的内容。
下一步是调用draw_net函数;它首先在字典中命名输入和输出节点。之后,通过传递config、genome和主要节点(输入和输出)的名称来调用draw_net函数。我们传递值True以可视化输出,如下所示。
列表 10.9 EDL_10_2_NEAT_XOR_Visualized.ipynb:调用draw_net函数
node_names = {-1: 'X1', -2: 'X2', 0: 'Output'} ❶
draw_net(config, genome, True, node_names=node_names) ❷
❶ 命名输入和输出节点。
❷ 使用 True 调用函数以查看。
图 10.3 显示了我们所称的fred的基本和未进化的基因组的输出。如图所示,网络是一个非常简单的单节点网络,有两个输入,分别标记为X1和X2。

图 10.3 一个初始的 NEAT 网络可视化
到目前为止,NEAT 的种群应该已经进化,我们可以再次调用draw_net函数,这次传入获胜的基因组 winner。在种群上调用run会输出获胜者,或最佳基因组。然后,从获胜者创建网络以展示激活。接下来,使用获胜者的基因组调用draw_net来可视化网络,如下所示。
列表 10.10 EDL_10_2_NEAT_XOR_Visualized.ipynb:可视化获胜基因组
winner = p.run(eval_genomes) ❶
print('\nBest genome:\n{!s}'.format(winner)) ❷
print('\nOutput:')
winner_net = neat.nn.FeedForwardNetwork.create(winner, config)
for xi, xo in zip(X, Y): ❸
output = winner_net.activate(xi)
print(" input {!r}, expected output {!r}, got {!r}".format(xi, xo,
➥ output))
draw_net(config, winner, True, node_names=node_names) ❹
❶ 进化获胜基因组
❷ 输出获胜分数
❸ 遍历并显示激活
❹ 绘制进化的获胜基因组
图 10.4 显示了获胜基因组网络的输出。这显然与经典的深度学习网络中的常规层不相似。相反,我们在这里看到的是一个能够高效处理 XOR 问题的优化网络。

图 10.4 显示获胜基因组网络
能够可视化最终演化的网络有助于理解 NEAT 的工作原理。可视化一个演化的网络也有助于了解配置参数是否足够。正如我们在下一节中看到的,NEAT 有许多旋钮和开关,我们需要理解它们来开发解决更复杂问题的解决方案。
10.3 锻炼 NEAT 的能力
NEAT 及其在 NEAT-Python 中的实现是封装了我们在这本书中实践过的许多优化模式的工具。NEAT 将网络超参数、架构和参数优化以及拓扑增强纳入其中。但它做得好吗?
在本节中,我们回顾了我们使用 sklearn 包制作示例数据集的一个有趣的视觉分类示例。如果你还记得,我们在第六章中展示了使用 EC 进行参数权重优化。这不仅提供了一个很好的基线,还展示了 NEAT 的几个其他配置选项。
在 Google Colab 中打开 EDL_10_3_NEAT_Circles.ipynb 笔记本。如需帮助,请参考附录。通过菜单选择 Runtime > Run All 来运行笔记本中的所有单元格。

图 10.5 配置数据集生成参数
我们从数据集参数表和图 10.5 所示的输出开始。这是我们之前章节中用来生成各种分类问题数据集的相同表单。首先,我们使用月亮问题生成一个简单的数据集。生成的输出显示了一个应该相对容易用简单网络进行分类的数据集。
由于我们现在处理的问题比 XOR 更复杂,我们希望修改 NEAT 配置文件中的配置选项。列表 10.11 仍然只是所有config选项的部分视图,我们再次强调关键的选项,首先是将最大fitness_threshold从4.0降低到3.0,即 75%。然后,我们增加或添加一个中间或中间节点层。在 NEAT 中,我们不按层来考虑节点;我们只关心输入/输出和中间或中间节点的数量。如果这些中间节点恰好按层排列,那是一个愉快的意外,但不是可以预期的。接下来,我们面临几个选项,首先是兼容性。这些选项是用于speciation的,将在稍后介绍。最后要注意的是,我们已经通过添加另外两个可能的功能(identity和relu)来更新了激活选项。
列表 10.11 EDL_10_3_NEAT_Circles.ipynb:检查配置选项
[NEAT]
fitness_criterion = max
fitness_threshold = 3.0 ❶
pop_size = 250
reset_on_extinction = 0
[DefaultGenome]
num_inputs = 2
num_hidden = 10 ❷
num_outputs = 1
initial_connection = partial_direct 0.5
feed_forward = True
compatibility_disjoint_coefficient = 1.0 ❸
compatibility_weight_coefficient = 0.6 ❸
conn_add_prob = 0.2
conn_delete_prob = 0.2
node_add_prob = 0.2
node_delete_prob = 0.2
activation_default = sigmoid ❹
activation_options = sigmoid identity relu ❹
activation_mutate_rate = 0.1 ❹
❶ 将 fitness_threshold 降低到 3.0。
❷ 增加隐藏中间节点的数量
❸ 用于物种形成
❹ 激活选项已扩展。
图 10.6 显示了应用新配置选项的起始genome网络的输出。值得注意的是,并非所有节点都连接到输出,节点 10 既没有连接到输入也没有连接到输出。这允许网络消除不必要的节点,从而防止过拟合或欠拟合的问题。

图 10.6 未进化的随机网络的输出
在这一点上,跳到笔记本的底部并查看以下列表中的进化代码。大部分代码已经介绍过,但请注意添加了CustomReporter到population,p,使用add_reporter函数调用。添加这个自定义报告允许我们微调进化输出,并允许我们添加可视化。
列表 10.12 EDL_10_3_NEAT_Circles.ipynb:进化网络
p = neat.Population(config) ❶
p.add_reporter(CustomReporter(False)) ❷
winner = p.run(eval_genomes) ❸
print('\nBest genome:\n{!s}'.format(winner)) ❹
print('\nOutput:') ❹
winner_net = neat.nn.FeedForwardNetwork.create(winner, config)
show_predictions(winner_net, X, Y)
draw_net(config, winner, True, node_names=node_names) ❹
❶ 创建人口
❷ 添加 CustomReporter 进行可视化
❸ 评估胜者
❹ 输出结果
滚动到CustomReporter类定义,如列表 10.13 所示。NEAT-Python 允许进行各种自定义,而这个实现只是标准报告器的副本,增加了一些用于可视化拟合进度的代码。在这个新的报告器类中,我们在post_evaluate函数中添加了自定义代码,该函数在genomes被evaluated后调用。我们不希望这段代码在每次迭代时都渲染,因此我们添加了一个模检查,该检查由在init函数中设置的新的self.gen_display参数控制。如果generation等于显示的 gen,则代码从genome创建一个网络,并在更新show_predictions函数中对其进行evaluate。
列表 10.13 EDL_10_3_NEAT_Circles.ipynb:CustomReporter类定义
from neat.math_util import mean, stdev
class CustomReporter(neat.reporting.BaseReporter): ❶
"Uses 'print' to output information about the run; an example
reporter class."
def __init__(self, show_species_detail,
➥ gen_display=100): ❷
#omitted
def post_evaluate(self, config, population, species,
➥ best_genome): ❸
#omitted
if (self.generation) % self.gen_display == 0 : ❹
net = neat.nn.FeedForwardNetwork.create(best_genome, config)
show_predictions(net, X, Y)
time.sleep(5)
❶ 类定义继承自 BaseReporter。
❷ 初始化函数,仅供参考
❸ 添加到 post_evaluate
❹ 自定义代码的开始
回想第六章中我们如何首先在 Keras 网络上使用show_predictions函数。在 NEAT 上使用此函数已经更新,如下面的列表所示。与前一段代码的主要变化是使用net.activate函数而不是 Keras 中的model.predict。
列表 10.14 EDL_10_3_NEAT_Circles.ipynb:更新的show_predictions函数
def show_predictions(net, X, Y, name=""):
x_min, x_max = X[ :, 0].min() – 1, X[ :, 0].max() + 1 ❶
y_min, y_max = X[:, 1].min() – 1, X[:, 1].max() + 1 ❶
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.01), ❶
➥ np.arange(y_min, y_max, 0.01)) ❶
X_temp = np.c_[xx.flatten(), yy.flatten()] ❶
Z = []
for x in X_temp:
Z.append(net.activate(x)) ❷
Z = np.array(Z) ❷
plt.figure("Predictions " + name)
plt.contourf(xx, yy, Z.reshape(xx.shape),
➥ cmap=plt.cm.Spectral) ❸
plt.ylabel('x2')
plt.xlabel('x1')
plt.scatter(X[:, 0], X[:, 1],c=Y, s=40, cmap=plt.cm.Spectral)
plt.show() ❹
❶ 创建输入和输出的网格
❷ 激活网络并输出结果
❸ 使用光谱图绘制结果
❹ 显示输出
图 10.7 显示了在月亮问题数据集上进化 NEAT 网络的结果。注意,网络中的大多数节点都没有输出到输出节点。你的结果和网络可能会有所不同,但在图中,你可以看到只有两个节点是相关的,并且连接到输出节点。

图 10.7 进化 NEAT 网络在月亮问题上的输出
如果你还记得,在第六章中,我们最困难的问题是圆的问题。请将问题切换到圆,然后再次运行笔记本。根据经验,我们知道这个问题可以使用标准的 Keras 网络解决。然而,鉴于我们当前的配置选项,找到解决方案的可能性不大。
10.3.1 学习练习
使用以下练习来探索 NEAT 的能力:
-
更改图 10.5 中的数据样本数量。观察这对 NEAT 近似的影响。
-
更改图 10.5 中的问题类型,然后重新运行笔记本。NEAT 是否比其他问题处理得更好?
-
在列表 10.11 中增加或减少隐藏节点 (
num_hidden) 的数量。然后尝试解决各种问题类型。隐藏节点的数量对构建解决方案有什么影响?
在我们正确解决圆的问题之前,我们将在下一节中深入探讨使用 NEAT 的一个更实际的例子。
10.4 练习 NEAT 进行图像分类
为了真正理解 NEAT 的局限性和能力,我们在本节提供了一个实际的比较。一个著名的例子是:使用 MNIST 手写数字数据集进行图像分类。对于我们的下一个练习,我们使用 NEAT 来分类 MNIST 数据集。
在 Google Colab 中打开 EDL_10_4_NEAT_Images.ipynb 笔记本。如需帮助,请参考附录。通过菜单选择运行 > 运行所有来运行笔记本中的所有单元格。
这个笔记本加载了 MNIST 数据集,如图 10.8 所示。我们只使用数据集的训练数据部分来 evaluate 批量样本上的 genome fitness。数据加载后,它被归一化,然后显示一个样本数字。请注意,我们正在使用完整数据集的所有 10 个类别。

图 10.8 加载 MNIST 训练数据集
接下来,我们查看 NEAT 配置选项的各种更改,如列表 10.15 所示。第一个更改是将 fitness 阈值设置为 .25,即 25%。我们将更新 fitness 函数,以评分演化的网络在准确性而不是错误上的表现。然后,注意输入已增加到 784,以匹配输入图像的 28×28 像素,这与 Keras 模型没有区别。在这个练习中,我们为了演示将隐藏节点的数量设置为 10。之后,我们将 initial_connection 选项更改为 full_direct。这实际上意味着我们从一个完全连接的网络开始,这与 Keras 顺序模型没有太大区别。在显示的配置选项底部附近,我们可以看到为 activation 函数、identity 和 relu 设置的选项。最后,我们看到一个新的聚合选项正在被采用。聚合是在节点或感知器内部发生的操作,默认情况下,我们总是假设它是求和。在 NEAT 中,我们可以更改节点使用的聚合函数,就像我们在这里做的那样。
列表 10.15 EDL_10_4_NEAT_Images.ipynb:更新配置选项
[NEAT]
fitness_criterion = max
fitness_threshold = .25 ❶
pop_size = 100
reset_on_extinction = 1
[DefaultGenome]
num_inputs = 784 ❷
num_hidden = 10 ❸
num_outputs = 10
initial_connection = full_direct ❹
feed_forward = True
compatibility_disjoint_coefficient = 1.0
compatibility_weight_coefficient = 0.6
conn_add_prob = 0.2
conn_delete_prob = 0.2
node_add_prob = 0.2
node_delete_prob = 0.2
activation_default = relu
activation_options = identity relu ❺
activation_mutate_rate = 0.0
aggregation_default = sum
aggregation_options = sum mean product min max
➥ median ❻
aggregation_mutate_rate = 0.2 ❼
❶ Fitness现在是准确率。
❷ 展平的图像输入
❸ 中间节点的最大数量
❹ 总是开始完全连接
❺ 激活函数的选择
❻ 修改节点聚合函数
❼ 总是开始完全连接
从配置中,我们跳转到更新evaluation函数,如列表 10.16 所示。回想一下,我们现在想使用准确率来评分fitness,因为我们的网络将用于图像分类。这意味着我们想在图像集——通常是训练集——上评分网络。然而,评分整个训练图像集是不切实际的,所以我们取随机批次的图像来evaluate一个genome。在这个笔记本中,我们使用256的值来加速性能。这个批次大小用于生成一个随机索引集,该索引集将用于从训练集X和Y中提取数据。
列表 10.16 EDL_10_4_NEAT_Images.ipynb:随机批处理图像
BATCH_SIZE = 256 ❶
idx = np.random.randint(0, X.shape[0], BATCH_SIZE) ❷
xs, ys = X[idx], Y[idx] ❸
❶ 设置常数
❷ 抽取随机索引
❸ 从原始数据中提取批次
在评估图像批次和标签提取之后,我们可以根据准确率评估genome网络,如列表 10.17 所示。随着代码遍历批次中的每个图像和标签,它首先将 2D 28×28 图像展平为 784 个输入。从那里,它激活网络并应用SoftMax和np.argmax函数来获取预测类别。类别预测被收集在yis变量中,并随后使用balanced_accuracy_score函数提取平衡准确率分数。关于平衡准确率的详细解释可以在优秀的 SciKit Learn 文档页面上找到,该页面涵盖了损失和指标类型:mng.bz/Q8G4。总之,平衡准确率平衡了来自不平衡数据集的预测。由于我们用于评估的批次数据是随机的,我们不能假设预测将是平衡的。使用平衡准确率可以使评估克服任何偏差。
列表 10.17 EDL_10_4_NEAT_Images.ipynb:评估genome fitness
from sklearn.metrics import balanced_accuracy_score
yis = []
for x, y in zip(xs,ys): ❶
x = np.reshape(x, (784,)) ❷
output = net.activate(x) ❸
class_ = softmax(output) ❸
yis.append(np.argmax(class_)) ❸
print(ys, yis)
fitness = balanced_accuracy_score(ys, yis) ❹
print(fitness)
❶ 遍历批次数据
❷ 展平图像
❸ 激活并按类别评分输出
❹ 评估基因平衡准确率
滚动到下一个单元,如下面的列表所示,我们可以看到 NEAT 进化将使用的完成评估函数。代码与我们刚才审查的相同,但它展示了它在genome集评估函数中的应用。
列表 10.18 EDL_10_4_NEAT_Images.ipynb:evaluate fitness函数
def eval_genomes(genomes, config):
for genome_id, genome in genomes:
idx = np.random.randint(0, X.shape[0], BATCH_SIZE) ❶
xs, ys = X[idx], Y[idx] ❶
net = neat.nn.FeedForwardNetwork.create(genome, config)
score = 0
yis = []
for x, y in zip(xs,ys):
x = np.reshape(x, (784,)) ❷
output = net.activate(x)
output = softmax(output)
class_ = np.argmax(output) ❸
yis.append(class_)
genome.fitness = fitness = balanced_accuracy_score(ys, yis) ❹
❶ 提取随机批次数据
❷ 展平图像
❸ 获取预测类别
❹ 评估所有预测
运行进化的代码与之前显示的代码相同。即使添加了一些性能调整(例如,设置批量大小),运行此代码以达到 25%的准确率也需要一些时间。这是当前 NEAT-Python 实现的一个不幸后果。DEAP,我们在之前的例子中将其包装在 Keras/PyTorch 周围,提供了分布式计算选项。NEAT 作为一个更老的框架,没有这个选项。
图 10.9 展示了训练到 25%准确率的网络的预测准确性。此图是从之前笔记本中显示的plot_classify函数生成的。如您所见,考虑到评估是在训练集上进行的,结果是可以接受的。您的结果可能不太准确,并且将很大程度上取决于您设置的批量大小。较大的批量大小可以提高准确性,但也会将进化时间延长到几分钟甚至几小时。

图 10.9 获胜网络预测结果
最后,这个笔记本中的最后一块代码使用我们之前使用的draw_net函数绘制了获胜的genomes网络。遗憾的是,由于连接过多,这个网络的输出无法阅读。在大多数情况下,具有所选配置选项的进化网络有 10,000 个或更多的连接——是的,您读对了。
因此,鉴于这个图像分类笔记本表现不佳,使用 NEAT 这样的框架有什么好处呢?嗯,与之前章节中的一些进化示例一样,NEAT 在封闭形式、较小的数据集优化问题上表现最佳。正如稍后讨论的那样,这并不意味着进化拓扑不是 EDL 的一个可行应用,但这是一个需要更多微调的应用。
10.4.1 学习练习
使用以下练习来进一步测试 NEAT 的极限:
-
将目标数据集从 MNIST 手写数字更改为 Fashion-MNIST。模型的表现是否有所改善?
-
在列表 10.15 中增加或减少隐藏节点(
num_hidden)的数量,然后重新运行笔记本。 -
在列表 10.15 中调整超参数,尝试提高结果。您能将进化网络分类数字或时尚的程度如何?
我们在本书的最后一章探索另一个进化拓扑框架。然而,现在我们将在下一节中查看 NEAT 的一个有趣特性,该特性可以改善进化,称为speciation。
10.5 揭示拓扑进化中物种的作用
在下一个笔记本中,我们将探讨 NEAT 如何使用一个名为speciation的功能来跟踪population多样性。Speciation起源于生物学,是一种描述相似生物如何进化出独特特征以成为不同物种的方式。物种的概念,首先由达尔文提出,是一种描述或分解地球生命进化方式的方法。

图 10.10 狗品种的分类学
图 10.10 展示了生物学家如何在分类学图表中识别狗的物种和亚种。分类学 是生物学家用来展示或分类地球上生命进化的工具。在图的最上方,一个狗亚种被识别出来,展示了生物学家如何定义常见的家犬。
NEAT 使用将 基因组 分组到物种的概念来进行优化和多样化。将 基因组 分组到物种突出了如何使一个多样化的 种群 网络进化。如果你回忆起前面的章节,我们通常希望保持 种群 的多样化,以避免陷入某些局部极大值或极小值的陷阱。
不鼓励多样性往往会导致一个进化的 种群 过度专业化或固定在某个局部极小值/极大值。在现实世界中,过度专业化且无法适应的有机体会因为不断的变化而灭绝。
演化偏差
在我们的世界中,我们通常将物种灭绝纯粹视为一个坏事件。这当然是因为我们,作为人类,现在能够识别我们自己的行动在全世界数千种物种持续灭绝中的作用。然而,如果没有人类的干预,灭绝是一个生命在地球上经过数十亿年的自然过程。在进化计算中,灭绝也可以是一件好事,因为它鼓励多样性和更好的个体表现。
NEAT 还使用灭绝的概念来迫使物种不断进化或灭绝。这样做可以防止物种停滞或过度专业化,并鼓励 种群 多样化。在下一节中,我们将探讨使用 物种形成 如何帮助 NEAT 解决复杂问题。
10.5.1 调整 NEAT 物种形成
下一笔记本回顾了我们在前一个笔记本中查看的圆圈问题集。这次,我们看看对笔记本进行一些小的改进,以及 NEAT 的 物种形成 功能如何有所帮助。我们还探索了更多 NEAT 配置选项,其中有很多。
在 Google Colab 中打开 EDL_10_5_NEAT_Speciation.ipynb 笔记本。如有需要,请参考附录。通过选择菜单中的“运行”>“运行所有”来运行笔记本中的所有单元格。
NEAT-Python 主要由配置选项驱动,这些选项可以控制 基因组 进化的各个方面,包括节点连接、节点、激活/聚合函数和权重。所有这些选项都赋予了 NEAT 很大的能力,但它们也使得在复杂问题上进化网络变得更加困难。为了解决圆圈问题,我们需要更好地理解这些配置选项是如何相互作用的。
滚动到 NEAT 配置选项部分,如列表 10.19 所示。对于这个笔记本,我们已更新 fitness 函数以产生最大 fitness 为 1.0。因此,我们也更新了 fitness_threshold。中间节点的数量也增加到 25,以允许网络拓扑有更多的增长空间。根据经验,我们知道在简单的几层架构下,圆的问题是可以解决的。为了减少网络内部拓扑变化的数量,我们大大减少了连接和节点的添加或删除的可能性。
列表 10.19 EDL_10_5_NEAT_Speciation.ipynb:NEAT 配置选项
[NEAT]
fitness_criterion = max
fitness_threshold = .85 ❶
pop_size = 100
reset_on_extinction = 1
[DefaultGenome]
num_inputs = 2
num_hidden = 25 ❷
num_outputs = 1
initial_connection = partial_direct 0.5
feed_forward = True
compatibility_disjoint_coefficient = 1.0
compatibility_weight_coefficient = 0.6
conn_add_prob = 0.02 ❸
conn_delete_prob = 0.02 ❸
node_add_prob = 0.02 ❹
node_delete_prob = 0.02 ❹
❶ 修订后的 fitness 函数替换了阈值。
❷ 增加中间层
❸ 降低连接变化率
❹ 降低节点变化率
由于我们知道圆的问题可以通过仅改变权重来解决,所以我们专注于最小化权重变化。这里的想法是允许 genome 逐渐适应并缓慢调整权重。这与我们在训练深度学习网络时降低学习率的方式类似。在文件底部,我们还更新了两个选项以更好地控制 speciation,如列表 10.20 所示。第一个选项是 compatibility_threshold,它控制物种之间的距离——我们将在下一分钟看到这意味着什么。第二个是 max_stagnation,它控制等待检查物种灭绝之前的 generations 数量。
列表 10.20 EDL_10_5_NEAT_Speciation.ipynb:更多 NEAT 配置选项
weight_max_value = 30
weight_min_value = -30
weight_init_mean = 0.0
weight_init_stdev = 1.0
weight_mutate_rate = 0.08 ❶
weight_replace_rate = 0.01 ❷
weight_mutate_power = 0.1 ❸
enabled_default = True
enabled_mutate_rate = 0.01
[DefaultSpeciesSet]
compatibility_threshold = 1.0 ❹
[DefaultStagnation]
species_fitness_func = max
max_stagnation = 25 ❺
❶ 降低权重突变的可能性
❷ 降低权重替换的可能性
❸ 降低权重突变的数量
❹ 降低物种间的兼容性
❺ 增加物种停滞代数
接下来,我们看看 fitness 评估函数是如何更新的,以更好地评估二分类问题。如果你还记得,我们之前使用 MSE 进行 fitness 评估。这次,我们对此进行了一些修改,以更好地考虑错误的类别分类。我们可以使用类似二元交叉熵的函数来计算这里的错误,但相反,我们使用了一种更简单的方法来计算预期类别与网络输出之间的距离。因此,如果预期类别是 0 且网络输出 .9,则误差是 -.9。同样,如果类别是 1 且网络输出 .2,则误差是 .8。将误差平方并附加到结果中消除了符号,并允许我们使用 np.mean 在以后提取平均误差。然后,通过从最大 fitness(现在为 1)中减去平均/均值误差来计算总 fitness,如下面的列表所示。
列表 10.21 EDL_10_5_NEAT_Speciation.ipynb:更新 fitness 评估
results = []
for x, y in zip(X,Y):
yi = net.activate(x)[0] ❶
if y < .5: ❷
error = yi – y ❷
else:
error = y – yi ❸
print(yi, error)
results.append(error*error) ❹
fitness = 1 - np.mean(results) ❺
print(fitness)
❶ 预测介于 0 和 1 之间的类别值
❷ 计算类别 0 的误差
❸ 计算类别 1 的误差
❹ 添加平方误差
❺ 最大 fitness(1) - 平均误差
当笔记本正在运行时,向下滚动到进化代码并查看结果。图 10.11 显示了网络在几代内的进化结果。在图的左侧,进化初期,NEAT 只跟踪三个网络(组)的物种。每个物种中的个体数量由我们之前看到的compatibility_threshold选项控制。兼容性是网络之间相似性的度量,可能因连接数、连接权重、节点等因素而不同。降低兼容性阈值会创建更多物种,因为网络之间兼容性/相似性的差异很小。同样,提高这个阈值会减少物种数量。

图 10.11 在圆上训练 NEAT 的结果
NEAT 跟踪每个“物种形成”在整个进化过程中的历史。max_stagnation选项控制等待多少代来评估特定物种的进度。停滞期过后,物种将根据进度变化或进步进行评估。如果在这个时候,一个物种在停滞期内没有变化,它就会灭绝并被从种群中移除。在图 10.11 的右侧,所有物种都被标记为灭绝。这是由于物种停滞,没有明显的改进fitness的变化。实际上,当前获胜的genome的结果看起来相对较好,所以停滞期可能太短了。
现在轮到你了,回到前面去探索各种配置选项,看看你是否能用大于.95的fitness解决圆的问题。前往并修改配置选项文件,并在每次更改后,从菜单中选择运行 > 运行全部来重新运行整个笔记本。
前往并查阅 NEAT-Python 配置选项文档,可在此处找到:mng.bz/X58E。此文档提供了关于许多可用选项的更多见解和细节。
“物种形成”不仅为种群提供了更多的多样性,还展示了进化网络如何变得停滞或陷入困境。NEAT 成功解决复杂问题的关键在于平衡配置选项的调整。幸运的是,就像任何事情一样,你越是用 NEAT 工作,你就越会了解如何调整这些旋钮以获得更好的结果。
10.5.2 学习练习
使用以下练习来提高你对 NEAT“物种形成”的理解:
-
在列表 10.20 中增加或减少
compatibility_threshold的值,然后重新运行笔记本以查看这对物种数量有何影响。 -
在列表 10.20 中增加或减少最大“停滞”代数(
max_stagnation)的数量,然后重新运行笔记本以查看结果。 -
增加或减少
种群大小,看看这可能会对物种形成产生什么影响。当你使用非常小的种群时会发生什么?
在下一章中,我们将花更多的时间研究 NEAT(NEAT-Python),并探索更多有趣和有趣的问题来解决。
摘要
-
神经进化拓扑增强(NEAT)是一个进化框架,它采用了从超参数优化到进化网络架构的多种技术。NEAT-Python 是一个优秀的框架,它将这些复杂性封装成一个简单的配置驱动解决方案。NEAT 是深度学习网络的一种进化自适应性架构。
-
NEAT 在
代际中适应和进化节点权重和网络架构。随着拓扑结构的变化可视化并检查 NEAT 网络,可以有助于理解框架的工作原理。 -
NEAT 可以解决各种问题,如不连续函数逼近以及其他复杂的分类问题,以及其他难以解决的封闭形式问题。
-
NEAT 可以用于执行图像分类任务,并取得一些有限的结果。
-
物种形成是一个进化术语,指的是根据相似特征将个体分类或分组成类或子集。NEAT 使用物种形成来评估可能已经停滞的个体(物种)组的性能。停滞的物种随后可以被淘汰,并从种群池中移除。灭绝允许在固定的种群内建立新的个体群体。
11 使用 NEAT 进行进化学习
本章涵盖
-
强化学习的介绍
-
探索 OpenAI Gym 中的复杂问题
-
使用 NEAT 作为代理解决强化学习问题
-
使用 NEAT 代理解决 Gym 的月球着陆器问题
-
使用深度 Q 网络解决 Gym 的月球着陆器问题
在上一章中,我们探讨了神经进化拓扑增强(NEAT)来解决我们在前几章中探讨的常见问题。在这一章中,我们研究学习的进化。首先,我们使用 NEAT 开发一个可以解决通常与强化学习相关问题的进化代理。然后,我们研究更困难的强化学习问题,并提供一个用于进化学习的 NEAT 解决方案。最后,我们通过使用称为 本能学习 的心理模型来探讨我们对学习本身的理解需要如何进化。
11.1 强化学习的介绍
强化 学习 (RL) 是一种基于动物行为和心理学的学习形式,试图复制生物体通过奖励来学习的方式。如果你曾经用某种形式的奖励,比如零食或表扬,来训练宠物做一个小把戏,那么你就理解了这个前提。许多人认为,理解高级意识和我们学习的基础是通过强化学习来建模的。
图 11.1a 展示了本书中涵盖的三种学习形式的比较:监督学习、代表性学习(生成建模)和强化学习。这三种学习类型都有所变化,从自监督学习到深度强化学习。

图 11.1a 不同形式学习的比较
强化学习通过让学习者,或称为 代理,观察环境的 state 来工作。这种对环境的 observation 或 view 通常被称为当前 state。代理消耗观察到的 state 并基于此 state 进行预测,或执行 action。然后,基于该 action,环境根据给定 state 的 action 提供奖励。
这个观察环境和代理执行 actions 的过程会一直持续到代理解决问题或失败为止。代理或学习者通过环境促进的奖励积累来学习,其中对于给定 state 通常产生更高奖励的 action 被认为更有价值。
强化学习代理的学习过程通常涉及代理开始时随机执行 actions。代理使用随机 actions 来探测环境,以找到那些产生最多奖励的 actions 或 actions 序列。这种学习被称为 尝试错误 或 蛮力。
尝试错误学习与执行功能
强化学习(RL)的一个关键批评是其使用试错法或蛮力学习。这种学习形式是重复的,并且可能极其昂贵。通常,它需要代理执行数百万次的动作来解决一个问题。虽然人类和其他动物往往以类似的方式学习任务,但我们很少需要数百万次迭代才能在某件事上变得擅长。
执行功能(EF)是大脑学习机制的过程,它使我们能够规划和完成复杂任务。虽然强化学习可以在代理中模拟执行功能,但其机制与 RL 有显著不同。EF 使我们能够查看我们以前从未完成过的复杂任务,并规划一系列动作以成功完成这些任务。目前正在进行研究,通过各种技术,包括进化优化,将执行功能提升到 RL 代理中。
虽然强化学习并非没有缺陷,但它确实提供了一种机制,使我们能够解决我们从未考虑过使用监督学习或对抗学习的复杂问题。如果允许,RL 还允许代理与环境交互并对其进行更改,从而产生更加复杂的问题。为了理解 RL 是如何工作的,我们将在下一节中查看并解决一个一阶问题。
11.1.1 冻结湖上的 Q 学习代理
现代强化学习是三种算法路径的结合:试错法、动态规划和蒙特卡洛模拟。在此基础上,1996 年由 Chris Watkins 推导出一种称为Q 学习的强化学习形式。自那时起,Q 学习已成为 RL 的基础概念,通常作为第一步教授给学生。
Q 学习通过给代理提供量化已知状态下给定动作质量的能力来实现,如图 11.1b 所示。通过能够衡量给定动作的质量,代理可以轻松选择正确的动作序列来解决给定的问题。代理仍然需要通过试错法摸索,探索环境以推导出这些动作或状态的质量。

图 11.1b 强化学习
为了了解这在实践中是如何工作的,我们首先构建一个 Q 学习代理,它可以解决来自 OpenAI Gym 的基本问题,称为冻结湖问题。OpenAI Gym([www.gymlibrary.dev/](https://www.gymlibrary.dev/))是一个开源项目,封装了数百个不同的问题环境。这些环境从经典的 Atari 游戏到基本的控制问题都有。
图 11.2 展示了我们为 Q 代理解决而开发的冻结湖环境的示意图。环境是一个由四个方格组成的 4x4 网格,代表一个冻结的湖,其中湖的某些区域被冻结得非常坚固且安全,可以穿越。湖的其他区域是不稳定的,有洞,会导致代理掉入并死亡,从而结束他们的旅程或阶段。

图 11.2 冻结湖环境
冰冻湖问题的目标是让智能体在网格中移动,使用 actions 中的 up、down、left 或 right 操作来穿越。当智能体到达右下角时,任务完成并获得奖励。如果智能体掉入湖中的洞里,旅程结束,智能体将获得负奖励。
幸运的是,通过 OpenAI Gym 的发展,构建强化学习智能体及其测试环境变得更加容易。我们将在下一笔记本中深入探讨如何加载环境和编写一个工作的 Q 智能体。
在 Google Colab 中打开 EDL_11_1_FrozenLake.ipynb 笔记本。如需帮助,请参阅附录。通过选择菜单中的“运行”>“运行所有”来运行笔记本中的所有单元格。在下面的列表中,我们首先关注的是 OpenAI Gym 的安装和导入。
列表 11.1 EDL_11_1_FrozenLake.ipynb:安装 OpenAI Gym
!pip install gym ❶
import numpy as np
import gym ❷
import random
❶ 安装基本 Gym
❷ 导入包
之后,我们将查看如何使用 Gym 创建环境。有数百个环境可供选择,要创建一个环境,只需将名称传递给 gym.make 函数,如列表 11.2 所示。然后,我们查询环境以获取 action 和 state 空间的大小;这些数字表示有多少个离散值可用。冰冻湖环境使用离散值来表示 action 和 state 空间。在许多环境中,我们使用连续或范围值来表示 action、state 或两者空间。
列表 11.2 EDL_11_1_FrozenLake.ipynb:创建环境
env = gym.make("FrozenLake-v0") ❶
action_size = env.action_space.n ❷
state_size = env.observation_space.n ❸
print(action_size, state_size)
❶ 创建环境
❷ 获取动作空间的大小
❸ 获取状态空间的大小
在 Q 学习中,智能体或学习者将其知识或学习封装在一个称为 Q 表 的表中。这个表的维度、列和行由 state 和可用的 actions 定义。代码中的下一步,如列表 11.3 所示,是创建这个表来表示智能体的知识。我们使用 np.zeros 创建一个由 action_size 和 state_size 值大小的数组。结果是包含值的数组(表),其中每一行代表 state,每一行上的列代表在该 state 上的 action 的质量。
列表 11.3 EDL_11_1_FrozenLake.ipynb:构建 Q 表
Q = np.zeros((state_size, action_size)) ❶
print(Q)
#========== printed Q table
[[0\. 0\. 0\. 0.] ❷
[0\. 0\. 0\. 0.] ❸
[0\. 0\. 0\. 0.]
[0\. 0\. 0\. 0.]
[0\. 0\. 0\. 0.]
[0\. 0\. 0\. 0.]
[0\. 0\. 0\. 0.]
[0\. 0\. 0\. 0.]
[0\. 0\. 0\. 0.]
[0\. 0\. 0\. 0.]
[0\. 0\. 0\. 0.]
[0\. 0\. 0\. 0.]
[0\. 0\. 0\. 0.]
[0\. 0\. 0\. 0.]
[0\. 0\. 0\. 0.]
[0\. 0\. 0\. 0.]]
❶ 创建一个值为零的数组
❷ 第一行和第一个状态
❸ 每行四个动作
接下来,我们来看一组 Q 学习者的标准超参数,如列表 11.4 所示。代理在湖上进行的每次旅行被定义为一场游戏。total_episodes超参数设置了代理将进行的总游戏数或旅行次数,而max_steps值定义了代理在单次旅行中可以采取的最大步数。还有两个其他值也被使用:learning_rate,类似于 DL 中的学习率,以及gamma,它是一个控制未来奖励对代理重要性的折扣因子。最后,底部的超参数组控制代理的探索。
列表 11.4 EDL_11_1_FrozenLake.ipynb:定义超参数
total_episodes = 20000 ❶
learning_rate = 0.7 ❷
max_steps = 99 ❸
gamma = 0.95 ❹
epsilon = 1.0 ❺
max_epsilon = 1.0 ❺
min_epsilon = 0.01 ❺
decay_rate = 0.005 ❺
❶ 训练尝试的总数
❷ 代理学习速度有多快
❸ 一场游戏中的最大步数
❹ 未来奖励的折扣率
❺ 控制代理探索
Q 学习中的一个基本概念是探索与利用之间的权衡,或者使用代理获得的知识。当代理最初开始训练时,其知识较低,在 Q 表中表示为所有零。在没有知识的情况下,代理通常会依赖于随机选择的动作。然后,随着知识的增加,代理可以开始使用 Q 表中的值来确定下一个最佳动作。不幸的是,如果代理的知识不完整,总是选择最佳动作可能会导致灾难。因此,我们引入了一个名为epsilon的超参数来控制代理探索的频率。
我们可以通过查看列表 11.5 中显示的choose_action函数来了解这种探索和利用是如何工作的。在这个函数中,生成一个随机均匀值并与epsilon进行比较。如果值小于epsilon,代理将从action空间中随机选择一个动作并返回它。否则,代理将选择当前状态的最大质量动作并返回。随着代理在环境中训练,epsilon值将随着时间的推移减少或衰减,以表示代理知识的积累和减少探索的倾向。
列表 11.5 EDL_11_1_FrozenLake.ipynb:选择 动作
def choose_action(state):
if random.uniform(0, 1) > epsilon: ❶
return np.argmax(Q[state,:]) ❷
else:
return env.action_space.sample() ❸
❶ 随机动作的概率,探索
❷ 为给定状态选择最大动作
❸ 随机采样一个动作
代理通过 Q 函数计算的知识积累来学习。Q 函数中的术语代表当前的 Q-质量值、奖励以及应用折扣因子 gamma。这种学习方法封装在learn函数中,该函数应用了以下列表中显示的 Q 学习函数。我们在这里不深入探讨这个函数,因为我们的重点是 NEAT 如何在不使用 Q 函数的情况下解决相同的问题。
列表 11.6 EDL_11_1_FrozenLake.ipynb:learn函数
def learn(reward, state, action, new_state):
Q[state, action] = Q[state, action] + learning_rate
➥ * (reward + gamma * np.max(Q[new_state, :]) –
➥ Q[state, action]) ❶
❶ 根据状态/动作计算质量
训练智能体的代码分为两个循环,第一个循环遍历回合,第二个循环遍历每个回合的旅程或步骤。在每一步中,智能体使用choose_action函数选择下一个action,然后通过调用env.step(action)来执行action。调用step函数的输出用于通过调用learn函数更新智能体的知识在 Q 表中。然后,检查确认回合是否完成或不完整,以及智能体是否掉入洞中或到达了终点。随着智能体遍历回合,epsilon值会衰减或减少,这代表着随着时间的推移,智能体探索的需求减少,如下所示。
列表 11.7 EDL_11_1_FrozenLake.ipynb:训练函数
rewards = []
for episode in range(total_episodes):
state = env.reset() ❶
step = 0
done = False
total_rewards = 0
for step in range(max_steps):
action = choose_action(state) ❷
new_state, reward, done, info = env.step(action) ❷
learn(reward, state, action, new_state) ❸
total_rewards += reward
state = new_state
if done == True: ❹
break
epsilon = min_epsilon ❺
+ (max_epsilon - min_epsilon)*np.exp(-decay_rate*episode)
rewards.append(total_rewards)
print ("Score over time: " + str(sum(rewards)/total_episodes))
print(Q)
❶ 重置环境
❷ 选择动作并在环境中执行
❸ 学习并更新 Q 表
❹ 如果完成并且回合结束,则中断
❺ 随时间衰减 epsilon 探索
在这个例子中,我们训练智能体在一定的次数中运行,而不考虑性能的改进。在智能体训练完成后,我们通过在环境中运行模拟的智能体来测试其知识,如下所示。
列表 11.8 EDL_11_1_FrozenLake.ipynb:训练智能体
for episode in range(5): ❶
state = env.reset()
step = 0
done = False
print("****************************************************")
print("EPISODE ", episode)
for step in range(max_steps):
action = np.argmax(Q[state,:]) ❷
new_state, reward, done, info = env.step(action) ❷
if done:
env.render() ❸
if new_state == 15:
print("Goal reached ")
else:
print("Aaaah ☠️")
print("Number of steps", step)
break
state = new_state
env.close()
❶ 遍历 5 个回合
❷ 执行状态的最大最佳动作
❸ 渲染环境
图 11.3 显示了运行训练智能体五个回合的输出。从结果中,你可以看到智能体是如何在最大允许步骤(99 步)内一般解决环境的。如果你想尝试改进智能体解决环境的一致性速度,尝试修改超参数,然后再次运行笔记本。下一节将展示一些有助于提高你对强化学习(RL)知识的练习。

图 11.3 模拟智能体在冰面上的输出
11.1.2 学习练习
使用这些练习来提高你对主题材料的了解:
-
从列表 11.4 中更改
learning_rate和gamma超参数。观察它们对智能体学习的影响。 -
从列表 11.4 中更改探索衰减率
decay_rate。观察这对智能体训练的影响。 -
修改训练的
EPISODES数量。观察这对智能体性能的影响。
当然,在这个阶段,我们可以编写一个进化优化器来调整超参数,就像我们之前所做的那样。然而,使用 NEAT,我们可以做得更好,实际上可以替代使用强化学习(RL)来解决这类问题。不过,在我们达到这一点之前,我们将在下一节中查看如何加载更复杂的 OpenAI Gym 环境。
11.2 探索 OpenAI Gym 中的复杂问题
OpenAI Gym 提供了大量的训练环境,旨在提高强化学习(RL)。在我们将 NEAT 应用于这些环境之前,我们需要做一些额外的准备工作来使用 Colab。在本章和下一章中,我们将探索以下列表中描述的各种 Gym 环境:
-
山车——这里的目的是将一辆从两个山丘的谷地开始的汽车开到目标山丘的顶部。为此,汽车需要来回摇晃,直到获得足够的动力爬到更高的山丘顶部。
-
摆锤——这个问题的目标是施加力量于摆动的摆臂,使其保持直立位置。这需要根据摆锤的位置知道何时施加力量。
-
小车和杆——这个经典的 Gym 问题要求在移动的货车上平衡一个杆。同样,这需要代理/模型平衡货车的位置和速度。
-
月球着陆器——从旧视频游戏中复制而来,月球着陆器的目标是将月球着陆器平稳地降落在平坦的着陆面上。关键是飞行器的着陆速度必须足够低,以避免损坏着陆器并失败。
在下一本快速笔记中,我们将设置并探索上述列表中的各种 Gym 环境。
在 Google Colab 中打开 EDL_11_2_Gym_Setup.ipynb 笔记本。如需帮助,请参阅附录。通过选择菜单中的“运行”>“运行所有”来运行笔记本中的所有单元格。
由于 Colab 是服务器端笔记本,通常不需要提供 UI 输出。为了渲染一些更美观的 Gym 环境,我们必须安装一些虚拟界面驱动程序和相关辅助工具,如下面的列表所示。我们还安装了一些工具来渲染我们的环境输出并回放为视频,这使得我们的实验更加有趣。
列表 11.9 EDL_11_2_Gym_Setup.ipynb:安装所需软件包
!apt-get install -y xvfb x11-utils ❶
!pip install gym[box2d]==0.17.* \ ❷
pyvirtualdisplay==0.2.* \ ❸
PyOpenGL==3.1.* \ ❸
PyOpenGL-accelerate==3.1.* \ ❸
mediapy \ ❹
piglet -q) ❺
❶ 安装视频设备驱动程序
❷ 安装带有 box2d 的 Gym
❸ 图形辅助工具
❹ 用于播放视频/媒体
❺ 模板引擎
我们需要创建一个虚拟显示并启动它。实现这一点的代码只需要几行简单代码,如下面的列表所示。
列表 11.10 EDL_11_2_Gym_Setup.ipynb:创建虚拟显示
from pyvirtualdisplay import Display
display = Display(visible=0, size=(1400, 900)) ❶
display.start() ❷
❶ 创建虚拟显示
❷ 开始显示
导入之后,我们现在可以创建一个环境并将一个帧渲染到单元格的输出中。这个单元格使用 Colab 表单提供一系列环境选项供选择。我们的目标是能够构建 NEAT 代理/解决方案,以应对每个环境。使用 env.render 函数并传入模式为 rgb_array 以输出 2D 数组,可以可视化环境本身。然后,可以使用 plt.imshow 将此输出渲染,如下面的列表所示。
列表 11.11 EDL_11_2_Gym_Setup.ipynb:创建虚拟显示
#@title Setup Environment { run: "auto" }
ENVIRONMENT = "CartPole-v1" #@param ["CartPole-v1", "Acrobot-v1",
➥ "CubeCrash-v0", "MountainCar-v0", "LunarLander-v2"] ❶
env = gym.make(ENVIRONMENT) ❷
state = env.reset()
plt.imshow(env.render(mode='rgb_array')) ❸
print("action space: {0!r}".format(env.action_space)) ❹
print("observation space: {0!r}".format ❹
➥ (env.observation_space)) ❹
❶ 环境名称列表
❷ 创建环境
❸ 渲染一个帧并绘制它
❹ 打印动作/状态空间
渲染单个帧是可以的,但我们真正想看到的是环境如何运行或播放。我们接下来要查看的下一个单元,如列表 11.12 所示,通过创建一个环境和然后让智能体通过环境来运行,正是这样做的。随着模拟的运行,环境将每一帧渲染到一个列表中。然后,这个帧列表被转换成视频并输出在单元下方。图 11.4 显示了在 Colab 笔记本中渲染到视频输出的月球着陆环境。
列表 11.12 EDL_11_2_Gym_Setup.ipynb:环境的视频渲染
env = gym.make(ENVIRONMENT)
fitnesses = []
frames = []
for run in range(SIMULATION_RUNS): ❶
state = env.reset()
fitness = 0
for I in range(SIMULATION_ITERATIONS): ❷
action = env.action_space.sample() ❸
state, reward, done, info = env.step ❸
➥ (np.argmax(action)) ❸
frames.append(env.render(mode='rgb_array')) ❹
fitness += reward
if done: ❺
fitnesses.append(fitness)
break
mediapy.show_video(frames, fps=30) ❻
❶ 运行 n 次模拟
❷ 每次运行中的最大步数
❸ 执行最大动作
❹ 将渲染的帧追加到列表中
❺ 如果完成,这将停止模拟运行。
❻ 将帧集合渲染为视频

图 11.4 月球着陆环境的视频输出
尝试运行各种其他环境选项,以可视化我们探索的其他可能性。所有这些环境在state空间是连续的,而action空间是离散的方面是相似的。我们通过可能的状态数量来衡量环境的复杂性。
图 11.5 显示了每个环境,action和state空间的大小以及相对复杂性。每个环境的相对复杂性是通过将state空间的大小提高到action空间的幂来计算的,公式如下:
相对复杂性 = size_state_space × size_action_space
以山车问题的一个版本为例,其中state_space = 2 和 action_space = 3。因此,相对复杂性可以表示为以下公式:相对复杂性 = 2³ = 2 × 2 × 2 = 8。

图 11.5 Gym 环境的比较
通常,图 11.5 中显示的 Box2D 环境的子类使用深度强化学习(DRL)来解决。DRL 是 RL 的扩展,它使用深度学习来解决 Q 方程或其他 RL 函数。本质上,深度学习取代了state或 Q 表的需求,使用神经网络。

图 11.6 Q 学习和深度 Q 学习比较
图 11.6 显示了 Q 学习和深度 Q 学习或深度 Q 网络(DQN)之间的比较。这个 DQN 模型已被证明非常灵活,能够解决各种 RL 问题,从经典的 Atari 游戏到小车和月球着陆问题。
深度 Q 学习通过使用我们之前查看的 Q 学习函数作为训练网络的检查或监督器来工作。这意味着,在内部,DQN 模型通过监督训练来学习,这种训练以 Q 学习方程的形式提供。下一节的学习练习可以帮助加强你在本节中学到的内容。
11.2.1 学习练习
完成以下练习,以加深对本节概念的理解:
-
打开并运行笔记本中提供的所有模拟环境。同时熟悉每个环境的
action和observation/state空间。 -
在互联网上搜索并探索其他 Gym 环境,这些环境可能是原始的,也可能是其他人扩展的。有数百个 Gym 环境可以探索。
-
在笔记本中添加一个新的环境,并演示智能体如何在这个新环境中随机玩耍。
自从 DQN(深度 Q 网络)的发展以来,已经出现了许多变体和采用深度学习网络结合强化学习的方法。在所有情况下,学习的基础都是 Q 或其他派生学习方程形式的强化学习。在下一节中,我们将展示如何超越派生的强化学习方程,让解决方案自我演化。
11.3 使用 NEAT 解决强化学习问题
在本节中,我们使用 NEAT(神经进化算法)来解决我们刚刚查看的一些困难的强化学习 Gym 问题。然而,重要的是要强调,我们用来派生网络和解决未知方程的方法不是强化学习,而是进化以及 NEAT。虽然我们确实使用了强化学习环境和以强化学习的方式训练智能体,但底层方法不是强化学习。
使用 NEAT 和一个演化的 NEAT 智能体群体相对简单,正如我们在下一个笔记本中看到的那样。在 Google Colab 中打开 EDL_11_3_NEAT_Gym.ipynb 笔记本。如有需要,请参考附录。通过选择菜单中的“Runtime > Run All”来运行笔记本中的所有单元格。
我们刚刚回顾了设置代码,因此我们可以直接跳到 NEAT 配置。配置与之前我们看到的是相似的,但现在我们定义网络num_inputs等于state或observation空间的大小,而num_outputs等于action空间的大小。这意味着 NEAT 智能体的输入是state/observation,输出是action,如下所示。
列表 11.13 EDL_11_3_NEAT_Gyms.ipynb:NEAT 配置
inputs = env.observation_space.shape[0] ❶
outputs = env.action_space.n ❷
config = f'''
[NEAT]
fitness_criterion = max
fitness_threshold = 175.0 ❸
pop_size = 250
reset_on_extinction = 0
[DefaultGenome]
num_inputs = {inputs} ❹
num_hidden = 1
num_outputs = {outputs} ❺
❶ 状态空间的大小
❷ 动作空间的大小
❸ 定义适应度阈值
❹ 将状态空间映射到输入
❺ 将动作空间映射到输出
接下来,我们回顾我们的测试genome fred,以了解如何评估individual fitness。我们可以看到fred是如何从genome配置中创建的,然后实例化为网络net。这个网络通过传递一个任意的环境state并输出一个action集来测试。为了执行action,使用np.argmax(action)来提取用于调用env.step的action索引,如下所示。
列表 11.14 EDL_11_3_NEAT_Gyms.ipynb:genome和ENVIRONMENT
env = gym.make(ENVIRONMENT) ❶
state = env.reset()
print(state)
key = "fred"
fred = config.genome_type(key) ❷
fred.configure_new(config.genome_config) ❷
net = neat.nn.FeedForwardNetwork.create(fred, config) ❸
action = net.activate(state) ❹
print(action)
state, reward, done, info = env.step(np.argmax(action)) ❺
print(state, reward, done, info)
❶ 创建环境
❷ 配置初始随机基因组
❸ 从基因组构建网络
❹ 输入状态并输出动作
❺ 执行动作
与之前一样,我们可以使用 fred 来推导基础 genome evaluate 函数。代码,如列表 11.15 所示,模仿了我们已经设置的样本视频演示播放代码。不过,这次 genome 的 fitness 是基于奖励的累积来计算的。这意味着——这个微妙的不同很重要——genome 的 fitness 是奖励的总和,但代理在任何时候都没有训练/学习如何消耗或使用这些奖励。进化使用奖励来 evaluate 具有最佳 fitness 的代理——即能够累积最多奖励的代理。
列表 11.15 EDL_11_3_NEAT_Gyms.ipynb:评估 genome fitness
#@title Simulation Options { run: "auto" } ❶
SIMULATION_ITERATIONS = 200 ❶
SIMULATION_RUNS = 10 #@param {type:"slider", min:1, ❶
➥ max:10, step:1} ❶
frames = []
fitnesses = []
for run in range(SIMULATION_RUNS):
state = env.reset()
fitness = 0
for i in range(SIMULATION_ITERATIONS):
action = net.activate(state) ❷
state, reward, done, info = env.step
➥ (np.argmax(action)) ❸
frames.append(env.render(mode='rgb_array'))
fitness += reward ❹
if done:
fitnesses.append(fitness)
break
print(fitnesses)
mediapy.show_video(frames, fps=30) ❺
❶ 模拟参数的 Colab 表单
❷ 将状态传递给网络以激活动作
❸ 在环境中执行步骤
❹ 将奖励添加到 fitness
❺ 重新播放模拟运行
这段简单的代码可以轻松地转换为 eval_genomes/eval_genome 函数集,其中 eval_genomes 是父函数,传递 genomes 的 population,而 individual genome 的评估是通过 eval_genome 来完成的。内部,列表 11.16 中显示的代码与列表 11.15 中我们查看的代码相同,没有视频帧捕获代码。毕竟,我们不需要为每个 genome 模拟捕获视频。
列表 11.16 EDL_11_3_NEAT_Gyms.ipynb:评估 genome fitness
def eval_genome(genome, config):
net = neat.nn.FeedForwardNetwork.create
➥ (genome, config) ❶
fitnesses = []
for run in range(SIMULATION_RUNS):
state = env.reset()
fitness = 0
for I in range(SIMULATION_ITERATIONS):
action = net.activate(state) ❷
state, reward, done, info = env.step ❷
➥ (np.argmax(action)) ❷
fitness += reward
if done:
fitnesses.append(fitness)
break
return -9999.99 if len(fitnesses) <
➥ 1 else min(fitnesses) ❸
def eval_genomes(genomes, config):
for genome_id, genome in genomes: ❹
genome.fitness = eval_genome(genome, config) ❹
print(eval_genome(fred, config)) ❺
❶ 从 genome 创建网络
❷ 将状态传递给网络以激活动作
❸ 返回最小 fitness
❹ 遍历 genome 种群
❺ 在 fred 上测试函数
现在,进化 population 的代码变得非常简单和优雅。创建 pop 并添加默认的 statistics 和 out 报告器以生成进度更新。之后,我们使用一个名为 neat.ParallelEvaluator 的新功能来提供进化的内部多线程处理。在 Colab 的免费版本中,此功能的使用有限,但如果你有一台功能强大的计算机,可以尝试在本地运行此代码以获得更好的性能。最后,最后一行调用 pop.run 来运行进化并产生一个获胜的 genome,如下所示。图 11.7 显示了输出进化的 NEAT 代理 population 以解决 cart pole Gym 环境。

图 11.7 NEAT 代理的输出
列表 11.17 EDL_11_3_NEAT_Gyms.ipynb:进化 population
pop = neat.Population(config) ❶
stats = neat.StatisticsReporter() ❷
pop.add_reporter(stats) ❷
pop.add_reporter(neat.StdOutReporter(True)) ❸
pe = neat.ParallelEvaluator(multiprocessing
➥ .cpu_count(), eval_genome) ❹
winner = pop.run(pe.evaluate) ❺
❶ 创建种群
❷ 使用标准的统计报告器
❸ 添加标准输出报告器
❹ 使用并行执行
❺ 评估最佳 genome
虽然 fitness 与代理的最大奖励相关,但重要的是要强调我们并没有训练 RL 代理。相反,进化的网络正在进化它们自己的内部功能以累积奖励并变得更加适合。fitness 与奖励相关的事实只是一个有用的指标,用于描述 individual 的性能。
11.3.1 学习练习
使用以下附加练习来帮助巩固你对本节内容的理解:
-
使用 NEAT 代理尝试其他环境,看看是否能够以及如何快速地实现解决方案,如果可能的话。
-
修改
SIMULATION_RUNS和SIMULATION_ITERATIONS的数量,然后重新评估 NEAT 代理。 -
修改 NEAT 配置中的隐藏神经元数量
num_hidden,如列表 11.13 所示。查看重新运行各种环境时这一变化的影响。
那么,这种学习或进化被称为什么或如何描述?好吧,我们将在下一章中讨论一组思想和理论。现在,我们想要看看我们是否可以改进 NEAT 代理的进化以解决更困难的问题。
11.4 使用 NEAT 代理解决 Gym 的月球着陆问题
很可能,如果你在其他的强化学习环境中运行了上一个笔记本,你会发现我们的过程仅适用于简单的强化学习环境。实际上,在远更复杂的环境中,如月球着陆问题中进化解决方案,根本没有任何进展。这是因为仅使用奖励来进化 NEAT 代理/网络所需的复杂性不足以实现。
在第十二章中,我们将探讨一系列可以帮助我们解决月球着陆问题的策略,但到目前为止,我们来看一个来自 NEAT-Python 仓库示例的解决方案。NEAT-Python 拥有一系列设计用于无笔记本运行的示例。为了方便,我们将月球着陆示例转换为 Colab 笔记本,以展示改进的 NEAT 求解器。
注意:本例中的代码演示了进化 NEAT 网络以解决更复杂强化学习问题的一种可能解决方案。这个解决方案高度定制化,并使用复杂的数学概念来细化 fitness 评估。如前所述,我们将在下一章中探讨更优雅的解决方案,但请随时查阅这个笔记本。
在 Google Colab 中打开 EDL_11_4_NEAT_LunarLander.ipynb 笔记本。如需帮助,请参阅附录。通过选择菜单中的“运行”>“运行所有”来运行笔记本中的所有单元格。一如既往,这个笔记本是从之前的示例扩展而来的,并共享一个共同的代码库。我们在本笔记本中关注的重点部分都集中在改进 fitness 评估和 gene 操作符。
我们首先关注对 gene 操作符的改进和专门的 LanderGenome 类,如列表 11.18 所示。这个类的核心引入了一个新的参数,称为 discount。discount 参数的前提是引入一个随时间减少奖励的因素。折现奖励,即随着时间的推移减少未来或过去奖励的影响,是强化学习中发展出的一个概念。Q-learning 方程中使用的 gamma 项代表未来奖励的衰减。然而,在这个解决方案中,衰减的奖励并不直接影响代理的 action;相反,它们被用来更好地评估其 fitness。
列表 11.18 EDL_11_4_NEAT_LunarLander.ipynb:自定义 基因组 配置
class LanderGenome(neat.DefaultGenome):
def __init__(self, key):
super().__init__(key)
self.discount = None ❶
def configure_new(self, config):
super().configure_new(config)
self.discount = 0.01 + 0.98 * random.random() ❷
def configure_crossover(self, genome1, genome2, config):
super().configure_crossover(genome1, genome2, config)
self.discount = random.choice((genome1
➥ .discount, genome2.discount)) ❸
def mutate(self, config):
super().mutate(config)
self.discount += random.gauss(0.0, 0.05)
self.discount = max(0.01, min(0.99, self
➥ .discount)) ❹
def distance(self, other, config):
dist = super().distance(other, config)
disc_diff = abs(self.discount - other.discount)
return dist + disc_diff ❺
def __str__(self):
return f"Reward discount: {self.discount}\n{super().__str__()}"
❶ 创建折扣参数
❷ 设置折扣值
❸ 交叉/配对折扣
❹ 突变折扣
❺ 计算基因组距离
现在,基因组的适应性是通过一个compute_fitness函数来评估的,该函数不再直接在环境中模拟代理,而是使用记录的动作历史,如列表 11.19 所示。这个关于剧集和步骤的历史为每个基因组回放,其中基因组内的折扣因子用于评估与先前代理动作相关的重要性。本质上,代理的适应性是通过与其他代理先前表现的比较来计算的。虽然我们不能说这个解决方案使用了强化学习,但它确实使用了先前代理奖励和未来进化代理之间的归一化和折扣差异。
列表 11.19 EDL_11_4_NEAT_LunarLander.ipynb:计算 适应性
def compute_fitness(genome, net, episodes, min_reward, max_reward):
m = int(round(np.log(0.01) / np.log(genome.discount)))
discount_function = [genome.discount ** (m - i)
➥ for I in range(m + 1)] ❶
reward_error = []
for score, data in episodes: ❷
# Compute normalized discounted reward.
dr = np.convolve(data[:, -1], discount_function)[m:]
dr = 2 * (dr–- min_reward) / (max_reward–- min_reward)–- 1.0
dr = np.clip(dr, -1.0, 1.0) ❸
for row, dr in zip(data, dr): ❹
observation = row[:8]
action = int(row[8])
output = net.activate(observation)
reward_error.append(float((output[action]–- dr)
➥ ** 2)) ❺
return reward_error
❶ 创建折扣函数
❷ 遍历剧集
❸ 根据函数折扣奖励
❹ 遍历剧集步骤
❺ 计算奖励误差的差异
这个笔记本中有大量代码围绕着模拟代理环境交互、记录它并在基因组群体中评估它。我们接下来要查看的关键元素是运行模拟的代码:PooledErrorCompute类中的simulate函数。与之前的笔记本不同,通过模拟运行的代理代码(如列表 11.20 所示)基于当前步骤进行简单探索,给模拟机会添加探索步骤到模拟数据中。每次模拟运行都被添加到数据中以评分和提取最成功的运行,成功仍然是通过累积的总奖励来衡量的。
列表 11.20 EDL_11_4_NEAT_LunarLander.ipynb:模拟运行
def simulate(self, nets):
scores = []
for genome, net in nets: ❶
observation = env.reset()
step = 0
data = []
while 1:
step += 1
if step < 200 and random.random() < 0.2: ❷
action = env.action_space.sample()
else:
output = net.activate(observation)
action = np.argmax(output)
observation, reward, done, info = env.step(action)
data.append(np.hstack((observation,
➥ action, reward))) ❸
if done:
break
data = np.array(data)
score = np.sum(data[:, -1]) ❹
self.episode_score.append(score)
scores.append(score)
self.episode_length.append(step)
self.test_episodes.append((score, data)) ❺
❶ 遍历基因组网络
❷ 决定探索或利用
❸ 将步骤输出添加到数据中
❹ 评分最佳模拟运行
❺ 将数据添加到测试剧集
这个解决方案确实借鉴了强化学习过程,并试图用奖励来衡量误差。这里的总奖励误差对个体适应性有直接影响。
在笔记本运行时,请自行审查所有其余的代码——并且它将运行相当长的时间。这个笔记本可能需要 8 小时以上才能进化,而且可能仍然无法解决问题。
随着这个笔记本的训练,我们看到适应性快速收敛,但随后,事情迅速达到平台期。事实上,你可能直到进化到 1,000+ 代之后才找不到任何正面的奖励或适应性评估。进步很慢,但如果你有耐心,NEAT 代理最终可以被进化来解决月球着陆环境。
尽管 NEAT 代理借鉴了强化学习环境和一些技术来帮助解决问题,但实际的进化并不是我们所说的 DRL。相反,我们需要考虑其他进化概念或想法,这些概念或想法描述了如何进化一个能够自我进化其学习函数的代理。本质上,我们已经进化了一个能够进化其自身学习系统或学习函数的代理。
虽然进化的内部学习函数不太可能类似于 Q 学习方程,但我们能确认它能够解决复杂的强化学习环境。正是这个学习函数的进化,成为了我们在下一章和最后一章中探讨的最有趣和最有力的概念。
11.4.1 学习练习
执行以下练习将有助于你复习和改进对内容的理解:
-
将运行此笔记本的结果与上一节中探索的标准 NEAT Gym 练习进行比较。代理在相同数量的
generations后表现如何?这是你预期的吗? -
在笔记本中添加一个不同的环境,以查看改进的
fitness评估如何增加或减少 NEAT 代理的性能。 -
实现不同的探索方法。目前,这个笔记本使用固定的探索率。通过实现一个衰减的探索率,就像之前示例中看到的那样,增加一些变化。
本节展示了 NEAT 的强大能力,可以进化一个能够内部复制 Q 学习强化学习过程的个体。在下一节中,我们将查看一个称为 DQN 的基线 DRL 实现,并将其与我们使用 NEXT 所做的工作进行比较。
11.5 使用深度 Q 网络解决 Gym 的月球着陆器问题
当深度强化学习(DRL)首次展示出深度 Q 学习模型能够仅使用观察``状态作为输入解决经典的 Atari 游戏时,它引起了人们的关注。这是一个重大的突破,从那时起,DRL 已经证明能够比人类更好地解决许多复杂任务。在本节的笔记本中,我们查看 DQN 的经典实现,作为解决月球着陆器问题的替代方案。
在 Atari 上的深度 Q 网络
使用 DQN 解决经典 Atari 游戏是有效的,但需要大量的迭代。解决甚至像 Breakout 这样的基本 Atari 环境所需的训练次数可能达到数百万次。自那时以来,强化学习方法的改进已经减少了所需的训练次数,但总体而言,DRL 是一个计算成本高昂的任务。幸运的是,与 EC 不同,DRL 是深度学习带来的计算增强的主要受益者。
在 Google Colab 中打开 EDL_11_5_DQN_LunarLander.ipynb 笔记本。如需帮助,请参考附录。通过选择菜单中的“运行”>“运行所有”来运行笔记本中的所有单元格。
这个笔记本被设置为使用相同的环境,但移除了进化和代码库。现在,我们的重点是 DQN 模型在 Gym 问题上的工作方式。这意味着我们从 DQNAgent 类定义开始,如下面的列表所示。init 函数设置基本超参数并保存 action 和 observation 的大小。它还添加了一个 memory,用于存储模拟中的经验以及代理的大脑或模型。
列表 11.21 EDL_11_5_DQN_Gyms.ipynb:DQN 代理
import tensorflow.keras as k ❶
import tensorflow.keras.layers as kl ❶
class DQNAgent():
def __init__(self, state_size, action_size, episodes=100):
self.weight_backup = "backup_weights.h5"
self.state_size = state_size ❷
self.action_size = action_size ❷
self.memory = deque(maxlen=2000) ❸
self.learning_rate = 0.001 ❹
self.gamma = 0.95
self.exploration_rate = 1.0
self.exploration_min = 0.1
self.exploration_decay =
➥ (self.exploration_rate-self.exploration_min) / episodes
self.brain = self._build_model() ❺
❶ 导入深度学习包
❷ 保存动作/观察大小
❸ 创建一个存储记忆的地方
❹ 设置超参数
❺ 创建代理的模型或大脑
在 build_model 函数中接下来定义代理的深度学习模型或大脑。该函数中的代码创建了一个三层模型,以 state 空间作为输入,以 action 空间作为输出。模型使用 mse 作为损失函数和 Adam 优化器进行编译。本例的独特之处在于模型能够加载包含先前训练的模型权重的文件,如下面的列表所示。
列表 11.22 EDL_11_5_DQN_Gyms.ipynb:构建大脑
def _build_model(self):
model = k.Sequential() ❶
model.add(kl.Dense(24, input_dim=self.state_size, activation='relu'))
model.add(kl.Dense(24, activation='relu')) ❷
model.add(kl.Dense(self.action_size,
➥ activation='linear')) ❸
model.compile(loss='mse', optimizer=k.optimizers.Adam(learning_rate=self
➥ .learning_rate)) ❹
if os.path.isfile(self.weight_backup): ❺
model.load_weights(self.weight_backup)
self.exploration_rate = self.exploration_min
return model
❶ 从基本模型开始
❷ 向模型添加层
❸ 输出层与动作大小匹配。
❹ 使用均方误差(MSE)损失编译模型
❺ 如果可能,加载先前的模型权重
在我们继续 DQNAgent 定义的其他部分之前,让我们回顾一下训练代码。代码,如列表 11.23 所示,首先设置 BATCH_SIZE 和 EPISODES 的主要超参数。然后,它开始循环遍历 episode 的数量,模拟代理直到 env.step 在每个 episode 中输出 done 等于 True。如果代理未完成,它将 state 输入到 agent.act 函数以输出动作预测,然后将其应用于 env.step 函数以输出下一个 state、reward 和 done。接下来,调用 agent.remember 将 action 和后果添加到代理的记忆中。在每个 episode 结束时,当 done == True,调用 agent.remember,回放所有记住的 actions 并使用结果来训练模型。
列表 11.23 EDL_11_5_DQN_Gyms.ipynb:训练代理
BATCH_SIZE = 256 #@param {type:"slider", min:32,
➥ max:256, step:2} ❶
EPISODES = 1000 #@param {type:"slider", min:10, max:1000, step:1}
state_size = env.observation_space.shape[0]
action_size = env.action_space.n
agent = DQNAgent(state_size, action_size, episodes=EPISODES)
groups = { "reward" : {"total", "max", "avg"},
"agent" : {"explore_rate", "mem_usage"}}
plotlosses = PlotLosses(groups=groups)
total_rewards = 0
for ep in nb.tqdm(range(EPISODES)):
rewards = []
state = env.reset()
state = np.reshape(state, [1, state_size]) ❷
done = False
index = 0
while not done:
action = agent.act(state) ❸
next_state, reward, done, _ = env.step(action) ❸
rewards.append(reward)
next_state = np.reshape(next_state,
➥ [1, state_size]) ❷
agent.remember(state, action, reward, next_state,
➥ done) ❹
state = next_state
agent.replay(BATCH_SIZE) ❺
total_rewards += sum(rewards)
plotlosses.update({'total': sum(rewards),
'max': max(rewards),
"avg" : total_rewards/(ep+1),
"explore_rate" : agent.exploration_rate,
"mem_usage" : agent.mem_usage(),
})
plotlosses.send()
❶ 设置主要超参数
❷ 重新塑形状态以供模型使用
❸ 预测并执行动作
❹ 记住动作和后果
❺ 回放动作并训练代理
现在,我们回到 DQNAgent 定义,并回顾列表 11.24 中的 act、remember、和 replay 函数。第一个函数 act 评估探索的机会,并在探索时响应随机 动作,如果不是探索则响应预测 动作。第二个函数 remember 存储代理在模拟过程中积累的经验。这里使用的内存是一个出队类,它使用固定大小,并在填满时自动丢弃最老的记忆。第三个函数 replay 从代理内存中提取一批经验,前提是有足够的记忆。这批经验随后用于回放代理的 动作 并评估每个先前执行的 动作(随机或预测)的质量。动作 的质量 目标 使用 Q-learning 方程的一种形式来计算。然后使用 fit 函数在单个周期内使用计算出的值来更新模型。最后,在 replay 函数的末尾,如果需要,通过 exploration_decay 更新探索的机会——exploration_rate。
列表 11.24 EDL_11_5_DQN_Gyms.ipynb:act、remember 和 replay 函数
def act(self, state):
if np.random.rand() <= self.exploration_rate: ❶
return random.randrange(self.action_size)
act_values = self.brain.predict(state)
return np.argmax(act_values[0])
def remember(self, state, action, reward, next_state, done):
self.memory.append((state, action, reward,
➥ next_state, done)) ❷
def replay(self, batch_size):
if len(self.memory) < batch_size: ❸
return
sample_batch = random.sample(self.memory,
➥ batch_size) ❹
for state, action, reward, next_state, done in sample_batch:
target = reward
if not done:
target = reward + self.gamma * np.amax(self.brain.predict(next_state)[0])
target_f = self.brain.predict(state) ❺
target_f[0][action] = target
self.brain.fit(state, target_f, epochs=1, verbose=0)
if self.exploration_rate > self.exploration_min:
self.exploration_rate -= self.exploration_decay
❶ 选择随机或预测动作
❷ 将数据追加到出队内存中
❸ 检查内存是否大于批量大小
❹ 从内存中提取经验并进行训练
❺ 评估 Q-learning 函数的目标预测
图 11.8 展示了在月球着陆器环境中训练 DQN 代理 1,000 个周期的结果。在图中,你可以看到代理如何随着学习掌握环境而逐渐积累奖励。

图 11.8 在月球着陆器环境中训练代理的结果
DQN 和 DRL 是 AI 和 ML 中的强大进步,展示了数字智能在某些任务上可能比人类做得更好的潜力。然而,DRL 需要克服的一些关键挑战仍然存在,包括多任务或泛化学习。我们将在下一章探讨如何利用进化来探索可能用于泛化形式的学习,如 DRL。
摘要
-
强化学习是另一种动态学习形式,它使用奖励来强化给定当前
状态时选择最佳适当动作的选择。 -
Q-learning 是一种 RL 的实现,它使用
状态或动作查找表或策略来提供关于代理应采取的下一个最佳可能动作的决策。 -
能够区分各种学习形式很重要,包括生成式、监督式和强化学习。
-
OpenAI Gym 是一个用于评估和探索 RL 或其他奖励/决策求解模型各种实现的框架和工具包。
-
在 Colab 笔记本中运行 OpenAI Gym 可以用于探索各种复杂度不同的环境。
-
OpenAI Gym 是一个常见的强化学习算法基准和探索工具。
-
NEAT 可以通过使用典型的强化学习来解决各种样本强化学习 Gym 环境。
-
可以开发一个 NEAT 智能体,通过采样和回放技术来解决更复杂的强化学习环境。
-
深度 Q 学习是强化学习的一种高级形式,它使用深度学习代替 Q 表或策略。深度 Q 网络已被用于解决复杂环境,如月球着陆游戏。
12 进化机器学习及其超越
本章节涵盖
-
基因表达编程与进化与机器学习
-
重新审视使用 Geppy 的强化学习
-
本能学习
-
使用遗传编程的广义学习
-
进化机器学习的未来
-
基于本能的深度学习和深度强化学习的泛化
在上一章中,我们深入探讨了如何将进化解决方案如 NEAT 应用于解决强化学习问题。在本章中,我们继续探讨这些相同的概念,同时也退后一步,探讨进化方法如何应用于扩展我们对机器学习的理解。具体来说,研究进化搜索在其中的作用可以扩展我们开发广义机器学习的方法。
广义机器学习(又称广义人工智能)
广义机器学习,或称广义智能,是一个专注于构建能够解决多个任务的模型的研究领域。通常,在机器学习中,我们开发模型来对单一数据源进行分类或回归,通过迭代训练模型并使用相似数据验证性能。广义机器学习的目标是开发能够预测多种不同形式的数据或环境的模型。在数据科学中,你可能听到这个问题被称为跨领域或多模态,这意味着我们正在构建一个模型来处理跨领域的问题。
深度学习针对的是函数逼近和优化,通常是为了解决特定问题而设计的。在本书中,我们探讨了通过改进超参数搜索、优化网络架构和神经进化网络等方法来增强或改进深度学习的方法。
在本章中,我们将注意力从深度学习转向,探讨使用进化来帮助我们泛化解决机器学习问题的一些例子。我们首先研究进化的函数,然后转向开发可能解决多个问题的更广义的函数。在扩展泛化的基础上,我们接着探讨一个试图封装广义函数学习的想法,称为本能学习。
从广义本能学习出发,我们转向一个有趣的例子,使用遗传编程训练一个蚂蚁智能体。然后,我们探讨如何通过进一步的进化使特定智能体泛化。最后,我们以讨论机器学习中进化的未来作为本章的结尾。在下一节中,我们首先关注机器学习的核心——函数,以及它如何可以通过基因表达编程(GEP)进行进化。
12.1 基因表达编程与进化与机器学习
函数,或函数逼近器,是任何机器学习算法的核心。这个函数的作用是接收数据或输入并输出结果或预测。图 12.1 展示了本书中涵盖的各种学习形式,使用深度学习作为针对每种学习类型的函数或函数逼近器。

图 12.1 学习函数的例子
在本节中,我们查看一个笔记本,它构建了实际函数,而不是使用进化的近似。这里的优点是进化消除了在学习过程中使用损失或误差函数的需求。图 12.2 展示了这种类型的学习是如何操作的。如果你回顾第十一章,这就是我们使用 NEAT 作为函数逼近器执行的过程,取代了传统的深度学习。

图 12.2 进化学习
对于本节中的笔记本,我们使用 Geppy,这是 DEAP 的扩展,它改进了 GEP 的 DEAP 实现。如果你还记得,我们在第三章已经看过 GEP。我们在这里做的工作是关于进化函数以及它与通用机器学习的关系。
在 Google Colab 中打开 EDL_12_1_GEPPY.ipynb 笔记本。如需帮助,请参阅附录。通过选择菜单中的“运行”>“运行所有”来运行笔记本中的所有单元。
由于 Geppy 是 DEAP 的扩展,大部分代码看起来与我们之前覆盖的非常相似;因此,我们在这里只回顾新特性。我们首先查看的第一个代码单元块,如下所示,展示了我们正在进化的解决方案的目标函数。这个简单的线性函数是我们希望通过进化复制的目标函数。
列表 12.1 EDL_12_1_GEPPY.ipynb:定义目标函数
def f(x1):
return 6 * x1 + 22 ❶
❶ 地面真实函数
这个笔记本使用一组基本的表达式运算符来生成表达式树,如下所示。
列表 12.2 EDL_12_1_GEPPY.ipynb:添加表达式运算符
import operator
pset = gep.PrimitiveSet('Main', input_names=['x1']) ❶
pset.add_function(operator.add, 2) ❷
pset.add_function(operator.sub, 2) ❷
pset.add_function(operator.mul, 2) ❷
pset.add_function(protected_div, 2) ❸
pset.add_ephemeral_terminal(name='enc', gen=lambda:
➥ random.randint(-10, 10)) ❹
❶ 构建运算符集合
❷ 添加标准数学运算符
❸ 添加一个用于除法的特殊运算符
❹ 添加一个常数/瞬时运算符
接下来,我们跳转到evaluate函数,它展示了我们如何确定种群中每个individual的fitness。toolbox.compile函数从individual的gene序列生成函数。然后,从样本X1输入生成输出。之后,通过计算平均绝对误差来返回fitness,如下所示。
列表 12.3 EDL_12_1_GEPPY.ipynb:evaluate函数
def evaluate(individual):
func = toolbox.compile(individual) ❶
Yp = np.array(list(map(func, X1))) ❷
return np.mean(np.abs(Y - Yp)), ❸
toolbox.register('evaluate', evaluate) ❹
❶ 从个体编译函数
❷ 输出预测结果
❸ 返回平均绝对误差
❹ 在工具箱中注册函数
使用 Geppy 而不是 DEAP 的基本基因表达库的好处是我们可以访问一些与这种形式进化相关的有用扩展和算子。这些新算子通过调整突变和交叉遗传算子来帮助 GEP 进化,如列表 12.4 所示。额外的突变算子,以mut_为前缀,允许函数被反转和转置。随后,额外的交叉算子,以cx_为前缀,提供两点交叉和基因``交叉。两点交叉允许将基因序列沿基因序列分割成两个位置。基因``交叉的过程允许每个基因``染色体进行交叉。
列表 12.4 EDL_12_1_GEPPY.ipynb:注册自定义算子
toolbox.register('mut_uniform', gep.mutate_uniform,
➥ pset=pset, ind_pb=0.05, pb=1) ❶
toolbox.register('mut_invert', gep.invert, pb=0.1) ❶
toolbox.register('mut_is_transpose', gep.is_transpose, ❶
➥ pb=0.1) ❶
toolbox.register('mut_ris_transpose', ❶
➥ gep.ris_transpose, pb=0.1) ❶
toolbox.register('mut_gene_transpose', ❶
➥ gep.gene_transpose, pb=0.1) ❶
toolbox.register('cx_1p', gep.crossover_one_point, ❷
➥ pb=0.4) ❷
toolbox.register('cx_2p', gep.crossover_two_point, ❷
➥ pb=0.2)
toolbox.register('cx_gene', gep.crossover_gene, pb=0.1) ❷
toolbox.register('mut_ephemeral', gep.mutate_
➥ uniform_ephemeral, ind_pb='1p') ❸
toolbox.pbs['mut_ephemeral'] = 1 ❸
❶ 为突变添加
❷ 为交叉添加
❸ 处理常数/临时算子
在之前,我们通常用一个单一的基因序列来表示我们的遗传编码。Geppy 通过将基因序列分解成组件,或染色体来工作。将基因序列分解成染色体可以将复杂的基因序列隔离成有用的部分。然后,通过进化,这些部分可以在交叉操作期间交换,从而保持有用的部分。
通过查看基因的注册,我们可以看到基因和染色体是如何定义的,如列表 12.5 所示。参数h代表染色体的数量,参数n_genes代表每个染色体中基因的数量。染色体模板和基因序列在toolbox中注册,正如我们之前所看到的。
列表 12.5 EDL_12_1_GEPPY.ipynb:定义基因、头部和染色体
h = 7 ❶
n_genes = 2 ❷
toolbox = gep.Toolbox()
toolbox.register('gene_gen', gep.Gene, pset=pset,
➥ head_length=h) ❸
toolbox.register('individual', creator.Individual, gene_gen=toolbox.gene_gen,
➥ n_genes=n_genes,
➥ linker=operator.add) ❹
toolbox.register("population", tools.initRepeat, list,
➥ toolbox.individual) ❸
❶ 设置头部长度
❷ 染色体中的基因数量
❸ 注册基因染色体
❹ 注册基因序列
进化函数的代码,如列表 12.6 所示,只有几行长,从创建种群和设置使用HallOfFame类跟踪的最佳个体数量开始。之后,只需调用gep_simple来在n_gen代进化解决方案即可。
列表 12.6 EDL_12_1_GEPPY.ipynb:进化函数
n_pop = 100 ❶
n_gen = 100 ❶
pop = toolbox.population(n=n_pop) ❷
hof = tools.HallOfFame(3) ❸
pop, log = gep.gep_simple(pop, toolbox, n_generations=n_gen, n_elites=1,
stats=stats, hall_of_fame=
➥ hof, verbose=True) ❹
❶ 种群和代数的参数
❷ 创建种群
❸ 设置最佳值的最大数量
❹ 进化
由于大多数原始进化函数都有冗余项或算子,Geppy 提供的一个有用功能是简化。使用最佳个体调用gep.simplify函数生成顶级解决方案。正如您可以从以下列表的结果中看到的那样,最终函数与列表 12.1 中的目标函数完全匹配。
列表 12.7 EDL_12_1_GEPPY.ipynb:简化的进化函数
best_ind = hof[0] ❶
symplified_best = gep.simplify(best_ind) ❷
print('Symplified best individual: ')
print(symplified_best) ❸
# output
Symplified best individual: ❸
6*x1 + 22
❶ 获取最佳解
❷ 提取简化视图
❸ 显示输出
笔记本中的最后一个单元,如列表 12.8 所示,使用 Geppy 的另一个有用功能渲染原始函数,而不是简化函数。对gep.export_expression_tree的调用将函数渲染成如图 12.3 所示的漂亮图表。请注意,您看到的原始图表可能与图中的不同,但结果——一个简化的表达式——应该是相同的。

图 12.3 进化方程表达式树
列表 12.8 EDL_12_1_GEPPY.ipynb:显示进化方程
rename_labels = {'add': '+', 'sub': '-', 'mul': '*', 'protected_div': '/'}
gep.export_expression_tree(best_ind, rename_labels,
➥ 'data/numerical_expression_tree.png') ❶
from IPython.display import Image
Image(filename='data/numerical_expression_tree.png') ❷
❶ 生成表达式树的图像
❷ 在笔记本中显示树
这个笔记本的目的是展示一个可以推导出显式函数的有用工具。在这个例子中,得到的函数与目标函数相同,但这种情况并不总是如此。在许多情况下,得到的函数可以提供对数据关系的洞察,这些关系之前并未被理解,正如我们在本章后面所看到的。在此之前,让我们跳到下一节的一些学习练习。
12.1.1 学习练习
使用这些练习帮助您提高对内容的知识:
-
修改列表 12.1 中的目标函数,然后重新运行笔记本。进化的表现如何?
-
通过修改列表 12.5 中的
head、h和基因数量n_genes参数来修改chromosomes。重新运行笔记本以查看这对进化过程有何影响。 -
向列表 12.2 中添加或删除表达式运算符。例如,您可以添加
cos或sin等运算符——只需检查 DEAP 文档。确保还更新列表 12.8 中的标签。
在下一节中,我们将从简单的例子转向经典控制问题,通过重新审视 OpenAI Gym。
12.2 使用 Geppy 重新审视强化学习
为了展示 Geppy 在进化方程方面的有效性,我们通过 OpenAI Gym 查看一个班级控制问题。想法是让 Geppy 进化一个可以驱动几个最复杂的 OpenAI Gym 环境的方程。这个例子反映了我们在第十一章中用 NEAT 所做的工作,如果您需要回顾一些元素,请参考那些笔记本。
在 Google Colab 中打开 EDL_12_2_GEPPY_Gym.ipynb 笔记本。如果您需要帮助,请参阅附录。通过从菜单中选择运行时 > 运行所有来运行笔记本中的所有单元。
可能不稳定
如第十一章所述,由于虚拟驾驶员设置和其他自定义安装的组件,这些笔记本电脑可能会出现崩溃。如果在执行过程中笔记本电脑崩溃,请断开连接并删除运行时,然后重新启动并重新运行。从菜单中选择运行时 > 断开连接并删除运行时,然后运行时 > 运行所有。
在这个笔记本中,我们尝试解决两个连续控制问题:山车(连续)和摆锤(连续)。"连续"这个词用来定义环境的动作和观察(状态)空间。巧合的是,也可能有些令人困惑,"连续"也可能指代接收连续奖励的代理。也就是说,在每一步,环境都会提供一个奖励,无论是正面的还是负面的。设置笔记本和渲染它们的动作或观察空间相当简单,如下面的列表所示。
列表 12.9 EDL_12_2_GEPPY_Gym.ipynb:渲染环境空间
ENVIRONMENT = "MountainCarContinuous-v0" #@param ['Pendulum-v0',
➥ 'MountainCarContinuous-v0'] ❶
env = gym.make(ENVIRONMENT) ❷
env.reset()
plt.imshow(env.render(mode='rgb_array')) ❸
print("action space: {0!r}".format(env.action_space)) ❹
print("observation space: {0!r}".format ❹
➥ (env.observation_space)) ❹
❶ Colab 表单的环境选项
❷ 创建环境
❸ 渲染环境的图像
❹ 显示动作/观察空间
图 12.4 显示了我们要尝试进化解决方案方程的两个环境。这两个环境都使用连续的动作空间,而我们在第十一章中探索的 Gym 环境使用的是离散的动作空间。"连续"意味着动作现在可以是给定范围内的实数值:摆锤为-2 到+2,山车为-1 到+1。这是完美的,因为我们的导出方程现在可以输出该范围内的值。

图 12.4 渲染环境和空间
每个环境的观察空间都不同,因此我们必须对定义原始集的方式做轻微的调整,如列表 12.10 所示。由于这两个环境使用不同的观察空间,因此在设置输入的基本集合时,我们需要考虑这些变化。对于摆锤,观察空间是摆锤的x、y坐标。同样,对于山车,观察空间是汽车的x、y坐标及其速度。这意味着摆锤的函数是f(x, y),而对于山车,它是f(x, y, 速度)。
列表 12.10 EDL_12_2_GEPPY_Gym.ipynb:设置原始集
if ENVIRONMENT == "Pendulum-v0":
pset = gep.PrimitiveSet('Main', input_names=
➥ ['x', 'y', 'velocity']) ❶
elif ENVIRONMENT == "MountainCarContinuous-v0":
pset = gep.PrimitiveSet('Main', input_names=
➥ ['x', 'y']) ❷
pset.add_function(operator.add, 2) ❸
pset.add_function(operator.sub, 2)
pset.add_function(operator.mul, 2)
pset.add_function(protected_div, 2)
pset.add_ephemeral_terminal(name='enc', gen=lambda: random.randint(-10, 10))
❶ 摆锤的集合
❷ 山车的集合
❸ 如前所述创建剩余集合
正如我们在第十一章中所做的那样,我们根据个体积累奖励的能力来确定其适应度。然而,这次我们不是使用 NEAT 网络来预测动作,而是使用进化的函数/方程来计算动作。在这个代码块中,如列表 12.11 所示,为每个环境编写了两个简单的函数。这些示例仅用于测试,并不代表内部最终进化的解决方案。根据环境,对func的调用使用展开运算符*将观察到的状态展开为函数的参数。然后,函数的输出,可以是任何东西,通过convert_to_action和clamp函数转换为环境适当的动作空间。
列表 12.11 EDL_12_2_GEPPY_Gym.ipynb:确定个体``适应度
if ENVIRONMENT == "Pendulum-v0":
def func(x, y, velocity): ❶
return x * y / velocity
elif ENVIRONMENT == "MountainCarContinuous-v0":
def func(x, y): ❶
return x * y
def clamp(minv, maxv, val): ❷
return min(max(minv, val), maxv)
def convert_to_action(act, env): ❸
return clamp(env.action_space.low, env.action_space.high, act)
frames = []
fitness = 0
state = env.reset()
for i in range(SIMULATION_STEPS):
action = convert_to_action(func(*state), env) ❹
state, reward, done, info = env.step([action])
fitness += reward # reward for each step ❺
frames.append(env.render(mode='rgb_array'))
if done:
break
❶ 样本函数
❷ 限制值在范围内
❸ 将计算值转换到环境中
❹ 计算/转换成输出动作
❺ 将奖励添加到总适应度
现在,真正的 evaluate 函数,如下面的列表所示,可以从列表 12.11 编写。
列表 12.12 EDL_12_2_GEPPY_Gym.ipynb:真正的 evaluate 函数
def evaluate(individual):
func = toolbox.compile(individual) ❶
fitness = 0
for run in range(SIMULATION_RUNS):
state = env.reset()
actions=[]
for i in range(SIMULATION_STEPS):
action = convert_to_action(func(*state), env) ❷
state, reward, done, info = env.step([action]) ❸
fitness += reward ❹
if done:
break
return fitness,
toolbox.register('evaluate', evaluate) ❺
❶ 从个体编译函数
❷ 计算/转换动作
❸ 取得一步
❹ 将奖励添加到总适应度
❺ 注册函数
运行进化的代码相当直接,如下面的列表所示。
列表 12.13 EDL_12_2_GEPPY_Gym.ipynb:进化解决方案
POPULATION = 250 #@param {type:"slider", min:10, ❶
➥ max:1000, step:5} ❶
GENERATIONS = 25 #@param {type:"slider", min:10, ❶
➥ max:250, step:1} ❶
pop = toolbox.population(n=POPULATION)
hof = tools.HallOfFame(3)
for gen in range(GENERATIONS):
pop, log = gep.gep_simple(pop, toolbox, n_generations=1, n_elites=1,
stats=stats, hall_of_fame=
➥ hof, verbose=True) ❷
clear_output()
print(f"GENERATION: {gen}")
best = hof[0]
show_best(best) ❸
❶ 进化超参数
❷ 进化一代
❸ 展示最佳个体的性能
在每个 generation 之后,我们调用 show_best 函数运行 individual 通过模拟并将其渲染成视频,如下面的列表所示。
列表 12.14 EDL_12_2_GEPPY_Gym.ipynb:展示最适应的
def show_best(best):
func = toolbox.compile(best) ❶
frames = []
fitness = 0
state = env.reset()
for i in range(SIMULATION_STEPS): ❷
action = convert_to_action(func(*state), env)
state, reward, done, info = env.step([action])
frames.append(env.render(mode='rgb_array'))
fitness += reward
if done:
break
mediapy.show_video(frames, fps=30) ❸
try:
symplified_best = gep.simplify(best)
print(f'Symplified best individual: {fitness}')
print(symplified_best) ❹
except:
pass
❶ 编译成函数
❷ 在一次运行中模拟函数
❸ 将记录的模拟渲染成视频
❹ 展示函数的简化形式
图 12.5 展示了使用简化方程的 mountain car 解决方案的结果。结果输出显示了推导出的方程被用来从不同的环境中获得良好的奖励。值得注意的是,由于任务和输入的相似性,每个方程都是不同的。

图 12.5 在环境中进化方程的结果
使用 Geppy 进化方程解决这些环境的能力令人印象深刻。它展示了进化在克服控制问题上的有效性。然而,我们本章的目标是探讨如何进化更通用的学习解决方案。我们将在下一节中更深入地探讨这一点,但现在,让我们跳入一些可以帮助加强本节概念的学习任务。
12.2.1 学习练习
使用这些练习回顾材料并加强你所学的知识:
-
在互联网上搜索其他可能的 OpenAI Gym 环境,我们可能可以用这种技术解决。
-
向集合中添加新的表达式运算符,如
cos或sin。重新运行笔记本以查看这些运算符是否以及如何被用于结果推导出的方程中。 -
尝试使用摆锤或山车获得最佳的
fitness。这可以通过多种方式完成,包括增加population、增加起始的generations数量以及引入新的表达式。
在下一节中,我们将扩展我们对使用进化来进化更通用解决方案的理解。
12.3 介绍本能学习
本能学习(IL)是本书作者提出的一个概念,旨在概括特质或功能如何既可以通过进化也可以通过学习而发展。它基于对人类和其他生物体的生物学观察,并借鉴了强化学习(RL)和其他行为学习方法的概念。在我们接下来的笔记本中,我们将运用一些本能学习的概念,尝试为上一节中的例子进化出一种更通用的解决方案。在此之前,让我们更深入地了解一下什么是本能学习。
12.3.1 本能学习的基本原理
IL 是一个抽象的思维模型,旨在指导使用进化来开发更通用的机器学习。IL 的倡议是,如果进化可以发展人类的一般化,它同样可以发展数字生命。当然,进化出某种形式的通用数字生命并非易事,因此 IL 试图描述一些基本模式和起点。
在自然界中,我们观察到许多生物体都有我们所说的本能,这描述了一些形式的学习或进化行为。对于许多物种来说,区分学习和进化的本能可能是显而易见的。一个很好的例子是狗,其中一些品种与特定的自然行为相关联;然而,狗也可以学习新的行为,这些行为可以成为本能。
一条狗通常通过行为学习来学习本能,例如通过强化或基于奖励的学习。最初,训练狗完成一项任务需要重复的试错过程,但经过一定量的训练后,狗“就知道了”。狗“就知道”的那个阶段就是我们所说的本能的过渡。在我们深入探讨这种过渡是如何发生之前,让我们在下节中更深入地探讨生物心智如何工作的通用理论。
思维的双过程理论
思维的双过程理论描述了生物生命中的高级思维过程。其观点是我们的大脑中有两个过程在起作用:一个低级过程,称为系统 1,另一个更自觉的过程,称为系统 2。图 12.6 展示了这两个过程之间的关键区别。

图 12.6 双过程理论
这两个思维过程通常被认为是分开的,但系统 2 中的思维或行为可以通过足够的训练和实践转化为系统 1。因此,强化学习的有意识行为是一个系统 2 过程,通过足够的试错转化为系统 1 过程。我们可以将这种过渡过程称为条件反射,它起源于巴甫洛夫式条件反射。
巴甫洛夫的狗和巴甫洛夫式条件反射
伊万·巴甫洛夫(1849–1936)是一位心理学家,他通过强化(基于奖励)学习表明,狗可以被训练在听到铃声时分泌唾液。这种条件或学习行为展示了从系统 2 过程到系统 1 过程的转变,通过持续训练,狗本能地在听到铃声时分泌唾液。
图 12.7 从本能学习的角度描述了双过程理论。根据进化,生物体获得某些遗传特征或本能。根据双过程理论,生物体可以思考和将思想和行动转化为本能。

图 12.7 双过程理论与本能学习
假设双过程理论是准确的,我们也可以假设进化发展了这一过程以提高效率。换句话说,我们假设在高等生命进化的某个阶段,进化从构建硬编码的本能转变为让生物体发展自己的本能。这意味着,在某个时刻,生物体进化出了我们现在称之为双过程理论的本能。
IL 是寻找衍生出这种双过程或系统 2 本能的进化基础。它关乎理解在生命进化的某个时刻,本能如何变成了思想。因此,强化学习是双过程理论的一种简单形式,而 IL 是寻找 RL 起源的搜索。下一节中我们看到的笔记本示例展示了进化泛化本能(函数)可能有多么低效。
12.3.2 发展泛化本能
下一本笔记本展示了生物体如何以及为什么可能从仅仅发展本能发展到更高层次的思想形式。在接下来的笔记本中,我们仅使用进化来尝试发展一个单一的方程(本能),以解决这两个 Gym 问题。在强化学习(RL)中,这被称为多任务 RL,许多人认为这是一个难题。
在 Google Colab 中打开 EDL_12_3_GEPPY_Instinctual.ipynb 笔记本。如有需要,请参考附录。通过选择菜单中的“运行”>“运行所有”来运行笔记本中的所有单元格。
在这个笔记本中,我们研究在上一项练习中可能学习到的本能是如何进化的,以及如何用于泛化学习。因此,我们首先研究的是那些先前开发的本能(即方程或函数)如何被重用,如下列所示。函数instinct1是从在摆动环境中运行 EDL_12_2_GEPPY_Gyms.ipynb 笔记本中进化的。同样,instinct2是从同一笔记本在山车环境中进化的。请参考图 12.5 来回顾那些生成的方程。
列表 12.15 EDL_12_3_GEPPY_Instinctual.ipynb:添加本能
def protected_div(x1, x2): ❶
if abs(x2) < 1e-6:
return 1
return x1 / x2
def instinct1(x1, x2): ❷
return protected_div((-x1 – 23*x2 + 1),x2)
def instinct2(x1, x2, x3): ❸
return protected_div(x1**2 – (x3 + x2)*(9*x1 + 56)/8,x1)
❶ 避免除以零
❷ 解决摆动问题的函数
❸ 解决山车问题的函数
在 IL(智能学习)中的一个主要概念是本能或函数是分层的,并且随着时间的推移而发展和添加。这个笔记本试图证明在两个不同任务中成功的模拟生物如何结合生成第三个进化的生物,这个第三个生物能够进化一个三级或更高层次的本能,以泛化两个环境的解决方案。
接下来,我们来看这些新定义的本能如何添加到 primitive 集中,如列表 12.16 所示。对于 PrimitiveSet 定义,我们将输入设置为包括 x、y 和 velocity 以及一个新的输入 e,它代表环境。然后,我们将可能的功能操作符简化为仅包括 add、sub 和 mul,以及新的本能操作符。由于基础本能应该包含所需的常数,因此移除了瞬时常数操作符。
列表 12.16 EDL_12_3_GEPPY_Instinctual.ipynb:完成 primitive 集合
pset = gep.PrimitiveSet('Main', input_names=
➥ ['e', 'x', 'y', 'velocity']) ❶
pset.add_function(operator.add, 2)
pset.add_function(operator.sub, 2)
pset.add_function(operator.mul, 2)
#pset.add_function(protected_div, 2) ❷
pset.add_function(instinct1, 2) ❸
pset.add_function(instinct2, 3) ❹
#pset.add_ephemeral_terminal... ❺
❶ 泛化输入和环境
❷ 简化函数集
❸ 使用两个操作符进行添加
❹ 使用三个操作符进行添加
❺ 消除常数
与之前的笔记本不同,这次我们进化一个智能体同时解决两个环境。为此,我们创建了一个环境列表,我们使用这个列表来进化智能体,如下面的列表所示。
列表 12.17 EDL_12_3_GEPPY_Instinctual.ipynb:创建环境
environments = [gym.make("MountainCarContinuous-v0"),
➥ gym.make("Pendulum-v0")] ❶
print(str(environments[1])) ❷
❶ 要评估的环境列表
❷ 在打印前转换为字符串
我们接下来要查看的下一个单元格块,如列表 12.18 所示,包含 evaluate 函数。这次,函数首先遍历 environments 列表,并为每个环境运行一系列模拟。输入 e 简单地表示环境索引,并将其作为第一个参数传递给目标函数。由于每个环境都有不同的 observation 状态,当仅用 2 个状态运行摆动问题时,我们在状态空间中追加一个 0 来表示第三个输入:速度。在函数结束时,两个环境的 fitness 值被平均并返回。
列表 12.18 EDL_12_3_GEPPY_Instinctual.ipynb:构建 evaluate 函数
def evaluate(individual):
func = toolbox.compile(individual)
total_fitness = 0
for env in environments: ❶
fitness = []
e = environments.index(env) ❷
for run in range(SIMULATION_RUNS):
state = env.reset()
actions=[]
for i in range(SIMULATION_STEPS):
if len(state) < 3:
state = np.append(state, 0) ❸
state = np.insert(state, 0, e)
action = convert_to_action(func(*state), env) ❹
state, reward, done, info = env.step([action])
fitness.append(reward)
if done:
break
total_fitness += sum(fitness)/len(fitness)
return total_fitness/2, ❺
toolbox.register('evaluate', evaluate)
❶ 遍历环境
❷ 根据环境索引填充 e
❸ 如有必要则追加到状态
❹ 转换为动作
❺ 返回平均适应度
我们接下来要查看的是更新的show_best函数,它现在将代理在两个环境中的模拟运行合并成一个视频。这次,函数遍历环境并对每个环境进行模拟。由于每个环境渲染到不同的窗口大小,我们使用cv2.resize函数将所有帧调整为相同的大小,如列表 12.19 所示。这样做是为了将所有帧合并成一个视频。在函数的末尾,表达式树被保存到文件中以便侧边观看。你可以通过打开左侧的系统文件夹,在数据文件夹中找到文件,然后双击它以打开一个侧边窗口来查看表达式树的进化。
列表 12.19 EDL_12_3_GEPPY_Instinctual.ipynb:更新show_best函数
rename_labels = {'add' : '+',
'sub': '-',
'mul': '*',
'protected_div': '/',
'instinct1': 'I1', ❶
'instinct2': 'I2'} ❶
def show_best(best):
func = toolbox.compile(best)
frames = []
fitness = 0
for env in environments: ❷
e = environments.index(env)
state = env.reset()
for i in range(SIMULATION_STEPS):
if len(state) < 3:
state = np.append(state, 0)
state = np.insert(state, 0, e)
action = convert_to_action(func(*state), env)
state, reward, done, info = env.step([action])
frame = env.render(mode='rgb_array')
frame = cv2.resize(frame, dsize=(600, 400),
interpolation=cv2.INTER_CUBIC) ❸
frames.append(frame)
fitness += reward
if done:
break
mediapy.show_video(frames, fps=30) ❹
gep.export_expression_tree(best, rename_labels, 'data/numerical_expression_tree.png')) ❺
❶ 为本能函数添加标签
❷ 遍历环境
❸ 调整捕获帧的大小
❹ 渲染视频
❺ 保存表达式树输出
图 12.8 显示了在 250 代和 1,000 个体的进化过程中生成的视频的捕获输出。你可能看到代理在很短的时间内就解决了其中一个或两个环境。

图 12.8 一个代理同时解决两个环境
图 12.9 显示了最终进化的表达式树。如果你运行这个笔记本,你可能会看到不同的树,但总体上,它可能很相似。这个表达式树有趣的地方在于instinct1和instinct2函数的重用和链式调用。实际上,它们成为了方程中的主要操作符,但它们并没有像我们预期的那样被完全使用。注意,在大多数情况下,输入甚至没有与本能的原始输入对齐。

图 12.9 最终进化的表达式树
在这一点上,你可能想知道我们是否可以仅仅让代理进化一个单一函数,而不添加本能操作符。这是一个合理的问题,所以让我们看看在下一节中它是如何工作的。
12.3.3 无本能地进化通用解
在接下来的笔记本中,我们采取与上一个练习相反的方法,对本能和 IL 不做任何假设。这意味着我们允许代理通过推导一个没有本能操作符的单一代数来解决两个环境。
在 Google Colab 中打开 EDL_12_3_GEPPY_Generalize.ipynb 笔记本。如需帮助,请参阅附录。通过选择菜单中的“运行”>“运行所有”来运行笔记本中的所有单元格。
这个笔记本的唯一变化是用于构建表达式树的primitive集的定义,如列表 12.20 所示。与 IL 示例相比,主要变化是省略了本能操作符,并添加了protected_ div和ephemeral常量。除了额外的输入外,这个操作符集与我们用于单个环境上的基本函数推导所使用的操作符集相同。
列表 12.20 EDL_12_3_GEPPY_Generalize.ipynb:设置原始集
pset = gep.PrimitiveSet('Main', input_names=
➥ ['e', 'x', 'y', ''elocit'']) ’ ❶
pset.add_function(operator.add, 2)
pset.add_function(operator.sub, 2)
pset.add_function(operator.mul, 2)
pset.add_function(protected_div, 2) ❷
#pset.add_function(instinct1, 2) ❸
#pset.add_function(instinct2, 3) ❸
pset.add_ephemeral_terminal(name='enc', gen=lambda:
➥ random.randint(-10, 10)) ❹
❶ 使用相同的输入
❷ 包含复杂的操作符
❸ 不包含本能
❹ 添加常数
笔记本中的其余部分与我们之前看到的一样,所以请放松并观看进化过程——或者,也许,缺乏进化。确实,通过在多个环境中运行此示例,你会发现,在最理想的情况下,智能体只能接近解决单个环境。图 12.10 展示了显示这种失败的示例视频输出。

图 12.10 智能体无法在两个任务中推广的失败
那么,发生了什么?为什么将先前开发的函数或本能作为新函数导出的操作符比导出新方程更成功?对这个问题的简单答案是限制复杂性和选择。可重用的本能降低了在进化新函数时的复杂性和选择。
这种重用和开发可重用代码和函数的概念是当今软件开发最佳实践的基石。今天,编码者从众多先前开发的组件或函数中构建应用程序。今天软件行业所发展的,可能只是模仿数百万年前形成的进化最佳实践。
图 12.11 展示了本能如何随时间进化,以章鱼(章鱼)进化为例。在早期,生物体可能进化了硬编码的本能,这与我们在 EDL_12_2_GEPPY_Gyms.ipynb 笔记本中所做的不太一样。通过进化的另一个阶段,生物体可能进化出一种三级本能,使其能够同时使用两种本能。最后,在进化的最后阶段,头足类发展出一种新的本能类型:系统-2 本能,或允许其思考的本能。
头足类(章鱼)进化
章鱼是一种令人难以置信的生物,它以工具使用和有意识的行为形式表现出高级智力。使章鱼特别独特的是,它的进化路径与我们通常认为的高等生命形式从哪里发展出来的大相径庭。它还独特之处在于它没有中枢神经系统或大脑,据信章鱼的大多数思想是在其整个身体中产生的。了解头足类的进化可能会为我们提供关于 IL 的有效性以及所有有意识的思想可能如何进化的深刻见解。

图 12.11 进化过程中的 IL
在下一节中,我们将探讨一种可能的建模、开发和进化双过程系统级-2 本能的方法。在此之前,让我们看看一些有用的学习示例,这将有助于你的回忆。
12.3.4 学习练习
使用以下练习来提高你对材料的理解,也许甚至可以自己开发新的技巧和技术:
-
从笔记本示例 EDL_12_3_GEPPY_Instinctual.ipynb 的
原始集合中移除一个基本本能,然后再次运行练习。你只使用一个本能就能得到类似的结果吗? -
返回并运行 EDL_12_2_GEPPY_Gyms.ipynb 笔记本,以推导出使用不同运算符的新方程。尝试添加新的运算符,如
cos或sin。接下来,将这些方程用于笔记本 EDL_12_3_GEPPY_Instinctual .ipynb 中,以查看结果。 -
向 EDL_12_3_GEPPY_Instinctual.ipynb 添加更多运算符,包括本能,并看看这对代理泛化有什么影响。
在下一节中,我们继续探讨通过查看开发双过程系统-2 本能的另一种方法来泛化学习。
12.4 使用遗传编程进行泛化学习
遗传编程是我们使用 GEPPY 探索的 GEP 技术的基础。使用 GP,可以开发出能够用布尔逻辑模拟决策过程的结构化代码,例如。GP 不仅是一种强大的机器学习技术,能够开发出有趣的解决方案,而且还能阐明系统-2 思维过程或本能如何进化。
在本节中,我们查看一个经典的遗传编程示例:遗传蚂蚁。在这个示例中,一个 ant 代理通过在环境中搜索以找到并消耗食物而进化。这个示例是从标准的 DEAP 示例中派生出来的,并在此处修改以展示重要概念,并展示如何将 ant 泛化以从多个不同的环境中进食。
在 Google Colab 中打开 EDL_12_4_DEAP_Ant.ipynb 笔记本。如有需要,请参考附录。通过选择菜单中的“运行”>“运行所有”来运行笔记本中的所有单元格。
这个笔记本使用了 DEAP 的 GP 组件,因此示例有些不同,但大部分代码与我们之前多次看到的代码相同。GP 与 GEP 非常相似,使用 原始 集合来定义主要函数集,如列表 12.21 所示。与使用 GEPPY 的 GEP 不同,GP 中的函数不是表达式树,而是实际的代码实现。如果你向下滚动到 原始 集合的设置,可以看到如何添加基本函数。注意 PrimitiveSet 是如何构建的,它不接受任何输入。这是因为生成的代码在运行时会自行拉取所需的输入。接下来,我们看到添加了三个 原始 二进制或三元运算符,随后是终端节点函数。这些函数在 GP 表达式树或代码 例程 执行时执行。
列表 12.21 EDL_12_4_DEAP_Ant.ipynb:设置 原始 集合
ant = AntSimulator(600) ❶
pset = gp.PrimitiveSet("MAIN", 0) ❷
pset.addPrimitive(ant.if_food_ahead, 2) ❸
pset.addPrimitive(prog2, 2) ❸
pset.addPrimitive(prog3, 3) ❸
pset.addTerminal(ant.move_forward) ❹
pset.addTerminal(ant.turn_left) ❹
pset.addTerminal(ant.turn_right) ❹
❶ 代表代理或环境
❷ 一个没有输入的新集合
❸ 定义基本原始运算符
❹ 定义终端或执行函数
现在,我们可以查看用于定义蚂蚁代理逻辑的原始操作符的定义。这些函数的设置使用partial函数——一个允许基础函数被包装并暴露变量输入参数的辅助函数。蚂蚁使用的三个操作符是prog2、prog3和if_then_else,但请注意,在内部,每个函数都会执行它接收的终端输入,如下面的列表所示。这意味着高级操作符消耗布尔逻辑进行操作。因此,我们稍后将看到的终端函数返回True或False。
列表 12.22 EDL_12_4_DEAP_Ant.ipynb:设置逻辑函数
def progn(*args): ❶
for arg in args:
arg()
def prog2(out1, out2): ❷
return partial(progn,out1,out2)
def prog3(out1, out2, out3): ❸
return partial(progn,out1,out2,out3)
def if_then_else(condition, out1, out2): ❹
out1() if condition() else out2()
❶ 基础部分函数
❷ 操作符接受两个输入
❸ 操作符接受三个输入
❹ 条件操作符
终端函数被写入AntSimulator类中,如列表 12.23 所示。不要过于关注每个函数中的实际代码。这段代码处理蚂蚁代理在网格环境中的位置、移动和朝向。值得注意的是,这些终端函数既不接收也不输出任何输出。
列表 12.23 EDL_12_4_DEAP_Ant.ipynb:终端函数
def turn_left(self): ❶
if self.moves < self.max_moves:
self.moves += 1
self.dir = (self.dir – 1) % 4
def turn_right(self): ❷
if self.moves < self.max_moves:
self.moves += 1
self.dir = (self.dir + 1) % 4
def move_forward(self): ❸
if self.moves < self.max_moves:
self.moves += 1
self.row = (self.row + self.dir_row[self.dir]) % self.matrix_row
self.col = (self.col + self.dir_col[self.dir]) % self.matrix_col
if self.matrix_exc[self.row][self.col] == "food":
self.eaten += 1
self.matrix_exc[self.row][self.col] = "empty"
self.matrix_exc[self.row][self.col] = "passed"
❶ 将蚂蚁转向 90 度向左
❷ 将蚂蚁转向 90 度向右
❸ 蚂蚁向前移动并消耗任何食物
从终端函数中,我们继续查看蚂蚁实现的单操作符自定义函数。再次强调,函数内的代码会检查网格以确定蚂蚁是否感知到前方有食物以及它面对的方向。下面的列表中展示的sense_food函数就是检测蚂蚁当前是否面对食物的函数。
列表 12.24 EDL_12_4_DEAP_Ant.ipynb:自定义操作符函数
def sense_food(self): ❶
ahead_row = (self.row + self.dir_row[self.dir]) % self.matrix_row
ahead_col = (self.col + self.dir_col[self.dir]) % self.matrix_col
return self.matrix_exc[ahead_row][ahead_col] == "food"
def if_food_ahead(self, out1, out2): ❷
return partial(if_then_else, self.sense_food, out1, out2) ❸
❶ 自定义操作符函数
❷ 内部终端辅助函数
❸ 使用预定义的操作符函数 if_then_else
被称为evalArtificalAnt的evaluate函数用于确定个体的适应度很简单。它首先使用gp.compile将个体基因序列转换为编译后的 Python 代码。然后,使用AntSimulator的run函数运行输出例程。之后,根据蚂蚁消耗的食物方格数量输出蚂蚁的适应度,如下面的列表所示。
列表 12.25 EDL_12_4_DEAP_Ant.ipynb:fitness evaluate函数
def evalArtificialAnt(individual):
routine = gp.compile(individual, pset) ❶
ant.run(routine) ❷
return ant.eaten, ❸
❶ 编译成 Python 代码
❷ 运行脚本例程
❸ 根据消耗的食物返回适应度
AntSimulator的run函数是执行结果表达式代码树的地方。在执行之前,环境通常会重置。然后,如果蚂蚁代理还有剩余的动作,它会通过调用生成的或进化的例程函数来执行一个动作,如下面的列表所示。你可以将其视为某种形式的意识决策思维过程,这在双过程理论中被描述为系统 2。
列表 12.26 EDL_12_4_DEAP_Ant.ipynb:运行例程
def run(self,routine):
self._reset() ❶
while self.moves < self.max_moves: ❷
routine() ❸
❶ 重置模拟环境
❷ 检查是否有剩余的动作
❸ 执行 GP 代码
与我们之前查看的 Gym 环境 不同,AntSimulator 可以加载任意描述的环境,如下面的列表所示。我们首先尝试进化一个ant以在原始 DEAP 示例中成功。
列表 12.27 EDL_12_4_DEAP_Ant.ipynb:定义环境
%%writefile santafe_trail1.txt ❶
S###............................ ❷
...#............................
...#.....................###....
...#....................#....#..
...#....................#....#..
...####.#####........##......... ❸
............#................#..
............#.......#...........
............#.......#........#..
............#.......#...........
....................#...........
............#................#..
............#...................
............#.......#.....###...
............#.......#..#........
.................#..............
................................ ❹
............#...........#.......
............#...#..........#....
............#...#...............
............#...#...............
............#...#.........#.....
............#..........#........
............#...................
...##..#####....#...............
.#..............#...............
.#..............#...............
.#......#######.................
.#.....#........................
.......#........................
..####..........................
................................
❶ 将文件以名称写入文件系统
❷ S 代表起始位置。
❸ #是食物。
❺ .是空空间。
这个例子运行非常快,完成后,你可以实时见证最佳ant如何在环境中移动,如图 12.12 所示。当你观察ant时,注意它是如何穿过网格空间的。

图 12.12 ant 在环境中移动
这是一个很有趣的例子,可以运行和探索。它还展示了遗传编程在创建代码方面的力量,但更重要的是,它揭示了一种创建本能的系统级 2 思维过程的方法。为了进一步展示这种能力,让我们继续使用相同的笔记本。

图 12.13 添加两个环境
在 Colab 中继续使用 EDL_12_4_DEAP_Ant.ipynb 笔记本。假设笔记本已完成执行,我们只需查看剩余的单元格,以了解如何进化ant以泛化到不同环境。图 12.13 显示了我们加载到AntSimulator中的两个附加环境,希望进化的ants可以跨环境泛化。接下来,我们查看将那些环境添加到ant模拟器的代码,如下列 12.28 所示。
列表 12.28 EDL_12_4_DEAP_Ant.ipynb:将环境添加到模拟器
ant.clear_matrix() ❶
with open("santafe_trail2.txt") as trail_file:
ant.add_matrix(trail_file) ❷
with open("santafe_trail3.txt") as trail_file:
ant.add_matrix(trail_file) ❸
❶ 清除现有环境
❷ 添加环境 2
❸ 添加环境 3
在不进行任何进一步进化的情况下,我们可以通过简单地运行visual_run函数来测试当前最佳ant在这些新环境中的表现,如下面的列表所示。从在两个新环境中运行ant的结果来看,它们的性能并不好。我们可以通过同时在这三个环境中进化ant来改进这一点。
列表 12.29 EDL_12_4_DEAP_Ant.ipynb:测试ant
ant.visualize_run(routine) ❶
❶ 在新环境中可视化ant
在所有三个环境中进化ant现在只是将特定环境添加到模拟器中并重新运行进化的简单问题。内部,一个评估reset函数调用的ant将ant放入随机选择的环境。由于ant现在可以随机切换到不同的环境,所以生成的代码routine必须考虑到更好的食物搜索,如下面的列表所示。
列表 12.30 EDL_12_4_DEAP_Ant.ipynb:泛化ant
ant.clear_matrix() ❶
with open("santafe_trail1.txt") as trail_file:
ant.add_matrix(trail_file) ❷
with open("santafe_trail2.txt") as trail_file:
ant.add_matrix(trail_file) ❷
with open("santafe_trail3.txt") as trail_file:
ant.add_matrix(trail_file) ❷
GENERATIONS = 100 #@param {type:"slider", min:10, max:1000, step:5}
algorithms.eaSimple(pop, toolbox, 0.5, 0.2,
➥ GENERATIONS, stats, halloffame=hof) ❸
❶ 清除现有环境
❷ 添加新环境
❸ 进化ant
进化完成后,你可以通过再次运行ant.visualize_run函数调用来可视化蚂蚁现在的表现。这个练习展示了基因编程如何被用来泛化一个智能体以解决多个环境。它是通过将低级终端函数,或我们可能称之为活动或本能,与可能代表思维的更高级布尔逻辑分开来做到这一点的。因此,蚂蚁智能体不仅推导出一个单一的核心函数或表达式树,而是两个不同的操作或思维系统。
因此,基因编程是寻找描述双过程系统级 2 思维的本能或过程的潜在途径。但请记住,一个思维系统可能与其他系统不相似,并且还需要确定这能否导致 AI 和机器学习中的更高形式的一般化和意识。我们在下一节更深入地讨论了进化和寻找更高形式的人工智能和机器学习。在此之前,让我们先探索一些学习练习。
12.4.1 学习练习
使用以下练习来扩展你对基因编程和遗传蚂蚁问题的知识:
-
为
蚂蚁添加一些新的环境以供探索和进化。确保这些新环境有大致相同数量的食物方块。 -
想想你可以如何改变或添加终端函数。也许,你可以添加一个
跳或飞函数,使蚂蚁在它面对的方向上移动几个空间。 -
添加一个新的操作符
感知函数,如感知食物,可以在一定距离上感知食物或其他任何东西。
基因编程为我们提供了一个潜在的基础,以寻找更高阶、双过程系统级 2 功能或本能。我们在本书的下一节讨论了 IL 和 EDL 的潜在未来。
12.5 进化机器学习的未来
在本节中,我们探讨了进化搜索在改善机器学习(ML)和,当然,深度学习(DL)应用方面的未来。虽然本书的重点是深度学习,但进化搜索在其他形式的机器学习中有广泛的应用。进化搜索有潜力帮助我们引导到新的学习形式或子学习方法,如 IL。我们通过讨论进化本身是否可能出了问题,开始我们的旅程,探讨进化搜索可能带来的可能性,在下一节中。
12.5.1 进化是否出了问题?
近年来,我们对进化过程的理解受到了严厉的审视。现在,受到审视的进化并不是什么新鲜事,但这一次,批评者本身就是进化论者。进化论者声称,我们对进化过程的当前理解没有解释进化步骤之间的巨大变化。
达尔文本人也因非常类似的原因对进化理论质疑了 20 年。他挣扎于一种不安的感觉,即作为进化变化基石的突变无法发展出像人眼这样复杂的东西。随着时间的推移和大量的统计和化石证据,突变驱动的进化被接受。
如果你已经尝试过这本书中许多持续时间较长的练习,你可能也对进化也有过类似的感受。其中一些练习模拟了基于突变的进化过程,涉及成千上万的个体和成千代的世代,只产生了非常微小的变化。当然,质疑突变作为变化的主要导演是很容易的,但除此之外还有什么可能呢?
12.5.2 进化塑性
进化 塑性,源于表型塑性的概念,试图描述没有突变的可能遗传变化。基本概念是,遗传变化可能发生在生物在其一生中,然后这些变化被传递给未来的世代。这些变化不是随机的,就像突变那样,而是与其他生物和环境相互作用的直接结果。
我们在 DNA 研究和我们及其他物种基因组理解方面的现代和快速创新改变了我们对可能性的理解。我们不再需要通过选择性育种来强制遗传变化,而是现在可以直接修改基因组,并让这些修改传递给未来的世代。也已经显示,这些变化可以很容易地进行——这也对我们的进化理解提出了质疑。
CRISPR 技术
成簇规律间隔短回文重复序列(CRISPR)是一项非常新的技术,它允许人类通过基因拼接的方式修改自身和其他物种。在某些情况下,这意味着移除不良基因,而在其他情况下,它仅仅意味着替换基因以产生一些新的效果,比如发光。这项技术之所以令人印象深刻,是因为它提供了一种低技术手段来改变物种的基因组。例如,你可以在互联网上购买 CRISPR 套件,允许你改变细菌甚至青蛙的 DNA。
那么,问题就来了,基因组是否可以通过非常具体的环境变化来改变,以及我们如何在进化中解释这些变化。对于一个传统的进化论者来说,这可能只是另一种形式的突变,而突变可以有效地描述这些变化。然而,对于我们这些数字进化论者来说,这提供了其他形式模拟的一些有趣机会。
无论哪种方式,进化塑性可能表明我们对进化的理解是会变化的,随之而来的是数字进化搜索的应用,在这种搜索中,我们不是通过缓慢且罕见的变异来驱动进化,而是使用其他遗传算子,这些算子提供了更快、选择性驱动的变化。在下一节中,我们将查看我们的最终笔记本,该笔记本展示了具有塑性的进化。
12.5.3 使用塑性改进进化
在本节中的笔记本中,我们回顾了我们最初演示 GA 尝试复制图像(如蒙娜丽莎)的一个原始示例。在这里,我们进行了一个单一且简单的更改,实现了一个新的塑性算子,该算子可以用来增强数字进化。这个算子的实现只是对塑性算子可能如何工作的一种解释。
打开 EDL_12_5_Genetic_Plasticity.ipynb 笔记本以及 EDL_2_6_Genetic_Algorithms.ipynb,以便进行比较。请从菜单中选择运行>运行所有来运行这两个笔记本。
检查两个笔记本的代码。我们在这里只关注塑性算子的实现。在生物学中,塑性假设任何环境变化对生物体都是有益的,因此为了模拟塑性算子,我们首先确定个体的适应性作为基准,如下所示。然后,我们强制执行 100%的新个体变异机会,并确定变化的适应性。如果变化提高了适应性,则返回修改后的个体;否则,我们保留原始内容。
列表 12.31 EDL_12_5_Genetic_Plasticity.ipynb:遗传塑性算子
def plasticity(individual):
original = individual.copy() ❶
f1 = fitness(individual) ❷
indvidual = mutation(individual, 1.0) ❸
f2 = fitness(individual) ❷
if f1 < f2:
return individual ❹
else:
return original ❹
plasticity(pop[parents[0]])
❶ 创建副本以保留原始内容
❷ 在更改前后确定适应性
❸ 强制执行变异后的更改
❹ 返回适应性更高的个体
图 12.14 显示了第二章中原始遗传算子示例以及包含塑性的新算子的进化结果。这两个笔记本中的输出是通过在 10,000 代中进化 300 个个体的种群来生成的。

图 12.14 使用遗传塑性算子的结果
从计算角度来看,这种形式的塑性实现成本较高。如果你并排运行笔记本,你会清楚地注意到这种差异。然而,除了计算差异之外,明显的是这种新的塑性算子如何改进了更细微的细节。也许具有讽刺意味的是,改进后的笔记本中的眼睛首先变得清晰可辨。
为此例开发的 塑性 操作符是进化如何通过稍微修改的 变异 形式得到改进的一个例子。正如我们在这一独立例子中看到的,这个新操作符确实表现得更好。由于额外的计算费用和尚未证实的塑性理论,我们还没有在本书的例子中使用它。然而,它确实提出了一个关于生物和数字进化未来的有趣问题。然而,这个例子确实展示了在进化搜索中使用计算的限制,这是我们将在下一节中探讨的内容。
12.5.4 计算和进化搜索
使用进化搜索来优化深度学习(DL)或机器学习(ML)的一个可能的关键限制因素是额外的计算成本。这是我们在这本书的几个例子中见证并努力适应的事情。这也成为进化搜索在实际应用中的关键限制因素。
深度学习(DL)已经从游戏行业的进步中受益,这使得它能够使用快速的 GPU 来大幅降低计算成本。正是因为这些计算进步,DL 才有优势成为未来人工智能(AI)和机器学习(ML)的关键技术。但如果我们能够通过分布式计算或甚至量子计算机来改进计算上的进化搜索呢?
在这本书中,DEAP 已经与 Google Colab 结合使用,这限制了框架分布式计算能力的应用。然而,对于任何严肃的项目来说,使用分布式计算可能会大大减轻额外的计算成本。不幸的是,在这个领域还没有做很多工作,所以它的有效性还有待观察。
然而,如果进化搜索的成本或时间可以降低,那么这将为更昂贵的探索开辟更多的可能性。像 IL 或使用进化来寻找新的学习方法等技术可能更加实用。研究人员不必花费数小时开发新算法,搜索可能由进化来完成。在本书的下一节和最后一节中,我们将探讨一种使用 IL 和 DL 的技术。
12.6 本能深度学习和深度强化学习中的泛化
在本节的最后,我们查看了一个笔记本,展示了如何将 IL 应用到 DRL 中,以泛化多个环境中的智能体。这个练习模仿了我们之前开发的 IL 泛化例子,但这次是通过 DRL 应用。像我们在 DQN 例子中使用的 DRL 网络相当简单,为 IL 的应用提供了一个良好的基础。
图 12.15 展示了一对 DQN DRL 网络如何用于解决两个不同的环境。然而,每个网络的中层已经被分割成三个不同的可共享本能槽位,这两个网络学习的目标是找到一组共享的基本本能:本能池。

图 12.15 将 IL 应用于 DL
我们还可以在图 12.15 中看到,共享本能不必在两个网络中的相同位置。同样,正如我们在 Geppy 练习中看到的那样,重用的本能函数操作符通常混合匹配来解决两个环境。然而,与 Geppy 不同,我们严格限制这些本能的放置和操作方式,甚至更进一步,允许每个网络的顶层和底层专门针对环境进行训练。
打开 EDL_12_6_Instinctual_DQN_GYMS.ipynb 笔记本。请通过菜单中的“运行”>“运行所有”来运行这两个笔记本。这个笔记本是从 EDL_11_5_DQN_GYMS.ipynb 扩展而来的,所以如果你需要回顾 DQN 或 DRL,请参考第 11.5 节。
我们首先关注的是对DQNAgent类_build_model函数的修改,如列表 12.32 所示。为了将网络分割成功能块(本能),我们使用了 Keras 功能 API,它允许深度学习网络以函数的形式进行描述。这也意味着每个层部分都可以被视为一个函数。因此,我们不是从一组静态的层或函数定义中构建模型,而是将本能列表中的层块传递进去。这个列表的第一个和最后一个元素是专门为环境定义的层,中间的层或函数是可重用的本能。图 12.16 解释了如何将build_model函数从标准深度学习网络转换为本能网络。
列表 12.32 EDL_12_6_Instinctual_DQN_GYMS.ipynb:构建模型
def _build_model(self, instincts):
inputs = k.Input(shape=(self.state_size,)) ❶
dense_in = instincts[0] ❷
dense1 = instincts[1]
dense2 = instincts[2] ❷
dense3 = instincts[3]
dense_out = instincts[4] ❷
x = dense_in(inputs)
x1 = dense1(x) ❸
x2 = dense2(x)
x3 = dense3(x)
x = kl.concatenate([x1, x2, x3]) ❹
outputs = dense_out(x)
model = k.Model(inputs=inputs, outputs=outputs) ❺
model.compile(loss='mse', optimizer=k.optimizers.Adam(learning_rate=self.learning_rate))
return model
❶ 切换到 Keras 功能 API
❷ 加载层/本能
❸ 执行前向传递
❹ 连接本能
❺ 构建/编译并返回模型

图 12.16 转换为 IL
这个笔记本包含几个示例单元,展示了如何填充层本能列表并用于创建DQNAgent。在这里,我们关注的是如何为多个环境创建本能层,如图列表 12.33 所示。这段代码在共享层池中创建了本能的基础集合——在这种情况下,是一个具有8个节点的标准Dense层,使用ReLU激活。然后,我们为输入和输出创建了特定于环境的层,以及每个环境的内存dequeue。在这个例子中,我们只使用了两个环境,所以四个层的池子就足够了。如果你将这项技术应用于超过两个环境,你可能需要增加共享池的大小。
列表 12.33 EDL_12_6_Instinctual_DQN_GYMS.ipynb:创建层
ENVIRONMENTS = len(environments) ❶
LAYERS = [ ❷
kl.Dense(8, activation="relu"),
kl.Dense(8, activation="relu"),
kl.Dense(8, activation="relu"),
kl.Dense(8, activation="relu"),
]
input_layers = [kl.Dense(24, activation="relu")
➥ for e in environments] ❸
output_layers = [kl.Dense(e.action_space.n,
➥ activation="linear") for e in environments] ❹
memories = [ deque(maxlen=2000) for e in environments] ❺
❶ 获取环境的数量
❷ 创建本能池
❸ 创建特定于输入环境的层
❹ 创建特定于输出环境的层
❺ 创建环境内存的持有者
现在,为了找到基本本能(功能层)是如何共享和重用来同时解决两个环境的,我们当然使用进化搜索,回归到我们老朋友:带有 DEAP 的遗传算法(GAs)。由于我们每个环境只使用三个本能,因此可以构建一个简单的基因序列,每个环境有三个基因,其中每个基因代表对共享本能层池的索引。图 12.17 展示了基因序列是如何构建的,其中每个环境模型描述了一组索引,这些索引链接回共享层池。

图 12.17 个体基因序列
我们可以在evaluate函数中看到这一切是如何结合在一起的。代码首先遍历每个环境,将基因序列转换为层索引。然后,构建一组特定和共享层以传递到模型中。然后,为每个环境构建一个新的代理模型并进行评估。注意,在以下列表中,训练被阻塞,直到evaluate函数以train=True被调用——我们很快就会明白这是为什么。
列表 12.34 EDL_12_6_Instinctual_DQN_GYMS.ipynb:evaluate 函数
def evaluate(individual, train=False):
total = 0
for i, env in enumerate(environments):
rewards = 0
layer1 = convert_layer(individual[i*3]) ❶
layer2 = convert_layer(individual[i*3+1]) ❶
layer3 = convert_layer(individual[i*3+2]) ❶
instincts = [input_layers[i],
LAYERS[layer1],
LAYERS[layer2],
LAYERS[layer3],
output_layers[i],
] ❷
state_size = env.observation_space.shape[0]
action_size = env.action_space.n
agent = DQNAgent(instincts, state_size,
➥ action_size, memories[i]) ❸
state = env.reset()
state = np.reshape(state, [1, state_size])
done=False
while not done: ❹
action = agent.act(state)
next_state, reward, done, _ = env.step(action)
rewards += reward
next_state = np.reshape(next_state, [1, state_size])
agent.remember(state, action, reward, next_state,
➥ done) ❹
state = next_state
total += rewards
if train:
agent.replay(32) ❺
print(total/len(environments))
return total/len(environments), ❻
❶ 从基因序列中提取层索引
❷ 为模型设置层
❸ 为层创建代理包装器
❹ 在环境中评估代理模型
❺ 仅在需要时进行训练
❻ 返回平均适应度
我们不针对每个个体训练派生模型和特定及共享层的原因是,这可能会导致内部冲突或二义性。我们在尝试在不使用本能的情况下在两个环境中训练单个模型时看到了这一点。相反,我们的目标是只为每个代训练最佳的代理。这意味着所有层在每个进化代中只针对最佳的个体进行一次训练或更新。如果我们试图坚持严格的进化搜索,我们永远不会这样做。但如果我们借鉴之前提到的可塑性概念,我们可以接受这种训练过程是来自环境的反应性改进或变异。
这意味着在进化环境时,我们现在在最佳当前个体上执行深度学习层(DL)的训练,如列表 12.35 所示。注意,没有进行其他训练,这意味着更新或调整模型权重是针对模型的本能的,这些本能可能不同。正因为我们的 DQN 代理具有这种近乎随机的特性,所以我们从训练中移除了任何探索或随机性。如果你查看DQNAgent.act函数,你会看到这一点。
列表 12.35 EDL_12_6_Instinctual_DQN_GYMS.ipynb:进化本能
for g in range(NGEN):
pop, logbook = algorithms.eaSimple(pop, toolbox, ❶
cxpb=CXPB, mutpb=MUTPB, ngen=RGEN, stats=stats, halloffame=hof,
➥ verbose=False)
best = hof[0]
best_fit = evaluate(best, train=True) ❷
❶ 执行一代进化
❷ 在最佳个体上运行评估并进行训练
这个例子现在已经被简化,以解释性能,在适当的情况下。它可以产生一组通用的本能,可以用来解决两种环境,但进化和训练需要时间。一个更健壮的本能深度学习器可能会使用更小和更多数量的本能,甚至可能共享输入和输出层的本能。
IL 的主要目标是找到一组基础本能或函数,可以在不同的任务上重复使用以泛化模型。目标是允许重复使用函数(本能)以在系统泛化学习方面表现出色。IL 的次要目标是找到可以动态重组这些本能的函数(本能),通过过程或思维。
IL 仍处于发展的早期阶段,但正接近解决其首要目标。仍然有几种替代的 DL 网络类型、应用和进化搜索方法可以应用。我希望能有研究人员和学生拥抱寻找这些在网络中共享的基本能函数的道路。
摘要
-
进化可以应用于许多其他形式的机器学习,并为扩展我们的知识提供了新的机会。
-
Geppy 和基因表达式编程是 GAs 的扩展,通过编写连贯的代码或函数来工作。Geppy 可以是生成复杂函数逼近问题的可读函数的绝佳工具。
-
Geppy 可以作为函数逼近工具应用于 DRL 问题上的进化搜索,以生成单个可读的函数。生成的函数可以用来应用并解决来自 OpenAI Gym 工具包的复杂控制环境。
-
IL 是一种将进化方法与深度学习结合的模式,以寻找建模和解决复杂问题的新的方法。
-
IL 的基础是本能,或模型或生物体的核心学习行为:
-
在自然界中,生物体发展出本能来解决任务,如进食或行走。本能的数量、大小和复杂性往往随着生命形式的复杂化而减少。
-
相反,生命形式通常必须学会控制其基本本能以完成基本或复杂任务。
-
-
进化的概念以及我们对进化的理解可能会适应和进化,以使我们能够进一步进化机器学习和深度学习。像进化可塑性这样的理论概念——生物体可能能够在繁殖之外进化——可能在进化计算和进化深度学习中具有有趣的应用。
-
IL 的基本原理可以通过将 DQN 代理网络分解为本能,然后让代理针对多个任务训练这些本能来展示。能够使用 IL 泛化到多个任务的代理可以展示出在任务之间接近更通用的模型。
附录。
A.1 访问源代码
书籍的源代码位于github.com/cxbxmxcx/EvolutionaryDeepLearning。您不需要将任何代码下载到本地机器上;所有代码都将运行在 Google Colaboratory 上,或简称 Colab。要打开您的第一个笔记本,请按照以下步骤操作:
-
访问
github.com/cxbxmxcx/EvolutionaryDeepLearning,并点击图 A.1 中显示的链接打开其中一个样本笔记本。![APPA_F01_Lanham.png]()
图 A.1 在代码库视图中点击笔记本链接
-
这将打开笔记本的视图。顶部将有一个 Colab 徽章(图 A.2)。点击徽章以在 Colab 中打开笔记本。
![APPA_F02_Lanham.png]()
图 A.2 样本笔记本的视图
-
一旦打开笔记本,您就可以跟随书中的其他练习。
Google Colab Pro
多年来,Colab 已经从最初的一个优秀的免费 Python 计算平台,转变为一个更具商业驱动力的平台。尽管它仍然是一个优秀的平台,但对于免费用户来说,现在对 GPU 资源的访问已经受限。因此,如果你发现自己大量使用 Colab,购买 Colab Pro 许可证可能是有益的。
A.2 在其他平台上运行代码
Colab 还可以连接到辅助的 Jupyter 运行时,无论是本地托管还是其他云资源。这只有在您配置了计算/GPU 资源并且能够复制 Colab 的设置时才有益。您可以通过点击位于右上角菜单栏中的运行时下拉菜单来访问此选项和进一步说明(图 A.3)。

图 A.3 Google Colab 运行时菜单
当然,另一种选择是将代码转换为 Python 并在笔记本之外运行它。为此,只需使用 Colab 中的功能将笔记本下载为 Python 文件。您可以通过导航到文件 > 下载 > 下载.py(如图 A.4 所示)来访问此功能。

图 A.4 下载笔记本作为 Python 文件




浙公网安备 33010602011771号