人工智能初学者实用指南-全-
人工智能初学者实用指南(全)
原文:
annas-archive.org/md5/eea58b044fd8c81a77190f836ee3c193译者:飞龙
前言
像 Alexa 和 Siri 这样的虚拟助手处理我们的请求,谷歌的汽车已经开始读取地址,亚马逊的价格和 Netflix 的推荐视频由人工智能决定。人工智能是最令人兴奋的技术之一,并且在现代世界中变得越来越重要。
初学者的动手人工智能将教你什么是人工智能,以及如何设计和构建智能应用程序。本书将教你如何利用像 TensorFlow 这样的包来创建强大的 AI 系统。你将首先回顾人工智能的最新变化,并了解人工神经网络(ANNs)如何使人工智能变得更加智能。你将探索前馈、递归、卷积和生成神经网络(FFNNs,...)
本书适合人群
本书面向 AI 初学者、想成为 AI 开发者的人以及对利用各种算法构建强大 AI 应用感兴趣的机器学习爱好者。
为了最大限度地发挥本书的作用
本章中的代码可以直接在 Jupyter 和 Python 中执行。本书的代码文件可以在以下章节中提供的 GitHub 链接找到。
下载示例代码文件
你可以从你的账户在www.packt.com下载本书的示例代码文件。如果你从其他地方购买了本书,可以访问www.packt.com/support,注册后将文件直接通过电子邮件发送给你。
你可以通过以下步骤下载代码文件:
-
登录或注册到www.packt.com。
-
选择 SUPPORT 选项卡。
-
点击“代码下载与勘误”。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
一旦文件下载完成,请确保使用最新版本解压或提取文件夹:
-
适用于 Windows 的 WinRAR/7-Zip
-
适用于 Mac 的 Zipeg/iZip/UnRarX
-
适用于 Linux 的 7-Zip/PeaZip
本书的代码包也托管在 GitHub 上,链接为github.com/PacktPublishing/Hands-On-Artificial-Intelligence-for-Beginners。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还提供了来自我们丰富的书籍和视频目录中的其他代码包,访问github.com/PacktPublishing/。赶快去看看吧!
使用的约定
本书中使用了多种文本约定。
CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名。示例如下:“tf.add函数将一个层添加到我们的网络中。”
代码块的设置如下:
import tensorflow as tffrom tensorflow.examples.tutorials.mnist import input_datamnist = input_data.read_data_sets("/tmp/data/", one_hot=True)
任何命令行输入或输出都如下所示:
tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(loss_func)
粗体:表示新术语、重要词汇或屏幕上显示的词汇。例如,菜单或对话框中的词语会显示为...
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中注明书名,并通过customercare@packtpub.com与我们联系。
勘误:虽然我们已尽全力确保内容的准确性,但错误仍然会发生。如果您在本书中发现错误,我们将不胜感激,如果您能将此报告给我们。请访问 www.packt.com/submit-errata,选择您的书籍,点击“勘误提交表单”链接,并输入相关详情。
盗版:如果您在互联网上发现我们作品的任何非法复制品,请您提供相关位置地址或网站名称。请通过copyright@packt.com与我们联系,并附上该素材的链接。
如果您有兴趣成为作者:如果您在某个领域拥有专业知识,并且有兴趣撰写或为书籍贡献内容,请访问 authors.packtpub.com。
评论
请留下评论。在您阅读并使用了本书后,为什么不在您购买书籍的网站上留下评论呢?潜在读者可以看到并参考您客观的意见来做出购买决定,我们在 Packt 也可以了解您对我们产品的看法,我们的作者可以看到您对他们书籍的反馈。谢谢!
欲了解更多关于 Packt 的信息,请访问 packt.com。
第一章:人工智能的历史
人工智能(AI)这个术语承载着沉重的分量。人工智能已经受益于超过 70 年的研究与发展。人工智能的历史丰富而曲折,但有一个不变的事实——不知疲倦的研究人员通过资金的增长与波动、承诺与疑虑,推动着我们实现越来越现实的人工智能。
在我们开始之前,让我们先剔除流行词汇和营销术语,明确人工智能的真正含义。为了本书的目的,我们将依赖以下定义:
人工智能是一个系统或算法,使计算机能够执行任务,而无需明确编程。
人工智能是一个跨学科的领域。虽然本书主要聚焦于深度学习的应用,但这一领域还涵盖了机器人学和物联网的元素,并且与广义自然语言处理研究有着紧密的重叠(如果它还没有完全吞并的话)。它还与人机交互(HCI)等领域密切相关,因为将人工智能融入我们的生活和现代世界变得越来越重要。
人工智能经历了几波浪潮,未来还会经历另一波(可能规模较小)。每次,我们都通过可用的计算能力推动人工智能的极限,而研究与开发会停滞不前。这个时代可能有所不同,因为我们受益于日益庞大且高效的数据存储、快速、廉价的计算能力以及一些世界上最赚钱的公司的资金支持。为了理解我们是如何走到今天这一步的,让我们从头开始。
在本章中,我们将涵盖以下主题:
-
人工智能的起源 – 1950–1974
-
重生 – 1980–1987
-
现代时代的到来 – 1997–2005
-
深度学习与未来 – 2012 年至今
人工智能的起源 – 1950–1974
自最早的数学家和思想家开始,人工智能就是一个被长期追寻的概念。古希腊人创造了自动机的神话,一种机器人形式,负责完成他们认为琐碎的任务,而在历史的早期,思想家们思考着什么是人类,并且是否能够复制人类智能。虽然很难准确确定人工智能作为一门研究领域的起点,但它的发展与计算机科学的早期进展是相似的。可以说,计算机科学作为一门学科,正是源于这种早期的愿望,即创造能自我思考的机器。
在第二次世界大战期间,英国数学家和密码破译专家艾伦·图灵开发了...
重生 – 1980–1987
1980 年代,深度学习的诞生标志着人工智能的“大脑”,成为现代人工智能研究的焦点。随着约翰·霍普菲尔德和大卫·鲁梅尔哈特对神经网络研究的复兴,以及日本、美国和英国的多项资助计划,人工智能研究重新回到了正轨。
1980 年代初,美国仍在忍受人工智能冬天的影响时,日本却资助了第五代计算机系统项目,旨在推动人工智能研究。在美国,国防高级研究计划局(DARPA)再次增加了对人工智能研究的资助,商业界也重新对人工智能应用产生了兴趣。IBM 的 T.J.沃森研究中心发布了语言翻译的统计方法(aclanthology.info/pdf/J/J90/J90-2002.pdf),该方法用概率模型取代了传统的基于规则的自然语言处理模型,开创了现代自然语言处理(NLP)的时代。
来自剑桥大学的学生辛顿(Hinton)坚持自己的研究,最终通过创造深度学习这一术语成名。他与鲁梅哈特(Rumelhart)合作,成为首批引入反向传播算法用于训练人工神经网络(ANNs)的研究人员之一,这一算法是现代深度学习的基础。与许多早期的研究者一样,辛顿也受到计算能力的限制,直到 26 年后,他的发现才真正产生深远影响。
到 1980 年代末期,个人计算革命与未达成的预期威胁着人工智能领域。商业化发展几乎停滞,主机计算机制造商停止生产能够处理人工智能相关语言的硬件,而面向人工智能的主机计算机制造商也纷纷破产。似乎一切都陷入了停滞。
现代时代的到来——1997-2005 年
1997 年,IBM 的深蓝系统(Deep Blue)战胜了世界象棋冠军加里·卡斯帕罗夫(Garry Kasparov),人工智能再次进入了公众视野。不到一年后,辛顿的前学生扬·勒昆(Yann LeCun)在贝尔实验室(Bell Labs)开发了卷积神经网络(CNN),这一技术得益于反向传播算法和多年在计算机视觉任务上的研究。霍赫雷特(Hochreiter)和施密杜贝尔(Schmidhuber)发明了首个记忆单元——长短期记忆单元(LSTM),至今仍广泛用于序列建模。
人工神经网络(ANNs)仍然面临许多挑战。计算和存储的限制使得这些网络无法扩展,而支持向量机(SVMs)等其他方法应运而生,成为替代方案。
深度学习与未来——2012 年至今
在过去的几年里,人工智能取得的进展超过了自其诞生以来 60 多年的所有发展。人工智能的普及得到了其日益显现的公共利益的进一步推动——自动驾驶汽车、个人助手,以及在社交媒体和广告中无处不在的应用。在大多数历史时期,人工智能是一个与普通大众几乎没有互动的领域,但现在它已经成为国际讨论的前沿话题。
今天的人工智能时代是三大趋势的结果:
-
人工智能研究人员和从业者可以获得的数据量和计算能力不断增加
-
乔弗里·辛顿(Geoffrey Hinton)及其多伦多大学实验室正在进行关于深度神经网络的持续研究
-
人工智能的日益普及的公共应用推动了人工智能的采纳并进一步融入主流技术文化
如今,公司、政府和其他组织已经从 2000 年代中期的大数据革命中受益,这一革命带来了大量的数据存储。最终,人工智能应用拥有了所需的数据进行训练。计算能力变得廉价,并且只会变得更加便宜。
在研究方面,2012 年,Hinton 和他的两名学生终于证明了深度神经网络能够在大规模视觉识别挑战中超越所有其他方法,成为人工智能现代时代的起点。
有趣的是,Hinton 团队在计算机视觉方面的工作也引入了利用图形处理单元(GPUs)来训练深度网络的想法。它还引入了 dropout 和 ReLu,这些已经成为深度学习的基石。我们将在接下来的章节中讨论这些内容。今天,Hinton 是全球引用次数最多的人工智能研究员。他是谷歌大脑的首席数据科学家,并且与现代人工智能的许多重大进展息息相关。
人工智能在 2011 年 IBM Watson 击败世界“危险边缘”冠军,以及 2016 年谷歌的 AlphaGo 击败世界围棋冠军之后,进一步进入了公众视野。
今天,我们比以往任何时候都更接近拥有能够通过图灵测试的机器。神经网络能够生成越来越真实的演讲、图像和写作模仿。强化学习方法和 Ian Goodfellow 的生成对抗网络(GANs)已经取得了令人难以置信的进展。最近,关于揭示深度神经网络内部工作原理的研究也在不断涌现。然而,随着这一领域的进展,我们也应当谨慎对待过度承诺。纵观人工智能历史,企业往往对人工智能的能力做出过高的承诺,结果我们经常看到能力上的持续失望。将人工智能的能力仅限于某些应用,并继续从生物学的角度来看待这一领域的研究,反而会阻碍其未来的发展。然而,在本书中,我们将看到今天的实际应用是有方向且现实的,人工智能领域正比以往任何时候都更接近实现真正的人工智能。
摘要
自 20 世纪 40 年代和 50 年代起,人工智能取得了巨大的进展。我们今天所使用的许多技术和思想直接基于这些早期的发现。在 20 世纪后半叶,像 Geoffrey Hinton 这样的先驱者通过高潮与低谷推动了人工智能的发展。今天,我们正朝着实现持续的人工智能发展迈进,且前景可期。
人工智能技术的发展与新硬件的进步以及越来越庞大的数据源密切相关。正如本书中所述,优秀的人工智能应用都是在考虑数据限制和硬件优化的基础上构建的。下一章将向您介绍机器学习的基本原理和……
第二章:机器学习基础
人工智能(AI)根植于数学和统计学。在创建人工神经网络(ANN)时,我们在表示线性空间的数据上进行数学运算;从本质上讲,它是应用数学和统计学。机器学习算法不过是函数逼近;它们试图找到输入与正确输出之间的映射关系。我们使用代数方法来创建能够学习这些映射关系的算法。
几乎所有机器学习都可以通过一个相对简单的公式来表示;将数据集和模型结合起来,再加上适用于数据集和模型的损失函数与优化技术。本节旨在回顾理解AI 底层原理所必需的基本数学工具和技术。
在本章中,我们将回顾线性代数和概率内容,然后进入基本和基础机器学习算法与系统的构建,最后介绍可以在未来所有方法中使用的优化技术。虽然我们在本章及后续章节中将使用数学符号和表达式,但我们将重点将这些概念转化为 Python 代码。通常,Python 比数学表达式更易读易懂,能够帮助读者更快入门。
在本章中,我们将涵盖以下主题:
-
应用数学基础
-
概率论
-
构建基础机器学习算法
技术要求
在本章中,我们将使用 Python 3 和 scikit-learn 科学计算包。你可以通过在终端或命令行中运行 pip install sklearn 来安装该包。
应用数学基础
当我们谈到与深度学习和 AI 相关的数学时,我们通常指的是线性代数。线性代数是一个连续数学的分支,涉及到向量空间的研究以及在向量空间中进行的运算。如果你还记得小学时学过的代数,代数通常处理未知变量。线性代数则是将这种研究扩展到具有任意维度的线性系统,这使得它成为一种连续数学的形式。
AI 依赖于张量这一基本构建块。在 AI 中,这些数学对象存储着在人工神经网络(ANN)中运作所需的信息;它们是 AI 中广泛使用的数据结构。正如我们将看到的,张量有一个秩,它本质上告诉我们数据的索引(即数据的行列数)。
尽管深度学习中的许多问题并不是严格的线性问题,但矩阵和张量的基本构建块仍然是 ANN 中解决、优化和逼近问题的主要数据结构。
想看看线性代数如何从编程角度帮助我们吗?看看下面的代码块:
import numpy as np
## Element-wise multiplication without utilizing linear algebra techniques
x = [1,2,3]
y = [4,5,6]
product = []
for i in range(len(x)):
product.append(x[i]*y[i])
## Element-wise multiplication utilizing linear algebra techniques
x = np.array([1,2,3])
y = np.array([4,5,6])
x * y
我们可以通过简单利用 NumPy 内建的线性代数函数来消除繁琐的循环。当你想到 AI 时,成千上万的操作必须在应用程序的运行时进行计算,线性代数的构建块也能帮助我们在编程上更高效。在接下来的部分中,我们将回顾这些基本概念,包括数学符号和 Python 代码。
以下示例将使用 Python 包 NumPy;import numpy as np
构建块 —— 标量、向量、矩阵和张量
在接下来的部分中,我们将介绍在 AI 应用中广泛使用的线性代数基本对象类型;标量、向量、矩阵和张量。
标量
标量只是单一的实数,可以是整数或浮动小数。在 Python 中,我们通过简单地赋值来创建标量:
my_scalar = 5
my_scalar = 5.098
向量
向量是单维的整数数组。从几何学角度来看,它们存储的是从一个点出发的变化方向和大小。当我们在接下来的几页中讨论主成分分析(PCA)时,您将看到向量如何在机器学习算法中起作用。在 Python 中,向量作为 numpy array 对象创建:
my_vector = np.array([5,6])
向量可以通过几种方式来书写:

矩阵
矩阵是包含行和列的二维数字列表。通常,矩阵中的行用 i 表示,列用 j 表示。
矩阵表示为:

我们可以像创建向量一样,轻松在 Python 中创建矩阵,作为 NumPy 数组:
matrix = np.array([[5,6], [6,9]])
唯一的区别是,我们向数组中添加了一个额外的向量来创建矩阵。
张量
虽然你可能以前听说过向量和矩阵,但张量这个名称可能对你来说是新的。张量是广义的矩阵,它们有不同的大小或秩,用来衡量它们的维度。
张量是三维(或更多)列表;你可以将它们视为一种多维数字对象,例如立方体。张量具有独特的传递性和形式;如果一个张量变换了另一个实体,它自己也必须发生变化。任何秩为 2 的张量都可以表示为矩阵,但并非所有矩阵都是自动的秩为 2 的张量。一个张量必须具备这种传递性。正如我们接下来会看到的,这在下一章的神经网络中将会发挥作用。我们可以在 Python 中创建如下所示的张量:
tensor = [[[1,2,3,4]],[[2,5,6,3]],[[7,6,3,4]]] ...
矩阵运算
人工神经网络(ANN)的基本运算基于矩阵运算。在这一部分,我们将回顾理解 ANN 机制所需掌握的基本运算。
标量运算
标量操作涉及向量(或矩阵)和标量。要对矩阵执行标量操作,只需将标量应用于矩阵中的每个元素:

在 Python 中,我们只需要做以下操作:
vector = np.array([[1,2], [1,2]])new_vector = vector + 2
逐元素操作
在逐元素操作中,位置非常重要。对应位置的值会被结合起来形成一个新的值。
要加法和/或减法矩阵或向量:


在 Python 中:
vector_one = np.array([[1,2],[3,4]])
vector_two = np.array([[5,6],[7,8]])
a + b
## You should see:
array([[ 6, 8],[10, 12]])
array([[ 6, 8],[10, 12]])
a - b
## You should see:
array([[-4, -4], [-4, -4]])
我们可以在向量上执行两种形式的乘法操作:点积 和 哈达玛积。
点积是乘法的一个特例,根植于更大的几何学理论中,这些理论被广泛应用于物理学和计算科学中。它是一个更一般的数学原理的特例,称为内积。在使用两个向量的点积时,输出是一个标量:

点积是机器学习中的重要工具。想象一个基本操作:假设我们在做一个简单的分类问题,想要判断一张图像是包含猫还是狗。如果我们用神经网络来做这个分类,它的表现将如下:

这里,y 是我们的分类结果,表示猫或狗。我们通过利用表示为 f 的网络来确定 y,其中输入是 x,而 w 和 b 分别表示权重和偏置因素(不用担心,我们将在接下来的章节中更详细地解释这些内容!)。我们的 x 和 w 都是矩阵,我们需要输出一个标量
,它代表猫或狗。我们只能通过对 w 和
进行点积来实现这一点。
回到我们的例子,如果这个函数输入的是一张未知的图像,计算点积可以告诉我们新向量与猫向量 (a) 或狗向量 (b) 的方向相似度,通过它们之间的角度来衡量(
):

如果向量更接近猫向量 (a) 的方向,我们会将图像分类为包含猫。如果它更接近狗向量 (b) 的方向,我们会将图像分类为包含狗。在深度学习中,这种情境的更复杂版本会反复执行;这是人工神经网络(ANNs)工作的核心。
在 Python 中,我们可以通过使用 numpy 中的内建函数 np.dot() 来计算两个向量的点积:
## Dot Product
vector_one = np.array([1,2,3])
vector_two = np.array([2,3,4])
np.dot(vector_one,vector_two) ## This should give us 20
另一方面,哈达玛积输出的是一个向量:

Hadamard 积是逐元素运算,这意味着新矩阵中的每个数字都是来自前一个矩阵中数字的标量倍数。回到 Python,我们可以使用简单的*操作符轻松执行此操作:
vector_one = np.array([1,2,3])
vector_two = np.array([2,3,4])
vector_one * vector_two
## You should see:
array([ 2, 6, 12])
既然我们已经了解了基本的矩阵运算,接下来让我们看看概率论如何在人工智能领域帮助我们。
基本统计学和概率理论
概率,即用于建模不确定场景的数学方法,是支撑人工智能算法的基础,帮助我们理解系统应该如何推理。那么,什么是概率呢?我们将其定义如下:
概率是一个频率,表示为样本大小 n 的分数 [1]。
简单来说,概率是对不确定性的数学研究。在这一部分,我们将介绍概率空间和概率分布的基础知识,以及解决简单问题的有用工具。
概率空间与一般理论
当讨论概率时,通常会提到某个事件发生的概率。例如,今天会下雨吗?苹果的价格会上涨还是下跌?在机器学习的背景下,概率告诉我们诸如评论被分类为正面或负面,或者信用卡是否会发生欺诈交易等事件的可能性。我们通过定义所谓的概率空间来衡量概率。概率空间是对某些事件概率的如何与为什么的度量。概率空间由三个特征定义:
-
样本空间,告诉我们可能的结果或情况
-
一组定义明确的事件;例如两个欺诈性的信用卡交易
-
每个事件的概率度量
虽然概率空间是一个值得单独研究的主题,但为了更好地理解,我们将坚持这个基本定义。
在概率论中,独立性的概念至关重要。独立性是指一个随机变量的值不受另一个随机变量值的影响。这在深度学习中是一个重要假设,因为非独立的特征往往会相互交织,并影响我们模型的预测能力。
在统计学中,关于事件的数据集合称为样本,它是从一个称为总体的理论数据超集中抽取的,整体代表了关于某个分组或事件的所有已知信息。例如,如果我们在街头对人们进行政治观点 A 或政治观点 B 的调查,我们将从总体中生成一个随机样本,总体是我们进行调查的城市、州或国家的所有人。
假设我们想使用这个样本来预测一个人持有两种政治观点之一的可能性,但我们主要对支持政治观点 A 的事件参与者进行了调查。在这种情况下,我们可能会有一个偏倚样本。在抽样时,重要的是要采取随机样本,以减少偏倚,否则我们使用样本进行的任何统计分析或建模也将存在偏倚。
概率分布
你可能见过类似下面的图表;它显示了数据集中的值以及这些值出现的次数。这被称为变量的分布。在这个特定的例子中,我们通过使用直方图来显示分布,直方图显示了变量的频率:

在这一部分,我们关注一种特定类型的分布,称为概率 分布。当我们讨论概率分布时,我们在谈论随机变量取某个特定值的可能性,我们通过将前面频率的值进行划分来创建一个分布……
概率质量函数
概率质量函数 (PMFs)是离散分布。该分布的随机变量可以取有限个值:

PMF 与我们通常看到的分布有些不同,这是因为它们的有限性。
概率密度函数
概率密度函数 (PDFs)是连续分布;来自该分布的值可以取无限多个值。例如,参见以下图像:

你可能见过类似的东西;它是标准正态分布或高斯分布的概率密度函数。
条件概率与联合概率
条件概率是指在已知y发生的情况下,x发生的概率。它是概率论中推理不确定性的关键工具之一。假设我们在谈论你中奖的概率,假设今天是晴天。也许你觉得今天很幸运!我们如何将这个写成概率表达式呢?它将是你中奖的概率A,给定今天是晴天的概率B,因此是P(A|B)。
联合概率是指两件事同时发生的概率:你中奖并且那天是晴天的概率是多少?
联合概率链式法则
联合概率在人工智能领域中非常重要;它是生成模型背后的机制,这些模型能够复制声音、图片和其他非结构化信息。这些模型学习现象的联合概率分布。它们生成给定对象或事件的所有可能值。链式法则是一种评估两个变量联合概率的技术。正式写作如下:

条件概率的贝叶斯定理
贝叶斯定理是机器学习领域中概率论的另一个基本原则。它允许我们通过反转事件的条件来计算某个事件发生的条件概率。贝叶斯定理的正式表达式为:

让我们使用贝叶斯定理来解决一个简单的条件概率问题。在下表中,我们看到一个患者接触到疾病的可能性:
| 疾病 (1%) | 无疾病 (99%) | |
|---|---|---|
| 测试阳性 | 80% | 9.6% |
| 测试阴性 | 20% | 90.4% |
我们如何解读这个表格?x轴告诉我们患病人群的百分比;如果你得了病,你就明确地位于“疾病”列。基于这个条件,y轴则表示你测试为阳性或阴性的可能性,这取决于你是否真正患病。
现在,假设我们得到了一个阳性测试结果;那么我们实际患病的概率是多少呢?我们可以使用贝叶斯公式来解答:

我们的答案是 7.8%,即在阳性测试结果的情况下,实际患病的概率为:
在以下代码中,我们可以看到如何基于可能性来运用贝叶斯公式建模这些条件事件。在机器学习和人工智能中,这在建模情境或分类物体时非常有用。条件概率问题也涉及到判别模型,我们将在生成对抗网络一节中讨论这些模型:
.p_diseasePos = 0.8 ## Chance of having the disease given a positive result
p_diseaseNeg = 0.2 ## Chance of having the disease given a negative result
p_noPos = 0.096
p_noNeg = 0.904
p_FalsePos = (.80 * .01) + (.096 * .99)
p_disease_given_pos = (.80 * .01) / p_FalsePos
print(p_disease_given_pos)
记住:进行乘法运算时,操作类型是非常重要的。我们可以使用哈达玛积(Hadamard 积)来乘以两个大小相等的向量或矩阵,输出将是另一个大小相等的向量或矩阵。在需要单个数字输出的情况下,我们使用点积(dot product)。点积在机器学习和深度学习中至关重要;在神经网络中,输入作为矩阵或向量传递到每一层,然后与另一个权重矩阵相乘,这构成了基础网络操作的核心。
概率分布及其基于这些分布的计算在机器学习领域依赖于贝叶斯思维。正如我们在后续章节中将看到的,人工智能中一些最具创新性的网络直接依赖于这些分布和贝叶斯定理的核心概念。回想一下,概率分布主要有两种形式:离散变量的概率质量函数(PMFs),以及连续变量的概率密度函数(PDFs);累计分布函数(CDF)也适用于任何随机变量。
贝叶斯定理实际上启发了一个独立的统计学分支,称为贝叶斯统计。到目前为止,我们讨论的是频率统计,它通过可重复事件的观测空间来衡量概率。另一方面,贝叶斯概率衡量信念的程度;即根据当前可用的信息,一个事件发生的可能性有多大?随着我们在接下来的章节中深入探讨人工神经网络(ANNs),这将变得非常重要。
构建基本的机器学习算法
如上一章所提到的,机器学习这个术语是在第一次 AI 寒冬之后提出的。如今,我们通常认为机器学习是深度学习和人工神经网络(ANNs)等领域的总称。
大多数机器学习解决方案可以分为分类问题或回归问题。分类问题是指输出变量是类别性的,例如欺诈与非欺诈。回归问题是指输出是连续的,例如金额或网站访问量。具有数值输出的问题可以是类别性的,但通常会转化为类别输出,如头等舱和二等舱。
在机器学习中...
监督学习算法
监督算法依赖于人工知识来完成任务。假设我们有一个与贷款偿还相关的数据集,包含多个人口统计指标,以及贷款是否还款的信息:
| 收入 | 年龄 | 婚姻状况 | 地点 | 存款 | 是否已还款 |
|---|---|---|---|---|---|
| $90,000 | 28 | 已婚 | 德克萨斯州奥斯汀 | $50,000 | y |
被称为目标的“已还款”列告诉我们贷款是否已经还清——它是我们想要预测的内容。包含申请人背景信息的数据被称为数据集的特征。在监督学习中,算法会根据特征来学习如何预测目标,换句话说,哪些指标能大概率地预测申请人是否会还款?在数学上,这一过程表现如下:

在这里,我们可以说我们的标签
是输入特征
的一个函数,再加上一些由于数据集自然产生的误差
。我们知道某一组特征很可能会产生某种结果。在监督学习中,我们设置一个算法来学习什么样的函数能够正确地将特征与结果进行映射。
为了说明有监督学习是如何工作的,我们将使用机器学习领域中的一个著名示例玩具数据集——鸢尾花数据集。它显示了四个特征:花萼长度、花萼宽度、花瓣长度和花瓣宽度。在这个数据集中,我们的目标变量(有时称为标签)是名称。该数据集可以在与本章对应的 GitHub 代码库中找到:
import pandas as pd
data = pd.read_csv("iris.csv")
data.head()
上述代码生成了以下输出:

现在我们已经准备好了数据,让我们开始一些有监督学习吧!
随机森林
随机森林是最常用的有监督学习算法之一。虽然它们既可以用于分类任务,也可以用于回归任务,但我们将专注于前者。随机森林是集成方法的一个例子,它通过聚合多个模型的输出,从而构建一个性能更强的模型。有时,你会听到它被称为将弱学习器组合成强学习器。
在 Python 中设置一个随机森林分类器非常简单,只需要借助 scikit-learn 库。首先,我们导入模块并设置数据。我们在这里不需要进行数据清洗,因为鸢尾花数据集已经经过预处理。
在训练机器学习算法之前,...
无监督学习算法
无监督学习算法在没有明确人工干预或标注的情况下,自主学习数据的特性。在人工智能领域,无监督学习技术通常学习生成数据集的概率分布。这些算法,如自编码器(我们将在本书后面讲解),对于许多任务非常有用,尤其是当我们对数据缺乏足够的信息,无法使用传统的有监督学习方法时。
PCA 是一种无监督的特征提取方法。它通过组合输入变量,使我们能够剔除那些对我们提供信息量最少的变量。之后,我们会得到一组彼此独立的新变量,这使得它们在基础线性模型中变得易于使用。
人工智能应用的根本问题之一是维度灾难。这种现象发生在数据中的维度数量很高时,使得学习算法难以有效执行。主成分分析(PCA)可以帮助缓解这个问题。PCA 是我们所说的降维的主要示例之一,它帮助我们将高维特征空间(大量数据属性)转化为低维特征空间(仅保留重要特征)。
降维可以通过两种主要方式进行:特征消除和特征提取。而特征消除可能涉及从数据集中任意去除特征,特征提取(PCA 就是一种形式)则为我们提供了更直观的降维方式。那么,它是如何工作的呢?简而言之:
-
我们创建一个描述所有数据之间关系的矩阵(相关矩阵或协方差矩阵)
-
我们将这个矩阵分解为独立的成分,称为特征值和特征向量,它们描述了我们数据的方向和大小。
-
然后,我们将原始数据转换或投影到这些成分上。
让我们手动分解这个过程,以在 Python 中说明这个过程。我们将使用与监督学习示例中相同的 Iris 数据集。首先,我们将创建相关矩阵:
features = (features - features.mean()) / features.std()
corr_matrix = np.corrcoef(data.values.T)
corr_matrix.corr()
输出应该如下所示:

我们的相关矩阵包含了每个矩阵元素之间相互关系的信息。这种关联记录在为算法提供信息时至关重要。较大的变异性通常表示有信号,而缺乏变异性则表示噪音。某一方向上的变异性越大,越能被检测到。接下来,我们将创建我们的eigen_values和eigen_vectors:
eigen_values, eigen_vectors = np.linalg.eig(corr_matrix)
输出应该如下所示:

特征向量和特征值成对出现;每个特征向量代表数据的方向,特征值告诉我们在该方向上存在多少变异。在 PCA 中,我们希望理解哪些输入对数据的变异性贡献最大(即:它们解释了多少数据)。通过计算特征向量及其对应的特征值,我们可以开始理解数据集中最重要的内容。
我们现在希望按从高到低的顺序对特征向量/特征值对进行排序。
eigenpairs = [[eigen_values[i], eigen_vectors[:,i]] for i in range(len(eigen_vectors))]
eigenpairs.sort(reverse=True)
最后,我们需要将这些对投影到低维空间。这是 PCA 的降维部分:
projection = np.hstack((eigenpairs[0][1].reshape(eig_vectors.shape[1],1),
eigenpairs[1][1].reshape(eig_vectors.shape[1],1)))
然后,我们将对原始数据进行这一转换:
transform = features.dot(projection)
我们可以将成分相互绘制出来:
fig = plt.figure(figsize=(8,8))
ax = fig.gca()
ax = sns.regplot(transform.iloc[:,0], transform.iloc[:,1],fit_reg=False, scatter_kws={'s':70}, ax=ax)
ax.set_xlabel('principal component 1', fontsize=10)
ax.set_ylabel('principal component 2', fontsize=10)
for tick in ax.xaxis.get_major_ticks():
tick.label.set_fontsize(12)
for tick in ax.yaxis.get_major_ticks():
tick.label.set_fontsize(12)
ax.set_title('Pricipal Component 1 vs Principal Component 2\n', fontsize=15)
plt.show()
你应该看到如下图所示:

那么,我们什么时候应该使用 PCA 呢?当以下情况成立时使用 PCA:
-
你是否有高维数据(太多的变量)并且想要一种逻辑的方式来减少它们?
-
你是否需要确保你的变量彼此独立?
然而,PCA 的一个缺点是它使得底层数据变得更加不透明,从而影响了它的可解释性。除了 PCA 和我们前面描述的 k-means 聚类模型之外,其他常见的非深度学习无监督学习算法包括:
-
K-means 聚类
-
层次聚类
-
混合模型
基本调优
那么,你已经建立了一个模型,现在该怎么办?可以收工了吗?很可能,你的模型仍然需要一些优化。机器学习过程的关键部分是优化我们的算法和方法。在本节中,我们将介绍优化的基本概念,并将在接下来的章节中继续学习调优方法。
有时,当我们的模型在新数据上的表现不好时,可能与它们过拟合或欠拟合有关。让我们介绍一些可以用来防止这种情况发生的方法。首先,我们来看一下之前训练的随机森林分类器。在你的笔记本中,调用 predict 方法,并将 x_test 数据传递进去以获得一些 ...
过拟合和欠拟合
过拟合 是当算法将训练数据学得过于完美,以至于无法准确预测新数据时发生的现象。过拟合的模型学习了训练集中的小细节,并且无法很好地进行泛化。举个例子,可以把它想象成你正在学习一门新语言。你不是学习这门语言的一般形式,比如西班牙语,而是从南美的一个偏远地区学会了它的本地方言,包括所有当地的俚语。如果你去西班牙,试图说这种西班牙语,可能会让当地人感到困惑! 欠拟合则是完全相反的情况;你没有学够西班牙语,无法有效沟通。从建模的角度来看,欠拟合的模型不够复杂,无法很好地泛化到新数据。
过拟合和欠拟合是与机器学习中的偏差/方差权衡现象相关的:
-
偏差 是你的模型在尝试近似预测时学习到的误差。理解模型是现实世界的简化版本,模型中的偏差是从尝试创建这一简化版本时所产生的误差。
-
方差 是你的误差如何根据输入数据的变化而变化的程度。它衡量了你的模型对输入数据复杂性的敏感度。
减少偏差的方法是增加模型的复杂性,尽管这样会增加方差并导致过拟合。另一方面,为了减少方差,我们可以通过简化模型来让其更好地进行泛化,尽管这会导致更高的偏差。如你所见,我们无法同时实现低偏差和低方差!一个好的模型应该在偏差和方差之间取得平衡。对抗过拟合有两种方法:交叉验证和正则化。我们现在将简要介绍交叉验证方法,并在第四章,你的第一个人工神经网络中,回到正则化部分,当我们开始构建第一个 ANN 时。
K 折交叉验证
你已经见过一种交叉验证的形式;将一部分数据隔离出来是我们可以使用的最简单形式的交叉验证。虽然这通常是一种良好的实践,但有时它可能会将一些重要特征排除在训练集之外,从而在测试时导致性能不佳。为了解决这个问题,我们可以通过一种叫做k-折交叉验证的技术进一步优化标准的交叉验证。
在 k 折交叉验证中,我们的数据集被平均划分为k个部分,由用户选择。作为经验法则,通常你应该选择 k = 5 或 k = 10,以获得最佳性能。然后模型将被训练并测试k次。在每次训练过程中,其中一个k数据段将作为验证集...
超参数优化
除了防止过拟合,我们还可以通过搜索最佳的模型超参数组合来优化模型。超参数是配置变量,它告诉模型使用哪些方法,而模型参数则是在训练过程中学习到的——我们将在接下来的章节中详细了解这些内容。超参数是程序化地添加到模型中的,并且在 Python 中的所有建模包中都可以找到。在我们之前构建的随机森林模型中,例如,n_estimators是一个超参数,它告诉模型构建多少棵树。搜索导致最佳模型性能的超参数组合的过程被称为超参数调优。
在 Python 中,我们可以通过对超参数的潜在值进行穷举搜索来调优超参数,这种方法称为网格搜索。让我们通过导入GrisSearchCV,使用我们的随机森林模型来看如何在 Python 中实现这一点:
from sklearn.model_selection import GridSearchCV
parameters = {
'n_estimators': [100, 500, 1000],
'max_features': [2, 3, 4],
'max_depth': [90, 100, 110, 120],
'min_samples_split': [6, 10, 14],
'min_samples_leaf': [2, 4, 6],
}
在这种情况下,我们将向网格搜索传递一些不同的超参数进行检查;你可以在分类器的文档中阅读它们的作用(scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html)。
为了创建搜索,我们只需初始化它:
search = GridSearchCV(estimator = rf_classifier, param_grid = parameters, cv = 3)
然后我们可以将其应用于数据:
search.fit(x_train, y_train)
search.best_params_
如果我们想要检查最佳参数组合的性能,我们可以通过在测试数据上评估它,轻松地在 sklearn 中实现:
best = search.best_estimator_
accuracy = evaluate(best, x_test, y_test)
超参数调优搜索可以应用于我们将在接下来的章节中使用的神经网络模型。
摘要
机器学习,进一步说,深度学习,其核心依赖于线性代数和统计学的基本构件。向量、矩阵和张量为我们在机器学习算法中表示输入数据和参数提供了手段,而它们之间的计算则是这些算法的核心操作。同样,分布和概率帮助我们在机器学习中对数据和事件进行建模。
我们还讨论了两类算法,这些算法将影响我们在后续章节中如何看待人工神经网络(ANNs):监督学习方法和无监督学习方法。在监督学习中,我们为算法提供一组特征和标签,它会学习如何适当地映射某些特征组合...
第三章:平台和其他必需品
在本章中,我们将讨论重要的库和框架,以便开始人工智能(AI)。我们将介绍三种最流行的深度学习框架——TensorFlow、PyTorch 和 Keras——的基本功能,展示如何在每个框架中启动和运行,因为我们将在接下来的章节中使用它们。我们将涉及用于 AI 的计算,并讨论如何通过 GPU 和其他先进的存储单元来改进它。最后,我们将讨论两种流行的深度学习云计算框架的基础知识:AWS 和 Google Cloud。
本章将涵盖以下主题:
-
Python 中深度学习的基本库:TensorFlow、PyTorch 和 Keras
-
用于 AI 的 CPU、GPU 和计算框架
-
AWS 和 Google Cloud 的基础知识
技术要求
我们将在 Python 3 中使用 TensorFlow、PyTorch 和 Keras 进行工作。建议您的计算机上配备 NVIDIA GPU。建议以下模型:
-
GTX 1080 Ti
-
GTX 1070
-
GTX 1060
如果您没有 NVIDIA GPU,请按照 云计算 部分的提示在 AWS 上使用 GPU 实例。
您还必须拥有 AWS 和 Google Cloud 账户;两者均免费,您可以在各自的网站上注册。
TensorFlow、PyTorch 和 Keras
在本节中,我们将介绍三种最流行的深度学习框架:TensorFlow、PyTorch 和 Keras。虽然我们将了解每个包的基本功能,但作为我们实践学习 AI 的一部分,我们将在后续章节中学习每个框架特定的深度学习功能。
TensorFlow
TensorFlow 是最流行、最贡献的深度学习库。最初由 Google Brain 开发,用于 Google 自己的 AI 产品,2015 年开源以来已成为深度学习的标准。TensorFlow 是 Google 自己的所有基于深度学习的产品的基础,如 Google 翻译和 Google Cloud 平台的机器学习 API。Google 特意设置 TensorFlow 以便并行化,因此在分布式环境中表现非常出色。
TensorFlow 提供了 Python、C++、Java 等的 API;然而,在本书中,我们将继续使用 Python。可以通过简单的 pip install tensorflow 命令从 PyPy 安装 TensorFlow。
基本构建块
如您可能从名称中猜到的那样,TensorFlow 依赖于我们在前一章中学到的张量的代数概念。从输入数据到参数,所有东西都存储在 TensorFlow 中的张量中。因此,TensorFlow 拥有其自己的函数来处理 NumPy 通常处理的许多基本操作。
在 TensorFlow 中编写张量时,实际上我们是在编写一个数组结构。记得数组如何可以是一个秩为 1 的张量吗?这正是我们在前面的例子中传递的内容。如果我们想传递一个秩为 3 的张量,我们只需写 x = tf.constant([1,2,3,4],[5,6,7,8],[9,10,11,12])。你会注意到,我们在下面的代码中定义了常量;这些只是 TensorFlow 中三种数据结构中的一种:
-
常量:定义的不可改变的值
-
占位符:将在 TensorFlow 会话中赋值的对象
-
变量:与常量类似,只是值可以改变
好的,回到代码。如果我们运行以下代码块,我们将得到一个 TensorFlow 对象,看起来像张量("Mul:0",shape=(4,),dtype=int32)。
## Import Tensorflow at tf for simplicity
import tensorflow as tf
## Define two constants
const1 = tf.constant([4])
const2 = tf.constant([5])
## Multiply the constants
product = tf.multiply(const1, const2)
为什么?因为 TensorFlow 基于 会话 的概念运行。TensorFlow 的底层代码是用 C++ 编写的,会话允许高层的 TensorFlow 包与低层的 C++ 运行时进行通信。
在运行 TensorFlow 会话之前,我们需要告诉它初始化我们声明的所有变量,然后运行初始化程序## 在 TensorFlow 中,我们必须先初始化一个会话对象
## Variable Initializer
init = initialize_all_variables()
## Initialize the session
sess = tf.Session()
sess.run(init)
## Run the session
print(sess.run(product))
## Close the session
sess.close()
TensorFlow 中最后一个重要的概念是 作用域。作用域帮助我们控制模型中的各个操作块:
with tf.name_scope("my_scope"):
## Define two constants
const1 = tf.constant([4])
const2 = tf.constant([5])
## Multiply the constants
product = tf.multiply(const1, const2)
就这样!我们已经成功地在 TensorFlow 中执行了第一个操作。在接下来的章节中,我们将学习更多关于 TensorFlow 在构建 人工神经网络(ANNs) 中的深入操作。
TensorFlow 图
TensorFlow 最重要和强大的功能之一是它的图。当你定义了前面描述的三种 TensorFlow 数据结构之一时,你会自动将一个 节点 和一个 边 添加到图中。节点代表操作,边代表张量,因此如果我们执行基本的乘法运算,如前面的例子中所示,const1 和 const2 将表示图中的边,tf.multiply 将表示一个节点,而 product 将表示该节点的输出边。TensorFlow 的图是 静态 的,这意味着我们不能在运行时更改它。
记住,ANN(人工神经网络)执行数百次计算;在每一步计算和解释时会极其消耗计算资源。TensorFlow 图...
PyTorch
PyTorch 是一个较新的但正在快速发展的深度学习库,基于用于 Facebook 深度学习算法的 Torch 框架。与 TensorFlow 不同,PyTorch 不是一个编译到底层语言的封装器,而是直接用 Python 编写,模仿 Python 的原生语法。如果你有过 Python 编程经验,PyTorch 会让你感觉非常熟悉。
可以通过以下命令轻松安装 PyTorch:
conda install pytorch torchvision -c pytorch
目前,PyTorch 没有 Windows 版本,这可能会使一些用户无法使用。
基本构建块
和 TensorFlow 一样,PyTorch 以张量形式表示数据。Torch 张量被定义为标准数据类型,如torch.FloatTensor()、torch.charTensor()和torch.intTensor()。如前所述,PyTorch 中的操作非常符合 Python 的风格。为了重复我们在前面 TensorFlow 中执行的相同乘法操作:
import torch x = torch.IntTensor([4])y = torch.IntTensor([5])product = x * y
由于其原生的 Python 风格,PyTorch 允许标准的 numpy 数组与 PyTorch 张量之间的轻松互动。在两者之间切换非常容易:
import torch import numpy as np## Create a numpy arraynumpy_array = np.random.randn(20,20)##Convert the numpy array to a pytorch tensorpytorch_tensor ...
PyTorch 图
PyTorch 看起来更符合 Python 风格,因为它具有动态图计算结构。由于 Python 是一种解释型语言,意味着操作是在运行时执行的,PyTorch 的图形功能旨在通过允许我们在运行时修改图中的变量来复制这一点。简单来说,PyTorch 的图是在你实际执行代码时创建的,而不像 TensorFlow 那样预先静态定义。架构上,这意味着你实际上可以在训练过程中改变网络架构,这使得 PyTorch 能够适应更多前沿、动态的架构。
Keras
Keras是目前最为高级的深度学习库,通常是人们在 AI 之旅中开始的地方。尽管本书将重点介绍使用 TensorFlow 的应用,但由于 Keras 的普及性和易用性,介绍它是非常重要的。
由谷歌的 François Chollet 编写,Keras 是一个可以在 TensorFlow 或其他库(如 Apache、MXNet 或 Theano)之上运行的封装库。像其他库一样,可以通过在终端或命令行中运行pip install keras来通过 PyPy 安装。功能上,它与 scikit-learn 的工作方式非常相似,因此是那些希望尽快动手实践深度学习的人的热门库。
像 PyTorch 一样,Keras 的设计旨在...
基本构建块
由于 Keras 被设计为一个模型级别的库,它不包含像 PyTorch 或基础 TensorFlow 那样进行基本操作的方法。相反,它利用 TensorFlow 作为后端。因此,它的基本操作与 TensorFlow 的基本操作相同:
import keras.backend as K
x = K.constant(5)
y = K.constant(6)
product = x * y
Keras 也使用与 TensorFlow 相同的图结构。我们将在下一章的你的第一个人工神经网络中学习更多关于 Keras 模型构建的方法。
总结
那么,最好的库是什么呢?正如你在以下截图中看到的,一个基准测试将 PyTorch 与其他深度学习库进行比较时,PyTorch 牢牢占据了领先地位:

最终,你选择的库主要取决于个人偏好;不过,一般来说:
-
Keras:最适合初学者或那些希望在 ANN 上做快速粗糙工作的用户
-
TensorFlow:广泛使用,有很多优秀的代码库和教程可以在线获取,并且在云计算机镜像和各种计算框架中有广泛的集成。
-
PyTorch:提供卓越的速度和易用性,但仍然在开发中,...
云计算基础
通常,本地 GPU 集群并不总是可用或实际可行。更多时候,许多企业正在将它们的 AI 应用程序迁移到云端,利用像 亚马逊云服务(AWS)或 谷歌云平台(GCP)这样的流行云服务提供商。当我们谈论云时,我们实际上是在谈论作为服务提供的数据库和计算资源。像 AWS 和 GCP 这样的云解决方案提供商在全球各地拥有数据中心,远程存储数据并运行计算任务。当你的数据在云中,或者你在云中运行程序时,实际上是在这些数据中心之一运行或存储。在云计算术语中,我们将这些数据中心或数据中心集群称为 区域。
云服务分为三种不同的提供结构:
-
基础设施即服务(IaaS):原始计算和网络资源,你可以用来构建基础设施,像在本地一样
-
平台即服务(PaaS):托管服务,隐藏了基础设施组件
-
软件即服务(SaaS):完全托管的解决方案,如在线电子邮件
在本节中,我们将讨论 IaaS 解决方案和 PaaS 解决方案。虽然云服务提供商确实为 AI 提供 SaaS 解决方案,但它们对我们需求来说过于高层。在本节中,我们将讨论你需要利用云计算能力的基本工具。在本章的最后,我们将在 维护 AI 应用程序 一节中更详细地讨论云计算。
AWS 基础
AWS 是市场上最受欢迎的云计算提供商。在本节中,我们将探索云端设置的基础知识,包括创建和连接 EC2 实例(亚马逊的主要云计算框架),以及如何在云中设置虚拟机。
我们还将简要介绍如何利用亚马逊的批量存储组件 S3。虽然 AWS 提供了多种机器学习服务,但我们将专注于如何利用 AWS 云计算架构为你的 AI 系统提供支持。
EC2 和虚拟机
AWS 系统的构建模块是 弹性云计算(EC2)实例;它是一个虚拟服务器,允许你在云中运行应用程序。在本章中,EC2 将是我们云计算工作的基础。对于开发者和数据科学家,亚马逊提供了一套名为 亚马逊机器镜像(AMI)的虚拟机,其中预装了你所需的一切,以便在云中开始深度学习。就我们的目的而言,亚马逊有一个 Ubuntu AMI 以及一个亚马逊 Linux 发行版 AMI,预装了 Python 3 和 TensorFlow、PyTorch 和 Keras。
要开始使用 EC2 进行深度学习,我们只需按照几个步骤操作:
-
登录到您的 Amazon Web Services 账户。
-
在搜索栏中搜索 EC2 并选择该服务以打开新的控制台。
-
选择“启动实例”按钮并在 AWS 市场中搜索 AWS 深度学习 AMI。您可以选择 Ubuntu 版本或 Amazon Linux。
-
选择一个 GPU 实例来运行您的镜像。我们建议选择 G2 或 P2 实例。每一页都选择“下一步”,直到到达“配置安全组”页面。在“源”下,选择“我的 IP”以仅允许使用您的 IP 地址访问。
-
点击“启动实例”。
-
创建一个新的私钥并将其保存在本地;这将帮助您稍后连接到您的实例。
现在,您应该已经设置好并准备好使用您的 AMI。如果您在 AWS 账户中已经有一个正在运行的 EC2 实例,选择该实例,然后右键点击“镜像”选项,选择“创建镜像”:

按照提示选择“创建镜像”。之后,您可以通过选择 EC2 -> AMIs 来找到该 AMI,位于主资源管理工具栏下。如果您仍然看不到您的 AMI,可以在 AWS 网站上找到更详细的说明:docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/tkv-create-ami-from-instance.html。
要使用您的新虚拟机,首先在 AWS 上启动实例。此处通过使用以下命令初始化 ssh(确保您在刚刚下载的 pem 密钥文件所在的目录):
cd /Users/your_username/Downloads/
ssh -L localhost:8888:localhost:8888 -i <your .pem file name> ubuntu@<Your instance DNS>
一旦您连接了终端或命令行,就可以像在本地计算机上使用命令行一样使用该界面。
S3 存储
Amazon 简单存储服务(Amazon S3),是 AWS 的大规模云存储解决方案。S3 被设计为简单、便宜且高效——它的工作方式就像您计算机上的本地目录一样。这些存储位置被称为存储桶,可以存储最多 5 TB 的数据。
要设置 S3,登录到您的 AWS 控制台,找到 S3 服务,然后点击“创建存储桶”。

您可以设置权限,控制谁可以访问 S3 存储桶中的数据,以便在需要限制访问时进行管理。
AWS Sagemaker
SageMaker 是 Amazon 提供的完全托管的云机器学习服务。作为平台即服务(PaaS)产品,SageMaker 是部署机器学习模型的最简单方式之一。与竞争对手不同,Amazon SageMaker 仅运行 Python 2.7。SageMaker 在云中处理机器学习服务有两个选项:
-
在托管的 Jupyter notebook 中创建并训练您的模型
-
从 Docker 化版本的模型开始训练
在接下来的章节中,我们将深入探讨如何使用 SageMaker 训练和部署模型。
Google Cloud Platform 基础
虽然 AWS 是云市场的主要玩家,并且已经占据了一段时间的市场主导地位,但在过去几年中,Google Cloud Platform(GCP)逐渐获得了人气,尤其是在机器学习领域。你可以通过访问cloud.google.com/free/并进入控制台,免费注册 GCP。请记住,你需要一个 Google 用户账户,例如 Gmail 账户,才能注册 Google 服务。尽管很多小任务可以在平台的免费层级中完成,GCP 也为新用户提供了 $300.00 的信用额度以帮助入门。
GCP 中的所有服务都在项目的框架下运行。项目是用于组织计算工具、用户和访问权限,以及计费的工具…
GCP 云存储
云存储是一个简单的、桶式结构的存储选项,类似于 AWS S3。像 AWS 一样,GCP 云存储可容纳最多 5 TB 的数据。与 AWS 或 Microsoft Azure 等竞争对手不同,GCP 的云存储在大文件的上传和下载速度上大约比竞争对手快三倍。云存储还拥有市场上最快的 吞吐量。吞吐量是一个云概念,衡量在特定时间内处理的数据量——简而言之,就是数据处理的速度。当创建依赖于流数据的某些应用时,这一点至关重要。云存储还支持创建跨服务区域的桶,这有助于提高数据的容错性和可用性。
要设置一个 Cloud 存储桶,请登录 GCP 控制台,搜索存储,然后点击创建存储桶:

除了标准的计算和存储服务外,GCP 还有另一个工具——ML 引擎,它为机器学习模型提供无缝的训练和部署操作。
GCP Cloud ML 引擎
Google Cloud Platform 的 Cloud ML Engine 相当于 AWS SageMaker。作为一个托管 PaaS,Cloud ML 处理机器学习算法的训练和部署过程。如果你在想——那像 AWS 上的基础计算服务 EC2 呢?GCP 也有类似服务。Compute Engine 是 GCP 对 Amazon EC2 的回应;它提供基础的、可扩展的云计算服务。虽然我们可以使用 Compute Engine 来搭建 AI 平台,但 GCP 已经使得通过 Cloud ML Engine 构建变得异常简单,因此我们将不会涵盖基础的 Compute Engine。
让我们深入了解细节。Cloud ML 引擎允许你:
-
在本地进行测试并在云端训练 scikit-learn 和 TensorFlow 模型
-
创建可重训练的…
CPUs、GPUs 和其他计算框架
AI 的进步始终与我们的计算能力密切相关。在本节中,我们将讨论用于驱动 AI 应用程序的 CPUs 和 GPUs,以及如何设置系统以支持加速的 GPU 处理。
计算机中的主要计算硬件被称为 中央处理单元 (CPU),CPU 设计用于一般计算任务。虽然本地 CPU 可以用来训练深度学习模型,但你可能会发现训练过程中计算机会长时间卡顿。训练 AI 应用时,使用 CPU 的“表亲”——图形处理单元 (GPU) 更加聪明。GPU 设计用于并行处理,正如人工神经网络(ANN)进行并行处理一样。正如我们在上一章中学到的,AI 应用需要大量的线性代数运算,而这些正是视频游戏所需要的运算类型。最初为游戏行业设计的 GPU,提供了成千上万个核心来处理这些运算,并进行并行化处理。这样,GPU 自然适合于构建深度学习算法。
在选择用于深度学习应用的 GPU 时,我们需要关注三个主要特征:
-
处理能力:GPU 的计算速度;定义为 核心数 x 速度
-
内存:GPU 处理不同大小数据的能力
-
RAM:你可以在任何给定时刻在 GPU 上处理的数据量
本节将重点介绍使用最受欢迎的深度学习 GPU 品牌——NVIDIA,其 CUDA 工具包让开箱即用的深度学习变得非常简单。作为 NVIDIA 的主要竞争对手,Radeon AMD GPU 使用一个名为 OpenCL 的工具包,而该工具包与大多数深度学习库的兼容性并不是开箱即用的。尽管 AMD GPU 提供了价格合理的强大硬件,但为了更顺利地上手,最好选择 NVIDIA 产品。
如果你的计算机没有 GPU 或者有其他 GPU,建议你利用 AWS 上的 GPU 实例来跟随本教程的步骤。
安装 GPU 库和驱动程序
现在,让我们为构建 AI 应用程序设置计算机。如果你希望在本地计算机上执行此任务,建议你安装 Windows 或 Linux 发行版。不幸的是,大多数 macOS 不支持 GPU,因此我们在本节中不会涉及 macOS。如果你的计算机没有 NVIDIA GPU,请按照以下步骤操作。如果你有 NVIDIA GPU,你可以选择跟随 AWS 部分,或者跳到以下部分。
如果你不确定是否拥有 NVIDIA GPU,可以使用以下终端命令检查其是否存在:
lspci -nnk | grep -i nvidia
使用 Linux (Ubuntu)
Linux 和 AI 就像天作之合。当你在 Android 手机上与 Google Assistant 对话,或者在 Windows 设备上与 Cortana 对话,抑或是在电视上看 Watson 如何赢得《危险边缘》游戏的那一局时,这一切都基于 Linux。
当我们在本章讨论 Linux 时,我们指的是一种特定的发行版,叫做 Ubuntu,它是 Linux 操作系统中最受欢迎的发行版之一。推荐你使用旧版的、更稳定的 Ubuntu(版本 14.04),虽然它会对你的 AI 应用产生很好的效果,但它肯定没有标准的 Windows 操作系统那么稳定。
如果你想在本地机器上使用 Ubuntu,查看 Ubuntu 的教程,了解如何在 Windows 机器上安装它(tutorials.ubuntu.com/tutorial/tutorial-ubuntu-on-windows#0)。如果你没有 PC 或者想设置虚拟实例,AWS 有一个很棒的教程(aws.amazon.com/getting-started/tutorials/launch-a-virtual-machine/),帮助你一步步完成设置:
- 要在 Ubuntu 上开始使用 GPU 深度学习,我们首先需要安装 GPU 驱动程序。在此示例中,我们将使用
wget和chmod来获取并设置读写权限:
wget http://us.download.nvidia.com/XFree86/Linuxx86_64/367.44/NVIDIA-Linux-x86_64-367.44.run
sudo chmod +x NVIDIA-Linux-x86_64-367.35.run
./NVIDIA-Linux-x86_64-367.35.run --silent
-
安装完成后,你可以通过运行简单的
nvidia-smi命令检查安装是否成功。 -
接下来,让我们安装 NVIDIA CUDA。CUDA 是一个 NVIDIA 软件包,它允许我们在 GPU 上运行 TensorFlow 模型:
wget"http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1604/x86_64/cuda-repo-ubuntu1604_8.0.44-1_amd64.deb"
## Install the drivers
sudo chmod +x cuda_7.5.18_linux.run
./cuda_7.5.18_linux.run --driver --silent
./cuda_7.5.18_linux.run --toolkit --silent
./cuda_7.5.18_linux.run --samples --silent
- 接下来,让我们将库添加到系统路径中:
echo ‘export LD_LIBRARY_PATH=”$LD_LIBRARY_PATH:/usr/local/cuda/lib64:/usr/local/cuda/extras/CUPTI/lib64"’ >> ~/.bashrc
- 最后,我们需要安装一个更高级别的软件包,叫做 cuNN,这是一个位于 CUDA 之上的特定库,为典型的深度学习操作提供高度优化的过程:
sudo scp cudnn-7.0-linux-x64-v4.0-prod.tgz
- 最后一步是将文件移动到正确的位置:
tar -xzvf cudnn-7.0-linux-x64-v4.0-prod.tgz
cp cuda/lib64/* /usr/local/cuda/lib64/
cp cuda/include/cudnn.h /usr/local/cuda/include/
- 就这样,我们在 Ubuntu 上已经为 GPU 加速做好了准备。我们的最后一步是简单地安装支持 GPU 的 TensorFlow 版本,并使用 Python 3:
pip3 install tensorflow-gpu
在 Windows 上
要在 Windows 上为深度学习设置 GPU,你必须运行 Windows 7 或更高版本。你可以通过 Windows 设备管理器 中的 显示适配器 部分验证是否拥有支持 CUDA 的 GPU。在这里,你会找到显卡的厂商名称和型号。
- 要查看你拥有什么类型的 GPU,打开命令行并运行:
control /name Microsoft.DeviceManager
-
如果你没有为你的 NVIDIA GPU 安装驱动程序,或者想要更新驱动程序,你可以在 NVIDIA 网站上根据你的设备找到正确的驱动程序:
www.nvidia.com/Download/index.aspx?lang=en-us。 -
接下来,去 NVIDIA 网站获取 CUDA 工具包(
developer.nvidia.com/cuda-downloads)。选择 CUDA 版本 ...
基本的 GPU 操作
现在我们的 GPU 已经为深度学习做好了设置,让我们学习如何使用它们。在 TensorFlow 中,GPU 以字符串的形式表示:
-
/cpu:0:你机器的 CPU -
/device:GPU:0:你机器的 GPU,如果有的话 -
/device:GPU:1:你机器的第二个 GPU,依此类推
分布式训练是指跨多个 GPU 训练网络的做法,它正成为 AI 领域中训练模型的越来越常见的方法。TensorFlow、Keras 和 PyTorch 都支持分布式训练。
日志记录是 TensorFlow 中的一项操作,用于将特定的命令集分配给系统上的特定 GPU 或 CPU。通过日志记录,我们还可以并行化我们的操作,这意味着我们可以同时跨多个 GPU 分配训练任务。为了做到这一点,我们使用一个简单的循环结构:
my_list = []
## Iterate through the available GPUs
for device in ['/gpu:0', '/gpu:1']:
## Utilize the TensorFlow device manager
with tf.device(device):
x = tf.constant([1,2,3], shape=[1,3])
y = tf.constant([1,2,3],shape [3,1])
my_list.append(tf.matmul(x, y))
with tf.device('/cpu:0'):
sum_operation = tf.add(x,y)
## Run everything through a session
sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))
sess.run(sum_operation)
我们首先创建一个空集,并使用迭代器将矩阵乘法过程分配到两个 GPU 上:GPU1 和 GPU2。另一个过程,即简单的加法,被分配给我们的 CPU。然后,我们将这两个过程都通过 TensorFlow 会话执行,正如我们之前所做的那样。
请记住,这种设备管理适用于本地设备,云架构的管理方式是不同的。
未来—TPU 及更多
Google 最近发布了一款名为张量处理单元(TPU)的新硬件,专门为 AI 应用设计。这些 TPU 提供比 CPU 或 GPU 高 15 到 30 倍的性能,并且在每瓦性能上高出 30 到 80 倍。大规模生产级 AI 应用中的权重数量可以从五百万到一亿不等,而这些 TPU 在执行这些操作时表现出色。
TPU 专门为 TensorFlow 处理量身定制,目前可在 Google Cloud 上使用。如果你有兴趣探索它们的功能,GCP 提供了一个很棒的教程(cloud.google.com/tpu/docs/quickstart)来帮助你入门。
总结
AI 平台和方法的领域非常广泛;在本章中,我们概述了 AI 领域中一些最有前景且广受好评的技术。对于深度学习包,我们了解了 TensorFlow、PyTorch 和 Keras。TensorFlow 由 Google 发布,是深度学习库中最受欢迎和最强大的一个。它利用会话和静态图来编译其底层的 C++代码。另一方面,PyTorch 是一个较新的库,采用动态图进行运行时执行,这使得它的使用感觉更像是原生 Python。最后,Keras 是一个高层次的库,运行在 TensorFlow 之上,适用于创建不需要太多定制的简单网络。
我们还讨论了云计算,利用 AWS 作为最流行的云计算服务,其主要的工作组件是 EC2 实例和 S3 桶。EC2 由虚拟服务器组成,可以用于扩展人工智能应用程序,在没有硬件或不需要硬件的情况下运行。S3,亚马逊的简单存储服务,使我们能够存储运行应用程序所需的数据和其他资源。最后,我们讲解了如何为你的计算机和编程语言启用 GPU 加速深度学习。在下一章,我们将通过创建我们的第一个人工神经网络(ANNs)来应用这些知识。
在下一章,我们将把第二章《机器学习基础》中学到的基本知识与本章的平台注册知识结合起来,创建我们的第一个人工神经网络(ANNs)。
第四章:你的第一个人工神经网络
在过去几章中,我们学习了机器学习的基础知识,并了解了如何设置我们的环境来创建人工智能(AI)应用程序。现在,既然我们已经掌握了基础知识,是时候将我们的知识付诸实践了。
在这一章中,我们将重点关注:
-
如何构建基本的 AI 应用程序,从构建一个基本的前馈网络开始,使用 TensorFlow。
-
我们将讨论人工神经网络(ANNs)的基本元素,并编写一个基本的前馈网络示例来进行说明。
ANN 使我们能够定义复杂的非线性问题,随着我们深入研究真正的深度学习机制,你将开始看到 AI 应用程序的强大之处...
技术要求
我们将使用在前一章中开发的支持 GPU 的 TensorFlow 环境。你将需要:
-
Python 3.6
-
GPU TensorFlow
-
PyTorch
网络构建模块
ANN 的最基本形式被称为前馈网络,有时也叫多层感知器。这些模型虽然本质上很简单,但包含了我们将要研究的各种类型的 ANN 的核心构建模块。
从本质上讲,前馈神经网络不过是一个有向图;层与层之间没有回归连接的循环,信息仅仅是顺着图向前流动。传统上,当这些网络被展示时,你会看到它们如下面的图所示:

一个前馈神经网络
在这种最基本的形式下,ANN 通常是...
网络层
输入层由我们传递给神经网络的特征组成。如果我们的输入是一个10 x 10像素的图像,那么我们将有 100 个输入单元。输入层实际上并不执行任何操作,但它是输入层和隐藏层之间的连接,这一点非常重要。
我们的输入层连接对输入向量执行线性变换,并将变换结果传递给隐藏层,通过隐藏层,这些结果通过激活函数进行转化。一旦我们完成这一计算,我们将结果传递给隐藏层。隐藏层是我们激活函数所在的位置,我们的网络可以有任意数量的隐藏层。隐藏层之所以被称为“隐藏层”,是因为它们计算的值在训练集中是看不见的;它们的任务是将网络的输入转化为输出层可以使用的东西。它们让我们能够学习数据集中的更复杂特征。
最后一层隐藏层的输出被发送到最终层,即输出层。输出层是我们网络中的最后一层,它将隐藏层的结果转化为你想要的输出形式,可以是二分类、实数等。我们通过利用特殊类型的激活函数来完成这一转化。一般来说:
-
对于分类问题,我们通常会使用一种叫做softmax的函数。
-
对于回归任务,我们将使用线性函数。
你选择激活函数的方式确实取决于你的损失函数,因为我们希望在训练过程中,损失函数的导数易于计算。
神经网络的命名和大小设置
我们通过网络中全连接层的数量来称呼网络,减去输入层。因此,下面的图示中的网络将是一个二层神经网络。单层网络则没有输入层;有时,你会听到逻辑回归被描述为单层网络的特殊情况,使用了sigmoid激活函数。当我们特别谈到深度神经网络时,指的是具有多个隐藏层的网络,如下图所示:

网络通常按其拥有的参数数量来衡量大小...
在我们的 MNIST 示例中设置网络参数
看看我们的 MNIST 示例,现在我们可以设置整体网络参数。我们将input_layer_size定义为输入数据的大小,即784。这将给出输入层中的神经元数量。参数hidden_one和hidden_two将定义隐藏层中的神经元数量。同时,number_classes会构建输出层,即输入数据可以被分类为的潜在类别数量:
## Size of the input data
input_layer_size = 784
## Define the size of the hidden layers; We want them to be smaller than the input
hidden_layer_one = 256
hidden_layer_two = 256
## Size of the potential output classes
number_classes = 10
总的来说,这些参数将根据MNIST数据集的形状,定义我们网络的输入层和输出层的大小。
激活函数
激活函数是使神经网络能够实现其功能的基础:以非线性方式将输入转换为所需的输出。因此,它们通常被称为非线性函数。结合我们之前学到的知识,在神经网络中,我们计算输入(X)与相应权重(w)的乘积和,并对其应用激活函数f(x),以获取该层的输出,并将其作为输入传递给下一层。
如果没有非线性函数,一个单元就只是一个简单的线性函数,我们的网络就会像线性回归一样。当我们思考传统的分析模型,比如线性回归或支持向量机时,...
历史上流行的激活函数
你常常会看到的三种激活函数是sigmoid、tanh和修正线性单元(ReLU)函数。尽管这些函数很受欢迎,但每个函数的使用都有一些常见的局限性。
在 2010 年代初期,使用 sigmoid 或 tanh 激活函数在网络的全连接层中非常流行。尽管 sigmoid 已经不太流行,但你仍然可能会看到它们。它们的值被限制在 0 和 1 之间(即:它们可以表示该范围内的任何值)。
import math
import numpy as np
def sigmoid(x):
s_out = []
for item in x:
s_out.append(1/(1+math.exp(-item)))
return s_out
我们可以轻松地在 Python 中绘制一个sigmoid函数来看看:
x = np.arange(-10., 10., 0.2)
f = sigmoid(x)
plt.plot(f,sig)
plt.show()

该函数也可以在 TensorFlow 中简单地通过tf.sigmoid来调用。tanh是一个非常类似的函数;它只是 sigmoid 的一个缩放版本:
import numpy as np
import matplotlib.pyplot as plt
tanh = np.tanh(np.arange(-5, 5, .1))
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(np.arange(-5, 5, .1), tanh)
ax.set_ylim([-1.0, 1.0])
ax.set_xlim([-5,5])
plt.show()

同样,tanh函数也可以在 TensorFlow 中通过tf.tanh来调用。
这两者都容易遇到梯度消失问题。sigmoid和tanh都容易出现所谓的饱和问题。当这些函数在两端饱和时,如果初始化的权重过大,梯度会变得接近零,导致饱和。
sigmoid和tanh函数本质上是有界的——它们有最大值和最小值。当非线性函数的输出值接近这些界限时,就会发生饱和。与sigmoid不同,tanh总是以零为中心,因此不太容易发生饱和。因此,在选择激活函数时,总是推荐使用tanh而非sigmoid。
由于这些激活函数的特殊性,2000 年代末期开始使用一种新的函数,称为ReLU:

ReLU 是一个简单且高效的非线性函数,它计算最大值:

ReLU 由于其线性和非饱和的特性,以及计算效率,使得收敛(即误差率较小)变得更快。ReLU 使用零矩阵,这比 sigmoid 或 tanh 函数所使用的指数运算要高效得多,有时使得其计算速度比 tanh 快多达六倍。然而,ReLU 也会遇到一个问题,即死亡神经元问题。当大梯度与不合适的权重更新相结合,导致 ReLU 函数输出零梯度时,非线性函数会在训练中“死亡”,这是由于学习率过大所致。像其他激活函数一样,ReLU 在 TensorFlow 中可以通过tf.nn.relu来调用。
近年来,实践者们已经放弃了sigmoid和tanh函数,转而基于 ReLU 创建了更稳定的方法。
现代激活函数方法
近年来,已经对 ReLU 函数做出了几种修改,以解决死亡神经元问题并增强其鲁棒性。最著名的解决方法是Leaky ReLU。Leaky ReLU 为 ReLU 函数引入了一个小斜率,以保持潜在的死神经元通过允许小的梯度保持神经元活跃。Leaky ReLU 函数在 TensorFlow 中可以这样使用:
tf.nn.leaky_relu(features,alpha=0.2,name=None)
另一种变体,参数化整流线性单元(PreLU),进一步改进了这一点,使得小梯度成为一个可以在训练过程中调整的参数。与预定义函数的斜率不同,斜率变成了一个可以适应的参数,因此...
权重和偏差因子
人工神经网络的两个关键部分是权重和偏置。这些元素帮助我们压缩和拉伸我们的非线性函数,以帮助我们更好地逼近目标函数。
权重在神经网络的每次变换中都起作用,帮助我们拉伸一个函数。它们本质上改变了非线性函数的陡峭度。偏置因子也是人工神经网络的重要组成部分;你可能已经在本章中展示的图示中注意到了它们。偏置因子是允许我们将激活函数左右移动的值,帮助我们最接近一个自然函数。
这个在实际中是如何工作的呢?假设你有一个简单的两神经元设置,如下图所示:

让我们看看将权重引入其中如何改变函数。在 Python 中,我们可以将其表示为一个简单的设置,它接收输入数据 x,一个权重矩阵 w,计算它们之间的点积,并通过一个非线性函数:
def single_output(x, w):
return np.tanh(np.dot(x, w))
然后我们可以用不同的权重值运行函数,并绘制它的输出:
x = np.arange(-5, 5, .1)
f1 = single_output(x, 0.5)
f2 = single_output(x, 1.0)
f3 = single_output(x, 2.0)
plt.plot(x,f1)
plt.plot(x,f2)
plt.plot(x,f3)
plt.show()
如果你运行此代码,你应该能看到如下图表:

正如你所看到的,简单的权重因子正在根据其大小弯曲我们的 sigmoid 函数!现在,假设你希望当输入为 5 时,网络的输出为 0.5。借助偏置因子,我们可以实现类似的变换:

在 Python 中,我们只需添加偏置因子来偏移输出:
def single_output(x, w, b):
return np.tanh(np.dot(x, w) + b)
运行以下代码,查看不同的偏置值如何将曲线从左右方向偏移;我们将所有权重设置为 1,以便清楚地看到偏移效果:
x = np.arange(-5, 5, .1)
f1 = single_output(x, 1.0, 2.0)
f2 = single_output(x, 1.0, 1.0)
f3 = single_output(x, 1.0, -2.0)
f4 = single_output(x, 1.0, 0.0)
plt.plot(x,f1)
plt.plot(x,f2)
plt.plot(x,f3)
plt.plot(x,f4)
plt.show()
代码应产生如下曲线。正如你在下图所看到的,我们已经将曲线进行了偏移,以便它更好地逼近我们想要的函数:

在神经网络的训练过程中,权重和偏置会被更新。学习调整权重和偏置的目标之一是使我们网络中的变换结果尽可能接近真实值。
在我们的 MNIST 示例中使用权重和偏置
让我们回到 MNIST 示例。根据之前定义的网络参数,我们来设置权重和偏置。我们将输入层与第一隐藏层之间的连接权重表示为 w1,第一隐藏层与第二隐藏层之间的权重表示为 w*2*,第二隐藏层与输出层之间的权重表示为 w_out。注意权重是如何跟踪给定层数据的输入大小和输出大小的:
weights = { 'w1': tf.Variable(tf.random_normal([input_layer_size, hidden_layer_one])), 'w2': tf.Variable(tf.random_normal([hidden_layer_one, hidden_layeR_two])), 'w_out': tf.Variable(tf.random_normal([hidden_layer_two, number_classes])) ...
损失函数
损失函数是神经网络的另一个重要组成部分,它衡量我们预测值与真实值之间的差异。
我们倾向于将函数看作是数学表达式;它们确实是这样,但它们也有形状和表面,或者说拓扑。拓扑本身是数学的一个分支内容,内容太多,不适合本书,但重要的要点是这些函数具有波峰波谷的拓扑,就像真实的拓扑图一样:

在人工智能领域,在创建神经网络时,我们试图找到这些损失函数中的最小点,称为全局最小值。这代表了我们实际值与预测值之间误差最小的点。达到全局最小值的过程称为收敛。
损失函数有两种变体:
-
凸损失函数:曲线向下的损失函数,例如碗的形状。这些损失函数是找到全局最小值的最简单方式。
-
非凸损失函数:看起来像前面的例子,具有许多波峰波谷。这些损失函数在寻找局部最小值时是最困难的。
非凸损失函数比较复杂,因为它们可能存在两个问题,称为局部最小值和鞍点。局部最小值顾名思义,就是损失函数中的低谷,它们不是整个拓扑结构中的最低点:

鞍点是拓扑中梯度等于零的区域。学习算法会在这些点上卡住,接下来我们将讨论如何通过神经网络的核心学习算法:梯度下降来解决这个问题。
那么,我们如何为网络选择损失函数呢?我们试图解决的问题将决定我们应选择哪个损失函数。选择合适的损失函数可能是一个耗时的任务,所以目前我们先关注一些通用规则。我们可以从使用均方误差(MSE)损失函数来处理回归问题开始。
使用损失函数进行简单回归
线性回归是我们可以实现的最简单的模型之一;你可能在自己的工作中已经使用过它。简单回归尝试找到两个线性分布变量的最佳拟合线。我们可以将之前学到的关于权重、偏差和损失函数的所有原则应用到简单回归中,看看它们是如何协同工作的。
现在,让我们回到损失函数。MSE 衡量观察值的实际值与预测值之间的平均平方差。输出是一个单一数字,表示当前权重集的成本或得分:
让我们在 TensorFlow 中开发一个线性回归模型,看看loss函数是如何工作的……
使用交叉熵进行二分类问题
尽管我们可以使用均方误差(MSE)解决回归问题,但分类问题则需要使用不同类型的损失函数。为此,我们使用一个叫做交叉熵的函数。交叉熵衡量的是分类模型的性能,该模型的输出值介于 0 和 1 之间。低交叉熵意味着预测的分类与实际分类相似。相反,高交叉熵则意味着预测的分类与实际分类不同。
它也被称为伯努利负对数似然和二元交叉熵:
def CrossEntropy(yHat, y):
if yHat == 1:
return -log(y)
else:
return -log(1 - y)
为具有多个正确答案的分类问题定义损失函数可能会更棘手;在接下来的章节中,我们将介绍多类分类。我们还会在本书的最后部分讨论如何定制你的损失函数。
在我们的 MNIST 示例中定义损失函数
由于我们有一个基本的二分类任务——识别数字,我们将利用交叉熵作为我们的损失函数。我们可以通过 TensorFlow 中内置的函数轻松实现它:
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(network_output, y))
随机梯度下降
梯度下降是我们找到损失函数全局最小值的方式,它也是神经网络学习的过程。在梯度下降中,我们计算每个梯度的值,即损失函数的斜率。这有助于我们达到最小值。可以把它想象成盲 fold 下山;你唯一能到达山底的方法就是感知地面的斜坡。在梯度下降中,我们利用微积分来“感觉”地面的斜率,确保我们正朝着最小值的正确方向前进。
在传统的梯度下降中,我们必须计算每个传入网络的样本的损失,这会导致大量冗余的计算。
我们可以通过利用一种叫做随机梯度下降的方法来减轻这种冗余。随机梯度下降实际上是最简单的优化方法之一,巧合的是,它也出奇地有效。通常在深度学习中,我们处理的是高维数据。与普通的梯度下降不同,使用随机梯度下降时,我们可以通过每次采样一个数据点的损失,来近似计算梯度,从而减少冗余。
学习率
由于随机梯度下降是随机抽样的,因此我们在损失函数的表面上的位置会到处跳动!我们可以通过减小一个叫做学习率的参数来缓解这种情况。学习率是一个叫做超参数的东西,它控制着网络的训练过程。学习率控制我们根据损失函数的梯度调整网络权重的幅度。换句话说,它决定了我们在尝试达到全局最小值时下降的速度。值越小,我们下降得越慢,就像图中右侧描述的那样。可以想象把轮胎缓慢地滚下山坡——...
在我们的 MNIST 示例中使用 Adam 优化器
自适应梯度下降法(Adam)采用初始学习率,并自适应地计算对其的更新。Adam 存储过去平方梯度和过去梯度的指数衰减平均值,这相当于测量类似动量的东西。这帮助我们在训练过程中防止过冲或不足。
Adam 可以通过以下命令行轻松地在 TensorFlow 中实现:
tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(loss_func)
tf.train 类包含了不同的优化器,这些优化器在运行时执行,并包含 TensorFlow 版本的 Adam 优化器;它将我们最初定义的 learning_rate 作为一个参数。然后我们调用 minimize 方法,并将其传递给我们的 loss 函数,将我们的学习过程封装成一个叫做 optimizer 的对象。
正则化
回想一下第二章,《机器学习基础》一章中提到的,过拟合和欠拟合发生在机器学习模型过度学习其训练数据集,或者没有足够学习它时。人工神经网络也不能避免这个问题!过拟合通常发生在神经网络中,因为它们的参数数量对于训练数据来说过于庞大。换句话说,模型对于其训练的数据量来说过于复杂。
防止我们网络中过拟合的一种方法是通过一种叫做正则化的技术。正则化通过缩小模型的参数来创建一个较不复杂的模型,从而减少过拟合。假设我们有一个损失...
训练过程
训练是我们教会神经网络学习的过程,并且这个过程在我们的代码中是程序化控制的。回顾一下,第二章 机器学习基础 中提到的,AI 世界中有两种学习方式:监督学习和无监督学习。通常,大多数人工神经网络是监督学习者,它们通过训练集进行学习。神经网络中一个训练周期的单元被称为周期(epoch)。在一个周期结束时,您的网络已接触到数据集中的每个数据点一次。在一个周期结束时,周期被定义为在第一次设置网络时的超参数。每个周期包含两个过程:正向传播和反向传播。
在一个周期内,我们有迭代,它告诉我们有多少比例的数据已经通过了正向传播和反向传播。例如,如果我们有一个包含 1,000 个数据点的数据集,并且将它们全部一次性通过正向传播和反向传播,那么我们就有一个周期内的一个迭代;然而,有时为了加快训练速度,我们希望将数据分成更小的批次进行处理,所以我们对数据进行小批量处理。我们可以将 1,000 点的数据集分成四个迭代,每个迭代包含 250 个数据点,所有这些都发生在同一个周期内。
将所有内容整合在一起
现在,我们已经讲解了基本前馈神经网络的所有元素,是时候开始将这些部分组装起来了。我们首先要做的是定义我们的超参数。我们将训练网络 50 个周期,每次将 100 个样本的批次输入到网络中。在每个周期结束时,batch_size 参数将告诉网络打印出当前的损失值以及网络的准确度:
## Network Parametersepochs = 15batch_size = 100display_step = 1
接下来,我们将为我们的 MNIST 数据特征 (x) 和正确标签 (y) 创建占位符,我们将在构建网络时使用这些占位符来表示数据:
# Create the Variablesx = tf.placeholder("float", None, ...
正向传播
正向传播是信息在训练阶段通过我们网络流动的过程。在我们详细讲解这个过程之前,让我们回到之前的 MNIST 示例。首先,我们需要初始化我们之前创建的前馈网络。为了做到这一点,我们可以简单地创建该函数的实例,并为其提供输入占位符,以及权重和偏置字典:
network_output = feedforward_network(x, weights, biases)
这个模型实例会被传入我们之前定义的loss函数中:
loss_func = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=network_output, labels=y))
然后,这些数据会被传递到我们定义的优化器中:
training_procedure = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(loss_func)
有了这些定义,你可以开始理解为什么占位符在 TensorFlow 中如此重要。输入占位符,以及随机初始化的权重和偏置,为 TensorFlow 构建模型提供了基础。如果没有这些,占位符,程序将无法知道如何编译网络,因为它无法理解输入、输出、权重和偏置值。
训练在 TensorFlow 中幕后进行,因此我们手动走一遍这个过程,以更好地理解背后的原理。最简单的来说,前向传播就是通过加权矩阵相乘并加上偏置单元,再通过激活函数处理。让我们看一下以下单一单元的示例,其中 X1:X3 代表我们的输入层(加上偏置值),而空的单元代表隐藏层中的节点:

记住,我们的输入和权重都是矩阵,因此我们将它们的乘法表示为点积。我们将这些值相加,得到结果,如下图所示:
。最后,我们将该操作的结果传入隐藏层的激活函数。
假设我们使用 ReLU 作为隐藏层单元的激活函数,那么我们的单元计算如下:

该函数的输出随后会被第二组权重矩阵乘以,就像我们对第一层输出所做的那样。这个过程可以根据网络中隐藏层的数量反复进行:

网络的最后一层通常包含一个激活函数,如 softmax,用于输出预测值。在整个过程中,我们计算该次传递的误差,然后通过一个叫做反向传播的过程将误差反馈到网络中。
反向传播
反向传播(或称反向传播算法)是我们在 AI 应用中使用的核心学习算法,学习它将对构建和调试神经网络至关重要。反向传播是误差反向传播的缩写,它是人工神经网络(ANNs)计算预测错误的方式。
你可以把它看作是我们之前讨论的梯度下降优化算法的补充。回想一下,ANNs 的核心目标是学习一组权重参数,帮助它们逼近一个能够复制训练数据的函数。反向传播衡量每个训练周期后误差的变化,而梯度下降则尝试优化误差。反向传播计算梯度...
前向传播和反向传播与 MNIST
记住,在 TensorFlow 中,要运行训练过程,我们需要通过一个会话来执行计算。首先,让我们打开一个新的训练会话。
- 在 TensorFlow 中,在我们进入训练过程之前,我们需要将模型类初始化为一个对象,并通过
tf.global_variables_initializer()命令将所有的变量占位符加载到线上:
with tf.Session() as sess:
## Initialize the variable
sess.run(tf.global_variables_initializer())
- 现在,我们可以编写训练过程的核心部分,也就是我们所说的训练循环。我们将根据模型的训练周期数来定义这个循环。在其中,我们首先对输入数据进行批处理;我们的模型不能也不应该一次性处理所有数据,因此我们通过之前定义的批次大小 100 来定义输入样本的总量。因此,在这 15 个训练周期中,我们会每次处理 100 个训练样本:
for epoch in range(epochs):
## Batch the incoming data
total_batch = int(mnist.train.num_examples/batch_size)
- 在同一个循环内,我们将创建另一个按批次运行训练过程的循环。我们将为 x 和 y 数据创建一个批次,并通过一个运行训练过程并计算损失的 TensorFlow 会话处理这些示例:
for batch in range(total_batch):
batch_x, batch_y = mnist.train.next_batch(batch_size)
_, loss = sess.run([training_procedure, loss_func], feed_dict={x: batch_x, y: batch_y})
- 到目前为止,我们已经定义了训练 TensorFlow 模型所需的最基本内容。除了训练过程本身,你通常还会看到一个过程,用于跟踪当前损失值以及准确度指标,以便我们知道模型训练得有多好(或者多差)。为此,我们将在之前定义的批处理循环的基础上定义一个新的循环,使其在每个训练周期结束时运行。在其中,我们通过运行网络的输出通过一个
softmax函数来获取该周期的预测。Softmax 函数会输出每个样本属于某个类别的概率,在分类问题中非常常见:
if epoch % display == 0:
pred = tf.nn.softmax(network_output)
- 一旦我们得到了这些概率,我们需要从通过 softmax 返回的张量中选择最高的概率。幸运的是,TensorFlow 为我们提供了一个非常简便的函数
tf.argmax。有了它,我们就可以将其与实际的 y 值进行比较,看看我们应该期待的结果:
correct_prediction = tf.equal(tf.argmax(pred, 1), tf.argmax(y, 1))
- 一旦我们得到了正确的预测,我们可以计算
accuracy,保存模型检查点,并返回当前周期、损失函数的值以及该步骤的模型准确度:
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
saver.save(sess, save_model)
print("Epoch:", '%04d' % (epoch+1), "loss {:.9f}".format(loss), "Accuracy:", accuracy.eval({x: mnist.test.images, y: mnist.test.labels}))
- 在训练过程结束时,你应该看到如下输出:

现在我们已经设置好网络并训练完成,让我们来看一下几个可以帮助我们检查训练进展的工具。
管理 TensorFlow 模型
TensorBoard 是一个内置于 TensorFlow 的工具,它可以在训练过程中可视化网络参数的值。它还可以帮助可视化数据,并运行几种探索性分析算法。它帮助你通过一个简单、易于使用的图形界面探索底层的 TensorFlow 图。
一旦一个 TensorFlow 模型经过训练,比如之前定义的 MNIST 示例,TensorFlow 允许我们创建一个叫做 模型摘要 的东西。摘要,正如你可能猜到的,是模型的简洁版本,包含了我们使用 TensorBoard 所需的关键要素。
- 为了创建一个摘要,我们首先需要初始化
writer;我们将在前面插入这一行...
保存模型检查点
在 TensorFlow 中,检查点是一个包含训练过程中计算的模型权重和梯度的二进制文件。如果你想加载一个模型进行进一步训练,或者在训练的某个时刻访问模型,我们可以保存并恢复包含所有必要训练信息的检查点。为了保存一个检查点,我们可以使用 TensorFlow 提供的保存工具:
save_model = os.path.join(job_dir, 'saved_mnist.ckpt')
saver = tf.train.Saver()
然后,在训练周期中,我们可以定期通过调用 saver 来保存检查点:
saver.save(sess, save_model)
请注意,你可以选择将模型检查点保存到你希望的任何目录中。TensorFlow saver 将创建三个文件:
-
一个
.meta文件,描述保存图的结构 -
一个
.data文件,存储图中所有变量的值(权重和梯度) -
一个
.index文件,用于标识特定的检查点
要从 TensorFlow 模型的检查点重新加载模型,我们需要加载元图,然后在 TensorFlow 会话中恢复图中变量的值。这里,meta_dir 是 .meta 文件的位置,restore_dir 是 .data 文件的位置:
init = tf.global_variables_initializer()
saver = tf.train.import_meta_graph(meta_dir)
with tf.Session() as sess:
sess.run(init)
saver.restore(sess, restore_dir)
TensorFlow 检查点只能用于训练,不能用于部署模型。部署模型有一个单独的过程,我们将在第十三章《部署与维护 AI 应用程序》中详细讲解。
总结
前馈网络是一类基础且重要的网络。本章帮助我们学习了神经网络的基本构建模块,也将为后续的网络主题提供帮助。
前馈神经网络最适合表示为有向图;信息沿一个方向流动,并通过矩阵乘法和激活函数进行转换。在人工神经网络(ANN)中,训练周期被分为若干个 epoch,每个 epoch 包含一个前向传播和一个反向传播。在前向传播中,信息从输入层流入,通过与输出层及其激活函数的连接进行转换,并通过输出层函数生成我们所需的输出形式;如概率等。
第五章:卷积神经网络
卷积神经网络(CNN),或称ConvNets,是前馈网络的一个特殊类别;它们主要用于计算机视觉任务,但也已被应用于其他具有非结构化数据的领域,例如自然语言处理。由于它们是前馈网络,它们与我们刚刚学习的简单网络非常相似;信息沿着一个方向传递,且由层、权重和偏差组成。
CNN 是 Facebook 用于图像标签、亚马逊用于产品推荐、以及自动驾驶汽车用于识别视野中物体的图像识别方法。在本章中,我们将讨论使 CNN 不同于标准前馈网络的功能,并举一些示例说明如何将其应用于多种任务。
在本章中,我们将讨论以下主题:
-
卷积
-
池化层
-
完整的卷积神经网络
-
用于图像标签的卷积神经网络
CNN 概述
CNN 是深度学习历史上最具影响力的网络类别之一。由 Yann LeCun(现为Facebook 人工智能研究负责人)发明,CNN 在 2012 年迎来了真正的突破,当时 Alex Krizhevsky 引入了深度卷积神经网络。
普通的神经网络在处理图像时扩展性较差;卷积神经网络(CNN)通过在网络的输入层添加一个或多个卷积层来改进传统的前馈神经网络。这些卷积层特别设计用来处理二维输入,例如图像甚至声音,如下图所示:

如你所见...
卷积层
假设我们有一个图像识别程序来识别图像中的物体,比如之前提到的例子。现在想象一下,用标准的前馈网络来分类图像有多困难;图像中的每个像素都会成为一个特征,并且必须通过网络传递,每个像素都有一套自己的参数。我们的参数空间会非常大,并且很可能会耗尽计算能力!图像在技术上只是高维向量,处理图像需要一些特殊的方法。
如果我们尝试用基础的前馈网络来完成这个任务,会发生什么呢?我们回想一下,基础的前馈网络是作用在向量空间上的。我们从图像开始,图像由独立的像素组成。假设我们的图像是 32×32 像素,输入到卷积层的数据将是32 x 32 x 3,其中3表示图像的 RGB 色彩尺度。要将其转化为向量空间,我们将得到一个3072 x 1的向量来表示整张图像:

假设我们的网络有10个神经元单元;使用简单的前馈模型,仅权重矩阵就会有30,720个可学习参数。CNN 通过其卷积层来缓解这个问题以及其他问题。
卷积层在创建 CNN 时有四个参数需要定义:
-
滤波器的数量,K
-
每个滤波器的大小,F
-
步幅,S
-
零填充的数量,P
在下一部分,我们将逐一讲解这些内容,并看看它们如何在网络结构中发挥作用。
层的参数和结构
卷积层使我们能够保持原始图像的尺寸,从而提高学习特征的能力并减少计算负担。它们通过使用一种叫做滤波器的东西来实现这一点,滤波器在图像上滑动,通过计算点积来学习特征。例如,CNN 的第一层的一个典型滤波器可能具有5 x 5 x 3的大小(即,5像素的宽度和高度,3是因为图像有三种颜色通道,RGB)。
在数学上,操作如下:

在这里,w 代表我们的滤波器,但它也代表我们的可学习权重。我们对输入滤波器进行转置,...
池化层
卷积层通常与池化层交替使用,池化层会下采样前一个卷积层的输出,以减少我们需要计算的参数量。这些层的一个特定形式,最大池化层,已经成为最广泛使用的变体。一般来说,最大池化层告诉我们,特征是否出现在卷积层所查看的区域内;它会在特定区域中寻找最显著的值(最大值),并将该值作为该区域的表示,如下所示:

最大池化层帮助后续的卷积层集中关注数据的更大部分,从而提供特征的抽象,帮助减少过拟合以及我们需要学习的超参数的数量,最终减少计算成本。这种自动特征选择的形式也通过防止网络过于关注图像的特定区域,帮助防止过拟合。
全连接层
CNN 的全连接层与普通前馈网络的全连接层工作方式相同。这个层将从图像中提取的输出映射到我们从网络中期望的输出,比如图像的标签:

在上面的图示中,我们的输入由蓝色节点表示,这些输入被送入第一个卷积层 A。接着是一个最大池化层、第二个卷积层,最后是全连接层,它将我们的输出转化为人类可读的输出。与普通前馈网络一样,我们通常会使用交叉熵损失函数来进行分类任务……
训练过程
当我们连接卷积层时,一个名为感受野或滤波器大小的超参数使得我们不必将单元连接到整个输入,而是专注于学习某一特定特征。我们的卷积层通常是从简单到复杂地学习特征。第一层通常学习低级特征,下一层学习中级特征,最后一层卷积层学习高级特征。这个过程的美妙之处在于,我们并没有明确告诉网络在这些不同层次上学习不同的特征;它通过训练过程以这种方式学习区分任务:

当我们经历这一过程时,我们的网络将生成一个二维的激活图,以跟踪特定滤波器在给定位置的响应。网络将学习保留那些在遇到形状边缘或物体的颜色时激活的滤波器。有时,这些滤波器可能会学到线条或更小的拼图部分,或者可能会学习图像的某些子部分,例如前面图中的马耳朵。每个特定卷积层的滤波器都会生成单独的激活图。如果我们有六个滤波器用于图像,我们将得到六个激活图,每个图聚焦于图像中的不同部分:

我们的卷积层将成为一系列堆叠的单独层,这些层之间插入了 ReLU 激活函数。我们通常在这里使用 ReLU 或 Leaky ReLU,以提高训练速度:

在这些卷积层之间,我们加入了最大池化层。这些结合了卷积和最大池化的层的输出将传递到一个全连接层,该层包含我们用于分类和其他任务的变换。一旦完成前向传播,误差将像在普通前馈网络中一样通过网络进行反向传播。
CNN 用于图像标注
让我们用新的 CNN 知识来测试一下。我们将通过 CNN 的一个最常见的任务——图像分类。
在一个图像分类任务中,我们的马会查看给定的图像,并确定某个物体是图像的概率。在下面的例子中,图像宽 248 像素,高 400 像素,并且有三个颜色通道:红色、绿色和蓝色(RGB)。因此,图像由248 x 400 x 3个数字组成,总共有 297,600 个数字。我们的任务是将这些数字转化为一个单一的分类标签;这匹马是吗?

虽然这对...来说可能是一个简单的任务
总结
卷积神经网络在解决许多计算机视觉任务中具有开创性作用。在本章中,我们了解了这些网络与我们基本的前馈网络有何不同,它们的结构是什么,以及我们如何使用它们。卷积神经网络主要用于计算机视觉任务,尽管它们也可以适应用于其他非结构化领域,例如自然语言处理和音频信号处理。
卷积神经网络(CNN)由卷积层和池化层交替组成,所有这些层的输出都连接到一个全连接层。 卷积神经网络通过使用滤波器对图像进行迭代。滤波器有大小和步长,步长决定了它们在输入图像上迭代的速度。通过利用零填充技术,可以更好地保证输入的一致性。
在下一章,我们将学习另一类重要的网络,称为递归神经网络。
第六章:循环神经网络
循环神经网络 (RNNs) 是最灵活的网络形式,并且广泛应用于 自然语言处理 (NLP)、金融服务以及其他多个领域。普通的前馈网络及其卷积变种接收固定的输入向量并输出固定的向量;它们假设所有输入数据相互独立。而 RNN 则是处理向量序列并输出向量序列的网络,能够帮助我们处理多种令人兴奋的数据类型。RNN 实际上是图灵完备的,它们能够模拟任意任务,因此从人工智能科学家的角度来看,RNN 是非常有吸引力的模型。
在本章中,我们将介绍...
技术要求
本章将使用 Python 3 中的 TensorFlow。对应的代码可以在本书的 GitHub 仓库中找到。
RNN 的构建模块
当我们思考人类如何思考时,我们不仅仅是一次性观察某个情境;我们会根据情境的上下文不断更新我们的思维。想象一下读书:每一章都是由多个单词组成的,这些单词构成了它的意义。普通的前馈网络不会将序列作为输入,因此很难对像自然语言这样的非结构化数据进行建模。而 RNN 可以帮助我们实现这一点。
基本结构
RNN 与其他网络的区别在于它们具有递归结构;它们在时间上是递归的。RNN 利用递归循环,使得信息能够在网络中保持。我们可以把它们看作是同一个网络的多个副本,信息在每次迭代中不断传递。如果没有递归,一个需要学习 10 个单词的句子的 RNN 就需要 10 个连接的相同层,每个单词一个。RNN 还在网络中共享参数。回想过去几章中我们提到的,在复杂的层次结构中,我们网络的参数数量可能会变得非常庞大。通过递归和参数共享,我们能够更有效地学习越来越长的序列结构,同时最小化需要学习的总参数量。
基本上,循环网络接收来自序列的项,并对其进行递归迭代。在下面的图示中,我们的序列 x 被输入到网络中:

RNN 具有记忆或隐藏状态,帮助它们管理和学习复杂的知识;我们用变量h表示它们。这些隐藏状态捕捉来自前一次传递的重要信息,并将其存储以供后续使用。RNN 的隐藏状态在初始时被设置为零,并在训练过程中不断更新。每次 RNN 的传递称为时间步骤;如果我们有一个 40 字符的序列,那么我们的网络就会有 40 个时间步骤。在每个 RNN 时间步骤中,网络会接受输入序列x并返回一个输出向量y以及更新后的隐藏状态。在第一次初始时间步骤之后,隐藏状态h也会与输入x一起,在每个新的时间步骤传递给网络。网络的输出是最后一个时间步骤之后隐藏状态的值。
虽然 RNN 是递归的,但我们可以轻松地将它们展开,绘制出它们在每个时间步骤的结构。我们可以将 RNN 看作一个框架,而不是一个黑盒,我们可以拆解它的内容,检查其内部工作原理:

在前面的图示中,我们的 RNN 被表示为函数f,我们将权重w应用到这个函数上,就像在任何其他神经网络中一样。然而,在 RNN 中,我们称这个函数为递归公式,它看起来大致如下:

这里,h[t]表示我们的新状态,它将是单一时间步骤的输出。h[t-1]表示我们的前一个状态,而x是来自序列的输入数据。这个递归公式会应用于序列中的每个元素,直到我们用完时间戳。
让我们通过在 TensorFlow 中构建基本的 RNN 来应用并分析它的工作原理。
普通递归神经网络
让我们一起走过基本 RNN 的架构;为了说明,我们将创建一个基本 RNN,预测序列中的下一个字母。作为训练数据,我们将使用莎士比亚《哈姆雷特》开头的段落。我们先将这个corpus保存为一个变量,以便在网络中使用。
corpus = "Hamlet was the only son of the King of Denmark. He loved his father and mother dearly--and was happy in the love of a sweet lady named Ophelia. Her father, Polonius, was the King's Chamberlain. While Hamlet was away studying at Wittenberg, his father died. Young Hamlet hastened home in great grief to hear that a serpent had stung the King, and that he was dead. The young Prince had loved ...
一对多
图像描述是一对多情境的一个例子。我们输入一个表示该图像的单一向量,并输出图像的变长描述。一对多情境在每个时间步骤输出标签、单词或其他输出:

音乐生成是另一种一对多情境的例子,其中我们输出一组变长的音符。
多对一
在情感分析等任务中,我们关注的是网络的单一且最终的隐藏状态。我们称这种情况为多对一情境。当我们到达最后一个时间步骤时,我们输出一个分类或其他值,这个值在计算图中由yt表示:

多对一情境也可以用于诸如音乐流派标签等任务,其中网络输入音符并预测流派。
多对多
如果我们想对视频中某个人的表情进行分类,或许标注某个时刻的场景,甚至进行语音识别,我们会使用多对多架构。多对多架构能够接收变长的输入序列,同时输出变长的序列:

在每个步骤中都会计算一个输出向量,我们可以在序列的每一步计算各自的损失。通常,在显式标注任务中,我们会使用 softmax 损失函数。最终的损失将是这些单独损失的总和。
时间反向传播
RNN 使用一种叫做时间反向传播的特殊变种,属于常规反向传播的一部分。像常规反向传播一样,这个过程通常会在 TensorFlow 中为我们处理;然而,值得注意的是,它与标准的前馈网络反向传播有所不同。
回顾一下,RNN 使用相同网络的小副本,每个副本都有自己的权重和偏差项。当我们在 RNN 中进行反向传播时,我们会在每个时间步计算梯度,并在计算损失时对整个网络的梯度进行求和。我们将有一个来自每个时间步的权重梯度,并且 W 的最终梯度将是这些梯度的总和……
存储单元——LSTM 和 GRU
虽然常规的 RNN 理论上可以接收长序列的信息,比如完整的文档,但它们在回顾信息时存在限制,无法回溯得太远。为了解决这个问题,研究人员开发了传统 RNN 的变种,使用了一种叫做记忆单元的单元,帮助网络记住重要信息。它们是为了解决传统 RNN 模型中的消失梯度问题而开发的。有两种主要的 RNN 变体利用了记忆单元架构,分别是GRU 和 LSTM。这两种架构是目前应用最广泛的 RNN 架构,因此我们会稍微关注它们的机制。
LSTM
LSTM 是一种特殊形式的 RNN,擅长学习长期依赖关系。LSTM 由 Hochreiter 和 Schmidhuber 在 1997 年提出,LSTM 拥有多个不同的层,信息会通过这些层来帮助它保留重要的信息并丢弃其他信息。与普通的循环网络不同,LSTM 不仅有一个标准的隐藏状态 h[t],还有一个专属于 LSTM 单元的状态,称为单元状态,我们将用 c[t] 表示。LSTM 的这些状态能够通过门控机制进行更新或调整。这些门控机制帮助控制信息通过单元的处理,包括一个激活函数和基本的逐点操作,比如向量乘法。
GRU
GRU 是在 2014 年作为对经典 LSTM 单元的全新改进而提出的。与 LSTM 的四个独立门不同,GRU 将遗忘门和输入门合并成一个更新门;你可以这样理解:没有被写入的内容就会被遗忘。它还将 LSTM 中的细胞状态 c[t] 与整体的隐藏状态 h[t] 合并。在一个周期结束时,GRU 暴露出整个隐藏状态:
- 更新门:更新门将遗忘门和输入门结合起来,本质上表明,未被写入记忆的内容会被遗忘。更新门接受我们的输入数据 x 并将其与隐藏状态相乘。这个表达式的结果随后与权重矩阵相乘,并通过一个 sigmoid 函数:

- 重置门:重置门的功能类似于 LSTM 中的输出门。它决定了什么内容以及多少内容会被写入记忆中。从数学上讲,它与更新门是相同的:

- 记忆门:一个 tanh 操作,实际上将重置门的输出存储到记忆中,类似于 LSTM 中的写入门。重置门的结果与原始输入结合,并通过记忆门来计算一个暂定的隐藏状态:

我们的最终状态是通过将暂定隐藏状态与更新门的输出进行操作来计算的:

GRU 的性能与 LSTM 相似;GRU 真正的优势在于由于其简化的结构,计算效率更高。在语言建模任务中,例如我们将在后面的章节中创建的智能助手,GRU 往往在数据较少的情况下表现更好。
使用 RNN 进行序列处理
现在我们已经了解了 RNN 的组件,接下来我们来探讨一下我们可以用它们做什么。在这一部分,我们将讨论两个主要示例:机器翻译 和 生成图像描述。在本书的后面部分,我们将利用 RNN 构建各种端到端系统。
神经机器翻译
机器翻译是一个序列到序列的问题;你会经常看到这些网络被描述为序列到序列(或Seq2Seq)模型。与传统的特征工程和 n-gram 计数技术不同,神经机器翻译将整个句子的意义映射到一个单一的向量,然后基于该单一的意义向量生成翻译。
机器翻译模型依赖于人工智能中的一个重要概念,即编码器/解码器范式。简而言之:
-
编码器解析输入并输出输入的压缩向量表示。我们通常使用 GRU 或 LSTM 来完成这个任务
-
解码器接收压缩后的表示,并从中推断出新的序列。
这些概念对于理解生成对抗网络(GAN)至关重要,我们将在接下来的章节中学习。该网络的第一部分充当编码器,解析你的英文句子;它表示该句子的英文摘要。
从架构上看,神经机器翻译网络是多对一和一对多的,它们背靠背排列,正如下图所示:

这是谷歌用于 Google Translate 的相同架构。
注意力机制
神经机器翻译(NMT)模型面临与一般 RNN 相同的长期依赖性问题。尽管我们看到 LSTM 能够缓解大部分这种行为,但在长句子中仍然会出现问题。特别是在机器翻译中,句子的翻译很大程度上依赖于编码器网络的隐藏状态所包含的信息量,我们必须确保这些最终状态尽可能丰富。我们通过一种叫做注意力机制的方式解决了这个问题。
注意力机制允许解码器根据上下文和到目前为止生成的内容选择输入句子的某些部分。我们利用一个叫做上下文向量的向量来存储来自...
生成图片描述
循环神经网络(RNN)也可以处理那些需要将固定输入转换为可变序列的问题。图片描述处理输入固定的图片,并输出该图片的完全可变描述。这些模型利用卷积神经网络(CNN)来输入图片,然后将 CNN 的输出传递给 RNN,RNN 会逐字生成描述:

我们将构建一个基于 Flicker 30 数据集的神经网络描述模型,该数据集由加利福尼亚大学伯克利分校提供,你可以在本章对应的 GitHub 仓库中找到。在这个案例中,我们会利用预训练的图像嵌入来节省时间;然而,你也可以在相应的 GitHub 仓库中找到端到端模型的示例。
- 让我们从导入开始:
import math
import os
import tensorflow as tf
import numpy as np
import pandas as pd
import tensorflow.python.platform
from keras.preprocessing import sequence
- 接下来,让我们从你从仓库下载的文件中加载图像数据。首先,我们定义一个路径来存放这些图像。
captions = 'image_captions.token'
features = 'features.npy'
- 接下来,我们可以加载图像和各种描述。我们将这些描述和图像称为“描述”和“图像”。
annotations = pd.read_table(captions, sep='\t', header=None, names=['image', 'caption'])
images = np.load(features,'r'),
captions = annotations['caption'].values
- 接下来,我们需要存储每个单词出现次数的计数:
occuranceDict = {} ## This Dictionary will store the occurance count of our words
wordCount = 0 ## wordCount is a value that we will use to keep track of the number of occurances of a word
- 首先,我们需要构建一个词汇表;我们需要对描述进行一些预处理。可以参考以下代码:
def ConstructVocab(captions):
'''Function to Construct the Vocab that we will be generating from'''
occuranceDict = {} ## This Dictionary will store the occurance count of our words
wordCount = 0 ## wordCount is a valuee that we will use to keep track of the number of occurances of a word
## Iterate over the captions to split them into individuals words to construct the vocab
for item in captions:
wordCount += 1
for word in item.lower().split(' '):
occuranceDict[word] = occuranceDict.get(word, 0) + 1
vocab = [word for word in occuranceDict if occuranceDict[word] >= 20]
## Set a dictionary to set a word for each index
IndexesToWords = {} ##
ixtoword[0] = '.'
## Set a dictionary to the indexes of each word at each steps
WordsToIndexes = {}
WordsToIndexes['#START#'] = 0
index = 1
## Iterate over the words in the vocab to store them and index their position.
for word in vocab:
WordstoIndexes[word] = index
IndexestoWords[index] = word
index += 1
## Set the wordcount for the occurance dictionary
occuranceDict['.'] = wordCount
## Initiative the word bias vectors
biasVector = np.array([1.0*occuranceDict[IndexestoWords[i]] for i in IndexestoWords])
biasVector = biasVector / np.sum(biasVector)
biasVector = np.log(biasVector)
biasVector = biasVector - np.max(biasVector)
## Return the dictionarties, as well as the bias vector
return WordstoIndexes, IndexestoWords, biasVector.astype(np.float32)
- 现在我们可以构建模型了。按照下面的注释理解每个部分的作用:
def captionRNN():
''' RNN for Image Captioning '''
## Define our Networks Parameters
dim_embed = 256
dim_hidden = 256
dim_in = 4096
batch_size = 128
momentum = 0.9
n_epochs = 150
## Initialize the embedding distribution and bias factor as a random uniform distribution.
captionEmbedding = tf.Variable(tf.random_uniform([n_words, dim_embed], -0.1, 0.1))
captionBias = tf.Variable(tf.zeros([dim_embed]))
## Initialize the embedding distribution and bias for the images
imgEmbedding = tf.Variable(tf.random_uniform([dim_in, dim_hidden], -0.1, 0.1))
imgBias = tf.Variable(tf.zeros([dim_hidden]))
## Initialize the encodings for the words
wordEncoding = tf.Variable(tf.random_uniform([dim_hidden, n_words], -0.1, 0.1))
wordBias = tf.Variable(init_b)
## Initialize the variables for our images
img = tf.placeholder(tf.float32, [batch_size, dim_in]) ## Placeholder for our image variables
capHolder = tf.placeholder(tf.int32, [batch_size, n_lstm_steps]) ## Placeholder for our image captions
mask = tf.placeholder(tf.float32, [batch_size, n_lstm_steps])
## Compute an initial embedding for the LSTM
imgEmbedding = tf.matmul(img, imgEmbedding) + imgBias
## Initialize the LSTM and its starting state
lstm = tf.contrib.rnn.BasicLSTMCell(dim_hidden)
state = self.lstm.zero_state(batch_size, dtype=tf.float32)
## Define a starting loss
totalLoss = 0.0
- 现在,我们将为模型训练这个循环:
## Training Cycle for the Model
with tf.variable_scope("RNN"):
for i in range(n_lstm_steps):
## Tell the model to utilizing the embedding corresponding to the appropriate caption,
## if not, utilize the image at the first embedding
if i > 0:
current_embedding = tf.nn.embedding_lookup(captionEmbedding, capHolder[:,i-1]) + captionBias
tf.get_variable_scope().reuse_variables()
else:
current_embedding = imgEmbedding
out, state = lstm(current_embedding, state) ## Output the current embedding and state from the LSTM
if i > 0:
labels = tf.expand_dims(capHolder[:, i], 1)
ix_range = tf.range(0, batch_size, 1) ## get the index range
indexes = tf.expand_dims(ix_range, 1) ## get the indexes
concat = tf.concat([indexes, labels],1) ## Concatonate the indexes with their labels
## Utilizng a "One Hot" encoding scheme for the labels
oneHot = tf.sparse_to_dense(concat, tf.stack([batch_size, n_words]), 1.0, 0.0)
## Run the results through a softmax function to generate the next word
logit = tf.matmul(out, wordEncoding) + wordBias
## Utilizing Cross Entropy as our Loss Function
crossEntropyLoss = tf.nn.softmax_cross_entropy_with_logits(logits=logit, labels=oneHot)
crossEntropyLoss = crossEntropyLoss * mask[:,i]
## Tell Tensorflow to reduce our loss
loss = tf.reduce_sum(crossEntropyLoss)
## Add the loss at each iteration to our total loss
totalLoss = totalLoss + loss
totalLoss = totalLoss / tf.reduce_sum(mask[:,1:])
return totalLoss, img, capHolder, mask
在训练过程中,我们利用一个独立的权重矩阵来帮助模型处理图像信息,并在每个 RNN 时间步周期中使用该权重矩阵来接收图像信息。
在每个步骤中,我们的 RNN 将计算网络词汇表中所有分数的分布,从该分布中抽取最可能的词,并将该词作为下一个 RNN 时间步的输入。这些模型通常是端到端训练的,这意味着 RNN 和 CNN 的反向传播是同时进行的。在这种情况下,我们只需要关注我们的 RNN:
def trainNeuralCaption(learning_rate=0.0001):
'''Function to train the Neural Machine Translation Model '''
## Initialize a Tensorflow Session
sess = tf.InteractiveSession()
## Load the images and construct the vocab using the functions we described above
images, captions = load_images('path/to/captions', 'path/to/features')
WordstoIndexes, IndexestoWords, init_b = constructVocab(captions)
## Store the indexes
index = (np.arange(len(images)).astype(int))
np.random.shuffle(index)
n_words = len(WordstoIndexes)
maxlen = np.max( [x for x in map(lambda x: len(x.split(' ')), captions) ] )
现在,让我们初始化标题 RNN 模型并开始构建模型。相关代码如下:
## Initialize the Caption RNN model function and build the model
nc = neuralCaption(dim_in, dim_hidden, dim_embed, batch_size, maxlen+2, n_words, init_b)
loss, image, sentence, mask = nc.build_model()
## Define our timestep and the overall learning rate
global_step = tf.Variable(0,trainable=False)
learning_rate = tf.train.exponential_decay(learning_rate, global_step, int(len(index)/batch_size), 0.95)
## Utilize Adam as our optimization function
train_op = tf.train.AdamOptimizer(learning_rate).minimize(loss)
## Initialize all our variables
tf.global_variables_initializer().run()
## Run the training cucle
for epoch in range(n_epochs):
for start, end in zip( range(0, len(index), batch_size), range(batch_size, len(index), batch_size)):
## Current Images and Captions
currentImages = images[index[start:end]]
currentCaptions = captions[index[start:end]]
current_caption_ind = [x for x in map(lambda cap: [WordstoIndexes[word] for word in cap.lower().split(' ')[:-1] if word in WordstoIndezes], current_captions)]
## Pad the incoming sequences
current_caption_matrix = sequence.pad_sequences(current_caption_ind, padding='post', maxlen=maxlen+1)
current_caption_matrix = np.hstack( [np.full( (len(current_caption_matrix),1), 0), current_caption_matrix] )
current_mask_matrix = np.zeros((current_caption_matrix.shape[0], current_caption_matrix.shape[1]))
nonzeros = np.array([x for x in map(lambda x: (x != 0).sum()+2, current_caption_matrix )])
for ind, row in enumerate(current_mask_matrix):
row[:nonzeros[ind]] = 1
## Run the operations in a TensorFlow session
_, currentLoss = sess.run([train_op, loss], feed_dict={
image: current_feats.astype(np.float32),
sentence : current_caption_matrix.astype(np.int32),
mask : current_mask_matrix.astype(np.float32)
})
print("Loss: ", currentLoss)
最后,我们需要做的就是训练模型;我们可以通过简单地调用training函数来实现:
trainNeuralCaption()
通常,你可能会看到许多用于文本生成的 RNN 变体,被称为字符-级别 RNN,或ChaRnn。建议避免使用字符级别的 RNN 模型。
RNN 的扩展
在过去的几年里,普通 RNN 有很多扩展。这里列出的并不是所有 RNN 领域的重大进展,但我们将回顾其中几个最值得注意的:双向 RNN 和 NTM。
双向 RNN
近年来,研究人员对传统 RNN 结构进行了若干改进。双向 RNN的出现是基于这样一个想法:它们不仅可以依赖序列中之前的信息,还可以依赖之后的信息。从结构上讲,双向 RNN 只是两个堆叠在一起的 RNN,它们的输出是每个单独网络的隐藏状态的组合。
神经图灵机
神经图灵机(NTM)是一种 RNN 形式,由 DeepMind 的 Alex Graves 于 2014 年开发。与具有内部状态的 LSTM 不同,NTM 具有外部记忆,这增加了它们处理复杂计算任务的能力。
NTM 由两个主要组件组成:控制器和记忆库。
-
NTM 控制器:NTM 中的控制器就是神经网络本身;它管理输入和记忆之间的信息流,并学习如何在整个过程中管理自己的记忆。
-
NTM 记忆库:实际的外部记忆,通常以张量形式表示
只要内存足够,NTM 就可以通过简单地更新其自身的记忆来反映算法,从而复制任何算法。其架构...
摘要
RNN 是我们推理文本输入的主要手段,并且有多种形式。在本章中,我们学习了 RNN 的递归结构,以及使用记忆单元的特殊版本。RNN 广泛应用于各种类型的序列预测,如生成音乐、文本、图像描述等。
RNN 与前馈网络不同,它们具有递归性;RNN 的每一步都依赖于网络在上一个状态下的记忆,以及其自身的权重和偏置因素。因此,普通的 RNN 在处理长期依赖问题时表现不佳;它们很难记住超过特定时间步数的序列。GRU 和 LSTM 利用记忆门控机制来控制它们记住和忘记的内容,从而克服了许多 RNN 在处理长期依赖时遇到的问题。带有注意力机制的 RNN/CNN 混合体在分类任务中实际上提供了最先进的性能。我们将在第九章中创建一个,智能助手的深度学习,讲解如何创建一个基本的智能代理。
在下一章,我们将超越 RNN,进入最激动人心的网络类别之一——生成网络,在这里我们将学习如何使用未标记的数据,以及如何从零开始生成图像和画作。
第七章:生成模型
生成模型是推动计算机理解世界的最有前景的方向。它们是真正的无监督模型,能够执行许多当前被认为是人工智能(AI)前沿的任务。生成模型之所以不同,正如其名称所示:它们生成数据。主要集中在计算机视觉任务上,这类网络有能力创造新的人脸、新的手写字,甚至是绘画作品。
在本节中,我们将介绍生成模型及其基础,特别关注两种最受欢迎的模型类型:变分自编码器(VAE)和生成对抗网络(GAN),你将学习到...
技术要求
在本章中,我们将使用 Python 3。你需要安装以下包:
-
NumPy
-
TensorFlow
-
PyTorch
如果你的机器支持 GPU,参考第三章《平台与其他基本要素》,这将非常有帮助。
走向 AI – 生成模型
生成模型是一类完全不同于我们目前所讨论的神经网络。我们到目前为止讨论的网络都是前馈网络。CNN 和 RNN 都是判别性网络,它们试图对数据进行分类。给定特定的输入,它们可以预测类别或其他标签。生成模型则相反,它们尝试在给定标签的情况下预测特征。它们通过拥有一个比学习的数据量小得多的参数集来实现这一点,这迫使它们以高效的方式理解数据的基本本质。
生成模型有两种主要类型,VAE 和 GAN。首先,我们将从生成模型的动机开始...
自编码器
自编码器及其编码器/解码器框架是生成模型的灵感来源。它们是一种自监督的表示学习技术,通过该技术,网络学习输入内容,以便像输入数据一样生成新的数据。在这一部分,我们将了解它们的架构和用途,作为对它们启发的生成网络的入门介绍。
网络架构
自编码器通过接受输入并生成一个较小的向量表示,来重构其自身输入。它们通过使用编码器对传入数据施加信息瓶颈,然后利用解码器基于该表示重建输入数据。这个方法基于这样一个理念:数据中存在结构(即关联等),这些结构是存在的,但并不容易被察觉。自编码器是自动学习这些关系的一种方式,而无需显式地进行学习。
在结构上,自编码器由输入层、隐藏层和输出层组成,如下图所示:
编码器学习保留尽可能多的相关信息……
构建自编码器
如果你认为重建输出的任务似乎没有什么用处,那你不是唯一一个这样想的人。我们究竟用这些网络做什么呢?自编码器帮助在没有已知标签特征的情况下提取特征。为了说明这一点,下面我们通过一个使用 TensorFlow 的示例来演示。我们将在这里重建 MNIST 数据集,稍后我们将比较标准自编码器和变分自编码器在同一任务上的表现。
让我们从导入库和数据开始。MNIST 数据集本身就包含在 TensorFlow 中,所以我们可以很方便地导入它:
import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)
为了简便起见,我们可以使用 tf.layers 库来构建自编码器。我们希望自编码器的架构遵循卷积/反卷积模式,其中解码器的输入层与输入大小相匹配,随后的层将数据压缩成越来越小的表示。解码器将是相同架构的反转,首先从小的表示开始,然后逐步变大。
将所有代码整合在一起后,它应该看起来像下面这样:

让我们从编码器开始;我们首先定义一个初始化器来初始化权重和偏置项,然后将编码器定义为一个接受输入 x 的函数。接着,我们将使用 tf.layers.dense 函数来创建标准的全连接神经网络层。编码器将有三层,第一层的大小与输入数据的维度(784)相匹配,随后每一层会逐渐变小:
initializer = tf.contrib.layers.xavier_initializer()
def encoder(x):
input_layer = tf.layers.dense(inputs=x, units=784, activation=tf.nn.relu,
kernel_initializer=initializer, bias_initializer=initializer
)
z_prime = tf.layers.dense(inputs=input_layer, units=256, activation=tf.nn.relu,
kernel_initializer=initializer, bias_initializer=initializer
)
z = tf.layers.dense(inputs=z_prime, units=128, activation=tf.nn.relu,
kernel_initializer=initializer, bias_initializer=initializer
)
return z
接下来,让我们构建解码器;它将使用与编码器相同的层类型和初始化器,只不过这次我们反转了这些层,解码器的第一层最小,最后一层最大。
def decoder(x):
x_prime_one = tf.layers.dense(inputs=x, units=128, activation=tf.nn.relu,
kernel_initializer=initializer, bias_initializer=initializer
)
x_prime_two = tf.layers.dense(inputs=x_prime_one, units=256, activation=tf.nn.relu,
kernel_initializer=initializer, bias_initializer=initializer
)
output_layer = tf.layers.dense(inputs=x_prime_two, units=784, activation=tf.nn.relu,
kernel_initializer=initializer, bias_initializer=initializer
)
return output_layer
在开始训练之前,让我们定义一些在训练循环中需要用到的超参数。我们将定义输入的大小、学习率、训练步数、训练周期的批次大小,以及我们希望多频繁地显示训练进度信息。
input_dim = 784
learning_rate = 0.001
num_steps = 1000
batch_size = 256
display = 1
然后,我们将定义输入数据的占位符,以便能够编译模型:
x = tf.placeholder("float", [None, input_dim])
接下来,我们将编译模型和优化器,就像你在上一章中看到的那样:
# Construct the full autoencoder
z = encoder(x)
## x_prime represents our predicted distribution
x_prime = decoder(z)
# Define the loss function and the optimizer
loss = tf.reduce_mean(tf.pow(x - x_prime, 2))
optimizer = tf.train.RMSPropOptimizer(learning_rate).minimize(loss)
最后,我们将编写训练循环的代码。到这一点为止,这些内容对你来说应该比较熟悉了;启动 TensorFlow 会话,并在每个 epoch/批次中迭代,计算每个点的损失和准确率:
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
## Training Loop
for i in range(1, num_steps+1):
## Feed Batches of MNIST Data
batch_x, _ = mnist.train.next_batch(batch_size)
## Run the Optimization Process
_, l = sess.run([optimizer, loss], feed_dict={x: batch_x})
## Display the loss at every 1000 out of 30,000 steps
if i % display == 0 or i == 1:
print('Step %i: Loss: %f' % (i, l))
对于这个具体示例,我们将额外添加一些内容;一个方法来将重建的图像与其原始版本并排显示。请记住,这段代码仍然包含在训练会话中,只是位于训练循环之外:
n = 4
canvas_orig = np.empty((28 * n, 28 * n))
canvas_recon = np.empty((28 * n, 28 * n))
for i in range(n):
batch_x, _ = mnist.test.next_batch(n)
# Encode and decode each individual written digit
g = sess.run(decoder, feed_dict={x: batch_x})
# Display original images
for j in range(n):
# Draw the original digits
canvas_orig[i * 28:(i + 1) * 28, j * 28:(j + 1) * 28] = batch_x[j].reshape([28, 28])
# Display reconstructed images
for j in range(n):
# Draw the reconstructed digits
canvas_recon[i * 28:(i + 1) * 28, j * 28:(j + 1) * 28] = g[j].reshape([28, 28])
# Plot the original image vs the reconstructed images.
print("Original Images")
plt.figure(figsize=(n, n))
plt.imshow(canvas_orig, origin="upper", cmap="gray")
plt.show()
print("Reconstructed Images")
plt.figure(figsize=(n, n))
plt.imshow(canvas_recon, origin="upper", cmap="gray")
plt.show()
训练后,您应该得到类似以下的结果,左侧是实际数字,右侧是重建的数字:

那么我们在这里做了什么?通过在无标签的数字上训练自编码器,我们完成了以下工作:
-
学习了数据集的潜在特征,且无需明确的标签
-
成功地学习了数据的分布,并从该分布中从头重建了图像
现在,假设我们想进一步开展工作,生成或分类我们还没有见过的新数字。为此,我们可以去掉解码器并附加一个分类器或生成器网络:

因此,编码器变成了初始化监督训练模型的一种方式。标准自编码器已在各种任务中得到应用。在本章的补充代码中,我们将演示一个示例,展示如何使用自编码器进行视觉异常检测。
变分自编码器
变分自编码器(VAE)基于标准自编码器的思想构建,是强大的生成模型,也是学习复杂分布的一种最流行的无监督方法。VAE 是基于贝叶斯推断的概率模型。概率模型正如其名所示:
概率模型将随机变量和概率分布纳入事件或现象的模型中。
变分自编码器(VAE)和其他生成模型是概率性的,因为它们试图学习一个分布,并利用该分布进行后续的采样。尽管所有生成模型都是概率模型,但并非所有概率模型都是生成模型。
概率结构的...
结构
像标准自编码器一样,变分自编码器(VAE)采用相同的编码器/解码器框架,但除了这一点,它们在数学上与其名称所示的有所不同。VAE 在指导网络时采取了概率的视角:

我们的编码器和解码器网络都在从其输入数据中生成分布。编码器从其训练数据生成一个分布,Z,然后这个分布成为解码器的输入分布。解码器接收这个分布,Z,并尝试从中重建原始分布,X。
编码器
编码器通过首先将其先验定义为标准正态分布来生成其分布。然后,在训练期间,这个分布会更新,解码器稍后可以轻松地从这个分布中进行采样。VAEs 中编码器和解码器在输出两个向量而不是一个方面是独特的:一个均值向量μ,另一个标准差向量σ。这些帮助定义了我们生成分布的限制。直观地说,均值向量控制输入的编码应该在哪里集中,而标准差控制编码可能从均值中变化的程度。编码器的这种约束迫使网络学习一个分布,从而...
解码器
像标准自动编码器一样,VAE 中的解码器是一个反向卷积网络,或者是一个反卷积网络。在处理解码时,数据是从生成过程中随机采样的,使得 VAE 成为少数可以直接从概率分布中采样而无需马尔可夫链蒙特卡洛方法的模型之一。由于随机生成过程的结果,我们从每次通过生成的编码将是数据的不同表示,同时保持相同的均值和标准差。这有助于解码器的采样技术;因为所有编码都是从相同的分布生成的,解码器学习到一个潜在数据点及其周围点都是同一类的成员。这使得解码器学会如何从类似但略有不同的编码中生成。
训练和优化 VAEs
VAEs 利用负对数似然损失作为它们的重构损失,以衡量在解码器重构阶段丢失了多少信息。如果解码器不能令输入令人满意地重构,则会产生大的重构损失。VAEs 还引入了称为Kullback–Leibler(KL)散度的东西到它们的损失函数中。KL 散度简单地衡量两个概率分布的差异程度;换句话说,它们彼此有多不同。我们希望最小化目标分布的均值和标准差与标准正态分布的 KL 距离。当均值为零且标准差为一时,它被正确地最小化。对数似然...
利用 VAE
我们可以在 TensorFlow 中构建一个变分自动编码器,以查看它与它更简单的标准自动编码器表亲的比较。在本节中,我们将使用相同的 MNIST 数据集,以便我们可以跨方法标准化我们的比较。让我们通过利用它来生成基于MNIST数据集的手写体来构建一个 VAE。将x视为每个我们试图学习的个体字符中的潜在特征,z为这些字符中的潜在特征。
首先,让我们从我们的导入开始:
import numpy as np
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
和之前一样,我们可以直接从 TensorFlow 库中导入'MNIST_data':
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
接下来,我们可以开始构建编码器。我们将继续使用之前相同的tf.layers包。在这里,我们的编码器看起来与之前的示例相似,我们的层会接收输入并逐渐压缩该输入,直到生成潜在的分布,z:
def encoder(x):
input_layer = tf.layers.dense(inputs=x, units=784, activation=tf.nn.elu,
kernel_initializer=initializer, bias_initializer=initializer,
name='input_layer'
)
hidden_1 = tf.layers.dense(inputs=input_layer, units=256, activation=tf.nn.elu,
kernel_initializer=initializer, bias_initializer=initializer
)
hidden_2 = tf.layers.dense(inputs=hidden_1, units=128, activation=tf.nn.elu,
kernel_initializer=initializer, bias_initializer=initializer
)
然而,在这里我们开始偏离标准自编码器了。虽然编码器中的最后一层会给出代表我们数据的潜在 z 分布,但我们需要计算
和
的值,这些值将有助于定义该分布。我们可以通过创建两个新的层来实现这一点,这两个层接收潜在的分布 z,并输出mu和sigma的值:
mu = tf.layers.dense(inputs=z, units=10, activation=None)
sigma = tf.layers.dense(inputs=z, units=10, activation=None)
接下来,我们将使用这些值来计算编码器的 KL 散度,最终将其用于构建我们的最终损失函数:
kl_div = -0.5 * tf.reduce_sum( 1 + sigma - tf.square(mu) - tf.exp(sigma), axis=1)
kl_div = tf.reduce_mean(latent_loss)
现在我们来创建变分自编码器的解码器部分;我们将创建一个反卷积模式,反转编码器的维度。所有这些将包含在一个名为decoder(z)的函数中:
def decoder(z, initializer):
layer_1 = fully_connected(z, 256, scope='dec_l1', activation_fn=tf.nn.elu,
kernel_initializer=initializer, bias_initializer=initializer
)
layer_2 = fully_connected(layer_1, 384, scope='dec_l2', activation_fn=tf.nn.elu,
kernel_initializer=initializer, bias_initializer=initializer
)
layer_3 = fully_connected(layer_2, 512, scope='dec_l3', activation_fn=tf.nn.elu,
kernel_initializer=initializer, bias_initializer=initializer
)
dec_out = fully_connected(layer_3, input_dim, scope='dec_l4', activation_fn=tf.sigmoid,
kernel_initializer=initializer, bias_initializer=initializer
)
在解码器函数下,我们将使用解码器的输出计算重建损失:
epsilon = 1e-10
rec_loss = -tf.reduce_sum(x * tf.log(epsilon + dec_out) + (1 - x) * tf.log(epsilon + 1 - dec_out), axis=1)
rec_loss = tf.reduce_mean(rec_loss)
和往常一样,我们将在初始化模型之前准备好训练参数。我们将定义学习率、训练批量大小、训练轮数、输入维度和总训练样本的大小:
learning_rate = 1e-4
batch_size = 100
epochs = 100
input_dim = 784
num_sample = 55000
n_z = 10
我们还将定义输入数据的占位符x:
x = tf.placeholder(name='x', dtype='float', shape=[None, input_dim])
在开始训练之前,我们将初始化模型、损失函数和optimizer:
## initialize the models
z, kl_div = encoder(x)
dec_out, rec_loss = decoder(x)
## Calculate the overall model loss term
loss = tf.reduce_mean(rec_loss + kl_div)
## Create the optimizer
optimizer = tf.train.AdamOptimizer(learning_rate).minimize(loss)
## Create the weight initializer
initializer = tf.contrib.layers.xavier_initializer()
最后,我们可以运行实际的训练过程。这将类似于我们已经构建和体验过的训练过程:
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for epoch in range(epochs):
for iter in range(num_sample // batch_size):
batch_x = mnist.train.next_batch(batch_size)
_, l, rl, ll = sess.run([optimizer, loss, rec_loss, kl_div], feed_dict={x: batch_x[0]})
if epoch % 5 == 0:
print('[Epoch {}] Total Loss: {}, Reconstruction Loss: {}, Latent Loss: {}'.format(epoch, l, rl, ll))
最后,我们可以使用以下代码生成从新训练的模型中生成的新样本:
z = np.random.normal(size=[batch_size, n_z])
x_generated = x_hat = self.sess.run(dec_out, feed_dict={z: z})
n = np.sqrt(batch_size).astype(np.int32)
I_generated = np.empty((h*n, w*n))
for i in range(n):
for j in range(n):
I_generated[i*h:(i+1)*h, j*w:(j+1)*w] = x_generated[i*n+j, :].reshape(28, 28)
plt.figure(figsize=(8, 8))
plt.imshow(I_generated, cmap='gray')
最终,你应该能得到如下所示的图像,左侧是原始数字,右侧是生成的数字:

观察一下生成的数字比原始自编码器清晰了多少:

现在,让我们看看如何通过 GAN 进一步推进这个过程。
生成对抗网络
生成对抗网络(GAN)是一类由 Ian Goodfellow 在 2014 年提出的网络。在 GAN 中,两个神经网络相互对抗作为敌人,采用演员-评论员模型,其中一个是创造者,另一个是审查者。创造者,称为生成器网络,试图创建能欺骗审查者(判别器网络)的样本。这两者之间相互博弈,生成器网络创造越来越逼真的样本,而判别器网络则变得越来越擅长识别这些样本。总结来说:
- 生成器试图最大化判别器将其输出判断为真实的概率,...
判别器网络
图像相关的 GAN 中的判别器网络是一个标准的卷积神经网络。它接收一张图片并输出一个数字,告诉我们这张图片是真实的还是伪造的。判别器接收图像并学习该图像的属性,以便它能成为一个良好的判定者,来判断生成器的输出。在 TensorFlow 中,我们可以将discriminator创建为一个函数,然后在后续的 TensorFlow 会话中运行它。这个框架和你在前面章节中看到的自编码器和变分自编码器差不多;我们将使用更高级的tf.layers API 来创建三个主要网络层和一个输出层。每个主网络层之后,我们会添加一个 dropout 层来进行正则化。最后一层会稍微不同,因为我们想对输出进行压缩。为此,我们将使用一个 sigmoid 激活函数,给出最终的输出,判断一张图片是否被认为是伪造的:
def discriminator(x, initializer, dropout_rate):
layer_1 = tf.layers.dense(x, units=1024, activation=tf.nn.relu, kernel_initializer=initializer,
bias_initializer=initializer, name='input_layer')
dropout_1 = tf.layers.dropout(inputs=layer_1, rate=dropout_rate, training=True)
layer_2 = tf.layers.dense(dropout_1, units=512, activation=tf.nn.relu, kernel_initializer=initializer,
bias_initializer=initializer, name='disc_layer_1')
dropout_2 = tf.layers.dropout(inputs=layer_2, rate=dropout_rate, training=True)
layer_3 = tf.layers.dense(dropout_2, units=256, activation=tf.nn.relu, kernel_initializer=initializer,
bias_initializer=initializer, name='disc_layer_2')
dropout_3 = tf.layers.dropout(inputs=layer_3, rate=dropout_rate, training=True)
output_layer = tf.layers.dense(dropout_3, units=1, activation=tf.sigmoid, kernel_initializer=initializer,
bias_initializer=initializer, name='disc_output')
return output_layer
现在我们已经定义了判别器,接下来我们继续讨论生成器。
生成器网络
你可以将 GAN 中的generator部分看作是一个逆卷积神经网络。和 VAE 一样,它使用通用的正态分布,唯一的区别是它通过上采样该分布来生成图像。这个分布代表我们的先验,并且在训练过程中随着 GAN 生成的图像变得越来越真实,判别器也无法判断其真假时,分布会不断更新。
在每一层之间,我们使用ReLu激活函数和batch_normalization来稳定每一层的输出。随着判别器开始检查generator的输出,generator会不断调整它抽样的分布,以便与目标分布更好地匹配。代码看起来会相当...
训练 GAN
GAN(生成对抗网络)容易训练,但由于其训练过程中存在许多不稳定的动态,优化起来较为困难。为了训练一个 GAN,我们在高维训练分布的子样本上训练生成器;由于这种分布本身并不存在,我们最初从标准正态(高斯)分布中进行采样。
生成器和判别器在一个最小最大游戏中共同训练,使用一个目标函数,也称为minimax函数:

让我们稍微解析一下。这个函数告诉我们发生了什么。首先来看一下第一个表达式的初始部分:

符号表示期望值,所以我们说判别器对于从实际分布中提取的真实图像的期望输出x将是:

同样,下面是第二个表达式:

它告诉我们,从生成分布中抽取的假图像,判别器的期望输出将是:

判别器希望最大化(
)目标函数,使得其对于真实数据D(x)的输出尽可能接近 1,而对于假数据D(G(z))的输出尽可能接近 0。与此同时,生成器则寻求相反的目标,最小化(
)目标函数,使得D(x)的输出尽可能接近 0,而D(G(z))的输出尽可能接近 1。数学上,这是生成器和判别器相互对抗的方式。
在训练 GAN 时,我们训练以最小化目标函数,使生成器能够获胜。我们希望生成器能够创建足够真实的例子来欺骗判别器。为此,我们并行训练和优化判别器和生成器,使用梯度上升法。在每次训练迭代中,我们会先在小批次中训练判别器网络,然后再在小批次中训练生成器网络,在这两种模式之间交替进行。判别器的梯度上升计算如下:

同时训练判别器和生成器可能会具有挑战性。如果我们尝试实际最小化生成器的损失函数,如下所示,我们将遇到一些问题:

如果我们查看minimax函数的图示,我们可以理解这是为什么:

优化过程寻找梯度信号,这些信号或多或少地告诉梯度下降该走哪条路。在minimax函数中,梯度下降的最大信号位于右侧,但我们实际上希望它学习位于函数左侧的值,在那里它被最小化为零,并且生成器成功欺骗了判别器。然而,随着生成器的优化,它将偏离最优点,导致我们远离应该到达的位置。为了解决这个问题,我们可以反转生成器的范式。我们可以让它专注于它做错的事情,而不是专注于它做对的事情:

通过最大化生成器的目标,我们实际上是在最大化错误的可能性。然而,这种并行训练过程仍然可能不稳定,稳定 GAN 仍然是当前非常活跃的研究领域。
让我们回到 TensorFlow 的过程。我们将从定义网络的训练参数开始:
learning_rate = 0.0002
batch_size = 100
epochs = 100
dropout_rate=0.5
接下来,我们需要定义我们的占位符,既包括输入x,也包括生成器将从中生成的z分布:
z = tf.placeholder(tf.float32, shape=(None, 100))
x = tf.placeholder(tf.float32, shape=(None, 784))
和之前一样,我们将创建一个 Glorot Initializer来初始化我们的权重和偏置值:
initializer = tf.contrib.layers.xavier_initializer()
一旦我们拥有了所有这些内容,我们就可以开始实际定义我们的网络部分了。你会注意到,在判别器部分,我们使用了一个叫做作用域(scope)的东西。作用域允许我们重复使用 TensorFlow 图中的项而不会产生错误——在这种情况下,我们想要在连续两次中使用判别器函数中的变量,所以我们使用了 TensorFlow 提供的tf.variable_scope函数。两者之间,我们只需使用scope.reuse_variables()函数来告诉 TensorFlow 我们在做什么:
G = generator(z, initializer)
with tf.variable_scope('discriminator_scope') as scope:
disc_real = discriminator(x, initializer, 0.5)
scope.reuse_variables()
disc_fake = discriminator(G, initializer, 0.5)
最后,我们将为生成器和判别器定义损失函数,并设置优化器:
epsilon = 1e-2
disc_loss = tf.reduce_mean(-tf.log(disc_real + epsilon) - tf.log(1 - disc_fake + epsilon))
gen_loss = tf.reduce_mean(-tf.log(disc_fake + epsilon))
disc_optim = tf.train.AdamOptimizer(lr).minimize(disc_loss)
gen_optim = tf.train.AdamOptimizer(lr).minimize(gen_loss)
然后,我们可以像前两个示例那样运行训练周期。你会看到的唯一两个不同点是,我们运行了两个优化过程,一个针对生成器,一个针对判别器:
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for epoch in range(epochs):
## Define the loss to update as a list
gen_loss = []
disc_loss = []
## Run the training iteration
for iter in range(training_data.shape[0] // batch_size):
## Batch the input for the discriminator
x_prime = training_data[iter*batch_size:(iter+1)*batch_size]
z_prime = np.random.normal(0, 1, (batch_size, 100))
## Run the discriminator session
_, DLoss = sess.run([disc_optim, disc_loss], {x: x_prime, z: z_prime, drop_out: 0.3})
disc_loss.append(DLoss)
## Run the generator session
z_prime = np.random.normal(0, 1, (batch_size, 100))
_, GLoss = sess.run([gen_optim, gen_loss], {z: z_prime, drop_out: 0.3})
gen_loss.append(GLoss)
if epoch % 5 == 0:
print('[%d/%d] - loss_d: %.3f, loss_g: %.3f' % ((epoch + 1), epochs, np.mean(D_losses), np.mean(G_losses)))
GANs 的计算开销相当大,因此训练该网络可能需要一段时间,除非你使用 Web 服务平台进行扩展。
如你所见,我们迄今为止运行的所有模型都是相互依赖的。即使是像 GANs 这样的先进生成模型,我们也可以使用某些方法来快速高效地创建强大的神经网络和更大的 AI 应用。
其他形式的生成模型
尽管我们只介绍了两种生成模型,但在文献中你可能会遇到许多不同类型的生成模型。以下图表并非详尽无遗,但它提供了关于现有生成模型类型的一个总体概览:

让我们来详细分析一下:
-
显式密度模型:直接从概率分布建模我们的数据。我们显式地定义概率并求解它。
-
隐式密度模型:学会从概率分布中采样,而无需定义该分布是什么
在显式密度模型中,我们有可解密度模型和近似密度...
完全可见的信念网络
完全可见的信念网络是一类显式密度模型,也是深度信念网络的一种形式。它们使用链式法则将概率分布! 分解成一个向量上的每个成员的乘积,如下所示:!。总的来说,它的公式是:

这个家族中最流行的模型是 PixelCNN,它是一个自回归生成模型。像素通过将图像生成问题转化为序列建模问题来处理图像生成问题,在这种方法中,下一像素的值由所有先前生成的像素值决定。网络一次扫描一个像素,并预测可能的像素值的条件分布。我们希望根据网络看到的最后几个像素给每个像素图像分配一个概率。例如,如果我们看的是与之前例子相同的马匹图像,我们将持续预测接下来预期的像素值如下所示:

基于我们已看到的特征,下一像素是否仍会包含马耳朵,还是会是背景?虽然它们的训练周期比 GAN 更稳定,但这些网络的最大问题是它们生成新样本的速度极慢;模型必须完全重新运行才能生成新样本。它们还会阻塞执行,意味着它们的过程不能并行运行。
隐马尔可夫模型
隐马尔可夫模型是一种马尔可夫模型,它本身是动态贝叶斯网络的一个子类。马尔可夫模型用于建模随机变化的系统,称为马尔可夫过程,也称为马尔可夫链。简而言之,马尔可夫过程是一系列事件,其中事件发生的概率仅依赖于前一个事件。
马尔可夫链如下所示:

在这个简单的链中,有三个状态,通过圆圈表示。我们接着有从一个状态过渡到另一个状态的概率,以及停留在当前状态的概率。马尔可夫链的经典例子是...
博尔兹曼机
博尔兹曼机是一类通用模型,输入为二进制向量,并且单元为每个二进制向量分配一个概率分布。正如下图所示,每个单元都依赖于其他所有单元:

Boltzmann 机使用一种叫做能量函数的东西,这与损失函数类似。对于任何给定的向量,特定状态的概率与每个能量函数值成正比。为了将其转换为实际的概率分布,需要对分布进行重新归一化,但这个问题又变成了另一个难以解决的问题。蒙特卡洛方法在这里再次被用作抽样的替代方案,从而使得 Boltzmann 机成为一种基于蒙特卡洛的方法。
假设我们有通过二进制特征表示的文档。Boltzmann 机可以帮助我们判断某个特定的单词或短语是否来自特定的文档。我们还可以使用 Boltzmann 机进行大规模复杂系统中的异常检测。它们在一定程度上表现良好,尽管在高维空间中这种方法效果不佳。
摘要
在这一章中,我们学习了人工智能中一些最令人兴奋的网络:变分自编码器和生成对抗网络(GANs)。这些网络都依赖于相同的基本概念——压缩数据,然后再从压缩后的数据中生成内容。你会记得这两种网络都是概率模型,意味着它们依赖于从概率分布中推断来生成数据。我们通过示例展示了这两种网络,并展示了如何使用它们来生成新图像。
除了学习这些令人兴奋的新技术外,最重要的是你学到了高级网络的构建块可以被分解成更小、更简单且重复的部分。当你思考……
参考文献
-
图片摘自 Ian Goodfellow, 《GANs 教程》,2017
第八章:强化学习
与生成对抗网络一起,强化学习算法是目前人工智能(AI)领域最为显著的进展。多年来,计算机科学家一直在努力创建能够像人类一样感知并对环境作出反应的算法和机器。强化学习就是这一努力的体现,它带来了广受欢迎的 AlphaGo 和自动驾驶汽车。在本章中,我们将介绍强化学习的基础,为本书后续章节中创建更先进的人工智能智能体打下基础。
强化学习发挥了人类从经验中学习的思想。像生成模型一样,它基于评估反馈进行学习。与指导性反馈不同...
技术要求
在本章中,我们将使用 Python 中的 TensorFlow。同时,我们还将使用 OpenAI gym 来测试我们的算法。
OpenAI gym 是一个用于开发和测试强化学习算法的开源工具包。它是用 Python 编写的,提供了从 Atari 游戏到机器人仿真等多种环境。当我们在本章及后续章节中开发强化学习算法时,gym 将为我们提供可以测试的环境,而这些环境通常是我们自己构建起来非常复杂的。
你将需要 macOS 或 Linux 环境来运行 gym。你可以通过运行一个简单的pip install命令来安装 gym:
pip install gym
现在你应该已经安装了 gym!如果遇到错误,可能是依赖问题。请检查官方 gym GitHub 仓库以获取最新的依赖项(github.com/openai/gym)。
强化学习原理
强化学习基于从与周围环境的互动中学习,并因此奖励在环境中采取的正面行动的概念。在强化学习中,我们将我们的算法称为智能体,因为它对周围的世界采取行动:

当智能体采取行动时,依据它是否采取了正确的行动,它会获得奖励或惩罚。我们在强化学习中的目标是让智能体学会采取能够最大化其从环境中获得的奖励的行动。这些概念并不新颖;事实上,它们已经存在...
马尔可夫过程
强化学习的核心是马尔可夫决策过程(MDP)。马尔可夫过程是随机事件的串联,其中事件发生的未来概率由最近事件的概率决定。它们通过在过程中特别添加奖励和决策扩展了基本的马尔可夫链。强化学习的基本问题可以建模为一个 MDP。马尔可夫模型是一类广泛应用于解决 MDP 问题的模型。
马尔可夫模型依赖于一个非常重要的属性,称为马尔可夫性质,即在马尔可夫过程中的当前状态完全表征并解释了那个时刻世界的状态;我们需要了解的关于预测未来事件的一切都依赖于我们当前所处的过程。例如,以下的马尔可夫过程模型描述了股市在任何给定时刻的状态。共有三种状态——牛市、熊市或停滞市场——以及在每个状态中停留或转换到另一个状态的相应概率:

导航马尔可夫决策过程(MDP)的实体称为代理。在这个例子中,代理就是股市本身。我们可以通过 SAP 记住马尔可夫过程的参数:
-
可能的状态集(S):代理在任何给定时刻可能处于的状态。我们在强化学习中谈论的状态指的就是这个。
-
可能的行动集(A):代理在其环境中可以采取的所有可能的行动。这些是状态之间的连接线;在两个状态之间可以发生哪些行动?
-
转移概率(P):转移到任何给定新状态的概率。
任何强化学习代理的目标是通过最大化它从采取特定行动中获得的奖励来解决给定的 MDP。
奖励
正如我们之前提到的,强化学习算法的目标是最大化它们潜在的未来奖励。在深度学习术语中,我们称之为预期奖励。在强化学习算法的训练过程中,每个时间步骤t,我们希望最大化回报R:

我们的最终奖励是每个时间步骤中所有预期奖励的总和——我们称之为累积奖励。在数学上,我们可以将上述方程式写作:

理论上,这个过程可以永远持续下去;终止...
策略
策略,简单来说,就是行动的方式;你的工作或教育机构有关于你如何、何时以及做什么事情的政策。在强化学习的语境下,这个术语没有区别。我们使用策略将状态映射到强化学习代理可以采取的潜在行动。数学上来说,强化学习中的策略用希腊字母π表示,它们告诉代理在给定的状态下应该采取什么行动。让我们通过一个简单的 MDP 来检验这一点;假设你熬夜到很晚,感到困倦,但也许你正沉浸在一部好电影中。你是继续清醒还是去睡觉?在这个场景中,我们会有三个状态:
-
你最初的困倦状态
-
充分休息
-
睡眠不足
每个状态都有与其相关的转移概率和奖励,这些基于采取的行动。

例如,假设你决定熬夜。在这种情况下,你会发现自己处于不睡觉的转移状态。从这里,你只能去一个地方——100%的概率进入睡眠不足状态。
如果你睡觉了,那么你有 90%的概率会恢复精力,10%的概率还是会感到疲倦。你也有可能因为没有睡得好而回到疲劳的状态。在这种情况下,我们希望选择那些能最大化奖励的行动(睡觉)。你脑海中告诉你去睡觉的声音就是策略。
我们的目标是学习一个策略 (
),使得网络的奖励最大化;我们称之为最优策略。在这种情况下,最优策略是确定性的,意味着在每个状态下都有一个明确的最优行动可采取。策略也可以是随机的,即从中可以抽取一个行动的分布。
强化学习代理可以在策略内或在策略外学习;当算法在策略内时,它通过所有代理的行动来学习策略,包括它可能采取的探索性行动。它改进了现有策略。在策略外学习是指偏离之前的策略,换句话说,是评估或学习一个与原始策略不同的策略。在本章后面,我们将讨论两种不同的强化学习方法——一种是在策略内的(策略梯度),另一种是在策略外的(Q 学习)。
为了帮助我们的算法学习最优策略,我们使用价值函数。
价值函数
价值函数帮助我们衡量在某些状态下的预期奖励;它表示在任何给定状态下,遵循某个策略的预期累计奖励。强化学习领域使用两种类型的价值函数;状态价值函数 V(s) 和 行动价值函数
。
状态价值函数描述了在遵循某个策略时,一个状态的价值。它是代理在某一状态s下,遵循策略π时,预期获得的回报。这个函数会给我们代理在开始遵循某个策略时,在状态s下的预期奖励:
让我们分解一下这个函数...
贝尔曼方程
作为强化学习领域最重要的方程之一,贝尔曼方程是解决强化学习问题的基石。贝尔曼是应用数学家,他开发了这个方程,它不仅仅是一个方程,更像是一个优化条件,用来模拟代理在某一时刻决策的奖励,基于该决策可能带来的预期选择和奖励。贝尔曼方程可以为状态值函数或动作值函数推导:


像往常一样,让我们分解这些方程式。我们将重点关注状态值函数。首先,我们有每个状态/行动对的所有策略的总和:

接下来,我们有转移概率;它是处于状态s,采取行动a,并最终到达状态的概率!:

接下来是我们之前讨论的累计奖励:

最后,我们有该函数的折扣值:

总的来说,我们描述了整个强化学习过程;我们希望找到一个满足贝尔曼方程的状态值函数或动作值函数。这里缺少一个关键部分;我们如何在实践中解决这个问题?一种选择是使用一种叫做动态规划的方法,它也是贝尔曼本人开发的优化方法。使用动态规划求解最优策略的一种方法是使用值迭代法。通过这种方式,我们将贝尔曼方程作为一个迭代更新函数。我们希望通过强制执行贝尔曼方程从 Q 收敛到 Q*:

让我们看看这在 Python 中是如何工作的,通过尝试解决推车倒立摆问题。通过值迭代法,我们从一个随机值函数开始,然后通过迭代过程找到改进的值函数,直到我们得到一个最优值函数。
我们可以尝试在推车倒立摆问题上进行这个操作。让我们导入gym并先生成一个随机的值函数:
import gym
import numpy as np
def gen_random_policy():
return (np.random.uniform(-1,1, size=4), np.random.uniform(-1,1))
接下来,让我们把策略转化为行动:
def policy_to_action(env, policy, obs):
if np.dot(policy[0], obs) + policy[1] > 0:
return 1
else:
return 0
最后,我们可以运行训练过程:
def run_episode(env, policy, t_max=1000, render=False):
obs = env.reset()
total_reward = 0
for i in range(t_max):
if render:
env.render()
selected_action = policy_to_action(env, policy, obs)
obs, reward, done, _ = env.step(selected_action)
total_reward += reward
if done:
break
return total_reward
if __name__ == '__main__':
env = gym.make('CartPole-v0')
n_policy = 500
policy_list = [gen_random_policy() for _ in range(n_policy)]
scores_list = [run_episode(env, p) for p in policy_list]
print('Best policy score = %f' %max(scores_list))
best_policy= policy_list[np.argmax(scores_list)]
print('Running with best policy:\n')
run_episode(env, best_policy, render=True)
虽然在这种简单的环境中值迭代方法有效,但当我们将其应用于更大、更复杂的环境时,很快就会遇到问题。因为我们必须单独计算每个状态/值对的值,许多未结构化的输入(如图像)变得庞大得难以处理。想象一下,如果每次我们的强化学习算法试图进行一次动作时,都需要为高级视频游戏中的每一个像素计算这个函数,那将是多么昂贵!
正因为如此,我们使用深度学习方法来为我们进行这些计算。深度神经网络可以充当函数逼近器。主要有两种方法,分别是深度 Q 学习和策略梯度。
Q–学习
Q 学习是一种利用动作值函数或 Q 函数来解决任务的强化学习方法。在本节中,我们将讨论传统的 Q 学习和深度 Q 学习。
标准 Q 学习基于 Q 表的核心概念。你可以将 Q 表看作一个参考表;每一行代表一个状态,每一列代表一个动作。表中的值是针对特定的动作和状态组合所获得的预期未来奖励。从程序步骤上,我们做如下操作:
-
初始化 Q 表
-
选择一个动作
-
执行动作
-
测量获得的奖励
-
更新 Q 值
让我们逐步解析这些步骤,以更好地理解算法。…
策略优化
策略优化方法是 Q 学习和价值函数逼近的替代方法。这些方法不是学习状态/动作对的 Q 值,而是通过计算梯度直接学习一个将状态映射到动作的策略π。从根本上讲,对于像优化问题这样的搜索,策略方法是一种从潜在的政策动作的随机分布中学习正确策略的手段。因此,我们的网络架构稍作调整,以直接学习策略:

因为每个状态都有一组可能的动作分布,所以优化问题变得更简单。我们不再需要为特定动作计算精确的奖励。回想一下,深度学习方法依赖于“回合”这一概念。在深度强化学习中,每个回合代表一场游戏或任务,而轨迹代表游戏或任务中的某些动作或方向。我们可以将轨迹定义为状态/动作/奖励组合的路径,用希腊字母 tau (Τ)表示:

想象一个机器人学习走路;如果我们使用 Q 学习或其他动态规划方法来完成这个任务,我们的算法需要精确地学习如何为每个可能的轨迹中的每个关节动作分配奖励。算法需要学习时机、如何弯曲机器人的肢体角度,等等。通过直接学习策略,算法可以简单地专注于当机器人走路时,如何移动机器人的脚。
在使用策略梯度方法时,我们可以像使用 Q 学习那样简单地定义一个个体策略;我们的策略是未来折扣奖励的期望总和:

因此,我们网络的目标是最大化某个策略,J,以最大化我们期望的未来奖励:

学习策略的一种方法是使用梯度方法,因此得名策略梯度。由于我们希望找到最大化奖励的策略,我们在潜在策略的分布上执行与梯度下降相反的操作——梯度上升:

新神经网络的参数成为我们策略的参数,因此通过执行梯度上升并更新网络参数,
变为
。完整推导策略梯度超出了本书的范围,但让我们简要介绍一些策略梯度方法在实际中的高级概念。请看以下的景观;有三条潜在路径和三种可能的最终奖励:

策略梯度方法旨在增加采取通向最高奖励路径的概率。从数学上讲,标准的策略梯度方法如下所示:

和往常一样,让我们稍微分解一下。我们可以从以下几点开始:我们的梯度,g,是梯度期望值与某一给定动作/状态对的策略的对数相乘,再乘以优势。优势即为动作价值函数减去状态价值函数:

在实现中,传统的策略梯度方法仍然会遇到问题。一个特别显著的问题被称为“信用分配问题”,即从一个长时间交互轨迹中获得的奖励信号发生在轨迹的末尾。策略梯度方法很难确定是哪一步动作导致了这个奖励,并且难以分配信用给该动作。
策略梯度是一类可以围绕这一思想构建的算法的概述。优化这些方法的关键在于各个优化过程的细节。在接下来的部分,我们将探讨几种改进传统策略梯度方法的方式。
策略优化的扩展
计算策略梯度的一种常见方法是Reinforce 算法。Reinforce 是一种蒙特卡洛策略梯度方法,它使用似然比来估计给定点上策略的价值。该算法可能导致较高的方差。
传统的策略梯度方法可能会比较具有挑战性,因为它们对步长参数的选择极其敏感。步长过大会使得正确的策略被噪声压倒——步长过小则训练变得异常缓慢。我们的下一类强化学习算法,近端策略优化(PPO),旨在解决策略梯度的这一短板。PPO 是一类新的强化学习算法,旨在...
总结
在本章中,我们学习了强化学习的重要基础,它是人工智能领域中最为显著的实践之一。
强化学习基于智能体在环境中行动的概念,并根据它所观察到的周围环境做出决策。智能体的行为由策略优化方法或动态规划方法指导,这些方法帮助它学习如何与环境互动。当我们更注重探索和离策略学习时,我们使用动态规划方法。另一方面,当我们面临密集的、连续的问题空间且只想优化我们关心的内容时,我们使用策略优化方法。
在接下来的章节中,我们将探讨强化学习在多个现实世界中的应用。
第九章:智能代理的深度学习
智能助手是我们日常生活中最显眼的人工智能(AI)形式之一。Siri、Alexa 和其他系统已经成为 21 世纪日常生活中的常见事物。本章将开始深入探讨人工神经网络(ANNs)在创建 AI 系统中的应用。在本章中,我们将介绍一个新话题,词嵌入,然后继续聚焦于递归神经网络(RNNs)和生成网络在自然语言处理任务中的应用。尽管整本书可以专门写关于自然语言处理的深度学习内容(实际上也已经有了相关书籍),我们会简要介绍…
技术要求
在本章中,我们将使用 Python 3 和一些你之前见过的标准 Python 包:
-
Numpy
-
TensorFlow
-
一台支持 GPU 的计算机,或一个用于云计算的 AWS 账户,详细信息请参见第三章,平台与其他必需工具
词嵌入
目前为止,在我们对 AI 和深度学习的讨论中,我们重点讨论了该领域在基本数学原理中的根基;那么,当我们面临像文本这样未结构化的数据源时,我们该怎么办呢?在前几章中,我们讨论了如何通过卷积将图像转换为数字,那么我们如何用文本做同样的事情呢?在现代 AI 系统中,我们使用一种叫做词嵌入的技术。
词嵌入本身并不是一种预测模型,而是处理文本的手段,使其能够作为预测模型的输入,或作为数据挖掘中的一种探索性技术。它是将单词和句子转换为数字向量的方式,这些数字向量被称为词嵌入。
Word2vec
Word2vec 算法由 Tomas Mikolav 在 2013 年于 Google 发明,是最早的现代嵌入方法之一。它是一个浅层的、两层的神经网络,其直觉与自编码器类似,目的是执行某个特定任务,但并不直接用于执行该任务。在 Word2vec 算法的情况下,这个任务就是学习自然语言的表示。你可以将这个算法看作一个上下文算法——它所知道的一切都来源于学习单词在句子中的上下文。它基于所谓的分布假设,该假设告诉我们每个单词的上下文是通过它相邻的单词来定义的。例如,假设有一个 500 维的语料库向量,语料库中的每个单词都会通过权重分布在这些元素上进行表示。这不是一种一对一的映射;每个单词的嵌入是依赖于语料库中其他所有单词的。
在其基本形式中,Word2vec 具有与我们已经看到的许多前馈神经网络类似的结构——它有一个输入层、一个隐藏层和一个输出层,所有这些都由矩阵W进行参数化。它逐字遍历输入语料库,并为每个单词开发向量。实际上,该算法包含两种不同的变体,CBOW(连续词袋模型)和 skip-gram 模型,它们以不同的方式处理词向量的生成。从架构上讲,skip-gram 模型和 CBOW 模型本质上是彼此的倒置版本。
skip-gram 模型如以下图所示:

skip-gram 模型的镜像是 CBOW 模型:

在 skip-gram 模型中,网络查看词语序列,并尝试预测某一特定词语组合发生的可能性。skip-gram 方法在给定一个特定单词的情况下预测上下文。该模型输入一个单一的字母,w,并输出一个词向量,w[1], w[2]... w[n]。分解这个过程,前述模型在训练过程中接收一个输入单词的 one-hot 编码表示。输入层与隐藏层之间的矩阵代表了网络正在构建的词汇表,其行将成为我们的向量。矩阵中的每一行对应词汇表中的一个单词。随着新数据流经网络,矩阵按行逐步更新,添加新的嵌入。再次提醒,我们实际上并不关心网络输出的结果;我们关心的是在矩阵 W 中创建的嵌入。该方法在训练数据较少时表现良好,并且适合嵌入稀有单词。在 CBOW 方法中,模型的输入是 w[1], w[2] ... w[n],即当前单词周围的上下文单词。CBOW 根据上下文预测单词。CBOW 方法比 skip-gram 方法速度更快,适合嵌入频繁出现的单词,但它需要大量的数据,因为它依赖于上下文作为输入。
为了进一步说明这一点,考虑这个简单的句子:
The dog jumped over the fence
skip-gram 模型通过关注一个主题来解析句子,将该主题分解为称为gram的块,每次跳过如下:
{The dog jumped, The dog over, dog jumped over, dog jumped the .... 等等}
另一方面,在 CBOW 方法下,gram 会依次遍历句子的上下文,如下所示:
{The dog jumped, dog jumped over, jumped over the, over the fence}
训练 Word2vec 模型
由于 Word2vec 模型本身是神经网络,我们像训练标准的前馈神经网络一样训练它们,使用损失函数和随机梯度下降。在训练过程中,算法扫描输入语料库,并将其批量输入。每处理完一个批次,就会计算一次损失。在优化时,我们希望像训练标准的前馈神经网络一样最小化损失。
让我们来看看如何在 TensorFlow 中创建和训练一个 Word2vec 模型:
- 首先,让我们从导入开始。我们将使用标准的
tensorflow和numpy导入,以及 Python 库 itertools,还会使用机器学习包scikit-learn中的两个实用函数。以下代码块展示了...
GloVe
全球化向量 (GloVe) 是斯坦福 NLP 小组在 2014 年开发的,作为 Word2Vec 的概率性后续版本。GloVe 旨在保留 Word2vec 使用的类比框架,但采用维度约简技术来保留关于单词本身的关键统计信息。与通过流式句子学习的 Word2vec 不同,GloVe 通过构建丰富的共现矩阵来学习词嵌入。共现矩阵是一个全球语义信息存储库,是 GloVe 算法的关键。GloVe 的创造者基于这样的原则开发了它:在上下文中,两个词之间的共现比率与其意义密切相关。
那么它是如何工作的,和 Word2vec 有什么不同呢?GloVe 通过以下方式创建词嵌入:
-
逐字迭代句子
-
对于每个词,算法会查看它的上下文。
-
给定词汇及其上下文,GloVe 会在共现矩阵中创建一个新条目。
-
GloVe 随后通过降低共现矩阵的维度来创建词嵌入。
-
在创建词嵌入后,GloVe 会根据该嵌入的准确性计算新的损失函数。
让我们通过 Python 实现一步步走过 GloVe 算法,看看它是如何进行的。我们将使用康奈尔电影台词数据集,它包含超过 200,000 行虚构的电影剧本台词。稍后,我们将在智能体中使用从该数据集生成的词嵌入。首先,让我们写一个函数来导入数据:
import os
def loadText(fileName, fields):
''' Function to Load the Movie Lines text '''
lines = {}
with open(fileName, 'r', encoding='iso-8859-1') as f:
for line in f:
values = line.split(" +++$+++ ")
lineObj = {}
for i, field in enumerate(fields):
lineObj[field] = values[i]
lines[lineObj['lineID']] = lineObj
return lines
然后我们可以使用这个函数来加载电影台词:
lines = {}
movie_lines = ["lineID","characterID","movieID","character","text"]
lines = loadText("/users/patricksmith/desktop/glove/movie_lines.txt", movie_lines)
现在,回到 GloVe。与 Word2vec 不同,GloVe 逐字解析句子,通过使用固定的上下文窗口大小来关注局部上下文。在词嵌入中,窗口大小表示算法关注的范围及其为词义提供上下文的方式。GloVe 中有两种上下文窗口大小——对称和不对称。例如,看看以下句子:
马在比赛的终点线上快速跑过。
在对称窗口大小的情况下,算法会查看主题两侧的单词。如果 GloVe 在前面的示例中查看单词 finish,窗口大小为 2,那么上下文将是 across the 和 line in。非对称窗口仅查看前面的单词,因此相同的窗口大小 2 将捕获 across 和 the,但不会捕获 line in。
让我们继续初始化我们的 GloVe 类和变量:
class GloVeModel():
def __init__(self, embedding_size, window_size, max_vocab_size=100000, min_occurrences=1,
scaling_factor=3/4, cooccurrence_cap=100, batch_size=512, learning_rate=0.05):
self.embedding_size = embedding_size
if isinstance(context_size, tuple):
self.left_context, self.right_context = context_size
elif isinstance(context_size, int):
self.left_context = self.right_context = context_size
self.max_vocab_size = max_vocab_size
self.min_occurrences = min_occurrences
self.scaling_factor = scaling_factor
self.cooccurrence_cap = cooccurrence_cap
self.batch_size = batch_size
self.learning_rate = learning_rate
self.__words = None
self.__word_to_id = None
self.__cooccurrence_matrix = None
self.__embeddings = None
def fit_to_corpus(self, corpus):
self.__fit_to_corpus(corpus, self.max_vocab_size, self.min_occurrences,
self.left_context, self.right_context)
self.__build_graph()
def __fit_to_corpus(self, corpus, vocab_size, min_occurrences, left_size, right_size):
word_counts = Counter()
cooccurrence_counts = defaultdict(float)
for region in corpus:
word_counts.update(region)
for l_context, word, r_context in _context_windows(region, left_size, right_size):
for i, context_word in enumerate(l_context[::-1]):
cooccurrence_counts[(word, context_word)] += 1 / (i + 1)
for i, context_word in enumerate(r_context):
cooccurrence_counts[(word, context_word)] += 1 / (i + 1)
if len(cooccurrence_counts) == 0:
raise ValueError("No coccurrences in corpus. Did you try to reuse a generator?")
self.__words = [word for word, count in word_counts.most_common(vocab_size)
if count >= min_occurrences]
self.__word_to_id = {word: i for i, word in enumerate(self.__words)}
self.__cooccurrence_matrix = {
(self.__word_to_id[words[0]], self.__word_to_id[words[1]]): count
for words, count in cooccurrence_counts.items()
if words[0] in self.__word_to_id and words[1] in self.__word_to_id}
我们得到一个共现矩阵,可以告诉我们在给定窗口大小的情况下,某些单词一起出现的频率:

虽然这张表看起来很简单,但它包含了关于单词共现的全局统计特性。我们可以从中计算出某些单词一起出现的概率:

由于共现矩阵的大小是组合性的(它会迅速变得非常大),这导致了极其庞大的共现信息矩阵。我们该如何解决这个问题?我们可以对矩阵进行分解,创建一个低维矩阵,其中每一行包含给定单词的向量表示。这对共现矩阵执行一种降维操作。然后我们通过标准化和对数平滑处理该矩阵中的发生信息。GloVe 将学习向量,使它们的差异预测发生比率。所有这些操作会保持丰富的全局统计特性,同时仍然保留使得 Word2vec 如此受欢迎的类比关系。
GloVe 通过最小化重建损失来训练,帮助模型找到能够解释原始数据中最大方差的低维表示。它使用最小二乘损失函数,旨在最小化单词的两个嵌入的点积与它们的共现计数的对数之间的差异:

让我们来解析一下;w[i] 是一个单词向量,b[i] 是特定单词 i 的偏置因子,而 w[j] 和 b[j] 是上下文向量的单词向量和偏置因子。X[ij] 是共现矩阵中记录 i 和 j 一起出现次数的计数。f 是一个加权函数,用于处理稀有和频繁的共现,以避免它们对结果产生偏差。总的来说,这个损失函数查看一个单词及其邻近上下文单词的加权共现,然后将其乘以右侧项,这个项计算了单词、其上下文、偏置和共现的组合。
让我们继续初始化 GloVe 的图形,并在 TensorFlow 中进行训练过程:
def __build_graph(self):
self.__graph = tf.Graph()
with self.__graph.as_default(), self.__graph.device(_device_for_node):
count_max = tf.constant([self.cooccurrence_cap], dtype=tf.float32,
name='max_cooccurrence_count')
scaling_factor = tf.constant([self.scaling_factor], dtype=tf.float32,
name="scaling_factor")
self.__focal_input = tf.placeholder(tf.int32, shape=[self.batch_size],
name="focal_words")
self.__context_input = tf.placeholder(tf.int32, shape=[self.batch_size],
name="context_words")
self.__cooccurrence_count = tf.placeholder(tf.float32, shape=[self.batch_size],
name="cooccurrence_count")
focal_embeddings = tf.Variable(
tf.random_uniform([self.vocab_size, self.embedding_size], 1.0, -1.0),
name="focal_embeddings")
context_embeddings = tf.Variable(
tf.random_uniform([self.vocab_size, self.embedding_size], 1.0, -1.0),
name="context_embeddings")
focal_biases = tf.Variable(tf.random_uniform([self.vocab_size], 1.0, -1.0),
name='focal_biases')
context_biases = tf.Variable(tf.random_uniform([self.vocab_size], 1.0, -1.0),
name="context_biases")
focal_embedding = tf.nn.embedding_lookup([focal_embeddings], self.__focal_input)
context_embedding = tf.nn.embedding_lookup([context_embeddings], self.__context_input)
focal_bias = tf.nn.embedding_lookup([focal_biases], self.__focal_input)
context_bias = tf.nn.embedding_lookup([context_biases], self.__context_input)
weighting_factor = tf.minimum(
1.0,
tf.pow(
tf.div(self.__cooccurrence_count, count_max),
scaling_factor))
embedding_product = tf.reduce_sum(tf.multiply(focal_embedding, context_embedding), 1)
log_cooccurrences = tf.log(tf.to_float(self.__cooccurrence_count))
distance_expr = tf.square(tf.add_n([
embedding_product,
focal_bias,
context_bias,
tf.negative(log_cooccurrences)]))
single_losses = tf.multiply(weighting_factor, distance_expr)
self.__total_loss = tf.reduce_sum(single_losses)
tf.summary.scalar("GloVe_loss", self.__total_loss)
self.__optimizer = tf.train.AdagradOptimizer(self.learning_rate).minimize(
self.__total_loss)
self.__summary = tf.summary.merge_all()
self.__combined_embeddings = tf.add(focal_embeddings, context_embeddings,
name="combined_embeddings")
接下来,我们将编写两个函数,为 GloVe 模型准备数据批次,和我们在 Word2vec 中做的一样。记住,这一切仍然包含在我们的 GloVe 类中:
def batchify(batch_size, *sequences):
for i in range(0, len(sequences[0]), batch_size):
yield tuple(sequence[i:i+batch_size] for sequence in sequences)
def MakeBatches(self):
''' Make Batches of Data to Feed The Model'''
cooccurrences = [(word_ids[0], word_ids[1], count)
for word_ids, count in self.__cooccurrence_matrix.items()]
i_indices, j_indices, counts = zip(*cooccurrences)
return list(batchify(self.batch_size, i_indices, j_indices, counts))
现在,我们将了解不同的属性。对于那些之前使用过 Java 的朋友来说,你们一定熟悉 getter 和 setter 的概念。这些方法可以控制变量的变化。@property 装饰器是 Python 对这些概念的回应,具体如下:
@property
def foo(self): return self._foo
## Is the same as
def foo(self):
return self._foo
foo = property(foo)
在这里,foo 函数被一个新的函数 property(foo) 替代,这个新函数是一个具有特殊属性的对象,称为描述符。现在,让我们回到 Word2vec:
@property
def vocab_size(self):
return len(self.__words)
@property
def words(self):
if self.__words is None:
raise NotFitToCorpusError("Need to fit model to corpus before accessing words.")
return self.__words
@property
def embeddings(self):
if self.__embeddings is None:
raise NotTrainedError("Need to train model before accessing embeddings")
return self.__embeddings
def id_for_word(self, word):
if self.__word_to_id is None:
raise NotFitToCorpusError("Need to fit model to corpus before looking up word ids.")
return self.__word_to_id[word]
我们还将为 ContextWindow 创建一个函数,告诉 GloVe 哪些词语需要关注:
def ContextWindow(region, left_size, right_size):
for i, word in enumerate(region):
start_index = i - left_size
end_index = i + right_size
left_context = _window(region, start_index, i - 1)
right_context = _window(region, i + 1, end_index)
yield (left_context, word, right_context)
## Function to Create the Window Itself
def window(region, start_index, end_index):
last_index = len(region) + 1
selected_tokens = region[max(start_index, 0):min(end_index, last_index) + 1]
return selected_tokens
最后,我们将编写一个用于训练的函数:
def train(self, num_epochs, log_dir=None, summary_batch_interval=1000):
## Initialize the total steps variable, which will be incrementally adjusted in training
total_steps = 0
## Start a TensorFlow session
with tf.Session(graph=self.__graph) as session:
if should_write_summaries:
summary_writer = tf.summary.FileWriter(log_dir, graph=session.graph)
## Initialize the variables in TensorFlow
tf.global_variables_initializer().run()
for epoch in range(num_epochs):
shuffle(batches)
for batch_index, batch in enumerate(batches):
i_s, j_s, counts = batch
if len(counts) != self.batch_size:
continue
feed_dict = {
self.__focal_input: i_s,
self.__context_input: j_s,
self.__cooccurrence_count: counts}
session.run([self.__optimizer], feed_dict=feed_dict)
最后,我们可以使用以下内容运行我们的 GloVe 模型:
model = GloVeModel(embedding_size=300, context_size=1)
model.fit_to_corpus(corpus)
model.train(num_epochs=100)
GloVe 关于共现信息的密集矩阵的想法并不新鲜;它来源于一种更传统的技术,叫做潜在语义分析(LDA),该技术通过使用一种叫做奇异值分解(SVD)的数学方法,分解词袋模型的文档矩阵来学习嵌入。
构建基本代理
使用 TensorFlow 构建人工助手的最简单方法是使用序列到序列(Seq2Seq)模型,这个我们在 RNN 章节中已经学过。

虽然最初是为神经机器翻译开发的,但我们可以调整这个模型使其作为智能聊天机器人,以便实现我们自己的目的。我们将创建作为助手“大脑”的 Python 类,命名为 IntelligentAssistant。然后,我们将创建助手的训练和聊天函数:
- 首先,让我们从标准导入开始,并初始化我们的变量。请特别注意这里的
mask变量;masks是占位符,允许……
总结
在这一部分中,我们学习了如何通过使用词嵌入和人工神经网络(ANN)来创建新型的、最先进的智能助手。词嵌入技术是自然语言人工智能应用的基石。它们使我们能够将自然语言编码为数学模型,然后将其输入到下游模型和任务中。
智能代理利用这些词嵌入进行推理。它们使用两个 RNN,一个编码器和一个解码器,构成所谓的 Seq2Seq 模型。如果你回想一下 RNN 章节中的内容,Seq2Seq 模型中的第一个 RNN 将输入编码为压缩表示,而第二个网络则从该压缩表示中获取信息来生成句子。通过这种方式,智能代理学会根据训练过程中学到的表示来回应用户。
在下一章节中,我们将探讨如何创建能够玩棋类游戏的智能代理。
第十章:游戏 AI 的深度学习
在过去几年中,人工智能(AI)最显著的应用之一就是游戏领域,尤其是随着 AlphaGo 和 AlphaGo Zero 的成功,游戏类 AI 成为公众广泛关注的话题。在本章中,我们将实现两种基本的游戏 AI 版本;一种用于视频游戏,另一种用于棋盘游戏。我们主要会使用强化学习方法作为我们的核心方法。我们还将介绍一些目前存在的最先进的游戏 AI 背后的方法。
在本章中,将涵盖以下主题:
-
游戏树与基本博弈论
-
构建一个 AI 代理来玩井字游戏
-
构建...
技术要求
在本章中,我们将使用以下内容:
-
TensorFlow
-
OpenAI Gym
强烈建议你在训练本章中的模型时,能够使用 GPU,无论是在自己的机器上还是通过云服务平台。
介绍
AI 最近在游戏领域引起了广泛关注。创造 AlphaGo 的 Google 研究团队 DeepMind 一直提倡在游戏应用中使用强化学习方法。越来越多的视频游戏公司开始使用深度学习方法来开发其 AI 玩家。那么,我们从哪里开始,AI 在游戏领域的发展又将走向何方呢?
传统上,游戏 AI 系统由一系列硬编码规则组成,覆盖了 AI 应对的各种行为。你是否曾玩过一些老旧的冒险游戏、第一人称射击游戏或策略游戏,其中的 AI 玩家显然是根据硬编码策略运作的?这些 AI 通常...
棋盘游戏的网络
当我们谈论为游戏设计算法时,实际上是在谈论为特定类型的游戏创建算法,这种游戏被称为有限的二人零和顺序游戏。这实际上就是在说:
-
两个独立角色之间的互动情境(一个游戏)
-
在任何时刻,两方玩家的互动方式是有限的
-
游戏是零和的,这意味着游戏的最终状态导致其中一方完全获胜
-
游戏是顺序进行的,这意味着角色按顺序一个接一个地进行移动
本节中,我们将介绍一些经典的游戏类型示例,如井字游戏、国际象棋和围棋。由于为围棋这样的棋盘游戏创建和训练一个算法将是一个巨大的任务,出于时间和计算约束,我们将在一个有限步骤且更为合理的游戏中创建一个代理:国际象棋。在本节中,我们将介绍一些游戏 AI 背后的基本概念,并通过 Python 示例演示不同的策略。
理解游戏树
如果你熟悉博弈论,你就知道有一整个数学分支致力于理解和分析游戏。在计算机科学的世界里,我们可以通过将游戏简化为一个决策树结构来分析它,这就是博弈树。博弈树是一种描绘棋盘游戏中可能走法和状态的方法。让我们以井字游戏的博弈树为例,如下所示:

这棵树展示了基于某一特定起始点的所有可能组合和游戏结果。从一个节点到另一个节点的移动表示一步棋,棋步持续进行...
AlphaGo 和智能游戏 AI
虽然 MCTS 已经是游戏 AI 的基石,但正是 DeepMind 的 AlphaGo 程序将游戏 AI 带入了现代时代。AlphaGo 及其衍生版本(AlphaGo Zero 和 AlphaGo Master)是利用 MCTS 来玩极其复杂的古老中国围棋的游戏 AI 系统。围棋有 10⁷⁶¹种可能的棋局组合,创造一个可以玩这款游戏的系统成为了 AI 领域的一个里程碑。它甚至成为了一部同名的广受关注的纪录片的主题。
AlphaGo 结合了 MCTS 和深度学习方法,使得 AlphaGo 程序变得非凡。DeepMind 训练了深度神经网络,类似于我们在本书中学习的那种,来学习游戏状态,并有效地引导 MCTS 以更智能的方式进行搜索。这个网络会查看棋盘的当前状态,以及之前的棋步,决定接下来该下什么棋。DeepMind 在 AlphaGo 中的主要创新是使用深度神经网络来理解游戏的状态,然后利用这种理解来智能地引导 MCTS 的搜索。系统的架构方式使得 AlphaGo 能够自我学习游戏,首先通过观看人类下棋,其次通过与自己对弈并纠正之前的错误。
该架构实际上使用了两种不同的神经网络:策略网络和价值网络:
-
价值网络:通过近似价值函数来减少 MCTS 搜索的深度。
-
策略网络:通过模拟未来的行动来减少 MCTS 搜索的广度。策略网络从实际的人类围棋游戏中学习,并据此发展出策略:

让我们深入了解这两者,以理解系统是如何工作的。
AlphaGo 策略网络
策略网络的目标是捕捉并理解棋盘上玩家的一般行动,以帮助 MCTS 在搜索过程中引导算法朝向有前景的行动;这减少了搜索的广度。在架构上,策略网络分为两部分:一个监督学习策略网络和一个强化学习策略网络。
第一个网络,监督网络,是一个 13 层的卷积神经网络(CNN)。它通过观察人类在游戏中进行的动作进行训练——准确来说是 3000 万次——并根据特定状态输出每个动作的概率分布。我们将这种类型的监督学习称为行为克隆。
...
AlphaGo 价值网络
价值网络被用来通过引导 MCTS 朝向某些节点来减少系统玩法中的误差。它有助于减少搜索的深度。AlphaGo 价值网络通过与自己进行更多的对局来进行训练,以优化从策略网络学到的策略,方法是通过估算价值函数,特别是行动价值函数。回顾一下第八章,强化学习,行动价值函数描述了在特定状态下采取某个行动的价值。它衡量的是从一对状态和行动中得到的累积奖励;对于给定的状态和我们采取的行动,这会增加多少奖励?它让我们假设,如果在第一步采取不同的行动,而不是代理人可能希望做的,然后在之后遵循策略,会发生什么。行动价值函数通常也被称为Q-函数,因为我们用 Q 来表示它:

网络通过利用来自策略网络的噪声版本的策略并回归棋盘状态与游戏结果,来逼近价值函数。
该网络使用我们在第八章中学习的强化算法进行训练,强化学习。如果你还记得,Reinforce 是一种蒙特卡洛策略梯度方法,使用似然比来估计某一点上的策略价值。Reinforce 算法试图最大化预期奖励,因此整个系统有两个目标:像专业人类玩家一样玩游戏,同时尝试赢得比赛。
AlphaGo 在行动中
我们已经讨论了 AlphaGo 如何帮助选择行动,现在让我们回到任何游戏系统的核心:游戏树。虽然 AlphaGo 利用了游戏树和 MCTS,作者们创造了一个变种,称为异步策略和价值 MCTS(APV-MCTS)。与我们之前讨论的标准 MCTS 不同,APV-MCTS 通过两种不同的指标来决定扩展和评估哪个节点:
-
价值网络的结果
-
蒙特卡洛模拟的结果
这些方法的结果与混合参数λ相结合。然后,算法根据在初始监督学习阶段获得的概率选择一个动作。虽然使用这些概率似乎有些反直觉……
视频游戏的网络
到目前为止,我们已经学习了如何使用强化学习方法来玩桌面游戏,利用 UCT 和 MCTS;现在,让我们看看在视频游戏中我们能做些什么。在第八章《强化学习》中,我们看到如何使用强化学习方法来完成诸如 OpenAI 倒立摆挑战之类的基本任务。在这一节中,我们将重点关注一组更困难的游戏:经典的雅达利视频游戏,这些游戏已成为深度学习任务的标准基准。
你可能在想——我们不能将使用在倒立摆环境中的方法扩展到雅达利游戏中吗? 其实可以,但我们需要处理的输入信息要多得多。在雅达利环境中,实际上在任何视频游戏环境中,网络的输入都是单个像素。与倒立摆的四个简单控制变量不同,我们现在处理的是 100,800 个变量(210 * 160 * 3)。因此,这些网络的复杂性和训练时间可能会更高。在这一节中,我们将尽量简化网络,使其更容易学习和训练。
我们将使用 OpenAI Gym 环境来模拟雅达利游戏《太空入侵者》:

对于那些不熟悉《太空入侵者》的朋友来说,概念很简单——你(屏幕底部的绿色火箭)必须在外星飞船摧毁你之前摧毁它们。外星飞船会用导弹攻击你,你也可以反击。谷歌的 DeepMind 最早在他们的论文《用深度强化学习玩雅达利》Playing Atari with Deep Reinforcement Learning中将《太空入侵者》作为基准任务提出,这也真正推动了将雅达利游戏作为基准任务,用智能体击败的概念。
我们将构建一个叫做深度 Q 网络的东西,我们在第八章《强化学习》中提到过它。在下一节中,我们将进一步探讨在该章节中涵盖的许多基本 Q 学习内容。话不多说——让我们开始吧!
构建深度 Q 网络
深度 Q 网络最早由 DeepMind 在他们的论文《通过深度强化学习实现人类级别的控制》Human-level control through deep reinforcement learning中提出,该论文发表在英国科学期刊《Nature》上,现在通常被称为Nature 论文。深度 Q 学习的目标是创建一个能够从高维输入(如视频游戏)中学习策略的 AI 代理。在我们的案例中,我们希望构建一个能够通过基本任务并向更困难任务推进的深度 Q 网络。
深度 Q 网络通过使用人工神经网络作为值逼近器来逼近 Q 值,而不是使用 Q 表单独计算它们。网络的输入将是一堆预处理过的帧,接下来...
利用目标网络
让我们回顾一下 Q-函数优化过程:

你可能会注意到,这个函数有一个独特的属性,那就是它是递归的;一组 Q 值依赖于另一组 Q 值的值。这在训练中会成为一个问题;如果我们改变一组值,就会改变另一组值。简单来说,当我们接近目标 Q 值时,Q 值会变得更远。它就像是在你快要完成比赛时不断把终点线往后移!
为了解决这个问题,Q 网络每经过 10,000 次迭代就会创建一个自身的副本,称为目标网络,它将表示目标 Q 值。我们可以通过在 TensorFlow 中创建一个目标网络变量来实现这一点,首先用类初始化,然后在 TensorFlow 会话中运行:
## def contained under __init__(self, actions): ##
## Initialize the base network
self.inputVal, self.QValue = self.deepQarchitecture()
## Initialize the target network
self.inputValT, self.QValueT = self.deepQarchitecture()
至于 10,000 次迭代,我们在开始构建 DQN 类时已经定义了self.update_time = 10000。
经验回放缓冲区
虽然我们在第八章中简要提到过,强化学习,但现在让我们深入探讨一下经验回放缓冲区的技术细节。经验回放是一种受生物启发的工具,它在每个时间步骤的过程中存储智能体的经验。具体来说,它在每个时间步骤存储[状态、动作、奖励、下一个状态]的配对:

与其在状态-动作对发生时立即运行 Q 学习,经验回放将这些对在它们被发现时存储起来。经验回放缓冲区有两个作用:
-
通过存储并随机抽样来记住过去的经验
-
减少经验被...的机会
选择动作
到目前为止,我们已经告诉网络遵循我们为其初始化的随机权重,但并没有给它指导,告诉它如何决定采取哪些动作来更新这些权重。在第八章中使用的策略方法,比如我们在强化学习中的应用,以及之前的井字棋案例,Q-learning 方法的目标是通过逼近 Q 函数的值,而不是直接学习策略。那么,我们该怎么做呢?
深度 Q 网络使用一种叫做探索的策略来决定采取什么动作。如果我们不使用这种策略,网络可能只会局限于游戏的最基本层次,因为它不会知道哪些动作能够帮助它提升!
为了解决这个问题,我们将采用一种叫做 ∈-贪婪 的策略。这种策略通过基于两种方法选择学习的动作;首先,选择那些为我们的模型提供最高奖励(最大 Q 值)的方法;其次,以 ∈ 的概率随机选择方法。在这种策略中,我们使用一个基本的不等式:

我们将 epsilon 变量设置为 1,并从一个分布中抽取随机数。如果这个数字不满足不等式并且小于 epsilon,我们的代理将探索这个空间,这意味着它会寻求更多关于它当前所在位置的信息,从而开始选择状态/动作对,以便我们可以计算 Q 值。如果数字实际上大于 epsilon,我们将利用我们已经知道的信息来选择一个 Q 值进行计算。算法以较高的∈开始,并在训练过程中按固定量减少其值。我们称这个过程为退火。在原始的 DeepMind 论文中,研究人员使用了这种策略,在前百万帧内将 epsilon 从 1 退火到 0.1,然后在之后保持 0.1。让我们回顾一下我们在本节开始时初始化的参数:
self.starting_ep = 1.0
self.ending_ep = 0.1
你会注意到我们使用了这些确切的规格。在网络的测试阶段,epsilon 将会显著降低,因此更偏向于利用策略。
让我们在 Python 中实现这个策略,并仔细研究它的机制。我们将定义一个名为 getAction 的函数,它位于我们的 deepQ 类中:
def select(self):
## Select a Q Value from the Base Q Network
QValue = self.QValue.eval(feed_dict = {self.iputVal:[self.currentState]})[0]
## Initialize actions as zeros
action = np.zeros(self.action)
action_index = 0
## If this timestep is the first, start with a random action
if self.timeStep % 1 == 0:
##
if random.random() <= self.starting_ep:
a_index = random.randrange(self.action)
action[a_index] = 1
else:
action_index = np.argmax(QValue)
action[action_index] = 1
else:
action[0] = 1
我们还将调整我们的 epsilon 值:
## Anneal the value of epsilon
if self.starting_ep > self.ending_ep and self.timeStep > self.observe:
self.starting_ep -= (self.starting_ep - self.ending_ep) / self.explore
现在我们已经定义了大部分网络,接下来我们进入训练阶段。
训练方法
首先,让我们定义我们的训练方法。我们将这个函数命名为 trainingPipeline;它将接受一个动作输入以及一个 y 输入,y 代表目标 Q 值,我们将其在这里定义为占位符,并根据这些动作/状态对计算 Q 值:

我们将使用均方误差 (MSE) 损失函数并进行计算,利用预测的 Q 值减去实际的 Q 值。最后,你可能会注意到我们使用了一个你可能不太熟悉的优化器,RMSProp。它是一种自适应学习率优化器,类似于 Adam,由 Geoffrey Hinton 提出。我们将...
训练网络
我们将给我们的训练函数起一个简单的名字:train。首先,我们将从回放记忆中输入小批次的数据:
def train(self):
''' Training procedure for the Q Network'''
minibatch = random.sample(self.replayBuffer, 32)
stateBatch = [data[0] for data in minibatch]
actionBatch = [data[1] for data in minibatch]
rewardBatch = [data[2] for data in minibatch]
nextBatch = [data[3] for data in minibatch]
接下来,我们将计算每个批次的 Q 值:
batch = []
qBatch = self.QValueT.eval(feed_dict = {self.inputValT: nextBatch})
for i in range(0, 32):
terminal = minibatch[i][4]
if terminal:
batch.append(rewardBatch[i])
else:
batch.append(rewardBatch[i] + self.gamma * np.max(qBatch[i]))
现在,让我们将这些内容结合起来,使用我们的训练方法。我们将使用之前定义的 trainStep 变量并运行训练周期。我们将输入三个变量作为输入;目标 Q 值,一个动作和一个状态:
self.trainStep.run(feed_dict={
self.yInput : batch,
self.actionInput : actionBatch,
self.inputVal : stateBatch
})
我们将定义一个处理函数来为我们保存网络权重和状态。虽然我们在本章中没有明确介绍保存器的定义,但你可以在 GitHub 仓库中的完整代码中找到它:
## Save the network on specific iterations
if self.timeStep % 10000 == 0:
self.saver.save(self.session, './savedweights' + '-atari', global_step = self.timeStep)
最后,让我们定义一个循环来将经验回放添加到其中:
def er_replay(self, nextObservation, action, reward, terminal):
newState = np.append(nextObservation, self.currentState[:,:,1:], axis = 2)
self.replayMemory.append((self.currentState, action, reward, newState, terminal))
if len(self.replayBuffer) > 40000:
self.replayBuffer.popleft()
if self.timeStep > self.explore:
self.trainQNetwork()
self.currentState = newState
self.timeStep += 1
我们已经组装好了网络,接下来让我们开始运行它!
运行网络
现在,让我们进入我们一直期待的时刻!让我们导入gym、NumPy、我们的深度 Q 网络文件以及一些处理函数:
import cv2import sysfrom deepQ import deepQimport numpy as npimport gym
我们将定义我们的代理类为Atari,并使用该类初始化环境、网络和动作:
class Atari: def __init__(self): self.env = gym.make('SpaceInvaders-v0') self.env.reset() self.actions = self.env.action_space.n self.deepQ = deepQ(self.actions) self.action0 = 0
我们的深度 Q 网络无法直接处理 Atari 游戏,因此我们需要编写一些预处理代码来处理视频输入。我们将这个函数称为preprocess,它将接收一个单一的游戏观察:
def preprocess(self,observation): ...
总结
在本章中,我们学到了很多东西,从如何实现 MCTS 方法来玩棋盘游戏,到创建一个先进的网络来玩 Atari 游戏,甚至了解了著名的 AlphaGo 系统背后的技术。让我们回顾一下我们所学到的内容。
强化学习方法已成为创建游戏 AI 的主要工具。无论是为现实生活中的棋盘游戏创建系统,还是为视频游戏创建系统,我们在第八章《强化学习》中学到的关于策略、Q 学习等基本概念,构成了这些复杂 AI 系统的基础。当我们为棋盘游戏创建 AI 时,我们依赖于游戏树的基本构建块,并使用 MCTS 来模拟从该游戏树中产生的各种游戏结果。对于像 AlphaGo 和棋类 AI 这样的更高级系统,我们利用神经网络来帮助指导 MCTS,并使其模拟更为有效。
在视频游戏 AI 方面,我们可以利用策略梯度方法或 Q 学习方法。在本章中,我们学习了如何利用后者的一种变体——深度 Q 学习来玩 Atari 游戏《太空入侵者》。深度 Q 学习通过使用目标网络和经验回放缓冲区等技术,改进了基本的 Q 学习,从而提升了性能。
在我们接下来的机器人深度学习章节中,我们将更深入地探讨强化学习方法如何创造智能系统。
第十一章:金融深度学习
深度学习是金融服务行业中最令人兴奋的新兴技术之一,正确应用时可以提高投资回报。虽然计算机视觉和自然语言处理 (NLP)等任务已经有了充分的研究,但人工智能 (AI) 技术在金融服务中的应用仍在不断发展。需要注意的是,一些最先进、最具盈利性的深度学习技术在人工智能领域并未公开,且未来也不会公开。金融服务领域的高盈利性决定了必须保护先进的算法和措施,因此在本章中,我们将重点讨论原则。
人工智能在金融服务行业中的应用具有复杂性;...
所需工具
一如既往,我们将使用 Python 3 进行分析。Python 是量化交易应用的优秀选择,尤其适用于频率高于几秒钟的场景。对于高频应用,建议使用 Java 或 C++ 等中级语言。
在本章中,我们将在标准深度学习技术栈之上使用专门的金融 Python 库:
Zipline—一个基于 Python 的算法交易库。目前它被用作量化交易网站 Quantopian (www.quantopian.com) 的回测工具。
人工智能在金融中的应用简介
尽管金融服务是计算密集型领域之一,但它充满了启发式方法。高级 AI 技术的应用充其量也只是脆弱的;许多公司根本没有采用能够轻松应用 AI 的策略。与硅谷争夺顶尖量化人才的竞争也让这个问题变得更加严重。你可能会自问,“我难道不需要有金融背景才能处理这些数据吗?”值得注意的是,全球两大顶级对冲基金是由参与过著名 Netflix 机器学习挑战赛的团队创办的。虽然研究算法交易技术有极大的好处,但你可以凭借对人工神经网络(ANNs)的了解来开始。
交易中的深度学习
交易是金融市场中买卖物品的行为;在金融术语中,我们称这些物品为衍生品。交易可以是短期的(当天内)、中期的(几天)或长期的(几周或更长)。根据全球最大银行之一摩根大通的专家,AI 应用在短期和中期交易策略上比人类更有效。在本节中,我们将探讨一些开发智能交易算法的基本策略,适用于短期和中期交易。但首先,让我们介绍一些基本概念。
交易策略试图利用市场低效来获利。算法训练中的一个核心策略被称为阿尔法,它是一种衡量业绩的指标。阿尔法通过将股票与一个指数进行对比,来衡量特定投资的主动回报。个别投资的表现与其匹配的指数之间的差异就是投资的阿尔法。在构建交易策略的网络时,我们希望我们的网络能够识别出那些为我们产生最大阿尔法的市场低效。
我们通常可以将传统的股票分析分为两类:
-
基本面分析则着眼于可能影响金融衍生品的基础因素,例如公司的一般财务健康状况。
-
技术分析从更数学的角度审视金融衍生品的实际表现,试图通过资产价格变动中的模式来预测价格的走势。
在这两种情况下,分析通常依赖于人工推理,而深度学习则进入了量化分析的领域,尤其是在所谓的算法交易中。广义上讲,算法交易就是其字面意义:由编码算法而非物理人类进行的交易。算法交易策略通过称为回测的过程进行验证,该过程将算法运行在历史数据上,以确定其是否能够在市场中表现良好。
算法交易应用于多个不同的领域:
-
买方-公司:公司利用算法交易来管理其中长期投资组合。
-
卖方-公司:公司利用高频算法交易来利用市场机会,并且推动市场本身的变化。
-
系统化交易者:这些个人和公司尝试将长期投资与短期投资高度相关的金融衍生品匹配。
这三种市场主体的共同点是,算法交易提供了一种比人类直觉更稳定、系统化的主动投资方式。
另一种策略依赖于技术指标,这些指标是基于数据历史分析的数学计算。大多数交易算法用于所谓的高频交易(HFT),它试图通过在各市场中进行大量极其快速的交易来利用市场低效。除非你拥有一些极为快速的计算机硬件,否则个人很难在这个领域竞争。相反,我们将用 TensorFlow 构建一些基本的算法用于非高频算法交易。
构建交易平台
在深入任何特定策略之前,让我们开始构建交易平台的基础。在这一部分,我们将构建处理数据输入和交易的代码,之后再深入研究两个具体策略。
基本交易功能
让我们从平台可以进行的基本操作开始;我们需要它能够买入、卖出或持有股票:
- 首先,让我们从一些导入开始:
import math
from time import time
from enum import Enum
- 为了将来方便起见,我们将把这些函数包装在一个名为
TradingPosition的类中:
class TradingPosition(object):
''' Class that manages the trading position of our platform'''
def __init__(self, code, buy_price, amount, next_price):
self.code = code ## Status code for what action our algorithm is taking
self.amount = amount ## The amount of the trade
self.buy_price = buy_price ## The purchase price of a trade
self.current_price = buy_price ## Buy price of the trade
self.current_value = self.current_price * self.amount
self.pro_value = next_price * self.amount
-
让我们拆解输入变量。我们初始化的第一个变量是
code,稍后我们将使用它作为执行买入、卖出或持有操作的状态码。接着,我们创建表示证券价格、证券数量(即股票数量)和证券当前价值的变量。 -
现在我们已经有了变量,我们可以开始编写交易操作的代码了。我们需要创建一个
status函数,用来追踪市场中价格的变化。为了简化,我们将这个函数命名为TradeStatus:
def TradeStatus(self, current_price, next_price, amount):
''' Manages the status of a trade that is in action '''
self.current_price = current_price ## updates the current price variable that is maintained within the class
self.current_value = self.current_price * amount
pro_value = next_price * amount
- 接下来,让我们创建一个
买入股票的函数:
def BuyStock(self, buy_price, amount, next_price):
''' Function to buy a stock '''
self.buy_price = ((self.amount * self.buy_price) + (amount * buy_price)) / (self.amount + amount)
self.amount += amount
self.TradeStatus(buy_price, next_price)
- 在这里,我们的函数接受一个
买入价格、数量以及序列中的下一个价格。我们计算买入价格,更新我们的交易量,并返回一个交易状态。接下来,让我们继续到卖出股票:
def SellStock(self, sell_price, amount, next_price):
''' Function to sell a stock '''
self.current_price = sell_price
self.amount -= amount
self.TradeStatus(sell_price, next_price)
- 在买入函数方面,我们输入
卖出价格和交易量,更新类的内部变量,并返回一个状态。最后,我们将创建一个简单的函数来持有股票,它大致上会告诉我们当前股票的价格状态:
def HoldStock(self, current_price, next_price):
''' Function to hold a stock '''
self.TradeStatus(current_price, next_price)
- 现在,让我们继续创建一个表示人工交易者的类。
创建人工交易者
尽管使用算法来指导交易决策是算法交易的定义,但这不一定是自动化交易。为了实现自动化交易,我们需要创建一个人工交易代理,它将代表我们执行交易策略:
- 我们将这个类命名为
Trader,并初始化执行交易算法所需的所有变量:
class Trader(object): ''' An Artificial Trading Agent ''' def __init__(self, market, cash=100000000.0): ## Initialize all the variables we need for our trader self.cash = cash ## Our Cash Variable self.market = market ## self.codes = market.codes self.reward = 0 self.positions = [] self.action_times = 0 self.initial_cash = cash self.max_cash = cash * 3 self.total_rewards ...
管理市场数据
与任何机器学习算法一样,选择市场预测算法的特征至关重要,这可能决定算法策略的成功与否。为了将价格曲线数据简化到最基本的部分,我们可以使用降维算法,比如 PCA,甚至可以嵌入股票信息,尝试捕捉最重要的潜在特征。正如我们所学,深度学习可以帮助我们克服这些选择问题,因为神经网络在训练过程中隐式地进行特征选择:
- 我们将创建一个新的类
MarketHandler,并初始化所有处理不同交易策略所需的参数和数据:
class MarketHandler(object):
''' Class for handling our platform's interaction with market data'''
Running = 0
Done = -1
def __init__(self, codes, start_date="2008-01-01", end_date="2018-05-31", **options):
self.codes = codes
self.index_codes = []
self.state_codes = []
self.dates = []
self.t_dates = []
self.e_dates = []
self.origin_frames = dict()
self.scaled_frames = dict()
self.data_x = None
self.data_y = None
self.seq_data_x = None
self.seq_data_y = None
self.next_date = None
self.iter_dates = None
self.current_date = None
## Initialize the stock data that will be fed in
self._init_data(start_date, end_date)
self.state_codes = self.codes + self.index_codes
self.scaler = [scaler() for _ in self.state_codes]
self.trader = Trader(self, cash=self.init_cash)
self.doc_class = Stock if self.m_type == 'stock' else Future
- 我们还需要初始化大量的数据处理过程,以便正确操作数据进行分析:
def _init_data_frames(self, start_date, end_date):
self._validate_codes()
columns, dates_set = ['open', 'high', 'low', 'close', 'volume'], set()
## Load the actual data
for index, code in enumerate(self.state_codes):
instrument_docs = self.doc_class.get_k_data(code, start_date, end_date)
instrument_dicts = [instrument.to_dic() for instrument in instrument_docs]
dates = [instrument[1] for instrument in instrument_dicts]
instruments = [instrument[2:] for instrument in instrument_dicts]
dates_set = dates_set.union(dates)
scaler = self.scaler[index]
scaler.fit(instruments)
instruments_scaled = scaler.transform(instruments)
origin_frame = pd.DataFrame(data=instruments, index=dates, columns=columns)
scaled_frame = pd.DataFrame(data=instruments_scaled, index=dates, columns=columns)
self.origin_frames[code] = origin_frame
self.scaled_frames[code] = scaled_frame
self.dates = sorted(list(dates_set))
for code in self.state_codes:
origin_frame = self.origin_frames[code]
scaled_frame = self.scaled_frames[code]
self.origin_frames[code] = origin_frame.reindex(self.dates, method='bfill')
self.scaled_frames[code] = scaled_frame.reindex(self.dates, method='bfill')
- 现在,我们初始化
env_data()方法并调用self类:
def _init_env_data(self):
if not self.use_sequence:
self._init_series_data()
else:
self._init_sequence_data()
- 最后,让我们初始化刚才创建的数据处理函数:
self._init_data_frames(start_date, end_date)
接下来,让我们开始构建我们平台的模型。
使用 LSTM 进行价格预测
让我们通过一个有监督学习的例子来开始,这个例子使用 LSTM 来预测给定股票的价格走势,基于其过去的表现。正如我们在之前的章节中所学,LSTM 和循环神经网络(RNN)在处理和预测序列数据时表现优越。这个模型将使用我们之前创建的交易平台结构:
- 让我们从导入库开始:
import tensorflow as tffrom sklearn.preprocessing import MinMaxScalerimport loggingimport os
- 让我们创建一个包含所有运行 RNN 所需代码的类,命名为
TradingRNN。我们还会初始化必要的变量:
class TradingRNN(): ''' An RNN Model for ...
回测您的算法
回测是指在历史数据上测试您的交易算法,以模拟其表现。虽然不能保证算法在实际市场中表现良好,但它能为我们提供一个良好的参考,帮助我们预测其表现。
在 Python 中,我们可以使用一个叫做Zipline的库来回测我们的算法。Zipline 是由在线交易算法平台 Quantopian 创建的回测平台,后来在 GitHub 上开源,供公众使用。它提供了十年的历史股票数据,并提供一个现实的交易环境,您可以在其中测试算法,包括交易成本、订单延迟和滑点。滑点是指交易发生时预期价格与实际执行价格之间的差价。要在 Python 中使用 Zipline,我们只需在命令行中运行pip install zipline。
每次使用 Zipline 时,我们都必须定义两个函数:
-
initialize(context):在 Zipline 开始运行您的算法之前会调用此函数。context 变量包含了您算法中所需的所有全局变量。Initialize 函数非常类似于我们在 TensorFlow 中初始化变量的方式,在运行会话之前进行初始化。 -
handle_data(context, data):这个函数做的正是它所说的:它将开盘、最高、最低和收盘的股市数据传递给您的算法,以及所需的上下文变量。
事件驱动交易平台
事件驱动投资是一种投资策略,重点关注可能影响股市走势的社会经济因素,尤其是在财报电话会议或并购等金融事件发生前。这种策略通常被大型基金采用,因为它们常常能获取一些并非完全公开的信息,而且需要在正确分析这些事件方面具备大量的专业知识。
为此,我们将从原始文本中提取事件并将其转化为元组,描述该事件。例如,如果我们说Google收购Facebook,则元组将是(Actor = Google, Action = buys, Object = Facebook, Time = January 1 2018)。这些元组可以帮助我们将事件简化为...
收集股票价格数据
大多数实时市场数据通过付费服务提供;比如 Bloomberg 终端或券商网站。目前,唯一一个不收费的实时金融市场数据 API 是 Alpha Vantage,它由商业和学术界联合维护。你可以通过在命令行中运行pip install alpha_vantage来安装它。你可以在 Alpha Vantage 网站上注册一个免费的 API 密钥。
一旦你获得了 API 密钥,就可以使用以下方式轻松查询api:
ts = TimeSeries(key='YOUR_API_KEY', output_format='pandas')
data, meta_data = ts.get_intraday(symbol='TICKER',interval='1min', outputsize='full')
生成词嵌入
对于我们的嵌入方案,我们将使用上一章中的 GloVe 实现:
from collections import Counter, defaultdictimport osfrom random import shuffleimport tensorflow as tfimport nltk
class GloVeModel(): def __init__(self, embedding_size, window_size, max_vocab_size=100000, min_occurrences=1, scaling_factor=3/4, cooccurrence_cap=100, batch_size=512, learning_rate=0.05): self.embedding_size = embedding_size#First we define the hyper-parameters of our model if isinstance(context_size, tuple): self.left_context, self.right_context = context_size elif isinstance(context_size, int): self.left_context = self.right_context = context_size self.max_vocab_size = max_vocab_size self.min_occurrences ...
用于事件嵌入的神经张量网络
神经张量网络(NTN)是一种新的神经网络形式,它的工作方式类似于标准的前馈网络,但它包含一个被称为张量层的层,而不是标准的隐藏层。该网络最初是作为通过连接未连接的实体来完善知识库的手段开发的。例如,如果我们有实体 Google 和 YouTube,网络将帮助将这两个实体连接起来,使得 Google -> 拥有 -> YouTube。它通过将不同的关系对传递通过网络而不是通过单一的向量来工作,且通过将它们作为张量传递来实现这一点。该张量的每个切片代表两实体之间关系的不同变化。
在事件驱动交易领域,我们之所以对 NTN 感兴趣,是因为它能够将实体彼此关联。对我们来说,这意味着学习我们在本节第一部分创建的实体事件对:
- 让我们从构建核心网络开始,并将其包含在一个名为
NTN的函数中:
def NTN(batch_placeholders, corrupt_placeholder, init_word_embeds, entity_to_wordvec,\
num_entities, num_relations, slice_size, batch_size, is_eval, label_placeholders):
d = 100
k = slice_size
ten_k = tf.constant([k])
num_words = len(init_word_embeds)
E = tf.Variable(init_word_embeds)
W = [tf.Variable(tf.truncated_normal([d,d,k])) for r in range(num_relations)]
V = [tf.Variable(tf.zeros([k, 2*d])) for r in range(num_relations)]
b = [tf.Variable(tf.zeros([k, 1])) for r in range(num_relations)]
U = [tf.Variable(tf.ones([1, k])) for r in range(num_relations)]
ent2word = [tf.constant(entity_i)-1 for entity_i in entity_to_wordvec]
entEmbed = tf.pack([tf.reduce_mean(tf.gather(E, entword), 0) for entword in ent2word])
- 仍然在
NTN函数内,我们将遍历我们的嵌入并开始从中生成关系嵌入:
predictions = list()
for r in range(num_relations):
e1, e2, e3 = tf.split(1, 3, tf.cast(batch_placeholders[r], tf.int32)) #TODO: should the split dimension be 0 or 1?
e1v = tf.transpose(tf.squeeze(tf.gather(entEmbed, e1, name='e1v'+str(r)),[1]))
e2v = tf.transpose(tf.squeeze(tf.gather(entEmbed, e2, name='e2v'+str(r)),[1]))
e3v = tf.transpose(tf.squeeze(tf.gather(entEmbed, e3, name='e3v'+str(r)),[1]))
e1v_pos = e1v
e2v_pos = e2v
e1v_neg = e1v
e2v_neg = e3v
num_rel_r = tf.expand_dims(tf.shape(e1v_pos)[1], 0)
preactivation_pos = list()
preactivation_neg = list()
- 最后,我们将通过非线性操作处理关系,并输出它们:
for slice in range(k):
preactivation_pos.append(tf.reduce_sum(e1v_pos*tf.matmul(W[r][:,:,slice], e2v_pos), 0))
preactivation_neg.append(tf.reduce_sum(e1v_neg*tf.matmul( W[r][:,:,slice], e2v_neg), 0))
preactivation_pos = tf.pack(preactivation_pos)
preactivation_neg = tf.pack(preactivation_neg)
temp2_pos = tf.matmul(V[r], tf.concat(0, [e1v_pos, e2v_pos]))
temp2_neg = tf.matmul(V[r], tf.concat(0, [e1v_neg, e2v_neg]))
preactivation_pos = preactivation_pos+temp2_pos+b[r]
preactivation_neg = preactivation_neg+temp2_neg+b[r]
activation_pos = tf.tanh(preactivation_pos)
activation_neg = tf.tanh(preactivation_neg)
score_pos = tf.reshape(tf.matmul(U[r], activation_pos), num_rel_r)
score_neg = tf.reshape(tf.matmul(U[r], activation_neg), num_rel_r)
if not is_eval:
predictions.append(tf.pack([score_pos, score_neg]))
else:
predictions.append(tf.pack([score_pos, tf.reshape(label_placeholders[r], num_rel_r)]))
- 最后,让我们返回所有包含
predictions的嵌入关系:
predictions = tf.concat(1, predictions)
return predictions
- 接下来,让我们定义网络的
loss函数。我们将从 TensorFlow 的原生操作手动构建出我们的loss函数:
def loss(predictions, regularization):
temp1 = tf.maximum(tf.sub(predictions[1, :], predictions[0, :]) + 1, 0)
temp1 = tf.reduce_sum(temp1)
temp2 = tf.sqrt(sum([tf.reduce_sum(tf.square(var)) for var in tf.trainable_variables()]))
temp = temp1 + (regularization * temp2)
return temp
- 我们将定义一个训练算法,该算法仅返回最小化的
loss函数,利用 TensorFlow 的内置函数:
def training(loss, learningRate):
return tf.train.AdagradOptimizer(learningRate).minimize(loss)
- 最后,我们将创建一个简单的函数来评估网络的性能:
def eval(predictions):
print("predictions "+str(predictions.get_shape()))
inference, labels = tf.split(0, 2, predictions)
return inference, labels
接下来,我们将通过卷积神经网络(CNN)来预测价格变动,完成我们的模型。
使用卷积神经网络预测事件
现在我们有了嵌入结构,是时候用 CNN 进行预测了。当你通常想到 CNN 以及我们在其上完成的工作时,你可能会想到计算机视觉任务,比如识别图像中的物体。尽管 CNN 是为此设计的,但它们在文本特征检测方面也表现出色。
当我们在 NLP 中使用 CNN 时,我们将标准的像素输入替换为词嵌入。在典型的计算机视觉任务中,您使用 CNN 的过滤器对图像的小块进行处理,而在 NLP 任务中,我们对嵌入矩阵的行使用相同的滑动窗口。因此,滑动窗口的宽度就变成了……
资产管理中的深度学习
在金融服务中,投资组合是个人或组织持有的一系列投资。为了实现最佳回报(如同任何人都希望的那样!),通过决定应该将多少资金投资于某些金融资产来优化投资组合。在投资组合优化理论中,目标是拥有一个能够最小化风险并最大化回报的资产配置。因此,我们需要创建一个算法,预测每个资产的预期风险和回报,以便找到最佳优化方案。传统上,这项工作由财务顾问完成,然而,人工智能已经被证明在许多传统顾问构建的投资组合中表现得更好。
最近,出现了几种尝试开发用于资产配置的深度学习模型。考虑到许多这些技术并未公开发布,我们将看看一些我们作为 AI 科学家可能会使用的基本方法,以完成这项任务。
我们的目标是对一个股票指数进行建模,并看看我们是否能以至少 1%的收益超越该指数。我们将有效地构建一个自编码器来编码潜在的市场信息,然后使用解码器来构建一个最佳投资组合。由于我们处理的是时间序列信息,我们将为编码器和解码器都使用 RNN。一旦我们在数据上训练好自编码器,我们将用它作为简单前馈网络的输入,来预测我们最佳的投资组合配置。
让我们来看看如何在 TensorFlow 中实现这一过程。
- 如常,我们首先导入所需的模块:
import numpy as np
import tensorflow as tf from tensorflow.contrib.rnn import LSTMCell
- 让我们加载我们的股票数据:
ibb = defaultdict(defaultdict)
ibb_full = pd.read_csv('data/ibb.csv', index_col=0).astype('float32')
ibb_lp = ibb_full.iloc[:,0]
ibb['calibrate']['lp'] = ibb_lp[0:104]
ibb['validate']['lp'] = ibb_lp[104:]
ibb_net = ibb_full.iloc[:,1]
ibb['calibrate']['net'] = ibb_net[0:104]
ibb['validate']['net'] = ibb_net[104:]
ibb_percentage = ibb_full.iloc[:,2]
ibb['calibrate']['percentage'] = ibb_percentage[0:104]
ibb['validate']['percentage'] = ibb_percentage[104:]
- 让我们通过创建
AutoEncoder开始我们的建模过程,并将其包含在AutoEncoder类中。我们将首先初始化主要的网络变量,就像之前做的那样:
class AutoEncoder():
''' AutoEncoder for Data Drive Portfolio Allocation '''
def __init__(self, config):
"""First, let's set up our hyperparameters"""
num_layers = tf.placeholder('int')
hidden_size = tf.placeholder('int')
max_grad_norm = tf.placeholder('int')
batch_size = tf.placeholder('int')
crd = tf.placeholder('int')
num_l = tf.placeholder('int')
learning_rate = tf.placeholder('float')
self.batch_size = batch_size
## sl will represent the length of an input sequence, which we would like to eb dynamic based on the data
sl = tf.placeholder("int")
self.sl = sl
- 接下来,我们将为输入数据创建
placeholders,x:
self.x = tf.placeholder("float", shape=[batch_size, sl], name='Input_data')
self.x_exp = tf.expand_dims(self.x, 1)
self.keep_prob = tf.placeholder("float")
- 接下来,让我们创建编码器。我们将创建一系列 LSTM 单元来编码序列数据,但我们将以一种我们尚未见过的方式来实现:使用 TensorFlow 中的一个便捷函数
MultiRNNCell。这个函数充当了一个更大的 RNN 占位符,我们可以在其中迭代,以便根据我们设置的num_layers参数动态创建层的数量:
## Create the Encoder as a TensorFlow Scope
with tf.variable_scope("Encoder") as scope:
## For the encoder, we will use an LSTM cell with Dropout
EncoderCell = tf.contrib.rnn.MultiRNNCell([LSTMCell(hidden_size) for _ in range(num_layers)])
EncoderCell = tf.contrib.rnn.DropoutWrapper(EncoderCell, output_keep_prob=self.keep_prob)
## Set the initial hidden state of the encoder
EncInitialState = EncoderCell.zero_state(batch_size, tf.float32)
## Weights Factor
W_mu = tf.get_variable('W_mu', [hidden_size, num_l])
## Outputs of the Encoder Layer
outputs_enc, _ = tf.contrib.rnn.static_rnn(cell_enc,
inputs=tf.unstack(self.x_exp, axis=2),
initial_state=initial_state_enc)
cell_output = outputs_enc[-1]
## Bias Factor
b_mu = tf.get_variable('b_mu', [num_l])
## Mean of the latent space variables
self.z_mu = tf.nn.xw_plus_b(cell_output, W_mu, b_mu, name='z_mu')
lat_mean, lat_var = tf.nn.moments(self.z_mu, axes=[1])
self.loss_lat_batch = tf.reduce_mean(tf.square(lat_mean) + lat_var - tf.log(lat_var) - 1)
- 接下来,我们将创建一个层来处理由编码器生成的隐藏状态:
## Layer to Generate the Initial Hidden State from the Encoder
with tf.name_scope("Initial_State") as scope:
## Weights Parameter State
W_state = tf.get_variable('W_state', [num_l, hidden_size])
## Bias Paramter State
b_state = tf.get_variable('b_state', [hidden_size])
## Hidden State
z_state = tf.nn.xw_plus_b(self.z_mu, W_state, b_state, name='hidden_state')
- 然后我们可以以与编码器层相同的方式创建
decoder层:
## Decoder Layer
with tf.variable_scope("Decoder") as scope:
DecoderCell = tf.contrib.rnn.MultiRNNCell([LSTMCell(hidden_size) for _ in range(num_layers)])
## Set an initial state for the decoder layer
DecState = tuple([(z_state, z_state)] * num_layers)
dec_inputs = [tf.zeros([batch_size, 1])] * sl
## Run the decoder layer
outputs_dec, _ = tf.contrib.rnn.static_rnn(cell_dec, inputs=dec_inputs, initial_state=DecState)
- 最后,我们将为网络创建输出层:
## Output Layer
with tf.name_scope("Output") as scope:
params_o = 2 * crd
W_o = tf.get_variable('W_o', [hidden_size, params_o])
b_o = tf.get_variable('b_o', [params_o])
outputs = tf.concat(outputs_dec, axis=0)
h_out = tf.nn.xw_plus_b(outputs, W_o, b_o)
h_mu, h_sigma_log = tf.unstack(tf.reshape(h_out, [sl, batch_size, params_o]), axis=2)
h_sigma = tf.exp(h_sigma_log)
dist = tf.contrib.distributions.Normal(h_mu, h_sigma)
px = dist.log_prob(tf.transpose(self.x))
loss_seq = -px
self.loss_seq = tf.reduce_mean(loss_seq)
- 现在我们已经构建了实际模型,我们可以继续设置训练过程。我们将使用指数衰减来调整学习率,这有助于通过缓慢降低学习率的值来稳定训练过程:
## Train the AutoEncoder
with tf.name_scope("Training") as scope:
## Global Step Function for Training
global_step = tf.Variable(0, trainable=False)
## Exponential Decay for the larning rate
lr = tf.train.exponential_decay(learning_rate, global_step, 1000, 0.1, staircase=False)
## Loss Function for the Network
self.loss = self.loss_seq + self.loss_lat_batch
## Utilize gradient clipping to prevent exploding gradients
grads = tf.gradients(self.loss, tvars)
grads, _ = tf.clip_by_global_norm(grads, max_grad_norm)
self.numel = tf.constant([[0]])
## Lastly, apply the optimization process
optimizer = tf.train.AdamOptimizer(lr)
gradients = zip(grads, tvars)
self.train_step = optimizer.apply_gradients(gradients, global_step=global_step)
self.numel = tf.constant([[0]])
- 现在,我们可以运行训练过程:
if True:
sess.run(model.init_op)
writer = tf.summary.FileWriter(LOG_DIR, sess.graph) # writer for Tensorboard
step = 0 # Step is a counter for filling the numpy array perf_collect
for i in range(max_iterations):
batch_ind = np.random.choice(N, batch_size, replace=False)
result = sess.run([model.loss, model.loss_seq, model.loss_lat_batch, model.train_step],
feed_dict={model.x: X_train[batch_ind], model.keep_prob: dropout})
if i % plot_every == 0:
perf_collect[0, step] = loss_train = result[0]
loss_train_seq, lost_train_lat = result[1], result[2]
batch_ind_val = np.random.choice(Nval, batch_size, replace=False)
result = sess.run([model.loss, model.loss_seq, model.loss_lat_batch, model.merged],
feed_dict={model.x: X_val[batch_ind_val], model.keep_prob: 1.0})
perf_collect[1, step] = loss_val = result[0]
loss_val_seq, lost_val_lat = result[1], result[2]
summary_str = result[3]
writer.add_summary(summary_str, i)
writer.flush()
print("At %6s / %6s train (%5.3f, %5.3f, %5.3f), val (%5.3f, %5.3f,%5.3f) in order (total, seq, lat)" % (
i, max_iterations, loss_train, loss_train_seq, lost_train_lat, loss_val, loss_val_seq, lost_val_lat))
step += 1
if False:
start = 0
label = [] # The label to save to visualize the latent space
z_run = []
while start + batch_size < Nval:
run_ind = range(start, start + batch_size)
z_mu_fetch = sess.run(model.z_mu, feed_dict={model.x: X_val[run_ind], model.keep_prob: 1.0})
z_run.append(z_mu_fetch)
start += batch_size
z_run = np.concatenate(z_run, axis=0)
label = y_val[:start]
plot_z_run(z_run, label)
saver = tf.train.Saver()
saver.save(sess, os.path.join(LOG_DIR, "model.ckpt"), step)
config = projector.ProjectorConfig()
embedding = config.embeddings.add()
embedding.tensor_name = model.z_mu.name
在对股票指数进行自编码后,我们将查看每只不同股票与其对应的自编码器版本之间的差异。然后我们将根据股票的自编码效果对其进行排名。随着算法学习到每只股票最重要的信息,股票与其经过自编码器处理后的版本的接近程度,提供了该股票在整个潜在投资组合中的衡量标准。
由于让多个股票共同贡献潜在信息没有益处,我们将限制所选股票为这些接近自编码版本的前十只股票:
communal_information = []
for i in range(0,83):
diff = np.linalg.norm((data.iloc[:,i] - reconstruct[:,i])) # 2 norm difference
communal_information.append(float(diff))
print("stock #, 2-norm, stock name")
ranking = np.array(communal_information).argsort()
for stock_index in ranking:
print(stock_index, communal_information[stock_index], stock['calibrate']['net'].iloc[:,stock_index].name) # print stock name from lowest different to highest
我们可以查看自编码器如何工作的方式如下:
which_stock = 1
stock_autoencoder = copy.deepcopy(reconstruct[:, which_stock])
stock_autoencoder[0] = 0
stock_autoencoder = stock_autoencoder.cumsum()
stock_autoencoder += (stock['calibrate']['lp'].iloc[0, which_stock])
pd.Series(stock['calibrate']['lp'].iloc[:, which_stock].as_matrix(), index=pd.date_range(start='01/06/2012', periods=104, freq='W')).plot(label='stock original', legend=True)
pd.Series(stock_autoencoder, index=pd.date_range(start='01/06/2012', periods = 104,freq='W')).plot(label='stock autoencoded', legend=True)
虽然我们仍然需要在可用的股票中做出选择,但我们的选择决策现在是基于这些股票的样本外表现,从而使得我们的市场自编码器成为一种新颖的数据驱动方法。
总结
在本章中,我们学习了如何将深度学习知识应用于金融服务行业。我们学习了交易系统的原理,然后在 TensorFlow 中设计了自己的交易系统。接着,我们探讨了如何创建另一种类型的交易系统,这种系统利用公司周围的事件来预测其股价。最后,我们探索了一种新颖的技术,用于嵌入股市并利用这些嵌入来预测价格变动。
由于金融市场具有特殊的性质,它们的建模可能会很棘手,但我们在本章中所涵盖的技术将为你提供构建进一步模型的基础。记得在将算法部署到实时环境中之前,始终进行回测!...
第十二章:机器人学中的深度学习
到目前为止,我们已经学习了如何构建一个智能聊天机器人,它可以像人类一样玩棋盘游戏,并从股市数据中提取洞察。在本章中,我们将进入许多人眼中认为 人工智能 (AI) 所代表的领域:自我学习的机器人。在 第八章,强化学习 章节中,您学习了强化学习的所有内容,以及如何利用这些方法处理基本任务。在本章中,我们将学习如何将这些方法应用于机器人的运动控制。
在本章中,我们将使用 GPU 来帮助训练这些强大的算法。如果您的计算机没有 GPU 支持,建议您使用 AWS 或 Google Cloud 来提供更多的计算能力。
本章将涵盖以下主题:
-
设置您的环境
-
设置深度确定性策略梯度模型
-
演员-评论家网络
-
DDPG 及其实现
技术要求
在本章中,我们将使用 TensorFlow 和 OpenAI gym 环境,因此您需要在计算机上安装以下程序:
-
TensorFlow
-
OpenAI gym
介绍
传统机器人技术,被称为 机器人过程自动化,是自动化物理任务的过程,这些任务通常由人类完成。类似于 机器学习 这一术语涵盖了多种方法和方法论,包括深度学习方法;机器人学涵盖了各种各样的技术和方法。一般来说,我们可以将这些方法分为两类:传统方法 和 人工智能方法。
传统的机器人控制编程通常需要以下几个步骤:
-
测量:机器人从传感器接收数据,以确定为完成特定任务应该采取的行动。
-
推理:机器人相对于其环境的方位是基于传感器接收到的数据。
-
建模:在每个动作状态下建模机器人必须做什么以完成一个动作。
-
控制:编写低级控制代码,例如模型用于控制机器人运动的转向机制。
-
模型部署:检查模型在实际环境中如何工作,这些环境是为其创建的。
在传统的机器人开发中,这些方法是硬编码的。然而,通过深度学习,我们可以创建能够从头到尾学习动作的算法,从而消除该过程中的第2到第4步,并显著缩短开发成功机器人所需的时间。更重要的是,深度学习技术具有泛化能力;我们无需为特定任务的变化编程动作,而是可以教会算法学习对该任务的普适响应。近年来,这种人工智能方法在机器人领域取得了突破,允许开发出更为先进的机器人。
由于机器人市场中零部件和控制系统缺乏一致性,设计和创建物理机器人可能是一个艰巨的任务。2018 年 2 月,OpenAI 在其训练环境中加入了虚拟模拟机器人手臂,开启了无数新应用和开发的大门。这些虚拟环境使用物理环境模拟器,允许我们立即开始测试机器人控制算法,而无需采购昂贵且不一致的零部件。
设置你的环境
我们将利用 OpenAI 的 gym 环境,这是我们在第八章《强化学习》中学习的内容,来创建一个智能机器人手臂。OpenAI 基于 Fetch 机器人手臂创建了一个虚拟环境,成为第一个完全虚拟化的机器人算法测试空间:

你应该已经安装了这些环境,因为我们在第十一章《金融深度学习》中安装了 gym 环境。我们只需再添加两个包来让这个机器人环境运行起来:
brew install cmake openmpi
cmake和openmpi都是为提高计算效率而设计的...
MuJoCo 物理引擎
MuJoCo 是一个虚拟环境,模拟现实物理环境,用于测试智能算法。这是 Google DeepMind、OpenAI 等领先 AI 研究者用来教虚拟机器人执行任务、虚拟人类以及蜘蛛等任务的环境。特别是 Google,在 2017 年发布了一段虚拟人类自学跑步和跳跃的视频,引起了广泛关注,展示了强化学习技术的应用:

MuJoCo 是一个付费许可程序,但它提供 30 天的免费试用,我们将使用这个试用期来完成我们的任务。如果你是学生,可以免费获得 MuJoCo 的永久许可证,用于个人项目。以下是获取 MuJoCo 并设置 AI 应用环境的几个步骤:
-
从 MuJoCo 网站下载二进制文件
-
注册 MuJoCo 的免费试用
-
将你的许可证密钥放入
~|.mujoco|mjkey.txt文件夹中 -
安装 MuJoCo 的 Python 包
这可能看起来有些繁琐,但它确实让我们能够使用 AI 界最前沿的模拟器。如果你在这些步骤中遇到问题,我们将在本书的 GitHub 仓库中保持最新的帮助文档。现在,让我们一起来完成这些步骤。
下载 MuJoCo 二进制文件
首先,下载 MuJoCo 程序本身。访问 www.roboti.us/index.html 并下载与您的操作系统对应的mjpro150 文件。您也可以通过命令行使用以下代码来下载:
cd ~mkdir .mujococd .mujococurl https://www.roboti.us/download/mjpro150_osx.zip
注册 MuJoCo 的免费试用
按照以下步骤注册 MuJoCo 的免费试用:
- 下载完 MuJoCo 的二进制文件后,访问
www.roboti.us/license.html注册免费试用。您应该会看到以下注册 MuJoCo 的提示框:

- 看看计算机 ID 框右侧的蓝色链接吗?您需要下载与您的操作系统对应的链接。这将生成一个 MuJoCo 密钥,用于跟踪您的计算机和试用。如果您使用的是 macOS,可以通过以下代码下载并获取密钥:
curl https://www.roboti.us/getid/getid_osx
sudo ./getid_osx
- 如果您使用的是 Linux 机器,可以通过以下代码下载 MuJoCo 密钥:
wget https://www.roboti.us/getid/getid_linux
sudo ./getid_linux
一旦您获得了密钥,将其放入提示框中。您应该会收到一封包含许可证的电子邮件,这将使您能够使用 MuJoCo。
配置 MuJoCo 文件
接下来,您需要配置您的访问文件。您应该会通过电子邮件收到来自 MuJoCo 的许可证密钥。一旦收到,请将其放入以下文件夹,以便程序能够访问:
~/.mujoco/mjkey.txt
安装 MuJoCo Python 包
最后,我们需要安装 MuJoCo Python 包,这将允许 Python 与该程序进行交互。我们可以通过简单的 pip install 来完成:
pip install mujoco-py
您现在应该可以访问最强大的机器人测试虚拟环境。如果在此过程中遇到任何问题,请记得您可以随时通过本书的 GitHub 仓库访问最新的故障排除页面。
设置深度确定性策略梯度模型
在第八章《强化学习》中,我们学习了如何使用策略优化方法处理连续动作空间。策略优化方法通过优化环境中采取的行动直接学习,如下图所示:

记住,策略梯度方法是离策略的,这意味着它们在某一时刻的行为不一定反映它们所遵循的策略。这些策略梯度算法使用策略迭代,评估给定的策略并跟随策略梯度来学习最优策略。...
经验重放缓冲区
接下来,让我们像在第十一章《金融深度学习》中一样创建经验重放缓冲区,在那一章中,我们研究了如何为游戏创建深度学习模型。
事后经验重放
为了改善机器人运动,OpenAI 的研究人员于 2018 年发布了一篇关于一种名为 后视经验回放(HER)的技术的论文,旨在克服在 稀疏环境 中训练强化学习算法时出现的问题。回想一下 第八章,强化学习,为一个智能体选择合适的奖励可以决定该智能体的表现成败。以下这种轶事通常是由于不良奖励函数的结果:
有一个朋友正在训练一个模拟的机器人手臂,让它伸向桌子上方的一个点。结果发现,这个点是相对于桌子定义的,而桌子并没有固定在任何地方。学习到的策略是猛烈地撞击桌子,导致桌子...
演员-评论员网络
DDPG 模型依赖于演员-评论员框架,该框架用于学习策略,而不需要一遍又一遍地计算价值函数。
演员-评论员模型是强化学习中的基础框架。它们是 生成对抗网络(GANs)的基础和灵感来源,后者我们在 第七章 生成模型 中学到过。
正如你可能已经猜到的,这些演员-评论员模型由两个部分组成:
-
行动者:估计策略函数
-
评论员:估计价值函数
这个过程如字面所示;演员模型尝试模仿一个动作,而评论员模型则批评演员模型以帮助其改善表现。让我们以机器人的应用为例;机器人运动的目标是创建一个模拟人类能力的机器人。在这种情况下,演员会想观察人类的动作以进行模仿,评论员则帮助引导机器人演员变得更像人类。通过演员-评论员模型,我们只是将这一范式转换成数学公式。总体来说,演员-评论员过程如下所示:

每个这些网络都有自己的损失函数。
行动者
演员网络也被称为 目标策略网络。演员通过使用小批量的梯度下降进行训练。它的损失由以下函数定义:

这里,s 表示从回放缓冲区采样的状态。
评论员
评论员的输出是动作价值函数 Q (^(s,a)) 的估计值,因此你可能会看到评论员网络有时被称为 动作-价值函数近似器。它的工作是帮助行动者适当地近似动作价值函数。
评论员模型与我们在第十章《游戏学习深度学习》中看到的 Q 函数近似器非常相似。评论员生成一个时间差异(TD)误差,用于更新其梯度。TD 误差帮助算法减少由高度相关数据预测时出现的方差。DDPG 利用目标网络,就像在第十章《游戏学习深度学习》中一样,只不过目标是通过利用演员的输出计算出来的:

这个目标网络为 TD 误差计算生成目标,并充当正则化器。我们来分解一下:
-
代表了我们的 TD 误差。 -
r^i 代表从某个动作中获得的奖励。
-
共同代表了演员和评论员模型的目标!。回忆一下!(gamma)代表了折扣因子。记住,折扣因子可以是介于 0 和 1 之间的任何值,表示当前奖励和未来奖励之间的相对重要性。所以,我们的目标变成了演员/评论员模型输出与混合参数的乘积。 -
代表演员网络的目标;它表示目标
是状态s的函数,给定一个特定的策略
。 -
同样,我们也可以说,演员网络的输出依赖于评论员网络,表示评论员网络的权重为
。
评论员尝试最小化它自己的损失函数:

深度确定性策略梯度
深度确定性策略梯度(DDPG)有一个自然扩展,通过将用于近似演员和评论员的前馈神经网络替换为递归神经网络。这个扩展称为递归确定性策略梯度算法(RDPG),并在 N. Heess、J. J. Hunt、T. P. Lillicrap 和 D. Silver 的论文中进行了讨论。基于记忆的控制与递归神经网络,2015 年。
循环评论员和演员通过时间反向传播(BPTT)进行训练。对有兴趣的读者,论文可以从arxiv.org/abs/1512.04455下载。
DDPG 的实现
本节将展示如何使用 TensorFlow 实现演员-评论员架构。代码结构几乎与上一章展示的 DQN 实现相同。
ActorNetwork是一个简单的多层感知机(MLP),它将观察状态作为输入:
class ActorNetwork:
def __init__(self, input_state, output_dim, hidden_layers, activation=tf.nn.relu):
self.x = input_state
self.output_dim = output_dim
self.hidden_layers = hidden_layers
self.activation = activation
with tf.variable_scope('actor_network'):
self.output = self._build()
self.vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES,
tf.get_variable_scope().name)
def _build(self):
layer = self.x
init_b = tf.constant_initializer(0.01)
for i, num_unit in enumerate(self.hidden_layers):
layer = dense(layer, num_unit, init_b=init_b, name='hidden_layer_{}'.format(i))
output = dense(layer, self.output_dim, activation=self.activation, init_b=init_b, name='output')
return output
构造函数需要四个参数:input_state、output_dim、hidden_layers和activation。input_state是一个表示观察状态的张量。output_dim是动作空间的维度。hidden_layers指定了隐藏层的数量和每层的单元数。activation表示输出层的激活函数。
CriticNetwork也是一个多层感知机(MLP),对于经典控制任务来说已经足够:
class CriticNetwork:
def __init__(self, input_state, input_action, hidden_layers):
assert len(hidden_layers) >= 2
self.input_state = input_state
self.input_action = input_action
self.hidden_layers = hidden_layers
with tf.variable_scope('critic_network'):
self.output = self._build()
self.vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES,
tf.get_variable_scope().name)
def _build(self):
layer = self.input_state
init_b = tf.constant_initializer(0.01)
for i, num_unit in enumerate(self.hidden_layers):
if i != 1:
layer = dense(layer, num_unit, init_b=init_b, name='hidden_layer_{}'.format(i))
else:
layer = tf.concat([layer, self.input_action], axis=1, name='concat_action')
layer = dense(layer, num_unit, init_b=init_b, name='hidden_layer_{}'.format(i))
output = dense(layer, 1, activation=None, init_b=init_b, name='output')
return tf.reshape(output, shape=(-1,))
网络将状态和动作作为输入。它首先将状态映射到一个隐藏的特征表示,然后将该表示与动作拼接,接着通过若干隐藏层。输出层生成与输入相对应的 Q 值。
演员-评论家网络将演员网络和评论家网络结合在一起:
class ActorCriticNet:
def __init__(self, input_dim, action_dim,
critic_layers, actor_layers, actor_activation,
scope='ac_network'):
self.input_dim = input_dim
self.action_dim = action_dim
self.scope = scope
self.x = tf.placeholder(shape=(None, input_dim), dtype=tf.float32, name='x')
self.y = tf.placeholder(shape=(None,), dtype=tf.float32, name='y')
with tf.variable_scope(scope):
self.actor_network = ActorNetwork(self.x, action_dim,
hidden_layers=actor_layers,
activation=actor_activation)
self.critic_network = CriticNetwork(self.x,
self.actor_network.get_output_layer(),
hidden_layers=critic_layers)
self.vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES,
tf.get_variable_scope().name)
self._build()
def _build(self):
value = self.critic_network.get_output_layer()
actor_loss = -tf.reduce_mean(value)
self.actor_vars = self.actor_network.get_params()
self.actor_grad = tf.gradients(actor_loss, self.actor_vars)
tf.summary.scalar("actor_loss", actor_loss, collections=['actor'])
self.actor_summary = tf.summary.merge_all('actor')
critic_loss = 0.5 * tf.reduce_mean(tf.square((value - self.y)))
self.critic_vars = self.critic_network.get_params()
self.critic_grad = tf.gradients(critic_loss, self.critic_vars)
tf.summary.scalar("critic_loss", critic_loss, collections=['critic'])
self.critic_summary = tf.summary.merge_all('critic')
构造函数需要六个参数,分别是:input_dim和action_dim是状态空间和动作空间的维度。critic_layers和actor_layers指定评论家网络和演员网络的隐藏层。actor_activation表示演员网络输出层的激活函数。scope是用于scope TensorFlow 变量的作用域名称。
构造函数首先创建一个self.actor_network演员网络的实例,输入为self.x,其中self.x表示当前状态。然后,它使用以下输入创建一个评论家网络的实例:self.actor_network.get_output_layer()作为演员网络的输出,self.x作为当前状态。给定这两个网络,构造函数调用self._build()来构建我们之前讨论的演员和评论家的损失函数。演员损失是-tf.reduce_mean(value),其中value是评论家网络计算的 Q 值。评论家损失是0.5 * tf.reduce_mean(tf.square((value - self.y))),其中self.y是目标网络计算的预测目标值的张量。
ActorCriticNet类提供了在给定当前状态的情况下计算动作和 Q 值的函数,即get_action和get_value。它还提供了get_action_value,该函数在给定当前状态和代理采取的动作的情况下计算状态-动作值函数:
class ActorCriticNet:
def get_action(self, sess, state):
return self.actor_network.get_action(sess, state)
def get_value(self, sess, state):
return self.critic_network.get_value(sess, state)
def get_action_value(self, sess, state, action):
return self.critic_network.get_action_value(sess, state, action)
def get_actor_feed_dict(self, state):
return {self.x: state}
def get_critic_feed_dict(self, state, action, target):
return {self.x: state, self.y: target,
self.critic_network.input_action: action}
def get_clone_op(self, network, tau=0.9):
update_ops = []
new_vars = {v.name.replace(network.scope, ''): v for v in network.vars}
for v in self.vars:
u = (1 - tau) * v + tau * new_vars[v.name.replace(self.scope, '')]
update_ops.append(tf.assign(v, u))
return update_ops
因为 DPG 的架构几乎与 DQN 相同,所以在本章中没有展示回放记忆和优化器的实现。更多细节,请参考上一章或访问我们的 GitHub 仓库(github.com/PacktPublishing/Python-Reinforcement-Learning-Projects)。通过将这些模块结合在一起,我们可以实现用于确定性策略梯度算法的DPG类:
class DPG:
def __init__(self, config, task, directory, callback=None, summary_writer=None):
self.task = task
self.directory = directory
self.callback = callback
self.summary_writer = summary_writer
self.config = config
self.batch_size = config['batch_size']
self.n_episode = config['num_episode']
self.capacity = config['capacity']
self.history_len = config['history_len']
self.epsilon_decay = config['epsilon_decay']
self.epsilon_min = config['epsilon_min']
self.time_between_two_copies = config['time_between_two_copies']
self.update_interval = config['update_interval']
self.tau = config['tau']
self.action_dim = task.get_action_dim()
self.state_dim = task.get_state_dim() * self.history_len
self.critic_layers = [50, 50]
self.actor_layers = [50, 50]
self.actor_activation = task.get_activation_fn()
self._init_modules()
这里,config包含 DPG 的所有参数,例如训练的批次大小和学习率。task是某个经典控制任务的实例。在构造函数中,通过调用_init_modules函数初始化了回放内存、Q 网络、目标网络和优化器:
def _init_modules(self):
# Replay memory
self.replay_memory = ReplayMemory(history_len=self.history_len,
capacity=self.capacity)
# Actor critic network
self.ac_network = ActorCriticNet(input_dim=self.state_dim,
action_dim=self.action_dim,
critic_layers=self.critic_layers,
actor_layers=self.actor_layers,
actor_activation=self.actor_activation,
scope='ac_network')
# Target network
self.target_network = ActorCriticNet(input_dim=self.state_dim,
action_dim=self.action_dim,
critic_layers=self.critic_layers,
actor_layers=self.actor_layers,
actor_activation=self.actor_activation,
scope='target_network')
# Optimizer
self.optimizer = Optimizer(config=self.config,
ac_network=self.ac_network,
target_network=self.target_network,
replay_memory=self.replay_memory)
# Ops for updating target network
self.clone_op = self.target_network.get_clone_op(self.ac_network, tau=self.tau)
# For tensorboard
self.t_score = tf.placeholder(dtype=tf.float32, shape=[], name='new_score')
tf.summary.scalar("score", self.t_score, collections=['dpg'])
self.summary_op = tf.summary.merge_all('dpg')
def choose_action(self, sess, state, epsilon=0.1):
x = numpy.asarray(numpy.expand_dims(state, axis=0), dtype=numpy.float32)
action = self.ac_network.get_action(sess, x)[0]
return action + epsilon * numpy.random.randn(len(action))
def play(self, action):
r, new_state, termination = self.task.play_action(action)
return r, new_state, termination
def update_target_network(self, sess):
sess.run(self.clone_op)
choose_action函数根据演员-评论家网络的当前估计和观察到的状态选择一个动作。
请注意,受epsilon控制的高斯噪声被加入以进行探索。
play函数将动作提交给模拟器,并返回模拟器的反馈。update_target_network函数根据当前的演员-评论家网络更新目标网络。
要开始训练过程,可以调用以下函数:
def train(self, sess, saver=None):
num_of_trials = -1
for episode in range(self.n_episode):
frame = self.task.reset()
for _ in range(self.history_len+1):
self.replay_memory.add(frame, 0, 0, 0)
for _ in range(self.config['T']):
num_of_trials += 1
epsilon = self.epsilon_min + \
max(self.epsilon_decay - num_of_trials, 0) / \
self.epsilon_decay * (1 - self.epsilon_min)
if num_of_trials % self.update_interval == 0:
self.optimizer.train_one_step(sess, num_of_trials, self.batch_size)
state = self.replay_memory.phi(frame)
action = self.choose_action(sess, state, epsilon)
r, new_frame, termination = self.play(action)
self.replay_memory.add(frame, action, r, termination)
frame = new_frame
if num_of_trials % self.time_between_two_copies == 0:
self.update_target_network(sess)
self.save(sess, saver)
if self.callback:
self.callback()
if termination:
score = self.task.get_total_reward()
summary_str = sess.run(self.summary_op, feed_dict={self.t_score: score})
self.summary_writer.add_summary(summary_str, num_of_trials)
self.summary_writer.flush()
break
在每个回合中,它调用replay_memory.phi来获取当前状态,并调用choose_action函数根据当前状态选择一个动作。这个动作通过调用play函数提交到模拟器中,模拟器返回相应的奖励、下一个状态和终止信号。然后,(当前状态, 动作, 奖励, 终止)转换被存储到回放内存中。每当执行update_interval步(默认update_interval = 1)时,演员-评论家网络会使用从回放内存中随机采样的一批转换进行训练。每经过time_between_two_copies步,目标网络会更新,Q 网络的权重将被保存到硬盘中。
训练步骤后,可以调用以下函数来评估我们训练的智能体的性能:
def evaluate(self, sess):
for episode in range(self.n_episode):
frame = self.task.reset()
for _ in range(self.history_len+1):
self.replay_memory.add(frame, 0, 0, 0)
for _ in range(self.config['T']):
print("episode {}, total reward {}".format(episode,
self.task.get_total_reward()))
state = self.replay_memory.phi(frame)
action = self.choose_action(sess, state, self.epsilon_min)
r, new_frame, termination = self.play(action)
self.replay_memory.add(frame, action, r, termination)
frame = new_frame
if self.callback:
self.callback()
if termination:
break
总结
在本章中,我们扩展了在第八章中获得的有关强化学习的知识,学习了 DDPG、HER,以及如何将这些方法结合起来创建一个能够独立控制机器人手臂的强化学习算法。
我们用来解决游戏挑战的深度 Q 网络在离散空间中工作;当构建适用于更流畅运动任务(如机器人或自动驾驶汽车)的算法时,我们需要一种能够处理连续动作空间的算法。为此,使用策略梯度方法,它直接从一组动作中学习一个策略。我们可以通过使用经验回放缓冲区来改进这一学习过程,它存储了过去的正向经验,以便在训练过程中进行采样……
参考文献
Python 强化学习项目,Sean Saito,Yang Wenzhuo,和 Rajalingappaa Shanmugamani,www.packtpub.com/big-data-and-business-intelligence/python-reinforcement-learning-projects。
第十三章:部署与维护 AI 应用
在本书中,我们已经了解了如何创建人工智能(AI)应用以执行各种任务。尽管编写这些应用本身是一项不小的成就,但它通常只是将模型转化为可用生产系统所需工作的一个小部分。对于许多从业者来说,深度学习模型的工作流通常在验证阶段结束。你已经创建了一个表现非常好的网络;我们完成了吗?
数据科学家和机器学习工程师从发现阶段到部署阶段处理应用的情况变得越来越普遍。根据谷歌的说法,构建应用程序所需的时间中,超过 60-70% 的时间用于...
技术要求
本章虽然包含了一些通常是 DevOps 工程师工作的一部分的内容,但我们会根据需要了解的程度涉及这些工具和话题,并参考其他资源和工具,以帮助您更深入地学习相关话题。本章将使用以下内容:
-
TensorFlow
-
PyTorch
-
Docker,一个用于部署我们模型的容器化服务
-
亚马逊 Web 服务或谷歌云平台作为云提供商
-
Kubernetes 入门知识
介绍
AI 应用的部署与维护不仅仅是一个单一的动作;它是一个过程。在这一部分,我们将通过创建深度学习部署架构来在云中创建可持续的应用。这些架构将帮助我们创建端到端的系统:深度学习系统。
在许多机器学习/AI 应用中,典型的项目流程和工作流可能类似于以下内容:

训练过程是严格离线的,序列化的模型会推送到云端,并通过 API 与用户交互。这些过程通常会给我们带来几个不同的...
部署您的应用
那么,部署一个模型意味着什么呢?部署是一个涵盖广泛的术语,指的是将一个经过测试和验证的模型从本地计算机中拿出来,并在一个可持续的环境中进行设置,使其可以访问。部署有多种方式可以处理;在本章中,我们将重点介绍您应该了解的知识和最佳实践,以帮助您将模型投入生产。
您选择的部署架构取决于几个因素:
-
您的模型是否在一个环境中训练,并在另一个环境中进行生产化?
-
您期望您的模型被调用多少次以进行预测?
-
您的数据是随时间变化的,还是静态的?您是否需要处理大量的数据流入?
每个问题可以通过将我们的模型选择选项分为两个类别来回答。我们可以根据模型的服务位置以及训练方式来划分模型。下图展示了这些选项的矩阵,以及每种方法的成本和收益:

在一个与部署环境分离的特定数据上训练的模型,称为 离线模型,而在其部署环境中主动从新数据中学习的模型,称为 在线模型。
离线模型最简单的服务形式称为 批量服务。如果你是数据科学家或来自学术界,可能非常熟悉这种模型。批量服务就是将静态数据集输入到模型中,然后获取预测结果。通常,你可能会在本地机器上做这个,可能通过 Jupyter Notebook 或仅仅通过从终端或命令提示符运行脚本。在大多数情况下,我们希望我们的模型能够为更多用户所访问,因此我们将其封装成一个 Web 服务。
在线模型更难以管理,因为处理数据流和潜在的坏输入数据时可能会出现复杂问题。微软著名的 Tay Twitter 机器人就是一个完全在线学习模型的例子,它接受推文作为输入,迅速变得种族主义和粗俗。管理这些模型变得复杂,因为训练过程是开放的,必须采取许多防护措施来确保模型不会偏离预期输出太远。
自动化机器学习模型则变得越来越流行。它们有控制的输入,但会主动重新训练以考虑新的数据。想想 Netflix 的推荐系统——它通过根据你浏览和观看活动生成的数据进行训练,积极响应你的行为。
现在我们已经了解了我们的生态系统,让我们开始学习如何使用 TensorFlow 设置一个常见的 Web 服务部署架构。如果你不感兴趣手动部署过程,只想使用部署服务,可以跳过下一部分。
使用 TensorFlow Serving 部署模型
一般来说,在部署模型时,我们希望将模型的内部机制通过 HTTP 接口与公众隔离。对于传统的机器学习模型,我们会将序列化的模型包装在像 Flask 这样的部署框架中,创建一个 API,然后从那里提供我们的模型服务。这可能会导致依赖关系、版本控制和性能等一系列问题,因此,我们将使用由 TensorFlow 作者提供的工具,叫做 TensorFlow Serving。这个工具启动一个小型服务器,运行 TensorFlow 模型并提供访问接口。
TensorFlow Serving 实现了一种特定类型的远程过程调用,称为 GPRC。在计算机科学中,远程过程...
使用 Docker
由于我们将把模型部署到云端,我们需要某种机制来运行模型。虽然我们可以在 AWS 上启动一个虚拟机,但这对于我们的需求来说有点过头,而且有许多更简单(且更便宜)的方法可以帮助我们。
相反,我们将使用一种称为容器的工具。容器是一种轻量级的虚拟化技术,包含了运行应用程序所需的所有必要运行时包和方法。最流行的容器服务被称为Docker。
虽然我们不会在这里讲解 Docker 的安装过程,但你可以按照官方安装指南来安装 Docker:
-
创建一个 Docker 镜像
-
从 Docker 镜像创建一个容器
-
在容器中构建 TensorFlow Serving
Docker 镜像的配置定义在一个叫做 Docker 文件 的文件中。TensorFlow Serving 给我们提供了这些文件,一个用于利用 CPU,另一个用于利用 GPU。
谷歌的 TensorFlow 团队维护了一个适用于 TensorFlow Serving 的 Docker 镜像,可以直接使用:
- 一旦你安装了 Docker,可以通过终端或命令提示符中的
docker pull命令轻松获取:
docker pull tensorflow/serving
你应该会看到一系列类似以下的消息:

- 一旦你下载了 Docker 镜像,就可以继续在该镜像上创建一个容器。我们可以通过运行构建命令轻松完成:
## Builds a docker container from the image
docker build --pull -t $USER/tensorflow-serving-devel -f tensorflow_serving/tools/docker/Dockerfile.devel .
构建 Docker 容器可能需要一段时间——别担心,这是正常的。
- 一旦容器构建完成,继续运行容器:
## Run the container; we'll name it nn_container
docker run --name=nn_container -it $USER/tensorflow-serving-devel
- 现在你应该可以访问 Docker 容器的 shell。接下来,我们将在容器中下载实际的 TensorFlow Serving 文件:
git clone -b r1.6 https://github.com/tensorflow/serving
cd serving
- 最后,我们需要在容器中安装 TensorFlow 模型服务器。模型服务器将实际为我们的模型提供服务:
bazel build -c opt tensorflow_serving/model_servers:tensorflow_model_server
一旦我们有了一个容器,环境就配置好了。接下来要做的是将我们保存的模型放入 Docker 容器中。
当你退出 Docker 容器的 shell 时,容器会关闭。如果你想再次启动容器,可以在容器的目录中运行 docker start -i nn_container 命令。
让我们创建一个目录来放置我们的模型。还在容器的命令行中时,使用以下命令创建一个新目录:
mkdir model_serving
接下来,我们将把保存的模型上传到这个目录。从你之前保存分类器的地方,运行以下命令。你需要将 output_directory 替换为 TensorFlow SavedModel 二进制文件所在的子文件夹。
docker cp ./output_directory nn_container:/model_serving
让我们尝试为模型提供服务。在 Docker 容器中运行以下命令:
tensorflow_model_server --port=9000 --model_name=nn --model_base_path=/model_serving/binaries &> nn_log &
您的模型现在应该在本地运行,使用 TensorFlow 服务。然而,我们还没有完成,因为在部署到云端后,我们需要创建一种模型可以与请求交互的方式。为此,我们需要创建一个称为客户端的东西,它是一个小程序,作为模型与外部世界交流的门卫。
构建 TensorFlow 客户端
最后,我们需要构建一个客户端来与我们的 TensorFlow 模型进行交互。构建一个自定义客户端来与您的模型进行交互,这有点超出了本书的范围,因此我们在对应的 GitHub 存储库中提供了这个功能:
- 请继续使用以下代码下载它:
pip install git+ssh://git@github.com/PacktPublishing/hands-On-Artificial-Intelligence-for-Beginners/tf-client.git
- 让我们尝试使用客户端向模型发送请求:
from predict_client.prod_client import ProdClientclient = ProdClient('localhost:9000', 'simple', 1)req_data = [{'in_tensor_name': 'a', 'in_tensor_dtype': 'DT_INT32', 'data': 2}]client.predict(req_data)
这里发生了什么?
- 第一行导入了客户端本身,...
使用 Google Cloud Platform 进行训练和部署
对于更简单的部署过程,我们可以使用Google Cloud Platform(GCP)将 TensorFlow SavedModel 部署到生产环境。在本节中,我们将介绍如何使用 GCP 来训练和部署模型的基础知识。
GCP 目前提供了最简单和易于使用的接口之一,用于训练和部署模型。如果您有兴趣尽快将模型投入生产,GCP 通常是您的选择。具体来说,我们将使用 Cloud ML 服务,这是对我们刚刚学习过的 AWS SageMaker 的一个补充。Cloud ML 目前能够直接运行 TensorFlow、Scikit-learn 和 XGBoost,虽然您可以手动添加自己的包。与 SageMaker 相比,由于该库与 Google 集成,Cloud ML 能够更快地接收 TensorFlow 的自动更新,因此建议您用于基于 TensorFlow 的应用程序。
在开始之前,让我们设置一个新的 Google Cloud 存储桶,作为我们应用程序的基础。请登录您的 GCP 账号,找到 Cloud Storage,然后点击创建存储桶。您应该看到一个类似以下内容的屏幕:

这个存储桶将作为我们数据、模型、训练检查点和模型二进制文件的临时存储区。继续将我们一直在使用的creditcard.csv文件上传到这个存储桶中,我们很快就会用到它!
接下来,让我们准备好在 GCP 上训练我们的模型,我们需要添加几行代码,以便它可以从命令行运行。在之前包含模型代码的脚本中,我们将其添加到底部:
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument(
'--job-dir',
help='GCS location to write checkpoints and export models'
)
parser.add_argument(
'--train-file',
help='GCS or local paths to training data'
)
args, unknown = parser.parse_known_args()
c = SimpleClassifier()
c.train_model(**args.__dict__)
此脚本将使我们能够通过命令行传递模型的唯一参数job_dir。要查看完整的 GCP 就绪代码,请查看本章 GitHub 中的simple_classifier.py脚本。一旦您设置好 Cloud Storage 和脚本,我们就可以开始训练和部署!
在 GCP 上训练
Google 通过允许我们训练模型、存储模型并以最少的代码进行部署,简化了整个深度学习训练/部署过程。在我们开始在云端训练之前,先在本地训练我们的模型,确保一切按预期工作。首先,我们需要设置一些环境变量。最重要的是,我们需要将文件放入特定的结构中,以便与 Cloud ML 一起训练。在本章的 GitHub 文件夹中查找训练文件夹,你将找到正确的文件结构。你看到的 __init__.py 文件将告诉 GCP,我们的文件是一个可执行的 Python 程序。
首先,我们将定义一个作业目录,这应该是你的 simple_classifier.py ... 文件所在的文件夹。
在 GCP 上部署在线学习
当我们将 TensorFlow SavedModel 部署到 GCP 平台时,我们需要将整个 SavedModel 目录上传到 GCP 上的存储位置,或者像之前一样在云端进行训练。无论你选择哪种方法,你的 TensorFlow 模型二进制文件都应存储在 Google Cloud Storage 中。
你的模型二进制文件将是训练后创建的最终文件,并将具有 .pb 扩展名。
为了开始我们的部署过程,我们首先需要创建一个部署的模型对象。你可以通过以下命令创建它:
gcloud ml-engine models create "deployed_classifier"
接下来,我们将创建一个环境变量,让 GCP 知道我们的保存模型二进制文件的位置:
DEPLOYMENT_SOURCE="gs://classifier_bucket123/classifier_model/binaries"
现在,我们只需要运行如下命令,我们的分类器就会被部署!请记住,部署需要几分钟的时间来配置:
gcloud ml-engine versions create "version1"\
--model "deployed_classifier" --origin $DEPLOYMENT_SOURCE --runtime-version 1.9
正如你所见,我们通过几行代码完成了前面一整节内容的工作;像 AWS SageMaker 和 Google Cloud ML 这样的平台作为服务在建模过程中节省了大量时间。
现在,让我们尝试从我们的模型中获取预测。在请求预测之前,我们需要设置一些变量。输入数据文件将是一个包含一行数据的 json 文件。为了简便起见,我们已经将数据集中的一行作为 test.json 包含在 GitHub 文件夹中:
MODEL_NAME="deployed_classifier"
INPUT_DATA_FILE="test.json"
VERSION_NAME="version1"
最后,运行预测请求:
gcloud ml-engine predict --model $MODEL_NAME \
--version $VERSION_NAME \
--json-instances $INPUT_DATA_FILE
恭喜!你的模型现在已托管在云端。你应该会看到一个返回的 json 对象,其中包含两种潜在分类的概率:欺诈或非欺诈。虽然之前的 gcloud 命令非常适合发出单个请求,但我们通常希望将请求作为 Web 应用的一部分返回。在下一部分,我们将演示如何通过一个简单的 Flask 应用来做到这一点。
使用 API 进行预测
要开始,您需要创建一个 Google Cloud 服务账户密钥,以便您的应用程序能够访问该模型。请访问链接 console.cloud.google.com/apis/credentials/serviceaccountkey,并创建一个新账户。记得将密钥下载为 JSON 文件。要连接到 GCP,您需要将账户凭证设置为 GCP 可以访问的环境变量。请将其位置设置为环境变量:
GOOGLE_APPLICATION_CREDENTIALS = your_key.json
让我们创建一个名为 predict.py 的脚本(你可以在 chapter 文件夹中找到完整的脚本)。首先,我们将导入一些 Google 库,允许我们的程序连接到 API。GoogleCredentials 将帮助我们发现 ...
扩展您的应用程序
可扩展性是指系统处理越来越多工作负载的能力。当我们创建一个系统或程序时,我们希望确保它是可扩展的,以便在接收到过多请求时不会崩溃。扩展可以通过两种方式进行:
-
扩展上行:提升现有工作节点的硬件配置,例如从 CPU 升级到 GPU。
-
扩展外部:将工作负载分配给多个工作节点。Spark 是一个常用的框架来实现这一点。
扩展上行可以像将模型迁移到更大云实例一样简单。在本节中,我们将专注于如何分布 TensorFlow,以便扩展我们的应用程序。
使用分布式 TensorFlow 扩展计算能力
如果我们希望扩展计算资源怎么办?我们可以将 TensorFlow 进程分布到多个工作节点上,从而加速和简化训练。实际上,有三种框架可以用来分布 TensorFlow:原生分布式 TensorFlow、TensorFlowOnSpark 和 Horovod。在本节中,我们将专注于原生分布式 TensorFlow。
在分布式处理领域,我们可以采用两种方法来分配模型的计算负载,即模型并行和数据并行:
-
模型并行:将模型的训练层分布到多个设备上。
-
数据并行:将整个模型分布到多个设备上 ...
测试和维护您的应用程序
无论是在线学习还是离线学习,我们都应该建立系统和安全检查机制,及时告知我们模型的预测结果,甚至是关键部署架构是否出现问题。这里的测试指的是对输入、输出和错误进行硬编码检查,以确保我们的模型按照预期执行。在标准软件测试中,对于每一个输入,都应该有一个定义好的输出。但在机器学习领域,由于模型的输出会受到多种因素的影响,输出结果会有变动——这对于标准测试程序来说并不理想,是吗?在本节中,我们将讨论机器学习代码的测试过程,并探讨最佳实践。
一旦部署,AI 应用程序还需要维护。像 Jenkins 这样的 DevOps 工具可以帮助确保在将新版本的模型推送到生产环境之前通过测试。虽然我们当然不期望你作为机器学习工程师去创建开发管道,但我们将在本节中回顾其基本原理,以便你熟悉最佳实践。
测试深度学习算法
AI 社区在采用适当的测试程序方面严重滞后。一些最先进的 AI 公司依赖人工检查,而不是对他们的算法进行自动化测试。你已经在本书中进行了一种测试形式;通过交叉验证我们的模型,使用训练集、测试集和验证集来确保一切按预期工作。在本节中,我们将重点讨论 单元测试,它旨在以尽可能小的计算单元测试软件。换句话说,我们要测试算法的各个小部分,以确保更大的平台能够顺利运行。
测试我们的算法有助于我们追踪 非-破坏性 bug,这些 bug 已经变得无处不在……
总结
构建 AI 应用程序不仅仅是模型构建的基础内容 —— 它还涉及将模型部署到云端的生产环境中,在那里它们得以持久化。在这一章中,我们讨论了如何将一个经过验证的 TensorFlow 模型部署到云端的生产环境中。我们还讨论了如何扩展这些模型,以及如何测试你的应用程序的弹性。
在将 TensorFlow 应用程序从开发环境迁移到生产环境时,第一步是创建一个可以存储在云中的 TensorFlow SavedModel。从这里开始,有几个服务可以帮助简化你的部署过程,包括 AWS Lambda 和 Google Cloud ML Engine。
应用程序可以通过纵向或横向扩展来获得更多的计算能力和更快的处理速度。通过纵向扩展,我们为算法提供更大的计算资源;通过横向扩展,我们一次性为应用程序提供更多的资源。记住,已部署到生产环境的模型也应进行测试,基本的测试,如单元测试,可以帮助防止你的整个应用程序崩溃!
我们现在已经到达本书的结尾。随着你逐章学习,我希望你已经意识到深度学习为人工智能领域创造的激动人心的可能性。尽管毫无疑问,许多这些主题的研究将继续进行,并且新的方法将会被创造和使用,但你在本书中学到的基本概念将始终保持不变,为未来的突破性工作奠定基础。谁知道呢,也许进行这些突破性工作的人就是你!
参考文献
-
提到 Quora
-
TensorFlow 客户端引用


代表了我们的 TD 误差。
共同代表了演员和评论员模型的目标!
代表演员网络的目标;它表示目标
是状态s的函数,给定一个特定的策略
。
。
浙公网安备 33010602011771号